This commit is contained in:
2025-09-07 14:26:42 +04:00
parent 984013f774
commit 12316e57bb
20 changed files with 1686 additions and 210 deletions

183
lib/builder/executor_crun.v Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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
// ")!
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
```

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
-

View File

@@ -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)

View File

107
lib/virt/herorun2/README.md Normal file
View File

@@ -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

View File

@@ -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}')
}
}

View File

@@ -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')!

View File

@@ -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)!
}

View File

@@ -1,4 +1,4 @@
module herorun
module herorun2
import os
import freeflowuniverse.herolib.ui.console

View File

@@ -1,4 +1,4 @@
module herorun
module herorun2
import freeflowuniverse.herolib.osal.core as osal

View File

@@ -1,4 +1,4 @@
module herorun
module herorun2
// Base image types for containers
pub enum BaseImage {

View File

@@ -1,4 +1,4 @@
module herorun
module herorun2
import freeflowuniverse.herolib.osal.sshagent