diff options
author | Oliver Poignant <oliver@poignant.se> | 2015-08-18 00:08:58 +0200 |
---|---|---|
committer | Oliver Poignant <oliver@poignant.se> | 2015-08-18 00:08:58 +0200 |
commit | 53bfd3951daad49a28d773b54e608ee4e3f657f4 (patch) | |
tree | 47a47611e93968fb5de982262f71f538f40fec11 /GitAutoDeploy.py | |
parent | c2925d5784558e52d360150f8fff5e3408cb2065 (diff) | |
download | Git-Auto-Deploy-53bfd3951daad49a28d773b54e608ee4e3f657f4.zip Git-Auto-Deploy-53bfd3951daad49a28d773b54e608ee4e3f657f4.tar.gz Git-Auto-Deploy-53bfd3951daad49a28d773b54e608ee4e3f657f4.tar.bz2 |
Refactoring
Diffstat (limited to 'GitAutoDeploy.py')
-rwxr-xr-x | GitAutoDeploy.py | 581 |
1 files changed, 315 insertions, 266 deletions
diff --git a/GitAutoDeploy.py b/GitAutoDeploy.py index 7b5763a..d5337f9 100755 --- a/GitAutoDeploy.py +++ b/GitAutoDeploy.py @@ -1,325 +1,195 @@ #!/usr/bin/env python -import json, urlparse, sys, os, signal, socket, re -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from subprocess import call -from threading import Timer +class GitWrapper(): + """Wraps the git client. Currently uses git through shell command invocations.""" -class GitAutoDeploy(BaseHTTPRequestHandler): + def __init__(self): + pass - CONFIG_FILE_PATH = './GitAutoDeploy.conf.json' - config = None - debug = True - quiet = False - daemon = False + @staticmethod + def pull(repo_config): + """Pulls the latest version of the repo from the git server""" + from subprocess import call - @classmethod - def get_config(cls): + branch = ('branch' in repo_config) and repo_config['branch'] or 'master' - if not cls.config: + print "\nPost push request received" + print 'Updating ' + repo_config['path'] - try: - config_string = open(cls.CONFIG_FILE_PATH).read() - - except: - print "Could not load %s file" % cls.CONFIG_FILE_PATH - sys.exit(2) + res = call(['sleep 5; cd "' + + repo_config['path'] + + '" && unset GIT_DIR && git fetch origin && git update-index --refresh && git reset --hard origin/' + + branch + ' && git submodule init && git submodule update'], shell=True) + call(['echo "Pull result: ' + str(res) + '"'], shell=True) + return res - try: - cls.config = json.loads(config_string) + @staticmethod + def deploy(repo_config): + """Executes any supplied post-pull deploy command""" + from subprocess import call - except: - print "%s file is not valid JSON" % cls.CONFIG_FILE_PATH - sys.exit(2) + path = repo_config['path'] - for repository in cls.config['repositories']: + cmds = [] + if 'deploy' in repo_config: + cmds.append(repo_config['deploy']) - if not os.path.isdir(repository['path']): + gd = GitAutoDeploy().get_config()['global_deploy'] + if len(gd[0]) is not 0: + cmds.insert(0, gd[0]) + if len(gd[1]) is not 0: + cmds.append(gd[1]) - print "Directory %s not found" % repository['path'] - call(['git clone --recursive '+repository['url']+' '+repository['path']], shell=True) + print 'Executing deploy command(s)' - if not os.path.isdir(repository['path']): - print "Unable to clone repository %s" % repository['url'] - sys.exit(2) + for cmd in cmds: + call(['cd "' + path + '" && ' + cmd], shell=True) - else: - print "Repository %s successfully cloned" % repository['url'] - if not os.path.isdir(repository['path'] + '/.git'): - print "Directory %s is not a Git repository" % repository['path'] - sys.exit(2) +from BaseHTTPServer import BaseHTTPRequestHandler - cls.clear_lock(repository['path']) - return cls.config +class WebhookRequestHandler(BaseHTTPRequestHandler): + """Extends the BaseHTTPRequestHandler class and handles the incoming HTTP requests.""" def do_POST(self): - urls = self.parse_request() - self.respond() - Timer(1.0, self.do_process, [urls]).start() - - def do_process(self, urls): - - for url in urls: - - repos = self.getMatchingPaths(url) - for repo in repos: - - if self.lock(repo['path']): - - try: - n = 4 - while 0 < n and 0 != self.pull(repo['path'], repo['branch']): - --n - if 0 < n: - self.deploy(repo['path']) - - except: - call(['echo "Error during \'pull\' or \'deploy\' operation on path: ' + repo['path'] + '"'], - shell=True) - - finally: - self.unlock(repo['path']) - - def parse_request(self): - content_type = self.headers.getheader('content-type') - length = int(self.headers.getheader('content-length')) - body = self.rfile.read(length) - - items = [] - - try: - if content_type == "application/json" or content_type == "application/x-www-form-urlencoded": - post = urlparse.parse_qs(body) - - # If payload is missing, we assume GitLab syntax. - if content_type == "application/json" and "payload" not in post: - mode = "github" - - # If x-www-form-urlencoded, we assume BitBucket syntax. - elif content_type == "application/x-www-form-urlencoded": - mode = "bitbucket" - - # Oh GitLab, dear GitLab... - else: - mode = "gitlab" - - - if mode == "github": - response = json.loads(body) - items.append(response['repository']['url']) - - elif mode == "bitbucket": - for itemString in post['payload']: - item = json.loads(itemString) - items.append("ssh://hg@bitbucket.org" + item['repository']['absolute_url'][0:-1]) - - # Otherwise, we assume GitHub/BitBucket syntax. - elif mode == "gitlab": - for itemString in post['payload']: - item = json.loads(itemString) - items.append(item['repository']['url']) - - # WTF?! - else: - pass + """Invoked on incoming POST requests""" + from threading import Timer - except: - pass + # Extract repository URL(s) from incoming request body + repo_urls = self.get_repo_urls_from_request() - return items - - def getMatchingPaths(self, repoUrl): - res = [] - config = self.get_config() - for repository in config['repositories']: - if repository['url'] == repoUrl: - res.append({ - 'path': repository['path'], - 'branch': ('branch' in repository) and repository['branch'] or 'master' - }) - return res - - def respond(self): self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() - def lock(self, path): - return 0 == call(['sh lock.sh "' + path + '"'], shell=True) - - def unlock(self, path): - call(['sh unlock.sh "' + path + '"'], shell=True) - - @classmethod - def clear_lock(cls, path): - call(['sh clear_lock.sh "' + path + '"'], shell=True) - - def pull(self, path, branch): - if not self.quiet: - print "\nPost push request received" - print 'Updating ' + path - res = call(['sleep 5; cd "' + path + '" && unset GIT_DIR && git fetch origin && git update-index --refresh && git reset --hard origin/' + branch + ' && git submodule init && git submodule update'], shell=True) - call(['echo "Pull result: ' + str(res) + '"'], shell=True) - return res - - def deploy(self, path): - config = self.get_config() - for repository in config['repositories']: - if repository['path'] == path: - cmds = [] - if 'deploy' in repository: - cmds.append(repository['deploy']) - - gd = config['global_deploy'] - if len(gd[0]) is not 0: - cmds.insert(0, gd[0]) - if len(gd[1]) is not 0: - cmds.append(gd[1]) - - if(not self.quiet): - print 'Executing deploy command(s)' - for cmd in cmds: - call(['cd "' + path + '" && ' + cmd], shell=True) - - break - - -class GitAutoDeployMain: - - server = None - - def run(self): - for arg in sys.argv: - - if arg == '-d' or arg == '--daemon-mode': - GitAutoDeploy.daemon = True - GitAutoDeploy.quiet = True + # Wait one second before we do git pull (why?) + Timer(1.0, GitAutoDeploy.process_repo_urls, [repo_urls]).start() - if arg == '-q' or arg == '--quiet': - GitAutoDeploy.quiet = True + def get_repo_urls_from_request(self): + """Parses the incoming request and extracts all possible URLs to the repository in question. Since repos can + have both ssh://, git:// and https:// URIs, and we don't know which of them is specified in the config, we need + to collect and compare them all.""" + import json - if arg == '--ssh-keyscan': - print 'Scanning repository hosts for ssh keys...' - self.ssh_key_scan() + #content_type = self.headers.getheader('content-type') + length = int(self.headers.getheader('content-length')) + body = self.rfile.read(length) - if arg == '--force': - print '[KILLER MODE] Warning: The --force option will try to kill any process ' \ - 'using %s port. USE AT YOUR OWN RISK' % GitAutoDeploy.get_config()['port'] - self.kill_them_all() + data = json.loads(body) - if GitAutoDeploy.daemon: - pid = os.fork() - if pid > 0: - sys.exit(0) - os.setsid() + repo_urls = [] - self.create_pidfile() + gitlab_event = self.headers.getheader('X-Gitlab-Event') + github_event = self.headers.getheader('X-GitHub-Event') + user_agent = self.headers.getheader('User-Agent') - if not GitAutoDeploy.quiet: - print 'GitHub & GitLab auto deploy service v 0.1 started' - else: - print 'GitHub & GitLab auto deploy service v 0.1 started in daemon mode' + if not 'repository' in data: + print "ERROR - Unable to recognize data format" + return repo_urls - try: - self.server = HTTPServer((GitAutoDeploy.get_config()['host'], GitAutoDeploy.get_config()['port']), GitAutoDeploy) - sa = self.server.socket.getsockname() - print "Listeing on", sa[0], "port", sa[1] - self.server.serve_forever() + # Assume GitLab if the X-Gitlab-Event HTTP header is set + if gitlab_event: - except socket.error, e: + print "Received '%s' event from GitLab" % gitlab_event - if not GitAutoDeploy.quiet and not GitAutoDeploy.daemon: - print "Error on socket: %s" % e - self.debug_diagnosis() - sys.exit(1) + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_http_url', 'git_ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) - def ssh_key_scan(self): + # Assume GitHub if the X-GitHub-Event HTTP header is set + elif github_event: - for repository in GitAutoDeploy.get_config()['repositories']: + print "Received '%s' event from GitHub" % github_event - url = repository['url'] - print "Scanning repository: %s" % url - m = re.match('.*@(.*?):', url) + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_url', 'clone_url', 'ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) - if m: - port = repository['port'] - port = '' if not port else ('-p' + port) - call(['ssh-keyscan -t ecdsa,rsa ' + port + ' ' + m.group(1) + ' >> $HOME/.ssh/known_hosts'], shell=True) + # Assume BitBucket if the User-Agent HTTP header is set to 'Bitbucket-Webhooks/2.0' (or something similar) + elif user_agent.lower().find('bitbucket') != -1: - else: - print 'Could not find regexp match in path: %s' % url + print "Received event from BitBucket" - def kill_them_all(self): + # One repository may posses multiple URLs for different protocols + for k in ['url', 'git_url', 'clone_url', 'ssh_url']: + if k in data['repository']: + repo_urls.append(data['repository'][k]) - pid = self.get_pid_on_port(GitAutoDeploy.get_config()['port']) + if 'full_name' in data['repository']: + repo_urls.append('git@bitbucket.org:%s.git' % data['repository']['full_name']) + repo_urls.append('https://oliverpoignant@bitbucket.org/%s.git' % data['repository']['full_name']) - if not pid: - print '[KILLER MODE] I don\'t know the number of pid that is using my configured port\n ' \ - '[KILLER MODE] Maybe no one? Please, use --force option carefully' - return False + else: + print "ERROR - Unable to recognize request origin. Don't know how to handle the request." - os.kill(pid, signal.SIGKILL) - return True - - def create_pidfile(self): - import sys - from os import path, makedirs + return repo_urls - pid_file_path = GitAutoDeploy.get_config()['pidfilepath'] - pid_file_dir = path.dirname(pid_file_path) - # Create necessary directory structure if needed - if not path.exists(pid_file_dir): +class GitAutoDeploy(object): - try: - makedirs(pid_file_dir) + CONFIG_FILE_PATH = './GitAutoDeploy.conf.json' + debug = True + daemon = False - except OSError as e: - print "Unable to create PID file: %s" % e - sys.exit(2) + _instance = None + _server = None + _config = None + def __new__(cls, *args, **kwargs): + """Overload constructor to enable Singleton access""" + if not cls._instance: + cls._instance = super(GitAutoDeploy, cls).__new__( + cls, *args, **kwargs) + return cls._instance - with open(pid_file_path, 'w') as f: - f.write(str(os.getpid())) + @staticmethod + def lock(path): + from subprocess import call + return 0 == call(['sh lock.sh "' + path + '"'], shell=True) - def read_pidfile(self): - with open(GitAutoDeploy.get_config()['pidfilepath'],'r') as f: - return f.readlines() + @staticmethod + def unlock(path): + from subprocess import call + call(['sh unlock.sh "' + path + '"'], shell=True) - def remove_pidfile(self): - os.remove(GitAutoDeploy.get_config()['pidfilepath']) + @staticmethod + def clear_lock(path): + from subprocess import call + call(['sh clear_lock.sh "' + path + '"'], shell=True) - def debug_diagnosis(self): - if not GitAutoDeploy.debug: + @staticmethod + def debug_diagnosis(): + if GitAutoDeploy.debug is False: return - port = GitAutoDeploy.get_config()['port'] - pid = self.get_pid_on_port(port) - - if not pid: + port = GitAutoDeploy().get_config()['port'] + pid = GitAutoDeploy.get_pid_on_port(port) + if pid is False: print 'I don\'t know the number of pid that is using my configured port' return - + print 'Process with pid number %s is using port %s' % (pid, port) with open("/proc/%s/cmdline" % pid) as f: cmdline = f.readlines() print 'cmdline ->', cmdline[0].replace('\x00', ' ') - def get_pid_on_port(self, port): + @staticmethod + def get_pid_on_port(port): + import os with open("/proc/net/tcp",'r') as f: file_content = f.readlines()[1:] pids = [int(x) for x in os.listdir('/proc') if x.isdigit()] - conf_port = str(GitAutoDeploy.get_config()['port']) + conf_port = str(port) mpid = False for line in file_content: - - if mpid: + if mpid is not False: break _, laddr, _, _, _, _, _, _, _, inode = line.split()[:10] @@ -329,7 +199,6 @@ class GitAutoDeployMain: continue for pid in pids: - try: path = "/proc/%s/fd" % pid if os.access(path, os.R_OK) is False: @@ -347,18 +216,196 @@ class GitAutoDeployMain: return mpid - def stop(self): - if self.server is not None: - self.server.socket.close() + @staticmethod + def process_repo_urls(urls): + from subprocess import call - def exit(self): + repo_config = GitAutoDeploy().get_matching_repo_config(urls) - if not GitAutoDeploy.quiet: - print '\nGoodbye' + if not repo_config: + print 'Unable to find any of the repository URLs in the config: %s' % ', '.join(urls) + return + + if GitAutoDeploy.lock(repo_config['path']): + + try: + n = 4 + while 0 < n and 0 != GitWrapper.pull(repo_config): + n -= 1 + if 0 < n: + GitWrapper.deploy(repo_config) + + except Exception as e: + print e + call(['echo "Error during \'pull\' or \'deploy\' operation on path: ' + repo_config['path'] + '"'], + shell=True) + + finally: + GitAutoDeploy.unlock(repo_config['path']) + + def get_config(self): + import json + import sys + import os + from subprocess import call + + if self._config: + return self._config + + try: + config_string = open(self.CONFIG_FILE_PATH).read() + + except Exception as e: + print "Could not load %s file\n" % self.CONFIG_FILE_PATH + raise e + + try: + self._config = json.loads(config_string) + + except Exception as e: + print "%s file is not valid JSON\n" % self.CONFIG_FILE_PATH + raise e + + for repository in self._config['repositories']: + + if not os.path.isdir(repository['path']): + + print "Directory %s not found" % repository['path'] + call(['git clone --recursive '+repository['url']+' '+repository['path']], shell=True) + + if not os.path.isdir(repository['path']): + print "Unable to clone repository %s" % repository['url'] + sys.exit(2) + + else: + print "Repository %s successfully cloned" % repository['url'] + + if not os.path.isdir(repository['path'] + '/.git'): + print "Directory %s is not a Git repository" % repository['path'] + sys.exit(2) + + self.clear_lock(repository['path']) + + return self._config + + def get_matching_repo_config(self, urls): + """Iterates over the various repo URLs provided as argument (git://, ssh:// and https:// for the repo) and + compare them to any repo URL specified in the config""" + + config = self.get_config() + + for url in urls: + for repo_config in config['repositories']: + if repo_config['url'] == url: + return repo_config + + def ssh_key_scan(self): + import re + from subprocess import call + + for repository in self.get_config()['repositories']: + + url = repository['url'] + print "Scanning repository: %s" % url + m = re.match('.*@(.*?):', url) + + if m is not None: + port = repository['port'] + port = '' if port is None else ('-p' + port) + call(['ssh-keyscan -t ecdsa,rsa ' + port + ' ' + m.group(1) + ' >> $HOME/.ssh/known_hosts'], shell=True) + + else: + print 'Could not find regexp match in path: %s' % url + + def kill_conflicting_processes(self): + import os + + pid = GitAutoDeploy.get_pid_on_port(self.get_config()['port']) + + if pid is False: + print '[KILLER MODE] I don\'t know the number of pid that is using my configured port\n ' \ + '[KILLER MODE] Maybe no one? Please, use --force option carefully' + return False + + os.kill(pid, signal.SIGKILL) + return True + + def create_pid_file(self): + import os + with open(self.get_config()['pidfilepath'], 'w') as f: + f.write(str(os.getpid())) + + def read_pid_file(self): + with open(self.get_config()['pidfilepath'],'r') as f: + return f.readlines() + + def remove_pid_file(self): + import os + os.remove(self.get_config()['pidfilepath']) + + def exit(self): + import sys - self.remove_pidfile() + print '\nGoodbye' + self.remove_pid_file() sys.exit(0) + def run(self): + from sys import argv + import sys + import os + from BaseHTTPServer import HTTPServer + import socket + + if '-d' in argv or '--daemon-mode' in argv: + self.daemon = True + + if '-q' in argv or '--quiet' in argv: + sys.stdout = open(os.devnull, 'w') + + if '--ssh-keygen' in argv: + print 'Scanning repository hosts for ssh keys...' + self.ssh_key_scan() + + if '--force' in argv: + print 'Attempting to kill any other process currently occupying port %s' % self.get_config()['port'] + self.kill_conflicting_processes() + + if GitAutoDeploy.daemon: + pid = os.fork() + if pid > 0: + sys.exit(0) + os.setsid() + + self.create_pid_file() + + if self.daemon: + print 'GitHub & GitLab auto deploy service v 0.1 started in daemon mode' + + # Disable output in daemon mode + sys.stdout = open(os.devnull, 'w') + + else: + print 'GitHub & GitLab auto deploy service v 0.1 started' + + try: + self._server = HTTPServer((self.get_config()['host'], self.get_config()['port']), WebhookRequestHandler) + sa = self._server.socket.getsockname() + print "Listening on", sa[0], "port", sa[1] + self._server.serve_forever() + + except socket.error, e: + + if not GitAutoDeploy.daemon: + print "Error on socket: %s" % e + GitAutoDeploy.debug_diagnosis() + + sys.exit(1) + + def stop(self): + if self._server is not None: + self._server.socket.close() + def signal_handler(self, signum, frame): self.stop() @@ -367,7 +414,7 @@ class GitAutoDeployMain: return elif signum == 2: - print '\nKeyboard Interrupt!!!' + print '\nRequested close by keyboard interrupt signal' elif signum == 6: print 'Requested close by SIGABRT (process abort signal). Code 6.' @@ -375,11 +422,13 @@ class GitAutoDeployMain: self.exit() if __name__ == '__main__': - gadm = GitAutoDeployMain() + import signal + + app = GitAutoDeploy() - signal.signal(signal.SIGHUP, gadm.signal_handler) - signal.signal(signal.SIGINT, gadm.signal_handler) - signal.signal(signal.SIGABRT, gadm.signal_handler) + signal.signal(signal.SIGHUP, app.signal_handler) + signal.signal(signal.SIGINT, app.signal_handler) + signal.signal(signal.SIGABRT, app.signal_handler) signal.signal(signal.SIGPIPE, signal.SIG_IGN) - gadm.run() + app.run()
\ No newline at end of file |