造个轮子,用python写的web项目自动部署系统
虽然已经有了Jenkis等强大的持续集成系统,但仍阻挡不了我对造轮子的热爱。
适用框架:Thinkphp,正增加对Laravel的支持
功能:将项目代码进行版本控制,便于保存旧版本,快速切换不同版本。
优点:无需安装!配置超简单!上线快!
要求:
1.备份你的线上代码,以防万一
2.将新的项目目录使用zip压缩
3.第一次使用,需要把项目根目录设置为软链接到某空目录,此空目录权限需和项目目录应该有的访问权限相同,随后程序会自动将项目根目录软链接到新的项目路径
4.第3步设置完成后,直接运行以下代码,根据提示配置即可。
流程概述:
1.解压项目zip文件,以project.zip举例
2.将解压后的项目目录(project)以版本号的形式更改名字(project.201710011300),移动到版本库(path_to_appstore/project/project.201710011300)
3.对project.201710011300进行处理,包括更改owner,清除误上传的缓存(如有)
4.将项目根目录(path_to_app_base)软链接到该版本号目录(ln -sfn path_to_appstore/project/project.201710011300 path_to_app_base)
5.将该版本的特定目录(如用户上传文件),软链接到指定线上目录((ln -sfn path_to_appstore/project/project.201710011300/special_path path_to_project_special)
#!/usr/bin/python # -*- coding: UTF-8 -*- import os,time,subprocess,shutil defaultAppName='project.haha.com' #项目名 defaultStaticFolderName='Uploads' #用户上传静态文件夹默认名 appOwner='www:www' #默认文件所有者 defaultAppParentPath='/data/wwwroot/' #项目所在目录 defaultAppPath=defaultAppParentPath+defaultAppName #项目路径 defaultAppStaticPath=defaultAppParentPath+defaultAppName+'.'+defaultStaticFolderName #项目的特殊资源目录 newFileDir='' #上传文件所在路径 defaultNewFile='upload.zip' #上传文件名 newFile='' #根据用户输入判断 factoryDir=defaultAppParentPath+'/factoryDir' #存放上传的zip文件 newFileUnzipedWorkDir=factoryDir+'/'+'unziped' #在此目录中对上传文件进行解压等操作,以防万一 appBackDir=defaultAppParentPath+'/appback/'+defaultAppName+'/' #项目的备份文件夹 appStoreParent=defaultAppParentPath+'appstore/' #项目版本库的上级文件夹 appStore=appStoreParent+defaultAppName+'/' #项目版本库 appCleanRuntimeDir=['/Runtime/Cache','/Runtime/Data','/Runtime/Temp'] #需要清除的缓存路径 timeSign=time.strftime("%a-%b-%d-%H-%M-%S-%Y", time.localtime()) #随后文件中使用的时间标记(如版本号) #确定上传文件所在目录并验证 def judgePath(): global newFileDir,defaultAppPath print "压缩包默认已上传至"+defaultAppPath newFileDir=raw_input("如果是以上目录,请press enter,如果不是,请输入存放目录:") if newFileDir=='': newFileDir=defaultAppPath if os.path.isdir(newFileDir)==False: print "亲,目录并不存在啊 ( >﹏<。)" return judgePath() if os.access(newFileDir, os.W_OK)==False: print '亲,我木有权限进入:'+newFileDir+' ( >﹏<。)' return judgePath() else: return os.path.abspath(newFileDir)+'/' # abspath:返回path规范化的绝对路径 #确定上传的文件名并验证 def judegeNewFile(): global newFile newFile=raw_input("默认zip文件是"+defaultNewFile+",如果是,请press enter,如果不是,请输入文件名:") if newFile=='': newFile=defaultNewFile if os.path.exists(newFileDir+newFile)==False: print '亲,'+newFileDir+newFile+'并不存在啊 ( >﹏<。)' return judegeNewFile() if os.access(newFileDir+newFile, os.W_OK)==False: print '亲,我木有写权限:'+newFile+' ( >﹏<。)' return judegeNewFile() else: return newFile #start... #删除zip文件工作目录 if os.path.isdir(newFileUnzipedWorkDir)==True: shutil.rmtree(newFileUnzipedWorkDir) os.makedirs(newFileUnzipedWorkDir) print '***注意***,默认操作的项目是:'+defaultAppName #获取上传文件所在的路径 newFileDir= judgePath() #获取上传文件的名字 newFile= judegeNewFile() filePath=newFileDir+newFile #上传文件路径 #移动文件到处理目录并改名 print '将'+newFile+'移动至'+factoryDir+'进行处理...' newFilePath=factoryDir+'/'+newFile shutil.move(filePath,newFilePath) print "已将zip文件移动至"+newFileUnzipedWorkDir #解压文件 print '开始解压文件...' pUnzip=subprocess.Popen(["unzip",newFilePath,'-d',newFileUnzipedWorkDir], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE) #注意,如果使用stderr但不使用stdin,那unzip时是不行的,为什么?万能的网友请告诉我 '''Popen.communicate(input=None) Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child. communicate() returns a tuple (stdoutdata, stderrdata). Note that if you want to send data to the process’s stdin, you need to create the Popen object with stdin=PIPE. Similarly, to get anything other than None in the result tuple, you need to give stdout=PIPE and/or stderr=PIPE too. ''' tupleUnzip = pUnzip.communicate('A') if tupleUnzip[-1]=='': print "解压文件完毕" else: print "解压出错了.错误为:"+tupleUnzip[-1] exit() unzipedFileList = os.listdir(newFileUnzipedWorkDir) if len(unzipedFileList)>1: print "解压后发现上传文件并不是一个文件夹,目前功能很low,请压缩整个文件夹再上传" exit() afterUnzipedFindThisFolder=unzipedFileList[0];#解压完后,发现的文件夹名字 #把新项目文件放到版本库中 thisVerName=defaultAppName+"."+timeSign #在压缩文件处理车间处理过的文件放到appstore中并重命名 '''
tips
mv用法 格式:mv dir1 dir2 如果目录dir2不存在,将目录dir1改名为dir2;否则,将dir1移动到dir2中。 ''' print "准备把处理好的解压文件放到"+appStore+'/'+thisVerName shutil.move(newFileUnzipedWorkDir+'/'+afterUnzipedFindThisFolder,appStore+'/'+thisVerName) print "已将处理好的解压文件放到"+appStore+'/'+thisVerName #删除缓存 for dir in appCleanRuntimeDir: absDir=appStore+'/'+thisVerName+'/'+dir if (os.path.exists(absDir)): #清理 try: shutil.rmtree(absDir) print "已删除"+absDir except OSError,e: print "删除失败!!!"+str(e) else: print '缓存目录'+appStore+'/'+dir+'不存在,无法清理' #更改文件所有者 pChown=subprocess.Popen(["chown",appOwner,appStore+'/'+thisVerName,'-R'], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE) tupleChown = pChown.communicate(); if tupleChown[-1]=='': print "已将文件夹的所有者改为:"+appOwner else: print "更改所有者失败.错误为:"+tupleChown[-1] exit() #创建软连接 print "开始创建软链接"
#1.创建整个项目的软链接
pLn=subprocess.Popen(["ln",'-sfn',appStore+'/'+thisVerName,defaultAppPath], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE) tupleLn = pLn.communicate() if tupleLn[-1]=='': print "已将"+defaultAppName+'软链接到:'+appStore+'/'+thisVerName else: print "创建软链接错误.错误为:"+tupleChown[-1] exit() #2.创建特殊目录软连接 shutil.move(appStore+'/'+thisVerName+"/"+defaultStaticFolderName,appStore+'/'+thisVerName+"/"+defaultStaticFolderName+'.bak') if (os.path.exists(defaultAppStaticPath)==False): print '发现'+defaultAppStaticPath+'不存在,准备复制' os.makedirs(defaultAppStaticPath) #TODO:这里是异步,因此需要等待一秒,有待改进 time.sleep(1) print '准备复制的文件夹为'+appStore+'/'+thisVerName+"/"+defaultStaticFolderName+'.bak' print '最终目录为'+defaultAppStaticPath #由于subprocess中无法识别星号*,所以暂时放弃了 #cp -R ./Public.bak/* /webserver/wwwroot/Public #pCpStatic=subprocess.Popen(["cp",'-R',appStore+'/'+thisVerName+"/"+defaultStaticFolderName+'.bak'+'/*',defaultAppStaticPath], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE) #tupleCpStatic = pCpStatic.communicate() #if tupleCpStatic[-1]=='': # print "已将"+appStore+'/'+thisVerName+"/"+defaultStaticFolderName+'复制到链接到:'+defaultAppStaticPath #else: # print "复制错误.错误为:"+tupleCpStatic[-1] # exit() os.popen('cp -R '+appStore+'/'+thisVerName+"/"+defaultStaticFolderName+'.bak'+'/* '+defaultAppStaticPath) pLnStatic=subprocess.Popen(["ln",'-sfn',defaultAppStaticPath,appStore+'/'+thisVerName+"/"+defaultStaticFolderName], stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE) tupleLnStatic = pLnStatic.communicate() if tupleLnStatic[-1]=='': print "已将"+defaultAppStaticPath+'软链接到:'+appStore+'/'+thisVerName+"/"+defaultStaticFolderName else: print "特殊目录创建软链接错误.错误为:"+tupleLnStatic[-1] exit() ''' TODO: 1.严格限制各个文件夹权限,满足访问的基础上,尽可能最小化权限 2.解压后确定是根目录 1.当前项目配置中的app_debug检测 '''