Files
herolib/lib/virt/heropods/mycelium.v
2025-11-25 18:53:18 +01:00

365 lines
10 KiB
V

module heropods
import incubaid.herolib.osal.core as osal
import incubaid.herolib.clients.mycelium
import crypto.sha256
import time
// Initialize Mycelium for HeroPods
//
// This method:
// 1. Validates required configuration
// 2. Checks that Mycelium binary is installed
// 3. Checks that Mycelium service is running
// 4. Retrieves the host's Mycelium IPv6 address
//
// Prerequisites:
// - Mycelium must be installed on the system
// - Mycelium service must be running
//
// Thread Safety:
// This is called during HeroPods initialization, before any concurrent operations.
fn (mut self HeroPods) mycelium_init() ! {
if !self.mycelium_enabled {
return
}
// Validate required configuration
if self.mycelium_version == '' {
return error('Mycelium configuration error: "version" is required. Use heropods.enable_mycelium to configure.')
}
if self.mycelium_ipv6_range == '' {
return error('Mycelium configuration error: "ipv6_range" is required. Use heropods.enable_mycelium to configure.')
}
if self.mycelium_key_path == '' {
return error('Mycelium configuration error: "key_path" is required. Use heropods.enable_mycelium to configure.')
}
if self.mycelium_peers.len == 0 {
return error('Mycelium configuration error: "peers" is required. Use heropods.enable_mycelium to configure.')
}
self.logger.log(
cat: 'mycelium'
log: 'START mycelium_init() - Initializing Mycelium IPv6 overlay network'
) or {}
// Check if Mycelium is installed - it's a prerequisite
if !self.mycelium_check_installed()! {
return error('Mycelium is not installed. Please install Mycelium first. See: https://github.com/threefoldtech/mycelium')
}
self.logger.log(
cat: 'mycelium'
log: 'Mycelium binary found'
logtype: .stdout
) or {}
// Check if Mycelium service is running - it's a prerequisite
if !self.mycelium_check_running()! {
return error('Mycelium service is not running. Please start Mycelium service first (e.g., mycelium --key-file ${self.mycelium_key_path} --peers <peers>)')
}
self.logger.log(
cat: 'mycelium'
log: 'Mycelium service is running'
logtype: .stdout
) or {}
// Get and cache the host's Mycelium IPv6 address
self.mycelium_get_host_address()!
self.logger.log(
cat: 'mycelium'
log: 'END mycelium_init() - Mycelium initialized successfully with address ${self.mycelium_ip6}'
logtype: .stdout
) or {}
}
// Check if Mycelium binary is installed
fn (mut self HeroPods) mycelium_check_installed() !bool {
return osal.cmd_exists('mycelium')
}
// Check if Mycelium service is running
fn (mut self HeroPods) mycelium_check_running() !bool {
// Try to inspect Mycelium - if it succeeds, it's running
mycelium.inspect(key_file_path: self.mycelium_key_path) or { return false }
return true
}
// Get the host's Mycelium IPv6 address
fn (mut self HeroPods) mycelium_get_host_address() ! {
self.logger.log(
cat: 'mycelium'
log: 'Retrieving host Mycelium IPv6 address...'
logtype: .stdout
) or {}
// Use mycelium inspect to get the address
inspect_result := mycelium.inspect(key_file_path: self.mycelium_key_path)!
if inspect_result.address == '' {
return error('Failed to get Mycelium IPv6 address from inspect')
}
self.mycelium_ip6 = inspect_result.address
self.logger.log(
cat: 'mycelium'
log: 'Host Mycelium IPv6 address: ${self.mycelium_ip6}'
logtype: .stdout
) or {}
}
// Setup Mycelium IPv6 networking for a container
//
// This method:
// 1. Creates a veth pair for Mycelium connectivity
// 2. Moves one end into the container's network namespace
// 3. Assigns a Mycelium IPv6 address to the container
// 4. Configures IPv6 forwarding and routing
//
// Thread Safety:
// This is called from container.start() which is already serialized per container.
// Multiple containers can be started concurrently, each with their own veth pair.
fn (mut self HeroPods) mycelium_setup_container(container_name string, container_pid int) ! {
if !self.mycelium_enabled {
return
}
self.logger.log(
cat: 'mycelium'
log: 'Setting up Mycelium IPv6 for container ${container_name} (PID: ${container_pid})'
logtype: .stdout
) or {}
// Create unique veth pair names using hash (same pattern as IPv4 networking)
short_hash := sha256.hexhash(container_name)[..6]
veth_container := 'vmy-${short_hash}'
veth_host := 'vmyh-${short_hash}'
// Delete veth pair if it already exists (cleanup from previous run)
osal.exec(cmd: 'ip link delete ${veth_container} 2>/dev/null', stdout: false) or {}
osal.exec(cmd: 'ip link delete ${veth_host} 2>/dev/null', stdout: false) or {}
// Create veth pair
self.logger.log(
cat: 'mycelium'
log: 'Creating veth pair: ${veth_container} <-> ${veth_host}'
logtype: .stdout
) or {}
osal.exec(
cmd: 'ip link add ${veth_container} type veth peer name ${veth_host}'
stdout: false
)!
// Bring up host end
osal.exec(
cmd: 'ip link set ${veth_host} up'
stdout: false
)!
// Move container end into container's network namespace
self.logger.log(
cat: 'mycelium'
log: 'Moving ${veth_container} into container namespace'
logtype: .stdout
) or {}
osal.exec(
cmd: 'ip link set ${veth_container} netns ${container_pid}'
stdout: false
)!
// Configure container end inside the namespace
// Bring up the interface
osal.exec(
cmd: 'nsenter -t ${container_pid} -n ip link set ${veth_container} up'
stdout: false
)!
// Get the Mycelium IPv6 prefix from the host
// Extract the prefix from the full address (e.g., "400:1234:5678::/64" from "400:1234:5678::1")
mycelium_prefix := self.mycelium_get_ipv6_prefix()!
// Assign IPv6 address to container (use ::1 in the subnet)
container_ip6 := '${mycelium_prefix}::1/64'
self.logger.log(
cat: 'mycelium'
log: 'Assigning IPv6 address ${container_ip6} to container'
logtype: .stdout
) or {}
osal.exec(
cmd: 'nsenter -t ${container_pid} -n ip addr add ${container_ip6} dev ${veth_container}'
stdout: false
)!
// Enable IPv6 forwarding on the host
self.logger.log(
cat: 'mycelium'
log: 'Enabling IPv6 forwarding'
logtype: .stdout
) or {}
osal.exec(
cmd: 'sysctl -w net.ipv6.conf.all.forwarding=1'
stdout: false
) or {
self.logger.log(
cat: 'mycelium'
log: 'Warning: Failed to enable IPv6 forwarding: ${err}'
logtype: .error
) or {}
osal.Job{}
}
// Get the link-local address of the host end of the veth pair
veth_host_ll := self.mycelium_get_link_local_address(veth_host)!
// Add route in container for Mycelium traffic (400::/7 via link-local)
self.logger.log(
cat: 'mycelium'
log: 'Adding route for ${self.mycelium_ipv6_range} via ${veth_host_ll}'
logtype: .stdout
) or {}
osal.exec(
cmd: 'nsenter -t ${container_pid} -n ip route add ${self.mycelium_ipv6_range} via ${veth_host_ll} dev ${veth_container}'
stdout: false
)!
// Add route on host for container's IPv6 address
self.logger.log(
cat: 'mycelium'
log: 'Adding host route for ${mycelium_prefix}::1/128'
logtype: .stdout
) or {}
osal.exec(
cmd: 'ip route add ${mycelium_prefix}::1/128 dev ${veth_host}'
stdout: false
)!
self.logger.log(
cat: 'mycelium'
log: 'Mycelium IPv6 setup complete for container ${container_name}'
logtype: .stdout
) or {}
}
// Get the IPv6 prefix from the host's Mycelium address
//
// Extracts the /64 prefix from the full IPv6 address
// Example: "400:1234:5678::1" -> "400:1234:5678:"
fn (mut self HeroPods) mycelium_get_ipv6_prefix() !string {
if self.mycelium_ip6 == '' {
return error('Mycelium IPv6 address not set')
}
// Split the address by ':' and take the first 3 parts for /64 prefix
parts := self.mycelium_ip6.split(':')
if parts.len < 3 {
return error('Invalid Mycelium IPv6 address format: ${self.mycelium_ip6}')
}
// Reconstruct the prefix (first 3 parts)
prefix := '${parts[0]}:${parts[1]}:${parts[2]}'
return prefix
}
// Get the link-local IPv6 address of an interface
//
// Link-local addresses are used for routing within the same network segment
// They start with fe80::
fn (mut self HeroPods) mycelium_get_link_local_address(interface_name string) !string {
self.logger.log(
cat: 'mycelium'
log: 'Getting link-local address for interface ${interface_name}'
logtype: .stdout
) or {}
// Get IPv6 addresses for the interface
cmd := "ip -6 addr show dev ${interface_name} | grep 'inet6 fe80' | awk '{print \$2}' | cut -d'/' -f1"
result := osal.exec(
cmd: cmd
stdout: false
)!
link_local := result.output.trim_space()
if link_local == '' {
return error('Failed to get link-local address for interface ${interface_name}')
}
self.logger.log(
cat: 'mycelium'
log: 'Link-local address for ${interface_name}: ${link_local}'
logtype: .stdout
) or {}
return link_local
}
// Cleanup Mycelium networking for a container
//
// This method:
// 1. Removes the veth pair
// 2. Removes routes
//
// Thread Safety:
// This is called from container.stop() and container.delete() which are serialized per container.
fn (mut self HeroPods) mycelium_cleanup_container(container_name string) ! {
if !self.mycelium_enabled {
return
}
self.logger.log(
cat: 'mycelium'
log: 'Cleaning up Mycelium IPv6 for container ${container_name}'
logtype: .stdout
) or {}
// Remove veth interfaces (they should be auto-removed when container stops, but cleanup anyway)
short_hash := sha256.hexhash(container_name)[..6]
veth_host := 'vmyh-${short_hash}'
osal.exec(
cmd: 'ip link delete ${veth_host} 2>/dev/null'
stdout: false
) or {}
// Remove host route (if it exists)
mycelium_prefix := self.mycelium_get_ipv6_prefix() or {
self.logger.log(
cat: 'mycelium'
log: 'Warning: Could not get Mycelium prefix for cleanup: ${err}'
logtype: .error
) or {}
return
}
osal.exec(
cmd: 'ip route del ${mycelium_prefix}::1/128 2>/dev/null'
stdout: false
) or {}
self.logger.log(
cat: 'mycelium'
log: 'Mycelium IPv6 cleanup complete for container ${container_name}'
logtype: .stdout
) or {}
}
// Inspect Mycelium status and return information
//
// Returns the public key and IPv6 address of the Mycelium node
pub fn (mut self HeroPods) mycelium_inspect() !mycelium.MyceliumInspectResult {
if !self.mycelium_enabled {
return error('Mycelium is not enabled')
}
return mycelium.inspect(key_file_path: self.mycelium_key_path)!
}