SSH 除了可以直接连接远程服务器,以终端形式显示外,也可以直接用来执行命令,只需要把命令放到 ssh 的参数后面即可,例如:
ssh root@example "echo Hello"
这对于执行一些临时命令,例如运行备份之类的,还挺有用。
对于很长的或者多个命令,可以使用 << 多行文本,例如
ssh root@example << HERE echo "HELLO" echo $USER HERE
但是对于上面的第二个命令,$USER,存在转义问题,$USER 会在本地执行,而不是在服务器上。
解决方式是可以添加引号来避免本地提前解释执行。
ssh root@example << 'HERE' echo "HELLO" echo $USER HERE
此处是来自 POSIX 的规范,linux 和 mac 同样适用,具体出处我也不清楚,可以参考文章底部最后一个链接,和自行 ai 搜索
还有一种解决方法,也是我想说的,来自今天搜索 rpcssh,发现的一个十多年的帖子。
declare 命令可以返回变量或者方法的定义,使用这种方法,就可以自己不用写相关转义了。例如本地有个下面的方法:
hello() { echo "Hello, world, I'm coming from $(uname -n)."}
可以这样执行
ssh root@example"$(declare -f hello); hello"
$(declare -f hello) 会自动把本地定义的这个hello方法的内容获取出来,并且发送到目标服务器时自动转义,然后在远程服务器执行这个hello方法。
注意这种方法只能用于 shell 中定义的方法,不能是二进制文件
原作者写了一个叫rpcsh的方法,用于把本地方法发送到远程,挺简洁方便的,我就直接放在下面了
# rpcsh -- Runs a function on a remote host# This function pushes out a given set of variables and functions to# another host via ssh, then runs a given function with optional arguments.# Usage:# rpcsh -h remote_host [ -p ssh-port ] -u remote_login -v "variable list" \# -f "function list" -m mainfunc## The "function list" is a list of shell functions to push to the remote host# (including the main function to execute, and any functions that it calls)# Use the "variable list" to send a group of variables to the remote host.# Finally "mainfunc" is the name of the function (from "function list") # to execute on the remote side. Any additional parameters specified gets# passed along to mainfunc.rpcsh() { if ! args=("$(getopt -l "rmthost:,rmthostport:,rmtlogin:,pushvars:,pushfuncs:,rmtmain:" -o "h:p:u:v:f:m:A" -- "$@")") then exit 1 fi sshvars=( -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ) eval set -- "${args[@]}" while [ -n "$1" ] do case $1 in -h|--rmthost) rmthost=$2; shift; shift;; -p|--rmtport) sshvars=( "${sshvars[@]}" -p $2 ); shift; shift;; -u|--rmtlogin) rmtlogin=$2; shift; shift;; -v|--pushvars) pushvars=$2; shift; shift;; -f|--pushfuncs) pushfuncs=$2; shift; shift;; -m|--rmtmain) rmtmain=$2; shift; shift;; -A) sshvars=( "${sshvars[@]}" -A ); shift;; -i) sshvars=( "${sshvars[@]}" -i $2 ); shift; shift;; --) shift; break;; esac done rmtargs=( "$@" ) ssh ${sshvars[@]} ${rmtlogin}@${rmthost} " $(declare -p rmtargs 2>/dev/null) $([ -n "$pushvars" ] && declare -p $pushvars 2>/dev/null) $(declare -f $pushfuncs 2>/dev/null) $rmtmain \"\${rmtargs[@]}\" "}
原地址 https://gist.github.com/derekp7/9978986
有什么用呢,如果你有多个服务器,需要维护什么备份脚本什么,按照传统的方法,比如把脚本上传到服务器,或者git,当你的脚本更新时候,所有的服务器上的文件都需要更新,你需要一个个登录过去获取最新文件,这显然不是很方便。
当然你可以使用类似宝塔面板这种,可以在一个地方控制好几个服务器,这显然又增加了复杂性,增加了依赖。
还有就是一个干净服务器,通常需要装一些东西,进行初始化,例如我想安装docker,fail2ban,然后配置相关参数,每次都要这么设置都挺费事的,就可以用上面的方法。
例如下面的脚本
#!/bin/bashset -euo pipefail# 定义远程服务器列表(可根据实际需求修改)REMOTE_SERVERS=("192.168.2.1" "192.168.2.2")# 默认SSH端口DEFAULT_SSH_PORT=22# 默认SSH登录用户DEFAULT_SSH_USER="root"# ===================== 核心远程执行函数 =====================rpcsh() { local rmthost="" rmtport="${DEFAULT_SSH_PORT}" rmtlogin="${DEFAULT_SSH_USER}" local pushvars="" pushfuncs="" rmtmain="" sshvars=() rmtargs=() local use_ssh_agent=0 ssh_identity="" # 解析命令行参数 if ! args=$(getopt -o h:p:u:v:f:m:Ai: --long rmthost:,rmthostport:,rmtlogin:,pushvars:,pushfuncs:,rmtmain: -- "$@"); then echo "ERROR: 参数解析失败" >&2 return 1 fi eval set -- "${args}" sshvars=( -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 ) while [[ $# -gt 0 ]]; do case "$1" in -h|--rmthost) rmthost="$2" shift 2 ;; -p|--rmtport) rmtport="$2" sshvars+=( "-p" "$2" ) shift 2 ;; -u|--rmtlogin) rmtlogin="$2" shift 2 ;; -v|--pushvars) pushvars="$2" shift 2 ;; -f|--pushfuncs) pushfuncs="$2" shift 2 ;; -m|--rmtmain) rmtmain="$2" shift 2 ;; -A) use_ssh_agent=1 sshvars+=( "-A" ) shift ;; -i) ssh_identity="$2" sshvars+=( "-i" "$2" ) shift 2 ;; --) shift rmtargs=( "$@" ) break ;; *) echo "ERROR: 未知参数 $1" >&2 return 1 ;; esac done # 校验必要参数 if [[ -z "${rmthost}" || -z "${rmtmain}" ]]; then echo "ERROR: 必须指定远程主机(--rmthost/-h)和执行函数(--rmtmain/-m)" >&2 return 1 fi # 构建远程执行脚本 local remote_script="" # 传递参数数组 remote_script+="$(declare -p rmtargs 2>/dev/null); " # 传递指定变量 if [[ -n "${pushvars}" ]]; then remote_script+="$(declare -p ${pushvars} 2>/dev/null); " fi # 传递指定函数 if [[ -n "${pushfuncs}" ]]; then remote_script+="$(declare -f ${pushfuncs} 2>/dev/null); " fi # 执行主函数 remote_script+="${rmtmain} \"\${rmtargs[@]}\"" # 执行SSH远程命令 echo "INFO: 连接到 ${rmtlogin}@${rmthost}:${rmtport} 执行 ${rmtmain}" ssh "${sshvars[@]}" "${rmtlogin}@${rmthost}" "${remote_script}"}# ===================== 业务功能函数 =====================# 安装Docker(含Docker Compose)install_docker() { echo "===== 开始安装Docker =====" if command -v docker &>/dev/null; then echo "INFO: Docker已安装,跳过安装步骤" return 0 fi # 卸载旧版本 apt-get remove -y docker docker-engine docker.io containerd runc || true # 设置仓库 apt-get update apt-get install -y ca-certificates curl gnupg lsb-release mkdir -p /etc/apt/trusted.gpg.d curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/trusted.gpg.d/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null # 安装Docker Engine apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # 启动并开机自启 systemctl enable --now docker # 验证安装 if docker --version &>/dev/null; then echo "SUCCESS: Docker安装完成" else echo "ERROR: Docker安装失败" >&2 return 1 fi}# 安装fail2baninstall_fail2ban() { echo "===== 开始安装fail2ban =====" if command -v fail2ban-server &>/dev/null; then echo "INFO: fail2ban已安装,跳过安装步骤" return 0 fi apt-get update apt-get install -y fail2ban systemctl enable --now fail2ban if fail2ban-server --version &>/dev/null; then echo "SUCCESS: fail2ban安装完成" else echo "ERROR: fail2ban安装失败" >&2 return 1 fi}# 配置fail2ban(SSH防护为例)config_fail2ban() { echo "===== 开始配置fail2ban =====" local config_file="/etc/fail2ban/jail.local" # 备份原有配置 if [[ -f "${config_file}" ]]; then cp "${config_file}" "${config_file}.bak.$(date +%Y%m%d%H%M%S)" fi # 写入基础配置 cat > "${config_file}" << EOF[DEFAULT]ignoreip = 127.0.0.1/8 192.168.0.0/16bantime = 86400findtime = 3600maxretry = 5banaction = iptables-multiportbackend = systemd[sshd]enabled = trueport = sshfilter = sshdlogpath = /var/log/auth.logmaxretry = 3EOF # 重启服务 systemctl restart fail2ban if systemctl is-active --quiet fail2ban; then echo "SUCCESS: fail2ban配置完成并已重启" else echo "ERROR: fail2ban配置后启动失败" >&2 return 1 fi}# 服务器备份(示例:备份/etc目录到/backup)server_backup() { echo "===== 开始服务器备份 =====" local backup_dir="/backup/$(date +%Y%m%d)" local backup_file="${backup_dir}/etc_backup.tar.gz" # 创建备份目录 mkdir -p "${backup_dir}" # 备份/etc目录 tar -zcf "${backup_file}" /etc \ --exclude=/etc/ssh/ssh_host_* \ --exclude=/etc/machine-id \ --exclude=/etc/resolv.conf # 校验备份文件 if [[ -f "${backup_file}" && $(stat -c%s "${backup_file}") -gt 1024 ]]; then echo "SUCCESS: 备份完成,文件路径: ${backup_file}" # 可选:保留最近7天备份 find /backup -type d -mtime +7 -delete else echo "ERROR: 备份失败" >&2 return 1 fi}# ===================== 脚本入口/帮助信息 =====================# 显示帮助信息show_help() { cat << EOF服务器管理脚本 - server_manager用法: $0 [选项] <命令> [命令参数]可用命令: install_docker 安装Docker及Docker Compose install_fail2ban 安装fail2ban config_fail2ban 配置fail2ban(SSH防护) server_backup 执行服务器备份(默认备份/etc目录) help 显示此帮助信息远程执行选项: -r 远程执行模式(需配合以下参数) -h <IP> 远程服务器IP(必填) -p <端口> 远程SSH端口(默认:22) -u <用户> 远程SSH登录用户(默认:root) -i <密钥文件> SSH私钥文件路径 -A 启用SSH Agent转发示例: # 本地执行备份 $0 server_backup # 远程执行备份(单个服务器) $0 -r -h 192.168.2.1 server_backup # 远程执行Docker安装(指定用户和端口) $0 -r -h 192.168.2.2 -u ubuntu -p 2222 install_docker # 显示帮助 $0 helpEOF}# 主执行逻辑main() { local remote_mode=0 local remote_host="" remote_port="${DEFAULT_SSH_PORT}" remote_user="${DEFAULT_SSH_USER}" local ssh_identity="" use_agent=0 local command="" command_args=() # 解析顶层参数 while [[ $# -gt 0 ]]; do case "$1" in -r) remote_mode=1 shift ;; -h) remote_host="$2" shift 2 ;; -p) remote_port="$2" shift 2 ;; -u) remote_user="$2" shift 2 ;; -i) ssh_identity="$2" shift 2 ;; -A) use_agent=1 shift ;; help|--help|-h) show_help exit 0 ;; *) # 第一个非选项参数作为命令,剩余作为命令参数 command="$1" shift command_args=( "$@" ) break ;; esac done # 校验命令是否为空 if [[ -z "${command}" ]]; then echo "ERROR: 必须指定要执行的命令" >&2 show_help exit 1 fi # 校验命令是否存在 if ! declare -f "${command}" &>/dev/null; then echo "ERROR: 未知命令 '${command}'" >&2 show_help exit 1 fi # 执行逻辑:本地/远程 if [[ ${remote_mode} -eq 1 ]]; then # 远程执行模式 if [[ -z "${remote_host}" ]]; then echo "ERROR: 远程执行模式必须指定 -h <远程服务器IP>" >&2 exit 1 fi # 构建rpcsh参数 local rpcsh_args=( -h "${remote_host}" -p "${remote_port}" -u "${remote_user}" -m "${command}" -f "${command}" # 传递要执行的函数到远程 ) [[ -n "${ssh_identity}" ]] && rpcsh_args+=( -i "${ssh_identity}" ) [[ ${use_agent} -eq 1 ]] && rpcsh_args+=( -A ) rpcsh_args+=( -- "${command_args[@]}" ) # 执行远程调用 rpcsh "${rpcsh_args[@]}" else # 本地执行模式 echo "INFO: 本地执行命令 ${command}" "${command}" "${command_args[@]}" fi}# 启动主程序main "$@"
之后就可以按照下面方法使用
# 显示帮助(可用命令)server_manager help# 本地执行备份server_manager server_backup# 远程执行备份server_manager -r -h 192.168.2.1 server_backup# 远程执行Docker安装(指定用户、端口、SSH密钥)server_manager -r -h 192.168.2.2 install_docker# 远程配置fail2ban(启用SSH Agent转发)server_manager -r -h 192.168.2.1 config_fail2ban
注意的是上面脚本我是ai生成的,我只是提供了思路,请不要直接使用。
服务器可以自己本地维护一个列表,输入-r时给出列表选择哪个服务器执行,就可以一个脚本管理多个服务器了。
像是后面脚本新增功能,哪里代码有问题,都只需要改本地的。也可以本地用定时任务定时执行,不需要每个服务器都部署定时任务。
虽然性能上肯定有损耗,但是好在没有任何第三方依赖,所有linux mac都能用,啥第三方软件也不用装(当然还是要openssh)
参考文章