Данная статья предназначена для разработчиков, которые работают с SVN и удаленных хостингом с доступом по ftp и ssh, и ограничением на установку чего либо.nnnnПроцесс деплоя очень прост, обновление файлов на хостинг, которое реализовано скриптом. Программист формирует код, после выполняется скрипт который заменяет текущие файлы, можно запускать либо автоматически либо вручную.nnТехнические требования:Локальный серверn
- SVN хранилище , добавлен в PATH путь к svn.exe
- используется python 2.5, добавлен в PATH путь к python.exe
- есть SSH
Удаленный хостингn
- есть ssh, ftp
- нельзя устанавливать ПО
C:\Program Files\CruiseControl\projects\project1 содержит код проекта, синхронизуруется с продуктивным сервером. Т.е. код на продуктиве совпадает с кодом в папке.nnСкрипт синхронизации у меня расположен C:\svn\apply_svn_changes\publish_web_site.pynnПользователь имеет доступ к ssh, ftpnnnСайт на удаленном хостинге находится по пути web/project в папке ftpnnЗапуск скрипта выполняется следующей командойn
> python C:\svn\apply_svn_changes\publish_web_site.py -c "C:\Program Files\CruiseControl\projects\project1\source" --host [email protected] -p ftp_password -d sites/ms
Скрипт просматривает папку «C:\Program Files\CruiseControl\projects\project1\source», проверяет версию кода, и сравнивает с версией в хранилище. Таким образом определяет что нужно удалить, а что — скопировать или создать. Для этого используется команда svn diff
.Далее происходит формирование плана выполнения и сохранение его во временной директории.nЗатем скрипт просматривает план, соединяется по ftp с хостингом и выполняет план. По окончанию отключается, все изменения фиксирует в лог файлn
n
# -*- coding: cp1251 -*-nimport loggingnfrom subprocess import Popen, PIPEnfrom time import sleepnndef main():n logging.getLogger('MyLogger').debug('main app started')n usage = """usage: %prog -c PATH -r REVISION --host user@host -d start_dir -p passwordnUse to upload website from repositoryn"""n from optparse import OptionParsern parser = OptionParser(usage=usage)n parser.add_option("-c", "--copy", dest="copy", action = "store",n help = "PATH of local working copy", type="string")n parser.add_option("--host", dest="host",n help = "host to connect to", type="string")n parser.add_option("-r", "--revision", dest="revision",n help = "override revision number from which to update sources", type="string")n parser.add_option("-p", "--password", dest="password",n help = "password to use when connecting to ftp", type="string")n parser.add_option("-d", "--directory", dest="directory",n help = "directory, relative to ftp root, in which project lies", type="string")n (options, args) = parser.parse_args()nn if None == options.copy:n print("-c option is required")n logging.getLogger('MyLogger').info('omitted -c option')n return 2n if None == options.host:n print("--host option is required")n logging.getLogger('MyLogger').info('omitted --host option')n return 2n if None == options.directory:n print("-d option is required")n logging.getLogger('MyLogger').info('omitted -d option')n return 2n if "/" == options.directory:n print("root dir is not allowed!")n logging.getLogger('MyLogger').warning('trying access root / dir is not allowed, please use subfolders')n return 2n tmp = {'copy': options.copy, 'revision': options.revision, 'host': options.host, 'directory': options.directory}n logging.getLogger('MyLogger').debug('called with these options: '+' '.join('-%s %s' % (k, v) for k, v in tmp.items()))n # here I MUST check if network is reachablen port = 21n username, host = options.host.split('@',1)n if False == is_server_reachable(host, port): #checking FTP port 21n print('ftp service on specified host is not available')n logging.getLogger('MyLogger').critical('host '+options.host+':'+str(port)+' was unreachable')n return 1n base_revision = 0n head_revision = 0n url = ''n path = options.copy#'C:\Program Files\CruiseControl\projects\email_sender_server\source'n # here I must take revision number into considerationn if None == options.revision:n cmd = 'svn info -r BASE "' + path + '"'n logging.getLogger('MyLogger').debug(cmd)n svn = Popen(cmd, stdout = PIPE)n i = 0n for line in svn.stdout.readlines():n i = i + 1n if i < 9:n l = line[:-2].decode('cp1251').split(': ',1)n if l[0] == 'Revision': base_revision = l[1]n svn.wait()n else:n base_revision = options.revision n cmd = 'svn info -r HEAD "' + path + '"'n logging.getLogger('MyLogger').debug(cmd) n svn = Popen(cmd, stdout = PIPE)n i = 0n for line in svn.stdout.readlines():n i = i + 1n if i < 9:n l = line[:-2].decode('cp1251').split(': ',1)n if l[0] == 'Revision': head_revision = l[1]n if l[0] == 'URL': url = l[1] n info = {'head': head_revision, 'base': base_revision, 'url': url}n svn.wait() n cmd = "svn diff --summarize -r"+info['base']+":"+info['head']+' "'+info['url'] + '"'n logging.getLogger('MyLogger').debug(cmd)n svn = Popen(cmd, stdout = PIPE)n commands = []n f = open("C:\\scenario.txt", "w")n for line in svn.stdout.readlines():n l = line.decode('866')[:-1]n action = l[0:7].strip()n source = l[7:]n if source == info['url']:n continuen commands.append((action,source))n f.write(action + "," + source)n f.close()n svn.wait()nn logging.getLogger('MyLogger').debug('updating local working copy')n cmd = 'svn up "'+path+'"'n logging.getLogger('MyLogger').debug(cmd)n logging.getLogger('MyLogger').debug('at revision '+head_revision)n svn = Popen(cmd)n svn.wait()n import osn cmd = []n for line in commands:n action,source = linen #logging.getLogger('MyLogger').info('source: '+source.strip()+' action: '+action+' url:'+info['url'])n if source.strip() == info['url'].strip():n print(source) n continuen onserver = source.replace(info['url'] + "/", "").strip()n action = action[0:1]n if action == 'D':n c = 'rm -rf "./'+onserver + '"'n if c not in cmd:n cmd.append(c)n c = 'rm -f "./'+onserver + '"' n if c not in cmd:n cmd.append(c)n else:n dirs_to_cr = onserver.split('/')n if os.path.isdir(path + "\\" + onserver):n c = 'mkdir -p "./'+"/".join(dirs_to_cr[j] for j in range(0,len(dirs_to_cr)))+'"'n if c not in cmd:n cmd.append(c)n else:n if 1 < len(dirs_to_cr):n c = 'mkdir -p "./'+"/".join(dirs_to_cr[j] for j in range(0,len(dirs_to_cr)-1))+'"'n if c not in cmd: n cmd.append(c)n c = 'put "/cygdrive/' + path[0:1] + path[2:].replace("\\", '/') + '/' + onserver + '" -o "./' + onserver + '"'n if c not in cmd:n cmd.append(c)n if 0 == len(cmd):n print("I got nothing to do this time :(")n logging.getLogger('MyLogger').info('nothing to do')n else:n cmd.insert(0, 'open -u '+username+','+options.password+' '+host)n cmd.insert(1, 'cd '+options.directory)n cmd.append("exit")n from tempfile import mkdtempn tmpdir = mkdtemp()n tmpscenario = tmpdir+"/ftp_scenario.txt"n f = open(tmpscenario, "wb")n for c in cmd:n logging.getLogger('MyLogger').info('lftp: ' + c)n f.write(bytes(c + chr(10),'cp1251'))n f.close()n logging.getLogger('MyLogger').debug('logging in to ftp')n ftp = get_ftp(tmpscenario)n logging.getLogger('MyLogger').info('communicating')n ftp.communicate()n logging.getLogger('MyLogger').info('finished communicating')n ftp.wait() n import osn os.remove(tmpscenario)n os.rmdir(tmpdir)n logging.getLogger('MyLogger').debug('main application finished') n return 0nndef get_ftp(script):n cmd = 'C:/lftp/lftp.exe -f "'+windows_path_to_cygpath(script)+'"'n logging.getLogger('MyLogger').debug(cmd)n ftp = Popen(cmd)n #ftp.stdin.write(bytes(cmd+"\n", 'cp1251'))n return ftpnndef is_server_reachable(hostname, port):n import socketn sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)n try:n sock.connect((hostname, port))n except socket.error:n return False n sock.close()n return True nndef windows_path_to_cygpath(winpath):n return '/cygdrive/' + winpath[0:1] + winpath[2:].replace("\\", '/')
nn
python "C:\svn\apply_svn_changes\publish_web_site.py" --help