#   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"] = 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)
