Rate this post

Данная статья предназначена для разработчиков, которые работают с SVN и удаленных хостингом с доступом по ftp и ssh, и ограничением на установку чего либо.nnhosting-svnnnПроцесс деплоя очень прост, обновление файлов на хостинг, которое реализовано скриптом. Программист формирует код, после выполняется скрипт который заменяет текущие файлы, можно запускать либо автоматически либо вручную.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[sociallocker]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[/sociallocker]nnСкрипт также можно загрузить с ключем —helpn

python "C:\svn\apply_svn_changes\publish_web_site.py" --help