PageRenderTime 56ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/Client/src/bkr/client/__init__.py

https://github.com/beaker-project/beaker
Python | 1219 lines | 1152 code | 43 blank | 24 comment | 36 complexity | fc955379c94ee6c7b2f8b47203b92742 MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0
  1. # -*- coding: utf-8 -*-
  2. # This program is free software; you can redistribute it and/or modify
  3. # it under the terms of the GNU General Public License as published by
  4. # the Free Software Foundation; either version 2 of the License, or
  5. # (at your option) any later version.
  6. import glob
  7. import json
  8. import optparse
  9. import os
  10. import re
  11. import sys
  12. import xml.dom.minidom
  13. from optparse import OptionGroup
  14. import pkg_resources
  15. from six.moves.urllib_parse import urljoin
  16. from bkr.client.command import Command
  17. from bkr.common.pyconfig import PyConfigParser
  18. user_config_file = os.environ.get("BEAKER_CLIENT_CONF", None)
  19. if not user_config_file:
  20. user_conf = os.path.expanduser('~/.beaker_client/config')
  21. old_conf = os.path.expanduser('~/.beaker')
  22. if os.path.exists(user_conf):
  23. user_config_file = user_conf
  24. elif os.path.exists(old_conf):
  25. user_config_file = old_conf
  26. sys.stderr.write(
  27. "%s is deprecated for config, please use %s instead\n" % (old_conf, user_conf))
  28. else:
  29. pass
  30. system_config_file = None
  31. if os.path.exists('/etc/beaker/client.conf'):
  32. system_config_file = '/etc/beaker/client.conf'
  33. conf = PyConfigParser()
  34. if system_config_file:
  35. conf.load_from_file(system_config_file)
  36. if user_config_file:
  37. conf.load_from_file(user_config_file)
  38. _host_filter_presets = None
  39. def host_filter_presets():
  40. global _host_filter_presets
  41. if _host_filter_presets is not None:
  42. return _host_filter_presets
  43. _host_filter_presets = {}
  44. config_files = (
  45. sorted(glob.glob(pkg_resources.resource_filename('bkr.client', 'host-filters/*.conf')))
  46. + sorted(glob.glob('/etc/beaker/host-filters/*.conf')))
  47. user_config_file = os.path.expanduser('~/.beaker_client/host-filter')
  48. if os.path.exists(user_config_file):
  49. config_files.append(user_config_file)
  50. for f in config_files:
  51. with open(f) as fobj:
  52. for line in fobj:
  53. matched = re.match('^(\w+)\s+(\S+.*)$', line)
  54. if matched:
  55. preset, xml = matched.groups()
  56. _host_filter_presets[preset] = xml
  57. if not _host_filter_presets:
  58. sys.stderr.write("No presets found for the --host-filter option")
  59. raise SystemExit(1)
  60. return _host_filter_presets
  61. class BeakerCommand(Command):
  62. enabled = False
  63. requires_login = True
  64. def set_hub(self, username=None, password=None, **kwargs):
  65. if kwargs.get('hub'):
  66. self.conf['HUB_URL'] = kwargs['hub']
  67. if kwargs.get('insecure'):
  68. self.conf['SSL_VERIFY'] = False
  69. proxy_user = kwargs.get('proxy_user')
  70. self.container.set_hub(username, password, auto_login=self.requires_login,
  71. proxy_user=proxy_user)
  72. def requests_session(self):
  73. try:
  74. import requests
  75. except ImportError:
  76. # requests is not available for Python < 2.6 (for example on RHEL5),
  77. # so client commands which use it will raise this exception.
  78. raise RuntimeError('The requests package is not available on your system')
  79. # share cookiejar with hub, to re-use authentication token
  80. cookies = self.hub._transport.cookiejar
  81. # use custom CA cert/bundle, if given
  82. ca_cert = self.conf.get('CA_CERT', None)
  83. ssl_verify = self.conf.get('SSL_VERIFY', True)
  84. # HUB_URL will have no trailing slash in the config
  85. base_url = self.conf['HUB_URL'] + '/'
  86. class BeakerClientRequestsSession(requests.Session):
  87. """
  88. Custom requests.Session class with a few conveniences for bkr client code.
  89. """
  90. def __init__(self):
  91. super(BeakerClientRequestsSession,
  92. self).__init__() # pylint: disable=bad-super-call
  93. self.cookies = cookies
  94. if not ssl_verify:
  95. self.verify = False
  96. elif ca_cert:
  97. self.verify = ca_cert
  98. def request(self, method, url, **kwargs):
  99. # callers can pass in a relative URL and we will figure it out for them
  100. url = urljoin(base_url, url)
  101. # turn 'json' parameter into a suitably formatted request
  102. if 'json' in kwargs:
  103. kwargs['data'] = json.dumps(kwargs.pop('json'))
  104. kwargs.setdefault('headers', {}).update({'Content-Type': 'application/json'})
  105. return super(BeakerClientRequestsSession, self).request(
  106. method, url, **kwargs) # pylint: disable=bad-super-call
  107. return BeakerClientRequestsSession()
  108. t_id_types = dict(T='RecipeTask',
  109. TR='RecipeTaskResult',
  110. R='Recipe',
  111. RS='RecipeSet',
  112. J='Job')
  113. def check_taskspec_args(self, args, permitted_types=None):
  114. # The server is the one that actually parses these, but we can check
  115. # a few things on the client side first to catch errors early and give
  116. # the user better error messages.
  117. for task in args:
  118. if ':' not in task:
  119. self.parser.error('Invalid taskspec %r: '
  120. 'see "Specifying tasks" in bkr(1)' % task)
  121. type, id = task.split(':', 1)
  122. if type not in self.t_id_types:
  123. self.parser.error('Unrecognised type %s in taskspec %r: '
  124. 'see "Specifying tasks" in bkr(1)' % (type, task))
  125. if permitted_types is not None and type not in permitted_types:
  126. self.parser.error('Taskspec type must be one of [%s]'
  127. % ', '.join(permitted_types))
  128. def prettyxml(option, opt_str, value, parser):
  129. # prettyxml implies debug as well.
  130. parser.values.prettyxml = True
  131. parser.values.debug = True
  132. def generate_kickstart(template_file):
  133. abs_path = os.path.abspath(template_file)
  134. return open(abs_path, 'r').read()
  135. generateKickstart = generate_kickstart
  136. def generate_kernel_options(template_file):
  137. abs_path = os.path.abspath(template_file)
  138. lines = []
  139. # look for ## kernel_options: line
  140. kernel_options_line_prefix = '## kernel_options:'
  141. for line in open(abs_path, 'r').read().split("\n"):
  142. if line.startswith(kernel_options_line_prefix):
  143. line = line.split(kernel_options_line_prefix)[-1]
  144. lines.append(line.strip())
  145. break
  146. return " ".join(lines)
  147. generateKernelOptions = generate_kernel_options
  148. class BeakerWorkflow(BeakerCommand):
  149. doc = xml.dom.minidom.Document()
  150. def __init__(self, *args, **kwargs):
  151. """ Initialize Workflow """
  152. super(BeakerWorkflow, self).__init__(*args, **kwargs)
  153. self.multi_host = False
  154. def options(self):
  155. """ Default options that all Workflows use """
  156. # General options related to the operation of bkr
  157. self.parser.add_option(
  158. "--dry-run", "--dryrun",
  159. default=False, action="store_true", dest="dryrun",
  160. help="Don't submit job to scheduler",
  161. )
  162. self.parser.add_option(
  163. "--debug",
  164. default=False,
  165. action="store_true",
  166. help="Print generated job XML",
  167. )
  168. self.parser.add_option(
  169. "--pretty-xml", "--prettyxml",
  170. action="callback",
  171. callback=prettyxml,
  172. default=False,
  173. help="Pretty-print generated job XML with indentation",
  174. )
  175. self.parser.add_option(
  176. "--wait",
  177. default=False,
  178. action="store_true",
  179. help="Wait on job completion",
  180. )
  181. self.parser.add_option(
  182. "--no-wait", "--nowait",
  183. default=False,
  184. action="store_false",
  185. dest="wait",
  186. help="Do not wait on job completion [default]",
  187. )
  188. self.parser.add_option(
  189. "--quiet",
  190. default=False,
  191. action="store_true",
  192. help="Be quiet, don't print warnings",
  193. )
  194. distro_options = OptionGroup(self.parser,
  195. 'Options for selecting distro tree(s)')
  196. distro_options.add_option(
  197. "--family",
  198. help="Use latest distro of this family for job",
  199. )
  200. distro_options.add_option(
  201. "--tag",
  202. action="append",
  203. default=[],
  204. help="Use latest distro tagged with TAG",
  205. )
  206. distro_options.add_option(
  207. "--distro",
  208. help="Use named distro for job",
  209. )
  210. distro_options.add_option(
  211. "--variant",
  212. help="Use only VARIANT in job",
  213. )
  214. distro_options.add_option(
  215. "--arch",
  216. action="append",
  217. dest="arches",
  218. default=[],
  219. help="Use only ARCH in job",
  220. )
  221. self.parser.add_option_group(distro_options)
  222. system_options = OptionGroup(self.parser,
  223. 'Options for selecting system(s)')
  224. system_options.add_option(
  225. "--machine", metavar="FQDN",
  226. help="Require this machine for job",
  227. )
  228. system_options.add_option(
  229. "--ignore-system-status",
  230. action="store_true",
  231. default=False,
  232. help="Always use the system given by --machine, regardless of its status"
  233. )
  234. system_options.add_option(
  235. "--systype", metavar="TYPE",
  236. default=None,
  237. help="Require system of TYPE for job (Machine, Laptop, ..) [default: Machine]",
  238. )
  239. system_options.add_option(
  240. "--hostrequire", metavar='"TAG OPERATOR VALUE"',
  241. action="append",
  242. default=[],
  243. help="Additional <hostRequires/> for job (example: labcontroller=lab.example.com)",
  244. )
  245. system_options.add_option(
  246. "--keyvalue", metavar='"KEY OPERATOR VALUE"',
  247. action="append",
  248. default=[],
  249. help="Require system with matching legacy key-value (example: NETWORK=e1000)",
  250. )
  251. system_options.add_option(
  252. "--random",
  253. default=False,
  254. action="store_true",
  255. help="Pick systems randomly (default is owned, in group, other)"
  256. )
  257. system_options.add_option(
  258. "--host-filter",
  259. metavar="NAME",
  260. default=None,
  261. help="Apply pre-defined host filter"
  262. )
  263. self.parser.add_option_group(system_options)
  264. task_options = OptionGroup(self.parser, 'Options for selecting tasks')
  265. task_options.add_option(
  266. "--task",
  267. action="append",
  268. default=[],
  269. help="Include named task in job",
  270. )
  271. task_options.add_option(
  272. "--taskfile", metavar="FILENAME",
  273. default=None,
  274. help="Include all tasks from this file in job"
  275. )
  276. task_options.add_option(
  277. "--package",
  278. action="append",
  279. default=[],
  280. help="Include all tasks for PACKAGE in job",
  281. )
  282. task_options.add_option(
  283. "--task-type", metavar="TYPE",
  284. action="append", dest="type",
  285. default=[],
  286. help="Include all tasks of TYPE in job",
  287. )
  288. task_options.add_option(
  289. "--install", metavar="PACKAGE",
  290. default=[],
  291. action="append",
  292. help="Install PACKAGE using /distribution/pkginstall",
  293. )
  294. task_options.add_option(
  295. "--kdump",
  296. default=False,
  297. action="store_true",
  298. help="Enable kdump using /kernel/networking/kdump",
  299. )
  300. task_options.add_option(
  301. "--ndump",
  302. default=False,
  303. action="store_true",
  304. help="Enable ndnc using /kernel/networking/ndnc",
  305. )
  306. task_options.add_option(
  307. "--suppress-install-task",
  308. dest="suppress_install_task",
  309. action="store_true",
  310. default=False,
  311. help="Omit /distribution/check-install which is included by default",
  312. )
  313. # for compat only
  314. task_options.add_option("--type", action="append",
  315. help=optparse.SUPPRESS_HELP)
  316. self.parser.add_option_group(task_options)
  317. job_options = OptionGroup(self.parser, 'Options for job configuration')
  318. job_options.add_option(
  319. '--job-owner', metavar='USERNAME',
  320. help='Submit job on behalf of USERNAME '
  321. '(submitting user must be a submission delegate for job owner)',
  322. )
  323. job_options.add_option(
  324. "--job-group", metavar='GROUPNAME',
  325. help="Associate a group to this job"
  326. )
  327. job_options.add_option(
  328. "--whiteboard",
  329. default="",
  330. help="Set the whiteboard for this job",
  331. )
  332. job_options.add_option(
  333. "--taskparam", metavar="NAME=VALUE",
  334. action="append",
  335. default=[],
  336. help="Set parameter NAME=VALUE for every task in job",
  337. )
  338. job_options.add_option(
  339. "--ignore-panic",
  340. default=False,
  341. action="store_true",
  342. help="Do not abort job if panic message appears on serial console",
  343. )
  344. job_options.add_option(
  345. "--reserve", action="store_true",
  346. help="Reserve system at the end of the recipe",
  347. )
  348. job_options.add_option(
  349. "--reserve-duration", metavar="SECONDS",
  350. help="Release system automatically SECONDS after being reserved [default: 24 hours]",
  351. )
  352. job_options.add_option(
  353. "--cc",
  354. default=[],
  355. action="append",
  356. help="Notify additional e-mail address on job completion",
  357. )
  358. job_options.add_option(
  359. "--priority",
  360. default="Normal",
  361. help="Request PRIORITY for job (Low, Medium, Normal, High, Urgent) [default: %default]"
  362. )
  363. job_options.add_option(
  364. "--retention-tag", metavar="TAG",
  365. default="Scratch",
  366. help="Specify data retention policy for this job [default: %default]",
  367. )
  368. job_options.add_option(
  369. "--product",
  370. default=None,
  371. help="Associate job with PRODUCT for data retention purposes"
  372. )
  373. # for compat only
  374. job_options.add_option("--retention_tag", help=optparse.SUPPRESS_HELP)
  375. self.parser.add_option_group(job_options)
  376. installation_options = OptionGroup(self.parser, 'Options for installation')
  377. installation_options.add_option(
  378. "--method",
  379. default=None,
  380. help="Installation source method (nfs, http, ftp)",
  381. )
  382. installation_options.add_option(
  383. "--kernel-options", metavar="OPTIONS",
  384. default=None,
  385. help="Pass OPTIONS to kernel during installation",
  386. )
  387. installation_options.add_option(
  388. "--kernel-options-post", metavar="OPTIONS",
  389. default=None,
  390. help="Pass OPTIONS to kernel after installation",
  391. )
  392. installation_options.add_option(
  393. "--kickstart", metavar="FILENAME",
  394. default=None,
  395. help="Use this kickstart template for installation. Rendered on the server!",
  396. )
  397. installation_options.add_option(
  398. "--ks-append", metavar="COMMANDS",
  399. default=[], action="append",
  400. help="Specify additional kickstart commands to add to the base kickstart file",
  401. )
  402. installation_options.add_option(
  403. "--ks-meta", metavar="OPTIONS",
  404. default=None,
  405. help="Pass kickstart metadata OPTIONS when generating kickstart",
  406. )
  407. installation_options.add_option(
  408. "--repo", metavar="URL",
  409. action="append",
  410. default=[],
  411. help=("Configure repo at <URL> in the kickstart. The repo "
  412. "will be available during initial package installation "
  413. "and subsequent recipe execution."),
  414. )
  415. installation_options.add_option(
  416. "--repo-post", metavar="URL",
  417. default=[], action="append",
  418. help=("Configure repo at <URL> as part of kickstart %post "
  419. "execution. The repo will NOT be available during "
  420. "initial package installation."),
  421. )
  422. # for compat only
  423. installation_options.add_option("--kernel_options",
  424. help=optparse.SUPPRESS_HELP)
  425. installation_options.add_option("--kernel_options_post",
  426. help=optparse.SUPPRESS_HELP)
  427. self.parser.add_option_group(installation_options)
  428. multihost_options = OptionGroup(self.parser, 'Options for multi-host testing')
  429. multihost_options.add_option(
  430. "--clients", metavar="NUMBER",
  431. default=0,
  432. type=int,
  433. help="Include NUMBER client hosts in multi-host test [default: %default]",
  434. )
  435. multihost_options.add_option(
  436. "--servers", metavar="NUMBER",
  437. default=0,
  438. type=int,
  439. help="Include NUMBER server hosts in multi-host test [default: %default]",
  440. )
  441. self.parser.add_option_group(multihost_options)
  442. def get_arches(self, *args, **kwargs):
  443. """
  444. Get all arches that apply to either this distro, or the distro which
  445. will be selected by the given family and tag.
  446. Variant can be used as a further filter.
  447. """
  448. distro = kwargs.get("distro")
  449. family = kwargs.get("family")
  450. tags = kwargs.get("tag")
  451. variant = kwargs.get("variant")
  452. if not hasattr(self, 'hub'):
  453. self.set_hub(**kwargs)
  454. if distro:
  455. return self.hub.distros.get_arch(dict(distro=distro, variant=variant))
  456. else:
  457. return self.hub.distros.get_arch(dict(osmajor=family, tags=tags, variant=variant))
  458. getArches = get_arches
  459. def get_os_majors(self, *args, **kwargs):
  460. """
  461. Get all OsMajors, optionally filter by tag
  462. """
  463. tags = kwargs.get("tag", [])
  464. if not hasattr(self, 'hub'):
  465. self.set_hub(**kwargs)
  466. return self.hub.distros.get_osmajors(tags)
  467. getOsMajors = get_os_majors
  468. def get_system_os_major_arches(self, *args, **kwargs):
  469. """
  470. Get all OsMajors/arches that apply to this system, optionally filter by tag
  471. """
  472. fqdn = kwargs.get("machine", '')
  473. tags = kwargs.get("tag", [])
  474. if not hasattr(self, 'hub'):
  475. self.set_hub(**kwargs)
  476. return self.hub.systems.get_osmajor_arches(fqdn, tags)
  477. getSystemOsMajorArches = get_system_os_major_arches
  478. def get_family(self, *args, **kwargs):
  479. """
  480. Get the family / OS major for a particular distro
  481. """
  482. distro = kwargs.get("distro", None)
  483. family = kwargs.get("family", None)
  484. if family:
  485. return family
  486. if not hasattr(self, 'hub'):
  487. self.set_hub(**kwargs)
  488. return self.hub.distros.get_osmajor(distro)
  489. getFamily = get_family
  490. def get_task_names_from_file(self, kwargs):
  491. """
  492. Get list of task(s) from a file
  493. """
  494. task_names = []
  495. tasklist = kwargs.get('taskfile')
  496. if tasklist:
  497. if not os.path.exists(tasklist):
  498. self.parser.error("Task file not found: %s\n" % tasklist)
  499. with open(tasklist) as fobj:
  500. for line in fobj:
  501. # If the line does not start with /, assume it is not a
  502. # valid test and don't submit it to the scheduler.
  503. if line.startswith('/'):
  504. task_names.append(line.rstrip())
  505. return task_names
  506. getTaskNamesFromFile = get_task_names_from_file
  507. def get_tasks(self, *args, **kwargs):
  508. """
  509. Get all requested tasks
  510. """
  511. types = kwargs.get("type", None)
  512. packages = kwargs.get("package", None)
  513. self.n_clients = kwargs.get("clients", 0)
  514. self.n_servers = kwargs.get("servers", 0)
  515. quiet = kwargs.get("quiet", False)
  516. if not hasattr(self, 'hub'):
  517. self.set_hub(**kwargs)
  518. # We only want valid tasks
  519. filter = dict(valid=1)
  520. # Pre Filter based on osmajor
  521. filter['osmajor'] = self.getFamily(*args, **kwargs)
  522. tasks = []
  523. valid_tasks = dict()
  524. task_names = list(kwargs['task'])
  525. task_names.extend(self.getTaskNamesFromFile(kwargs))
  526. if task_names:
  527. for task in self.hub.tasks.filter(dict(names=task_names,
  528. osmajor=filter['osmajor'])):
  529. valid_tasks[task['name']] = task
  530. for name in task_names:
  531. task = valid_tasks.get(name, None)
  532. if task:
  533. tasks.append(task)
  534. elif not quiet:
  535. sys.stderr.write('WARNING: task %s not applicable for distro, ignoring\n' % name)
  536. if self.n_clients or self.n_servers:
  537. self.multi_host = True
  538. if types:
  539. filter['types'] = types
  540. if packages:
  541. filter['packages'] = packages
  542. if types or packages:
  543. ntasks = self.hub.tasks.filter(filter)
  544. multihost_tasks = self.hub.tasks.filter(dict(types=['Multihost']))
  545. if self.multi_host:
  546. for t in ntasks[:]:
  547. if t not in multihost_tasks:
  548. # FIXME add debug print here
  549. ntasks.remove(t)
  550. else:
  551. for t in ntasks[:]:
  552. if t in multihost_tasks:
  553. # FIXME add debug print here
  554. ntasks.remove(t)
  555. tasks.extend(ntasks)
  556. return tasks
  557. getTasks = get_tasks
  558. def get_install_task_name(self, *args, **kwargs):
  559. """
  560. Returns the name of the task which is injected at the start of the recipe.
  561. Its job is to check for any problems in the installation.
  562. We have one implementation:
  563. /distribution/check-install is used by default
  564. """
  565. return '/distribution/check-install'
  566. getInstallTaskName = get_install_task_name
  567. def process_template(self, recipeTemplate,
  568. requestedTasks,
  569. taskParams=None,
  570. distroRequires=None,
  571. hostRequires=None,
  572. role='STANDALONE',
  573. arch=None,
  574. whiteboard=None,
  575. install=None,
  576. reserve=None,
  577. reserve_duration=None,
  578. **kwargs):
  579. """
  580. Add tasks and additional requires to our template
  581. """
  582. if taskParams is None:
  583. taskParams = []
  584. actualTasks = []
  585. for task in requestedTasks:
  586. if arch not in task['arches']:
  587. actualTasks.append(task['name'])
  588. # Don't create empty recipes
  589. if actualTasks or reserve or kwargs.get('allow_empty_recipe', False):
  590. # Copy basic requirements
  591. recipe = recipeTemplate.clone()
  592. if whiteboard:
  593. recipe.whiteboard = whiteboard
  594. if distroRequires:
  595. recipe.addDistroRequires(distroRequires)
  596. if hostRequires:
  597. recipe.addHostRequires(hostRequires)
  598. add_install_task = not kwargs.get("suppress_install_task", False)
  599. if add_install_task:
  600. install_task_name = self.getInstallTaskName(**kwargs)
  601. # Don't add it if it's already explicitly requested
  602. if dict(name=install_task_name, arches=[]) not in requestedTasks:
  603. recipe.addTask(install_task_name)
  604. if install:
  605. paramnode = self.doc.createElement('param')
  606. paramnode.setAttribute('name', 'PKGARGNAME')
  607. paramnode.setAttribute('value', ' '.join(install))
  608. recipe.addTask('/distribution/pkginstall', paramNodes=[paramnode])
  609. if kwargs.get("ndump"):
  610. recipe.addTask('/kernel/networking/ndnc')
  611. if kwargs.get("kdump"):
  612. recipe.addTask('/kernel/networking/kdump')
  613. for task in actualTasks:
  614. recipe.addTask(task, role=role, taskParams=taskParams)
  615. if reserve:
  616. recipe.addReservesys(duration=reserve_duration)
  617. # process kickstart template if given
  618. ksTemplate = kwargs.get("kickstart", None)
  619. if ksTemplate:
  620. kickstart = generateKickstart(ksTemplate)
  621. # additional kernel options from template
  622. kernel_options = recipe.kernel_options + " " + generateKernelOptions(ksTemplate)
  623. recipe.kernel_options = kernel_options.strip()
  624. recipe.addKickstart(kickstart)
  625. else:
  626. recipe = None
  627. return recipe
  628. processTemplate = process_template
  629. class BeakerJobTemplateError(ValueError):
  630. """
  631. Raised to indicate that something invalid or impossible has been requested
  632. while a BeakerJob template was being used to generate a job definition.
  633. """
  634. pass
  635. class BeakerBase(object):
  636. doc = xml.dom.minidom.Document()
  637. def clone(self):
  638. cloned = self.__class__()
  639. cloned.node = self.node.cloneNode(True)
  640. return cloned
  641. def toxml(self, prettyxml=False, **kwargs):
  642. """ return xml of job """
  643. if prettyxml:
  644. myxml = self.node.toprettyxml()
  645. else:
  646. myxml = self.node.toxml()
  647. return myxml
  648. class BeakerJob(BeakerBase):
  649. def __init__(self, *args, **kwargs):
  650. self.node = self.doc.createElement('job')
  651. whiteboard = self.doc.createElement('whiteboard')
  652. whiteboard.appendChild(self.doc.createTextNode(kwargs.get('whiteboard', '')))
  653. if kwargs.get('cc'):
  654. notify = self.doc.createElement('notify')
  655. for cc in kwargs.get('cc'):
  656. ccnode = self.doc.createElement('cc')
  657. ccnode.appendChild(self.doc.createTextNode(cc))
  658. notify.appendChild(ccnode)
  659. self.node.appendChild(notify)
  660. self.node.appendChild(whiteboard)
  661. if kwargs.get('retention_tag'):
  662. self.node.setAttribute('retention_tag', kwargs.get('retention_tag'))
  663. if kwargs.get('product'):
  664. self.node.setAttribute('product', kwargs.get('product'))
  665. if kwargs.get('job_group'):
  666. self.node.setAttribute('group', kwargs.get('job_group'))
  667. if kwargs.get('job_owner'):
  668. self.node.setAttribute('user', kwargs.get('job_owner'))
  669. def add_recipe_set(self, recipeSet=None):
  670. """
  671. Properly add a recipeSet to this job
  672. """
  673. if recipeSet:
  674. if isinstance(recipeSet, BeakerRecipeSet):
  675. node = recipeSet.node
  676. elif isinstance(recipeSet, xml.dom.minidom.Element):
  677. node = recipeSet
  678. else:
  679. raise TypeError('recipeSet must be BeakerRecipeSet or xml.dom.minidom.Element')
  680. if len(node.getElementsByTagName('recipe')) > 0:
  681. self.node.appendChild(node.cloneNode(True))
  682. addRecipeSet = add_recipe_set
  683. def add_recipe(self, recipe=None):
  684. """
  685. Properly add a recipe to this job
  686. """
  687. if recipe:
  688. if isinstance(recipe, BeakerRecipe):
  689. node = recipe.node
  690. elif isinstance(recipe, xml.dom.minidom.Element):
  691. node = recipe
  692. else:
  693. raise TypeError('recipe must be BeakerRecipe or xml.dom.minidom.Element')
  694. if len(node.getElementsByTagName('task')) > 0:
  695. recipeSet = self.doc.createElement('recipeSet')
  696. recipeSet.appendChild(node.cloneNode(True))
  697. self.node.appendChild(recipeSet)
  698. addRecipe = add_recipe
  699. class BeakerRecipeSet(BeakerBase):
  700. def __init__(self, *args, **kwargs):
  701. self.node = self.doc.createElement('recipeSet')
  702. self.node.setAttribute('priority', kwargs.get('priority', ''))
  703. def add_recipe(self, recipe=None):
  704. """
  705. Properly add a recipe to this recipeSet
  706. """
  707. if recipe:
  708. if isinstance(recipe, BeakerRecipe):
  709. node = recipe.node
  710. elif isinstance(recipe, xml.dom.minidom.Element):
  711. node = recipe
  712. else:
  713. raise TypeError('recipe must be BeakerRecipe or xml.dom.minidom.Element')
  714. if len(node.getElementsByTagName('task')) > 0:
  715. self.node.appendChild(node.cloneNode(True))
  716. addRecipe = add_recipe
  717. class BeakerRecipeBase(BeakerBase):
  718. def __init__(self, *args, **kwargs):
  719. self.node.setAttribute('whiteboard', '')
  720. andDistroRequires = self.doc.createElement('and')
  721. partitions = self.doc.createElement('partitions')
  722. distroRequires = self.doc.createElement('distroRequires')
  723. hostRequires = self.doc.createElement('hostRequires')
  724. repos = self.doc.createElement('repos')
  725. distroRequires.appendChild(andDistroRequires)
  726. self.node.appendChild(distroRequires)
  727. self.node.appendChild(hostRequires)
  728. self.node.appendChild(repos)
  729. self.node.appendChild(partitions)
  730. def _addBaseHostRequires(self, **kwargs):
  731. """
  732. Add hostRequires
  733. """
  734. machine = kwargs.get("machine", None)
  735. force = kwargs.get('ignore_system_status', False)
  736. systype = kwargs.get("systype", None)
  737. keyvalues = kwargs.get("keyvalue", [])
  738. requires = kwargs.get("hostrequire", [])
  739. random = kwargs.get("random", False)
  740. host_filter = kwargs.get('host_filter', None)
  741. if machine and force:
  742. # if machine is specified, emit a warning message that any
  743. # other host selection criteria is ignored
  744. for opt in ['hostrequire', 'keyvalue', 'random', 'systype',
  745. 'host_filter']:
  746. if kwargs.get(opt, None):
  747. sys.stderr.write('Warning: Ignoring --%s'
  748. ' because --machine was specified\n' % opt.replace('_', '-'))
  749. hostRequires = self.node.getElementsByTagName('hostRequires')[0]
  750. hostRequires.setAttribute("force", "%s" % kwargs.get('machine'))
  751. else:
  752. if machine:
  753. hostMachine = self.doc.createElement('hostname')
  754. hostMachine.setAttribute('op', '=')
  755. hostMachine.setAttribute('value', '%s' % machine)
  756. self.addHostRequires(hostMachine)
  757. if systype:
  758. systemType = self.doc.createElement('system_type')
  759. systemType.setAttribute('op', '=')
  760. systemType.setAttribute('value', '%s' % systype)
  761. self.addHostRequires(systemType)
  762. p2 = re.compile(r'\s*([\!=<>]+|&gt;|&lt;|(?<=\s)like(?=\s))\s*')
  763. for keyvalue in keyvalues:
  764. splitkeyvalue = p2.split(keyvalue, 3)
  765. if len(splitkeyvalue) != 3:
  766. raise BeakerJobTemplateError(
  767. '--keyvalue option must be in the form "KEY OPERATOR VALUE"')
  768. key, op, value = splitkeyvalue
  769. mykeyvalue = self.doc.createElement('key_value')
  770. mykeyvalue.setAttribute('key', '%s' % key)
  771. mykeyvalue.setAttribute('op', '%s' % op)
  772. mykeyvalue.setAttribute('value', '%s' % value)
  773. self.addHostRequires(mykeyvalue)
  774. for require in requires:
  775. if require.lstrip().startswith('<'):
  776. myrequire = xml.dom.minidom.parseString(require).documentElement
  777. else:
  778. splitrequire = p2.split(require, 3)
  779. if len(splitrequire) != 3:
  780. raise BeakerJobTemplateError(
  781. '--hostrequire option must be in the form "TAG OPERATOR VALUE"')
  782. key, op, value = splitrequire
  783. myrequire = self.doc.createElement('%s' % key)
  784. myrequire.setAttribute('op', '%s' % op)
  785. myrequire.setAttribute('value', '%s' % value)
  786. self.addHostRequires(myrequire)
  787. if random:
  788. self.addAutopick(random)
  789. if host_filter:
  790. _host_filter_presets = host_filter_presets()
  791. host_filter_expanded = _host_filter_presets.get(host_filter, None)
  792. if host_filter_expanded:
  793. self.addHostRequires(xml.dom.minidom.parseString
  794. (host_filter_expanded).documentElement)
  795. else:
  796. sys.stderr.write('Pre-defined host-filter does not exist: %s\n' % host_filter)
  797. sys.exit(1)
  798. def add_base_requires(self, *args, **kwargs):
  799. """
  800. Add base requires
  801. """
  802. self._addBaseHostRequires(**kwargs)
  803. distro = kwargs.get("distro", None)
  804. family = kwargs.get("family", None)
  805. variant = kwargs.get("variant", None)
  806. method = kwargs.get("method", None)
  807. ks_metas = []
  808. ks_meta = kwargs.get("ks_meta", "")
  809. kernel_options = kwargs.get("kernel_options", '')
  810. kernel_options_post = kwargs.get("kernel_options_post", '')
  811. ks_appends = kwargs.get("ks_append", [])
  812. tags = kwargs.get("tag", [])
  813. repos = kwargs.get("repo", [])
  814. postrepos = kwargs.get("repo_post", [])
  815. ignore_panic = kwargs.get("ignore_panic", False)
  816. if distro:
  817. distroName = self.doc.createElement('distro_name')
  818. if '%' not in distro:
  819. distroName.setAttribute('op', '=')
  820. else:
  821. distroName.setAttribute('op', 'like')
  822. distroName.setAttribute('value', '%s' % distro)
  823. self.addDistroRequires(distroName)
  824. else:
  825. if family:
  826. distroFamily = self.doc.createElement('distro_family')
  827. distroFamily.setAttribute('op', '=')
  828. distroFamily.setAttribute('value', '%s' % family)
  829. self.addDistroRequires(distroFamily)
  830. for tag in tags:
  831. distroTag = self.doc.createElement('distro_tag')
  832. distroTag.setAttribute('op', '=')
  833. distroTag.setAttribute('value', '%s' % tag)
  834. self.addDistroRequires(distroTag)
  835. if variant:
  836. distroVariant = self.doc.createElement('distro_variant')
  837. distroVariant.setAttribute('op', '=')
  838. distroVariant.setAttribute('value', '%s' % variant)
  839. self.addDistroRequires(distroVariant)
  840. if method:
  841. ks_metas.append("method=%s" % method)
  842. if ks_meta:
  843. ks_metas.append(ks_meta)
  844. self.ks_meta = ' '.join(ks_metas)
  845. if kernel_options:
  846. self.kernel_options = kernel_options
  847. if kernel_options_post:
  848. self.kernel_options_post = kernel_options_post
  849. for ks_command in ks_appends:
  850. ks_append = self.doc.createElement('ks_append')
  851. ks_append.appendChild(self.doc.createCDATASection(ks_command))
  852. self.ks_appends.appendChild(ks_append.cloneNode(True))
  853. for i, repo in enumerate(repos):
  854. myrepo = self.doc.createElement('repo')
  855. myrepo.setAttribute('name', 'myrepo_%s' % i)
  856. myrepo.setAttribute('url', '%s' % repo)
  857. self.addRepo(myrepo)
  858. if postrepos:
  859. self.addPostRepo(postrepos)
  860. if ignore_panic:
  861. self.add_ignore_panic()
  862. addBaseRequires = add_base_requires
  863. def add_repo(self, node):
  864. self.repos.appendChild(node.cloneNode(True))
  865. addRepo = add_repo
  866. def add_post_repo(self, repourl_lst):
  867. """
  868. Function to add repos only in %post section of kickstart
  869. add_repo() function does add the repos to be available during the
  870. installation time whereas this function appends the yum repo config
  871. files in the %post section of the kickstart so that they are ONLY
  872. available after the installation.
  873. """
  874. post_repo_config = ""
  875. for i, repoloc in enumerate(repourl_lst):
  876. post_repo_config += '''
  877. cat << EOF >/etc/yum.repos.d/beaker-postrepo%(i)s.repo
  878. [beaker-postrepo%(i)s]
  879. name=beaker-postrepo%(i)s
  880. baseurl=%(repoloc)s
  881. enabled=1
  882. gpgcheck=0
  883. skip_if_unavailable=1
  884. EOF
  885. ''' % dict(i=i, repoloc=repoloc)
  886. post_repo_config = "\n%post" + post_repo_config + "%end\n"
  887. ks_append = self.doc.createElement('ks_append')
  888. ks_append.appendChild(self.doc.createCDATASection(post_repo_config))
  889. self.ks_appends.appendChild(ks_append.cloneNode(True))
  890. addPostRepo = add_post_repo
  891. def add_host_requires(self, nodes):
  892. """
  893. Accepts either xml, dom.Element or a list of dom.Elements
  894. """
  895. if isinstance(nodes, str):
  896. parse = xml.dom.minidom.parseString(nodes.strip())
  897. nodes = []
  898. for node in parse.getElementsByTagName("hostRequires"):
  899. nodes.extend(node.childNodes)
  900. elif isinstance(nodes, xml.dom.minidom.Element):
  901. nodes = [nodes]
  902. if isinstance(nodes, list):
  903. for node in nodes:
  904. if isinstance(node, xml.dom.minidom.Element):
  905. self.and_host_requires.appendChild(node.cloneNode(True))
  906. addHostRequires = add_host_requires
  907. def add_distro_requires(self, nodes):
  908. """
  909. Accepts either xml, dom.Element or a list of dom.Elements
  910. """
  911. if isinstance(nodes, str):
  912. parse = xml.dom.minidom.parseString(nodes.strip())
  913. nodes = []
  914. for node in parse.getElementsByTagName("distroRequires"):
  915. nodes.extend(node.childNodes)
  916. elif isinstance(nodes, xml.dom.minidom.Element):
  917. nodes = [nodes]
  918. if isinstance(nodes, list):
  919. for node in nodes:
  920. if isinstance(node, xml.dom.minidom.Element):
  921. self.and_distro_requires.appendChild(node.cloneNode(True))
  922. addDistroRequires = add_distro_requires
  923. def add_task(self, task, role='STANDALONE', paramNodes=None, taskParams=None):
  924. if taskParams is None:
  925. taskParams = []
  926. if paramNodes is None:
  927. paramNodes = []
  928. recipeTask = self.doc.createElement('task')
  929. recipeTask.setAttribute('name', '%s' % task)
  930. recipeTask.setAttribute('role', '%s' % role)
  931. params = self.doc.createElement('params')
  932. for param in paramNodes:
  933. params.appendChild(param)
  934. for taskParam in taskParams:
  935. param = self.doc.createElement('param')
  936. param.setAttribute('name', taskParam.split('=', 1)[0])
  937. param.setAttribute('value', taskParam.split('=', 1)[1])
  938. params.appendChild(param)
  939. recipeTask.appendChild(params)
  940. self.node.appendChild(recipeTask)
  941. addTask = add_task
  942. def add_reservesys(self, duration=None):
  943. reservesys = self.doc.createElement('reservesys')
  944. if duration:
  945. reservesys.setAttribute('duration', duration)
  946. self.node.appendChild(reservesys)
  947. addReservesys = add_reservesys
  948. def add_partition(self, name=None, type=None, fs=None, size=None):
  949. """
  950. Add a partition node
  951. """
  952. if name:
  953. partition = self.doc.createElement('partition')
  954. partition.setAttribute('name', str(name))
  955. else:
  956. raise BeakerJobTemplateError(u'You must specify name when adding a partition')
  957. if size:
  958. partition.setAttribute('size', str(size))
  959. else:
  960. raise BeakerJobTemplateError(u'You must specify size when adding a partition')
  961. if type:
  962. partition.setAttribute('type', str(type))
  963. if fs:
  964. partition.setAttribute('fs', str(fs))
  965. self.partitions.appendChild(partition)
  966. addPartition = add_partition
  967. def add_kickstart(self, kickstart):
  968. recipeKickstart = self.doc.createElement('kickstart')
  969. recipeKickstart.appendChild(self.doc.createCDATASection(kickstart))
  970. self.node.appendChild(recipeKickstart)
  971. addKickstart = add_kickstart
  972. def add_autopick(self, random):
  973. recipeAutopick = self.doc.createElement('autopick')
  974. random = u'{}'.format(random).lower()
  975. recipeAutopick.setAttribute('random', random)
  976. self.node.appendChild(recipeAutopick)
  977. addAutopick = add_autopick
  978. def add_ignore_panic(self):
  979. recipeIgnorePanic = self.doc.createElement('watchdog')
  980. recipeIgnorePanic.setAttribute('panic', 'ignore')
  981. self.node.appendChild(recipeIgnorePanic)
  982. def set_ks_meta(self, value):
  983. self.node.setAttribute('ks_meta', value)
  984. def get_ks_meta(self):
  985. return self.node.getAttribute('ks_meta')
  986. ks_meta = property(get_ks_meta, set_ks_meta)
  987. def set_kernel_options(self, value):
  988. self.node.setAttribute('kernel_options', value)
  989. def get_kernel_options(self):
  990. return self.node.getAttribute('kernel_options')
  991. kernel_options = property(get_kernel_options, set_kernel_options)
  992. def set_kernel_options_post(self, value):
  993. self.node.setAttribute('kernel_options_post', value)
  994. def get_kernel_options_post(self):
  995. return self.node.getAttribute('kernel_options_post')
  996. kernel_options_post = property(get_kernel_options_post, set_kernel_options_post)
  997. def set_whiteboard(self, value):
  998. self.node.setAttribute('whiteboard', value)
  999. def get_whiteboard(self):
  1000. return self.node.getAttribute('whiteboard')
  1001. whiteboard = property(get_whiteboard, set_whiteboard)
  1002. def get_and_distro_requires(self):
  1003. return self.node.getElementsByTagName('distroRequires')[0].getElementsByTagName('and')[0]
  1004. and_distro_requires = andDistroRequires = property(get_and_distro_requires)
  1005. def get_and_host_requires(self):
  1006. hostRequires = self.node.getElementsByTagName('hostRequires')[0]
  1007. if not hostRequires.getElementsByTagName('and'):
  1008. andHostRequires = self.doc.createElement('and')
  1009. hostRequires.appendChild(andHostRequires)
  1010. return hostRequires.getElementsByTagName('and')[0]
  1011. and_host_requires = andHostRequires = property(get_and_host_requires)
  1012. def get_repos(self):
  1013. return self.node.getElementsByTagName('repos')[0]
  1014. repos = property(get_repos)
  1015. def get_partitions(self):
  1016. return self.node.getElementsByTagName('partitions')[0]
  1017. partitions = property(get_partitions)
  1018. @property
  1019. def ks_appends(self):
  1020. existing = self.node.getElementsByTagName('ks_appends')
  1021. if existing:
  1022. return existing[0]
  1023. ks_appends = self.doc.createElement('ks_appends')
  1024. self.node.appendChild(ks_appends)
  1025. return ks_appends
  1026. class BeakerRecipe(BeakerRecipeBase):
  1027. def __init__(self, *args, **kwargs):
  1028. self.node = self.doc.createElement('recipe')
  1029. super(BeakerRecipe, self).__init__(*args, **kwargs)
  1030. def add_guest_recipe(self, guestrecipe):
  1031. """ properly add a guest recipe to this recipe """
  1032. if isinstance(guestrecipe, BeakerGuestRecipe):
  1033. self.node.appendChild(guestrecipe.node.cloneNode(True))
  1034. elif (isinstance(guestrecipe, xml.dom.minidom.Element)
  1035. and guestrecipe.tagName == 'guestrecipe'):
  1036. self.node.appendChild(guestrecipe.cloneNode(True))
  1037. else:
  1038. raise BeakerJobTemplateError(u'Invalid guest recipe')
  1039. addGuestRecipe = add_guest_recipe
  1040. class BeakerGuestRecipe(BeakerRecipeBase):
  1041. def __init__(self, *args, **kwargs):
  1042. self.node = self.doc.createElement('guestrecipe')
  1043. super(BeakerGuestRecipe, self).__init__(*args, **kwargs)
  1044. def set_guestargs(self, value):
  1045. self.node.setAttribute('guestargs', value)
  1046. def get_guestargs(self):
  1047. return self.node.getAttribute('guestargs')
  1048. guestargs = property(get_guestargs, set_guestargs)
  1049. def set_guestname(self, value):
  1050. self.node.setAttribute('guestname', value)
  1051. def get_guestname(self):
  1052. return self.node.getAttribute('guestname')
  1053. guestname = property(get_guestname, set_guestname)