背景:
一个项目有时候要上传到测试机,有时候要上传到正式机。
有一次因为配置错了环境,白忙活了一整天,决定这种事还是交给脚本来实现吧。
思路:
1、先用正则表达式把环境变量给抓出来,然后修改成要上传服务器的环境变量后写回文件。
2、抛弃一些子目录如log,venv等(虽然不要里面的内容,但要把它们本身这个空目录加入tar),开始tar打包。
3、计算打包后文件的md5。
4、用SFTP协议传到服务器,具体用python的第三方lib:paramiko,博主自己写了个类封装。
5、计算传输后文件的md5,比对两者md5,如果相同则说明数据完整,可以继续,否则exit。
6、用paramiko中的exec_command()可以操控服务器执行我们的cmd。
7、之后做些收尾工作,比如复制好virtualenv、重启supervisor呀之类的。
# coding=utf-8
import os
import shutil
import time
import re
import tarfile
import hashlib
import paramiko
src_root = 'g:\\projects\\myApp\\'
des_root = 'g:\\projects\\myapp_helper\\'
linux_root = '/home/www/myapp_uploads/'
today = time.strftime('%Y-%m-%d-%H-%M', time.localtime(time.time()))
des_target = os.path.join(des_root, 'test_myapp%s\\' % today)
print u'目标根目录:', des_target
class MySSH(object):
def __init__(self, host, username, password):
self.host = host
self.username = username
self.password = password
self.ssh_fd = None
self.sftp_fd = None
self.ssh_connect()
self.sftp_open()
def ssh_connect(self):
try:
print u'连接SSH...'
self.ssh_fd = paramiko.SSHClient()
self.ssh_fd.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh_fd.connect(self.host, 22, username=self.username, password=self.password)
stdin, stdout, stderr = self.ssh_fd.exec_command("mkdir %s" % linux_root)
print stdout.readlines()
print u'连接SSH 成功...'
except Exception, ex:
print 'ssh %s@%s: %s' % (self.username, self.host, ex)
exit()
def sftp_open(self):
print u'打开SFTP 成功...'
self.sftp_fd = self.ssh_fd.open_sftp()
def sftp_put(self, from_path, to_path):
'''
上传文件到远程服务器
'''
return self.sftp_fd.put(from_path, to_path)
def sftp_get(self, from_path, to_path):
'''
从远程服务器下载文件
'''
return self.sftp_fd.get(from_path, to_path)
def exe(self, cmd):
'''
让远程服务器执行cmd
'''
stdin, stdout, stderr = self.ssh_fd.exec_command(cmd)
print u'执行%s:' % cmd
print stderr.readlines()
if cmd.find('md5') == 0:
return stdout.readline()
def close(self):
self.sftp_fd.close()
self.ssh_fd.close()
print u'关闭SSH连接...成功'
def __del__(self):
self.close()
def copy_project():
'''
1、先把src项目拷贝到des下
2、除去某些子目录:discard_dirs = ['myproto', 'log', 'myapp_env', 'test', 'tmp', 'upload', '.idea', '.git']
3、除去某些不需要的文件: .gitignore, README.md
4、遇到配置文件,把环境变量改成测试环境...api_status = 2
'''
if not os.path.exists(des_target):
print u'创建根目录:', des_target
os.makedirs(des_target)
discard_dirs = ['myproto', 'log', 'myapp_env', 'test', 'tmp', 'upload', '.idea', '.git', 'uploads']
for root, dirs, files in os.walk(src_root, topdown=False):
t_root = root.replace(src_root, des_target)
for dir in dirs:
t_dir = os.path.join(t_root, dir)
# print t_dir
if len(list(set(t_dir.split('\\')).intersection(set(discard_dirs)))) > 0:
continue
if not os.path.exists(t_dir):
print u'目录安全,创建...',t_dir
os.makedirs(t_dir)
if not os.listdir(t_dir):
print u'目录为空,删除...',t_dir
os.removedirs(t_dir)
for file in files:
if file.endswith('.pyc'):
continue
if file in ['.gitignore', 'README.md']:
continue
src_file = os.path.join(root, file)
des_file = os.path.join(t_root, file)
if len(list(set(src_file.split('\\')).intersection(set(discard_dirs)))) > 0:
continue
des_folder = des_file[0: -len(des_file.split('\\')[-1])]
if not os.path.exists(des_folder):
print u'目录安全,创建...',des_folder
os.makedirs(des_folder)
# print src_file, ' > ', des_file
if file != 'settings.py':
shutil.copy(src_file, des_file)
else:
out = open(des_file, 'w')
datas = open(src_file, 'r')
pattern = re.compile(r'api_status\s*=\s*\d')
for line in datas.readlines():
if pattern.match(line):
line = 'api_status = 2\n'
out.write(line)
datas.close()
out.close()
# 补全
print des_target+'log'
os.mkdir(des_target+'log')
os.mkdir(des_target+'uploads')
def tar_project(dir_path):
'''
1、用tarfile来打包,按照这种方法打包是会忽略空目录的。
2、在tar中加入空目录。
dir_path = g:\projects\chanzai_helper\test_chanzai2016-11-22-11-09
return filename, filepath
'''
# 创建压缩包名
tar_name = "%s.tar.gz" % dir_path.split('\\')[-2]
tar_fullpath = des_root+tar_name
print u'压缩包名字:', tar_fullpath
tar = tarfile.open(tar_fullpath, "w:gz")
# 创建压缩包
for root, dirs, files in os.walk(dir_path):
for file in files:
fullpath = os.path.join(root, file)
# print u'压缩:', fullpath
filename = fullpath.replace(des_target, '')
tar.add(fullpath, arcname=filename)
tar.add(des_target+'log', arcname="log/")
tar.add(des_target+'uploads', arcname="uploads/")
tar.close()
return tar_name, tar_fullpath
def CalcMD5(filepath):
'''
获取文件md5, 用来检验文件传输完整性
'''
with open(filepath, 'rb') as f:
md5obj = hashlib.md5()
md5obj.update(f.read())
hash = md5obj.hexdigest()
return hash
copy_project()
filename, filepath = tar_project(des_target)
from_md5 = CalcMD5(filepath)
print u'压缩文件md5 = %s' % from_md5
# 删除本地的文件夹
shutil.rmtree(des_target)
ssh = MySSH('xxx.xx.xx.xxx', 'your_name', 'your_pass')
print u'传输:', filename
ssh.sftp_put(filepath, linux_root + filename)
# 获取linux那边文件的md5
cmd = "md5sum %s%s|cut -d ' ' -f1" % (linux_root, filename)
to_md5 = ssh.exe(cmd)
print u'传输过去的文件md5 = %s' % to_md5
to_md5 = to_md5.strip('\n')
if from_md5 != to_md5:
print len(from_md5), len(to_md5)
print u'ERROR: 文件传输不完整...'
exit()
else:
print u'OK: 文件传输完整!'
# 解压缩 tar 包
folder = filename.split('.')[0]
cmd = 'mkdir %s%s;cd %s%s;tar -zxvf %s%s' % (linux_root, folder, linux_root, folder, linux_root, filename)
ssh.exe(cmd)
# 删除linux - tar包
cmd = 'cd %s; rm -rf %s%s' % (linux_root, linux_root, filename)
err = ssh.exe(cmd)
if err:
print err
exit()
# 删除/home/www/myAppbak
cmd = 'cd /home/www; rm -rf /home/www/myAppbak'
err = ssh.exe(cmd)
if err:
print err
exit()
# 交换当前工作目录
cmd = 'mv /home/www/myApp /home/www/myAppbak'
err = ssh.exe(cmd)
if err:
print err
exit()
cmd = 'mv %s%s /home/www/myApp' % (linux_root, folder)
err = ssh.exe(cmd)
if err:
print err
exit()
# 把 虚拟环境复制过去
cmd = 'cp -r /home/www/myAppbak/myapp_env /home/www/myApp/myapp_env'
err = ssh.exe(cmd)
if err:
print err
exit()
# 在supervisor中重启
cmd = 'supervisorctl restart myapp'
err = ssh.exe(cmd)
if err:
print err
exit()