-- SSH utilities module for auto-boot-ollama-host -- Provides SSH command execution functionality local utils = require("utils") local config = require("config") local ssh_module = {} -- Quote a string for safe single-quoted POSIX shell context local function sq(s) -- Replace ' with: '\'' (close, escape quote, reopen) return "'" .. tostring(s):gsub("'", "'\\''") .. "'" end -- Helper function to log SSH commands with proper formatting local function log_ssh_command(prefix, command, full_command) if config.is_debug() then utils.log(prefix .. full_command) else utils.log(prefix .. sq(command)) end end -- Execute a remote command over SSH -- Signature: ssh(command, user, host, port, identity_file) function ssh_module.execute(command, user, host, port, identity_file) -- Basic validation and defaults user = tostring(user or "") host = tostring(host or "") port = tonumber(port or 22) or 22 identity_file = tostring(identity_file or "") -- Build base ssh command (run locally) -- -oBatchMode to avoid interactive prompts -- -oConnectTimeout for faster failure -- -oStrictHostKeyChecking uses known_hosts; adjust if needed local dest = (user ~= "" and (user .. "@" .. host) or host) local pieces = { "ssh", "-p", tostring(port), "-o", "BatchMode=yes", "-o", "ConnectTimeout=30", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=1", "-o", "UserKnownHostsFile=/root/.ssh/known_hosts", "-o", "StrictHostKeyChecking=yes", } if identity_file ~= "" then table.insert(pieces, "-i") table.insert(pieces, identity_file) end table.insert(pieces, dest) -- Pass remote command as provided; caller is responsible for proper quoting table.insert(pieces, "--") -- Quote the remote command to prevent shell interpretation of && and || table.insert(pieces, sq(command)) -- Join with spaces for os.execute local function join(args) -- We only quote the remote command explicitly. Other args are simple tokens. return table.concat(args, " ") end local full = join(pieces) -- Log SSH command log_ssh_command("SSH exec: ", command, full) local ok, reason, code = os.execute(full) if ok == true or ok == 0 then utils.log("SSH command completed successfully") return true else local msg = string.format("SSH failed: reason=%s code=%s", tostring(reason), tostring(code)) utils.log(msg) return false, msg end end -- Execute a remote command over SSH and return the output -- Signature: ssh.execute_with_output(command, user, host, port, identity_file) -- Returns: success, output, error_message function ssh_module.execute_with_output(command, user, host, port, identity_file) -- Basic validation and defaults user = tostring(user or "") host = tostring(host or "") port = tonumber(port or 22) or 22 identity_file = tostring(identity_file or "") -- Build base ssh command (run locally) local dest = (user ~= "" and (user .. "@" .. host) or host) local pieces = { "ssh", "-p", tostring(port), "-o", "BatchMode=yes", "-o", "ConnectTimeout=30", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=1", -- "-o", "UserKnownHostsFile=/root/.ssh/known_hosts", "-o", "StrictHostKeyChecking=no", } if identity_file ~= "" then table.insert(pieces, "-i") table.insert(pieces, identity_file) end table.insert(pieces, dest) -- Pass remote command as provided table.insert(pieces, "--") -- Quote the remote command to prevent shell interpretation of && and || table.insert(pieces, sq(command)) -- Join with spaces for io.popen local function join(args) return table.concat(args, " ") end local full = join(pieces) -- Log SSH command log_ssh_command("SSH exec (with output): ", command, full) -- Use io.popen to capture output local fh = io.popen(full, "r") if not fh then return false, "", "Failed to open SSH command" end local output = fh:read("*a") local success, reason, code = fh:close() if success then utils.log("SSH command completed successfully with output") return true, output, nil else local msg = string.format("SSH failed: reason=%s code=%s", tostring(reason), tostring(code)) utils.log(msg) return false, output, msg end end return ssh_module