This commit is contained in:
2025-07-21 12:46:31 +02:00
parent bf01a35686
commit 1fe0f04226
8 changed files with 376 additions and 5 deletions

View File

@@ -0,0 +1,323 @@
# Builder Module: System Automation and Remote Execution
The `builder` module in Herolib provides a powerful framework for automating system tasks and executing commands on both local and remote machines. It offers a unified interface to manage nodes, execute commands, perform file operations, and maintain persistent state.
## Key Components
- **`BuilderFactory`**: Responsible for creating and managing `Node` instances.
- **`Node`**: Represents a target system (local or remote). It encapsulates system properties (platform, CPU type, environment variables) and provides methods for interaction.
- **`Executor`**: An interface (implemented by `ExecutorLocal` and `ExecutorSSH`) that handles the actual command execution and file operations on the target system.
- **NodeDB (via `Node.done` map)**: A key-value store within each `Node` for persistent state, caching, and tracking execution history.
## Getting Started
### Initializing a Builder and Node
First, import the `builder` module and create a new `BuilderFactory` instance. Then, create a `Node` object, which can represent either the local machine or a remote server.
```v
import freeflowuniverse.herolib.builder
// Create a new builder factory
mut b := builder.new()!
// Create a node for the local machine
mut local_node := b.node_local()!
// Create a node for a remote server via SSH
// Format: "user@ip_address:port" or "ip_address:port" or "ip_address"
mut remote_node := b.node_new(ipaddr: "root@195.192.213.2:2222")!
// Node with custom name and debug enabled
mut named_debug_node := b.node_new(
name: "my_remote_server",
ipaddr: "user@server.example.com:22",
debug: true
)!
```
### `Node` Properties
A `Node` object automatically detects and caches system information. You can access these properties:
```v
// Get platform type (e.g., .osx, .ubuntu, .alpine, .arch)
println(node.platform)
// Get CPU architecture (e.g., .intel, .arm)
println(node.cputype)
// Get hostname
println(node.hostname)
// Get environment variables
env_vars := node.environ_get()!
println(env_vars['HOME'])
// Get node information (category, sshkey, user, ipaddress, port)
info := node.info()
println(info['category'])
```
## Command Execution
The `Node` object provides methods to execute commands on the target system.
### `node.exec(args ExecArgs) !string`
Executes a command and returns its standard output.
```v
import freeflowuniverse.herolib.builder { ExecArgs }
// Execute a command with stdout
result := node.exec(cmd: "ls -la /tmp", stdout: true)!
println(result)
// Execute silently (no stdout)
node.exec(cmd: "mkdir -p /tmp/my_dir", stdout: false)!
```
### `node.exec_silent(cmd string) !string`
Executes a command silently (no stdout) and returns its output.
```v
output := node.exec_silent("echo 'Hello from remote!'")!
println(output)
```
### `node.exec_interactive(cmd string) !`
Executes a command in an interactive shell.
```v
// This will open an interactive shell session
node.exec_interactive("bash")!
```
### `node.exec_cmd(args NodeExecCmd) !string`
A more advanced command execution method that supports caching, periodic execution, and temporary script handling.
```v
import freeflowuniverse.herolib.builder { NodeExecCmd }
// Execute a command, cache its result for 24 hours (48*3600 seconds)
// and provide a description for logging.
result := node.exec_cmd(
cmd: "apt-get update",
period: 48 * 3600,
description: "Update system packages"
)!
println(result)
// Execute a multi-line script
script_output := node.exec_cmd(
cmd: "
echo 'Starting script...'
ls -la /
echo 'Script finished.'
",
name: "my_custom_script",
stdout: true
)!
println(script_output)
```
### `node.exec_retry(args ExecRetryArgs) !string`
Executes a command with retries until it succeeds or a timeout is reached.
```v
import freeflowuniverse.herolib.builder { ExecRetryArgs }
// Try to connect to a service, retrying every 100ms for up to 10 seconds
result := node.exec_retry(
cmd: "curl --fail http://localhost:8080/health",
retrymax: 100, // 100 retries
period_milli: 100, // 100ms sleep between retries
timeout: 10 // 10 seconds total timeout
)!
println("Service is up: ${result}")
```
### `node.cmd_exists(cmd string) bool`
Checks if a command exists on the target system.
```v
if node.cmd_exists("docker") {
println("Docker is installed.")
} else {
println("Docker is not installed.")
}
```
## File System Operations
The `Node` object provides comprehensive file and directory management capabilities.
### `node.file_write(path string, text string) !`
Writes content to a file on the target system.
```v
node.file_write("/tmp/my_file.txt", "This is some content.")!
```
### `node.file_read(path string) !string`
Reads content from a file on the target system.
```v
content := node.file_read("/tmp/my_file.txt")!
println(content)
```
### `node.file_exists(path string) bool`
Checks if a file or directory exists on the target system.
```v
if node.file_exists("/tmp/my_file.txt") {
println("File exists.")
}
```
### `node.delete(path string) !`
Deletes a file or directory (recursively for directories) on the target system.
```v
node.delete("/tmp/my_dir")!
```
### `node.list(path string) ![]string`
Lists the contents of a directory on the target system.
```v
files := node.list("/home/user")!
for file in files {
println(file)
}
```
### `node.dir_exists(path string) bool`
Checks if a directory exists on the target system.
```v
if node.dir_exists("/var/log") {
println("Log directory exists.")
}
```
### File Transfers (`node.upload` and `node.download`)
Transfer files between the local machine and the target node using `rsync` or `scp`.
```v
import freeflowuniverse.herolib.builder { SyncArgs }
// Upload a local file to the remote node
node.upload(
source: "/local/path/to/my_script.sh",
dest: "/tmp/remote_script.sh",
stdout: true // Show rsync/scp output
)!
// Download a file from the remote node to the local machine
node.download(
source: "/var/log/syslog",
dest: "/tmp/local_syslog.log",
stdout: false
)!
// Upload a directory, ignoring .git and examples folders, and deleting extra files on destination
node.upload(
source: "/local/repo/",
dest: "~/code/my_project/",
ignore: [".git/*", "examples/"],
delete: true,
fast_rsync: true
)!
```
## Node Database (`node.done`)
The `node.done` map provides a simple key-value store for persistent data on the node. This data is cached in Redis.
```v
// Store a value
node.done_set("setup_complete", "true")!
// Retrieve a value
status := node.done_get("setup_complete") or { "false" }
println("Setup complete: ${status}")
// Check if a key exists
if node.done_exists("initial_config") {
println("Initial configuration done.")
}
// Print all stored 'done' items
node.done_print()
// Reset all stored 'done' items
node.done_reset()!
```
## Bootstrapping and Updates
The `bootstrapper` module provides functions for installing and updating Herolib components on nodes.
### `node.hero_install() !`
Installs the Herolib environment on the node.
```v
node.hero_install()!
```
### `node.hero_update(args HeroUpdateArgs) !`
Updates the Herolib code on the node, with options for syncing from local, git reset, or git pull.
```v
import freeflowuniverse.herolib.builder { HeroUpdateArgs }
// Sync local Herolib code to the remote node (full sync)
node.hero_update(sync_from_local: true, sync_full: true)!
// Reset git repository on the remote node and pull latest from 'dev' branch
node.hero_update(git_reset: true, branch: "dev")!
```
### `node.vscript(args VScriptArgs) !`
Uploads and executes a Vlang script (`.vsh` or `.v`) on the remote node.
```v
import freeflowuniverse.herolib.builder { VScriptArgs }
// Upload and execute a local V script on the remote node
node.vscript(path: "/local/path/to/my_script.vsh", sync_from_local: true)!
```
## Port Forwarding
The `portforward_to_local` function allows forwarding a remote port on an SSH host to a local port.
```v
import freeflowuniverse.herolib.builder { portforward_to_local, ForwardArgsToLocal }
// Forward remote port 8080 on 192.168.1.100 to local port 9000
portforward_to_local(
name: "my_app_forward",
address: "192.168.1.100",
remote_port: 8080,
local_port: 9000
)!
```

