From 1dd8c29735ba66281bdb0de29b0165583c397e6f Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 24 Aug 2025 14:47:48 +0200 Subject: [PATCH] ... --- examples/osal/sshagent.vsh | 65 ++++++++++++ lib/osal/sshagent/builder_integration.v | 100 ++++++++++++++++++ lib/osal/sshagent/factory.v | 13 +++ lib/osal/sshagent/sshagent.v | 134 +++++++++++++++++++++++- 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 examples/osal/sshagent.vsh create mode 100644 lib/osal/sshagent/builder_integration.v diff --git a/examples/osal/sshagent.vsh b/examples/osal/sshagent.vsh new file mode 100644 index 00000000..5bcc6bb9 --- /dev/null +++ b/examples/osal/sshagent.vsh @@ -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') \ No newline at end of file diff --git a/lib/osal/sshagent/builder_integration.v b/lib/osal/sshagent/builder_integration.v new file mode 100644 index 00000000..f85bf3d9 --- /dev/null +++ b/lib/osal/sshagent/builder_integration.v @@ -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') +} \ No newline at end of file diff --git a/lib/osal/sshagent/factory.v b/lib/osal/sshagent/factory.v index 32e9b427..71c9c33e 100644 --- a/lib/osal/sshagent/factory.v +++ b/lib/osal/sshagent/factory.v @@ -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() +} diff --git a/lib/osal/sshagent/sshagent.v b/lib/osal/sshagent/sshagent.v index 6cc30d90..eb14178c 100644 --- a/lib/osal/sshagent/sshagent.v +++ b/lib/osal/sshagent/sshagent.v @@ -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 {