In the case where a relative path, or just a filename, is passed as --output-env-file to container image prepare, the output file ends up relative to the cli-container-image-prepare.yaml playbook. In almost all cases, this is not what is expected, especially as the output file could end up in the playbook directory. Instead, absolutize the path, so that the output file ends up relative to the working directory from where the command was run. Signed-off-by: James Slagle <jslagle@redhat.com> Change-Id: I9227365ef2900b1f077ac1abdcb4443957b15958
1099 lines
42 KiB
Python
1099 lines
42 KiB
Python
# Copyright 2015 Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
#
|
|
|
|
import copy
|
|
import datetime
|
|
import errno
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
|
|
from osc_lib import exceptions as oscexc
|
|
from osc_lib.i18n import _
|
|
import six
|
|
from six.moves.urllib import parse
|
|
import yaml
|
|
|
|
from tripleo_common.image.builder import buildah
|
|
from tripleo_common.image import image_uploader
|
|
from tripleo_common.image import kolla_builder
|
|
from tripleo_common.utils.locks import processlock
|
|
from tripleoclient import utils as oooutils
|
|
|
|
from tripleoclient import command
|
|
from tripleoclient import constants
|
|
from tripleoclient import exceptions
|
|
from tripleoclient import utils
|
|
|
|
|
|
def build_env_file(params, command_options):
|
|
|
|
f = six.StringIO()
|
|
f.write('# Generated with the following on %s\n#\n' %
|
|
datetime.datetime.now().isoformat())
|
|
f.write('# openstack %s\n#\n\n' %
|
|
' '.join(command_options))
|
|
|
|
yaml.safe_dump({'parameter_defaults': params}, f,
|
|
default_flow_style=False)
|
|
return f.getvalue()
|
|
|
|
|
|
class UploadImage(command.Command):
|
|
"""Push overcloud container images to registries."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".UploadImage")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(UploadImage, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--config-file",
|
|
dest="config_files",
|
|
metavar='<yaml config file>',
|
|
default=[],
|
|
action="append",
|
|
required=True,
|
|
help=_("YAML config file specifying the image build. May be "
|
|
"specified multiple times. Order is preserved, and later "
|
|
"files will override some options in previous files. "
|
|
"Other options will append."),
|
|
)
|
|
parser.add_argument(
|
|
"--cleanup",
|
|
dest="cleanup",
|
|
metavar='<full, partial, none>',
|
|
default=image_uploader.CLEANUP_FULL,
|
|
help=_("Cleanup behavior for local images left after upload. "
|
|
"The default 'full' will attempt to delete all local "
|
|
"images. 'partial' will leave images required for "
|
|
"deployment on this host. 'none' will do no cleanup.")
|
|
)
|
|
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
if parsed_args.cleanup not in image_uploader.CLEANUP:
|
|
raise oscexc.CommandError('--cleanup must be one of: %s' %
|
|
', '.join(image_uploader.CLEANUP))
|
|
lock = processlock.ProcessLock()
|
|
uploader = image_uploader.ImageUploadManager(
|
|
parsed_args.config_files, cleanup=parsed_args.cleanup, lock=lock)
|
|
try:
|
|
uploader.upload()
|
|
except KeyboardInterrupt: # ctrl-c
|
|
self.log.warning('Upload was interrupted by ctrl-c.')
|
|
|
|
|
|
class BuildImage(command.Command):
|
|
"""Build overcloud container images with kolla-build."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".BuildImage")
|
|
|
|
@staticmethod
|
|
def images_from_deps(images, dep):
|
|
'''Builds a list from the dependencies depth-first. '''
|
|
if isinstance(dep, list):
|
|
for v in dep:
|
|
BuildImage.images_from_deps(images, v)
|
|
elif isinstance(dep, dict):
|
|
for k, v in dep.items():
|
|
images.append(k)
|
|
BuildImage.images_from_deps(images, v)
|
|
else:
|
|
images.append(dep)
|
|
|
|
def get_parser(self, prog_name):
|
|
default_kolla_conf = os.path.join(
|
|
sys.prefix, 'share', 'tripleo-common', 'container-images',
|
|
'tripleo_kolla_config_overrides.conf')
|
|
parser = super(BuildImage, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--config-file",
|
|
dest="config_files",
|
|
metavar='<yaml config file>',
|
|
default=[],
|
|
action="append",
|
|
help=_("YAML config file specifying the images to build. May be "
|
|
"specified multiple times. Order is preserved, and later "
|
|
"files will override some options in previous files. "
|
|
"Other options will append. If not specified, the default "
|
|
"set of containers will be built."),
|
|
)
|
|
parser.add_argument(
|
|
"--kolla-config-file",
|
|
dest="kolla_config_files",
|
|
metavar='<config file>',
|
|
default=[default_kolla_conf],
|
|
action="append",
|
|
required=True,
|
|
help=_("Path to a Kolla config file to use. Multiple config files "
|
|
"can be specified, with values in later files taking "
|
|
"precedence. By default, tripleo kolla conf file {conf} "
|
|
"is added.").format(conf=default_kolla_conf),
|
|
)
|
|
parser.add_argument(
|
|
'--list-images',
|
|
dest='list_images',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Show the images which would be built instead of '
|
|
'building them.')
|
|
)
|
|
parser.add_argument(
|
|
'--list-dependencies',
|
|
dest='list_dependencies',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Show the image build dependencies instead of '
|
|
'building them.')
|
|
)
|
|
parser.add_argument(
|
|
"--exclude",
|
|
dest="excludes",
|
|
metavar='<container-name>',
|
|
default=[],
|
|
action="append",
|
|
help=_("Name of a container to match against the list of "
|
|
"containers to be built to skip. Can be specified multiple "
|
|
"times."),
|
|
)
|
|
parser.add_argument(
|
|
'--use-buildah',
|
|
dest='use_buildah',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Use Buildah instead of Docker to build the images '
|
|
'with Kolla.')
|
|
)
|
|
parser.add_argument(
|
|
"--work-dir",
|
|
dest="work_dir",
|
|
default='/tmp/container-builds',
|
|
metavar='<container builds directory>',
|
|
help=_("TripleO container builds directory, storing configs and "
|
|
"logs for each image and its dependencies.")
|
|
)
|
|
parser.add_argument(
|
|
"--build-timeout",
|
|
dest="build_timeout",
|
|
default=None,
|
|
type=int,
|
|
metavar="<build timeout in seconds>",
|
|
help=_("Build timeout in seconds.")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
self.log.warning('This command is deprecated. Please use "openstack '
|
|
'tripleo container image build" instead.')
|
|
|
|
fd, path = tempfile.mkstemp(prefix='kolla_conf_')
|
|
with os.fdopen(fd, 'w') as tmp:
|
|
tmp.write('[DEFAULT]\n')
|
|
if parsed_args.list_images or parsed_args.list_dependencies:
|
|
tmp.write('list_dependencies=true')
|
|
kolla_config_files = list(parsed_args.kolla_config_files)
|
|
kolla_config_files.append(path)
|
|
# Generate an unique work directory so we can keep configs and logs
|
|
# each time we run the command; they'll be stored in work_dir.
|
|
kolla_work_dir = os.path.join(parsed_args.work_dir, str(uuid.uuid4()))
|
|
|
|
# Make sure the unique work directory exists
|
|
if not os.path.exists(kolla_work_dir):
|
|
self.log.debug("Creating container builds "
|
|
"workspace in: %s" % kolla_work_dir)
|
|
os.makedirs(kolla_work_dir)
|
|
|
|
builder = kolla_builder.KollaImageBuilder(parsed_args.config_files)
|
|
result = builder.build_images(kolla_config_files,
|
|
parsed_args.excludes,
|
|
parsed_args.use_buildah,
|
|
kolla_work_dir)
|
|
|
|
if parsed_args.use_buildah:
|
|
deps = json.loads(result)
|
|
kolla_cfg = utils.get_read_config(kolla_config_files)
|
|
bb = buildah.BuildahBuilder(
|
|
kolla_work_dir, deps,
|
|
utils.get_from_cfg(kolla_cfg, "base"),
|
|
utils.get_from_cfg(kolla_cfg, "type"),
|
|
utils.get_from_cfg(kolla_cfg, "tag"),
|
|
utils.get_from_cfg(kolla_cfg, "namespace"),
|
|
utils.get_from_cfg(kolla_cfg, "registry"),
|
|
utils.getboolean_from_cfg(kolla_cfg, "push"),
|
|
parsed_args.build_timeout,
|
|
self.app.options.debug)
|
|
bb.build_all()
|
|
elif parsed_args.list_dependencies:
|
|
deps = json.loads(result)
|
|
yaml.safe_dump(
|
|
deps,
|
|
self.app.stdout,
|
|
indent=2,
|
|
default_flow_style=False
|
|
)
|
|
elif parsed_args.list_images:
|
|
deps = json.loads(result)
|
|
images = []
|
|
BuildImage.images_from_deps(images, deps)
|
|
yaml.safe_dump(
|
|
images,
|
|
self.app.stdout,
|
|
default_flow_style=False
|
|
)
|
|
elif result:
|
|
self.app.stdout.write(result)
|
|
|
|
|
|
class PrepareImageFiles(command.Command):
|
|
"""Generate files defining the images, tags and registry."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".PrepareImageFiles")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(PrepareImageFiles, self).get_parser(prog_name)
|
|
try:
|
|
roles_file = utils.rel_or_abs_path(
|
|
constants.OVERCLOUD_ROLES_FILE,
|
|
constants.TRIPLEO_HEAT_TEMPLATES)
|
|
except exceptions.DeploymentError:
|
|
roles_file = None
|
|
defaults = kolla_builder.container_images_prepare_defaults()
|
|
|
|
parser.add_argument(
|
|
"--template-file",
|
|
dest="template_file",
|
|
default=kolla_builder.DEFAULT_TEMPLATE_FILE,
|
|
metavar='<yaml template file>',
|
|
help=_("YAML template file which the images config file will be "
|
|
"built from.\n"
|
|
"Default: %s") % kolla_builder.DEFAULT_TEMPLATE_FILE,
|
|
)
|
|
parser.add_argument(
|
|
"--push-destination",
|
|
dest="push_destination",
|
|
metavar='<location>',
|
|
help=_("Location of image registry to push images to. "
|
|
"If specified, a push_destination will be set for every "
|
|
"image entry."),
|
|
)
|
|
parser.add_argument(
|
|
"--tag",
|
|
dest="tag",
|
|
default=defaults['tag'],
|
|
metavar='<tag>',
|
|
help=_("Override the default tag substitution. "
|
|
"If --tag-from-label is specified, "
|
|
"start discovery with this tag.\n"
|
|
"Default: %s") % defaults['tag'],
|
|
)
|
|
parser.add_argument(
|
|
"--tag-from-label",
|
|
dest="tag_from_label",
|
|
metavar='<image label>',
|
|
help=_("Use the value of the specified label(s) to discover the "
|
|
"tag. Labels can be combined in a template format, "
|
|
"for example: {version}-{release}"),
|
|
)
|
|
parser.add_argument(
|
|
"--namespace",
|
|
dest="namespace",
|
|
default=defaults['namespace'],
|
|
metavar='<namespace>',
|
|
help=_("Override the default namespace substitution.\n"
|
|
"Default: %s") % defaults['namespace'],
|
|
)
|
|
parser.add_argument(
|
|
"--prefix",
|
|
dest="prefix",
|
|
default=defaults['name_prefix'],
|
|
metavar='<prefix>',
|
|
help=_("Override the default name prefix substitution.\n"
|
|
"Default: %s") % defaults['name_prefix'],
|
|
)
|
|
parser.add_argument(
|
|
"--suffix",
|
|
dest="suffix",
|
|
default=defaults['name_suffix'],
|
|
metavar='<suffix>',
|
|
help=_("Override the default name suffix substitution.\n"
|
|
"Default: %s") % defaults['name_suffix'],
|
|
)
|
|
parser.add_argument(
|
|
'--set',
|
|
metavar='<variable=value>',
|
|
action='append',
|
|
help=_('Set the value of a variable in the template, even if it '
|
|
'has no dedicated argument such as "--suffix".')
|
|
)
|
|
parser.add_argument(
|
|
"--exclude",
|
|
dest="excludes",
|
|
metavar='<regex>',
|
|
default=[],
|
|
action="append",
|
|
help=_("Pattern to match against resulting imagename entries to "
|
|
"exclude from the final output. Can be specified multiple "
|
|
"times."),
|
|
)
|
|
parser.add_argument(
|
|
"--include",
|
|
dest="includes",
|
|
metavar='<regex>',
|
|
default=[],
|
|
action="append",
|
|
help=_("Pattern to match against resulting imagename entries to "
|
|
"include in final output. Can be specified multiple "
|
|
"times, entries not matching any --include will be "
|
|
"excluded. --exclude is ignored if --include is used."),
|
|
)
|
|
parser.add_argument(
|
|
"--output-images-file",
|
|
dest="output_images_file",
|
|
metavar='<file path>',
|
|
help=_("File to write resulting image entries to, as well as "
|
|
"stdout. Any existing file will be overwritten."),
|
|
)
|
|
parser.add_argument(
|
|
'--environment-file', '-e', metavar='<file path>',
|
|
action='append', dest='environment_files',
|
|
help=_('Environment files specifying which services are '
|
|
'containerized. Entries will be filtered to only contain '
|
|
'images used by containerized services. (Can be specified '
|
|
'more than once.)')
|
|
)
|
|
parser.add_argument(
|
|
'--environment-directory', metavar='<HEAT ENVIRONMENT DIRECTORY>',
|
|
action='append', dest='environment_directories',
|
|
default=[os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)],
|
|
help=_('Environment file directories that are automatically '
|
|
'added to the update command. Entries will be filtered '
|
|
'to only contain images used by containerized services. '
|
|
'Can be specified more than once. Files in directories are '
|
|
'loaded in ascending sort order.')
|
|
)
|
|
parser.add_argument(
|
|
"--output-env-file",
|
|
dest="output_env_file",
|
|
metavar='<file path>',
|
|
help=_("File to write heat environment file which specifies all "
|
|
"image parameters. Any existing file will be overwritten."),
|
|
)
|
|
parser.add_argument(
|
|
'--roles-file', '-r', dest='roles_file',
|
|
default=roles_file,
|
|
help=_(
|
|
'Roles file, overrides the default %s in the t-h-t templates '
|
|
'directory used for deployment. May be an '
|
|
'absolute path or the path relative to the templates dir.'
|
|
) % constants.OVERCLOUD_ROLES_FILE
|
|
)
|
|
parser.add_argument(
|
|
'--modify-role',
|
|
dest='modify_role',
|
|
help=_('Name of ansible role to run between every image upload '
|
|
'pull and push.')
|
|
)
|
|
parser.add_argument(
|
|
'--modify-vars',
|
|
dest='modify_vars',
|
|
help=_('Ansible variable file containing variables to use when '
|
|
'invoking the role --modify-role.')
|
|
)
|
|
return parser
|
|
|
|
def parse_set_values(self, subs, set_values):
|
|
if not set_values:
|
|
return
|
|
for s in set_values:
|
|
try:
|
|
(n, v) = s.split(('='), 1)
|
|
subs[n] = v
|
|
except ValueError:
|
|
msg = _('Malformed --set(%s). '
|
|
'Use the variable=value format.') % s
|
|
raise oscexc.CommandError(msg)
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
self.log.warning("[DEPRECATED] This command has been deprecated and "
|
|
"replaced by the 'openstack tripleo container image "
|
|
"prepare' command.")
|
|
|
|
roles_data = utils.fetch_roles_file(parsed_args.roles_file) or set()
|
|
|
|
env = utils.build_prepare_env(
|
|
parsed_args.environment_files,
|
|
parsed_args.environment_directories
|
|
)
|
|
|
|
if roles_data:
|
|
service_filter = kolla_builder.build_service_filter(
|
|
env, roles_data)
|
|
else:
|
|
service_filter = None
|
|
mapping_args = {
|
|
'tag': parsed_args.tag,
|
|
'namespace': parsed_args.namespace,
|
|
'name_prefix': parsed_args.prefix,
|
|
'name_suffix': parsed_args.suffix,
|
|
}
|
|
self.parse_set_values(mapping_args, parsed_args.set)
|
|
pd = env.get('parameter_defaults', {})
|
|
kolla_builder.set_neutron_driver(pd, mapping_args)
|
|
|
|
output_images_file = (parsed_args.output_images_file
|
|
or 'container_images.yaml')
|
|
modify_role = None
|
|
modify_vars = None
|
|
append_tag = None
|
|
if parsed_args.modify_role:
|
|
modify_role = parsed_args.modify_role
|
|
append_tag = time.strftime('-modified-%Y%m%d%H%M%S')
|
|
if parsed_args.modify_vars:
|
|
with open(parsed_args.modify_vars) as m:
|
|
modify_vars = yaml.safe_load(m.read())
|
|
|
|
prepare_data = kolla_builder.container_images_prepare(
|
|
excludes=parsed_args.excludes,
|
|
includes=parsed_args.includes,
|
|
service_filter=service_filter,
|
|
push_destination=parsed_args.push_destination,
|
|
mapping_args=mapping_args,
|
|
output_env_file=parsed_args.output_env_file,
|
|
output_images_file=output_images_file,
|
|
tag_from_label=parsed_args.tag_from_label,
|
|
modify_role=modify_role,
|
|
modify_vars=modify_vars,
|
|
append_tag=append_tag,
|
|
template_file=parsed_args.template_file
|
|
)
|
|
if parsed_args.output_env_file:
|
|
params = prepare_data[parsed_args.output_env_file]
|
|
output_env_file_expanded = os.path.expanduser(
|
|
parsed_args.output_env_file)
|
|
if os.path.exists(output_env_file_expanded):
|
|
self.log.warning("Output env file exists, "
|
|
"moving it to backup.")
|
|
shutil.move(output_env_file_expanded,
|
|
output_env_file_expanded + ".backup")
|
|
utils.safe_write(output_env_file_expanded,
|
|
build_env_file(params, self.app.command_options))
|
|
|
|
result = prepare_data[output_images_file]
|
|
result_str = yaml.safe_dump({'container_images': result},
|
|
default_flow_style=False)
|
|
sys.stdout.write(result_str)
|
|
|
|
if parsed_args.output_images_file:
|
|
utils.safe_write(parsed_args.output_images_file, result_str)
|
|
|
|
|
|
class DiscoverImageTag(command.Command):
|
|
"""Discover the versioned tag for an image."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".DiscoverImageTag")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(DiscoverImageTag, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--image",
|
|
dest="image",
|
|
metavar='<container image>',
|
|
required=True,
|
|
help=_("Fully qualified name of the image to discover the tag for "
|
|
"(Including registry and stable tag)."),
|
|
)
|
|
parser.add_argument(
|
|
"--tag-from-label",
|
|
dest="tag_from_label",
|
|
metavar='<image label>',
|
|
help=_("Use the value of the specified label(s) to discover the "
|
|
"tag. Labels can be combined in a template format, "
|
|
"for example: {version}-{release}"),
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
self.log.warning("[DEPRECATED] This command has been deprecated and "
|
|
"replaced by the 'openstack tripleo container image "
|
|
"prepare' command.")
|
|
|
|
lock = processlock.ProcessLock()
|
|
uploader = image_uploader.ImageUploadManager([], lock=lock)
|
|
print(uploader.discover_image_tag(
|
|
image=parsed_args.image,
|
|
tag_from_label=parsed_args.tag_from_label
|
|
))
|
|
|
|
|
|
class TripleOContainerImagePush(command.Command):
|
|
"""Push specified image to registry."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoContainerImagePush")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOContainerImagePush, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--local",
|
|
dest="local",
|
|
default=False,
|
|
action="store_true",
|
|
help=_("Use this flag if the container image is already on the "
|
|
"current system and does not need to be pulled from a "
|
|
"remote registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--registry-url",
|
|
dest="registry_url",
|
|
metavar='<registry url>',
|
|
default=None,
|
|
help=_("URL of the destination registry in the form "
|
|
"<fqdn>:<port>.")
|
|
)
|
|
parser.add_argument(
|
|
"--append-tag",
|
|
dest="append_tag",
|
|
default='',
|
|
help=_("Tag to append to the existing tag when pushing the "
|
|
"container. ")
|
|
)
|
|
parser.add_argument(
|
|
"--username",
|
|
dest="username",
|
|
metavar='<username>',
|
|
help=_("Username for the destination image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
dest="password",
|
|
metavar='<password>',
|
|
help=_("Password for the destination image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--source-username",
|
|
dest="source_username",
|
|
metavar='<source_username>',
|
|
help=_("Username for the source image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--source-password",
|
|
dest="source_password",
|
|
metavar='<source_password>',
|
|
help=_("Password for the source image registry.")
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
dest="dry_run",
|
|
action="store_true",
|
|
help=_("Perform a dry run upload. The upload action is not "
|
|
"performed, but the authentication process is attempted.")
|
|
)
|
|
parser.add_argument(
|
|
"--multi-arch",
|
|
dest="multi_arch",
|
|
action="store_true",
|
|
help=_("Enable multi arch support for the upload.")
|
|
)
|
|
parser.add_argument(
|
|
"--cleanup",
|
|
dest="cleanup",
|
|
action="store_true",
|
|
default=False,
|
|
help=_("Remove local copy of the image after uploading")
|
|
)
|
|
parser.add_argument(
|
|
dest="image_to_push",
|
|
metavar='<image to push>',
|
|
help=_("Container image to upload. Should be in the form of "
|
|
"<registry>/<namespace>/<name>:<tag>. If tag is "
|
|
"not provided, then latest will be used.")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
lock = processlock.ProcessLock()
|
|
manager = image_uploader.ImageUploadManager(lock=lock)
|
|
uploader = manager.uploader('python')
|
|
|
|
source_image = parsed_args.image_to_push
|
|
|
|
if parsed_args.local or source_image.startswith('containers-storage:'):
|
|
storage = 'containers-storage:'
|
|
if not source_image.startswith(storage):
|
|
source_image = storage + source_image.replace('docker://', '')
|
|
elif not parsed_args.local:
|
|
self.log.warning('Assuming local container based on provided '
|
|
'container path. (e.g. starts with '
|
|
'containers-storage:)')
|
|
source_url = parse.urlparse(source_image)
|
|
image_name = source_url.geturl()
|
|
image_source = None
|
|
if parsed_args.source_username or parsed_args.source_password:
|
|
self.log.warning('Source credentials ignored for local images')
|
|
else:
|
|
storage = 'docker://'
|
|
if not source_image.startswith(storage):
|
|
source_image = storage + source_image
|
|
source_url = parse.urlparse(source_image)
|
|
image_source = source_url.netloc
|
|
image_name = source_url.path[1:]
|
|
if len(image_name.split('/')) != 2:
|
|
raise exceptions.DownloadError('Invalid container. Provided '
|
|
'container image should be '
|
|
'<registry>/<namespace>/<name>:'
|
|
'<tag>')
|
|
if parsed_args.source_username or parsed_args.source_password:
|
|
if not parsed_args.source_username:
|
|
self.log.warning('Skipping authentication - missing source'
|
|
' username')
|
|
elif not parsed_args.source_password:
|
|
self.log.warning('Skipping authentication - missing source'
|
|
' password')
|
|
else:
|
|
uploader.authenticate(source_url,
|
|
parsed_args.source_username,
|
|
parsed_args.source_password)
|
|
|
|
registry_url_arg = parsed_args.registry_url
|
|
if registry_url_arg is None:
|
|
registry_url_arg = image_uploader.get_undercloud_registry()
|
|
if not registry_url_arg.startswith('docker://'):
|
|
registry_url = 'docker://%s' % registry_url_arg
|
|
else:
|
|
registry_url = registry_url_arg
|
|
reg_url = parse.urlparse(registry_url)
|
|
|
|
session = uploader.authenticate(reg_url,
|
|
parsed_args.username,
|
|
parsed_args.password)
|
|
try:
|
|
if not parsed_args.dry_run:
|
|
task = image_uploader.UploadTask(
|
|
image_name=image_name,
|
|
pull_source=image_source,
|
|
push_destination=registry_url_arg,
|
|
append_tag=parsed_args.append_tag,
|
|
modify_role=None,
|
|
modify_vars=None,
|
|
cleanup=parsed_args.cleanup,
|
|
multi_arch=parsed_args.multi_arch)
|
|
|
|
uploader.add_upload_task(task)
|
|
uploader.run_tasks()
|
|
except OSError as e:
|
|
if e.errno == errno.EACCES:
|
|
self.log.error("Unable to upload due to permissions. "
|
|
"Please prefix command with sudo.")
|
|
raise oscexc.CommandError(e)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
class TripleOContainerImageDelete(command.Command):
|
|
"""Delete specified image from registry."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoContainerImageDelete")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOContainerImageDelete, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--registry-url",
|
|
dest="registry_url",
|
|
metavar='<registry url>',
|
|
default=None,
|
|
help=_("URL of registry images are to be listed from in the "
|
|
"form <fqdn>:<port>.")
|
|
)
|
|
parser.add_argument(
|
|
dest="image_to_delete",
|
|
metavar='<image to delete>',
|
|
help=_("Full URL of image to be deleted in the "
|
|
"form <fqdn>:<port>/path/to/image")
|
|
)
|
|
parser.add_argument(
|
|
"--username",
|
|
dest="username",
|
|
metavar='<username>',
|
|
help=_("Username for image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
dest="password",
|
|
metavar='<password>',
|
|
help=_("Password for image registry.")
|
|
)
|
|
parser.add_argument(
|
|
'-y', '--yes',
|
|
help=_('Skip yes/no prompt (assume yes).'),
|
|
default=False,
|
|
action="store_true")
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
if not parsed_args.yes:
|
|
confirm = utils.prompt_user_for_confirmation(
|
|
message=_("Are you sure you want to delete this image "
|
|
"[y/N]? "),
|
|
logger=self.log)
|
|
if not confirm:
|
|
raise oscexc.CommandError("Action not confirmed, exiting.")
|
|
|
|
lock = processlock.ProcessLock()
|
|
manager = image_uploader.ImageUploadManager(lock=lock)
|
|
uploader = manager.uploader('python')
|
|
registry_url_arg = parsed_args.registry_url
|
|
if registry_url_arg is None:
|
|
registry_url_arg = image_uploader.get_undercloud_registry()
|
|
url = uploader._image_to_url(registry_url_arg)
|
|
session = uploader.authenticate(url, parsed_args.username,
|
|
parsed_args.password)
|
|
|
|
try:
|
|
uploader.delete(parsed_args.image_to_delete, session=session)
|
|
except OSError as e:
|
|
if e.errno == errno.EACCES:
|
|
self.log.error("Unable to remove due to permissions. "
|
|
"Please prefix command with sudo.")
|
|
raise oscexc.CommandError(e)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
class TripleOContainerImageList(command.Lister):
|
|
"""List images discovered in registry."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoContainerImageList")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOContainerImageList, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--registry-url",
|
|
dest="registry_url",
|
|
metavar='<registry url>',
|
|
default=None,
|
|
help=_("URL of registry images are to be listed from in the "
|
|
"form <fqdn>:<port>.")
|
|
)
|
|
parser.add_argument(
|
|
"--username",
|
|
dest="username",
|
|
metavar='<username>',
|
|
help=_("Username for image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
dest="password",
|
|
metavar='<password>',
|
|
help=_("Password for image registry.")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
lock = processlock.ProcessLock()
|
|
manager = image_uploader.ImageUploadManager(lock=lock)
|
|
uploader = manager.uploader('python')
|
|
registry_url_arg = parsed_args.registry_url
|
|
if registry_url_arg is None:
|
|
registry_url_arg = image_uploader.get_undercloud_registry()
|
|
url = uploader._image_to_url(registry_url_arg)
|
|
session = uploader.authenticate(url, parsed_args.username,
|
|
parsed_args.password)
|
|
try:
|
|
results = uploader.list(url.geturl(), session=session)
|
|
finally:
|
|
session.close()
|
|
|
|
cliff_results = []
|
|
for r in results:
|
|
cliff_results.append((r,))
|
|
return (("Image Name",), cliff_results)
|
|
|
|
|
|
class TripleOContainerImageShow(command.ShowOne):
|
|
"""Show image selected from the registry."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoContainerImageShow")
|
|
|
|
@property
|
|
def formatter_default(self):
|
|
return 'json'
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOContainerImageShow, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--username",
|
|
dest="username",
|
|
metavar='<username>',
|
|
help=_("Username for image registry.")
|
|
)
|
|
parser.add_argument(
|
|
"--password",
|
|
dest="password",
|
|
metavar='<password>',
|
|
help=_("Password for image registry.")
|
|
)
|
|
parser.add_argument(
|
|
dest="image_to_inspect",
|
|
metavar='<image to inspect>',
|
|
help=_(
|
|
"Image to be inspected, for example: "
|
|
"docker.io/library/centos:7 or "
|
|
"docker://docker.io/library/centos:7")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
lock = processlock.ProcessLock()
|
|
manager = image_uploader.ImageUploadManager(lock=lock)
|
|
uploader = manager.uploader('python')
|
|
url = uploader._image_to_url(parsed_args.image_to_inspect)
|
|
session = uploader.authenticate(url, parsed_args.username,
|
|
parsed_args.password)
|
|
try:
|
|
image_inspect_result = uploader.inspect(
|
|
parsed_args.image_to_inspect,
|
|
session=session)
|
|
finally:
|
|
session.close()
|
|
|
|
return self.format_image_inspect(image_inspect_result)
|
|
|
|
def format_image_inspect(self, image_inspect_result):
|
|
column_names = ['Name']
|
|
data = [image_inspect_result.pop('Name')]
|
|
|
|
result_fields = list(image_inspect_result.keys())
|
|
result_fields.sort()
|
|
for field in result_fields:
|
|
column_names.append(field)
|
|
data.append(image_inspect_result[field])
|
|
|
|
return column_names, data
|
|
|
|
|
|
class TripleOImagePrepareDefault(command.Command):
|
|
"""Generate a default ContainerImagePrepare parameter."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoImagePrepare")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOImagePrepareDefault, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--output-env-file",
|
|
dest="output_env_file",
|
|
metavar='<file path>',
|
|
help=_("File to write environment file containing default "
|
|
"ContainerImagePrepare value."),
|
|
)
|
|
parser.add_argument(
|
|
'--local-push-destination',
|
|
dest='push_destination',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Include a push_destination to trigger upload to a local '
|
|
'registry.')
|
|
)
|
|
parser.add_argument(
|
|
'--enable-registry-login',
|
|
dest='registry_login',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Use this flag to enable the flag to have systems attempt '
|
|
'to login to a remote registry prior to pulling their '
|
|
'containers. This flag should be used when '
|
|
'--local-push-destination is *NOT* used and the target '
|
|
'systems will have network connectivity to the remote '
|
|
'registries. Do not use this for an overcloud that '
|
|
'may not have network connectivity to a remote registry.')
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
cip = copy.deepcopy(kolla_builder.CONTAINER_IMAGE_PREPARE_PARAM)
|
|
if parsed_args.push_destination:
|
|
for entry in cip:
|
|
entry['push_destination'] = True
|
|
params = {
|
|
'ContainerImagePrepare': cip
|
|
}
|
|
if parsed_args.registry_login:
|
|
if parsed_args.push_destination:
|
|
self.log.warning('[WARNING] --local-push-destination was used '
|
|
'with --enable-registry-login. Please make '
|
|
'sure you understand the use of these '
|
|
'parameters together as they can cause '
|
|
'deployment failures.')
|
|
self.log.warning('[NOTE] Make sure to update the paramter_defaults'
|
|
' with ContainerImageRegistryCredentials for the '
|
|
'registries requiring authentication.')
|
|
params['ContainerImageRegistryLogin'] = True
|
|
|
|
env_data = build_env_file(params, self.app.command_options)
|
|
self.app.stdout.write(env_data)
|
|
if parsed_args.output_env_file:
|
|
if os.path.exists(parsed_args.output_env_file):
|
|
self.log.warning("Output env file exists, "
|
|
"moving it to backup.")
|
|
shutil.move(parsed_args.output_env_file,
|
|
parsed_args.output_env_file + ".backup")
|
|
utils.safe_write(parsed_args.output_env_file, env_data)
|
|
|
|
|
|
class TripleOImagePrepare(command.Command):
|
|
"""Prepare and upload containers from a single command."""
|
|
|
|
auth_required = False
|
|
log = logging.getLogger(__name__ + ".TripleoImagePrepare")
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(TripleOImagePrepare, self).get_parser(prog_name)
|
|
try:
|
|
roles_file = utils.rel_or_abs_path(
|
|
constants.OVERCLOUD_ROLES_FILE,
|
|
constants.TRIPLEO_HEAT_TEMPLATES)
|
|
except exceptions.DeploymentError:
|
|
roles_file = None
|
|
parser.add_argument(
|
|
'--environment-file', '-e', metavar='<file path>',
|
|
action='append', dest='environment_files',
|
|
help=_('Environment file containing the ContainerImagePrepare '
|
|
'parameter which specifies all prepare actions. '
|
|
'Also, environment files specifying which services are '
|
|
'containerized. Entries will be filtered to only contain '
|
|
'images used by containerized services. (Can be specified '
|
|
'more than once.)')
|
|
)
|
|
parser.add_argument(
|
|
'--environment-directory', metavar='<HEAT ENVIRONMENT DIRECTORY>',
|
|
action='append', dest='environment_directories',
|
|
default=[os.path.expanduser(constants.DEFAULT_ENV_DIRECTORY)],
|
|
help=_('Environment file directories that are automatically '
|
|
'added to the environment. '
|
|
'Can be specified more than once. Files in directories are '
|
|
'loaded in ascending sort order.')
|
|
)
|
|
parser.add_argument(
|
|
'--roles-file', '-r', dest='roles_file',
|
|
default=roles_file,
|
|
help=_(
|
|
'Roles file, overrides the default %s in the t-h-t templates '
|
|
'directory used for deployment. May be an '
|
|
'absolute path or the path relative to the templates dir.'
|
|
) % constants.OVERCLOUD_ROLES_FILE
|
|
)
|
|
parser.add_argument(
|
|
"--output-env-file",
|
|
dest="output_env_file",
|
|
metavar='<file path>',
|
|
help=_("File to write heat environment file which specifies all "
|
|
"image parameters. Any existing file will be overwritten."),
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
dest='dry_run',
|
|
action='store_true',
|
|
default=False,
|
|
help=_('Do not perform any pull, modify, or push operations. '
|
|
'The environment file will still be populated as if these '
|
|
'operations were performed.')
|
|
)
|
|
parser.add_argument(
|
|
"--cleanup",
|
|
dest="cleanup",
|
|
metavar='<full, partial, none>',
|
|
default=image_uploader.CLEANUP_FULL,
|
|
help=_("Cleanup behavior for local images left after upload. "
|
|
"The default 'full' will attempt to delete all local "
|
|
"images. 'partial' will leave images required for "
|
|
"deployment on this host. 'none' will do no cleanup.")
|
|
)
|
|
parser.add_argument(
|
|
"--log-file",
|
|
dest="log_file",
|
|
default=constants.CONTAINER_IMAGE_PREPARE_LOG_FILE,
|
|
help=_("Log file to be used for python logging. "
|
|
"By default it would be logged to "
|
|
"$HOME/container_image_prepare.log.")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
self.log.debug("take_action(%s)" % parsed_args)
|
|
|
|
if parsed_args.cleanup not in image_uploader.CLEANUP:
|
|
raise oscexc.CommandError('--cleanup must be one of: %s' %
|
|
', '.join(image_uploader.CLEANUP))
|
|
|
|
role_file = None
|
|
if parsed_args.roles_file:
|
|
role_file = utils.rel_or_abs_path(parsed_args.roles_file,
|
|
constants.TRIPLEO_HEAT_TEMPLATES)
|
|
env_dirs = [os.path.abspath(x)
|
|
for x in parsed_args.environment_directories]
|
|
env_files = [os.path.abspath(x)
|
|
for x in parsed_args.environment_files]
|
|
extra_vars = {
|
|
"roles_file": role_file,
|
|
"environment_directories": env_dirs,
|
|
"environment_files": env_files,
|
|
"cleanup": parsed_args.cleanup,
|
|
"dry_run": parsed_args.dry_run,
|
|
"log_file": parsed_args.log_file}
|
|
|
|
if self.app_args.verbose_level >= 3:
|
|
extra_vars["debug"] = True
|
|
|
|
if parsed_args.output_env_file:
|
|
extra_vars["output_env_file"] = os.path.abspath(
|
|
parsed_args.output_env_file)
|
|
|
|
with oooutils.TempDirs() as tmp:
|
|
oooutils.run_ansible_playbook(
|
|
playbook='cli-container-image-prepare.yaml',
|
|
inventory='localhost,',
|
|
workdir=tmp,
|
|
playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS,
|
|
verbosity=oooutils.playbook_verbosity(self=self),
|
|
extra_vars=extra_vars)
|