# Copyright 2012 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import threading import time import urllib2 from zuul.lib import gerrit from zuul.model import TriggerEvent, Change class GerritEventConnector(threading.Thread): """Move events from Gerrit to the scheduler.""" log = logging.getLogger("zuul.GerritEventConnector") def __init__(self, gerrit, sched): super(GerritEventConnector, self).__init__() self.gerrit = gerrit self.sched = sched self._stopped = False def stop(self): self._stopped = True self.gerrit.addEvent(None) def _handleEvent(self): data = self.gerrit.getEvent() if self._stopped: return event = TriggerEvent() event.type = data.get('type') change = data.get('change') if change: event.project_name = change.get('project') event.branch = change.get('branch') event.change_number = change.get('number') event.change_url = change.get('url') patchset = data.get('patchSet') if patchset: event.patch_number = patchset.get('number') event.refspec = patchset.get('ref') event.approvals = data.get('approvals', []) event.comment = data.get('comment') refupdate = data.get('refUpdate') if refupdate: event.project_name = refupdate.get('project') event.ref = refupdate.get('refName') event.oldrev = refupdate.get('oldRev') event.newrev = refupdate.get('newRev') self.sched.addEvent(event) self.gerrit.eventDone() def run(self): while True: if self._stopped: return try: self._handleEvent() except: self.log.exception("Exception moving Gerrit event:") class Gerrit(object): log = logging.getLogger("zuul.Gerrit") replication_timeout = 60 replication_retry_interval = 5 def __init__(self, config, sched): self.sched = sched self.server = config.get('gerrit', 'server') user = config.get('gerrit', 'user') if config.has_option('gerrit', 'sshkey'): sshkey = config.get('gerrit', 'sshkey') else: sshkey = None if config.has_option('gerrit', 'port'): port = config.get('gerrit', 'port') else: port = 29418 self.gerrit = gerrit.Gerrit(self.server, user, port, sshkey) self.gerrit.startWatching() self.gerrit_connector = GerritEventConnector( self.gerrit, sched) self.gerrit_connector.start() def stop(self): self.gerrit_connector.stop() self.gerrit_connector.join() def report(self, change, message, action): self.log.debug("Report change %s, action %s, message: %s" % (change, action, message)) if not change.number: self.log.debug("Change has no number; not reporting") return if not action: self.log.debug("No action specified; not reporting") return changeid = '%s,%s' % (change.number, change.patchset) return self.gerrit.review(change.project.name, changeid, message, action) def _getInfoRefs(self, project): url = "https://%s/p/%s/info/refs" % (self.server, project) data = urllib2.urlopen(url).read() ret = {} for line in data.split('\n'): if not line: continue revision, ref = line.split() ret[ref] = revision return ret def isMerged(self, change, head=None): self.log.debug("Checking if change %s is merged" % change) if not change.number: self.log.debug("Change has no number; considering it merged") # Good question. It's probably ref-updated, which, ah, # means it's merged. return True data = self.gerrit.query(change.number) change._data = data change.is_merged = self._isMerged(change) if not head: return change.is_merged if not change.is_merged: return False # Wait for the ref to show up in the repo head = 'refs/heads/' + head self.log.debug("Waiting for %s to appear in git repo" % (change)) start = time.time() while time.time() - start < self.replication_timeout: refs = {} try: refs = self._getInfoRefs(change.project.name) except: self.log.exception("Exception looking for ref in head %s" % head) ref = refs.get(head, '') if change._data['currentPatchSet']['revision'] == ref: self.log.debug("Change %s is in the git repo" % (change)) return True time.sleep(self.replication_retry_interval) self.log.debug("Change %s did not appear in the git repo" % (change)) return False def _isMerged(self, change): data = change._data if not data: return False status = data.get('status') if not status: return False self.log.debug("Change %s status: %s" % (change, status)) if status == 'MERGED' or status == 'SUBMITTED': return True def canMerge(self, change, allow_needs): if not change.number: self.log.debug("Change has no number; considering it merged") # Good question. It's probably ref-updated, which, ah, # means it's merged. return True data = change._data if not data: return False if not 'submitRecords' in data: return False try: for sr in data['submitRecords']: if sr['status'] == 'OK': return True elif sr['status'] == 'NOT_READY': for label in sr['labels']: if label['status'] == 'OK': continue elif label['status'] in ['NEED', 'REJECT']: # It may be our own rejection, so we ignore if label['label'].lower() not in allow_needs: return False continue else: # IMPOSSIBLE return False else: # CLOSED, RULE_ERROR return False except: self.log.exception("Exception determining whether change" "%s can merge:" % change) return False return True def getChange(self, number, patchset, changes=None): self.log.info("Getting information for %s,%s" % (number, patchset)) if changes is None: changes = {} data = self.gerrit.query(number) project = self.sched.projects[data['project']] change = Change(project) change._data = data change.number = number change.patchset = patchset change.project = project change.branch = data['branch'] change.url = data['url'] max_ps = 0 for ps in data['patchSets']: if ps['number'] == patchset: change.refspec = ps['ref'] if int(ps['number']) > int(max_ps): max_ps = ps['number'] if max_ps == patchset: change.is_current_patchset = True else: change.is_current_patchset = False change.is_merged = self._isMerged(change) if change.is_merged: # This change is merged, so we don't need to look any further # for dependencies. return change key = '%s,%s' % (number, patchset) changes[key] = change def cachedGetChange(num, ps): key = '%s,%s' % (num, ps) if key in changes: return changes.get(key) c = self.getChange(num, ps, changes) return c if 'dependsOn' in data: parts = data['dependsOn'][0]['ref'].split('/') dep_num, dep_ps = parts[3], parts[4] dep = cachedGetChange(dep_num, dep_ps) if not dep.is_merged: change.needs_change = dep if 'neededBy' in data: for needed in data['neededBy']: parts = needed['ref'].split('/') dep_num, dep_ps = parts[3], parts[4] dep = cachedGetChange(dep_num, dep_ps) if not dep.is_merged and dep.is_current_patchset: change.needed_by_changes.append(dep) return change