This commit is contained in:
2025-08-24 14:47:48 +02:00
parent e7a36f47e8
commit 1dd8c29735
4 changed files with 310 additions and 2 deletions

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.osal.sshagent
import freeflowuniverse.herolib.builder
import freeflowuniverse.herolib.ui.console
console.print_header('SSH Agent Management Example')
// Create SSH agent with single instance guarantee
mut agent := sshagent.new_single()!
println('SSH Agent initialized and ensured single instance')
// Show diagnostics
diag := agent.diagnostics()
console.print_header('SSH Agent Diagnostics:')
for key, value in diag {
console.print_item('${key}: ${value}')
}
// Show current agent status
println(agent)
// Example: Generate a test key if no keys exist
if agent.keys.len == 0 {
console.print_header('No keys found, generating example key...')
mut key := agent.generate('example_key', '')!
console.print_debug('Generated key: ${key}')
// Load the generated key
key.load()!
console.print_debug('Key loaded into agent')
}
// Example: Push key to remote node (uncomment and modify for actual use)
/*
console.print_header('Testing remote node key deployment...')
mut b := builder.new()!
// Create connection to remote node
mut node := b.node_new(
ipaddr: 'root@192.168.1.100:22' // Replace with actual remote host
name: 'test_node'
)!
if agent.keys.len > 0 {
key_name := agent.keys[0].name
console.print_debug('Pushing key "${key_name}" to remote node...')
// Push the key
agent.push_key_to_node(mut node, key_name)!
// Verify access
if agent.verify_key_access(mut node, key_name)! {
console.print_debug('✓ SSH key access verified')
} else {
console.print_debug('✗ SSH key access verification failed')
}
// Optional: Remove key from remote (for testing)
// agent.remove_key_from_node(mut node, key_name)!
// console.print_debug('Key removed from remote node')
}
*/
console.print_header('SSH Agent example completed successfully')

View File

@@ -0,0 +1,100 @@
module sshagent
import freeflowuniverse.herolib.builder
import freeflowuniverse.herolib.ui.console
// push SSH public key to a remote node's authorized_keys
pub fn (mut agent SSHAgent) push_key_to_node(mut node builder.Node, key_name string) ! {
// Verify this is an SSH node
node_info := node.info()
if node_info['category'] != 'ssh' {
return error('Can only push keys to SSH nodes, got: ${node_info['category']}')
}
// Find the key
mut key := agent.get(name: key_name) or {
return error('SSH key "${key_name}" not found in agent')
}
// Get public key content
pubkey_content := key.keypub()!
// Check if authorized_keys file exists on remote
home_dir := node.environ_get()!['HOME'] or {
return error('Could not determine HOME directory on remote node')
}
ssh_dir := '${home_dir}/.ssh'
authorized_keys_path := '${ssh_dir}/authorized_keys'
// Ensure .ssh directory exists with correct permissions
node.exec_silent('mkdir -p ${ssh_dir}')!
node.exec_silent('chmod 700 ${ssh_dir}')!
// Check if key already exists
if node.file_exists(authorized_keys_path) {
existing_keys := node.file_read(authorized_keys_path)!
if existing_keys.contains(pubkey_content.trim_space()) {
console.print_debug('SSH key already exists on remote node')
return
}
}
// Add key to authorized_keys
node.exec_silent('echo "${pubkey_content}" >> ${authorized_keys_path}')!
node.exec_silent('chmod 600 ${authorized_keys_path}')!
console.print_debug('SSH key "${key_name}" successfully pushed to node')
}
// remove SSH public key from a remote node's authorized_keys
pub fn (mut agent SSHAgent) remove_key_from_node(mut node builder.Node, key_name string) ! {
// Verify this is an SSH node
node_info := node.info()
if node_info['category'] != 'ssh' {
return error('Can only remove keys from SSH nodes, got: ${node_info['category']}')
}
// Find the key
mut key := agent.get(name: key_name) or {
return error('SSH key "${key_name}" not found in agent')
}
// Get public key content
pubkey_content := key.keypub()!
// Get authorized_keys path
home_dir := node.environ_get()!['HOME'] or {
return error('Could not determine HOME directory on remote node')
}
authorized_keys_path := '${home_dir}/.ssh/authorized_keys'
if !node.file_exists(authorized_keys_path) {
console.print_debug('authorized_keys file does not exist on remote node')
return
}
// Remove the key line from authorized_keys
escaped_key := pubkey_content.replace('/', '\\/')
node.exec_silent('sed -i "\\|${escaped_key}|d" ${authorized_keys_path}')!
console.print_debug('SSH key "${key_name}" removed from remote node')
}
// verify SSH key access to remote node
pub fn (mut agent SSHAgent) verify_key_access(mut node builder.Node, key_name string) !bool {
// This would attempt to connect with the specific key
// For now, we'll do a simple connectivity test
node_info := node.info()
if node_info['category'] != 'ssh' {
return error('Can only verify access to SSH nodes')
}
// Test basic connectivity
result := node.exec_silent('echo "SSH key verification successful"') or {
return false
}
return result.contains('SSH key verification successful')
}

View File

