# Copyright (C) 2014 - 2015 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import cliapp from contextlib import closing, contextmanager from fs.tempfs import TempFS import os import re import logging from firehose.config import FirehoseConfig import morphlib from debian.debian_support import Version @contextmanager def firehose_git(app): try: username = (app.runcmd_unchecked(["git", "config", "--global", "user.name"]))[1].strip() email = (app.runcmd_unchecked(["git", "config", "--global", "user.email"]))[1].strip() app.runcmd(["git", "config", "--global", "user.name", "Firehose merge bot"]) app.runcmd(["git", "config", "--global", "user.email", "firehose@merge.bot"]) yield () finally: app.runcmd(["git", "config", "--global", "user.name", username]) app.runcmd(["git", "config", "--global", "user.email", email]) class FirehosePlugin(cliapp.Plugin): sha_filename="" def enable(self): logging.basicConfig(filename='/var/log/firehose.log',level=logging.INFO) logging.info("Firehose plugin enabled") self.app.add_subcommand('firehose', self.firehose_cmd, arg_synopsis='some-firehose.yaml...') self.app.settings.string(['gerrit-username'], 'Username for gerrit', default=None) self.app.settings.string(['gerrit-url'], 'URL for gerrit', default='gerrit.baserock.org') self.app.settings.string(['git-username'], 'git username', default=None) self.app.settings.string(['git-email'], 'git email address', default=None) def disable(self): logging.info("Firehose plugin disabled") pass def firehose_cmd(self, args): confs = [] if len(args) == 0: raise cliapp.AppException("Expected list of firehoses on command line") for fname in args: with open(fname, "r") as fh: confs.append(FirehoseConfig(fname, fh)) # Ensure all incoming configurations are based on, and landing in, the # same repository. This is because we're only supporting an aggregated # integration mode for now. if len(set(c.landing_repo for c in confs)) > 1: raise cliapp.AppException("Not all firehoses have the same landing repo") if len(set(c.landing_baseref for c in confs)) > 1: raise cliapp.AppException("Not all firehoses have the same landing baseref") if len(set(c.landing_destref for c in confs)) > 1: raise cliapp.AppException("Not all firehoses have the same landing destref") # Ensure that all incoming configurations have unique things they are # integrating into. Note: this allows for the same upstream to be # tracked in multiple configs, providing they are all targetting a # different part of the system. (e.g. linux kernels for multiple BSPs) if len(set("%s:%s" % (c.landing_stratum, c.landing_chunk) for c in confs)) != len(confs): raise cliapp.AppException("Not all firehoses have unique landing locations") with closing(TempFS(temp_dir=self.app.settings['tempdir'])) as tfs, \ firehose_git(self.app): self.base_path = tfs.getsyspath("/") self.make_workspace() self.setup_git() self.make_branch(confs[0].landing) self.log_sha(os.path.basename(fname.replace('.yaml',''))) self.insert_githook() self.reset_to_tracking(confs[0].landing) self.load_morphologies() for c in confs: self.update_for_conf(c) if self.updated_morphologies(): self.commit_and_push(os.path.basename(fname.replace(".yaml", ""))) def make_path(self, *subpath): logging.info("make_path: " + self.base_path) return os.path.join(self.base_path, *subpath) def make_workspace(self): logging.info("make_workspace: " + self.make_path("ws")) self.app.subcommands['init']([self.make_path("ws")]) def setup_git(self): git_username = self.app.settings['git-username'] git_email = self.app.settings['git-email'] if (len(git_username) > 1) and (len(git_email) > 1): logging.info("setup_git: " + git_username + " " + git_email) self.app.runcmd(['git', 'config', '--global', 'user.name', git_username]) self.app.runcmd(['git', 'config', '--global', 'user.email', git_email]) def make_branch(self, root): os.chdir(self.make_path("ws")) try: logging.info("make_branch: " + \ root['repo'] + " " + root['destref'] + " " + root['baseref']) self.app.subcommands['branch']([root['repo'], root['destref'], root['baseref']]) except cliapp.AppException, ae: if "already exists in" in str(ae): self.app.subcommands['checkout']([root['repo'], root['destref']]) else: raise repopath = root['repo'].replace(':', '/') self.gitpath = self.make_path("ws", root['destref'], repopath) logging.info("gitpath: " + self.gitpath) def log_sha(self, name): sha = self.app.runcmd(['git', 'rev-parse', 'HEAD'], cwd=self.gitpath).strip() self.sha_filename = '/var/lib/firehose/'+name+'_log.json' if (os.path.exists(os.path.abspath(self.sha_filename))): with open(self.sha_filename) as f: log = f.read() if sha in log: logging.info('Everything up-to-date') print 'Everything up-to-date' exit(0) # Save the hash to a file with open(self.sha_filename, 'w') as log: log.write('{\"sha\":\"'+sha+'\"}') logging.info('log_sha: wrote {\"sha\":\"'+sha+'\"}') def insert_githook(self): gerrit_username = self.app.settings['gerrit-username'] gerrit_url = self.app.settings['gerrit-url'] status, output, error = \ self.app.runcmd_unchecked(['scp', '-p', '-P', '29418', gerrit_username+'@'+ gerrit_url+':hooks/commit-msg', self.gitpath+ '/.git/hooks/commit-msg'], cwd=self.gitpath) if status != 0: # Remove the previously recorded hash os.remove(self.sha_filename) if "publickey" in error: raise cliapp.AppException( "Cannot insert githook. "+ "Check your ssh public key is registered in gerrit") else: self.app.output.write(error) raise cliapp.AppException( "Woah! unable to insert githook") def reset_to_tracking(self, root): logging.info('reset_to_tracking') branch_head_sha = self.app.runcmd(['git', 'rev-parse', 'HEAD'], cwd=self.gitpath).strip() self.app.runcmd(['git', 'reset', '--hard', 'origin/%s' % root['baseref']], cwd=self.gitpath) self.app.runcmd(['git', 'reset', '--soft', branch_head_sha], cwd=self.gitpath) def load_morphologies(self): logging.info('load_morphologies') ws = morphlib.workspace.open(self.gitpath) sb = morphlib.sysbranchdir.open_from_within(self.gitpath) loader = morphlib.morphloader.MorphologyLoader() morphs = morphlib.morphset.MorphologySet() for morph in sb.load_all_morphologies(loader): morphs.add_morphology(morph) self.ws = ws self.sb = sb self.loader = loader self.morphs = morphs self.lrc, self.rrc = morphlib.util.new_repo_caches(self.app) def find_cached_repo(self, stratum, chunk): logging.info('find_cached_repo') urls = [] def wanted_spec(m, kind, spec): if not m['kind'] == 'stratum' and kind == 'chunks': return False if m.get('name') == stratum and spec.get('name') == chunk: return True def process_spec(m, kind, spec): urls.append(spec['repo']) return False self.morphs.traverse_specs(process_spec, wanted_spec) if len(urls) != 1: # Remove the previously recorded hash os.remove(self.sha_filename) raise cliapp.AppException( "Woah! expected 1 chunk matching %s:%s (got %d)" % ( stratum, chunk, len(urls))) return self.lrc.get_updated_repo(urls[0]) def git_in_repo_unchecked(self, repo, *args): args = list(args) args.insert(0, "git") return self.app.runcmd_unchecked(args, cwd=repo.path) def git_in_repo(self, repo, *args): args = list(args) args.insert(0, "git") return self.app.runcmd(args, cwd=repo.path) def all_shas_for_refs(self, repo): logging.info('all_shas_for_refs') all_lines = self.git_in_repo(repo, "for-each-ref").strip().split("\n") for refline in all_lines: (sha, objtype, name) = refline.split(None, 2) if objtype == 'tag': sha = self.git_in_repo(repo, "rev-list", "-1", sha).strip() yield name, sha def interested_in_ref(self, conf, refname): logging.info('interested_in_ref') if conf.tracking_mode == 'follow-tip': return refname == conf.tracking_ref elif conf.tracking_mode == 'refs': return any(re.match(filterstr, refname) for filterstr in conf.tracking_filters) else: raise FirehoseConfigError(conf, "Unknown value: %s" % conf.tracking_mode, ["tracking", "mode"]) def rewrite_ref(self, conf, ref): logging.info('rewrite_ref') if conf.tracking_mode == 'refs': for transform in conf.tracking_transforms: ref = re.sub(transform['match'], transform['replacement'], ref) return ref def compare_refs(self, ref1, ref2): logging.info('compare_refs: ' + str(ref1) + " " + str(ref2)) if ref1[0] == ref2[0]: return 0 v1 = Version(ref1[0].replace("/", "-")) v2 = Version(ref2[0].replace("/", "-")) if v1 < v2: return -1 return 1 def sanitise_refname(self, refname): if refname.startswith("refs/"): logging.info('sanitise_refname: ' + \ refname + " -> " + "/".join((refname.split("/"))[2:])) return "/".join((refname.split("/"))[2:]) else: return refname def update_refs(self, stratum, chunk, sha, refname): logging.info('update_refs') def wanted_spec(m, kind, spec): if not m['kind'] == 'stratum' and kind == 'chunks': return False if m.get('name') == stratum and spec.get('name') == chunk: return True def process_spec(m, kind, spec): spec['ref'] = sha spec['unpetrify-ref'] = refname return True self.morphs.traverse_specs(process_spec, wanted_spec) def update_for_conf(self, conf): stratum = conf.landing_stratum chunk = conf.landing_chunk logging.info('update_for_conf: ' + stratum + " " + chunk) crc = self.find_cached_repo(stratum, chunk) interesting_refs = [ (self.rewrite_ref(conf, name), name, sha) for (name, sha) in self.all_shas_for_refs(crc) if self.interested_in_ref(conf, name)] interesting_refs.sort(self.compare_refs) (_, refname, sha) = interesting_refs.pop() refname = self.sanitise_refname(refname) self.update_refs(stratum, chunk, sha, refname) def updated_morphologies(self): logging.info('updated_morphologies') if not any(m.dirty for m in self.morphs.morphologies): return False for morph in self.morphs.morphologies: if morph.dirty: self.loader.unset_defaults(morph) self.loader.save_to_file( self.sb.get_filename(morph.repo_url, morph.filename), morph) morph.dirty = False return True def commit_and_push(self, name): gerrit_username = self.app.settings['gerrit-username'] gerrit_url = self.app.settings['gerrit-url'] commit_msg = 'Update to '+name+' ref in definitions.git' branch_name = 'HEAD:refs/for/master/firehose/'+name (code, out, err) = self.app.runcmd_unchecked( ['git', 'commit', '-a', '-m', commit_msg], cwd=self.gitpath) logging.info('commit_and_push: ' + str(code)) if code == 0: status, output, error = \ self.app.runcmd_unchecked(['git', 'push', 'ssh://'+gerrit_username+ '@'+gerrit_url+ ':29418/baserock/'+ 'baserock/definitions', branch_name], cwd=self.gitpath) if status != 0: # Remove the previously recorded hash os.remove(self.sha_filename) if "publickey" in error: raise cliapp.AppException( "Could not push to gerrit. "+ "Check your ssh public key is "+ "registered in gerrit") else: self.app.output.write(error) raise cliapp.AppException( "Woah! unable to push changes to gerrit")