PageRenderTime 150ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/master/buildbot/schedulers/forcesched.py

https://gitlab.com/murder187ss/buildbot
Python | 780 lines | 733 code | 26 blank | 21 comment | 12 complexity | b9acf1a9c1f1914bec7f43fdd625cad5 MD5 | raw file
  1. # This file is part of Buildbot. Buildbot is free software: you can
  2. # redistribute it and/or modify it under the terms of the GNU General Public
  3. # License as published by the Free Software Foundation, version 2.
  4. #
  5. # This program is distributed in the hope that it will be useful, but WITHOUT
  6. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  7. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  8. # details.
  9. #
  10. # You should have received a copy of the GNU General Public License along with
  11. # this program; if not, write to the Free Software Foundation, Inc., 51
  12. # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13. #
  14. # Copyright Buildbot Team Members
  15. from future.utils import iteritems
  16. from future.utils import itervalues
  17. import email.utils as email_utils
  18. import re
  19. import traceback
  20. from twisted.internet import defer
  21. from twisted.python.reflect import accumulateClassList
  22. from buildbot import config
  23. from buildbot.process.properties import Properties
  24. from buildbot.schedulers import base
  25. from buildbot.util import identifiers
  26. from buildbot.worker_transition import deprecatedWorkerModuleAttribute
  27. class ValidationError(ValueError):
  28. pass
  29. class CollectedValidationError(ValueError):
  30. def __init__(self, errors):
  31. self.errors = errors
  32. ValueError.__init__(self, "\n".join([k + ":" + v for k, v in iteritems(errors)]))
  33. class ValidationErrorCollector(object):
  34. def __init__(self):
  35. self.errors = {}
  36. @defer.inlineCallbacks
  37. def collectValidationErrors(self, name, fn, *args, **kwargs):
  38. res = None
  39. try:
  40. res = yield defer.maybeDeferred(fn, *args, **kwargs)
  41. except CollectedValidationError as e:
  42. for name, e in iteritems(e.errors):
  43. self.errors[name] = e
  44. except ValueError as e:
  45. self.errors[name] = str(e)
  46. defer.returnValue(res)
  47. def maybeRaiseCollectedErrors(self):
  48. errors = self.errors
  49. if errors:
  50. raise CollectedValidationError(errors)
  51. DefaultField = object() # sentinel object to signal default behavior
  52. class BaseParameter(object):
  53. """
  54. BaseParameter provides a base implementation for property customization
  55. """
  56. spec_attributes = ["name", "fullName", "label", "tablabel", "type", "default", "required",
  57. "multiple", "regex", "hide"]
  58. name = ""
  59. parentName = None
  60. label = ""
  61. tablabel = ""
  62. type = ""
  63. default = ""
  64. required = False
  65. multiple = False
  66. regex = None
  67. debug = True
  68. hide = False
  69. @property
  70. def fullName(self):
  71. """A full name, intended to uniquely identify a parameter"""
  72. # join with '_' if both are set (cannot put '.', because it is used as **kwargs)
  73. if self.parentName and self.name:
  74. return self.parentName + '_' + self.name
  75. # otherwise just use the one that is set
  76. # (this allows empty name for "anonymous nests")
  77. return self.name or self.parentName
  78. def setParent(self, parent):
  79. self.parentName = parent.fullName if parent else None
  80. def __init__(self, name, label=None, tablabel=None, regex=None, **kw):
  81. """
  82. @param name: the name of the field, used during posting values
  83. back to the scheduler. This is not necessarily a UI value,
  84. and there may be restrictions on the characters allowed for
  85. this value. For example, HTML would require this field to
  86. avoid spaces and other punctuation ('-', '.', and '_' allowed)
  87. @type name: unicode
  88. @param label: (optional) the name of the field, used for UI display.
  89. @type label: unicode or None (to use 'name')
  90. @param regex: (optional) regex to validate the value with. Not used by
  91. all subclasses
  92. @type regex: unicode or regex
  93. """
  94. if name in ["owner", "builderNames", "builderid"]:
  95. config.error("%s cannot be used as a parameter name, because it is reserved" % (name,))
  96. self.name = name
  97. self.label = name if label is None else label
  98. self.tablabel = self.label if tablabel is None else tablabel
  99. if regex:
  100. self.regex = re.compile(regex)
  101. if 'value' in kw:
  102. config.error("Use default='%s' instead of value=... to give a "
  103. "default Parameter value" % kw['value'])
  104. # all other properties are generically passed via **kw
  105. self.__dict__.update(kw)
  106. def getFromKwargs(self, kwargs):
  107. """Simple customization point for child classes that do not need the other
  108. parameters supplied to updateFromKwargs. Return the value for the property
  109. named 'self.name'.
  110. The default implementation converts from a list of items, validates using
  111. the optional regex field and calls 'parse_from_args' for the final conversion.
  112. """
  113. args = kwargs.get(self.fullName, [])
  114. # delete white space for args
  115. for arg in args:
  116. if not arg.strip():
  117. args.remove(arg)
  118. if len(args) == 0:
  119. if self.required:
  120. raise ValidationError("'%s' needs to be specified" % (self.label))
  121. if self.multiple:
  122. args = self.default
  123. else:
  124. args = [self.default]
  125. if self.regex:
  126. for arg in args:
  127. if not self.regex.match(arg):
  128. raise ValidationError("%s:'%s' does not match pattern '%s'"
  129. % (self.label, arg, self.regex.pattern))
  130. try:
  131. arg = self.parse_from_args(args)
  132. except Exception as e:
  133. # an exception will just display an alert in the web UI
  134. # also log the exception
  135. if self.debug:
  136. traceback.print_exc()
  137. raise e
  138. if arg is None:
  139. raise ValidationError("need %s: no default provided by config"
  140. % (self.fullName,))
  141. return arg
  142. def updateFromKwargs(self, properties, kwargs, collector, **unused):
  143. """Primary entry point to turn 'kwargs' into 'properties'"""
  144. properties[self.name] = self.getFromKwargs(kwargs)
  145. def parse_from_args(self, l):
  146. """Secondary customization point, called from getFromKwargs to turn
  147. a validated value into a single property value"""
  148. if self.multiple:
  149. return map(self.parse_from_arg, l)
  150. else:
  151. return self.parse_from_arg(l[0])
  152. def parse_from_arg(self, s):
  153. return s
  154. def getSpec(self):
  155. spec_attributes = []
  156. accumulateClassList(self.__class__, 'spec_attributes', spec_attributes)
  157. ret = {}
  158. for i in spec_attributes:
  159. ret[i] = getattr(self, i)
  160. return ret
  161. class FixedParameter(BaseParameter):
  162. """A fixed parameter that cannot be modified by the user."""
  163. type = "fixed"
  164. hide = True
  165. default = ""
  166. def parse_from_args(self, l):
  167. return self.default
  168. class StringParameter(BaseParameter):
  169. """A simple string parameter"""
  170. spec_attributes = ["size"]
  171. type = "text"
  172. size = 10
  173. def parse_from_arg(self, s):
  174. return s
  175. class TextParameter(StringParameter):
  176. """A generic string parameter that may span multiple lines"""
  177. spec_attributes = ["cols", "rows"]
  178. type = "textarea"
  179. cols = 80
  180. rows = 20
  181. def value_to_text(self, value):
  182. return str(value)
  183. class IntParameter(StringParameter):
  184. """An integer parameter"""
  185. type = "int"
  186. default = 0
  187. parse_from_arg = int # will throw an exception if parse fail
  188. class BooleanParameter(BaseParameter):
  189. """A boolean parameter"""
  190. type = "bool"
  191. def getFromKwargs(self, kwargs):
  192. return kwargs.get(self.fullName, None) == [True]
  193. class UserNameParameter(StringParameter):
  194. """A username parameter to supply the 'owner' of a build"""
  195. spec_attributes = ["need_email"]
  196. type = "username"
  197. default = ""
  198. size = 30
  199. need_email = True
  200. def __init__(self, name="username", label="Your name:", **kw):
  201. BaseParameter.__init__(self, name, label, **kw)
  202. def parse_from_arg(self, s):
  203. if not s and not self.required:
  204. return s
  205. if self.need_email:
  206. e = email_utils.parseaddr(s)
  207. if e[0] == '' or e[1] == '':
  208. raise ValidationError("%s: please fill in email address in the "
  209. "form 'User <email@email.com>'" % (self.name,))
  210. return s
  211. class ChoiceStringParameter(BaseParameter):
  212. """A list of strings, allowing the selection of one of the predefined values.
  213. The 'strict' parameter controls whether values outside the predefined list
  214. of choices are allowed"""
  215. spec_attributes = ["choices", "strict"]
  216. type = "list"
  217. choices = []
  218. strict = True
  219. def parse_from_arg(self, s):
  220. if self.strict and s not in self.choices:
  221. raise ValidationError("'%s' does not belong to list of available choices '%s'" % (s, self.choices))
  222. return s
  223. def getChoices(self, master, scheduler, buildername):
  224. return self.choices
  225. class InheritBuildParameter(ChoiceStringParameter):
  226. """A parameter that takes its values from another build"""
  227. type = ChoiceStringParameter.type
  228. name = "inherit"
  229. compatible_builds = None
  230. def getChoices(self, master, scheduler, buildername):
  231. return self.compatible_builds(master.status, buildername)
  232. def getFromKwargs(self, kwargs):
  233. raise ValidationError("InheritBuildParameter can only be used by properties")
  234. def updateFromKwargs(self, master, properties, changes, kwargs, **unused):
  235. arg = kwargs.get(self.fullName, [""])[0]
  236. splitted_arg = arg.split(" ")[0].split("/")
  237. if len(splitted_arg) != 2:
  238. raise ValidationError("bad build: %s" % (arg))
  239. builder, num = splitted_arg
  240. builder_status = master.status.getBuilder(builder)
  241. if not builder_status:
  242. raise ValidationError("unknown builder: %s in %s" % (builder, arg))
  243. b = builder_status.getBuild(int(num))
  244. if not b:
  245. raise ValidationError("unknown build: %d in %s" % (num, arg))
  246. props = {self.name: (arg.split(" ")[0])}
  247. for name, value, source in b.getProperties().asList():
  248. if source == "Force Build Form":
  249. if name == "owner":
  250. name = "orig_owner"
  251. props[name] = value
  252. properties.update(props)
  253. changes.extend(b.changes)
  254. class WorkerChoiceParameter(ChoiceStringParameter):
  255. """A parameter that lets the worker name be explicitly chosen.
  256. This parameter works in conjunction with 'buildbot.process.builder.enforceChosenWorker',
  257. which should be added as the 'canStartBuild' parameter to the Builder.
  258. The "anySentinel" parameter represents the sentinel value to specify that
  259. there is no worker preference.
  260. """
  261. anySentinel = '-any-'
  262. label = 'Worker'
  263. required = False
  264. strict = False
  265. def __init__(self, name='workername', **kwargs):
  266. ChoiceStringParameter.__init__(self, name, **kwargs)
  267. def updateFromKwargs(self, kwargs, **unused):
  268. workername = self.getFromKwargs(kwargs)
  269. if workername == self.anySentinel:
  270. # no preference, so dont set a parameter at all
  271. return
  272. ChoiceStringParameter.updateFromKwargs(self, kwargs=kwargs, **unused)
  273. def getChoices(self, master, scheduler, buildername):
  274. if buildername is None:
  275. # this is the "Force All Builds" page
  276. workernames = master.status.getWorkerNames()
  277. else:
  278. builderStatus = master.status.getBuilder(buildername)
  279. workernames = [worker.getName() for worker in builderStatus.getWorkers()]
  280. workernames.sort()
  281. workernames.insert(0, self.anySentinel)
  282. return workernames
  283. deprecatedWorkerModuleAttribute(locals(), WorkerChoiceParameter,
  284. compat_name="BuildslaveChoiceParameter")
  285. class NestedParameter(BaseParameter):
  286. """A 'parent' parameter for a set of related parameters. This provides a
  287. logical grouping for the child parameters.
  288. Typically, the 'fullName' of the child parameters mix in the parent's
  289. 'fullName'. This allows for a field to appear multiple times in a form
  290. (for example, two codebases each have a 'branch' field).
  291. If the 'name' of the parent is the empty string, then the parent's name
  292. does not mix in with the child 'fullName'. This is useful when a field
  293. will not appear multiple time in a scheduler but the logical grouping is
  294. helpful.
  295. The result of a NestedParameter is typically a dictionary, with the key/value
  296. being the name/value of the children.
  297. """
  298. spec_attributes = ["layout", "columns"] # field is recursive, and thus managed in custom getSpec
  299. type = 'nested'
  300. layout = 'vertical'
  301. fields = None
  302. columns = None
  303. def __init__(self, name, fields, **kwargs):
  304. BaseParameter.__init__(self, fields=fields, name=name, **kwargs)
  305. # reasonable defaults for the number of columns
  306. if self.columns is None:
  307. num_visible_fields = len([field for field in fields if not field.hide])
  308. if num_visible_fields >= 4:
  309. self.columns = 2
  310. else:
  311. self.columns = 1
  312. if self.columns > 4:
  313. config.error("UI only support up to 4 columns in nested parameters")
  314. # fix up the child nodes with the parent (use None for now):
  315. self.setParent(None)
  316. def setParent(self, parent):
  317. BaseParameter.setParent(self, parent)
  318. for field in self.fields:
  319. field.setParent(self)
  320. @defer.inlineCallbacks
  321. def collectChildProperties(self, kwargs, properties, collector, **kw):
  322. """Collapse the child values into a dictionary. This is intended to be
  323. called by child classes to fix up the fullName->name conversions."""
  324. childProperties = {}
  325. for field in self.fields:
  326. yield collector.collectValidationErrors(field.fullName,
  327. field.updateFromKwargs,
  328. kwargs=kwargs,
  329. properties=childProperties,
  330. collector=collector,
  331. **kw)
  332. kwargs[self.fullName] = childProperties
  333. @defer.inlineCallbacks
  334. def updateFromKwargs(self, kwargs, properties, collector, **kw):
  335. """By default, the child values will be collapsed into a dictionary. If
  336. the parent is anonymous, this dictionary is the top-level properties."""
  337. yield self.collectChildProperties(kwargs=kwargs, properties=properties,
  338. collector=collector, **kw)
  339. # default behavior is to set a property
  340. # -- use setdefault+update in order to collapse 'anonymous' nested
  341. # parameters correctly
  342. if self.name:
  343. d = properties.setdefault(self.name, {})
  344. else:
  345. # if there's no name, collapse this nest all the way
  346. d = properties
  347. d.update(kwargs[self.fullName])
  348. def getSpec(self):
  349. ret = BaseParameter.getSpec(self)
  350. ret['fields'] = [field.getSpec() for field in self.fields]
  351. return ret
  352. ParameterGroup = NestedParameter
  353. class AnyPropertyParameter(NestedParameter):
  354. """A generic property parameter, where both the name and value of the property
  355. must be given."""
  356. type = NestedParameter.type
  357. def __init__(self, name, **kw):
  358. fields = [
  359. StringParameter(name='name', label="Name:"),
  360. StringParameter(name='value', label="Value:"),
  361. ]
  362. NestedParameter.__init__(self, name, label='', fields=fields, **kw)
  363. def getFromKwargs(self, kwargs):
  364. raise ValidationError("AnyPropertyParameter can only be used by properties")
  365. @defer.inlineCallbacks
  366. def updateFromKwargs(self, master, properties, kwargs, collector, **kw):
  367. yield self.collectChildProperties(master=master,
  368. properties=properties,
  369. kwargs=kwargs,
  370. collector=collector,
  371. **kw)
  372. pname = kwargs[self.fullName].get("name", "")
  373. pvalue = kwargs[self.fullName].get("value", "")
  374. if not pname:
  375. return
  376. validation = master.config.validation
  377. pname_validate = validation['property_name']
  378. pval_validate = validation['property_value']
  379. if not pname_validate.match(pname) \
  380. or not pval_validate.match(pvalue):
  381. raise ValidationError("bad property name='%s', value='%s'" % (pname, pvalue))
  382. properties[pname] = pvalue
  383. class CodebaseParameter(NestedParameter):
  384. """A parameter whose result is a codebase specification instead of a property"""
  385. type = NestedParameter.type
  386. codebase = ''
  387. def __init__(self,
  388. codebase,
  389. name=None,
  390. label=None,
  391. branch=DefaultField,
  392. revision=DefaultField,
  393. repository=DefaultField,
  394. project=DefaultField,
  395. **kwargs):
  396. """
  397. A set of properties that will be used to generate a codebase dictionary.
  398. The branch/revision/repository/project should each be a parameter that
  399. will map to the corresponding value in the sourcestamp. Use None to disable
  400. the field.
  401. @param codebase: name of the codebase; used as key for the sourcestamp set
  402. @type codebase: unicode
  403. @param name: optional override for the name-currying for the subfields
  404. @type codebase: unicode
  405. @param label: optional override for the label for this set of parameters
  406. @type codebase: unicode
  407. """
  408. name = name or codebase
  409. if label is None and codebase:
  410. label = "Codebase: " + codebase
  411. fields_dict = dict(branch=branch, revision=revision,
  412. repository=repository, project=project)
  413. for k, v in iteritems(fields_dict):
  414. if v is DefaultField:
  415. v = StringParameter(name=k, label=k.capitalize() + ":")
  416. elif isinstance(v, basestring):
  417. v = FixedParameter(name=k, default=v)
  418. fields_dict[k] = v
  419. fields = filter(None, fields_dict.values())
  420. NestedParameter.__init__(self, name=name, label=label,
  421. codebase=codebase,
  422. fields=fields, **kwargs)
  423. def createSourcestamp(self, properties, kwargs):
  424. # default, just return the things we put together
  425. return kwargs.get(self.fullName, {})
  426. @defer.inlineCallbacks
  427. def updateFromKwargs(self, sourcestamps, kwargs, properties, collector, **kw):
  428. yield self.collectChildProperties(sourcestamps=sourcestamps,
  429. properties=properties,
  430. kwargs=kwargs,
  431. collector=collector,
  432. **kw)
  433. # convert the "property" to a sourcestamp
  434. ss = self.createSourcestamp(properties, kwargs)
  435. if ss is not None:
  436. sourcestamps[self.codebase] = ss
  437. def oneCodebase(**kw):
  438. return [CodebaseParameter('', **kw)]
  439. class ForceScheduler(base.BaseScheduler):
  440. """
  441. ForceScheduler implements the backend for a UI to allow customization of
  442. builds. For example, a web form be populated to trigger a build.
  443. """
  444. compare_attrs = base.BaseScheduler.compare_attrs + \
  445. ('builderNames',
  446. 'reason', 'username',
  447. 'forcedProperties')
  448. def __init__(self, name, builderNames,
  449. username=UserNameParameter(),
  450. reason=StringParameter(name="reason", default="force build", size=20),
  451. reasonString="A build was forced by '%(owner)s': %(reason)s",
  452. buttonName=None,
  453. codebases=None,
  454. label=None,
  455. properties=None):
  456. """
  457. Initialize a ForceScheduler.
  458. The UI will provide a set of fields to the user; these fields are
  459. driven by a corresponding child class of BaseParameter.
  460. Use NestedParameter to provide logical groupings for parameters.
  461. The branch/revision/repository/project fields are deprecated and
  462. provided only for backwards compatibility. Using a Codebase(name='')
  463. will give the equivalent behavior.
  464. @param name: name of this scheduler (used as a key for state)
  465. @type name: unicode
  466. @param builderNames: list of builders this scheduler may start
  467. @type builderNames: list of unicode
  468. @param username: the "owner" for a build (may not be shown depending
  469. on the Auth configuration for the master)
  470. @type username: BaseParameter
  471. @param reason: the "reason" for a build
  472. @type reason: BaseParameter
  473. @param codebases: the codebases for a build
  474. @type codebases: list of string's or CodebaseParameter's;
  475. None will generate a default, but [] will
  476. remove all codebases
  477. @param properties: extra properties to configure the build
  478. @type properties: list of BaseParameter's
  479. """
  480. if not self.checkIfType(name, str):
  481. config.error("ForceScheduler name must be a unicode string: %r" %
  482. name)
  483. if not name:
  484. config.error("ForceScheduler name must not be empty: %r" %
  485. name)
  486. if not identifiers.ident_re.match(name):
  487. config.error("ForceScheduler name must be an identifier: %r" %
  488. name)
  489. if not self.checkIfListOfType(builderNames, str):
  490. config.error("ForceScheduler '%s': builderNames must be a list of strings: %r" %
  491. (name, builderNames))
  492. if self.checkIfType(reason, BaseParameter):
  493. self.reason = reason
  494. else:
  495. config.error("ForceScheduler '%s': reason must be a StringParameter: %r" %
  496. (name, reason))
  497. if properties is None:
  498. properties = []
  499. if not self.checkIfListOfType(properties, BaseParameter):
  500. config.error("ForceScheduler '%s': properties must be a list of BaseParameters: %r" %
  501. (name, properties))
  502. if self.checkIfType(username, BaseParameter):
  503. self.username = username
  504. else:
  505. config.error("ForceScheduler '%s': username must be a StringParameter: %r" %
  506. (name, username))
  507. self.forcedProperties = []
  508. self.label = name if label is None else label
  509. # Use the default single codebase form if none are provided
  510. if codebases is None:
  511. codebases = [CodebaseParameter(codebase='')]
  512. elif not codebases:
  513. config.error("ForceScheduler '%s': 'codebases' cannot be empty; use [CodebaseParameter(codebase='', hide=True)] if needed: %r " % (name, codebases))
  514. elif not isinstance(codebases, list):
  515. config.error("ForceScheduler '%s': 'codebases' should be a list of strings or CodebaseParameter, not %s" % (name, type(codebases)))
  516. codebase_dict = {}
  517. for codebase in codebases:
  518. if isinstance(codebase, basestring):
  519. codebase = CodebaseParameter(codebase=codebase)
  520. elif not isinstance(codebase, CodebaseParameter):
  521. config.error("ForceScheduler '%s': 'codebases' must be a list of strings or CodebaseParameter objects: %r" % (name, codebases))
  522. self.forcedProperties.append(codebase)
  523. codebase_dict[codebase.codebase] = dict(branch='', repository='', revision='')
  524. base.BaseScheduler.__init__(self,
  525. name=name,
  526. builderNames=builderNames,
  527. properties={},
  528. codebases=codebase_dict)
  529. if properties:
  530. self.forcedProperties.extend(properties)
  531. # this is used to simplify the template
  532. self.all_fields = [NestedParameter(name='', fields=[username, reason])]
  533. self.all_fields.extend(self.forcedProperties)
  534. self.reasonString = reasonString
  535. self.buttonName = buttonName or name
  536. def checkIfType(self, obj, chkType):
  537. return isinstance(obj, chkType)
  538. def checkIfListOfType(self, obj, chkType):
  539. isListOfType = True
  540. if self.checkIfType(obj, list):
  541. for item in obj:
  542. if not self.checkIfType(item, chkType):
  543. isListOfType = False
  544. break
  545. else:
  546. isListOfType = False
  547. return isListOfType
  548. @defer.inlineCallbacks
  549. def gatherPropertiesAndChanges(self, collector, **kwargs):
  550. properties = {}
  551. changeids = []
  552. sourcestamps = {}
  553. for param in self.forcedProperties:
  554. yield collector.collectValidationErrors(param.fullName,
  555. param.updateFromKwargs,
  556. master=self.master,
  557. properties=properties,
  558. changes=changeids,
  559. sourcestamps=sourcestamps,
  560. collector=collector,
  561. kwargs=kwargs)
  562. changeids = map(lambda a: type(a) == int and a or a.number, changeids)
  563. real_properties = Properties()
  564. for pname, pvalue in iteritems(properties):
  565. real_properties.setProperty(pname, pvalue, "Force Build Form")
  566. defer.returnValue((real_properties, changeids, sourcestamps))
  567. @defer.inlineCallbacks
  568. def computeBuilderNames(self, builderNames=None, builderid=None):
  569. if builderNames is None:
  570. if builderid is not None:
  571. builder = yield self.master.data.get(('builders', str(builderid)))
  572. builderNames = [builder['name']]
  573. else:
  574. builderNames = self.builderNames
  575. else:
  576. builderNames = list(set(builderNames).intersection(self.builderNames))
  577. defer.returnValue(builderNames)
  578. @defer.inlineCallbacks
  579. def force(self, owner, builderNames=None, builderid=None, **kwargs):
  580. """
  581. We check the parameters, and launch the build, if everything is correct
  582. """
  583. builderNames = yield self.computeBuilderNames(builderNames, builderid)
  584. if not builderNames:
  585. raise KeyError("builderNames not specified or not supported")
  586. # Currently the validation code expects all kwargs to be lists
  587. # I don't want to refactor that now so much sure we comply...
  588. kwargs = dict((k, [v]) if not isinstance(v, list) else (k, v) for k, v in iteritems(kwargs))
  589. # probably need to clean that out later as the IProperty is already a
  590. # validation mechanism
  591. collector = ValidationErrorCollector()
  592. reason = yield collector.collectValidationErrors(self.reason.fullName,
  593. self.reason.getFromKwargs, kwargs)
  594. if owner is None:
  595. owner = yield collector.collectValidationErrors(self.owner.fullName,
  596. self.owner.getFromKwargs, kwargs)
  597. properties, changeids, sourcestamps = yield self.gatherPropertiesAndChanges(
  598. collector, **kwargs)
  599. collector.maybeRaiseCollectedErrors()
  600. properties.setProperty("reason", reason, "Force Build Form")
  601. properties.setProperty("owner", owner, "Force Build Form")
  602. r = self.reasonString % {'owner': owner, 'reason': reason}
  603. # turn sourcestamps into a list
  604. for cb, ss in iteritems(sourcestamps):
  605. ss['codebase'] = cb
  606. sourcestamps = list(itervalues(sourcestamps))
  607. # everything is validated, we can create our source stamp, and buildrequest
  608. res = yield self.addBuildsetForSourceStampsWithDefaults(
  609. reason=r,
  610. sourcestamps=sourcestamps,
  611. properties=properties,
  612. builderNames=builderNames,
  613. )
  614. defer.returnValue(res)