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.

976 lines
37KB

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