diff --git a/examples/virt/herorun/README.md b/examples/virt/herorun/archive/README.md similarity index 100% rename from examples/virt/herorun/README.md rename to examples/virt/herorun/archive/README.md diff --git a/examples/virt/herorun/cleanup.vsh b/examples/virt/herorun/archive/cleanup.vsh similarity index 100% rename from examples/virt/herorun/cleanup.vsh rename to examples/virt/herorun/archive/cleanup.vsh diff --git a/examples/virt/herorun/execute.vsh b/examples/virt/herorun/archive/execute.vsh similarity index 100% rename from examples/virt/herorun/execute.vsh rename to examples/virt/herorun/archive/execute.vsh diff --git a/examples/virt/herorun/setup.vsh b/examples/virt/herorun/archive/setup.vsh similarity index 100% rename from examples/virt/herorun/setup.vsh rename to examples/virt/herorun/archive/setup.vsh diff --git a/examples/virt/herorun/setup_python_alpine.vsh b/examples/virt/herorun/archive/setup_python_alpine.vsh similarity index 100% rename from examples/virt/herorun/setup_python_alpine.vsh rename to examples/virt/herorun/archive/setup_python_alpine.vsh diff --git a/examples/virt/herorun/setup_with_script.vsh b/examples/virt/herorun/archive/setup_with_script.vsh similarity index 100% rename from examples/virt/herorun/setup_with_script.vsh rename to examples/virt/herorun/archive/setup_with_script.vsh diff --git a/examples/virt/herorun/test_hello_world.vsh b/examples/virt/herorun/archive/test_hello_world.vsh similarity index 100% rename from examples/virt/herorun/test_hello_world.vsh rename to examples/virt/herorun/archive/test_hello_world.vsh diff --git a/examples/virt/herorun/basic_example.vsh b/examples/virt/herorun/basic_example.vsh new file mode 100644 index 00000000..0acd62f2 --- /dev/null +++ b/examples/virt/herorun/basic_example.vsh @@ -0,0 +1,35 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.virt.herorun +import freeflowuniverse.herolib.ui.console + +// Create container factory +mut factory := herorun.new(reset: false)! + +// Create a new Alpine container +mut container := factory.new(name: 'test-alpine', image: .alpine_3_20)! + +// Start the container +container.start()! + +// Execute commands in the container +result := container.exec(cmd: 'ls -la /', stdout: true)! +console.print_debug('Container ls result: ${result}') + +// Test file operations +container.exec(cmd: 'echo "Hello from container" > /tmp/test.txt', stdout: false)! +content := container.exec(cmd: 'cat /tmp/test.txt', stdout: false)! +console.print_debug('File content: ${content}') + +// Get container status and resource usage +status := container.status()! +cpu := container.cpu_usage()! +mem := container.mem_usage()! + +console.print_debug('Container status: ${status}') +console.print_debug('CPU usage: ${cpu}%') +console.print_debug('Memory usage: ${mem} MB') + +// Clean up +container.stop()! +container.delete()! \ No newline at end of file diff --git a/examples/virt/herorun/builder_integration.vsh b/examples/virt/herorun/builder_integration.vsh new file mode 100644 index 00000000..f018cafb --- /dev/null +++ b/examples/virt/herorun/builder_integration.vsh @@ -0,0 +1,36 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.virt.herorun +import freeflowuniverse.herolib.builder +import freeflowuniverse.herolib.ui.console + +// Create container +mut factory := herorun.new()! +mut container := factory.new(name: 'builder-test', image: .ubuntu_24_04)! +container.start()! + +// Get builder node for the container +mut node := container.node()! + +// Use builder methods to interact with container +node.file_write('/tmp/script.sh', ' +#!/bin/bash +echo "Running from builder node" +whoami +pwd +ls -la / +')! + +result := node.exec(cmd: 'chmod +x /tmp/script.sh && /tmp/script.sh', stdout: true)! +console.print_debug('Builder execution result: ${result}') + +// Test file operations through builder +exists := node.file_exists('/tmp/script.sh') +console.print_debug('Script exists: ${exists}') + +content := node.file_read('/tmp/script.sh')! +console.print_debug('Script content: ${content}') + +// Clean up +container.stop()! +container.delete()! \ No newline at end of file diff --git a/examples/virt/herorun/herorun2.vsh b/examples/virt/herorun/herorun2.vsh new file mode 100644 index 00000000..17dc32e4 --- /dev/null +++ b/examples/virt/herorun/herorun2.vsh @@ -0,0 +1,4 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.virt.herorun + diff --git a/lib/builder/executor_crun.v b/lib/builder/executor_crun.v index 21124ce6..7e3c1c68 100644 --- a/lib/builder/executor_crun.v +++ b/lib/builder/executor_crun.v @@ -12,12 +12,21 @@ import freeflowuniverse.herolib.core.texttools @[heap] pub struct ExecutorCrun { pub mut: - container_id string //to map to virt/herorun/container - retry int = 1 // nr of times something will be retried before failing, need to check also what error is, only things which should be retried need to be done - debug bool = true + container_id string // container ID for crun + retry int = 1 + debug bool = true } fn (mut executor ExecutorCrun) init() ! { + // Verify container exists and is running + result := osal.exec(cmd: 'crun state ${executor.container_id}', stdout: false) or { + return error('Container ${executor.container_id} not found or not accessible') + } + + // Parse state to ensure container is running + if !result.contains('"status":"running"') { + return error('Container ${executor.container_id} is not running') + } } pub fn (mut executor ExecutorCrun) debug_on() { @@ -31,142 +40,167 @@ pub fn (mut executor ExecutorCrun) debug_off() { pub fn (mut executor ExecutorCrun) exec(args_ ExecArgs) !string { mut args := args_ if executor.debug { - console.print_debug('execute ${executor.ipaddr.addr}: ${args.cmd}') + console.print_debug('execute in container ${executor.container_id}: ${args.cmd}') } - //TODO: implement - res := osal.exec(cmd: args.cmd, stdout: args.stdout, debug: executor.debug)! + + mut cmd := 'crun exec ${executor.container_id} ${args.cmd}' + if args.cmd.contains('\n') { + // For multiline commands, write to temp file first + temp_script := '/tmp/crun_script_${rand.uuid_v4()}.sh' + script_content := texttools.dedent(args.cmd) + os.write_file(temp_script, script_content)! + + // Copy script into container and execute + executor.file_write('/tmp/exec_script.sh', script_content)! + cmd = 'crun exec ${executor.container_id} bash /tmp/exec_script.sh' + } + + res := osal.exec(cmd: cmd, stdout: args.stdout, debug: executor.debug)! return res.output } pub fn (mut executor ExecutorCrun) exec_interactive(args_ ExecArgs) ! { mut args := args_ - mut port := '' + if args.cmd.contains('\n') { args.cmd = texttools.dedent(args.cmd) - // need to upload the file first - executor.file_write('/tmp/toexec.sh', args.cmd)! - args.cmd = 'bash /tmp/toexec.sh' + executor.file_write('/tmp/interactive_script.sh', args.cmd)! + args.cmd = 'bash /tmp/interactive_script.sh' } - //TODO: implement - - console.print_debug(args.cmd) - osal.execute_interactive(args.cmd)! + + cmd := 'crun exec -t ${executor.container_id} ${args.cmd}' + console.print_debug(cmd) + osal.execute_interactive(cmd)! } pub fn (mut executor ExecutorCrun) file_write(path string, text string) ! { if executor.debug { - console.print_debug('${executor.ipaddr.addr} file write: ${path}') + console.print_debug('Container ${executor.container_id} file write: ${path}') } - //TODO implement use pathlib and write functionality + + // Write to temp file first, then copy into container + temp_file := '/tmp/crun_file_${rand.uuid_v4()}' + os.write_file(temp_file, text)! + defer { os.rm(temp_file) or {} } + + // Use crun exec to copy file content + cmd := 'cat ${temp_file} | crun exec -i ${executor.container_id} tee ${path} > /dev/null' + osal.exec(cmd: cmd, stdout: false)! } pub fn (mut executor ExecutorCrun) file_read(path string) !string { if executor.debug { - console.print_debug('${executor.ipaddr.addr} file read: ${path}') + console.print_debug('Container ${executor.container_id} file read: ${path}') } - //TODO implement use pathlib and read functionality + + return executor.exec(cmd: 'cat ${path}', stdout: false) } pub fn (mut executor ExecutorCrun) file_exists(path string) bool { if executor.debug { - console.print_debug('${executor.ipaddr.addr} file exists: ${path}') + console.print_debug('Container ${executor.container_id} file exists: ${path}') } + output := executor.exec(cmd: 'test -f ${path} && echo found || echo not found', stdout: false) or { return false } - if output == 'found' { - return true - } - //TODO: can prob be done better, because we can go in the path of the container and check there - return false + return output.trim_space() == 'found' } -// carefull removes everything pub fn (mut executor ExecutorCrun) delete(path string) ! { if executor.debug { - console.print_debug('${executor.ipaddr.addr} file delete: ${path}') + console.print_debug('Container ${executor.container_id} delete: ${path}') } - executor.exec(cmd: 'rm -rf ${path}', stdout: false) or { panic(err) } - //TODO: can prob be done better, because we can go in the path of the container and delete there + executor.exec(cmd: 'rm -rf ${path}', stdout: false)! } -// upload from local FS to executor FS -pub fn (mut executor ExecutorCrun) download(args SyncArgs) ! { - //TODO implement - rsync.rsync(rsargs)! -} - -// download from executor FS to local FS pub fn (mut executor ExecutorCrun) upload(args SyncArgs) ! { - - //TODO implement - mut rsargs := rsync.RsyncArgs{ - source: args.source - dest: args.dest - delete: args.delete - ipaddr_dst: addr - ignore: args.ignore - ignore_default: args.ignore_default - stdout: args.stdout - fast_rsync: args.fast_rsync + // For container uploads, we need to copy files from host to container + // Use crun exec with tar for efficient transfer + + mut src_path := pathlib.get(args.source) + if !src_path.exists() { + return error('Source path ${args.source} does not exist') + } + + if src_path.is_dir() { + // For directories, use tar to transfer + temp_tar := '/tmp/crun_upload_${rand.uuid_v4()}.tar' + osal.exec(cmd: 'tar -cf ${temp_tar} -C ${src_path.path_dir()} ${src_path.name()}', stdout: false)! + defer { os.rm(temp_tar) or {} } + + // Extract in container + cmd := 'cat ${temp_tar} | crun exec -i ${executor.container_id} tar -xf - -C ${args.dest}' + osal.exec(cmd: cmd, stdout: args.stdout)! + } else { + // For single files + executor.file_write(args.dest, src_path.read()!)! } - rsync.rsync(rsargs)! } -// get environment variables from the executor -pub fn (mut executor ExecutorCrun) environ_get() !map[string]string { - env := executor.exec(cmd: 'env', stdout: false) or { return error('can not get environment') } - // if executor.debug { - // console.print_header(' ${executor.ipaddr.addr} env get') - // } +pub fn (mut executor ExecutorCrun) download(args SyncArgs) ! { + // Download from container to host + if executor.dir_exists(args.source) { + // For directories + temp_tar := '/tmp/crun_download_${rand.uuid_v4()}.tar' + cmd := 'crun exec ${executor.container_id} tar -cf - -C ${args.source} . > ${temp_tar}' + osal.exec(cmd: cmd, stdout: false)! + defer { os.rm(temp_tar) or {} } + + // Extract on host + osal.exec(cmd: 'mkdir -p ${args.dest} && tar -xf ${temp_tar} -C ${args.dest}', stdout: args.stdout)! + } else { + // For single files + content := executor.file_read(args.source)! + os.write_file(args.dest, content)! + } +} +pub fn (mut executor ExecutorCrun) environ_get() !map[string]string { + env := executor.exec(cmd: 'env', stdout: false) or { + return error('Cannot get environment from container ${executor.container_id}') + } + mut res := map[string]string{} - if env.contains('\n') { - for line in env.split('\n') { - if line.contains('=') { - splitted := line.split('=') - key := splitted[0].trim(' ') - val := splitted[1].trim(' ') - res[key] = val - } + for line in env.split('\n') { + if line.contains('=') { + parts := line.split_once('=') or { continue } + key := parts[0].trim(' ') + val := parts[1].trim(' ') + res[key] = val } } return res } -/* -Executor info or meta data -accessing type Executor won't allow to access the -fields of the struct, so this is workaround -*/ pub fn (mut executor ExecutorCrun) info() map[string]string { - //TODO implement more info return { - 'category': 'crun' + 'category': 'crun' + 'container_id': executor.container_id + 'runtime': 'crun' } } -// ssh shell on the node default ssh port, or any custom port that may be -// forwarding ssh traffic to certain container - pub fn (mut executor ExecutorCrun) shell(cmd string) ! { - //TODO: implement if cmd.len > 0 { - panic('TODO IMPLEMENT SHELL EXEC OVER SSH') + osal.execute_interactive('crun exec -t ${executor.container_id} ${cmd}')! + } else { + osal.execute_interactive('crun exec -t ${executor.container_id} /bin/sh')! } - os.execvp('ssh', ['-o StrictHostKeyChecking=no', '${executor.user}@${executor.ipaddr.addr}', - '-p ${executor.ipaddr.port}'])! } pub fn (mut executor ExecutorCrun) list(path string) ![]string { if !executor.dir_exists(path) { - panic('Dir Not found') + return error('Directory ${path} does not exist in container') } - mut res := []string{} - //TODO: implement + output := executor.exec(cmd: 'ls ${path}', stdout: false)! + mut res := []string{} for line in output.split('\n') { - res << line + line_trimmed := line.trim_space() + if line_trimmed != '' { + res << line_trimmed + } } return res } @@ -175,9 +209,5 @@ pub fn (mut executor ExecutorCrun) dir_exists(path string) bool { output := executor.exec(cmd: 'test -d ${path} && echo found || echo not found', stdout: false) or { return false } - //TODO: implement - if output.trim_space() == 'found' { - return true - } - return false -} + return output.trim_space() == 'found' +} \ No newline at end of file diff --git a/lib/virt/herorun/config_template.json b/lib/virt/herorun/config_template.json new file mode 100644 index 00000000..e1ef0f56 --- /dev/null +++ b/lib/virt/herorun/config_template.json @@ -0,0 +1,119 @@ +{ + "ociVersion": "1.0.2", + "process": { + "terminal": true, + "user": { + "uid": 0, + "gid": 0 + }, + "args": [ + "/bin/sh" + ], + "env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm" + ], + "cwd": "/", + "capabilities": { + "bounding": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "effective": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "inheritable": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ], + "permitted": [ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_NET_BIND_SERVICE" + ] + }, + "rlimits": [ + { + "type": "RLIMIT_NOFILE", + "hard": 1024, + "soft": 1024 + } + ], + "noNewPrivileges": true + }, + "root": { + "path": "${rootfs_path}", + "readonly": false + }, + "mounts": [ + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + { + "destination": "/dev", + "type": "tmpfs", + "source": "tmpfs", + "options": [ + "nosuid", + "strictatime", + "mode=755", + "size=65536k" + ] + }, + { + "destination": "/sys", + "type": "sysfs", + "source": "sysfs", + "options": [ + "nosuid", + "noexec", + "nodev", + "ro" + ] + } + ], + "linux": { + "namespaces": [ + { + "type": "pid" + }, + { + "type": "network" + }, + { + "type": "ipc" + }, + { + "type": "uts" + }, + { + "type": "mount" + } + ], + "maskedPaths": [ + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "readonlyPaths": [ + "/proc/asound", + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + } +} \ No newline at end of file diff --git a/lib/virt/herorun/container.v b/lib/virt/herorun/container.v index 08c84781..153e17b5 100644 --- a/lib/virt/herorun/container.v +++ b/lib/virt/herorun/container.v @@ -5,39 +5,80 @@ import freeflowuniverse.herolib.osal.tmux import freeflowuniverse.herolib.osal.core as osal import time import freeflowuniverse.herolib.builder +import json -// Container struct and related functionality pub struct Container { pub mut: name string - //TODO: add properties we need for crun usage - node ?builder.Node - tmux ?tmux.Pane + node ?&builder.Node + tmux_pane ?&tmux.Pane factory &ContainerFactory } - -pub fn (self Container) start() ! { - +pub fn (mut self Container) start() ! { + status := self.status()! + if status == .running { + console.print_debug('Container ${self.name} is already running') + return + } + + osal.exec(cmd: 'crun start ${self.name}', stdout: true)! + console.print_green('Container ${self.name} started') } - -pub fn (self Container) stop() ! { - +pub fn (mut self Container) stop() ! { + status := self.status()! + if status == .stopped { + console.print_debug('Container ${self.name} is already stopped') + return + } + + osal.exec(cmd: 'crun kill ${self.name} SIGTERM', stdout: false) or {} + time.sleep(2 * time.second) + + // Force kill if still running + if self.status()! == .running { + osal.exec(cmd: 'crun kill ${self.name} SIGKILL', stdout: false) or {} + } + console.print_green('Container ${self.name} stopped') } -//execute command inside the container -pub fn (self Container) exec(args osal.ExecArgs) ! { - //TODO: use same args as osal.exec but then run inside the builder.node, use self.node()! - self.node()!.exec(args)! +pub fn (mut self Container) delete() ! { + self.stop()! + osal.exec(cmd: 'crun delete ${self.name}', stdout: false) or {} + console.print_green('Container ${self.name} deleted') } -//TODO: add whatever else we need +// Execute command inside the container +pub fn (mut self Container) exec(args osal.ExecArgs) !string { + // Ensure container is running + if self.status()! != .running { + self.start()! + } + + // Use the builder node to execute inside container + mut node := self.node()! + return node.exec(cmd: args.cmd, stdout: args.stdout) +} - -//return as enum pub fn (self Container) status() !ContainerStatus { - //TODO + result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { + return .unknown + } + + // Parse JSON output from crun state + state := json.decode(map[string]json.Any, result) or { + return .unknown + } + + status_str := state['status'] or { json.Any('') }.str() + + return match status_str { + 'running' { .running } + 'stopped' { .stopped } + 'paused' { .paused } + else { .unknown } + } } pub enum ContainerStatus { @@ -47,37 +88,101 @@ pub enum ContainerStatus { unknown } -//in percentage??? is that per core??? +// Get CPU usage in percentage pub fn (self Container) cpu_usage() !f64 { - //TODO + // Use cgroup stats to get CPU usage + result := osal.exec(cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/cpu.stat', stdout: false) or { + return 0.0 + } + + // Parse cpu.stat file and calculate usage percentage + // This is a simplified implementation + for line in result.split_into_lines() { + if line.starts_with('usage_usec') { + usage := line.split(' ')[1].f64() + return usage / 1000000.0 // Convert to percentage + } + } + return 0.0 } -//in MByte +// Get memory usage in MB pub fn (self Container) mem_usage() !f64 { - //TODO + result := osal.exec(cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/memory.current', stdout: false) or { + return 0.0 + } + + bytes := result.trim_space().f64() + return bytes / (1024 * 1024) // Convert to MB } - -pub struct TmuxPaneargs { +pub struct TmuxPaneArgs { pub mut: window_name string - pane_nr int - pane_name string //optional - cmd string //optional, will execute this cmd - reset bool //if true will reset everything and restart a cmd - env map[string]string //optional, will set these env vars in the pane + pane_nr int + pane_name string // optional + cmd string // optional, will execute this cmd + reset bool // if true will reset everything and restart a cmd + env map[string]string // optional, will set these env vars in the pane } - -// -pub fn (self Container) tmux_pane() !tmux.Pane { - //TODO: check if tmux session exist, if not create if sessionname is given in factory - //TODO: check if window exist, if not create - //TODO: check if pane exist, if not create +pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane { + mut tmux_session := self.factory.tmux_session + if tmux_session == '' { + tmux_session = 'herorun' + } + + // Get or create tmux session + mut session := tmux.session_get(name: tmux_session) or { + tmux.session_new(name: tmux_session)! + } + + // Get or create window + mut window := session.window_get(name: args.window_name) or { + session.window_new(name: args.window_name)! + } + + // Get or create pane + mut pane := window.pane_get(nr: args.pane_nr) or { + window.pane_new()! + } + + if args.reset { + pane.clear()! + } + + // Set environment variables if provided + for key, value in args.env { + pane.send_keys('export ${key}="${value}"')! + } + + // Execute command if provided + if args.cmd != '' { + // First enter the container namespace + pane.send_keys('crun exec ${self.name} ${args.cmd}')! + } + + self.tmux_pane = &pane + return &pane } - -// -pub fn (self Container) node() !builder.Node { - //TODO: check if builder.Node is already there, if not initialize it and return +pub fn (mut self Container) node() !&builder.Node { + if node := self.node { + return node + } + + // Create a new ExecutorCrun for this container + mut executor := builder.ExecutorCrun{ + container_id: self.name + } + + mut b := builder.new()! + mut node := &builder.Node{ + name: 'container_${self.name}' + executor: executor + factory: &b + } + + self.node = node + return node } \ No newline at end of file diff --git a/lib/virt/herorun/container_create.v b/lib/virt/herorun/container_create.v new file mode 100644 index 00000000..837a1560 --- /dev/null +++ b/lib/virt/herorun/container_create.v @@ -0,0 +1,75 @@ +module herorun + +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.tmux +import freeflowuniverse.herolib.osal.core as osal +import time +import freeflowuniverse.herolib.builder +import json + + + + + +pub enum ContainerImage { + alpine_3_20 + ubuntu_24_04 + ubuntu_25_04 +} + + + +@[params] +pub struct ContainerNewArgs { +pub: + name string @[required] + image ContainerImage = .alpine_3_20 + reset bool +} + +pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { + if args.name in self.containers && !args.reset { + return self.containers[args.name] + } + + // Create container config + self.create_container_config(args)! + + // Create container using crun + osal.exec(cmd: 'crun create --bundle /containers/configs/${args.name} ${args.name}', stdout: true)! + + mut container := &Container{ + name: args.name + factory: &self + } + + self.containers[args.name] = container + return container +} + + +fn (self ContainerFactory) create_container_config(args ContainerNewArgs) ! { + // Determine rootfs path based on image + mut rootfs_path := '' + match args.image { + .alpine_3_20 { + rootfs_path = '/containers/images/alpine/rootfs' + } + .ubuntu_24_04 { + rootfs_path = '/containers/images/ubuntu/24.04/rootfs' + } + .ubuntu_25_04 { + rootfs_path = '/containers/images/ubuntu/25.04/rootfs' + } + } + + config_dir := '/containers/configs/${args.name}' + osal.exec(cmd: 'mkdir -p ${config_dir}', stdout: false)! + + // Generate OCI config.json + config_content := $tmpl('config_template.json') + config_path := '${config_dir}/config.json' + + mut p := pathlib.get_file(path: config_path, create: true)! + p.write(config_content)! +} \ No newline at end of file diff --git a/lib/virt/herorun/container_image.v b/lib/virt/herorun/container_image.v new file mode 100644 index 00000000..6898e38c --- /dev/null +++ b/lib/virt/herorun/container_image.v @@ -0,0 +1,52 @@ +module herorun + +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.tmux +import freeflowuniverse.herolib.osal.core as osal +import time +import freeflowuniverse.herolib.builder +import json + + + +@[heap] +pub struct ContainerImage { +pub: + image_name string @[required] //image is located in /containers/images//rootfs + docker_unc string //optional + +} + +pub struct ContainerImageArgs { +pub: + image_name string @[required] //image is located in /containers/images//rootfs + docker_unc string + reset bool +} + + +pub fn (mut self ContainerFactory) image_new(args ContainerImageArgs) !&ContainerImage { + //if docker unc is given, we need to download the image and extract it to /containers/images//rootfs, use podman for it + //if podman not installed give error + //attach image to self.factory.images .. +} + +pub fn (mut self ContainerFactory) images_list() ![]&ContainerImage { + //TODO: ... +} + + + +//TODO: export to .tgz file +pub fn (mut self ContainerImage) export(...) !{ + //export dir if exist to the define path, if not exist then error +} + + +pub fn (mut self ContainerImage) import(...) !{ + //import from .tgz file to /containers/images//rootfs, if already exist give error, unless if we specify reset +} + +pub fn (mut self ContainerImage) delete() !{ + //TODO: +} diff --git a/lib/virt/herorun/factory.v b/lib/virt/herorun/factory.v index 709cfe1f..f022fc08 100644 --- a/lib/virt/herorun/factory.v +++ b/lib/virt/herorun/factory.v @@ -2,97 +2,133 @@ module herorun import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.tmux +import freeflowuniverse.herolib.osal.core as osal import time import freeflowuniverse.herolib.builder +import freeflowuniverse.herolib.core.pathlib +import os -// Container struct and related functionality pub struct ContainerFactory { pub mut: - tmux_session string //this is the name for tmux session if we will use it + tmux_session string // tmux session name if used + containers map[string]&Container + images map[string]&Image } @[params] pub struct FactoryInitArgs { pub: reset bool -} +} pub fn new(args FactoryInitArgs) !ContainerFactory { - mut f:= ContainerFactory{} + mut f := ContainerFactory{} f.init(args)! return f } -fn (self ContainerFactory) init(args ContainerFactoryInitArgs) ! { - // Alpine (as before) - alpine_ver := '3.20.3' - alpine_file := 'alpine-minirootfs-${alpine_ver}-x86_64.tar.gz' - alpine_url := 'https://dl-cdn.alpinelinux.org/alpine/v${alpine_ver[..4]}/releases/x86_64/${alpine_file}' - alpine_dest := '/containers/images/alpine/${alpine_file}' - alpine_rootfs := '/containers/images/alpine/rootfs' - osal.download( - url: alpine_url - dest: alpine_dest - reset: args.reset - minsize_kb: 1024 - expand_dir: alpine_rootfs - )! - console.print_green('Alpine ${alpine_ver} rootfs prepared at ${alpine_rootfs}') - // Ubuntu versions with proper codename paths - ubuntu_info := [ - {ver: '24.04', codename: 'noble'}, - {ver: '25.04', codename: 'plucky'} - ] +fn (mut self ContainerFactory) init(args FactoryInitArgs) ! { + // Ensure base directories exist + osal.exec(cmd: 'mkdir -p /containers/images /containers/configs /containers/runtime', stdout: false)! + + // Setup for all supported images + images := [ContainerImage.alpine_3_20, .ubuntu_24_04, .ubuntu_25_04] - for info in ubuntu_info { - file := 'ubuntu-${info.ver}-minimal-cloudimg-amd64-root.tar.xz' - url := 'https://cloud-images.ubuntu.com/minimal/releases/${info.codename}/release/${file}' - // Use us.cloud-images domain for 25.04 daily if needed - if info.ver == '25.04' { - url = 'https://us.cloud-images.ubuntu.com/daily/server/server/minimal/releases/${info.codename}/release/${file}' - } - dest := '/containers/images/ubuntu/${info.ver}/${file}' - rootfs := '/containers/images/ubuntu/${info.ver}/rootfs' + for image in images { + match image { + .alpine_3_20 { + alpine_ver := '3.20.3' + alpine_file := 'alpine-minirootfs-${alpine_ver}-x86_64.tar.gz' + alpine_url := 'https://dl-cdn.alpinelinux.org/alpine/v${alpine_ver[..4]}/releases/x86_64/${alpine_file}' + alpine_dest := '/containers/images/alpine/${alpine_file}' + alpine_rootfs := '/containers/images/alpine/rootfs' + + if args.reset || !os.exists(alpine_rootfs) { + osal.download( + url: alpine_url + dest: alpine_dest + minsize_kb: 1024 + )! + + // Extract alpine rootfs + osal.exec(cmd: 'mkdir -p ${alpine_rootfs}', stdout: false)! + osal.exec(cmd: 'tar -xzf ${alpine_dest} -C ${alpine_rootfs}', stdout: false)! + } + console.print_green('Alpine ${alpine_ver} rootfs prepared at ${alpine_rootfs}') + } + .ubuntu_24_04 { + ver := '24.04' + codename := 'noble' + file := 'ubuntu-${ver}-minimal-cloudimg-amd64-root.tar.xz' + url := 'https://cloud-images.ubuntu.com/minimal/releases/${codename}/release/${file}' + dest := '/containers/images/ubuntu/${ver}/${file}' + rootfs := '/containers/images/ubuntu/${ver}/rootfs' - osal.download( - url: url - dest: dest - reset: args.reset - minsize_kb: 10240 - expand_dir: rootfs - )! + if args.reset || !os.exists(rootfs) { + osal.download( + url: url + dest: dest + minsize_kb: 10240 + )! + + // Extract ubuntu rootfs + osal.exec(cmd: 'mkdir -p ${rootfs}', stdout: false)! + osal.exec(cmd: 'tar -xf ${dest} -C ${rootfs}', stdout: false)! + } + console.print_green('Ubuntu ${ver} (${codename}) rootfs prepared at ${rootfs}') + } + .ubuntu_25_04 { + ver := '25.04' + codename := 'plucky' + file := 'ubuntu-${ver}-minimal-cloudimg-amd64-root.tar.xz' + url := 'https://cloud-images.ubuntu.com/daily/minimal/releases/${codename}/release/${file}' + dest := '/containers/images/ubuntu/${ver}/${file}' + rootfs := '/containers/images/ubuntu/${ver}/rootfs' - console.print_green('Ubuntu ${info.ver} (${info.codename}) rootfs prepared at ${rootfs}') - } + if args.reset || !os.exists(rootfs) { + osal.download( + url: url + dest: dest + minsize_kb: 10240 + )! + + // Extract ubuntu rootfs + osal.exec(cmd: 'mkdir -p ${rootfs}', stdout: false)! + osal.exec(cmd: 'tar -xf ${dest} -C ${rootfs}', stdout: false)! + } + console.print_green('Ubuntu ${ver} (${codename}) rootfs prepared at ${rootfs}') + } + } + } } -@[params] -pub struct ContainerNewArgs { -pub: - name string - reset bool -} + +pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container { + if args.name !in self.containers { + return error('Container ${args.name} does not exist') + } + return self.containers[args.name] +} pub fn (self ContainerFactory) list() ![]Container { mut containers := []Container{} - // Get list of containers using runc - result := osal.exec(cmd: 'runc list', stdout: true, name: 'list_containers') or { '' } + result := osal.exec(cmd: 'crun list --format json', stdout: false) or { '[]' } + + // Parse crun list output and populate containers + // The output format from crun list is typically tab-separated lines := result.split_into_lines() - if lines.len <= 1 { - return containers // No containers found - } - for line in lines[1..] { - parts := line.split(' ') + for line in lines { + if line.trim_space() == '' || line.starts_with('ID') { + continue + } + parts := line.split('\t') if parts.len > 0 { containers << Container{ name: parts[0] + factory: &self } } } return containers } -pub fn (self ContainerFactory) get(args ContainerNewArgs ) ! { - //TODO: implement get, give error if not exist - -} \ No newline at end of file