...
This commit is contained in:
35
examples/virt/herorun/basic_example.vsh
Normal file
35
examples/virt/herorun/basic_example.vsh
Normal file
@@ -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()!
|
||||
36
examples/virt/herorun/builder_integration.vsh
Normal file
36
examples/virt/herorun/builder_integration.vsh
Normal file
@@ -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()!
|
||||
4
examples/virt/herorun/herorun2.vsh
Normal file
4
examples/virt/herorun/herorun2.vsh
Normal file
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
119
lib/virt/herorun/config_template.json
Normal file
119
lib/virt/herorun/config_template.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
75
lib/virt/herorun/container_create.v
Normal file
75
lib/virt/herorun/container_create.v
Normal file
@@ -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)!
|
||||
}
|
||||
52
lib/virt/herorun/container_image.v
Normal file
52
lib/virt/herorun/container_image.v
Normal file
@@ -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/<image_name>/rootfs
|
||||
docker_unc string //optional
|
||||
|
||||
}
|
||||
|
||||
pub struct ContainerImageArgs {
|
||||
pub:
|
||||
image_name string @[required] //image is located in /containers/images/<image_name>/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/<image_name>/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/<image_name>/rootfs, if already exist give error, unless if we specify reset
|
||||
}
|
||||
|
||||
pub fn (mut self ContainerImage) delete() !{
|
||||
//TODO:
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user