View File

@@ -8,7 +8,7 @@ import freeflowuniverse.herolib.core.herocmds
import freeflowuniverse.herolib.installers.base import freeflowuniverse.herolib.installers.base
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.ui import freeflowuniverse.herolib.ui
import freeflowuniverse.herolib.osal import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.core import freeflowuniverse.herolib.core
import freeflowuniverse.herolib.core.playbook import freeflowuniverse.herolib.core.playbook
import freeflowuniverse.herolib.core.playcmds import freeflowuniverse.herolib.core.playcmds

View File

@@ -0,0 +1,39 @@
!!bizmodel.revenue_define bizname:'threefold' name:'cloud_services' extrapolate:1
descr:'Cloud Services Revenue'
revenue:'10:10000USD,46:1000000USD'
cogs_percent:'0%'
!!bizmodel.funding_define bizname:'threefold' name:'initial_investment'
descr:'Seed Capital'
investment:'0:5000000USD'
type:'capital'
!!bizmodel.department_define bizname:'threefold' name:'engineering' descr:'Engineering Department'
!!bizmodel.department_define bizname:'threefold' name:'operations' descr:'Operations Department'
!!bizmodel.department_define bizname:'threefold' name:'sales' descr:'Sales Department'
!!bizmodel.department_define bizname:'threefold' name:'admin' descr:'Admin Department'
!!bizmodel.employee_define bizname:'threefold' name:'engineer'
descr:'Engineer'
cost:'7000USD'
indexation:'5%'
nrpeople:'0:5,12:6,24:7,36:9,48:10,60:11,72:12,84:14,96:16,108:18,120:20'
department:'engineering'
!!bizmodel.employee_define bizname:'threefold' name:'operations_staff'
descr:'Operations Staff'
cost:'5000USD'
indexation:'5%'
nrpeople:'2'
department:'operations'
cost_percent_revenue:'4%'
!!bizmodel.cost_define bizname:'threefold' name:'travel'
descr:'Travel Costs'
cost:'0USD'
cost_percent_revenue:'3%'
!!bizmodel.cost_define bizname:'threefold' name:'office_rent'
descr:'Offices Rent (3 locations)'
cost:'24000USD'
indexation:'3%'

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.biz.bizmodel
import os
bizmodel.play(heroscript_path: '${os.dir(@FILE)}/bizmodel.heroscript')!
mut m := bizmodel.get("threefold")!
m.sheet.pprint(nr_columns: 5)!

View File

@@ -1,7 +1,7 @@
module builder module builder
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.osal.core.rsync import freeflowuniverse.herolib.osal.rsync
// import freeflowuniverse.herolib.core.pathlib // import freeflowuniverse.herolib.core.pathlib
import os import os

View File

@@ -3,7 +3,7 @@ module builder
import os import os
import rand import rand
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.osal.core.rsync import freeflowuniverse.herolib.osal.rsync
import freeflowuniverse.herolib.core.pathlib import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.data.ipaddress import freeflowuniverse.herolib.data.ipaddress
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console

View File

@@ -1,4 +1,4 @@
module done module core
import freeflowuniverse.herolib.core.base import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.data.dbfs import freeflowuniverse.herolib.data.dbfs

View File

@@ -1,4 +1,4 @@
module done module core
fn test_done_set() ! { fn test_done_set() ! {
done_set('mykey', 'myvalue')! done_set('mykey', 'myvalue')!