[Linux] SSH列表选择登录与文件传送
系统安装fzf, rsync和sshpass
保存以下脚本, 赋予执行权限并放在$PATH路径下
#!/usr/bin/env python
"""
sys require: fzf rsync sshpass
登录:
sshc # 选择登录服务器(fzf)
sshc <名称> # 指定名称登录服务器
sshc . # 登录上次登录的服务器
文件传送:
sshc push -l <本地文件路径> -r <服务器文件路径> # 选择服务器上传文件
sshc pull -l <本地文件路径> -r <服务器文件路径> # 选择服务器下载文件
sshc pull --name <名称> -l <本地文件路径> -r <服务器文件路径> # 指定服务器下载文件
建议使用公钥而非密码登录
"""
import sys
from pathlib import Path
import json
import subprocess
conf_file = Path.home() / '.ssh' / 'ssh_server.json'
last_login_file = Path.home() / '.ssh' / 'last_login'
# 获取服务器列表配置
def get_config():
if not Path(conf_file).exists:
# 配置文件内容示例
example = """
{
"servers": [
{"enable": 0, "name": "termux", "user": "u0_a355", "host": "192.168.31.239", "port": "8022", "passwd": ""},
{"enable": 0, "name": "sshd4a", "user": "v", "host": "192.168.31.239", "port": "2222", "passwd": ""},
{"enable": 0, "name": "laptop", "user": "root", "host": "192.168.31.189", "port": "22", "passwd": ""}
]
}
"""
with open(conf_file, 'w') as f:
f.write(example)
print(f'请在{conf_file}中配置services')
exit(1)
with open(conf_file, 'r+') as f:
return json.load(f)
cfg = get_config()
objs = [obj for obj in cfg['servers'] if obj['enable'] == 1] # 只收集启用的服务器
# 使用fzf选择一个服务器
def chose_server():
names = [i["name"] for i in objs]
selected_name = run_fzf_cmd(names)
if not selected_name:
exit(0)
return get_server(selected_name)
# 指定名称获取服务器
def get_server(name):
obj = next((d for d in objs if d.get("name") == name and d.get("enable") == 1), None)
assert (obj), f"未找到配置: {name}"
assert (obj.get('user')), f"未配置user: {name}"
assert (obj.get('host')), f"未配置host: {name}"
port = obj.get('port')
if not port:
obj['port'] = '22'
return obj
# 使用fzf选择服务器
def run_fzf_cmd(options):
options_str = "\n".join(options)
process = subprocess.Popen(
["fzf", "--reverse"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True # 设置为True以便处理文本而不是字节
)
selected, _ = process.communicate(input=options_str)
return selected.strip()
# 使用fzf的python包装选择服务器
def fzf_chose(options):
# pip install pyfzf
from pyfzf.pyfzf import FzfPrompt
fzf = FzfPrompt()
selected_name = fzf.prompt(options, "--reverse")
if not selected_name:
exit(0)
return selected_name[0]
# 获取上次登录服务器
def last_login():
if Path(last_login_file).exists():
with open(last_login_file, 'r') as f:
return f.read()
return None
# 记录本次登录服务器
def record_login(name):
# 保存最后一次登录server
last = last_login()
if last != name:
with open(last_login_file, 'w') as f:
f.write(name)
# 登录一个服务器
def ssh(obj):
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd, 'ssh', '-o', 'StrictHostKeyChecking=no', '-p', obj["port"], f'{obj["user"]}@{obj["host"]}']
else:
command = ['ssh', '-o', 'StrictHostKeyChecking=no', '-p', obj["port"], f'{obj["user"]}@{obj["host"]}']
record_login(obj['name'])
subprocess.run(command)
# 使用rsync传送文件
def rsync(obj, local_path, remot_path, type='pull'):
remote_path = f'{obj["user"]}@{obj["host"]}:{remot_path}'
param_tuple = (local_path, remote_path) if type == 'push' else (remote_path, local_path)
ssh_cmd = f'ssh -o StrictHostKeyChecking=no -p {obj["port"]}'
passwd = obj.get('passwd')
if passwd:
ssh_cmd = f'sshpass -p {passwd} {ssh_cmd}'
command = ['rsync', '-partial', '-avz', '-e', ssh_cmd, param_tuple[0], param_tuple[1]]
subprocess.run(command)
# 使用scp传送文件
def scp(obj, local_path, remot_path, type='pull'):
remote_path = f'{obj["user"]}@{obj["host"]}:{remot_path}'
param_tuple = (local_path, remote_path) if type == 'push' else (remote_path, local_path)
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd,'scp', '-p', obj["port"], param_tuple[0], param_tuple[1]]
else:
command = ['scp', '-p', obj["port"], param_tuple[0], param_tuple[1]]
subprocess.run(command)
def main():
if len(sys.argv) <= 1:
# 交互式选择登录服务器
obj = chose_server()
ssh(obj)
return
elif len(sys.argv) == 2:
# 指定名称登录服务器
name = sys.argv[1]
if name == '.': # 使用.来使用上次登录的服务
name = last_login()
assert (name), "未找到上次登录信息"
obj = get_server(name)
ssh(obj)
return
else:
# 使用rsync传送文件(交互式选择服务器)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('type', type=str, help='pull或push')
parser.add_argument('-l', type=str, required=True, help='本地文件')
parser.add_argument('-r', type=str, required=True, help='远程文件')
parser.add_argument('--name', type=str, help='指定服务器传送文件')
args = parser.parse_args()
local_path = args.l
remote_path = args.r
type = args.type
name = args.name
obj = get_server(name) if name else chose_server()
rsync(obj, local_path, remote_path, type)
return
if __name__ == "__main__":
main()
第一次运行会生成配置文件$HOME/.ssh/ssh_server.json, 修改服务器列表后就可以使用了
更新: 参数简化固定位置, 增加服务器间文件传送
#!/usr/bin/env python
"""
sys require: fzf rsync sshpass
登录:
sshc # 选择登录服务器(fzf)
sshc <名称> # 指定名称登录服务器
sshc . # 登录上次登录的服务器
文件传送:
sshc push <本地文件路径> <服务器文件路径> # 选择服务器上传文件
sshc pull <服务器文件路径> <本地文件路径> # 选择服务器下载文件
sshc push <本地文件路径> <服务器文件路径> <名称> # 指定服务器上传文件
sshc pull <服务器文件路径> <本地文件路径> <名称> # 指定服务器下载文件
服务器间传送文件:
sshc cp <名称1>:<服务器文件路径> <名称2>:<服务器文件路径> # 将名称1的服务器文件传送到名称2的服务器
sshc cp <服务器1文件路径> <服务器2文件路径> <任意值> # 选择两次服务器把第一个服务器的文件传送到第二个服务器
建议使用公钥而非密码登录
"""
import sys
from pathlib import Path
import json
import subprocess
conf_file = Path.home() / '.ssh' / 'ssh_server.json'
last_login_file = Path.home() / '.ssh' / 'last_login'
# 获取服务器列表配置
def get_config():
if not Path(conf_file).exists:
# 配置文件内容示例
example = """
{
"servers": [
{"enable": 0, "name": "termux", "user": "u0_a355", "host": "192.168.31.239", "port": "8022", "passwd": ""},
{"enable": 0, "name": "sshd4a", "user": "v", "host": "192.168.31.239", "port": "2222", "passwd": ""},
{"enable": 0, "name": "laptop", "user": "root", "host": "192.168.31.189", "port": "22", "passwd": ""}
]
}
"""
with open(conf_file, 'w') as f:
f.write(example)
print(f'请在{conf_file}中配置services')
exit(1)
with open(conf_file, 'r+') as f:
return json.load(f)
cfg = get_config()
objs = [obj for obj in cfg['servers'] if obj['enable'] == 1] # 只收集启用的服务器
# 使用fzf选择一个服务器
def chose_server():
names = [i["name"] for i in objs]
selected_name = run_fzf_cmd(names)
if not selected_name:
exit(0)
return get_server(selected_name)
# 指定名称获取服务器
def get_server(name):
obj = next((d for d in objs if d.get("name") == name and d.get("enable") == 1), None)
assert (obj), f"未找到配置: {name}"
assert (obj.get('user')), f"未配置user: {name}"
assert (obj.get('host')), f"未配置host: {name}"
port = obj.get('port')
if not port:
obj['port'] = '22'
return obj
# 使用fzf选择服务器
def run_fzf_cmd(options):
options_str = "\n".join(options)
process = subprocess.Popen(
["fzf", "--reverse"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True # 设置为True以便处理文本而不是字节
)
selected, _ = process.communicate(input=options_str)
return selected.strip()
# 使用fzf的python包装选择服务器
def fzf_chose(options):
# pip install pyfzf
from pyfzf.pyfzf import FzfPrompt
fzf = FzfPrompt()
selected_name = fzf.prompt(options, "--reverse")
if not selected_name:
exit(0)
return selected_name[0]
# 获取上次登录服务器
def last_login():
if Path(last_login_file).exists():
with open(last_login_file, 'r') as f:
return f.read()
return None
# 记录本次登录服务器
def record_login(name):
# 保存最后一次登录server
last = last_login()
if last != name:
with open(last_login_file, 'w') as f:
f.write(name)
# 登录一个服务器
def ssh(obj):
command = ['ssh', '-o', 'StrictHostKeyChecking=no', '-p', obj["port"], obj2path(obj)]
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd] + command
record_login(obj['name'])
subprocess.run(command)
# 使用rsync传送文件
def rsync(obj, type, path1, path2):
if type == 'pull':
path1 = obj2path(obj, path1)
else:
path2 = obj2path(obj, path2)
ssh_cmd = f'ssh -o StrictHostKeyChecking=no -p {obj["port"]}'
passwd = obj.get('passwd')
if passwd:
ssh_cmd = f'sshpass -p {passwd} {ssh_cmd}'
command = ['rsync', '-partial', '-avz', '-e', ssh_cmd, path1, path2]
subprocess.run(command)
# 使用scp传送文件
def scp(obj, local_path, remot_path, type='pull'):
remote_path = obj2path(obj, remot_path)
param_tuple = (local_path, remote_path) if type == 'push' else (remote_path, local_path)
command = ['scp', '-p', obj["port"], param_tuple[0], param_tuple[1]]
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd] + command
subprocess.run(command)
def obj2path(obj, path=None):
r = f'{obj["user"]}@{obj["host"]}'
if path:
r = f'{r}:{path}'
return r
# 使用rsync在两台服务器之前传送文件
def rsync_server_cp(source_obj, source_path, target_obj, target_path):
# 会有内外部ip问题?
target_port = target_obj["port"]
target_path = obj2path(target_obj, target_path)
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -p {target_port}"
target_passwd = target_obj.get('passwd')
if target_passwd:
ssh_cmd = f'sshpass -p {target_passwd} {ssh_cmd}'
rsync_cmd = f'rsync -partial -avz -e "{ssh_cmd}" "{source_path}" "{target_path}"'
command = ['ssh', '-o', 'StrictHostKeyChecking=no', '-p', source_obj["port"], obj2path(source_obj), rsync_cmd]
source_passwd = source_obj.get('passwd')
if source_passwd:
command = ['sshpass', '-p', source_passwd] + command
subprocess.run(command)
def main():
argv = sys.argv
arglen = len(argv)
if arglen <= 1:
# 交互式选择登录服务器
obj = chose_server()
ssh(obj)
return
p1 = argv[1]
if arglen == 2:
# 指定名称登录服务器
name = p1
if name == '.': # 使用.来使用上次登录的服务
name = last_login()
assert (name), "未找到上次登录信息"
obj = get_server(name)
ssh(obj)
return
action = p1
if action == 'pull' or action == 'push':
# 使用rsync传送文件
path1 = argv[2]
path2 = argv[3]
if arglen > 4:
name = argv[4]
obj = get_server(name)
else:
obj = chose_server()
rsync(obj, action, path1, path2)
return
if action == 'cp':
# 在两台服务器之间传送文件
source_path = argv[2]
target_path = argv[3]
if len(argv) == 4:
# 三个参数表示在文件参数中指定了服务器
def parse_file_param(str):
parts = str.split(':', 1)
assert(len(parts) == 2), f"参数格式不正确: {str}"
return (parts[0], parts(1))
(source_name, source_path) = parse_file_param(source_path)
(target_name, target_path) = parse_file_param(target_path)
source_obj = get_server(source_name)
target_obj = get_server(target_name)
elif arglen > 4:
# 四个参数表示手动选择服务器(第四个参数仅作为flag)
source_obj = chose_server()
target_obj = chose_server()
rsync_server_cp(source_obj, source_path, target_obj, target_path)
return
if __name__ == "__main__":
main()
二更: 文件传送参数重新整理, 允许省略目标路径(设置默认上传目录)
#!/usr/bin/env python
"""
sys require: fzf rsync sshpass
登录:
sshc # 选择登录服务器(fzf)
sshc <名称> # 指定名称登录服务器
sshc . # 登录上次登录的服务器
文件传送:
sshc pull <[-n <名称>:]服务器文件路径> [<本地文件路径>] # 下载文件
sshc push <本地文件路径> <[-n <名称>:][服务器文件路径]> # 上传文件
服务器间传送文件:
sshc cp <[-n <名称1>:]服务器文件路径> <[-n <名称2>:][服务器文件路径]> # 将名称1的服务器文件传送到名称2的服务器
文件传送如果未传目标路径时, 将传送到工作目录(本地)或push_dir(服务器)
建议使用公钥而非密码登录
"""
import sys
import os
from pathlib import Path
import json
import subprocess
conf_file = Path.home() / '.ssh' / 'ssh_server.json'
last_login_file = Path.home() / '.ssh' / 'last_login'
# 获取服务器列表配置
def get_config():
if not Path(conf_file).exists:
# 配置文件内容示例
example = """
{
"servers": [
{"enable": 0, "name": "termux", "user": "u0_a355", "host": "192.168.31.239", "port": "8022", "passwd": "", "push_dir": "/sdcard/Download/"},
{"enable": 0, "name": "sshd4a", "user": "v", "host": "192.168.31.239", "port": "2222", "passwd": "", "push_dir": "/sdcard/Download/"},
{"enable": 0, "name": "laptop", "user": "root", "host": "192.168.31.189", "port": "22", "passwd": "", "push_dir": "/home/root/Downloads/"}
]
}
"""
with open(conf_file, 'w') as f:
f.write(example)
print(f'请在{conf_file}中配置services')
exit(1)
with open(conf_file, 'r+') as f:
return json.load(f)
cfg = get_config()
objs = [obj for obj in cfg['servers'] if obj['enable'] == 1] # 只收集启用的服务器
# 使用fzf选择一个服务器
def chose_server():
names = [i["name"] for i in objs]
selected_name = run_fzf_cmd(names)
if not selected_name:
exit(0)
return get_server(selected_name)
# 指定名称获取服务器
def get_server(name):
obj = next((d for d in objs if d.get("name") == name and d.get("enable") == 1), None)
assert (obj), f"未找到配置: {name}"
assert (obj.get('user')), f"未配置user: {name}"
assert (obj.get('host')), f"未配置host: {name}"
port = obj.get('port')
if not port:
obj['port'] = '22'
return obj
# 使用fzf选择服务器
def run_fzf_cmd(options):
options_str = "\n".join(options)
process = subprocess.Popen(
["fzf", "--reverse"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True # 设置为True以便处理文本而不是字节
)
selected, _ = process.communicate(input=options_str)
return selected.strip()
# 使用fzf的python包装选择服务器
def fzf_chose(options):
# pip install pyfzf
from pyfzf.pyfzf import FzfPrompt
fzf = FzfPrompt()
selected_name = fzf.prompt(options, "--reverse")
if not selected_name:
exit(0)
return selected_name[0]
# 获取上次登录服务器
def last_login():
if Path(last_login_file).exists():
with open(last_login_file, 'r') as f:
return f.read()
return None
# 记录本次登录服务器
def record_login(name):
# 保存最后一次登录server
last = last_login()
if last != name:
with open(last_login_file, 'w') as f:
f.write(name)
# 登录一个服务器
def ssh(obj):
command = ['ssh', '-o', 'StrictHostKeyChecking=no', '-p', obj["port"], obj2path(obj)]
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd] + command
record_login(obj['name'])
subprocess.run(command)
# 使用rsync传送文件
def rsync(obj, type, path1, path2):
if type == 'pull':
path1 = obj2path(obj, path1)
if not path2:
path2 = os.getcwd()
else:
if not path2:
path2 = obj.get("push_dir")
assert(path2), f"服务器未配置默认推送目录: {obj['name']}"
path2 = obj2path(obj, path2)
ssh_cmd = f'ssh -o StrictHostKeyChecking=no -p {obj["port"]}'
passwd = obj.get('passwd')
if passwd:
ssh_cmd = f'sshpass -p {passwd} {ssh_cmd}'
command = ['rsync', '-partial', '-avz', '-e', ssh_cmd, path1, path2]
subprocess.run(command)
# 使用scp传送文件
def scp(obj, local_path, remot_path, type='pull'):
if type == 'pull' and not local_path:
local_path = os.getcwd()
remote_path = obj2path(obj, remot_path)
param_tuple = (local_path, remote_path) if type == 'push' else (remote_path, local_path)
command = ['scp', '-p', obj["port"], param_tuple[0], param_tuple[1]]
passwd = obj.get('passwd')
if passwd:
command = ['sshpass', '-p', passwd] + command
subprocess.run(command)
# 使用rsync在两台服务器之前传送文件
def rsync_server_cp(source_obj, source_path, target_obj, target_path):
# 会有内外部ip问题?
target_port = target_obj["port"]
if not target_path:
target_path = target_obj.get("push_dir")
assert(target_path), f"服务器未配置默认推送目录: {target_obj['name']}"
target_path = obj2path(target_obj, target_path)
ssh_cmd = f"ssh -o StrictHostKeyChecking=no -p {target_port}"
target_passwd = target_obj.get('passwd')
if target_passwd:
ssh_cmd = f'sshpass -p {target_passwd} {ssh_cmd}'
rsync_cmd = f'rsync -partial -avz -e "{ssh_cmd}" "{source_path}" "{target_path}"'
command = ['ssh', '-o', 'StrictHostKeyChecking=no', '-p', source_obj["port"], obj2path(source_obj), rsync_cmd]
source_passwd = source_obj.get('passwd')
if source_passwd:
command = ['sshpass', '-p', source_passwd] + command
subprocess.run(command)
def obj2path(obj, path=None):
r = f'{obj["user"]}@{obj["host"]}'
if path:
r = f'{r}:{path}'
return r
def parse_file_param(str):
parts = str.split(':', 1)
return (parts[0], None if len(parts) == 1 else parts[1])
def parse_server_path_param(arr, begin_idx):
p1 = arr_get(arr, begin_idx)
p2 = arr_get(arr, begin_idx + 1)
if p1 == '-n':
(name, path) = parse_file_param(p2)
return (get_server(name), path, arr[2:])
else:
return (chose_server(), p1, arr[1:])
def arr_get(arr, idx, default=None):
return default if len(arr) <= idx else arr[idx]
def main():
argv = sys.argv
arglen = len(argv)
if arglen <= 1:
# 交互式选择登录服务器
obj = chose_server()
ssh(obj)
return
p1 = argv[1]
if arglen == 2:
# 指定名称登录服务器
name = p1
if name == '.': # 使用.来使用上次登录的服务
name = last_login()
assert (name), "未找到上次登录信息"
obj = get_server(name)
ssh(obj)
return
action = p1
if action == 'pull' or action == 'push':
# 使用rsync传送文件
if action == 'pull':
(obj, path1, argv1) = parse_server_path_param(argv[2:], 0)
path2 = arr_get(argv1, 0)
else:
path1 = argv[2]
(obj, path2, _) = parse_server_path_param(argv[3:], 0)
rsync(obj, action, path1, path2)
return
if action == 'cp':
# 在两台服务器之间传送文件
(source_obj, source_path, argv1) = parse_server_path_param(argv[2:], 0)
(target_obj, target_path, _) = parse_server_path_param(argv1, 0)
rsync_server_cp(source_obj, source_path, target_obj, target_path)
return
if __name__ == "__main__":
main()