Files
zuul/zuul/model.py
T
James E. Blair c8a1e05849 Add queue-name parameter to job
Allow an optional queue-name parameter to be set for a job.
As projects with that job are combined with others into shared
change queues, give the queue that name.  This allows us to, say,
set the queue name of the tempest gate job to 'integrated' and
end up with the shared change queue of all the OpenStack integrated
projects named 'integrated'.

With that, we can do things like emit stats for the 'integrated'
queue.

Change-Id: Iafd218d7cd519312ccbf97de7c070e8d3b82038c
2014-03-20 10:26:07 -07:00

1133 lines
37 KiB
Python

# 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 re
import time
from uuid import uuid4
import extras
OrderedDict = extras.try_imports(['collections.OrderedDict',
'ordereddict.OrderedDict'])
MERGER_MERGE = 1 # "git merge"
MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve"
MERGER_CHERRY_PICK = 3 # "git cherry-pick"
MERGER_MAP = {
'merge': MERGER_MERGE,
'merge-resolve': MERGER_MERGE_RESOLVE,
'cherry-pick': MERGER_CHERRY_PICK,
}
PRECEDENCE_NORMAL = 0
PRECEDENCE_LOW = 1
PRECEDENCE_HIGH = 2
PRECEDENCE_MAP = {
None: PRECEDENCE_NORMAL,
'low': PRECEDENCE_LOW,
'normal': PRECEDENCE_NORMAL,
'high': PRECEDENCE_HIGH,
}
def time_to_seconds(s):
if s.endswith('s'):
return int(s[:-1])
if s.endswith('m'):
return int(s[:-1]) * 60
if s.endswith('h'):
return int(s[:-1]) * 60 * 60
if s.endswith('d'):
return int(s[:-1]) * 24 * 60 * 60
if s.endswith('w'):
return int(s[:-1]) * 7 * 24 * 60 * 60
raise Exception("Unable to parse time value: %s" % s)
class Pipeline(object):
"""A top-level pipeline such as check, gate, post, etc."""
def __init__(self, name):
self.name = name
self.description = None
self.failure_message = None
self.success_message = None
self.dequeue_on_new_patchset = True
self.job_trees = {} # project -> JobTree
self.manager = None
self.queues = []
self.precedence = PRECEDENCE_NORMAL
self.trigger = None
self.start_actions = None
self.success_actions = None
self.failure_actions = None
self.window = None
self.window_floor = None
self.window_increase_type = None
self.window_increase_factor = None
self.window_decrease_type = None
self.window_decrease_factor = None
def __repr__(self):
return '<Pipeline %s>' % self.name
def setManager(self, manager):
self.manager = manager
def addProject(self, project):
job_tree = JobTree(None) # Null job == job tree root
self.job_trees[project] = job_tree
return job_tree
def getProjects(self):
return sorted(self.job_trees.keys(), lambda a, b: cmp(a.name, b.name))
def addQueue(self, queue):
self.queues.append(queue)
def getQueue(self, project):
for queue in self.queues:
if project in queue.projects:
return queue
return None
def getJobTree(self, project):
tree = self.job_trees.get(project)
return tree
def getJobs(self, changeish):
tree = self.getJobTree(changeish.project)
if not tree:
return []
return changeish.filterJobs(tree.getJobs())
def _findJobsToRun(self, job_trees, item):
torun = []
if item.item_ahead:
# Only run jobs if any 'hold' jobs on the change ahead
# have completed successfully.
if self.isHoldingFollowingChanges(item.item_ahead):
return []
for tree in job_trees:
job = tree.job
result = None
if job:
if not job.changeMatches(item.change):
continue
build = item.current_build_set.getBuild(job.name)
if build:
result = build.result
else:
# There is no build for the root of this job tree,
# so we should run it.
torun.append(job)
# If there is no job, this is a null job tree, and we should
# run all of its jobs.
if result == 'SUCCESS' or not job:
torun.extend(self._findJobsToRun(tree.job_trees, item))
return torun
def findJobsToRun(self, item):
tree = self.getJobTree(item.change.project)
if not tree:
return []
return self._findJobsToRun(tree.job_trees, item)
def haveAllJobsStarted(self, item):
for job in self.getJobs(item.change):
build = item.current_build_set.getBuild(job.name)
if not build or not build.start_time:
return False
return True
def areAllJobsComplete(self, item):
for job in self.getJobs(item.change):
build = item.current_build_set.getBuild(job.name)
if not build or not build.result:
return False
return True
def didAllJobsSucceed(self, item):
for job in self.getJobs(item.change):
if not job.voting:
continue
build = item.current_build_set.getBuild(job.name)
if not build:
return False
if build.result != 'SUCCESS':
return False
return True
def didAnyJobFail(self, item):
for job in self.getJobs(item.change):
if not job.voting:
continue
build = item.current_build_set.getBuild(job.name)
if build and build.result and (build.result != 'SUCCESS'):
return True
return False
def isHoldingFollowingChanges(self, item):
for job in self.getJobs(item.change):
if not job.hold_following_changes:
continue
build = item.current_build_set.getBuild(job.name)
if not build:
return True
if build.result != 'SUCCESS':
return True
if not item.item_ahead:
return False
return self.isHoldingFollowingChanges(item.item_ahead)
def setResult(self, item, build):
if build.retry:
item.removeBuild(build)
elif build.result != 'SUCCESS':
# Get a JobTree from a Job so we can find only its dependent jobs
root = self.getJobTree(item.change.project)
tree = root.getJobTreeForJob(build.job)
for job in tree.getJobs():
fakebuild = Build(job, None)
fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild)
def setUnableToMerge(self, item, msg):
item.current_build_set.unable_to_merge = True
item.current_build_set.unable_to_merge_message = msg
root = self.getJobTree(item.change.project)
for job in root.getJobs():
fakebuild = Build(job, None)
fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild)
def setDequeuedNeedingChange(self, item):
item.dequeued_needing_change = True
root = self.getJobTree(item.change.project)
for job in root.getJobs():
fakebuild = Build(job, None)
fakebuild.result = 'SKIPPED'
item.addBuild(fakebuild)
def getChangesInQueue(self):
changes = []
for shared_queue in self.queues:
changes.extend([x.change for x in shared_queue.queue])
return changes
def getAllItems(self):
items = []
for shared_queue in self.queues:
items.extend(shared_queue.queue)
return items
def formatStatusHTML(self):
ret = ''
for queue in self.queues:
if len(self.queues) > 1:
s = 'Change queue: %s' % queue.name
ret += s + '\n'
ret += '-' * len(s) + '\n'
for item in queue.queue:
ret += self.formatStatus(item, html=True)
return ret
def formatStatusJSON(self):
j_pipeline = dict(name=self.name,
description=self.description)
j_queues = []
j_pipeline['change_queues'] = j_queues
for queue in self.queues:
j_queue = dict(name=queue.name)
j_queues.append(j_queue)
j_queue['heads'] = []
j_queue['window'] = queue.window
j_queue['dependent'] = queue.dependent
j_changes = []
for e in queue.queue:
if not e.item_ahead:
if j_changes:
j_queue['heads'].append(j_changes)
j_changes = []
j_changes.append(self.formatItemJSON(e))
if (len(j_changes) > 1 and
(j_changes[-2]['remaining_time'] is not None) and
(j_changes[-1]['remaining_time'] is not None)):
j_changes[-1]['remaining_time'] = max(
j_changes[-2]['remaining_time'],
j_changes[-1]['remaining_time'])
if j_changes:
j_queue['heads'].append(j_changes)
return j_pipeline
def formatStatus(self, item, indent=0, html=False):
changeish = item.change
indent_str = ' ' * indent
ret = ''
if html and hasattr(changeish, 'url') and changeish.url is not None:
ret += '%sProject %s change <a href="%s">%s</a>\n' % (
indent_str,
changeish.project.name,
changeish.url,
changeish._id())
else:
ret += '%sProject %s change %s based on %s\n' % (
indent_str,
changeish.project.name,
changeish._id(),
item.item_ahead)
for job in self.getJobs(changeish):
build = item.current_build_set.getBuild(job.name)
if build:
result = build.result
else:
result = None
job_name = job.name
if not job.voting:
voting = ' (non-voting)'
else:
voting = ''
if html:
if build:
url = build.url
else:
url = None
if url is not None:
job_name = '<a href="%s">%s</a>' % (url, job_name)
ret += '%s %s: %s%s' % (indent_str, job_name, result, voting)
ret += '\n'
return ret
def formatItemJSON(self, item):
changeish = item.change
ret = {}
ret['active'] = item.active
if hasattr(changeish, 'url') and changeish.url is not None:
ret['url'] = changeish.url
else:
ret['url'] = None
ret['id'] = changeish._id()
if item.item_ahead:
ret['item_ahead'] = item.item_ahead.change._id()
else:
ret['item_ahead'] = None
ret['items_behind'] = [i.change._id() for i in item.items_behind]
ret['failing_reasons'] = item.current_build_set.failing_reasons
ret['zuul_ref'] = item.current_build_set.ref
ret['project'] = changeish.project.name
ret['enqueue_time'] = int(item.enqueue_time * 1000)
ret['jobs'] = []
max_remaining = 0
for job in self.getJobs(changeish):
now = time.time()
build = item.current_build_set.getBuild(job.name)
elapsed = None
remaining = None
result = None
url = None
if build:
result = build.result
url = build.url
if build.start_time:
if build.end_time:
elapsed = int((build.end_time -
build.start_time) * 1000)
remaining = 0
else:
elapsed = int((now - build.start_time) * 1000)
if build.estimated_time:
remaining = max(
int(build.estimated_time * 1000) - elapsed,
0)
if remaining and remaining > max_remaining:
max_remaining = remaining
ret['jobs'].append(
dict(
name=job.name,
elapsed_time=elapsed,
remaining_time=remaining,
url=url,
result=result,
voting=job.voting))
if self.haveAllJobsStarted(item):
ret['remaining_time'] = max_remaining
else:
ret['remaining_time'] = None
return ret
class ActionReporter(object):
"""An ActionReporter has a reporter and its configured paramaters"""
def __repr__(self):
return '<ActionReporter %s, %s>' % (self.reporter, self.params)
def __init__(self, reporter, params):
self.reporter = reporter
self.params = params
def report(self, change, message):
"""Sends the built message off to the configured reporter.
Takes the change and message and adds the configured parameters.
"""
return self.reporter.report(change, message, self.params)
def getSubmitAllowNeeds(self):
"""Gets the submit allow needs from the reporter based off the
parameters."""
return self.reporter.getSubmitAllowNeeds(self.params)
class ChangeQueue(object):
"""DependentPipelines have multiple parallel queues shared by
different projects; this is one of them. For instance, there may
a queue shared by interrelated projects foo and bar, and a second
queue for independent project baz. Pipelines have one or more
PipelineQueues."""
def __init__(self, pipeline, dependent=True, window=0, window_floor=1,
window_increase_type='linear', window_increase_factor=1,
window_decrease_type='exponential', window_decrease_factor=2):
self.pipeline = pipeline
self.name = ''
self.assigned_name = None
self.generated_name = None
self.projects = []
self._jobs = set()
self.queue = []
self.dependent = dependent
self.window = window
self.window_floor = window_floor
self.window_increase_type = window_increase_type
self.window_increase_factor = window_increase_factor
self.window_decrease_type = window_decrease_type
self.window_decrease_factor = window_decrease_factor
def __repr__(self):
return '<ChangeQueue %s: %s>' % (self.pipeline.name, self.name)
def getJobs(self):
return self._jobs
def addProject(self, project):
if project not in self.projects:
self.projects.append(project)
self._jobs |= set(self.pipeline.getJobTree(project).getJobs())
names = [x.name for x in self.projects]
names.sort()
self.generated_name = ', '.join(names)
for job in self._jobs:
if job.queue_name:
if (self.assigned_name and
job.queue_name != self.assigned_name):
raise Exception("More than one name assigned to "
"change queue: %s != %s" %
(self.assigned_name, job.queue_name))
self.assigned_name = job.queue_name
self.name = self.assigned_name or self.generated_name
def enqueueChange(self, change):
item = QueueItem(self.pipeline, change)
self.enqueueItem(item)
item.enqueue_time = time.time()
return item
def enqueueItem(self, item):
item.pipeline = self.pipeline
if self.dependent and self.queue:
item.item_ahead = self.queue[-1]
item.item_ahead.items_behind.append(item)
self.queue.append(item)
def dequeueItem(self, item):
if item in self.queue:
self.queue.remove(item)
if item.item_ahead:
item.item_ahead.items_behind.remove(item)
for item_behind in item.items_behind:
if item.item_ahead:
item.item_ahead.items_behind.append(item_behind)
item_behind.item_ahead = item.item_ahead
item.item_ahead = None
item.items_behind = []
item.dequeue_time = time.time()
def moveItem(self, item, item_ahead):
if not self.dependent:
return False
if item.item_ahead == item_ahead:
return False
# Remove from current location
if item.item_ahead:
item.item_ahead.items_behind.remove(item)
for item_behind in item.items_behind:
if item.item_ahead:
item.item_ahead.items_behind.append(item_behind)
item_behind.item_ahead = item.item_ahead
# Add to new location
item.item_ahead = item_ahead
item.items_behind = []
if item.item_ahead:
item.item_ahead.items_behind.append(item)
return True
def mergeChangeQueue(self, other):
for project in other.projects:
self.addProject(project)
self.window = min(self.window, other.window)
# TODO merge semantics
def isActionable(self, item):
if self.dependent and self.window:
return item in self.queue[:self.window]
else:
return True
def increaseWindowSize(self):
if self.dependent:
if self.window_increase_type == 'linear':
self.window += self.window_increase_factor
elif self.window_increase_type == 'exponential':
self.window *= self.window_increase_factor
def decreaseWindowSize(self):
if self.dependent:
if self.window_decrease_type == 'linear':
self.window = max(
self.window_floor,
self.window - self.window_decrease_factor)
elif self.window_decrease_type == 'exponential':
self.window = max(
self.window_floor,
self.window / self.window_decrease_factor)
class Project(object):
def __init__(self, name):
self.name = name
self.merge_mode = MERGER_MERGE_RESOLVE
def __str__(self):
return self.name
def __repr__(self):
return '<Project %s>' % (self.name)
class Job(object):
def __init__(self, name):
# If you add attributes here, be sure to add them to the copy method.
self.name = name
self.queue_name = None
self.failure_message = None
self.success_message = None
self.failure_pattern = None
self.success_pattern = None
self.parameter_function = None
self.hold_following_changes = False
self.voting = True
self.branches = []
self._branches = []
self.files = []
self._files = []
def __str__(self):
return self.name
def __repr__(self):
return '<Job %s>' % (self.name)
def copy(self, other):
if other.failure_message:
self.failure_message = other.failure_message
if other.success_message:
self.success_message = other.success_message
if other.failure_pattern:
self.failure_pattern = other.failure_pattern
if other.success_pattern:
self.success_pattern = other.success_pattern
if other.parameter_function:
self.parameter_function = other.parameter_function
if other.branches:
self.branches = other.branches[:]
self._branches = other._branches[:]
if other.files:
self.files = other.files[:]
self._files = other._files[:]
self.hold_following_changes = other.hold_following_changes
self.voting = other.voting
def changeMatches(self, change):
matches_branch = False
for branch in self.branches:
if hasattr(change, 'branch') and branch.match(change.branch):
matches_branch = True
if hasattr(change, 'ref') and branch.match(change.ref):
matches_branch = True
if self.branches and not matches_branch:
return False
matches_file = False
for f in self.files:
if hasattr(change, 'files'):
for cf in change.files:
if f.match(cf):
matches_file = True
if self.files and not matches_file:
return False
return True
class JobTree(object):
""" A JobTree represents an instance of one Job, and holds JobTrees
whose jobs should be run if that Job succeeds. A root node of a
JobTree will have no associated Job. """
def __init__(self, job):
self.job = job
self.job_trees = []
def addJob(self, job):
t = JobTree(job)
self.job_trees.append(t)
return t
def getJobs(self):
jobs = []
for x in self.job_trees:
jobs.append(x.job)
jobs.extend(x.getJobs())
return jobs
def getJobTreeForJob(self, job):
if self.job == job:
return self
for tree in self.job_trees:
ret = tree.getJobTreeForJob(job)
if ret:
return ret
return None
class Build(object):
def __init__(self, job, uuid):
self.job = job
self.uuid = uuid
self.url = None
self.number = None
self.result = None
self.build_set = None
self.launch_time = time.time()
self.start_time = None
self.end_time = None
self.estimated_time = None
self.pipeline = None
self.canceled = False
self.retry = False
self.parameters = {}
self.worker = Worker()
def __repr__(self):
return ('<Build %s of %s on %s>' %
(self.uuid, self.job.name, self.worker))
class Worker(object):
"""A model of the worker running a job"""
def __init__(self):
self.name = "Unknown"
self.hostname = None
self.ips = []
self.fqdn = None
self.program = None
self.version = None
self.extra = {}
def updateFromData(self, data):
"""Update worker information if contained in the WORK_DATA response."""
self.name = data.get('worker_name', self.name)
self.hostname = data.get('worker_hostname', self.hostname)
self.ips = data.get('worker_ips', self.ips)
self.fqdn = data.get('worker_fqdn', self.fqdn)
self.program = data.get('worker_program', self.program)
self.version = data.get('worker_version', self.version)
self.extra = data.get('worker_extra', self.extra)
def __repr__(self):
return '<Worker %s>' % self.name
class BuildSet(object):
# Merge states:
NEW = 1
PENDING = 2
COMPLETE = 3
def __init__(self, item):
self.item = item
self.other_changes = []
self.builds = {}
self.result = None
self.next_build_set = None
self.previous_build_set = None
self.ref = None
self.commit = None
self.zuul_url = None
self.unable_to_merge = False
self.unable_to_merge_message = None
self.failing_reasons = []
self.merge_state = self.NEW
def setConfiguration(self):
# The change isn't enqueued until after it's created
# so we don't know what the other changes ahead will be
# until jobs start.
if not self.other_changes:
next_item = self.item.item_ahead
while next_item:
self.other_changes.append(next_item.change)
next_item = next_item.item_ahead
if not self.ref:
self.ref = 'Z' + uuid4().hex
def addBuild(self, build):
self.builds[build.job.name] = build
build.build_set = self
def removeBuild(self, build):
del self.builds[build.job.name]
def getBuild(self, job_name):
return self.builds.get(job_name)
def getBuilds(self):
keys = self.builds.keys()
keys.sort()
return [self.builds.get(x) for x in keys]
class QueueItem(object):
"""A changish inside of a Pipeline queue"""
def __init__(self, pipeline, change):
self.pipeline = pipeline
self.change = change # a changeish
self.build_sets = []
self.dequeued_needing_change = False
self.current_build_set = BuildSet(self)
self.build_sets.append(self.current_build_set)
self.item_ahead = None
self.items_behind = []
self.enqueue_time = None
self.dequeue_time = None
self.reported = False
self.active = False
def __repr__(self):
if self.pipeline:
pipeline = self.pipeline.name
else:
pipeline = None
return '<QueueItem 0x%x for %s in %s>' % (
id(self), self.change, pipeline)
def resetAllBuilds(self):
old = self.current_build_set
self.current_build_set.result = 'CANCELED'
self.current_build_set = BuildSet(self)
old.next_build_set = self.current_build_set
self.current_build_set.previous_build_set = old
self.build_sets.append(self.current_build_set)
def addBuild(self, build):
self.current_build_set.addBuild(build)
build.pipeline = self.pipeline
def removeBuild(self, build):
self.current_build_set.removeBuild(build)
def setReportedResult(self, result):
self.current_build_set.result = result
class Changeish(object):
"""Something like a change; either a change or a ref"""
def __init__(self, project):
self.project = project
def equals(self, other):
raise NotImplementedError()
def isUpdateOf(self, other):
raise NotImplementedError()
def filterJobs(self, jobs):
return filter(lambda job: job.changeMatches(self), jobs)
def getRelatedChanges(self):
return set()
class Change(Changeish):
def __init__(self, project):
super(Change, self).__init__(project)
self.branch = None
self.number = None
self.url = None
self.patchset = None
self.refspec = None
self.files = []
self.needs_change = None
self.needed_by_changes = []
self.is_current_patchset = True
self.can_merge = False
self.is_merged = False
self.failed_to_merge = False
self.approvals = []
def _id(self):
return '%s,%s' % (self.number, self.patchset)
def __repr__(self):
return '<Change 0x%x %s>' % (id(self), self._id())
def equals(self, other):
if self.number == other.number and self.patchset == other.patchset:
return True
return False
def isUpdateOf(self, other):
if ((hasattr(other, 'number') and self.number == other.number) and
(hasattr(other, 'patchset') and
self.patchset is not None and
other.patchset is not None and
int(self.patchset) > int(other.patchset))):
return True
return False
def getRelatedChanges(self):
related = set()
if self.needs_change:
related.add(self.needs_change)
for c in self.needed_by_changes:
related.add(c)
related.update(c.getRelatedChanges())
return related
class Ref(Changeish):
def __init__(self, project):
super(Ref, self).__init__(project)
self.ref = None
self.oldrev = None
self.newrev = None
def _id(self):
return self.newrev
def __repr__(self):
rep = None
if self.newrev == '0000000000000000000000000000000000000000':
rep = '<Ref 0x%x deletes %s from %s' % (
id(self), self.ref, self.oldrev)
elif self.oldrev == '0000000000000000000000000000000000000000':
rep = '<Ref 0x%x creates %s on %s>' % (
id(self), self.ref, self.newrev)
else:
# Catch all
rep = '<Ref 0x%x %s updated %s..%s>' % (
id(self), self.ref, self.oldrev, self.newrev)
return rep
def equals(self, other):
if (self.project == other.project
and self.ref == other.ref
and self.newrev == other.newrev):
return True
return False
def isUpdateOf(self, other):
return False
class NullChange(Changeish):
def __repr__(self):
return '<NullChange for %s>' % (self.project)
def _id(self):
return None
def equals(self, other):
return False
def isUpdateOf(self, other):
return False
class TriggerEvent(object):
def __init__(self):
self.data = None
# common
self.type = None
self.project_name = None
self.trigger_name = None
# Representation of the user account that performed the event.
self.account = None
# patchset-created, comment-added, etc.
self.change_number = None
self.change_url = None
self.patch_number = None
self.refspec = None
self.approvals = []
self.branch = None
self.comment = None
# ref-updated
self.ref = None
self.oldrev = None
self.newrev = None
# timer
self.timespec = None
# For events that arrive with a destination pipeline (eg, from
# an admin command, etc):
self.forced_pipeline = None
def __repr__(self):
ret = '<TriggerEvent %s %s' % (self.type, self.project_name)
if self.branch:
ret += " %s" % self.branch
if self.change_number:
ret += " %s,%s" % (self.change_number, self.patch_number)
if self.approvals:
ret += ' ' + ', '.join(
['%s:%s' % (a['type'], a['value']) for a in self.approvals])
ret += '>'
return ret
def getChange(self, project, trigger):
if self.change_number:
change = trigger.getChange(self.change_number, self.patch_number)
elif self.ref:
change = Ref(project)
change.ref = self.ref
change.oldrev = self.oldrev
change.newrev = self.newrev
change.url = trigger.getGitwebUrl(project, sha=self.newrev)
else:
change = NullChange(project)
return change
class EventFilter(object):
def __init__(self, types=[], branches=[], refs=[], event_approvals={},
comment_filters=[], email_filters=[], username_filters=[],
timespecs=[], require_approvals=[]):
self._types = types
self._branches = branches
self._refs = refs
self._comment_filters = comment_filters
self._email_filters = email_filters
self._username_filters = username_filters
self.types = [re.compile(x) for x in types]
self.branches = [re.compile(x) for x in branches]
self.refs = [re.compile(x) for x in refs]
self.comment_filters = [re.compile(x) for x in comment_filters]
self.email_filters = [re.compile(x) for x in email_filters]
self.username_filters = [re.compile(x) for x in username_filters]
self.event_approvals = event_approvals
self.require_approvals = require_approvals
self.timespecs = timespecs
for a in self.require_approvals:
if 'older-than' in a:
a['older-than'] = time_to_seconds(a['older-than'])
if 'newer-than' in a:
a['newer-than'] = time_to_seconds(a['newer-than'])
if 'email-filter' in a:
a['email-filter'] = re.compile(a['email-filter'])
def __repr__(self):
ret = '<EventFilter'
if self._types:
ret += ' types: %s' % ', '.join(self._types)
if self._branches:
ret += ' branches: %s' % ', '.join(self._branches)
if self._refs:
ret += ' refs: %s' % ', '.join(self._refs)
if self.event_approvals:
ret += ' event_approvals: %s' % ', '.join(
['%s:%s' % a for a in self.event_approvals.items()])
if self._comment_filters:
ret += ' comment_filters: %s' % ', '.join(self._comment_filters)
if self._email_filters:
ret += ' email_filters: %s' % ', '.join(self._email_filters)
if self._username_filters:
ret += ' username_filters: %s' % ', '.join(self._username_filters)
if self.timespecs:
ret += ' timespecs: %s' % ', '.join(self.timespecs)
ret += '>'
return ret
def matches(self, event, change):
def normalizeCategory(name):
name = name.lower()
return re.sub(' ', '-', name)
# event types are ORed
matches_type = False
for etype in self.types:
if etype.match(event.type):
matches_type = True
if self.types and not matches_type:
return False
# branches are ORed
matches_branch = False
for branch in self.branches:
if branch.match(event.branch):
matches_branch = True
if self.branches and not matches_branch:
return False
# refs are ORed
matches_ref = False
for ref in self.refs:
if ref.match(event.ref):
matches_ref = True
if self.refs and not matches_ref:
return False
# comment_filters are ORed
matches_comment_filter = False
for comment_filter in self.comment_filters:
if (event.comment is not None and
comment_filter.search(event.comment)):
matches_comment_filter = True
if self.comment_filters and not matches_comment_filter:
return False
# We better have an account provided by Gerrit to do
# email filtering.
if event.account is not None:
account_email = event.account.get('email')
# email_filters are ORed
matches_email_filter = False
for email_filter in self.email_filters:
if (account_email is not None and
email_filter.search(account_email)):
matches_email_filter = True
if self.email_filters and not matches_email_filter:
return False
# username_filters are ORed
account_username = event.account.get('username')
matches_username_filter = False
for username_filter in self.username_filters:
if (account_username is not None and
username_filter.search(account_username)):
matches_username_filter = True
if self.username_filters and not matches_username_filter:
return False
# approvals are ANDed
for category, value in self.event_approvals.items():
matches_approval = False
for eapproval in event.approvals:
if (normalizeCategory(eapproval['description']) == category and
int(eapproval['value']) == int(value)):
matches_approval = True
if not matches_approval:
return False
if self.require_approvals and not change.approvals:
# A change with no approvals can not match
return False
now = time.time()
for rapproval in self.require_approvals:
matches_approval = False
for approval in change.approvals:
found_approval = True
by = approval.get('by', {})
for k, v in rapproval.items():
if k == 'username':
if (by.get('username', '') != v):
found_approval = False
elif k == 'email-filter':
if (not v.search(by.get('email', ''))):
found_approval = False
elif k == 'newer-than':
t = now - v
if (approval['grantedOn'] < t):
found_approval = False
elif k == 'older-than':
t = now - v
if (approval['grantedOn'] >= t):
found_approval = False
else:
if (normalizeCategory(approval['description']) != k or
int(approval['value']) != v):
found_approval = False
if found_approval:
matches_approval = True
break
if not matches_approval:
return False
# timespecs are ORed
matches_timespec = False
for timespec in self.timespecs:
if (event.timespec == timespec):
matches_timespec = True
if self.timespecs and not matches_timespec:
return False
return True
class Layout(object):
def __init__(self):
self.projects = {}
self.pipelines = OrderedDict()
self.jobs = {}
self.metajobs = []
def getJob(self, name):
if name in self.jobs:
return self.jobs[name]
job = Job(name)
if name.startswith('^'):
# This is a meta-job
regex = re.compile(name)
self.metajobs.append((regex, job))
else:
# Apply attributes from matching meta-jobs
for regex, metajob in self.metajobs:
if regex.match(name):
job.copy(metajob)
self.jobs[name] = job
return job