1
0
mirror of https://github.com/ansible/mazer.git synced 2026-02-05 12:45:17 +01:00
Files
mazer/ansible_galaxy_cli/cli/galaxy.py
2018-09-13 09:08:19 -04:00

359 lines
16 KiB
Python

########################################################################
#
# (C) 2018, Ansible by Red Hat
#
# This file is part of Mazer
#
# Mazer is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Mazer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
########################################################################
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import logging
import os
import sys
from ansible_galaxy_cli import cli
from ansible_galaxy_cli import __version__ as galaxy_cli_version
from ansible_galaxy.actions import build
from ansible_galaxy.actions import info
from ansible_galaxy.actions import init
from ansible_galaxy.actions import install
from ansible_galaxy.actions import list as list_action
from ansible_galaxy.actions import remove
from ansible_galaxy.actions import version
from ansible_galaxy.config import defaults
from ansible_galaxy.config import config
from ansible_galaxy_cli import exceptions as cli_exceptions
from ansible_galaxy import matchers
from ansible_galaxy.models.context import GalaxyContext
from ansible_galaxy.models.build_context import BuildContext
from ansible_galaxy import rest_api
log = logging.getLogger(__name__)
def exit_without_ignore(ignore_errors, msg=None, rc=1):
"""
Exits with the specified return code unless the
option --ignore-errors was specified
"""
ignore_error_blurb = '- you can use --ignore-errors to skip failed roles and finish processing the list.'
if not ignore_errors:
message = ignore_error_blurb
if msg:
message = '%s:\n%s' % (msg, ignore_error_blurb)
raise cli_exceptions.GalaxyCliError(message)
class GalaxyCLI(cli.CLI):
'''command to manage Ansible roles in shared repostories, the default of which is Ansible Galaxy *https://galaxy.ansible.com*.'''
SKIP_INFO_KEYS = ("name", "description", "readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url")
VALID_ACTIONS = ("build", "info", "init", "install", "list", "remove", "version")
VALID_ACTION_ALIASES = {'content-install': 'install'}
def __init__(self, args):
self.api = None
super(GalaxyCLI, self).__init__(args)
def set_action(self):
super(GalaxyCLI, self).set_action()
# specific to actions
if self.action == "build":
self.parser.set_usage("usage: %prog build [options]")
self.parser.add_option('--collection-path', dest='collection_path', default=None,
help='The path in which the collection repository is located. The default is the current working directory.')
self.parser.add_option('--output-path', dest='output_path', default=None,
help='The path in which the collection artifact will be created. The default is ./releases/.')
if self.action == "info":
self.parser.set_usage("usage: %prog info [options] repo_name[,version]")
elif self.action == "init":
self.parser.set_usage("usage: %prog init [options] role_name")
self.parser.add_option('--init-path', dest='init_path', default="./",
help='The path in which the skeleton role will be created. The default is the current working directory.')
self.parser.add_option('--type', dest='role_type', action='store', default='default',
help="Initialize using an alternate role type. Valid types include: 'default' and 'apb'.")
self.parser.add_option('--role-skeleton', dest='role_skeleton', default=None,
help='The path to a role skeleton that the new role should be based upon.')
elif self.action == "install":
self.parser.set_usage("usage: %prog install [options] [-r FILE | repo_name(s)[,version] | scm+repo_url[,version] | tar_file(s)]")
self.parser.add_option('-g', '--global', dest='global_install', action='store_true',
help='Install content to the path containing your global or system-wide content. The default is the '
'global_content_path configured in your mazer.yml file (/usr/share/ansible/content, if not configured)')
self.parser.add_option('-e', '--editable', dest='editable_install', action='store_true',
help='Link a local directory into the content path for development and testing')
self.parser.add_option('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
help='Ignore errors and continue with the next specified repo.')
self.parser.add_option('-n', '--no-deps', dest='no_deps', action='store_true', default=False, help='Don\'t download roles listed as dependencies')
self.parser.add_option('-r', '--role-file', dest='role_file', help='A file containing a list of roles to be imported')
self.parser.add_option('--namespace', dest='namespace', default=None,
help='The namespace to use when installing content (required for installs from local scm repo or archives)')
elif self.action == "remove":
self.parser.set_usage("usage: %prog remove repo1 repo2 ...")
elif self.action == "list":
self.parser.set_usage("usage: %prog list [repo_name]")
elif self.action == "version":
self.parser.set_usage("usage: %prog version")
# options that apply to more than one action
if self.action in ['init', 'info']:
self.parser.add_option('--offline', dest='offline', default=False, action='store_true', help="Don't query the galaxy API when creating roles")
if self.action not in ("init", "version"):
# NOTE: while the option type=str, the default is a list, and the
# callback will set the value to a list.
self.parser.add_option('-C', '--content-path', dest='content_path',
help='The path to the directory containing your Galaxy content. The default is the content_path configured in your'
'mazer.yml file (~/.ansible/content, if not configured)', type='str')
if self.action in ("init", "install"):
self.parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='Force overwriting an existing repo')
def parse(self):
''' create an options parser for bin/ansible '''
self.parser = cli.CLI.base_parser(
usage="usage: %%prog [%s] [--help] [options] ..." % "|".join(self.VALID_ACTIONS),
epilog="\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
)
# common
self.parser.add_option('-s', '--server', dest='server_url', default=None, help='The API server destination')
self.parser.add_option('-c', '--ignore-certs', action='store_true', dest='ignore_certs', default=None,
help='Ignore SSL certificate validation errors.')
self.set_action()
super(GalaxyCLI, self).parse()
if self.action == 'install' and getattr(self.options, 'content_path') and getattr(self.options, 'global_install'):
raise cli_exceptions.CliOptionsError('--content-path and --global are mutually exclusive')
def _get_galaxy_context(self, options, config):
# use content_path from options if availble but fallback to configured content_path
options_content_path = None
if hasattr(options, 'content_path'):
options_content_path = options.content_path
raw_content_path = options_content_path or config.content_path
if hasattr(options, 'global_install') and options.global_install:
raw_content_path = config.global_content_path
content_path = os.path.expanduser(raw_content_path)
server = config.server.copy()
if getattr(options, 'server_url', None):
server['url'] = options.server_url
if getattr(options, 'ignore_certs', None):
# use ignore certs from options if available, but fallback to configured ignore_certs
server['ignore_certs'] = options.ignore_certs
galaxy_context = GalaxyContext(server=server, content_path=content_path)
return galaxy_context
def run(self):
raw_config_file_path = os.environ.get('ANSIBLE_GALAXY_CONFIG', defaults.CONFIG_FILE)
self.config_file_path = os.path.abspath(os.path.expanduser(raw_config_file_path))
super(GalaxyCLI, self).run()
self.config = config.load(self.config_file_path)
log.debug('configuration: %s', json.dumps(self.config.as_dict(), indent=None))
# cli --server value or the url field of the first server in config
# TODO: pass list of server config objects to GalaxyContext and/or create a GalaxyContext later
# server_url = self.options.server_url or self.config['servers'][0]['url']
# ignore_certs = self.options.ignore_certs or self.config['servers'][0]['ignore_certs']
galaxy_context = self._get_galaxy_context(self.options, self.config)
log.debug('galaxy context: %s', galaxy_context)
self.api = rest_api.GalaxyAPI(galaxy_context)
log.debug('execute action: %s', self.action)
log.debug('execute action with options: %s', self.options)
log.debug('execute action with args: %s', self.args)
return self.execute()
def execute_build(self):
'''Create a collection artifact from a collection src repository.'''
log.debug('options: %s', self.options)
log.debug('args: %s', self.args)
galaxy_context = self._get_galaxy_context(self.options, self.config)
# default to looking for collection in cwd if not specified
cwd = os.getcwd()
collection_path = self.options.collection_path or cwd
# write the output archive to output_path, which will default to collection
# relative releases/
output_path = self.options.output_path or os.path.join(collection_path, 'releases')
build_context = BuildContext(collection_path=collection_path,
output_path=output_path)
return build.build(galaxy_context,
build_context,
display_callback=self.display)
# TODO: most of this logic should be out of cli class
def execute_init(self):
"""
creates the skeleton framework of a role that complies with the galaxy metadata format.
"""
init_path = self.options.init_path
force = self.options.force
role_skeleton_path = self.options.role_skeleton
role_name = self.args.pop(0).strip() if self.args else None
if not role_name:
raise cli_exceptions.CliOptionsError("- no role name specified for init")
role_path = os.path.join(init_path, role_name)
if os.path.exists(role_path):
if os.path.isfile(role_path):
raise cli_exceptions.GalaxyCliError("- the path %s already exists, but is a file - aborting" % role_path)
elif not force:
raise cli_exceptions.GalaxyCliError("- the directory %s already exists."
"you can use --force to re-initialize this directory,\n"
"however it will reset any main.yml files that may have\n"
"been modified there already." % role_path)
if role_skeleton_path is not None:
skeleton_ignore_expressions = self.config.options['role_skeleton_ignore']
else:
this_dir, this_filename = os.path.split(__file__)
type_path = getattr(self.options, 'role_type', "default")
role_skeleton_path = os.path.join(this_dir, '../', 'data/role_skeleton', type_path)
self.log.debug('role_skeleton_path: %s', role_skeleton_path)
skeleton_ignore_expressions = ['^.*/.git_keep$']
return init.init(role_name,
init_path,
role_path,
force,
role_skeleton_path,
skeleton_ignore_expressions,
self.options.role_type,
display_callback=self.display)
def execute_info(self):
"""
prints out detailed information about an installed role as well as info available from the galaxy API.
"""
if len(self.args) == 0:
# the user needs to specify a role
raise cli_exceptions.CliOptionsError("- you must specify a user/role name")
log.debug('args=%s', self.args)
galaxy_context = self._get_galaxy_context(self.options, self.config)
content_specs = self.args
# FIXME: rc?
return info.info_content_specs(galaxy_context, self.api, content_specs,
display_callback=self.display,
offline=self.options.offline)
def execute_install(self):
"""
uses the args list of roles to be installed, unless -f was specified. The list of roles
can be a name (which will be downloaded via the galaxy API and github), or it can be a local .tar.gz file.
"""
install_content_type = 'all'
self.log.debug('self.options: %s', self.options)
galaxy_context = self._get_galaxy_context(self.options, self.config)
requested_content_specs = self.args
try:
rc = install.install_content_specs(galaxy_context,
editable=self.options.editable_install,
content_spec_strings=requested_content_specs,
install_content_type=install_content_type,
namespace_override=self.options.namespace,
display_callback=self.display,
ignore_errors=self.options.ignore_errors,
no_deps=self.options.no_deps,
force_overwrite=self.options.force)
except Exception as e:
log.exception(e)
raise
return rc
def execute_remove(self):
"""
removes the list of roles passed as arguments from the local system.
"""
if len(self.args) == 0:
raise cli_exceptions.CliOptionsError('- you must specify at least one role to remove.')
galaxy_context = self._get_galaxy_context(self.options, self.config)
if self.args:
match_filter = matchers.MatchLabels(self.args)
return remove.remove(galaxy_context,
repository_match_filter=match_filter,
display_callback=self.display)
def execute_list(self):
"""
lists the roles installed on the local system or matches a single role passed as an argument.
"""
galaxy_context = self._get_galaxy_context(self.options, self.config)
match_filter = matchers.MatchAll()
if self.args:
match_filter = matchers.MatchNamespacesOrLabels(self.args)
return list_action.list(galaxy_context,
repository_match_filter=match_filter,
display_callback=self.display)
def execute_version(self):
return version.version(config_file_path=self.config_file_path,
cli_version=galaxy_cli_version,
display_callback=self.display)