From 12316e57bb7543b352bdce005ff331009ccd7a49 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 7 Sep 2025 14:26:42 +0400 Subject: [PATCH] ... --- lib/builder/executor_crun.v | 183 ++++ lib/installers/virt/herorunner/.heroscript | 13 + .../virt/herorunner/herorunner_actions.v | 69 ++ .../virt/herorunner/herorunner_factory_.v | 79 ++ .../virt/herorunner/herorunner_model.v | 37 + lib/installers/virt/herorunner/readme.md | 44 + lib/virt/herorun/README.md | 107 --- lib/virt/herorun/container.v | 138 ++- lib/virt/herorun/factory.v | 108 ++- lib/virt/herorun/instructions.md | 5 + lib/virt/herorun/instructions_crun.md | 867 ++++++++++++++++++ lib/virt/herorun/readme.md | 0 lib/virt/herorun2/README.md | 107 +++ lib/virt/herorun2/container.v | 95 ++ lib/virt/{herorun => herorun2}/executor.v | 6 +- lib/virt/herorun2/factory.v | 30 + .../{herorun => herorun2}/hetzner_backend.v | 2 +- lib/virt/{herorun => herorun2}/installer.v | 2 +- lib/virt/{herorun => herorun2}/interfaces.v | 2 +- lib/virt/{herorun => herorun2}/nodes.v | 2 +- 20 files changed, 1686 insertions(+), 210 deletions(-) create mode 100644 lib/builder/executor_crun.v create mode 100644 lib/installers/virt/herorunner/.heroscript create mode 100644 lib/installers/virt/herorunner/herorunner_actions.v create mode 100644 lib/installers/virt/herorunner/herorunner_factory_.v create mode 100644 lib/installers/virt/herorunner/herorunner_model.v create mode 100644 lib/installers/virt/herorunner/readme.md create mode 100644 lib/virt/herorun/instructions.md create mode 100644 lib/virt/herorun/instructions_crun.md create mode 100644 lib/virt/herorun/readme.md create mode 100644 lib/virt/herorun2/README.md create mode 100644 lib/virt/herorun2/container.v rename lib/virt/{herorun => herorun2}/executor.v (98%) create mode 100644 lib/virt/herorun2/factory.v rename lib/virt/{herorun => herorun2}/hetzner_backend.v (99%) rename lib/virt/{herorun => herorun2}/installer.v (99%) rename lib/virt/{herorun => herorun2}/interfaces.v (98%) rename lib/virt/{herorun => herorun2}/nodes.v (98%) diff --git a/lib/builder/executor_crun.v b/lib/builder/executor_crun.v new file mode 100644 index 00000000..21124ce6 --- /dev/null +++ b/lib/builder/executor_crun.v @@ -0,0 +1,183 @@ +module builder + +import os +import rand +import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.osal.rsync +import freeflowuniverse.herolib.core.pathlib +import freeflowuniverse.herolib.data.ipaddress +import freeflowuniverse.herolib.ui.console +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 +} + +fn (mut executor ExecutorCrun) init() ! { +} + +pub fn (mut executor ExecutorCrun) debug_on() { + executor.debug = true +} + +pub fn (mut executor ExecutorCrun) debug_off() { + executor.debug = false +} + +pub fn (mut executor ExecutorCrun) exec(args_ ExecArgs) !string { + mut args := args_ + if executor.debug { + console.print_debug('execute ${executor.ipaddr.addr}: ${args.cmd}') + } + //TODO: implement + res := osal.exec(cmd: args.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' + } + //TODO: implement + + console.print_debug(args.cmd) + osal.execute_interactive(args.cmd)! +} + +pub fn (mut executor ExecutorCrun) file_write(path string, text string) ! { + if executor.debug { + console.print_debug('${executor.ipaddr.addr} file write: ${path}') + } + //TODO implement use pathlib and write functionality +} + +pub fn (mut executor ExecutorCrun) file_read(path string) !string { + if executor.debug { + console.print_debug('${executor.ipaddr.addr} file read: ${path}') + } + //TODO implement use pathlib and read functionality +} + +pub fn (mut executor ExecutorCrun) file_exists(path string) bool { + if executor.debug { + console.print_debug('${executor.ipaddr.addr} 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 +} + +// carefull removes everything +pub fn (mut executor ExecutorCrun) delete(path string) ! { + if executor.debug { + console.print_debug('${executor.ipaddr.addr} file 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 +} + +// 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 + } + 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') + // } + + 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 + } + } + } + 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' + } +} + +// 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') + } + 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') + } + mut res := []string{} + //TODO: implement + output := executor.exec(cmd: 'ls ${path}', stdout: false)! + for line in output.split('\n') { + res << line + } + return res +} + +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 +} diff --git a/lib/installers/virt/herorunner/.heroscript b/lib/installers/virt/herorunner/.heroscript new file mode 100644 index 00000000..aad50adc --- /dev/null +++ b/lib/installers/virt/herorunner/.heroscript @@ -0,0 +1,13 @@ + +!!hero_code.generate_installer + name:'herorunner' + classname:'HeroRunner' + singleton:0 + templates:0 + default:1 + title:'' + supported_platforms:'' + reset:0 + startupmanager:0 + hasconfig:0 + build:0 \ No newline at end of file diff --git a/lib/installers/virt/herorunner/herorunner_actions.v b/lib/installers/virt/herorunner/herorunner_actions.v new file mode 100644 index 00000000..be6713bf --- /dev/null +++ b/lib/installers/virt/herorunner/herorunner_actions.v @@ -0,0 +1,69 @@ +module herorunner + +import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.core.pathlib +import freeflowuniverse.herolib.installers.ulist +import os + +//////////////////// following actions are not specific to instance of the object + +fn installed() !bool { + return false +} + +// get the Upload List of the files +fn ulist_get() !ulist.UList { + return ulist.UList{} +} + +fn upload() ! { +} + +fn install() ! { + console.print_header('install herorunner') + osal.package_install(' + xz-utils + crun')! + + // osal.exec( + // cmd: ' + + // ' + // stdout: true + // name: 'herorunner_install' + // )! +} + +fn destroy() ! { + // mut systemdfactory := systemd.new()! + // systemdfactory.destroy("zinit")! + + // osal.process_kill_recursive(name:'zinit')! + // osal.cmd_delete('zinit')! + + // osal.package_remove(' + // podman + // conmon + // buildah + // skopeo + // runc + // ')! + + // //will remove all paths where go/bin is found + // osal.profile_path_add_remove(paths2delete:"go/bin")! + + // osal.rm(" + // podman + // conmon + // buildah + // skopeo + // runc + // /var/lib/containers + // /var/lib/podman + // /var/lib/buildah + // /tmp/podman + // /tmp/conmon + // ")! +} diff --git a/lib/installers/virt/herorunner/herorunner_factory_.v b/lib/installers/virt/herorunner/herorunner_factory_.v new file mode 100644 index 00000000..dbf50fce --- /dev/null +++ b/lib/installers/virt/herorunner/herorunner_factory_.v @@ -0,0 +1,79 @@ +module herorunner + +import freeflowuniverse.herolib.core.playbook { PlayBook } +import freeflowuniverse.herolib.ui.console +import json +import freeflowuniverse.herolib.osal.startupmanager + +__global ( + herorunner_global map[string]&HeroRunner + herorunner_default string +) + +/////////FACTORY + +@[params] +pub struct ArgsGet { +pub mut: + name string = 'default' +} + +pub fn new(args ArgsGet) !&HeroRunner { + return &HeroRunner{} +} + +pub fn get(args ArgsGet) !&HeroRunner { + return new(args)! +} + +pub fn play(mut plbook PlayBook) ! { + if !plbook.exists(filter: 'herorunner.') { + return + } + mut install_actions := plbook.find(filter: 'herorunner.configure')! + if install_actions.len > 0 { + return error("can't configure herorunner, because no configuration allowed for this installer.") + } + mut other_actions := plbook.find(filter: 'herorunner.')! + for other_action in other_actions { + if other_action.name in ['destroy', 'install', 'build'] { + mut p := other_action.params + reset := p.get_default_false('reset') + if other_action.name == 'destroy' || reset { + console.print_debug('install action herorunner.destroy') + destroy()! + } + if other_action.name == 'install' { + console.print_debug('install action herorunner.install') + install()! + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////# LIVE CYCLE MANAGEMENT FOR INSTALLERS /////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////// + +@[params] +pub struct InstallArgs { +pub mut: + reset bool +} + +pub fn (mut self HeroRunner) install(args InstallArgs) ! { + switch(self.name) + if args.reset || (!installed()!) { + install()! + } +} + +pub fn (mut self HeroRunner) destroy() ! { + switch(self.name) + destroy()! +} + +// switch instance to be used for herorunner +pub fn switch(name string) { + herorunner_default = name +} diff --git a/lib/installers/virt/herorunner/herorunner_model.v b/lib/installers/virt/herorunner/herorunner_model.v new file mode 100644 index 00000000..9e7b2f39 --- /dev/null +++ b/lib/installers/virt/herorunner/herorunner_model.v @@ -0,0 +1,37 @@ +module herorunner + +import freeflowuniverse.herolib.data.paramsparser +import freeflowuniverse.herolib.data.encoderhero +import os + +pub const version = '0.0.0' +const singleton = false +const default = true + +// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED +@[heap] +pub struct HeroRunner { +pub mut: + name string = 'default' +} + +// your checking & initialization code if needed +fn obj_init(mycfg_ HeroRunner) !HeroRunner { + mut mycfg := mycfg_ + if mycfg.password == '' && mycfg.secret == '' { + return error('password or secret needs to be filled in for ${mycfg.name}') + } + return mycfg +} + +// called before start if done +fn configure() ! { + // mut installer := get()! +} + +/////////////NORMALLY NO NEED TO TOUCH + +pub fn heroscript_loads(heroscript string) !HeroRunner { + mut obj := encoderhero.decode[HeroRunner](heroscript)! + return obj +} diff --git a/lib/installers/virt/herorunner/readme.md b/lib/installers/virt/herorunner/readme.md new file mode 100644 index 00000000..48a12d5f --- /dev/null +++ b/lib/installers/virt/herorunner/readme.md @@ -0,0 +1,44 @@ +# herorunner + + + +To get started + +```vlang + + +import freeflowuniverse.herolib.installers.something.herorunner as herorunner_installer + +heroscript:=" +!!herorunner.configure name:'test' + password: '1234' + port: 7701 + +!!herorunner.start name:'test' reset:1 +" + +herorunner_installer.play(heroscript=heroscript)! + +//or we can call the default and do a start with reset +//mut installer:= herorunner_installer.get()! +//installer.start(reset:true)! + + + + +``` + +## example heroscript + +```hero +!!herorunner.configure + homedir: '/home/user/herorunner' + username: 'admin' + password: 'secretpassword' + title: 'Some Title' + host: 'localhost' + port: 8888 + +``` + + diff --git a/lib/virt/herorun/README.md b/lib/virt/herorun/README.md index 7c22a4aa..e69de29b 100644 --- a/lib/virt/herorun/README.md +++ b/lib/virt/herorun/README.md @@ -1,107 +0,0 @@ -# HeroRun - Remote Container Management - -A V library for managing remote containers using runc and tmux, with support for multiple cloud providers. - -## Features - -- **Multi-provider support**: Currently supports Hetzner, with ThreeFold coming soon -- **Automatic setup**: Installs required packages (runc, tmux, curl, xz-utils) automatically -- **Container isolation**: Uses runc for lightweight container management -- **tmux integration**: Each container gets its own tmux session for multiple concurrent shells -- **Clean API**: Simple interface that hides infrastructure complexity - -## Project Structure - -```txt -lib/virt/herorun/ -├── interfaces.v # Shared interfaces and parameter structs -├── nodes.v # Node management and SSH connectivity -├── container.v # Container struct and lifecycle operations -├── executor.v # Optimized command execution engine -├── factory.v # Provider abstraction and backend creation -├── hetzner_backend.v # Hetzner cloud implementation -└── README.md # This file -``` - -## Usage - -### Basic Example - -```v -import freeflowuniverse.herolib.virt.herorun - -// Create user with SSH key -mut user := herorun.new_user(keyname: 'id_ed25519')! - -// Create Hetzner backend -mut backend := herorun.new_hetzner_backend( - node_ip: '65.21.132.119' - user: 'root' -)! - -// Connect to node (installs required packages automatically) -backend.connect(keyname: user.keyname)! - -// Send a test command to the node -backend.send_command(cmd: 'ls')! - -// Get or create container (uses tmux behind the scenes) -mut container := backend.get_or_create_container(name: 'test_container')! - -// Attach to container tmux session -container.attach()! - -// Send command to container -container.send_command(cmd: 'ls')! - -// Get container logs -logs := container.get_logs()! -println('Container logs:') -println(logs) - -``` - -### Running the Example - -```bash -# Make the example executable -chmod +x examples/virt/herorun/herorun.vsh - -# Run it -./examples/virt/herorun/herorun.vsh -``` - -## Architecture - -### Interfaces - -- **NodeBackend**: Defines operations for connecting to and managing remote nodes -- **ContainerBackend**: Defines operations for container lifecycle management - -### Providers - -- **HetznerBackend**: Implementation for Hetzner cloud servers -- **ThreeFoldBackend**: (Coming soon) Implementation for ThreeFold nodes - -### Key Components - -1. **SSH Integration**: Uses herolib's sshagent module for secure connections -2. **tmux Management**: Uses herolib's tmux module for session management -3. **Container Runtime**: Uses runc for lightweight container execution -4. **Hetzner Integration**: Uses herolib's hetznermanager module - -## Dependencies - -- `freeflowuniverse.herolib.osal.sshagent` -- `freeflowuniverse.herolib.osal.tmux` -- `freeflowuniverse.herolib.installers.web.hetznermanager` -- `freeflowuniverse.herolib.ui.console` - -## Future Enhancements - -- ThreeFold backend implementation -- Support for additional cloud providers (AWS, GCP, etc.) -- Container image management -- Network configuration -- Volume mounting -- Resource limits and monitoring diff --git a/lib/virt/herorun/container.v b/lib/virt/herorun/container.v index 449f9a42..08c84781 100644 --- a/lib/virt/herorun/container.v +++ b/lib/virt/herorun/container.v @@ -2,94 +2,82 @@ 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 // Container struct and related functionality pub struct Container { -pub: - name string - node Node pub mut: - tmux tmux.Tmux + name string + //TODO: add properties we need for crun usage + node ?builder.Node + tmux ?tmux.Pane + factory &ContainerFactory } -// Implement ContainerBackend interface for Container -pub fn (mut c Container) attach() ! { - console.print_header('🔗 Attaching to container: ${c.name}') - // Create or get the session for this container - if !c.tmux.session_exist(c.name) { - console.print_stdout('Starting new tmux session for container ${c.name}') +pub fn (self Container) start() ! { - // Use the tmux convenience method to create session and window in one go - shell_cmd := 'ssh ${c.node.settings.user}@${c.node.settings.node_ip}' - c.tmux.window_new( - session_name: c.name - name: 'main' - cmd: shell_cmd - reset: true - )! - - // Wait for the session and window to be properly created - time.sleep(500 * time.millisecond) - - // Rescan to make sure everything is properly registered - c.tmux.scan()! - } - - console.print_green('Attached to container session ${c.name}') } -pub fn (mut c Container) send_command(args ContainerCommandArgs) ! { - console.print_header('📝 Exec in container ${c.name}') - // Ensure session exists - if !c.tmux.session_exist(c.name) { - return error('Container session ${c.name} does not exist. Call attach() first.') - } +pub fn (self Container) stop() ! { - // Debug: print session info - mut session := c.tmux.session_get(c.name)! - console.print_debug('Session ${c.name} has ${session.windows.len} windows') - for window in session.windows { - console.print_debug(' Window: ${window.name} (ID: ${window.id})') - } - - // Try to get the main window - mut window := session.window_get(name: 'main') or { - // If main window doesn't exist, try to get the first window - if session.windows.len > 0 { - session.windows[0] - } else { - return error('No windows available in session ${c.name}') - } - } - - // Refresh window state to get current panes - window.scan()! - - // Get the first pane and send the command - if window.panes.len > 0 { - mut pane := window.panes[0] - - // Send command to enter the container first, then the actual command - container_enter_cmd := 'cd /containers/${c.name} && runc exec ${c.name} ${args.cmd}' - pane.send_command(container_enter_cmd)! - } else { - return error('No panes available in container ${c.name}') - } } -pub fn (mut c Container) get_logs() !string { - // Get the session and window - mut session := c.tmux.session_get(c.name)! - mut window := session.window_get(name: 'main')! - - // Get logs from the first pane - if window.panes.len > 0 { - mut pane := window.panes[0] - return pane.logs_all()! - } else { - return error('No panes available in container ${c.name}') - } +//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)! } + +//TODO: add whatever else we need + + +//return as enum +pub fn (self Container) status() !ContainerStatus { + //TODO +} + +pub enum ContainerStatus { + running + stopped + paused + unknown +} + +//in percentage??? is that per core??? +pub fn (self Container) cpu_usage() !f64 { + //TODO +} + +//in MByte +pub fn (self Container) mem_usage() !f64 { + //TODO +} + + +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 +} + + +// +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 (self Container) node() !builder.Node { + //TODO: check if builder.Node is already there, if not initialize it and return +} \ No newline at end of file diff --git a/lib/virt/herorun/factory.v b/lib/virt/herorun/factory.v index b4892c80..709cfe1f 100644 --- a/lib/virt/herorun/factory.v +++ b/lib/virt/herorun/factory.v @@ -1,30 +1,98 @@ module herorun import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.tmux +import time +import freeflowuniverse.herolib.builder -// Provider types -pub enum Provider { - hetzner - threefold +// 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 } -// Factory function to create appropriate backend -pub fn new_backend(provider Provider, args NewNodeArgs) !NodeBackend { - match provider { - .hetzner { - console.print_header('🏭 Creating Hetzner Backend') - backend := new_hetzner_backend(args)! - return backend - } - .threefold { - console.print_header('🏭 Creating ThreeFold Backend') - // TODO: Implement ThreeFold backend - return error('ThreeFold backend not implemented yet') +@[params] +pub struct FactoryInitArgs { +pub: + reset bool +} + +pub fn new(args FactoryInitArgs) !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'} + ] + + 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' + + osal.download( + url: url + dest: dest + reset: args.reset + minsize_kb: 10240 + expand_dir: rootfs + )! + + console.print_green('Ubuntu ${info.ver} (${info.codename}) rootfs prepared at ${rootfs}') + } +} + +@[params] +pub struct ContainerNewArgs { +pub: + name string + reset bool +} + +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 { '' } + lines := result.split_into_lines() + if lines.len <= 1 { + return containers // No containers found + } + for line in lines[1..] { + parts := line.split(' ') + if parts.len > 0 { + containers << Container{ + name: parts[0] + } } } + return containers } -// Convenience function for Hetzner (most common case) -pub fn new_hetzner_node(args NewNodeArgs) !NodeBackend { - return new_backend(.hetzner, args)! -} +pub fn (self ContainerFactory) get(args ContainerNewArgs ) ! { + //TODO: implement get, give error if not exist + +} \ No newline at end of file diff --git a/lib/virt/herorun/instructions.md b/lib/virt/herorun/instructions.md new file mode 100644 index 00000000..5e6c77c1 --- /dev/null +++ b/lib/virt/herorun/instructions.md @@ -0,0 +1,5 @@ + + +- use builder... for remote execution inside the container + - make an executor like we have for SSH but then for the container, so we can use this to execute commands inside the container +- \ No newline at end of file diff --git a/lib/virt/herorun/instructions_crun.md b/lib/virt/herorun/instructions_crun.md new file mode 100644 index 00000000..5242ca68 --- /dev/null +++ b/lib/virt/herorun/instructions_crun.md @@ -0,0 +1,867 @@ +crun(1) General Commands Manual crun(1) + + + +NAME + crun - a fast and lightweight OCI runtime + + + +SYNOPSIS + crun [global options] command [command options] [arguments...] + + + +DESCRIPTION + crun is a command line program for running Linux containers that follow + the Open Container Initiative (OCI) format. + + + +COMMANDS + create Create a container. The runtime detaches from the container + process once the container environment is created. It is necessary to + successively use start for starting the container. + + + delete Remove definition for a container. + + + exec Exec a command in a running container. + + + list List known containers. + + + mounts add Add mounts while the container is running. It requires two + arguments: the container ID and a JSON file containing the mounts + section of the OCI config file. Each mount listed there is added to + the running container. The command is experimental and can be changed + without notice. + + + mounts remove Remove mounts while the container is running. It + requires two arguments: the container ID and a JSON file containing the + mounts section of the OCI config file. Only the destination attribute + for each mount is used. The command is experimental and can be changed + without notice. + + + kill Send the specified signal to the container init process. If no + signal is specified, SIGTERM is used. + + + ps Show the processes running in a container. + + + run Create and immediately start a container. + + + spec Generate a configuration file. + + + start Start a container that was previously created. A container + cannot be started multiple times. + + + state Output the state of a container. + + + pause Pause all the processes in the container. + + + resume Resume the processes in the container. + + + update Update container resource constraints. + + + checkpoint Checkpoint a running container using CRIU. + + + restore Restore a container from a checkpoint. + + + +STATE + By default, when running as root user, crun saves its state under the + /run/crun directory. As unprivileged user, instead the XDG_RUNTIME_DIR + environment variable is honored, and the directory + $XDG_RUNTIME_DIR/crun is used. The global option --root overrides this + setting. + + + +GLOBAL OPTIONS + --debug Produce verbose output. + + + --log=LOG-DESTINATION Define the destination for the error and warning + messages generated by crun. If the error happens late in the container + init process, when crun already stopped watching it, then it will be + printed to the container stderr. + + + It is specified in the form BACKEND:SPECIFIER. + + + These following backends are supported: + + o file:PATH + + o journald:IDENTIFIER + + o syslog:IDENTIFIER + + + If no backend is specified, then file: is used by default. + + + --log-format=FORMAT Define the format of the log messages. It can + either be text, or json. The default is text. + + + --log-level=LEVEL Define the log level. It can either be debug, + warning or error. The default is error. + + + --no-pivot Use chroot(2) instead of pivot_root(2) when creating the + container. This option is not safe, and should be avoided. + + + --root=DIR Defines where to store the state for crun containers. + + + --systemd-cgroup Use systemd for configuring cgroups. If not + specified, the cgroup is created directly using the cgroupfs backend. + + + --cgroup-manager=MANAGER Specify what cgroup manager must be used. + Permitted values are cgroupfs, systemd and disabled. + + + -?, --help Print a help list. + + + --usage Print a short usage message. + + + -V, --version Print program version + + +CREATE OPTIONS + crun [global options] create [options] CONTAINER + + + --bundle=PATH Path to the OCI bundle, by default it is the current + directory. + + + --config=FILE Override the configuration file to use. The default + value is config.json. + + + --console-socket=SOCKET Path to a UNIX socket that will receive the + ptmx end of the tty for the container. + + + --no-new-keyring Keep the same session key + + + --preserve-fds=N Additional number of FDs to pass into the container. + + + --pid-file=PATH Path to the file that will contain the container + process PID. + + +RUN OPTIONS + crun [global options] run [options] CONTAINER + + + --bundle=BUNDLE Path to the OCI bundle, by default it is the current + directory. + + + --config=FILE Override the configuration file to use. The default + value is config.json. + + + --console-socket=SOCKET Path to a UNIX socket that will receive the + ptmx end of the tty for the container. + + + --no-new-keyring Keep the same session key. + + + --preserve-fds=N Additional number of FDs to pass into the container. + + + --pid-file=PATH Path to the file that will contain the container + process PID. + + + --detach Detach the container process from the current session. + + +DELETE OPTIONS + crun [global options] delete [options] CONTAINER + + + --force Delete the container even if it is still running. + + + --regex=REGEX Delete all the containers that satisfy the specified + regex. + + +EXEC OPTIONS + crun [global options] exec [options] CONTAINER CMD + + + --apparmor=PROFILE Set the apparmor profile for the process. + + + --console-socket=SOCKET Path to a UNIX socket that will receive the + ptmx end of the tty for the container. + + + --cwd=PATH Set the working directory for the process to PATH. + + + --cap=CAP Specify an additional capability to add to the process. + + + --detach Detach the container process from the current session. + + + --cgroup=PATH Specify a sub-cgroup path inside the container cgroup. + The path must already exist in the container cgroup. + + + --env=ENV Specify an environment variable. + + + --no-new-privs Set the no new privileges value for the process. + + + --preserve-fds=N Additional number of FDs to pass into the container. + + + --process=FILE Path to a file containing the process JSON + configuration. + + + --process-label=VALUE Set the asm process label for the process + commonly used with selinux. + + + --pid-file=PATH Path to the file that will contain the new process PID. + + + -t --tty Allocate a pseudo TTY. + + + **-u USERSPEC --user=USERSPEC Specify the user in the form UID[:GID]. + + +LIST OPTIONS + crun [global options] list [options] + + + -q --quiet Show only the container ID. + + +KILL OPTIONS + crun [global options] kill [options] CONTAINER SIGNAL + + + --all Kill all the processes in the container. + + + --regex=REGEX Kill all the containers that satisfy the specified regex. + + +PS OPTIONS + crun [global options] ps [options] + + + --format=FORMAT Specify the output format. It must be either table or + json. By default table is used. + + +SPEC OPTIONS + crun [global options] spec [options] + + + -b DIR --bundle=DIR Path to the root of the bundle dir (default "."). + + + --rootless Generate a config.json file that is usable by an + unprivileged user. + + +UPDATE OPTIONS + crun [global options] update [options] CONTAINER + + + --blkio-weight=VALUE Specifies per cgroup weight. + + + --cpu-period=VALUE CPU CFS period to be used for hardcapping. + + + --cpu-quota=VALUE CPU CFS hardcap limit. + + + --cpu-rt-period=VALUE CPU realtime period to be used for hardcapping. + + + --cpu-rt-runtime=VALUE CPU realtime hardcap limit. + + + --cpu-share=VALUE CPU shares. + + + --cpuset-cpus=VALUE CPU(s) to use. + + + --cpuset-mems=VALUE Memory node(s) to use. + + + --kernel-memory=VALUE Kernel memory limit. + + + --kernel-memory-tcp=VALUE Kernel memory limit for TCP buffer. + + + --memory=VALUE Memory limit. + + + --memory-reservation=VALUE Memory reservation or soft_limit. + + + --memory-swap=VALUE Total memory usage. + + + --pids-limit=VALUE Maximum number of pids allowed in the container. + + + -r, --resources=FILE Path to the file containing the resources to + update. + + +CHECKPOINT OPTIONS + crun [global options] checkpoint [options] CONTAINER + + + --image-path=DIR Path for saving CRIU image files + + + --work-path=DIR Path for saving work files and logs + + + --leave-running Leave the process running after checkpointing + + + --tcp-established Allow open TCP connections + + + --ext-unix-sk Allow external UNIX sockets + + + --shell-job Allow shell jobs + + + --pre-dump Only checkpoint the container's memory without stopping the + container. It is not possible to restore a container from a pre-dump. + A pre-dump always needs a final checkpoint (without --pre-dump). It is + possible to make as many pre-dumps as necessary. For a second pre-dump + or for a final checkpoint it is necessary to use --parent-path to point + crun (and thus CRIU) to the pre-dump. + + + --parent-path=DIR Doing multiple pre-dumps or the final checkpoint + after one or multiple pre-dumps requires that crun (and thus CRIU) + knows the location of the pre-dump. It is important to use a relative + path from the actual checkpoint directory specified via --image-path. + It will fail if an absolute path is used. + + + --manage-cgroups-mode=MODE Specify which CRIU manage cgroup mode should + be used. Permitted values are soft, ignore, full or strict. Default is + soft. + + +RESTORE OPTIONS + crun [global options] restore [options] CONTAINER + + + -b DIR --bundle=DIR Container bundle directory (default ".") + + + --image-path=DIR Path for saving CRIU image files + + + --work-path=DIR Path for saving work files and logs + + + --tcp-established Allow open TCP connections + + + --ext-unix Allow external UNIX sockets + + + --shell-job Allow shell jobs + + + --detach Detach from the container's process + + + --pid-file=FILE Where to write the PID of the container + + + --manage-cgroups-mode=MODE Specify which CRIU manage cgroup mode should + be used. Permitted values are soft, ignore, full or strict. Default is + soft. + + + --lsm-profile=TYPE:NAME Specify an LSM profile to be used during + restore. TYPE can be either apparmor or selinux. + + + --lsm-mount-context=VALUE Specify a new LSM mount context to be used + during restore. This option replaces an existing mount context + information with the specified value. This is useful when restoring a + container into an existing Pod and selinux labels need to be changed + during restore. + + + +Extensions to OCI +run.oci.mount_context_type=context + Set the mount context type on volumes mounted with SELinux labels. + + + Valid context types are: + context (default) + fscontext + defcontext + rootcontext + + + More information on how the context mount flags works see the mount(8) + man page. + + +run.oci.seccomp.receiver=PATH + If the annotation run.oci.seccomp.receiver=PATH is specified, the + seccomp listener is sent to the UNIX socket listening on the specified + path. It can also set with the RUN_OCI_SECCOMP_RECEIVER environment + variable. It is an experimental feature, and the annotation will be + removed once it is supported in the OCI runtime specs. It must be an + absolute path. + + +run.oci.seccomp.plugins=PATH + If the annotation run.oci.seccomp.plugins=PLUGIN1[:PLUGIN2]... is + specified, the seccomp listener fd is handled through the specified + plugins. The plugin must either be an absolute path or a file name + that is looked up by dlopen(3). More information on how the lookup is + performed are available on the ld.so(8) man page. + + +run.oci.seccomp_fail_unknown_syscall=1 + If the annotation run.oci.seccomp_fail_unknown_syscall is present, then + crun will fail when an unknown syscall is encountered in the seccomp + configuration. + + +run.oci.seccomp_bpf_data=PATH + If the annotation run.oci.seccomp_bpf_data is present, then crun + ignores the seccomp section in the OCI configuration file and use the + specified data as the raw data to the seccomp(SECCOMP_SET_MODE_FILTER) + syscall. The data must be encoded in base64. + + + It is an experimental feature, and the annotation will be removed once + it is supported in the OCI runtime specs. + + +run.oci.keep_original_groups=1 + If the annotation run.oci.keep_original_groups is present, then crun + will skip the setgroups syscall that is used to either set the + additional groups specified in the OCI configuration, or to reset the + list of additional groups if none is specified. + + +run.oci.pidfd_receiver=PATH + It is an experimental feature and will be removed once the feature is + in the OCI runtime specs. + + + If present, specify the path to the UNIX socket that will receive the + pidfd for the container process. + + +run.oci.systemd.force_cgroup_v1=/PATH + If the annotation run.oci.systemd.force_cgroup_v1=/PATH is present, + then crun will override the specified mount point /PATH with a cgroup + v1 mount made of a single hierarchy none,name=systemd. It is useful to + run on a cgroup v2 system containers using older versions of systemd + that lack support for cgroup v2. + + + Note: Your container host has to have the cgroup v1 mount already + present, otherwise this will not work. If you want to run the container + rootless, the user it runs under has to have permissions to this + mountpoint. + + + For example, as root: + + mkdir /sys/fs/cgroup/systemd + mount cgroup -t cgroup /sys/fs/cgroup/systemd -o none,name=systemd,xattr + chown -R the_user.the_user /sys/fs/cgroup/systemd + + +run.oci.systemd.subgroup=SUBGROUP + Override the name for the systemd sub cgroup created under the systemd + scope, so the final cgroup will be like: + + /sys/fs/cgroup/$PATH/$SUBGROUP + + + When it is set to the empty string, a sub cgroup is not created. + + + If not specified, it defaults to container on cgroup v2, and to "" on + cgroup v1. + + + e.g. + + /sys/fs/cgroup//system.slice/foo-352700.scope/container + + +run.oci.delegate-cgroup=DELEGATED-CGROUP + If the run.oci.systemd.subgroup annotation is specified, yet another + sub-cgroup is created and the container process is moved here. + + + If a cgroup namespace is used, the cgroup namespace is created before + moving the container to the delegated cgroup. + + /sys/fs/cgroup/$PATH/$SUBGROUP/$DELEGATED-CGROUP + + + The runtime doesn't apply any limit to the $DELEGATED-CGROUP sub- + cgroup, the runtime uses only $PATH/$SUBGROUP. + + + The container payload fully manages $DELEGATE-CGROUP, the limits + applied to $PATH/$SUBGROUP still applies to $DELEGATE-CGROUP. + + + Since cgroup delegation is not safe on cgroup v1, this option is + supported only on cgroup v2. + + +run.oci.hooks.stdout=FILE + If the annotation run.oci.hooks.stdout is present, then crun will open + the specified file and use it as the stdout for the hook processes. + The file is opened in append mode and it is created if it doesn't + already exist. + + +run.oci.hooks.stderr=FILE + If the annotation run.oci.hooks.stderr is present, then crun will open + the specified file and use it as the stderr for the hook processes. + The file is opened in append mode and it is created if it doesn't + already exist. + + +run.oci.handler=HANDLER + It is an experimental feature. + + + If specified, run the specified handler for execing the container. The + only supported values are krun and wasm. + + o krun: When krun is specified, the libkrun.so shared object is loaded + and it is used to launch the container using libkrun. + + o wasm: If specified, run the wasm handler for container. Allows + running wasm workload natively. Accepts a .wasm binary as input and + if .wat is provided it will be automatically compiled into a wasm + module. Stdout of wasm module is relayed back via crun. + + +tmpcopyup mount options + If the tmpcopyup option is specified for a tmpfs, then the path that is + shadowed by the tmpfs mount is recursively copied up to the tmpfs + itself. + + +copy-symlink mount options + If the copy-symlink option is specified, if the source of a bind mount + is a symlink, the symlink is recreated at the specified destination + instead of attempting a mount that would resolve the symlink itself. + If the destination already exists and it is not a symlink with the + expected content, crun will return an error. + + +dest-nofollow + When this option is specified for a bind mount, and the destination of + the bind mount is a symbolic link, crun will mount the symbolic link + itself at the target destination. + + +src-nofollow + When this option is specified for a bind mount, and the source of the + bind mount is a symbolic link, crun will use the symlink itself rather + than the file or directory the symbolic link points to. + + +r$FLAG mount options + If a r$FLAG mount option is specified then the flag $FLAG is set + recursively for each children mount. + + + These flags are supported: + + o "rro" + + o "rrw" + + o "rsuid" + + o "rnosuid" + + o "rdev" + + o "rnodev" + + o "rexec" + + o "rnoexec" + + o "rsync" + + o "rasync" + + o "rdirsync" + + o "rmand" + + o "rnomand" + + o "ratime" + + o "rnoatime" + + o "rdiratime" + + o "rnodiratime" + + o "rrelatime" + + o "rnorelatime" + + o "rstrictatime" + + o "rnostrictatime" + + +idmap mount options + If the idmap option is specified then the mount is ID mapped using the + container target user namespace. This is an experimental feature and + can change at any time without notice. + + + The idmap option supports a custom mapping that can be different than + the user namespace used by the container. + + + The mapping can be specified after the idmap option like: + idmap=uids=0-1-10#10-11-10;gids=0-100-10. + + + For each triplet, the first value is the start of the backing file + system IDs that are mapped to the second value on the host. The length + of this mapping is given in the third value. + + + Multiple ranges are separated with #. + + + These values are written to the /proc/$PID/uid_map and + /proc/$PID/gid_map files to create the user namespace for the idmapped + mount. + + + The only two options that are currently supported after idmap are uids + and gids. + + + When a custom mapping is specified, a new user namespace is created for + the idmapped mount. + + + If no option is specified, then the container user namespace is used. + + + If the specified mapping is prepended with a '@' then the mapping is + considered relative to the container user namespace. The host ID for + the mapping is changed to account for the relative position of the + container user in the container user namespace. + + + For example, the mapping: uids=@1-3-10, given a configuration like + + "uidMappings": [ + { + "containerID": 0, + "hostID": 0, + "size": 1 + }, + { + "containerID": 1, + "hostID": 2, + "size": 1000 + } + ] + + + will be converted to the absolute value uids=1-4-10, where 4 is + calculated by adding 3 (container ID in the uids= mapping) and 1 + (hostID - containerID for the user namespace mapping where containerID + = 1 is found). + + + The current implementation doesn't take into account multiple user + namespace ranges, so it is the caller's responsibility to split a + mapping if it overlaps multiple ranges in the user namespace. In such + a case, there won't be any error reported. + + +Automatically create user namespace + When running as user different than root, an user namespace is + automatically created even if it is not specified in the config file. + The current user is mapped to the ID 0 in the container, and any + additional id specified in the files /etc/subuid and /etc/subgid is + automatically added starting with ID 1. + + + +CGROUP v1 + Support for cgroup v1 is deprecated and will be removed in a future + release. + + + +CGROUP v2 + Note: cgroup v2 does not yet support control of realtime processes and + the cpu controller can only be enabled when all RT processes are in the + root cgroup. This will make crun fail while running alongside RT + processes. + + + If the cgroup configuration found is for cgroup v1, crun attempts a + conversion when running on a cgroup v2 system. + + + These are the OCI resources currently supported with cgroup v2 and how + they are converted when needed from the cgroup v1 configuration. + + +Memory controller + + +------------+--------------------+----------------------+------------------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +------------+--------------------+----------------------+------------------+ + |limit | memory.max | y = x | | + +------------+--------------------+----------------------+------------------+ + |swap | memory.swap.max | y = x - memory_limit | the swap limit | + | | | | on cgroup v1 | + | | | | includes the | + | | | | memory usage too | + +------------+--------------------+----------------------+------------------+ + |reservation | memory.low | y = x | | + +------------+--------------------+----------------------+------------------+ + +PIDs controller + + +--------+--------------------+------------+---------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +--------+--------------------+------------+---------+ + |limit | pids.max | y = x | | + +--------+--------------------+------------+---------+ + +CPU controller + + +--------+--------------------+------------------+------------------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +--------+--------------------+------------------+------------------+ + |shares | cpu.weight | y=10^((log2(x)^2 | | + | | | + 125 * log2(x)) | | + | | | / 612.0 - 7.0 / | | + | | | 34.0) | | + +--------+--------------------+------------------+------------------+ + | | convert from | | | + | | [2-262144] to | | | + | | [1-10000] | | | + +--------+--------------------+------------------+------------------+ + |period | cpu.max | y = x | period and quota | + | | | | are written | + | | | | together | + +--------+--------------------+------------------+------------------+ + |quota | cpu.max | y = x | period and quota | + | | | | are written | + | | | | together | + +--------+--------------------+------------------+------------------+ + +blkio controller + + +--------------+----------------------+-------------------------+------------------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +--------------+----------------------+-------------------------+------------------+ + |weight | io.bfq.weight | y = x | | + +--------------+----------------------+-------------------------+------------------+ + |weight_device | io.bfq.weight | y = x | | + +--------------+----------------------+-------------------------+------------------+ + |weight | io.weight (fallback) | y = 1 + (x-10)*9999/990 | convert linearly | + | | | | from [10-1000] | + | | | | to [1-10000] | + +--------------+----------------------+-------------------------+------------------+ + |weight_device | io.weight (fallback) | y = 1 + (x-10)*9999/990 | convert linearly | + | | | | from [10-1000] | + | | | | to [1-10000] | + +--------------+----------------------+-------------------------+------------------+ + |rbps | io.max | y=x | | + +--------------+----------------------+-------------------------+------------------+ + |wbps | io.max | y=x | | + +--------------+----------------------+-------------------------+------------------+ + |riops | io.max | y=x | | + +--------------+----------------------+-------------------------+------------------+ + |wiops | io.max | y=x | | + +--------------+----------------------+-------------------------+------------------+ + +cpuset controller + + +--------+--------------------+------------+---------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +--------+--------------------+------------+---------+ + |cpus | cpuset.cpus | y = x | | + +--------+--------------------+------------+---------+ + |mems | cpuset.mems | y = x | | + +--------+--------------------+------------+---------+ + +hugetlb controller + + +----------------+--------------------+------------+---------+ + |OCI (x) | cgroup 2 value (y) | conversion | comment | + +----------------+--------------------+------------+---------+ + |.limit_in_bytes | hugetlb..max | y = x | | + +----------------+--------------------+------------+---------+ + User Commands crun(1) \ No newline at end of file diff --git a/lib/virt/herorun/readme.md b/lib/virt/herorun/readme.md new file mode 100644 index 00000000..e69de29b diff --git a/lib/virt/herorun2/README.md b/lib/virt/herorun2/README.md new file mode 100644 index 00000000..7c22a4aa --- /dev/null +++ b/lib/virt/herorun2/README.md @@ -0,0 +1,107 @@ +# HeroRun - Remote Container Management + +A V library for managing remote containers using runc and tmux, with support for multiple cloud providers. + +## Features + +- **Multi-provider support**: Currently supports Hetzner, with ThreeFold coming soon +- **Automatic setup**: Installs required packages (runc, tmux, curl, xz-utils) automatically +- **Container isolation**: Uses runc for lightweight container management +- **tmux integration**: Each container gets its own tmux session for multiple concurrent shells +- **Clean API**: Simple interface that hides infrastructure complexity + +## Project Structure + +```txt +lib/virt/herorun/ +├── interfaces.v # Shared interfaces and parameter structs +├── nodes.v # Node management and SSH connectivity +├── container.v # Container struct and lifecycle operations +├── executor.v # Optimized command execution engine +├── factory.v # Provider abstraction and backend creation +├── hetzner_backend.v # Hetzner cloud implementation +└── README.md # This file +``` + +## Usage + +### Basic Example + +```v +import freeflowuniverse.herolib.virt.herorun + +// Create user with SSH key +mut user := herorun.new_user(keyname: 'id_ed25519')! + +// Create Hetzner backend +mut backend := herorun.new_hetzner_backend( + node_ip: '65.21.132.119' + user: 'root' +)! + +// Connect to node (installs required packages automatically) +backend.connect(keyname: user.keyname)! + +// Send a test command to the node +backend.send_command(cmd: 'ls')! + +// Get or create container (uses tmux behind the scenes) +mut container := backend.get_or_create_container(name: 'test_container')! + +// Attach to container tmux session +container.attach()! + +// Send command to container +container.send_command(cmd: 'ls')! + +// Get container logs +logs := container.get_logs()! +println('Container logs:') +println(logs) + +``` + +### Running the Example + +```bash +# Make the example executable +chmod +x examples/virt/herorun/herorun.vsh + +# Run it +./examples/virt/herorun/herorun.vsh +``` + +## Architecture + +### Interfaces + +- **NodeBackend**: Defines operations for connecting to and managing remote nodes +- **ContainerBackend**: Defines operations for container lifecycle management + +### Providers + +- **HetznerBackend**: Implementation for Hetzner cloud servers +- **ThreeFoldBackend**: (Coming soon) Implementation for ThreeFold nodes + +### Key Components + +1. **SSH Integration**: Uses herolib's sshagent module for secure connections +2. **tmux Management**: Uses herolib's tmux module for session management +3. **Container Runtime**: Uses runc for lightweight container execution +4. **Hetzner Integration**: Uses herolib's hetznermanager module + +## Dependencies + +- `freeflowuniverse.herolib.osal.sshagent` +- `freeflowuniverse.herolib.osal.tmux` +- `freeflowuniverse.herolib.installers.web.hetznermanager` +- `freeflowuniverse.herolib.ui.console` + +## Future Enhancements + +- ThreeFold backend implementation +- Support for additional cloud providers (AWS, GCP, etc.) +- Container image management +- Network configuration +- Volume mounting +- Resource limits and monitoring diff --git a/lib/virt/herorun2/container.v b/lib/virt/herorun2/container.v new file mode 100644 index 00000000..17d21d6e --- /dev/null +++ b/lib/virt/herorun2/container.v @@ -0,0 +1,95 @@ +module herorun2 + +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.osal.tmux +import time + +// Container struct and related functionality +pub struct Container { +pub: + name string + node Node +pub mut: + tmux tmux.Tmux +} + +// Implement ContainerBackend interface for Container +pub fn (mut c Container) attach() ! { + console.print_header('🔗 Attaching to container: ${c.name}') + + // Create or get the session for this container + if !c.tmux.session_exist(c.name) { + console.print_stdout('Starting new tmux session for container ${c.name}') + + // Use the tmux convenience method to create session and window in one go + shell_cmd := 'ssh ${c.node.settings.user}@${c.node.settings.node_ip}' + c.tmux.window_new( + session_name: c.name + name: 'main' + cmd: shell_cmd + reset: true + )! + + // Wait for the session and window to be properly created + time.sleep(500 * time.millisecond) + + // Rescan to make sure everything is properly registered + c.tmux.scan()! + } + + console.print_green('Attached to container session ${c.name}') +} + +pub fn (mut c Container) send_command(args ContainerCommandArgs) ! { + console.print_header('📝 Exec in container ${c.name}') + + // Ensure session exists + if !c.tmux.session_exist(c.name) { + return error('Container session ${c.name} does not exist. Call attach() first.') + } + + // Debug: print session info + mut session := c.tmux.session_get(c.name)! + console.print_debug('Session ${c.name} has ${session.windows.len} windows') + for window in session.windows { + console.print_debug(' Window: ${window.name} (ID: ${window.id})') + } + + // Try to get the main window + mut window := session.window_get(name: 'main') or { + // If main window doesn't exist, try to get the first window + if session.windows.len > 0 { + session.windows[0] + } else { + return error('No windows available in session ${c.name}') + } + } + + // Refresh window state to get current panes + window.scan()! + + // Get the first pane and send the command + if window.panes.len > 0 { + mut pane := window.panes[0] + + // Send command to enter the container first, then the actual command + container_enter_cmd := 'cd /containers/${c.name} && runc exec ${c.name} ${args.cmd}' + pane.send_command(container_enter_cmd)! + } else { + return error('No panes available in container ${c.name}') + } +} + +pub fn (mut c Container) get_logs() !string { + // Get the session and window + mut session := c.tmux.session_get(c.name)! + mut window := session.window_get(name: 'main')! + + // Get logs from the first pane + if window.panes.len > 0 { + mut pane := window.panes[0] + return pane.logs_all()! + } else { + return error('No panes available in container ${c.name}') + } +} diff --git a/lib/virt/herorun/executor.v b/lib/virt/herorun2/executor.v similarity index 98% rename from lib/virt/herorun/executor.v rename to lib/virt/herorun2/executor.v index e6ec3b9c..1c9036a6 100644 --- a/lib/virt/herorun/executor.v +++ b/lib/virt/herorun2/executor.v @@ -1,8 +1,7 @@ -module herorun +module herorun2 import freeflowuniverse.herolib.osal.tmux import freeflowuniverse.herolib.osal.sshagent -import freeflowuniverse.herolib.virt.hetznermanager import freeflowuniverse.herolib.osal.core as osal import time import os @@ -18,7 +17,6 @@ pub mut: session_name string window_name string agent sshagent.SSHAgent - hetzner &hetznermanager.HetznerManager } @[params] @@ -218,7 +216,7 @@ fn (mut e Executor) create_container() ! { } } - setup_cmd=texttools.dedent(setup_cmd) + setup_cmd = texttools.dedent(setup_cmd) remote_cmd := 'ssh ${e.node.settings.user}@${e.node.settings.node_ip} "${setup_cmd}"' osal.exec(cmd: remote_cmd, stdout: false, name: 'container_create')! diff --git a/lib/virt/herorun2/factory.v b/lib/virt/herorun2/factory.v new file mode 100644 index 00000000..a8794207 --- /dev/null +++ b/lib/virt/herorun2/factory.v @@ -0,0 +1,30 @@ +module herorun2 + +import freeflowuniverse.herolib.ui.console + +// Provider types +pub enum Provider { + hetzner + threefold +} + +// Factory function to create appropriate backend +pub fn new_backend(provider Provider, args NewNodeArgs) !NodeBackend { + match provider { + .hetzner { + console.print_header('🏭 Creating Hetzner Backend') + backend := new_hetzner_backend(args)! + return backend + } + .threefold { + console.print_header('🏭 Creating ThreeFold Backend') + // TODO: Implement ThreeFold backend + return error('ThreeFold backend not implemented yet') + } + } +} + +// Convenience function for Hetzner (most common case) +pub fn new_hetzner_node(args NewNodeArgs) !NodeBackend { + return new_backend(.hetzner, args)! +} diff --git a/lib/virt/herorun/hetzner_backend.v b/lib/virt/herorun2/hetzner_backend.v similarity index 99% rename from lib/virt/herorun/hetzner_backend.v rename to lib/virt/herorun2/hetzner_backend.v index 3ade87cb..15422bdf 100644 --- a/lib/virt/herorun/hetzner_backend.v +++ b/lib/virt/herorun2/hetzner_backend.v @@ -1,4 +1,4 @@ -module herorun +module herorun2 import os import freeflowuniverse.herolib.ui.console diff --git a/lib/virt/herorun/installer.v b/lib/virt/herorun2/installer.v similarity index 99% rename from lib/virt/herorun/installer.v rename to lib/virt/herorun2/installer.v index 521663f7..d52600d6 100644 --- a/lib/virt/herorun/installer.v +++ b/lib/virt/herorun2/installer.v @@ -1,4 +1,4 @@ -module herorun +module herorun2 import freeflowuniverse.herolib.osal.core as osal diff --git a/lib/virt/herorun/interfaces.v b/lib/virt/herorun2/interfaces.v similarity index 98% rename from lib/virt/herorun/interfaces.v rename to lib/virt/herorun2/interfaces.v index 738c5481..db62b12d 100644 --- a/lib/virt/herorun/interfaces.v +++ b/lib/virt/herorun2/interfaces.v @@ -1,4 +1,4 @@ -module herorun +module herorun2 // Base image types for containers pub enum BaseImage { diff --git a/lib/virt/herorun/nodes.v b/lib/virt/herorun2/nodes.v similarity index 98% rename from lib/virt/herorun/nodes.v rename to lib/virt/herorun2/nodes.v index fd11f805..4e285132 100644 --- a/lib/virt/herorun/nodes.v +++ b/lib/virt/herorun2/nodes.v @@ -1,4 +1,4 @@ -module herorun +module herorun2 import freeflowuniverse.herolib.osal.sshagent