| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections |
| import os |
| import random |
| import re |
| import sys |
| |
| import buildbot |
| from buildbot import interfaces, util |
| from buildbot.buildslave import BuildSlave |
| from buildbot.interfaces import IRenderable |
| from buildbot.status import mail |
| from buildbot.status.builder import BuildStatus |
| from buildbot.status.status_push import HttpStatusPush |
| from infra_libs import command_line |
| from twisted.python import log |
| from zope.interface import implements |
| |
| from master.autoreboot_buildslave import AutoRebootBuildSlave |
| from buildbot.status.web.authz import Authz |
| from buildbot.status.web.baseweb import WebStatus |
| |
| import master.chromium_status_bb8 as chromium_status |
| |
| from common import chromium_utils |
| from master import buildbucket |
| from master import cbe_json_status_push |
| from master import monitoring_status_receiver |
| from master import pubsub_json_status_push |
| from master import status_logger |
| import config |
| |
| |
| # CQ uses this service account to authenticate to other services, including |
| # buildbucket. |
| CQ_SERVICE_ACCOUNT = ( |
| '5071639625-1lppvbtck1morgivc6sq4dul7klu27sd@developer.gserviceaccount.com') |
| |
| |
| # Sanity timeout for CQ builders - they are expected to finish under one hour |
| # anyway. The timeout is deliberately larger than that so that we only |
| # kill really crazy long builds that also gum up resources. |
| CQ_MAX_TIME = 4*60*60 |
| |
| |
| def HackMaxTime(maxTime=8*60*60): |
| """Set maxTime default value to 8 hours. This function must be called before |
| adding steps.""" |
| from buildbot.process.buildstep import RemoteShellCommand |
| assert RemoteShellCommand.__init__.func_defaults == (None, 1, 1, 1200, None, |
| {}, 'slave-config', True) |
| RemoteShellCommand.__init__.im_func.func_defaults = (None, 1, 1, 1200, |
| maxTime, {}, 'slave-config', True) |
| assert RemoteShellCommand.__init__.func_defaults == (None, 1, 1, 1200, |
| maxTime, {}, 'slave-config', True) |
| |
| HackMaxTime() |
| |
| |
| def HackBuildStatus(): |
| """Adds a property to Build named 'blamelist' with the blamelist.""" |
| old_setBlamelist = BuildStatus.setBlamelist |
| def setBlamelist(self, blamelist): |
| self.setProperty('blamelist', blamelist, 'Build') |
| old_setBlamelist(self, blamelist) |
| BuildStatus.setBlamelist = setBlamelist |
| |
| HackBuildStatus() |
| |
| |
| class InvalidConfig(Exception): |
| """Used by VerifySetup.""" |
| pass |
| |
| |
| def AutoSetupSlaves(builders, bot_password, max_builds=1, |
| missing_recipients=None, |
| preferred_builder_dict=None, |
| missing_timeout=300): |
| """Helper function for master.cfg to quickly setup c['slaves'].""" |
| missing_recipients = missing_recipients or [] |
| |
| slaves_dict = {} |
| for builder in builders: |
| auto_reboot = builder.get('auto_reboot', True) |
| notify_on_missing = builder.get('notify_on_missing', False) |
| slavenames = builder.get('slavenames', [])[:] |
| if 'slavename' in builder: |
| slavenames.append(builder['slavename']) |
| for slavename in slavenames: |
| properties = {} |
| if preferred_builder_dict: |
| preferred_builder = preferred_builder_dict.get(slavename) |
| if preferred_builder: |
| properties['preferred_builder'] = preferred_builder |
| # If a prior builder has configured this same slave, we treat |
| # auto_reboot and notify_on_missing as the disjunction of what all |
| # builders configure for that slave. Note, however, that if multiple |
| # builders configure properties for the same slave, the last one wins. |
| slaves_dict[slavename] = ( |
| slaves_dict.get(slavename, [None])[0] or auto_reboot, |
| slaves_dict.get(slavename, [None, None])[1] or notify_on_missing, |
| properties) |
| |
| slaves = [] |
| for (slavename, |
| (auto_reboot, notify_on_missing, properties)) in slaves_dict.iteritems(): |
| if auto_reboot: |
| slave_class = AutoRebootBuildSlave |
| else: |
| slave_class = BuildSlave |
| |
| if notify_on_missing: |
| slaves.append(slave_class(slavename, bot_password, max_builds=max_builds, |
| properties=properties, |
| notify_on_missing=missing_recipients, |
| missing_timeout=missing_timeout)) |
| else: |
| slaves.append(slave_class(slavename, bot_password, |
| properties=properties, max_builds=max_builds)) |
| |
| return slaves |
| |
| |
| # TODO(phajdan.jr): Make enforce_sane_slave_pools unconditional, |
| # http://crbug.com/435559 . No new code should use it. |
| def VerifySetup(c, slaves, enforce_sane_slave_pools=True): |
| """Verify all the available slaves in the slave configuration are used and |
| that all the builders have a slave.""" |
| # Extract the list of slaves associated to a builder and make sure each |
| # builder has its slaves connected. |
| # Verify each builder has at least one slave. |
| builders_slaves = set() |
| slaves_name = [s.slavename for s in c['slaves']] |
| slave_mapping = {} |
| for b in c['builders']: |
| builder_slaves = set() |
| slavename = b.get('slavename') |
| if slavename: |
| builder_slaves.add(slavename) |
| slavenames = b.get('slavenames', []) |
| for s in slavenames: |
| builder_slaves.add(s) |
| if not slavename and not slavenames: |
| raise InvalidConfig('Builder %s has no slave' % b['name']) |
| # Now test. |
| for s in builder_slaves: |
| if not s in slaves_name: |
| raise InvalidConfig('Builder %s using undefined slave %s' % (b['name'], |
| s)) |
| slave_mapping[b['name']] = builder_slaves |
| builders_slaves |= builder_slaves |
| if len(builders_slaves) != len(slaves_name): |
| raise InvalidConfig('Same slave defined multiple times') |
| |
| slaves_by_name = {chromium_utils.EntryToSlaveName(s): s |
| for s in slaves.GetSlaves()} |
| |
| # Make sure slave pools are either equal or disjoint. This makes capacity |
| # analysis and planning simpler, easier, and more reliable. |
| if enforce_sane_slave_pools: |
| for b1, b1_slaves in slave_mapping.iteritems(): |
| for b2, b2_slaves in slave_mapping.iteritems(): |
| if b1_slaves != b2_slaves and not b1_slaves.isdisjoint(b2_slaves): |
| raise InvalidConfig( |
| 'Builders %r and %r should have either equal or disjoint slave ' |
| 'pools' % (b1, b2)) |
| |
| # Explicitly specified slave pools must be consistent. |
| b1_pool = None |
| for slave in b1_slaves: |
| pool = chromium_utils.EntryToSlavePool(slaves_by_name.get(slave, {})) |
| if b1_pool is None: |
| b1_pool = pool |
| if b1_pool != pool: |
| raise InvalidConfig( |
| 'Slave %s for build %r belongs to pool %s, but expected %s' % ( |
| slave, b1, pool, b1_pool)) |
| |
| # Make sure each slave has their builder. |
| builders_name = [b['name'] for b in c['builders']] |
| for s in c['slaves']: |
| name = s.slavename |
| if not name in builders_slaves: |
| raise InvalidConfig('Slave %s not associated with any builder' % name) |
| |
| # Make sure every defined slave is used. |
| for s in slaves.GetSlaves(): |
| name = chromium_utils.EntryToSlaveName(s) |
| if not name in slaves_name: |
| raise InvalidConfig('Slave %s defined in your slaves_list is not ' |
| 'referenced at all' % name) |
| builders = s.get('builder', []) |
| if not isinstance(builders, (list, tuple)): |
| builders = [builders] |
| testers = s.get('testers', []) |
| if not isinstance(testers, (list, tuple)): |
| testers = [testers] |
| builders.extend(testers) |
| for b in builders: |
| if not b in builders_name: |
| raise InvalidConfig('Slave %s uses non-existent builder %s' % (name, |
| b)) |
| |
| class UsersAreEmails(util.ComparableMixin): |
| """Chromium already uses email addresses as user name so no need to do |
| anything. |
| """ |
| # Class has no __init__ method |
| # pylint: disable=W0232 |
| implements(interfaces.IEmailLookup) |
| |
| @staticmethod |
| def getAddress(name): |
| return name |
| |
| |
| class FilterDomain(util.ComparableMixin): |
| """Similar to buildbot.mail.Domain but permits filtering out people we don't |
| want to spam. |
| |
| Also loads default values from chromium_config.""" |
| implements(interfaces.IEmailLookup) |
| |
| compare_attrs = ['domain', 'permitted_domains'] |
| |
| |
| def __init__(self, domain=None, permitted_domains=None): |
| """domain is the default domain to append when only the naked username is |
| available. |
| permitted_domains is a whitelist of domains that emails will be sent to.""" |
| # pylint: disable=E1101 |
| self.domain = domain or config.Master.master_domain |
| self.permitted_domains = (permitted_domains or |
| config.Master.permitted_domains) |
| if self.permitted_domains: |
| assert isinstance(self.permitted_domains, tuple), ( |
| 'permitted_domains must be a tuple, now it is a %s (value: %s)' % |
| (type(self.permitted_domains), self.permitted_domains)) |
| |
| def getAddress(self, name): |
| """If name is already an email address, pass it through.""" |
| result = name |
| if self.domain and not '@' in result: |
| result = '%s@%s' % (name, self.domain) |
| if not '@' in result: |
| log.msg('Invalid blame email address "%s"' % result) |
| return None |
| if self.permitted_domains: |
| for p in self.permitted_domains: |
| if result.endswith(p): |
| return result |
| return None |
| return result |
| |
| # Based on django.utils.html.escapejs from the Django project |
| def EscapeJs(t): |
| """Replaces javascript special characters in the supplied string. |
| |
| Characters are replaced with a textual representation of their integer |
| ordinal.""" |
| js_escapes = { |
| '\\': '\\u005C', |
| '\'': '\\u0027', |
| '"': '\\u0022', |
| '>': '\\u003E', |
| '<': '\\u003C', |
| '&': '\\u0026', |
| '=': '\\u003D', |
| '-': '\\u002D', |
| ';': '\\u003B', |
| u'\u2028': '\\u2028', |
| u'\u2029': '\\u2029', |
| } |
| for k, v in js_escapes.items(): |
| t = t.replace(k, v) |
| return t |
| |
| def CreateWebStatus(port, templates=None, tagComparator=None, |
| customEndpoints=None, console_repo_filter=None, |
| console_builder_filter=None, web_template_globals=None, |
| **kwargs): |
| webstatus = WebStatus(port, **kwargs) |
| webstatus.templates.filters.update({"escapejs": EscapeJs}) |
| if templates: |
| # Manipulate the search path for jinja templates |
| # pylint: disable=F0401 |
| import jinja2 |
| # pylint: disable=E1101 |
| old_loaders = webstatus.templates.loader.loaders |
| # pylint: disable=E1101 |
| new_loaders = old_loaders[:1] |
| new_loaders.extend([jinja2.FileSystemLoader(x) for x in templates]) |
| new_loaders.extend(old_loaders[1:]) |
| webstatus.templates.loader.loaders = new_loaders |
| chromium_status.SetupChromiumPages( |
| webstatus, tagComparator, customEndpoints, |
| console_repo_filter=console_repo_filter, |
| console_builder_filter=console_builder_filter) |
| if web_template_globals: |
| webstatus.templates.globals.update(web_template_globals) |
| return webstatus |
| |
| |
| def GetMastername(): |
| # Get the master name from the directory name. Remove leading "master.". |
| return re.sub('^master.', '', os.path.basename(os.getcwd())) |
| |
| |
| class WrongHostException(Exception): |
| pass |
| |
| |
| def AutoSetupMaster(c, active_master, mail_notifier=False, |
| mail_notifier_mode=None, |
| public_html=None, templates=None, |
| order_console_by_time=False, |
| tagComparator=None, |
| customEndpoints=None, |
| enable_http_status_push=False, # DEPRECATED (NO-OP) |
| console_repo_filter=None, |
| console_builder_filter=None, |
| web_template_globals=None): |
| """Add common settings and status services to a master. |
| |
| If you wonder what all these mean, PLEASE go check the official doc! |
| http://buildbot.net/buildbot/docs/0.7.12/ or |
| http://buildbot.net/buildbot/docs/latest/full.html |
| |
| - Default number of logs to keep |
| - WebStatus and MailNotifier |
| - Debug ssh port. Just add a file named .manhole beside master.cfg and |
| simply include one line containing 'port = 10101', then you can |
| 'ssh localhost -p' and you can access your buildbot from the inside.""" |
| if active_master.in_production and not active_master.is_production_host: |
| log.err('ERROR: Trying to start the master on the wrong host.') |
| log.err('ERROR: This machine is %s, expected %s.' % ( |
| active_master.current_host, active_master.master_host)) |
| raise WrongHostException |
| |
| c['slavePortnum'] = active_master.slave_port |
| c['projectName'] = active_master.project_name |
| c['projectURL'] = config.Master.project_url |
| |
| c['properties'] = {'mastername': GetMastername()} |
| if 'buildbotURL' in c: |
| c['properties']['buildbotURL'] = c['buildbotURL'] |
| |
| # 'status' is a list of Status Targets. The results of each build will be |
| # pushed to these targets. buildbot/status/*.py has a variety to choose from, |
| # including web pages, email senders, and IRC bots. |
| c.setdefault('status', []) |
| if mail_notifier: |
| # pylint: disable=E1101 |
| c['status'].append(mail.MailNotifier( |
| fromaddr=active_master.from_address, |
| mode=mail_notifier_mode or 'problem', |
| relayhost=config.Master.smtp, |
| lookup=FilterDomain())) |
| |
| # Add in the pubsub pusher, which pushes all status updates to a pubsub |
| # topic. This will not run unless is_production_host is set to True. |
| # This will fail on a production host if it cannot find the service |
| # account file. |
| pubsub_pusher = pubsub_json_status_push.StatusPush.CreateStatusPush( |
| activeMaster=active_master) |
| if pubsub_pusher: |
| c['status'].append(pubsub_pusher) |
| else: |
| log.msg('Pubsub not enabled.') |
| |
| # Enable Chrome Build Extract status push if configured. This requires the |
| # configuration file to be defined and valid for this master. |
| status_push = None |
| try: |
| status_push = cbe_json_status_push.StatusPush.load( |
| active_master, |
| pushInterval=30, # Push every 30 seconds. |
| ) |
| except cbe_json_status_push.ConfigError as e: |
| log.err(None, 'Failed to load configuration; not installing CBE status ' |
| 'push: %s' % (e.message,)) |
| if status_push: |
| # A status push configuration was identified. |
| c['status'].append(status_push) |
| |
| kwargs = {} |
| if public_html: |
| kwargs['public_html'] = public_html |
| kwargs['order_console_by_time'] = order_console_by_time |
| # In Buildbot 0.8.4p1, pass provide_feeds as a list to signal what extra |
| # services Buildbot should be able to provide over HTTP. |
| if buildbot.version == '0.8.4p1': |
| kwargs['provide_feeds'] = ['json'] |
| if active_master.master_port: |
| # Actions we want to allow must be explicitly listed here. |
| # Deliberately omitted are: |
| # - gracefulShutdown |
| # - cleanShutdown |
| authz = Authz(forceBuild=True, |
| forceAllBuilds=True, |
| pingBuilder=True, |
| stopBuild=True, |
| stopAllBuilds=True, |
| cancelPendingBuild=True) |
| c['status'].append(CreateWebStatus( |
| active_master.master_port, |
| tagComparator=tagComparator, |
| customEndpoints=customEndpoints, |
| authz=authz, |
| num_events_max=3000, |
| templates=templates, |
| console_repo_filter=console_repo_filter, |
| console_builder_filter=console_builder_filter, |
| web_template_globals=web_template_globals, |
| **kwargs)) |
| if active_master.master_port_alt: |
| c['status'].append(CreateWebStatus( |
| active_master.master_port_alt, |
| tagComparator=tagComparator, |
| customEndpoints=customEndpoints, |
| num_events_max=3000, |
| templates=templates, |
| console_repo_filter=console_repo_filter, |
| console_builder_filter=console_builder_filter, |
| web_template_globals=web_template_globals, |
| **kwargs)) |
| |
| # Add a status logger and a ts_mon flushing receiver. |
| c['status'].append(status_logger.StatusEventLogger()) |
| c['status'].append(monitoring_status_receiver.MonitoringStatusReceiver()) |
| |
| # Keep last build logs, the default is too low. |
| c['buildHorizon'] = 1000 |
| c['logHorizon'] = 500 |
| # Must be at least 2x the number of slaves. |
| c['eventHorizon'] = 200 |
| # Tune cache sizes to speed up web UI. |
| c['caches'] = { |
| 'BuildRequests': 1000, |
| 'Changes': 1000, |
| 'SourceStamps': 1000, |
| 'chdicts': 1000, |
| 'ssdicts': 1000, |
| } |
| # Must be at least 2x the number of on-going builds. |
| c['buildCacheSize'] = 200 |
| |
| # See http://buildbot.net/buildbot/docs/0.8.1/Debug-Options.html for more |
| # details. |
| if os.path.isfile('.manhole'): |
| try: |
| from buildbot import manhole |
| except ImportError: |
| log.msg('Using manhole has an implicit dependency on Crypto.Cipher. You ' |
| 'need to install it manually:\n' |
| ' sudo apt-get install python-crypto\n' |
| 'on ubuntu or run:\n' |
| ' pip install --user pycrypto\n' |
| ' pip install --user pyasn1\n') |
| raise |
| |
| # If 'port' is defined, it uses the same valid keys as the current user. |
| values = {} |
| execfile('.manhole', values) |
| if 'debugPassword' in values: |
| c['debugPassword'] = values['debugPassword'] |
| interface = 'tcp:%s:interface=127.0.0.1' % values.get('port', 0) |
| if 'port' in values and 'user' in values and 'password' in values: |
| c['manhole'] = manhole.PasswordManhole(interface, values['user'], |
| values['password']) |
| elif 'port' in values: |
| c['manhole'] = manhole.AuthorizedKeysManhole(interface, |
| os.path.expanduser("~/.ssh/authorized_keys")) |
| |
| if active_master.buildbucket_bucket and active_master.service_account_path: |
| SetupBuildbucket(c, active_master) |
| |
| SetMasterProcessName() |
| |
| |
| def SetupBuildbucket(c, active_master): |
| def params_hook(params, build): |
| config_hook = c.get('buildbucket_params_hook') |
| if callable(config_hook): |
| config_hook(params, build) |
| |
| properties = params.setdefault('properties', {}) |
| properties.pop('requester', None) # Ignore externally set requester. |
| if build['created_by']: |
| identity_type, name = build['created_by'].split(':', 1) |
| if identity_type == 'user': |
| # There other types of identity, such as service, anonymous. We are |
| # interested in users only. For users, name is email address. |
| if name == CQ_SERVICE_ACCOUNT: |
| # Most of systems expect CQ's builds to be requested by |
| # 'commit-bot@chromium.org', so replace the service account with it. |
| name = 'commit-bot@chromium.org' |
| properties['requester'] = name |
| |
| buildbucket.setup( |
| c, |
| active_master, |
| buckets=[active_master.buildbucket_bucket], |
| build_params_hook=params_hook, |
| max_lease_count=buildbucket.NO_LEASE_LIMIT, |
| unique_change_urls=getattr( |
| active_master, 'buildbucket_unique_change_urls', False), |
| ) |
| |
| |
| def DumpSetup(c, important=None, filename='config.current.txt'): |
| """Writes a flattened version of the setup to a text file. |
| Some interesting classes are exploded with their variables |
| exposed, by default the BuildFactories and Schedulers. |
| Newlines and indentation are sprinkled through the representation, |
| to make the output more easily broken up and groked with grep or diff. |
| |
| Note that the heuristics of how to find classes that you want expanded |
| is not too hard to fool, but it seems to handle it usefully for |
| normal master configs. |
| |
| c The config: same as the rest of the utilities here. |
| important Array of classes to also expand. |
| filename Where to write this ill-defined but useful information. |
| """ |
| from buildbot.schedulers.base import BaseScheduler |
| from buildbot.process.factory import BuildFactory |
| |
| def hacky_repr(obj, name, indent, important): |
| def hacky_repr_class(obj, indent, subdent, important): |
| r = '%s {\n' % obj.__class__.__name__ |
| for (n, v) in vars(obj).iteritems(): |
| if not n.startswith('_'): |
| r += hacky_repr(v, "%s: " % n, indent + subdent, important) + ',\n' |
| r += indent + '}' |
| return r |
| |
| if isinstance(obj, list): |
| r = '[' + ', '.join(hacky_repr(o, '', '', important) for o in obj) + ']' |
| elif isinstance(obj, tuple): |
| r = '(' + ', '.join(hacky_repr(o, '', '', important) for o in obj) + ')' |
| else: |
| r = repr(obj) |
| if not isinstance(obj, basestring): |
| r = re.sub(' at 0x[0-9a-fA-F]*>', '>', r) |
| |
| subdent = ' ' |
| if any(isinstance(obj, c) for c in important): |
| r = hacky_repr_class(obj, indent, subdent, important) |
| elif len(r) > max(30, 76-len(indent)-len(name)) and \ |
| not isinstance(obj, basestring): |
| if isinstance(obj, list): |
| r = '[\n' |
| for o in obj: |
| r += hacky_repr(o, '', indent + subdent, important) + ',\n' |
| r += indent + ']' |
| elif isinstance(obj, tuple): |
| r = '(\n' |
| for o in obj: |
| r += hacky_repr(o, '', indent + subdent, important) + ',\n' |
| r += indent + ')' |
| elif isinstance(obj, dict): |
| r = '{\n' |
| for (n, v) in sorted(obj.iteritems(), key=lambda x: x[0]): |
| if not n.startswith('_'): |
| r += hacky_repr(v, "'%s': " % n, indent + subdent, important) |
| r += ',\n' |
| r += indent + '}' |
| return "%s%s%s" % (indent, name, r) |
| |
| important = (important or []) + [BaseScheduler, BuildFactory] |
| |
| with open(filename, 'w') as f: |
| print >> f, hacky_repr(c, 'config = ', '', important) |
| |
| |
| def Partition(item_tuples, num_partitions): |
| """Divides |item_tuples| into |num_partitions| separate lists. |
| |
| Perfect partitioning is NP hard, this is a "good enough" estimate. |
| |
| Args: |
| item_tuples: tuple in the format (weight, item_name). |
| num_partitions: int number of partitions to generate. |
| |
| Returns: |
| A list of lists of item_names with as close to equal weight as possible. |
| """ |
| assert num_partitions > 0, 'Must pass a positive number of partitions' |
| assert len(item_tuples) >= num_partitions, 'Need more items than partitions' |
| partitions = [[] for _ in xrange(num_partitions)] |
| def GetLowestSumPartition(): |
| return sorted(partitions, key=lambda x: sum([i[0] for i in x]))[0] |
| for item in sorted(item_tuples, reverse=True): |
| GetLowestSumPartition().append(item) |
| return sorted([sorted([name for _, name in p]) for p in partitions]) |
| |
| |
| class ConditionalProperty(util.ComparableMixin): |
| """A IRenderable that chooses between IRenderable options given a condition. |
| |
| A typical 'WithProperties' will be rendered as a string and included as an |
| argument. If it's an empty string, it will become a position argment, "". |
| |
| This property wraps two IRenderables (which can be 'WithProperties' instances) |
| and chooses between them based on a condition. This allows the full exclusion |
| and tailoring of property output. |
| |
| For example, to optionally add a parameter, one can use the following recipe: |
| args.append( |
| ConditionalProperty( |
| 'author_name', |
| WithProperties('--author-name=%(author_name)s'), |
| [], |
| ) |
| ) |
| |
| In this example, if 'author_name' is defined as a property, the first |
| IRenderable (WithProperties) will be rendered and the '--author-name' argument |
| will be inclided. Otherwise, the second will be rendered. Since 'step' |
| flattens the command list, returning '[]' will result in the argument being |
| completely omitted. |
| |
| If a more complex condition is required, a callable function may be used |
| as the 'prop' argument. In this case, the function will be passed a single |
| parameter, the build object, and is expected to return True if the 'present' |
| IRenderable should be used and 'False' if the 'absent' one should be used. |
| """ |
| implements(IRenderable) |
| compare_attrs = ('prop', 'present', 'absent') |
| |
| def __init__(self, condition, present, absent): |
| if not callable(condition): |
| self.prop = condition |
| condition = lambda build: (self.prop in build.getProperties()) |
| else: |
| self.prop = None |
| self.condition = condition |
| self.present = present |
| self.absent = absent |
| |
| def getRenderingFor(self, build): |
| # Test whether we're going to render |
| is_present = self.condition(build) |
| |
| # Choose our inner IRenderer and render it |
| renderer = (self.present) if is_present else (self.absent) |
| # Disable 'too many positional arguments' error | pylint: disable=E1121 |
| return IRenderable(renderer).getRenderingFor(build) |
| |
| |
| class PreferredBuilderNextSlaveFunc(object): |
| """ |
| This object, when used as a Builder's 'nextSlave' function, will choose |
| a slave builder whose 'preferred_builder' value is the same as the builder |
| name. If there is no such a builder, a builder is randomly chosen. |
| """ |
| |
| def __call__(self, builder, slave_builders): |
| if not slave_builders: |
| return None |
| |
| preferred_slaves = [ |
| s for s in slave_builders |
| if s.slave.properties.getProperty('preferred_builder') == builder.name] |
| return random.choice(preferred_slaves or slave_builders) |
| |
| |
| class PreferredBuilderNextSlaveFuncNG(object): |
| """ |
| This object, when used as a Builder's 'nextSlave' function, will choose |
| a slave whose 'preferred_builder' value is the same as the builder |
| name. If there is no such slave, a slave is randomly chosen that doesn't |
| prefer any builder. If there is no such slave, a slave is randomly |
| chosen with preference on slaves whose preferred builder has largest |
| currently available capacity. If several sets of slaves have equally large |
| capacity, a set is chosen arbitrarily dependent on internal dictionary order. |
| """ |
| |
| def __init__(self, choice=random.choice): |
| # Allow overriding the choice function for testability. |
| self._choice = choice |
| |
| def __call__(self, builder, slave_builders): |
| if not slave_builders: |
| return None |
| |
| prefs = collections.Counter( |
| s.slave.properties.getProperty('preferred_builder') |
| for s in slave_builders) |
| if builder.name in prefs: |
| # First choice: Slaves that prefer this builder. |
| key = builder.name |
| elif None in prefs: |
| # Second choice: Slaves that don't prefer any builders. |
| key = None |
| else: |
| # Third choice: Slaves that prefer other builders but have largest |
| # capacity left. If several groups of slaves with equal capacity exist, |
| # one group will be chosen arbitrarily, the actual slave will be chosen |
| # randomly. |
| key = prefs.most_common()[0][0] |
| result = self._choice( |
| [s for s in slave_builders |
| if s.slave.properties.getProperty('preferred_builder') == key]) |
| |
| log.msg('Assigning slave to %s. Preferred: %s. Chose %s.' % ( |
| builder.name, |
| [s.slave.properties.getProperty('preferred_builder') |
| for s in slave_builders], |
| result)) |
| |
| return result |
| |
| |
| def SetMasterProcessName(): |
| """Sets the name of this process to the name of the master. Linux only.""" |
| |
| if sys.platform != 'linux2': |
| return |
| |
| command_line.set_command_line("master: %s" % GetMastername()) |