@@ -30,3 +30,16 @@ pub fn loaded() bool {
mut agent := new() or { panic(err) }
return agent.active
}
// create new SSH agent with single instance guarantee
pub fn new_single(args_ SSHAgentNewArgs) !SSHAgent {
mut agent := new(args_)!
agent.ensure_single_agent()!
return agent
}
// check if SSH agent is properly configured and running
pub fn agent_status() !map[string]string {
mut agent := new()!
return agent.diagnostics()
}

View File

@@ -2,7 +2,7 @@ module sshagent
import os
import freeflowuniverse.herolib.core.pathlib
// import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct SSHAgent {
@@ -12,9 +12,139 @@ pub mut:
homepath pathlib.Path
}
// ensure only one SSH agent is running for the current user
pub fn (mut agent SSHAgent) ensure_single_agent() ! {
user := os.getenv('USER')
socket_path := get_agent_socket_path(user)
// Check if we have a valid agent already
if agent.is_agent_responsive() {
console.print_debug('SSH agent already running and responsive')
return
}
// Kill any orphaned agents
agent.cleanup_orphaned_agents()!
// Start new agent with consistent socket
agent.start_agent_with_socket(socket_path)!
// Set environment variables
os.setenv('SSH_AUTH_SOCK', socket_path, true)
agent.active = true
}
// get consistent socket path per user
fn get_agent_socket_path(user string) string {
return '/tmp/ssh-agent-${user}.sock'
}
// check if current agent is responsive
pub fn (mut agent SSHAgent) is_agent_responsive() bool {
if os.getenv('SSH_AUTH_SOCK') == '' {
return false
}
res := os.execute('ssh-add -l 2>/dev/null')
return res.exit_code == 0 || res.exit_code == 1 // 1 means no keys, but agent is running
}
// cleanup orphaned ssh-agent processes
pub fn (mut agent SSHAgent) cleanup_orphaned_agents() ! {
user := os.getenv('USER')
// Find ssh-agent processes for current user
res := os.execute('pgrep -u ${user} ssh-agent')
if res.exit_code == 0 && res.output.len > 0 {
pids := res.output.trim_space().split('\n')
for pid in pids {
if pid.trim_space() != '' {
// Check if this agent has a valid socket
if !agent.is_agent_pid_valid(pid.int()) {
console.print_debug('Killing orphaned ssh-agent PID: ${pid}')
os.execute('kill ${pid}')
}
}
}
}
}
// check if specific agent PID is valid and responsive
fn (mut agent SSHAgent) is_agent_pid_valid(pid int) bool {
// Try to find socket for this PID
res := os.execute('find /tmp -name "agent.*" -user ${os.getenv('USER')} 2>/dev/null | head -10')
if res.exit_code != 0 {
return false
}
for socket_path in res.output.split('\n') {
if socket_path.trim_space() != '' {
// Test if this socket responds
old_sock := os.getenv('SSH_AUTH_SOCK')
os.setenv('SSH_AUTH_SOCK', socket_path, true)
test_res := os.execute('ssh-add -l 2>/dev/null')
os.setenv('SSH_AUTH_SOCK', old_sock, true)
if test_res.exit_code == 0 || test_res.exit_code == 1 {
return true
}
}
}
return false
}
// start new ssh-agent with specific socket path
pub fn (mut agent SSHAgent) start_agent_with_socket(socket_path string) ! {
// Remove existing socket if it exists
if os.exists(socket_path) {
os.rm(socket_path)!
}
// Start ssh-agent with specific socket
cmd := 'ssh-agent -a ${socket_path}'
res := os.execute(cmd)
if res.exit_code != 0 {
return error('Failed to start ssh-agent: ${res.output}')
}
// Verify socket was created
if !os.exists(socket_path) {
return error('SSH agent socket was not created at ${socket_path}')
}
// Set environment variable
os.setenv('SSH_AUTH_SOCK', socket_path, true)
// Verify agent is responsive
if !agent.is_agent_responsive() {
return error('SSH agent started but is not responsive')
}
console.print_debug('SSH agent started with socket: ${socket_path}')
}
// get agent status and diagnostics
pub fn (mut agent SSHAgent) diagnostics() map[string]string {
mut diag := map[string]string{}
diag['socket_path'] = os.getenv('SSH_AUTH_SOCK')
diag['socket_exists'] = os.exists(diag['socket_path']).str()
diag['agent_responsive'] = agent.is_agent_responsive().str()
diag['loaded_keys_count'] = agent.keys.filter(it.loaded).len.str()
diag['total_keys_count'] = agent.keys.len.str()
// Count running ssh-agent processes
user := os.getenv('USER')
res := os.execute('pgrep -u ${user} ssh-agent | wc -l')
diag['agent_processes'] = if res.exit_code == 0 { res.output.trim_space() } else { '0' }
return diag
}
// get all keys from sshagent and from the local .ssh dir
pub fn (mut agent SSHAgent) init() ! {
// first get keys out of ssh-add
// first get keys out of ssh-add
agent.keys = []SSHKey{}
res := os.execute('ssh-add -L')
if res.exit_code == 0 {