标签:python paramiko 批量命令 argparse 批量上传下载
这阵子一直在学python,碰巧最近想把线上服务器环境做一些规范化/统一化,于是便萌生了用python写一个小工具的冲动。就功能方面来说,基本上是在“重复造轮子”吧,但是当我用这小工具完成了30多台服务器从系统层面到应用层面的一些规范化工作之后,觉得效果还不算那么low(高手可忽略这句话~~),这才敢拿出来跟小伙伴们分享一下。
(注:笔者所用为python版本为3.5,其他版本未经测试~~)
其实很简单,就"一个脚本"+"server信息文件"实现如题目所述的功能,能够像使用linux系统命令一样拿来即用。来看一些基本的使用截图吧
批量执行远程命令:
上传目录:
下载单个文件:
下载目录:
接下来直接看代码吧(我的老(lan)习惯,代码里注释还算详细,所以我就懒得再解释那么多喽)
#!/bin/env python3 # coding:utf-8 """ by ljk 20160704 """ from paramiko import SSHClient, AutoAddPolicy from os import path, walk, makedirs from re import split, match, search from sys import exit from argparse import ArgumentParser, RawTextHelpFormatter # ---------- # get_args()函数通过argparse模块的ArgumentParser类来生成帮助信息并获取命令行参数 # 生成一个全局变量字典对象args,保存处理过的命令行参数 # ---------- def get_args(): """实例化类,formatter_class参数允许help信息以自定义的格式显示""" parser = ArgumentParser(description="This is a tool for execute command(s) on remote server(s) or get/put file(s) from/to the remote server(s)\nNotice: please always use ‘/‘ as path separater!!!",formatter_class =RawTextHelpFormatter,epilog="Notice:\n If any options use more than once,the last one will overwrite the previous") parser.add_argument(‘-u‘,metavar=‘USER‘,dest=‘user‘,help="remote username",required=True) parser.add_argument(‘-p‘,metavar=‘PASSWORD‘,dest=‘passwd‘,help=" user‘s password") parser.add_argument(‘--pkey‘,nargs=‘?‘,metavar=‘PRIVATE KEY‘,dest=‘pkey‘,help="local private key,if value not followed by this option,the default is: ~/.ssh/id_rsa",default=None,const=‘%s/.ssh/id_rsa‘ % path.expanduser(‘~‘)) parser.add_argument(‘--server‘, metavar=‘SERVER_INFO_FILE‘, help="file include the remote server‘s information\nwith the format of ‘name-ip:port‘,such as ‘web1-192.168.1.100:22‘,one sever one line", required=True) remote_command = parser.add_argument_group(‘remote command‘,‘options for running remote command‘) remote_command.add_argument(‘--cmd‘,metavar=‘“COMMAND”‘,dest=‘cmd‘,help="command run on remote server,multiple commands sperate by ‘;‘") sftp = parser.add_argument_group(‘sftp‘,‘options for running sftp‘) sftp.add_argument(‘--put‘,metavar=‘‘,help="transfer from local to remote",nargs=2) sftp.add_argument(‘--get‘,metavar=‘‘,help="transfer from remote to local",nargs=2) # 全局字典 键(add_argument()中的dest):值(用户输入) # vars将Namespace object转换成dict object global args args = vars(parser.parse_args()) # 判断 --cmd --put --get 三个参数的唯一性 # 清除掉args字典中值为None的项.argparse默认给不出现的值赋值None n = 0 for i in (‘cmd‘,‘put‘,‘get‘): if i in args: if args[i] is None: del args[i] else: n+=1 if n > 1: print(‘\n Only one of the "--cmd --put --get" can be used!‘) exit(10) def get_ip_port(fname): """从制定文件(特定格式)中,取得主机名/主机ip/端口""" try: fobj = open(fname,‘r‘) except Exception as err: print(err) exit(10) for line in fobj.readlines(): if line != ‘\n‘ and not match(‘#‘,line): # 过滤空行和注释行 list_tmp = split(‘[-:]‘,line) server_name = list_tmp[0] server_ip = list_tmp[1] port = int(list_tmp[2]) yield (server_name,server_ip,port) def create_sshclient(server_ip,port): """根据命令行提供的参数,建立到远程server的ssh链接.这里本在run_command()函数内部。 摘出来的目的是为了让sftp功能也通过sshclient对象来创建sftp对象,因为初步观察t.connect()方法在使用key时有问题""" global client client = SSHClient() client.set_missing_host_key_policy(AutoAddPolicy()) try: client.connect(server_ip,port=port,username=args[‘user‘],password=args[‘passwd‘],key_filename=args[‘pkey‘]) except Exception as err: # 有异常,打印异常,并返回‘error‘ print(‘{}----{} error: {}‘.format(‘ ‘*4,server_ip,err)) return ‘error‘ # ---------- # run_command()函数提供两个功能 主功能:执行远程命令; 辅功能:在sftp_transfer()函数中执行一些远程命令 # ---------- def run_command(): """执行远程命令的主函数""" # stdout 假如通过分号提供单行的多条命令,所有命令的输出(在linux终端会输出的内容)都会存储于stdout # 据观察,下面三个变量的特点是无论"如何引用过一次"之后,其内容就会清空 # 有readlines()的地方都是流,用过之后就没有了 stdin,stdout,stderr = client.exec_command(args[‘cmd‘]) copy_out,copy_err = stdout.readlines(),stderr.readlines() if len(copy_out) != 0: print(‘%s----result:‘ % (‘ ‘*8)) for i in copy_out: print(‘%s%s‘ % (‘ ‘*12,i),end=‘‘) elif len(copy_err) != 0: print(‘%s----error:‘ % (‘ ‘*8)) for i in copy_err: print(‘%s%s‘ % (‘ ‘*12,i),end=‘‘) exit(10) client.close() # ---------- # sftp_transfer() 远程传输文件的主函数 # ---------- def sftp_transfer(source_path,destination_path,method): """文件传输的 主函数""" sftp = client.open_sftp() # 下面定义sftp_transfer()函数所需的一些子函数 def str_to_raw(s): """ !!此函数暂未使用,参数中的目录强制使用‘/‘作为分隔符!! 借用网友的代码,将会被反斜杠转义的字符做转换.将\转换为\\,这里的转换还不全,比如对‘\123‘这样的还无法转换成‘\\123‘ """ raw_map = {8:r‘\b‘, 7:r‘\a‘, 12:r‘\f‘, 10:r‘\n‘, 13:r‘\r‘, 9:r‘\t‘, 11:r‘\v‘} return r‘‘.join(i if ord(i) > 32 else raw_map.get(ord(i), i) for i in s) def process_arg_dir(src,dst): """处理目录时,自动检查用户输入,并在s_path和d_path后面都加上/""" if not src.endswith(‘/‘): src = src + ‘/‘ if not dst.endswith(‘/‘): dst = dst + ‘/‘ return src,dst def sftp_put(src, dst, space): """封装""" try: sftp.put(src, dst) print(‘%s%s‘ % (‘ ‘ * space, src)) except Exception as err: print(‘%s----Uploading %s Failed‘ % (‘ ‘ * space, src)) print(‘{}----{}‘.format(‘ ‘ * space, err)) exit(10) def sftp_get(src, dst, space): try: sftp.get(src, dst) print(‘%s%s‘ % (‘ ‘ * space, src)) except Exception as err: print(‘%s----Downloading %s Failed‘ % (‘ ‘ * space, src)) print(‘{}----{}‘.format(‘ ‘ * space, err)) exit(10) def sftp_transfer_rcmd(cmd=None,space=None): stdin,stdout,stderr = client.exec_command(cmd) copy_out, copy_err = stdout.readlines(), stderr.readlines() if len(copy_err) != 0: for i in copy_err: print(‘%s----%s‘ % (‘ ‘*space,i),end=‘‘) exit(10) else: return copy_out def check_remote_path(r_path): """通过client对象在远程linux执行命令,来判断远程路径是否存在,是文件还是目录""" check_cmd = ‘if [ -e {0} ];then if [ -d {0} ];then echo directory;elif [ -f {0} ];then echo file;fi;else echo no_exist;fi‘.format(r_path) # check_cmd命令会有三种‘正常输出’directory file no_exist check_result = sftp_transfer_rcmd(cmd=check_cmd)[0].strip(‘\n‘) if check_result == ‘directory‘: return ‘directory‘ elif check_result == ‘file‘: return ‘file‘ else: return ‘no_exist‘ # 子函数定义完毕 # 上传逻辑 if method == ‘put‘: print(‘%s----Uploading %s TO %s‘ % (‘ ‘*4,source_path,destination_path)) if path.isfile(source_path): # 判断是文件 if destination_path.endswith(‘/‘): """ put和get方法默认只针对文件,且都必须跟上文件名,否则会报错. 这里多加一层判断实现了目标路径可以不加文件名 """ sftp_transfer_rcmd(cmd=‘mkdir -p {}‘.format(destination_path),space=4) # 若目标路径不存在,则创建 destination_path = destination_path + path.basename(source_path) sftp_put(source_path, destination_path, 8) else: sftp_transfer_rcmd(cmd=‘mkdir -p {}‘.format( path.dirname(destination_path)), space=4) elif path.isdir(source_path): # 判断是目录 source_path,destination_path = process_arg_dir(source_path,destination_path) for root, dirs, files in walk(source_path): """通过 os.walk()函数取得目录下的所有文件,此函数默认包含 . ..的文件/目录,需要去掉""" for file_name in files: s_file = path.join(root,file_name).replace(‘\\‘,‘/‘) # 逐级取得每个sftp client端文件的全路径,并将路径中的\换成/ if not search(‘.*/\..*‘, s_file): """过滤掉路径中包含以.开头的目录或文件""" d_file = s_file.replace(source_path,destination_path,1) # 由local_file取得每个远程文件的全路径 d_path = path.dirname(d_file) check_remote_path_result = check_remote_path(d_path) if check_remote_path_result == ‘directory‘: sftp_put(s_file, d_file, 12) # 目标目录存在,直接上传 elif check_remote_path_result == ‘no_exist‘: print(‘%s----Create Remote Dir: %s‘ % (‘ ‘ * 8, path.dirname(d_file))) sftp_transfer_rcmd(cmd=‘mkdir -p {}‘.format(d_path)) sftp_put(s_file, d_file, 12) else: print(‘{}----the {} is file‘.format(‘ ‘ * 8, d_path)) exit(10) else: print(‘%s%s is not exist‘ % (‘ ‘*8,source_path)) exit(10) # 下载逻辑 elif method == ‘get‘: print(‘%s----Downloading %s TO %s‘ % (‘ ‘*4, source_path, destination_path)) check_remote_path_result = check_remote_path(source_path) if check_remote_path_result == ‘file‘: # 判断是文件 if path.isfile(destination_path): sftp_get(source_path, destination_path, 8) else: # 参数中的‘目标路径‘为目录或不存在 try: makedirs(destination_path) sftp_get(source_path, path.join(destination_path, path.basename(source_path)).replace(‘\\‘,‘/‘), 8) except Exception as err: print(‘%s----Create %s error‘ % (‘ ‘*4,destination_path)) print(‘{}{}‘.format(‘ ‘*8,err)) exit(10) sftp_get(source_path,destination_path,8) elif check_remote_path_result == ‘directory‘: # 判断是目录 source_path, destination_path = process_arg_dir(source_path, destination_path) def process_sftp_dir(path_name): """ 此函数递归处理sftp server端的目录和文件,并在client端创建所有不存在的目录,然后针对每个文件在两端的全路径执行get操作. path_name第一次的引用值应该是source_path的值 """ d_path = path_name.replace(source_path,destination_path,1) if not path.exists(d_path): # 若目标目录不存在则创建 print(‘%s----Create Local Dir: %s‘ % (‘ ‘*8,d_path)) try: makedirs(d_path) # 递归创建不存在的目录 except Exception as err: print(‘%s----Create %s Failed‘ % (‘ ‘*8,d_path)) print(‘{}----{}‘.format(‘ ‘*8,err)) exit(10) for name in (i for i in sftp.listdir(path=path_name) if not i.startswith(‘.‘)): """去掉以.开头的文件或目录""" s_file = path.join(path_name,name).replace(‘\\‘,‘/‘) # 在win环境下组合路径所用的‘\\‘换成‘/‘ d_file = s_file.replace(source_path,destination_path,1) # 目标端全路径 chk_r_path_result = check_remote_path(s_file) if chk_r_path_result == ‘file‘: # 文件 sftp_get(s_file,d_file,12) elif chk_r_path_result == ‘directory‘: # 目录 process_sftp_dir(s_file) # 递归调用本身 process_sftp_dir(source_path) else: print(‘%s%s is not exist‘ % (‘ ‘ * 8, source_path)) exit(10) client.close() if __name__ == "__main__": get_args() for server_name,server_ip,port in get_ip_port(args[‘server‘]): #循环处理每个主机 print(‘\n--------%s‘ % server_name) if create_sshclient(server_ip,port) == ‘error‘: continue # 区别处理 --cmd --put --get参数 if ‘cmd‘ in args: run_command() elif ‘put‘ in args: sftp_transfer(args[‘put‘][0],args[‘put‘][1],‘put‘) elif ‘get‘ in args: sftp_transfer(args[‘get‘][0],args[‘get‘][1],‘get‘)
其实之所以想造这个“简陋”的轮子,一方面能锻炼python coding,另一方面当时确实有这么一个需求。而且用自己的工具完成工作也是小有成就的(请勿拍砖~)。
另外,在使用paramiko模块的过程中,又促使我深入的了解了一些ssh登陆的详细过程。所以作为一枚运维,现在开始深刻的理解到“运维”和“开发”这俩概念之间的相互促进。
本文出自 “奋进的K” 博客,请务必保留此出处http://kaifly.blog.51cto.com/3209616/1832200
Python实现linux/windows通用批量‘命令/上传/下载’小工具
标签:python paramiko 批量命令 argparse 批量上传下载
原文地址:http://kaifly.blog.51cto.com/3209616/1832200