python utility to manage a tripleo based cloud
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1057 lines
41 KiB

  1. # Copyright 2015 Red Hat, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. #
  15. import copy
  16. import datetime
  17. import errno
  18. import json
  19. import logging
  20. import os
  21. import shutil
  22. import sys
  23. import tempfile
  24. import time
  25. import uuid
  26. from osc_lib import exceptions as oscexc
  27. from osc_lib.i18n import _
  28. import six
  29. from six.moves.urllib import parse
  30. import yaml
  31. from tripleo_common.image.builder import buildah
  32. from tripleo_common.image import image_uploader
  33. from tripleo_common.image import kolla_builder
  34. from tripleo_common.utils.locks import processlock
  35. from tripleoclient import command
  36. from tripleoclient import constants
  37. from tripleoclient import exceptions
  38. from tripleoclient import utils
  39. def build_env_file(params, command_options):
  40. f = six.StringIO()
  41. f.write('# Generated with the following on %s\n#\n' %
  42. datetime.datetime.now().isoformat())
  43. f.write('# openstack %s\n#\n\n' %
  44. ' '.join(command_options))
  45. yaml.safe_dump({'parameter_defaults': params}, f,
  46. default_flow_style=False)
  47. return f.getvalue()
  48. class UploadImage(command.Command):
  49. """Push overcloud container images to registries."""
  50. auth_required = False
  51. log = logging.getLogger(__name__ + ".UploadImage")
  52. def get_parser(self, prog_name):
  53. parser = super(UploadImage, self).get_parser(prog_name)
  54. parser.add_argument(
  55. "--config-file",
  56. dest="config_files",
  57. metavar='<yaml config file>',
  58. default=[],
  59. action="append",
  60. required=True,
  61. help=_("YAML config file specifying the image build. May be "
  62. "specified multiple times. Order is preserved, and later "
  63. "files will override some options in previous files. "
  64. "Other options will append."),
  65. )
  66. parser.add_argument(
  67. "--cleanup",
  68. dest="cleanup",
  69. metavar='<full, partial, none>',
  70. default=image_uploader.CLEANUP_FULL,
  71. help=_("Cleanup behavior for local images left after upload. "
  72. "The default 'full' will attempt to delete all local "
  73. "images. 'partial' will leave images required for "
  74. "deployment on this host. 'none' will do no cleanup.")
  75. )
  76. return parser
  77. def take_action(self, parsed_args):
  78. self.log.debug("take_action(%s)" % parsed_args)
  79. if parsed_args.cleanup not in image_uploader.CLEANUP:
  80. raise oscexc.CommandError('--cleanup must be one of: %s' %
  81. ', '.join(image_uploader.CLEANUP))
  82. lock = processlock.ProcessLock()
  83. uploader = image_uploader.ImageUploadManager(
  84. parsed_args.config_files, cleanup=parsed_args.cleanup, lock=lock)
  85. try:
  86. uploader.upload()
  87. except KeyboardInterrupt: # ctrl-c
  88. self.log.warning('Upload was interrupted by ctrl-c.')
  89. class BuildImage(command.Command):
  90. """Build overcloud container images with kolla-build."""
  91. auth_required = False
  92. log = logging.getLogger(__name__ + ".BuildImage")
  93. @staticmethod
  94. def images_from_deps(images, dep):
  95. '''Builds a list from the dependencies depth-first. '''
  96. if isinstance(dep, list):
  97. for v in dep:
  98. BuildImage.images_from_deps(images, v)
  99. elif isinstance(dep, dict):
  100. for k, v in dep.items():
  101. images.append(k)
  102. BuildImage.images_from_deps(images, v)
  103. else:
  104. images.append(dep)
  105. def get_parser(self, prog_name):
  106. default_kolla_conf = os.path.join(
  107. sys.prefix, 'share', 'tripleo-common', 'container-images',
  108. 'tripleo_kolla_config_overrides.conf')
  109. parser = super(BuildImage, self).get_parser(prog_name)
  110. parser.add_argument(
  111. "--config-file",
  112. dest="config_files",
  113. metavar='<yaml config file>',
  114. default=[],
  115. action="append",
  116. help=_("YAML config file specifying the images to build. May be "
  117. "specified multiple times. Order is preserved, and later "
  118. "files will override some options in previous files. "
  119. "Other options will append. If not specified, the default "
  120. "set of containers will be built."),
  121. )
  122. parser.add_argument(
  123. "--kolla-config-file",
  124. dest="kolla_config_files",
  125. metavar='<config file>',
  126. default=[default_kolla_conf],
  127. action="append",
  128. required=True,
  129. help=_("Path to a Kolla config file to use. Multiple config files "
  130. "can be specified, with values in later files taking "
  131. "precedence. By default, tripleo kolla conf file {conf} "
  132. "is added.").format(conf=default_kolla_conf),
  133. )
  134. parser.add_argument(
  135. '--list-images',
  136. dest='list_images',
  137. action='store_true',
  138. default=False,
  139. help=_('Show the images which would be built instead of '
  140. 'building them.')
  141. )
  142. parser.add_argument(
  143. '--list-dependencies',
  144. dest='list_dependencies',
  145. action='store_true',
  146. default=False,
  147. help=_('Show the image build dependencies instead of '
  148. 'building them.')
  149. )
  150. parser.add_argument(
  151. "--exclude",
  152. dest="excludes",
  153. metavar='<container-name>',
  154. default=[],
  155. action="append",
  156. help=_("Name of a container to match against the list of "
  157. "containers to be built to skip. Can be specified multiple "
  158. "times."),
  159. )
  160. parser.add_argument(
  161. '--use-buildah',
  162. dest='use_buildah',
  163. action='store_true',
  164. default=False,
  165. help=_('Use Buildah instead of Docker to build the images '
  166. 'with Kolla.')
  167. )
  168. parser.add_argument(
  169. "--work-dir",
  170. dest="work_dir",
  171. default='/tmp/container-builds',
  172. metavar='<container builds directory>',
  173. help=_("TripleO container builds directory, storing configs and "
  174. "logs for each image and its dependencies.")
  175. )
  176. return parser
  177. def take_action(self, parsed_args):
  178. self.log.debug("take_action(%s)" % parsed_args)
  179. fd, path = tempfile.mkstemp(prefix='kolla_conf_')
  180. with os.fdopen(fd, 'w') as tmp:
  181. tmp.write('[DEFAULT]\n')
  182. if parsed_args.list_images or parsed_args.list_dependencies:
  183. tmp.write('list_dependencies=true')
  184. kolla_config_files = list(parsed_args.kolla_config_files)
  185. kolla_config_files.append(path)
  186. # Generate an unique work directory so we can keep configs and logs
  187. # each time we run the command; they'll be stored in work_dir.
  188. kolla_work_dir = os.path.join(parsed_args.work_dir, str(uuid.uuid4()))
  189. # Make sure the unique work directory exists
  190. if not os.path.exists(kolla_work_dir):
  191. self.log.debug("Creating container builds "
  192. "workspace in: %s" % kolla_work_dir)
  193. os.makedirs(kolla_work_dir)
  194. builder = kolla_builder.KollaImageBuilder(parsed_args.config_files)
  195. result = builder.build_images(kolla_config_files,
  196. parsed_args.excludes,
  197. parsed_args.use_buildah,
  198. kolla_work_dir)
  199. if parsed_args.use_buildah:
  200. deps = json.loads(result)
  201. kolla_cfg = utils.get_read_config(kolla_config_files)
  202. bb = buildah.BuildahBuilder(
  203. kolla_work_dir, deps,
  204. utils.get_from_cfg(kolla_cfg, "base"),
  205. utils.get_from_cfg(kolla_cfg, "type"),
  206. utils.get_from_cfg(kolla_cfg, "tag"),
  207. utils.get_from_cfg(kolla_cfg, "namespace"),
  208. utils.get_from_cfg(kolla_cfg, "registry"),
  209. utils.getboolean_from_cfg(kolla_cfg, "push"))
  210. bb.build_all()
  211. elif parsed_args.list_dependencies:
  212. deps = json.loads(result)
  213. yaml.safe_dump(
  214. deps,
  215. self.app.stdout,
  216. indent=2,
  217. default_flow_style=False
  218. )
  219. elif parsed_args.list_images:
  220. deps = json.loads(result)
  221. images = []
  222. BuildImage.images_from_deps(images, deps)
  223. yaml.safe_dump(
  224. images,
  225. self.app.stdout,
  226. default_flow_style=False
  227. )
  228. elif result:
  229. self.app.stdout.write(result)
  230. class PrepareImageFiles(command.Command):
  231. """Generate files defining the images, tags and registry."""
  232. auth_required = False
  233. log = logging.getLogger(__name__ + ".PrepareImageFiles")
  234. def get_parser(self, prog_name):
  235. parser = super(PrepareImageFiles, self).get_parser(prog_name)
  236. try:
  237. roles_file = utils.rel_or_abs_path(
  238. constants.OVERCLOUD_ROLES_FILE,
  239. constants.TRIPLEO_HEAT_TEMPLATES)
  240. except exceptions.DeploymentError:
  241. roles_file = None
  242. defaults = kolla_builder.container_images_prepare_defaults()
  243. parser.add_argument(
  244. "--template-file",
  245. dest="template_file",
  246. default=kolla_builder.DEFAULT_TEMPLATE_FILE,
  247. metavar='<yaml template file>',
  248. help=_("YAML template file which the images config file will be "
  249. "built from.\n"
  250. "Default: %s") % kolla_builder.DEFAULT_TEMPLATE_FILE,
  251. )
  252. parser.add_argument(
  253. "--push-destination",
  254. dest="push_destination",
  255. metavar='<location>',
  256. help=_("Location of image registry to push images to. "
  257. "If specified, a push_destination will be set for every "
  258. "image entry."),
  259. )
  260. parser.add_argument(
  261. "--tag",
  262. dest="tag",
  263. default=defaults['tag'],
  264. metavar='<tag>',
  265. help=_("Override the default tag substitution. "
  266. "If --tag-from-label is specified, "
  267. "start discovery with this tag.\n"
  268. "Default: %s") % defaults['tag'],
  269. )
  270. parser.add_argument(
  271. "--tag-from-label",
  272. dest="tag_from_label",
  273. metavar='<image label>',
  274. help=_("Use the value of the specified label(s) to discover the "
  275. "tag. Labels can be combined in a template format, "
  276. "for example: {version}-{release}"),
  277. )
  278. parser.add_argument(
  279. "--namespace",
  280. dest="namespace",
  281. default=defaults['namespace'],
  282. metavar='<namespace>',
  283. help=_("Override the default namespace substitution.\n"
  284. "Default: %s") % defaults['namespace'],
  285. )
  286. parser.add_argument(
  287. "--prefix",
  288. dest="prefix",
  289. default=defaults['name_prefix'],
  290. metavar='<prefix>',
  291. help=_("Override the default name prefix substitution.\n"
  292. "Default: %s") % defaults['name_prefix'],
  293. )
  294. parser.add_argument(
  295. "--suffix",
  296. dest="suffix",
  297. default=defaults['name_suffix'],
  298. metavar='<suffix>',
  299. help=_("Override the default name suffix substitution.\n"
  300. "Default: %s") % defaults['name_suffix'],
  301. )
  302. parser.add_argument(
  303. '--set',
  304. metavar='<variable=value>',
  305. action='append',
  306. help=_('Set the value of a variable in the template, even if it '
  307. 'has no dedicated argument such as "--suffix".')
  308. )
  309. parser.add_argument(
  310. "--exclude",
  311. dest="excludes",
  312. metavar='<regex>',
  313. default=[],
  314. action="append",
  315. help=_("Pattern to match against resulting imagename entries to "
  316. "exclude from the final output. Can be specified multiple "
  317. "times."),
  318. )
  319. parser.add_argument(
  320. "--include",
  321. dest="includes",
  322. metavar='<regex>',
  323. default=[],
  324. action="append",
  325. help=_("Pattern to match against resulting imagename entries to "
  326. "include in final output. Can be specified multiple "
  327. "times, entries not matching any --include will be "
  328. "excluded. --exclude is ignored if --include is used."),
  329. )
  330. parser.add_argument(
  331. "--output-images-file",
  332. dest="output_images_file",
  333. metavar='<file path>',
  334. help=_("File to write resulting image entries to, as well as "
  335. "stdout. Any existing file will be overwritten."),
  336. )
  337. parser.add_argument(
  338. '--environment-file', '-e', metavar='<file path>',
  339. action='append', dest='environment_files',
  340. help=_('Environment files specifying which services are '
  341. 'containerized. Entries will be filtered to only contain '
  342. 'images used by containerized services. (Can be specified '
  343. 'more than once.)')
  344. )
  345. parser.add_argument(
  346. '--environment-directory', metavar='<HEAT ENVIRONMENT DIRECTORY>',
  347. action='append', dest='environment_directories',
  348. default=[os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)],
  349. help=_('Environment file directories that are automatically '
  350. 'added to the update command. Entries will be filtered '
  351. 'to only contain images used by containerized services. '
  352. 'Can be specified more than once. Files in directories are '
  353. 'loaded in ascending sort order.')
  354. )
  355. parser.add_argument(
  356. "--output-env-file",
  357. dest="output_env_file",
  358. metavar='<file path>',
  359. help=_("File to write heat environment file which specifies all "
  360. "image parameters. Any existing file will be overwritten."),
  361. )
  362. parser.add_argument(
  363. '--roles-file', '-r', dest='roles_file',
  364. default=roles_file,
  365. help=_(
  366. 'Roles file, overrides the default %s in the t-h-t templates '
  367. 'directory used for deployment. May be an '
  368. 'absolute path or the path relative to the templates dir.'
  369. ) % constants.OVERCLOUD_ROLES_FILE
  370. )
  371. parser.add_argument(
  372. '--modify-role',
  373. dest='modify_role',
  374. help=_('Name of ansible role to run between every image upload '
  375. 'pull and push.')
  376. )
  377. parser.add_argument(
  378. '--modify-vars',
  379. dest='modify_vars',
  380. help=_('Ansible variable file containing variables to use when '
  381. 'invoking the role --modify-role.')
  382. )
  383. return parser
  384. def parse_set_values(self, subs, set_values):
  385. if not set_values:
  386. return
  387. for s in set_values:
  388. try:
  389. (n, v) = s.split(('='), 1)
  390. subs[n] = v
  391. except ValueError:
  392. msg = _('Malformed --set(%s). '
  393. 'Use the variable=value format.') % s
  394. raise oscexc.CommandError(msg)
  395. def take_action(self, parsed_args):
  396. self.log.debug("take_action(%s)" % parsed_args)
  397. self.log.warning("[DEPRECATED] This command has been deprecated and "
  398. "replaced by the 'openstack tripleo container image "
  399. "prepare' command.")
  400. roles_data = utils.fetch_roles_file(parsed_args.roles_file) or set()
  401. env = utils.build_prepare_env(
  402. parsed_args.environment_files,
  403. parsed_args.environment_directories
  404. )
  405. if roles_data:
  406. service_filter = kolla_builder.build_service_filter(
  407. env, roles_data)
  408. else:
  409. service_filter = None
  410. mapping_args = {
  411. 'tag': parsed_args.tag,
  412. 'namespace': parsed_args.namespace,
  413. 'name_prefix': parsed_args.prefix,
  414. 'name_suffix': parsed_args.suffix,
  415. }
  416. self.parse_set_values(mapping_args, parsed_args.set)
  417. pd = env.get('parameter_defaults', {})
  418. kolla_builder.set_neutron_driver(pd, mapping_args)
  419. output_images_file = (parsed_args.output_images_file
  420. or 'container_images.yaml')
  421. modify_role = None
  422. modify_vars = None
  423. append_tag = None
  424. if parsed_args.modify_role:
  425. modify_role = parsed_args.modify_role
  426. append_tag = time.strftime('-modified-%Y%m%d%H%M%S')
  427. if parsed_args.modify_vars:
  428. with open(parsed_args.modify_vars) as m:
  429. modify_vars = yaml.safe_load(m.read())
  430. prepare_data = kolla_builder.container_images_prepare(
  431. excludes=parsed_args.excludes,
  432. includes=parsed_args.includes,
  433. service_filter=service_filter,
  434. push_destination=parsed_args.push_destination,
  435. mapping_args=mapping_args,
  436. output_env_file=parsed_args.output_env_file,
  437. output_images_file=output_images_file,
  438. tag_from_label=parsed_args.tag_from_label,
  439. modify_role=modify_role,
  440. modify_vars=modify_vars,
  441. append_tag=append_tag,
  442. template_file=parsed_args.template_file
  443. )
  444. if parsed_args.output_env_file:
  445. params = prepare_data[parsed_args.output_env_file]
  446. output_env_file_expanded = os.path.expanduser(
  447. parsed_args.output_env_file)
  448. if os.path.exists(output_env_file_expanded):
  449. self.log.warning("Output env file exists, "
  450. "moving it to backup.")
  451. shutil.move(output_env_file_expanded,
  452. output_env_file_expanded + ".backup")
  453. utils.safe_write(output_env_file_expanded,
  454. build_env_file(params, self.app.command_options))
  455. result = prepare_data[output_images_file]
  456. result_str = yaml.safe_dump({'container_images': result},
  457. default_flow_style=False)
  458. sys.stdout.write(result_str)
  459. if parsed_args.output_images_file:
  460. utils.safe_write(parsed_args.output_images_file, result_str)
  461. class DiscoverImageTag(command.Command):
  462. """Discover the versioned tag for an image."""
  463. auth_required = False
  464. log = logging.getLogger(__name__ + ".DiscoverImageTag")
  465. def get_parser(self, prog_name):
  466. parser = super(DiscoverImageTag, self).get_parser(prog_name)
  467. parser.add_argument(
  468. "--image",
  469. dest="image",
  470. metavar='<container image>',
  471. required=True,
  472. help=_("Fully qualified name of the image to discover the tag for "
  473. "(Including registry and stable tag)."),
  474. )
  475. parser.add_argument(
  476. "--tag-from-label",
  477. dest="tag_from_label",
  478. metavar='<image label>',
  479. help=_("Use the value of the specified label(s) to discover the "
  480. "tag. Labels can be combined in a template format, "
  481. "for example: {version}-{release}"),
  482. )
  483. return parser
  484. def take_action(self, parsed_args):
  485. self.log.debug("take_action(%s)" % parsed_args)
  486. self.log.warning("[DEPRECATED] This command has been deprecated and "
  487. "replaced by the 'openstack tripleo container image "
  488. "prepare' command.")
  489. lock = processlock.ProcessLock()
  490. uploader = image_uploader.ImageUploadManager([], lock=lock)
  491. print(uploader.discover_image_tag(
  492. image=parsed_args.image,
  493. tag_from_label=parsed_args.tag_from_label
  494. ))
  495. class TripleOContainerImagePush(command.Command):
  496. """Push specified image to registry."""
  497. auth_required = False
  498. log = logging.getLogger(__name__ + ".TripleoContainerImagePush")
  499. def get_parser(self, prog_name):
  500. parser = super(TripleOContainerImagePush, self).get_parser(prog_name)
  501. parser.add_argument(
  502. "--local",
  503. dest="local",
  504. default=False,
  505. action="store_true",
  506. help=_("Use this flag if the container image is already on the "
  507. "current system and does not need to be pulled from a "
  508. "remote registry.")
  509. )
  510. parser.add_argument(
  511. "--registry-url",
  512. dest="registry_url",
  513. metavar='<registry url>',
  514. default=None,
  515. help=_("URL of the destination registry in the form "
  516. "<fqdn>:<port>.")
  517. )
  518. parser.add_argument(
  519. "--append-tag",
  520. dest="append_tag",
  521. default='',
  522. help=_("Tag to append to the existing tag when pushing the "
  523. "container. ")
  524. )
  525. parser.add_argument(
  526. "--username",
  527. dest="username",
  528. metavar='<username>',
  529. help=_("Username for the destination image registry.")
  530. )
  531. parser.add_argument(
  532. "--password",
  533. dest="password",
  534. metavar='<password>',
  535. help=_("Password for the destination image registry.")
  536. )
  537. parser.add_argument(
  538. "--source-username",
  539. dest="source_username",
  540. metavar='<source_username>',
  541. help=_("Username for the source image registry.")
  542. )
  543. parser.add_argument(
  544. "--source-password",
  545. dest="source_password",
  546. metavar='<source_password>',
  547. help=_("Password for the source image registry.")
  548. )
  549. parser.add_argument(
  550. "--dry-run",
  551. dest="dry_run",
  552. action="store_true",
  553. help=_("Perform a dry run upload. The upload action is not "
  554. "performed, but the authentication process is attempted.")
  555. )
  556. parser.add_argument(
  557. "--multi-arch",
  558. dest="multi_arch",
  559. action="store_true",
  560. help=_("Enable multi arch support for the upload.")
  561. )
  562. parser.add_argument(
  563. "--cleanup",
  564. dest="cleanup",
  565. action="store_true",
  566. default=False,
  567. help=_("Remove local copy of the image after uploading")
  568. )
  569. parser.add_argument(
  570. dest="image_to_push",
  571. metavar='<image to push>',
  572. help=_("Container image to upload. Should be in the form of "
  573. "<registry>/<namespace>/<name>:<tag>. If tag is "
  574. "not provided, then latest will be used.")
  575. )
  576. return parser
  577. def take_action(self, parsed_args):
  578. self.log.debug("take_action(%s)" % parsed_args)
  579. lock = processlock.ProcessLock()
  580. manager = image_uploader.ImageUploadManager(lock=lock)
  581. uploader = manager.uploader('python')
  582. source_image = parsed_args.image_to_push
  583. if parsed_args.local or source_image.startswith('containers-storage:'):
  584. storage = 'containers-storage:'
  585. if not source_image.startswith(storage):
  586. source_image = storage + source_image.replace('docker://', '')
  587. elif not parsed_args.local:
  588. self.log.warning('Assuming local container based on provided '
  589. 'container path. (e.g. starts with '
  590. 'containers-storage:)')
  591. source_url = parse.urlparse(source_image)
  592. image_name = source_url.geturl()
  593. image_source = None
  594. if parsed_args.source_username or parsed_args.source_password:
  595. self.log.warning('Source credentials ignored for local images')
  596. else:
  597. storage = 'docker://'
  598. if not source_image.startswith(storage):
  599. source_image = storage + source_image
  600. source_url = parse.urlparse(source_image)
  601. image_source = source_url.netloc
  602. image_name = source_url.path[1:]
  603. if len(image_name.split('/')) != 2:
  604. raise exceptions.DownloadError('Invalid container. Provided '
  605. 'container image should be '
  606. '<registry>/<namespace>/<name>:'
  607. '<tag>')
  608. if parsed_args.source_username or parsed_args.source_password:
  609. if not parsed_args.source_username:
  610. self.log.warning('Skipping authentication - missing source'
  611. ' username')
  612. elif not parsed_args.source_password:
  613. self.log.warning('Skipping authentication - missing source'
  614. ' password')
  615. else:
  616. uploader.authenticate(source_url,
  617. parsed_args.source_username,
  618. parsed_args.source_password)
  619. registry_url_arg = parsed_args.registry_url
  620. if registry_url_arg is None:
  621. registry_url_arg = image_uploader.get_undercloud_registry()
  622. if not registry_url_arg.startswith('docker://'):
  623. registry_url = 'docker://%s' % registry_url_arg
  624. else:
  625. registry_url = registry_url_arg
  626. reg_url = parse.urlparse(registry_url)
  627. uploader.authenticate(reg_url,
  628. parsed_args.username,
  629. parsed_args.password)
  630. task = image_uploader.UploadTask(
  631. image_name=image_name,
  632. pull_source=image_source,
  633. push_destination=registry_url_arg,
  634. append_tag=parsed_args.append_tag,
  635. modify_role=None,
  636. modify_vars=None,
  637. dry_run=parsed_args.dry_run,
  638. cleanup=parsed_args.cleanup,
  639. multi_arch=parsed_args.multi_arch)
  640. try:
  641. uploader.add_upload_task(task)
  642. uploader.run_tasks()
  643. except OSError as e:
  644. if e.errno == errno.EACCES:
  645. self.log.error("Unable to upload due to permissions. "
  646. "Please prefix command with sudo.")
  647. raise oscexc.CommandError(e)
  648. class TripleOContainerImageDelete(command.Command):
  649. """Delete specified image from registry."""
  650. auth_required = False
  651. log = logging.getLogger(__name__ + ".TripleoContainerImageDelete")
  652. def get_parser(self, prog_name):
  653. parser = super(TripleOContainerImageDelete, self).get_parser(prog_name)
  654. parser.add_argument(
  655. "--registry-url",
  656. dest="registry_url",
  657. metavar='<registry url>',
  658. default=None,
  659. help=_("URL of registry images are to be listed from in the "
  660. "form <fqdn>:<port>.")
  661. )
  662. parser.add_argument(
  663. dest="image_to_delete",
  664. metavar='<image to delete>',
  665. help=_("Full URL of image to be deleted in the "
  666. "form <fqdn>:<port>/path/to/image")
  667. )
  668. parser.add_argument(
  669. "--username",
  670. dest="username",
  671. metavar='<username>',
  672. help=_("Username for image registry.")
  673. )
  674. parser.add_argument(
  675. "--password",
  676. dest="password",
  677. metavar='<password>',
  678. help=_("Password for image registry.")
  679. )
  680. parser.add_argument(
  681. '-y', '--yes',
  682. help=_('Skip yes/no prompt (assume yes).'),
  683. default=False,
  684. action="store_true")
  685. return parser
  686. def take_action(self, parsed_args):
  687. self.log.debug("take_action(%s)" % parsed_args)
  688. if not parsed_args.yes:
  689. confirm = utils.prompt_user_for_confirmation(
  690. message=_("Are you sure you want to delete this image "
  691. "[y/N]? "),
  692. logger=self.log)
  693. if not confirm:
  694. raise oscexc.CommandError("Action not confirmed, exiting.")
  695. lock = processlock.ProcessLock()
  696. manager = image_uploader.ImageUploadManager(lock=lock)
  697. uploader = manager.uploader('python')
  698. registry_url_arg = parsed_args.registry_url
  699. if registry_url_arg is None:
  700. registry_url_arg = image_uploader.get_undercloud_registry()
  701. url = uploader._image_to_url(registry_url_arg)
  702. session = uploader.authenticate(url, parsed_args.username,
  703. parsed_args.password)
  704. try:
  705. uploader.delete(parsed_args.image_to_delete, session=session)
  706. except OSError as e:
  707. if e.errno == errno.EACCES:
  708. self.log.error("Unable to remove due to permissions. "
  709. "Please prefix command with sudo.")
  710. raise oscexc.CommandError(e)
  711. class TripleOContainerImageList(command.Lister):
  712. """List images discovered in registry."""
  713. auth_required = False
  714. log = logging.getLogger(__name__ + ".TripleoContainerImageList")
  715. def get_parser(self, prog_name):
  716. parser = super(TripleOContainerImageList, self).get_parser(prog_name)
  717. parser.add_argument(
  718. "--registry-url",
  719. dest="registry_url",
  720. metavar='<registry url>',
  721. default=None,
  722. help=_("URL of registry images are to be listed from in the "
  723. "form <fqdn>:<port>.")
  724. )
  725. parser.add_argument(
  726. "--username",
  727. dest="username",
  728. metavar='<username>',
  729. help=_("Username for image registry.")
  730. )
  731. parser.add_argument(
  732. "--password",
  733. dest="password",
  734. metavar='<password>',
  735. help=_("Password for image registry.")
  736. )
  737. return parser
  738. def take_action(self, parsed_args):
  739. self.log.debug("take_action(%s)" % parsed_args)
  740. lock = processlock.ProcessLock()
  741. manager = image_uploader.ImageUploadManager(lock=lock)
  742. uploader = manager.uploader('python')
  743. registry_url_arg = parsed_args.registry_url
  744. if registry_url_arg is None:
  745. registry_url_arg = image_uploader.get_undercloud_registry()
  746. url = uploader._image_to_url(registry_url_arg)
  747. session = uploader.authenticate(url, parsed_args.username,
  748. parsed_args.password)
  749. results = uploader.list(url.geturl(), session=session)
  750. cliff_results = []
  751. for r in results:
  752. cliff_results.append((r,))
  753. return (("Image Name",), cliff_results)
  754. class TripleOContainerImageShow(command.ShowOne):
  755. """Show image selected from the registry."""
  756. auth_required = False
  757. log = logging.getLogger(__name__ + ".TripleoContainerImageShow")
  758. @property
  759. def formatter_default(self):
  760. return 'json'
  761. def get_parser(self, prog_name):
  762. parser = super(TripleOContainerImageShow, self).get_parser(prog_name)
  763. parser.add_argument(
  764. "--username",
  765. dest="username",
  766. metavar='<username>',
  767. help=_("Username for image registry.")
  768. )
  769. parser.add_argument(
  770. "--password",
  771. dest="password",
  772. metavar='<password>',
  773. help=_("Password for image registry.")
  774. )
  775. parser.add_argument(
  776. dest="image_to_inspect",
  777. metavar='<image to inspect>',
  778. help=_(
  779. "Image to be inspected, for example: "
  780. "docker.io/library/centos:7 or "
  781. "docker://docker.io/library/centos:7")
  782. )
  783. return parser
  784. def take_action(self, parsed_args):
  785. self.log.debug("take_action(%s)" % parsed_args)
  786. lock = processlock.ProcessLock()
  787. manager = image_uploader.ImageUploadManager(lock=lock)
  788. uploader = manager.uploader('python')
  789. url = uploader._image_to_url(parsed_args.image_to_inspect)
  790. session = uploader.authenticate(url, parsed_args.username,
  791. parsed_args.password)
  792. image_inspect_result = uploader.inspect(parsed_args.image_to_inspect,
  793. session=session)
  794. return self.format_image_inspect(image_inspect_result)
  795. def format_image_inspect(self, image_inspect_result):
  796. column_names = ['Name']
  797. data = [image_inspect_result.pop('Name')]
  798. result_fields = list(image_inspect_result.keys())
  799. result_fields.sort()
  800. for field in result_fields:
  801. column_names.append(field)
  802. data.append(image_inspect_result[field])
  803. return column_names, data
  804. class TripleOImagePrepareDefault(command.Command):
  805. """Generate a default ContainerImagePrepare parameter."""
  806. auth_required = False
  807. log = logging.getLogger(__name__ + ".TripleoImagePrepare")
  808. def get_parser(self, prog_name):
  809. parser = super(TripleOImagePrepareDefault, self).get_parser(prog_name)
  810. parser.add_argument(
  811. "--output-env-file",
  812. dest="output_env_file",
  813. metavar='<file path>',
  814. help=_("File to write environment file containing default "
  815. "ContainerImagePrepare value."),
  816. )
  817. parser.add_argument(
  818. '--local-push-destination',
  819. dest='push_destination',
  820. action='store_true',
  821. default=False,
  822. help=_('Include a push_destination to trigger upload to a local '
  823. 'registry.')
  824. )
  825. parser.add_argument(
  826. '--enable-registry-login',
  827. dest='registry_login',
  828. action='store_true',
  829. default=False,
  830. help=_('Use this flag to enable the flag to have systems attempt '
  831. 'to login to a remote registry prior to pulling their '
  832. 'containers. This flag should be used when '
  833. '--local-push-destination is *NOT* used and the target '
  834. 'systems will have network connectivity to the remote '
  835. 'registries. Do not use this for an overcloud that '
  836. 'may not have network connectivity to a remote registry.')
  837. )
  838. return parser
  839. def take_action(self, parsed_args):
  840. self.log.debug("take_action(%s)" % parsed_args)
  841. cip = copy.deepcopy(kolla_builder.CONTAINER_IMAGE_PREPARE_PARAM)
  842. if parsed_args.push_destination:
  843. for entry in cip:
  844. entry['push_destination'] = True
  845. params = {
  846. 'ContainerImagePrepare': cip
  847. }
  848. if parsed_args.registry_login:
  849. if parsed_args.push_destination:
  850. self.log.warning('[WARNING] --local-push-destination was used '
  851. 'with --enable-registry-login. Please make '
  852. 'sure you understand the use of these '
  853. 'parameters together as they can cause '
  854. 'deployment failures.')
  855. self.log.warning('[NOTE] Make sure to update the paramter_defaults'
  856. ' with ContainerImageRegistryCredentials for the '
  857. 'registries requiring authentication.')
  858. params['ContainerImageRegistryLogin'] = True
  859. env_data = build_env_file(params, self.app.command_options)
  860. self.app.stdout.write(env_data)
  861. if parsed_args.output_env_file:
  862. if os.path.exists(parsed_args.output_env_file):
  863. self.log.warning("Output env file exists, "
  864. "moving it to backup.")
  865. shutil.move(parsed_args.output_env_file,
  866. parsed_args.output_env_file + ".backup")
  867. utils.safe_write(parsed_args.output_env_file, env_data)
  868. class TripleOImagePrepare(command.Command):
  869. """Prepare and upload containers from a single command."""
  870. auth_required = False
  871. log = logging.getLogger(__name__ + ".TripleoImagePrepare")
  872. def get_parser(self, prog_name):
  873. parser = super(TripleOImagePrepare, self).get_parser(prog_name)
  874. try:
  875. roles_file = utils.rel_or_abs_path(
  876. constants.OVERCLOUD_ROLES_FILE,
  877. constants.TRIPLEO_HEAT_TEMPLATES)
  878. except exceptions.DeploymentError:
  879. roles_file = None
  880. parser.add_argument(
  881. '--environment-file', '-e', metavar='<file path>',
  882. action='append', dest='environment_files',
  883. help=_('Environment file containing the ContainerImagePrepare '
  884. 'parameter which specifies all prepare actions. '
  885. 'Also, environment files specifying which services are '
  886. 'containerized. Entries will be filtered to only contain '
  887. 'images used by containerized services. (Can be specified '
  888. 'more than once.)')
  889. )
  890. parser.add_argument(
  891. '--environment-directory', metavar='<HEAT ENVIRONMENT DIRECTORY>',
  892. action='append', dest='environment_directories',
  893. default=[os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)],
  894. help=_('Environment file directories that are automatically '
  895. 'added to the environment. '
  896. 'Can be specified more than once. Files in directories are '
  897. 'loaded in ascending sort order.')
  898. )
  899. parser.add_argument(
  900. '--roles-file', '-r', dest='roles_file',
  901. default=roles_file,
  902. help=_(
  903. 'Roles file, overrides the default %s in the t-h-t templates '
  904. 'directory used for deployment. May be an '
  905. 'absolute path or the path relative to the templates dir.'
  906. ) % constants.OVERCLOUD_ROLES_FILE
  907. )
  908. parser.add_argument(
  909. "--output-env-file",
  910. dest="output_env_file",
  911. metavar='<file path>',
  912. help=_("File to write heat environment file which specifies all "
  913. "image parameters. Any existing file will be overwritten."),
  914. )
  915. parser.add_argument(
  916. '--dry-run',
  917. dest='dry_run',
  918. action='store_true',
  919. default=False,
  920. help=_('Do not perform any pull, modify, or push operations. '
  921. 'The environment file will still be populated as if these '
  922. 'operations were performed.')
  923. )
  924. parser.add_argument(
  925. "--cleanup",
  926. dest="cleanup",
  927. metavar='<full, partial, none>',
  928. default=image_uploader.CLEANUP_FULL,
  929. help=_("Cleanup behavior for local images left after upload. "
  930. "The default 'full' will attempt to delete all local "
  931. "images. 'partial' will leave images required for "
  932. "deployment on this host. 'none' will do no cleanup.")
  933. )
  934. return parser
  935. def take_action(self, parsed_args):
  936. self.log.debug("take_action(%s)" % parsed_args)
  937. if parsed_args.cleanup not in image_uploader.CLEANUP:
  938. raise oscexc.CommandError('--cleanup must be one of: %s' %
  939. ', '.join(image_uploader.CLEANUP))
  940. roles_data = utils.fetch_roles_file(parsed_args.roles_file)
  941. env = utils.build_prepare_env(
  942. parsed_args.environment_files,
  943. parsed_args.environment_directories
  944. )
  945. lock = processlock.ProcessLock()
  946. params = kolla_builder.container_images_prepare_multi(
  947. env, roles_data, dry_run=parsed_args.dry_run,
  948. cleanup=parsed_args.cleanup, lock=lock)
  949. env_data = build_env_file(params, self.app.command_options)
  950. if parsed_args.output_env_file:
  951. if os.path.exists(parsed_args.output_env_file):
  952. self.log.warning("Output env file exists, "
  953. "moving it to backup.")
  954. shutil.move(parsed_args.output_env_file,
  955. parsed_args.output_env_file + ".backup")
  956. utils.safe_write(parsed_args.output_env_file, env_data)
  957. else:
  958. self.app.stdout.write(env_data)