1# Copyright: (c) 2013, James Cammarata <jcammarata@ansible.com>
2# Copyright: (c) 2018, Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import (absolute_import, division, print_function)
6__metaclass__ = type
7
8import os.path
9import re
10import shutil
11import textwrap
12import time
13import yaml
14
15from jinja2 import BaseLoader, Environment, FileSystemLoader
16from yaml.error import YAMLError
17
18import ansible.constants as C
19from ansible import context
20from ansible.cli import CLI
21from ansible.cli.arguments import option_helpers as opt_help
22from ansible.errors import AnsibleError, AnsibleOptionsError
23from ansible.galaxy import Galaxy, get_collections_galaxy_meta_info
24from ansible.galaxy.api import GalaxyAPI
25from ansible.galaxy.collection import build_collection, install_collections, publish_collection, \
26    validate_collection_name
27from ansible.galaxy.role import GalaxyRole
28from ansible.galaxy.token import BasicAuthToken, GalaxyToken, KeycloakToken, NoTokenSentinel
29from ansible.module_utils.ansible_release import __version__ as ansible_version
30from ansible.module_utils._text import to_bytes, to_native, to_text
31from ansible.module_utils import six
32from ansible.parsing.yaml.loader import AnsibleLoader
33from ansible.playbook.role.requirement import RoleRequirement
34from ansible.utils.display import Display
35from ansible.utils.plugin_docs import get_versioned_doclink
36
37display = Display()
38urlparse = six.moves.urllib.parse.urlparse
39
40
41class GalaxyCLI(CLI):
42    '''command to manage Ansible roles in shared repositories, the default of which is Ansible Galaxy *https://galaxy.ansible.com*.'''
43
44    SKIP_INFO_KEYS = ("name", "description", "readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url")
45
46    def __init__(self, args):
47        if len(args) > 1:
48            # Inject role into sys.argv[1] as a backwards compatibility step
49            if args[1] not in ['-h', '--help', '--version'] and 'role' not in args and 'collection' not in args:
50                # TODO: Should we add a warning here and eventually deprecate the implicit role subcommand choice
51                # Remove this in Ansible 2.13 when we also remove -v as an option on the root parser for ansible-galaxy.
52                idx = 2 if args[1].startswith('-v') else 1
53                args.insert(idx, 'role')
54            # since argparse doesn't allow hidden subparsers, handle dead login arg from raw args after "role" normalization
55            if args[1:3] == ['role', 'login']:
56                display.error(
57                    "The login command was removed in late 2020. An API key is now required to publish roles or collections "
58                    "to Galaxy. The key can be found at https://galaxy.ansible.com/me/preferences, and passed to the "
59                    "ansible-galaxy CLI via a file at {0} or (insecurely) via the `--token` "
60                    "command-line argument.".format(to_text(C.GALAXY_TOKEN_PATH)))
61                exit(1)
62
63        self.api_servers = []
64        self.galaxy = None
65        self._api = None
66        super(GalaxyCLI, self).__init__(args)
67
68    def init_parser(self):
69        ''' create an options parser for bin/ansible '''
70
71        super(GalaxyCLI, self).init_parser(
72            desc="Perform various Role and Collection related operations.",
73        )
74
75        # Common arguments that apply to more than 1 action
76        common = opt_help.argparse.ArgumentParser(add_help=False)
77        common.add_argument('-s', '--server', dest='api_server', help='The Galaxy API server URL')
78        common.add_argument('--api-key', dest='api_key',
79                            help='The Ansible Galaxy API key which can be found at '
80                                 'https://galaxy.ansible.com/me/preferences. You can also set the token '
81                                 'for the GALAXY_SERVER_LIST entry.')
82        common.add_argument('-c', '--ignore-certs', action='store_true', dest='ignore_certs',
83                            default=C.GALAXY_IGNORE_CERTS, help='Ignore SSL certificate validation errors.')
84        opt_help.add_verbosity_options(common)
85
86        force = opt_help.argparse.ArgumentParser(add_help=False)
87        force.add_argument('-f', '--force', dest='force', action='store_true', default=False,
88                           help='Force overwriting an existing role or collection')
89
90        github = opt_help.argparse.ArgumentParser(add_help=False)
91        github.add_argument('github_user', help='GitHub username')
92        github.add_argument('github_repo', help='GitHub repository')
93
94        offline = opt_help.argparse.ArgumentParser(add_help=False)
95        offline.add_argument('--offline', dest='offline', default=False, action='store_true',
96                             help="Don't query the galaxy API when creating roles")
97
98        default_roles_path = C.config.get_configuration_definition('DEFAULT_ROLES_PATH').get('default', '')
99        roles_path = opt_help.argparse.ArgumentParser(add_help=False)
100        roles_path.add_argument('-p', '--roles-path', dest='roles_path', type=opt_help.unfrack_path(pathsep=True),
101                                default=C.DEFAULT_ROLES_PATH, action=opt_help.PrependListAction,
102                                help='The path to the directory containing your roles. The default is the first '
103                                     'writable one configured via DEFAULT_ROLES_PATH: %s ' % default_roles_path)
104
105        # Add sub parser for the Galaxy role type (role or collection)
106        type_parser = self.parser.add_subparsers(metavar='TYPE', dest='type')
107        type_parser.required = True
108
109        # Add sub parser for the Galaxy collection actions
110        collection = type_parser.add_parser('collection', help='Manage an Ansible Galaxy collection.')
111        collection_parser = collection.add_subparsers(metavar='COLLECTION_ACTION', dest='action')
112        collection_parser.required = True
113        self.add_init_options(collection_parser, parents=[common, force])
114        self.add_build_options(collection_parser, parents=[common, force])
115        self.add_publish_options(collection_parser, parents=[common])
116        self.add_install_options(collection_parser, parents=[common, force])
117
118        # Add sub parser for the Galaxy role actions
119        role = type_parser.add_parser('role', help='Manage an Ansible Galaxy role.')
120        role_parser = role.add_subparsers(metavar='ROLE_ACTION', dest='action')
121        role_parser.required = True
122        self.add_init_options(role_parser, parents=[common, force, offline])
123        self.add_remove_options(role_parser, parents=[common, roles_path])
124        self.add_delete_options(role_parser, parents=[common, github])
125        self.add_list_options(role_parser, parents=[common, roles_path])
126        self.add_search_options(role_parser, parents=[common])
127        self.add_import_options(role_parser, parents=[common, github])
128        self.add_setup_options(role_parser, parents=[common, roles_path])
129        self.add_info_options(role_parser, parents=[common, roles_path, offline])
130        self.add_install_options(role_parser, parents=[common, force, roles_path])
131
132    def add_init_options(self, parser, parents=None):
133        galaxy_type = 'collection' if parser.metavar == 'COLLECTION_ACTION' else 'role'
134
135        init_parser = parser.add_parser('init', parents=parents,
136                                        help='Initialize new {0} with the base structure of a '
137                                             '{0}.'.format(galaxy_type))
138        init_parser.set_defaults(func=self.execute_init)
139
140        init_parser.add_argument('--init-path', dest='init_path', default='./',
141                                 help='The path in which the skeleton {0} will be created. The default is the '
142                                      'current working directory.'.format(galaxy_type))
143        init_parser.add_argument('--{0}-skeleton'.format(galaxy_type), dest='{0}_skeleton'.format(galaxy_type),
144                                 default=C.GALAXY_ROLE_SKELETON,
145                                 help='The path to a {0} skeleton that the new {0} should be based '
146                                      'upon.'.format(galaxy_type))
147
148        obj_name_kwargs = {}
149        if galaxy_type == 'collection':
150            obj_name_kwargs['type'] = validate_collection_name
151        init_parser.add_argument('{0}_name'.format(galaxy_type), help='{0} name'.format(galaxy_type.capitalize()),
152                                 **obj_name_kwargs)
153
154        if galaxy_type == 'role':
155            init_parser.add_argument('--type', dest='role_type', action='store', default='default',
156                                     help="Initialize using an alternate role type. Valid types include: 'container', "
157                                          "'apb' and 'network'.")
158
159    def add_remove_options(self, parser, parents=None):
160        remove_parser = parser.add_parser('remove', parents=parents, help='Delete roles from roles_path.')
161        remove_parser.set_defaults(func=self.execute_remove)
162
163        remove_parser.add_argument('args', help='Role(s)', metavar='role', nargs='+')
164
165    def add_delete_options(self, parser, parents=None):
166        delete_parser = parser.add_parser('delete', parents=parents,
167                                          help='Removes the role from Galaxy. It does not remove or alter the actual '
168                                               'GitHub repository.')
169        delete_parser.set_defaults(func=self.execute_delete)
170
171    def add_list_options(self, parser, parents=None):
172        list_parser = parser.add_parser('list', parents=parents,
173                                        help='Show the name and version of each role installed in the roles_path.')
174        list_parser.set_defaults(func=self.execute_list)
175
176        list_parser.add_argument('role', help='Role', nargs='?', metavar='role')
177
178    def add_search_options(self, parser, parents=None):
179        search_parser = parser.add_parser('search', parents=parents,
180                                          help='Search the Galaxy database by tags, platforms, author and multiple '
181                                               'keywords.')
182        search_parser.set_defaults(func=self.execute_search)
183
184        search_parser.add_argument('--platforms', dest='platforms', help='list of OS platforms to filter by')
185        search_parser.add_argument('--galaxy-tags', dest='galaxy_tags', help='list of galaxy tags to filter by')
186        search_parser.add_argument('--author', dest='author', help='GitHub username')
187        search_parser.add_argument('args', help='Search terms', metavar='searchterm', nargs='*')
188
189    def add_import_options(self, parser, parents=None):
190        import_parser = parser.add_parser('import', parents=parents, help='Import a role')
191        import_parser.set_defaults(func=self.execute_import)
192
193        import_parser.add_argument('--no-wait', dest='wait', action='store_false', default=True,
194                                   help="Don't wait for import results.")
195        import_parser.add_argument('--branch', dest='reference',
196                                   help='The name of a branch to import. Defaults to the repository\'s default branch '
197                                        '(usually master)')
198        import_parser.add_argument('--role-name', dest='role_name',
199                                   help='The name the role should have, if different than the repo name')
200        import_parser.add_argument('--status', dest='check_status', action='store_true', default=False,
201                                   help='Check the status of the most recent import request for given github_'
202                                        'user/github_repo.')
203
204    def add_setup_options(self, parser, parents=None):
205        setup_parser = parser.add_parser('setup', parents=parents,
206                                         help='Manage the integration between Galaxy and the given source.')
207        setup_parser.set_defaults(func=self.execute_setup)
208
209        setup_parser.add_argument('--remove', dest='remove_id', default=None,
210                                  help='Remove the integration matching the provided ID value. Use --list to see '
211                                       'ID values.')
212        setup_parser.add_argument('--list', dest="setup_list", action='store_true', default=False,
213                                  help='List all of your integrations.')
214        setup_parser.add_argument('source', help='Source')
215        setup_parser.add_argument('github_user', help='GitHub username')
216        setup_parser.add_argument('github_repo', help='GitHub repository')
217        setup_parser.add_argument('secret', help='Secret')
218
219    def add_info_options(self, parser, parents=None):
220        info_parser = parser.add_parser('info', parents=parents, help='View more details about a specific role.')
221        info_parser.set_defaults(func=self.execute_info)
222
223        info_parser.add_argument('args', nargs='+', help='role', metavar='role_name[,version]')
224
225    def add_install_options(self, parser, parents=None):
226        galaxy_type = 'collection' if parser.metavar == 'COLLECTION_ACTION' else 'role'
227
228        args_kwargs = {}
229        if galaxy_type == 'collection':
230            args_kwargs['help'] = 'The collection(s) name or path/url to a tar.gz collection artifact. This is ' \
231                                  'mutually exclusive with --requirements-file.'
232            ignore_errors_help = 'Ignore errors during installation and continue with the next specified ' \
233                                 'collection. This will not ignore dependency conflict errors.'
234        else:
235            args_kwargs['help'] = 'Role name, URL or tar file'
236            ignore_errors_help = 'Ignore errors and continue with the next specified role.'
237
238        install_parser = parser.add_parser('install', parents=parents,
239                                           help='Install {0}(s) from file(s), URL(s) or Ansible '
240                                                'Galaxy'.format(galaxy_type))
241        install_parser.set_defaults(func=self.execute_install)
242
243        install_parser.add_argument('args', metavar='{0}_name'.format(galaxy_type), nargs='*', **args_kwargs)
244        install_parser.add_argument('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
245                                    help=ignore_errors_help)
246
247        install_exclusive = install_parser.add_mutually_exclusive_group()
248        install_exclusive.add_argument('-n', '--no-deps', dest='no_deps', action='store_true', default=False,
249                                       help="Don't download {0}s listed as dependencies.".format(galaxy_type))
250        install_exclusive.add_argument('--force-with-deps', dest='force_with_deps', action='store_true', default=False,
251                                       help="Force overwriting an existing {0} and its "
252                                            "dependencies.".format(galaxy_type))
253
254        if galaxy_type == 'collection':
255            install_parser.add_argument('-p', '--collections-path', dest='collections_path',
256                                        default=C.COLLECTIONS_PATHS[0],
257                                        help='The path to the directory containing your collections.')
258            install_parser.add_argument('-r', '--requirements-file', dest='requirements',
259                                        help='A file containing a list of collections to be installed.')
260        else:
261            install_parser.add_argument('-r', '--role-file', dest='role_file',
262                                        help='A file containing a list of roles to be imported.')
263            install_parser.add_argument('-g', '--keep-scm-meta', dest='keep_scm_meta', action='store_true',
264                                        default=False,
265                                        help='Use tar instead of the scm archive option when packaging the role.')
266
267    def add_build_options(self, parser, parents=None):
268        build_parser = parser.add_parser('build', parents=parents,
269                                         help='Build an Ansible collection artifact that can be publish to Ansible '
270                                              'Galaxy.')
271        build_parser.set_defaults(func=self.execute_build)
272
273        build_parser.add_argument('args', metavar='collection', nargs='*', default=('.',),
274                                  help='Path to the collection(s) directory to build. This should be the directory '
275                                       'that contains the galaxy.yml file. The default is the current working '
276                                       'directory.')
277        build_parser.add_argument('--output-path', dest='output_path', default='./',
278                                  help='The path in which the collection is built to. The default is the current '
279                                       'working directory.')
280
281    def add_publish_options(self, parser, parents=None):
282        publish_parser = parser.add_parser('publish', parents=parents,
283                                           help='Publish a collection artifact to Ansible Galaxy.')
284        publish_parser.set_defaults(func=self.execute_publish)
285
286        publish_parser.add_argument('args', metavar='collection_path',
287                                    help='The path to the collection tarball to publish.')
288        publish_parser.add_argument('--no-wait', dest='wait', action='store_false', default=True,
289                                    help="Don't wait for import validation results.")
290        publish_parser.add_argument('--import-timeout', dest='import_timeout', type=int, default=0,
291                                    help="The time to wait for the collection import process to finish.")
292
293    def post_process_args(self, options):
294        options = super(GalaxyCLI, self).post_process_args(options)
295        display.verbosity = options.verbosity
296        return options
297
298    def run(self):
299
300        super(GalaxyCLI, self).run()
301
302        self.galaxy = Galaxy()
303
304        def server_config_def(section, key, required):
305            return {
306                'description': 'The %s of the %s Galaxy server' % (key, section),
307                'ini': [
308                    {
309                        'section': 'galaxy_server.%s' % section,
310                        'key': key,
311                    }
312                ],
313                'env': [
314                    {'name': 'ANSIBLE_GALAXY_SERVER_%s_%s' % (section.upper(), key.upper())},
315                ],
316                'required': required,
317            }
318        server_def = [('url', True), ('username', False), ('password', False), ('token', False),
319                      ('auth_url', False)]
320
321        config_servers = []
322
323        # Need to filter out empty strings or non truthy values as an empty server list env var is equal to [''].
324        server_list = [s for s in C.GALAXY_SERVER_LIST or [] if s]
325        for server_key in server_list:
326            # Config definitions are looked up dynamically based on the C.GALAXY_SERVER_LIST entry. We look up the
327            # section [galaxy_server.<server>] for the values url, username, password, and token.
328            config_dict = dict((k, server_config_def(server_key, k, req)) for k, req in server_def)
329            defs = AnsibleLoader(yaml.safe_dump(config_dict)).get_single_data()
330            C.config.initialize_plugin_configuration_definitions('galaxy_server', server_key, defs)
331
332            server_options = C.config.get_plugin_options('galaxy_server', server_key)
333            # auth_url is used to create the token, but not directly by GalaxyAPI, so
334            # it doesn't need to be passed as kwarg to GalaxyApi
335            auth_url = server_options.pop('auth_url', None)
336            token_val = server_options['token'] or NoTokenSentinel
337            username = server_options['username']
338
339            # default case if no auth info is provided.
340            server_options['token'] = None
341
342            if username:
343                server_options['token'] = BasicAuthToken(username,
344                                                         server_options['password'])
345            else:
346                if token_val:
347                    if auth_url:
348                        server_options['token'] = KeycloakToken(access_token=token_val,
349                                                                auth_url=auth_url,
350                                                                validate_certs=not context.CLIARGS['ignore_certs'])
351                    else:
352                        # The galaxy v1 / github / django / 'Token'
353                        server_options['token'] = GalaxyToken(token=token_val)
354
355            config_servers.append(GalaxyAPI(self.galaxy, server_key, **server_options))
356
357        cmd_server = context.CLIARGS['api_server']
358        cmd_token = GalaxyToken(token=context.CLIARGS['api_key'])
359        if cmd_server:
360            # Cmd args take precedence over the config entry but fist check if the arg was a name and use that config
361            # entry, otherwise create a new API entry for the server specified.
362            config_server = next((s for s in config_servers if s.name == cmd_server), None)
363            if config_server:
364                self.api_servers.append(config_server)
365            else:
366                self.api_servers.append(GalaxyAPI(self.galaxy, 'cmd_arg', cmd_server, token=cmd_token))
367        else:
368            self.api_servers = config_servers
369
370        # Default to C.GALAXY_SERVER if no servers were defined
371        if len(self.api_servers) == 0:
372            self.api_servers.append(GalaxyAPI(self.galaxy, 'default', C.GALAXY_SERVER, token=cmd_token))
373
374        context.CLIARGS['func']()
375
376    @property
377    def api(self):
378        if self._api:
379            return self._api
380
381        for server in self.api_servers:
382            try:
383                if u'v1' in server.available_api_versions:
384                    self._api = server
385                    break
386            except Exception:
387                continue
388
389        if not self._api:
390            self._api = self.api_servers[0]
391
392        return self._api
393
394    def _parse_requirements_file(self, requirements_file, allow_old_format=True):
395        """
396        Parses an Ansible requirement.yml file and returns all the roles and/or collections defined in it. There are 2
397        requirements file format:
398
399            # v1 (roles only)
400            - src: The source of the role, required if include is not set. Can be Galaxy role name, URL to a SCM repo or tarball.
401              name: Downloads the role to the specified name, defaults to Galaxy name from Galaxy or name of repo if src is a URL.
402              scm: If src is a URL, specify the SCM. Only git or hd are supported and defaults ot git.
403              version: The version of the role to download. Can also be tag, commit, or branch name and defaults to master.
404              include: Path to additional requirements.yml files.
405
406            # v2 (roles and collections)
407            ---
408            roles:
409            # Same as v1 format just under the roles key
410
411            collections:
412            - namespace.collection
413            - name: namespace.collection
414              version: version identifier, multiple identifiers are separated by ','
415              source: the URL or a predefined source name that relates to C.GALAXY_SERVER_LIST
416
417        :param requirements_file: The path to the requirements file.
418        :param allow_old_format: Will fail if a v1 requirements file is found and this is set to False.
419        :return: a dict containing roles and collections to found in the requirements file.
420        """
421        requirements = {
422            'roles': [],
423            'collections': [],
424        }
425
426        b_requirements_file = to_bytes(requirements_file, errors='surrogate_or_strict')
427        if not os.path.exists(b_requirements_file):
428            raise AnsibleError("The requirements file '%s' does not exist." % to_native(requirements_file))
429
430        display.vvv("Reading requirement file at '%s'" % requirements_file)
431        with open(b_requirements_file, 'rb') as req_obj:
432            try:
433                file_requirements = yaml.safe_load(req_obj)
434            except YAMLError as err:
435                raise AnsibleError(
436                    "Failed to parse the requirements yml at '%s' with the following error:\n%s"
437                    % (to_native(requirements_file), to_native(err)))
438
439        if file_requirements is None:
440            raise AnsibleError("No requirements found in file '%s'" % to_native(requirements_file))
441
442        def parse_role_req(requirement):
443            if "include" not in requirement:
444                role = RoleRequirement.role_yaml_parse(requirement)
445                display.vvv("found role %s in yaml file" % to_text(role))
446                if "name" not in role and "src" not in role:
447                    raise AnsibleError("Must specify name or src for role")
448                return [GalaxyRole(self.galaxy, self.api, **role)]
449            else:
450                b_include_path = to_bytes(requirement["include"], errors="surrogate_or_strict")
451                if not os.path.isfile(b_include_path):
452                    raise AnsibleError("Failed to find include requirements file '%s' in '%s'"
453                                       % (to_native(b_include_path), to_native(requirements_file)))
454
455                with open(b_include_path, 'rb') as f_include:
456                    try:
457                        return [GalaxyRole(self.galaxy, self.api, **r) for r in
458                                (RoleRequirement.role_yaml_parse(i) for i in yaml.safe_load(f_include))]
459                    except Exception as e:
460                        raise AnsibleError("Unable to load data from include requirements file: %s %s"
461                                           % (to_native(requirements_file), to_native(e)))
462
463        if isinstance(file_requirements, list):
464            # Older format that contains only roles
465            if not allow_old_format:
466                raise AnsibleError("Expecting requirements file to be a dict with the key 'collections' that contains "
467                                   "a list of collections to install")
468
469            for role_req in file_requirements:
470                requirements['roles'] += parse_role_req(role_req)
471
472        else:
473            # Newer format with a collections and/or roles key
474            extra_keys = set(file_requirements.keys()).difference(set(['roles', 'collections']))
475            if extra_keys:
476                raise AnsibleError("Expecting only 'roles' and/or 'collections' as base keys in the requirements "
477                                   "file. Found: %s" % (to_native(", ".join(extra_keys))))
478
479            for role_req in file_requirements.get('roles', []):
480                requirements['roles'] += parse_role_req(role_req)
481
482            for collection_req in file_requirements.get('collections', []):
483                if isinstance(collection_req, dict):
484                    req_name = collection_req.get('name', None)
485                    if req_name is None:
486                        raise AnsibleError("Collections requirement entry should contain the key name.")
487
488                    req_version = collection_req.get('version', '*')
489                    req_source = collection_req.get('source', None)
490                    if req_source:
491                        # Try and match up the requirement source with our list of Galaxy API servers defined in the
492                        # config, otherwise create a server with that URL without any auth.
493                        req_source = next(iter([a for a in self.api_servers if req_source in [a.name, a.api_server]]),
494                                          GalaxyAPI(self.galaxy, "explicit_requirement_%s" % req_name, req_source))
495
496                    requirements['collections'].append((req_name, req_version, req_source))
497                else:
498                    requirements['collections'].append((collection_req, '*', None))
499
500        return requirements
501
502    @staticmethod
503    def exit_without_ignore(rc=1):
504        """
505        Exits with the specified return code unless the
506        option --ignore-errors was specified
507        """
508        if not context.CLIARGS['ignore_errors']:
509            raise AnsibleError('- you can use --ignore-errors to skip failed roles and finish processing the list.')
510
511    @staticmethod
512    def _display_role_info(role_info):
513
514        text = [u"", u"Role: %s" % to_text(role_info['name'])]
515        text.append(u"\tdescription: %s" % role_info.get('description', ''))
516
517        for k in sorted(role_info.keys()):
518
519            if k in GalaxyCLI.SKIP_INFO_KEYS:
520                continue
521
522            if isinstance(role_info[k], dict):
523                text.append(u"\t%s:" % (k))
524                for key in sorted(role_info[k].keys()):
525                    if key in GalaxyCLI.SKIP_INFO_KEYS:
526                        continue
527                    text.append(u"\t\t%s: %s" % (key, role_info[k][key]))
528            else:
529                text.append(u"\t%s: %s" % (k, role_info[k]))
530
531        return u'\n'.join(text)
532
533    @staticmethod
534    def _resolve_path(path):
535        return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
536
537    @staticmethod
538    def _get_skeleton_galaxy_yml(template_path, inject_data):
539        with open(to_bytes(template_path, errors='surrogate_or_strict'), 'rb') as template_obj:
540            meta_template = to_text(template_obj.read(), errors='surrogate_or_strict')
541
542        galaxy_meta = get_collections_galaxy_meta_info()
543
544        required_config = []
545        optional_config = []
546        for meta_entry in galaxy_meta:
547            config_list = required_config if meta_entry.get('required', False) else optional_config
548
549            value = inject_data.get(meta_entry['key'], None)
550            if not value:
551                meta_type = meta_entry.get('type', 'str')
552
553                if meta_type == 'str':
554                    value = ''
555                elif meta_type == 'list':
556                    value = []
557                elif meta_type == 'dict':
558                    value = {}
559
560            meta_entry['value'] = value
561            config_list.append(meta_entry)
562
563        link_pattern = re.compile(r"L\(([^)]+),\s+([^)]+)\)")
564        const_pattern = re.compile(r"C\(([^)]+)\)")
565
566        def comment_ify(v):
567            if isinstance(v, list):
568                v = ". ".join([l.rstrip('.') for l in v])
569
570            v = link_pattern.sub(r"\1 <\2>", v)
571            v = const_pattern.sub(r"'\1'", v)
572
573            return textwrap.fill(v, width=117, initial_indent="# ", subsequent_indent="# ", break_on_hyphens=False)
574
575        def to_yaml(v):
576            return yaml.safe_dump(v, default_flow_style=False).rstrip()
577
578        env = Environment(loader=BaseLoader)
579        env.filters['comment_ify'] = comment_ify
580        env.filters['to_yaml'] = to_yaml
581
582        template = env.from_string(meta_template)
583        meta_value = template.render({'required_config': required_config, 'optional_config': optional_config})
584
585        return meta_value
586
587############################
588# execute actions
589############################
590
591    def execute_role(self):
592        """
593        Perform the action on an Ansible Galaxy role. Must be combined with a further action like delete/install/init
594        as listed below.
595        """
596        # To satisfy doc build
597        pass
598
599    def execute_collection(self):
600        """
601        Perform the action on an Ansible Galaxy collection. Must be combined with a further action like init/install as
602        listed below.
603        """
604        # To satisfy doc build
605        pass
606
607    def execute_build(self):
608        """
609        Build an Ansible Galaxy collection artifact that can be stored in a central repository like Ansible Galaxy.
610        By default, this command builds from the current working directory. You can optionally pass in the
611        collection input path (where the ``galaxy.yml`` file is).
612        """
613        force = context.CLIARGS['force']
614        output_path = GalaxyCLI._resolve_path(context.CLIARGS['output_path'])
615        b_output_path = to_bytes(output_path, errors='surrogate_or_strict')
616
617        if not os.path.exists(b_output_path):
618            os.makedirs(b_output_path)
619        elif os.path.isfile(b_output_path):
620            raise AnsibleError("- the output collection directory %s is a file - aborting" % to_native(output_path))
621
622        for collection_path in context.CLIARGS['args']:
623            collection_path = GalaxyCLI._resolve_path(collection_path)
624            build_collection(collection_path, output_path, force)
625
626    def execute_init(self):
627        """
628        Creates the skeleton framework of a role or collection that complies with the Galaxy metadata format.
629        Requires a role or collection name. The collection name must be in the format ``<namespace>.<collection>``.
630        """
631
632        galaxy_type = context.CLIARGS['type']
633        init_path = context.CLIARGS['init_path']
634        force = context.CLIARGS['force']
635        obj_skeleton = context.CLIARGS['{0}_skeleton'.format(galaxy_type)]
636
637        obj_name = context.CLIARGS['{0}_name'.format(galaxy_type)]
638
639        inject_data = dict(
640            description='your {0} description'.format(galaxy_type),
641            ansible_plugin_list_dir=get_versioned_doclink('plugins/plugins.html'),
642        )
643        if galaxy_type == 'role':
644            inject_data.update(dict(
645                author='your name',
646                company='your company (optional)',
647                license='license (GPL-2.0-or-later, MIT, etc)',
648                role_name=obj_name,
649                role_type=context.CLIARGS['role_type'],
650                issue_tracker_url='http://example.com/issue/tracker',
651                repository_url='http://example.com/repository',
652                documentation_url='http://docs.example.com',
653                homepage_url='http://example.com',
654                min_ansible_version=ansible_version[:3],  # x.y
655            ))
656
657            obj_path = os.path.join(init_path, obj_name)
658        elif galaxy_type == 'collection':
659            namespace, collection_name = obj_name.split('.', 1)
660
661            inject_data.update(dict(
662                namespace=namespace,
663                collection_name=collection_name,
664                version='1.0.0',
665                readme='README.md',
666                authors=['your name <example@domain.com>'],
667                license=['GPL-2.0-or-later'],
668                repository='http://example.com/repository',
669                documentation='http://docs.example.com',
670                homepage='http://example.com',
671                issues='http://example.com/issue/tracker',
672            ))
673
674            obj_path = os.path.join(init_path, namespace, collection_name)
675
676        b_obj_path = to_bytes(obj_path, errors='surrogate_or_strict')
677
678        if os.path.exists(b_obj_path):
679            if os.path.isfile(obj_path):
680                raise AnsibleError("- the path %s already exists, but is a file - aborting" % to_native(obj_path))
681            elif not force:
682                raise AnsibleError("- the directory %s already exists. "
683                                   "You can use --force to re-initialize this directory,\n"
684                                   "however it will reset any main.yml files that may have\n"
685                                   "been modified there already." % to_native(obj_path))
686
687        if obj_skeleton is not None:
688            own_skeleton = False
689            skeleton_ignore_expressions = C.GALAXY_ROLE_SKELETON_IGNORE
690        else:
691            own_skeleton = True
692            obj_skeleton = self.galaxy.default_role_skeleton_path
693            skeleton_ignore_expressions = ['^.*/.git_keep$']
694
695        obj_skeleton = os.path.expanduser(obj_skeleton)
696        skeleton_ignore_re = [re.compile(x) for x in skeleton_ignore_expressions]
697
698        if not os.path.exists(obj_skeleton):
699            raise AnsibleError("- the skeleton path '{0}' does not exist, cannot init {1}".format(
700                to_native(obj_skeleton), galaxy_type)
701            )
702
703        template_env = Environment(loader=FileSystemLoader(obj_skeleton))
704
705        # create role directory
706        if not os.path.exists(b_obj_path):
707            os.makedirs(b_obj_path)
708
709        for root, dirs, files in os.walk(obj_skeleton, topdown=True):
710            rel_root = os.path.relpath(root, obj_skeleton)
711            rel_dirs = rel_root.split(os.sep)
712            rel_root_dir = rel_dirs[0]
713            if galaxy_type == 'collection':
714                # A collection can contain templates in playbooks/*/templates and roles/*/templates
715                in_templates_dir = rel_root_dir in ['playbooks', 'roles'] and 'templates' in rel_dirs
716            else:
717                in_templates_dir = rel_root_dir == 'templates'
718
719            dirs[:] = [d for d in dirs if not any(r.match(d) for r in skeleton_ignore_re)]
720
721            for f in files:
722                filename, ext = os.path.splitext(f)
723
724                if any(r.match(os.path.join(rel_root, f)) for r in skeleton_ignore_re):
725                    continue
726                elif galaxy_type == 'collection' and own_skeleton and rel_root == '.' and f == 'galaxy.yml.j2':
727                    # Special use case for galaxy.yml.j2 in our own default collection skeleton. We build the options
728                    # dynamically which requires special options to be set.
729
730                    # The templated data's keys must match the key name but the inject data contains collection_name
731                    # instead of name. We just make a copy and change the key back to name for this file.
732                    template_data = inject_data.copy()
733                    template_data['name'] = template_data.pop('collection_name')
734
735                    meta_value = GalaxyCLI._get_skeleton_galaxy_yml(os.path.join(root, rel_root, f), template_data)
736                    b_dest_file = to_bytes(os.path.join(obj_path, rel_root, filename), errors='surrogate_or_strict')
737                    with open(b_dest_file, 'wb') as galaxy_obj:
738                        galaxy_obj.write(to_bytes(meta_value, errors='surrogate_or_strict'))
739                elif ext == ".j2" and not in_templates_dir:
740                    src_template = os.path.join(rel_root, f)
741                    dest_file = os.path.join(obj_path, rel_root, filename)
742                    template_env.get_template(src_template).stream(inject_data).dump(dest_file, encoding='utf-8')
743                else:
744                    f_rel_path = os.path.relpath(os.path.join(root, f), obj_skeleton)
745                    shutil.copyfile(os.path.join(root, f), os.path.join(obj_path, f_rel_path))
746
747            for d in dirs:
748                b_dir_path = to_bytes(os.path.join(obj_path, rel_root, d), errors='surrogate_or_strict')
749                if not os.path.exists(b_dir_path):
750                    os.makedirs(b_dir_path)
751
752        display.display("- %s %s was created successfully" % (galaxy_type.title(), obj_name))
753
754    def execute_info(self):
755        """
756        prints out detailed information about an installed role as well as info available from the galaxy API.
757        """
758
759        roles_path = context.CLIARGS['roles_path']
760
761        data = ''
762        for role in context.CLIARGS['args']:
763
764            role_info = {'path': roles_path}
765            gr = GalaxyRole(self.galaxy, self.api, role)
766
767            install_info = gr.install_info
768            if install_info:
769                if 'version' in install_info:
770                    install_info['installed_version'] = install_info['version']
771                    del install_info['version']
772                role_info.update(install_info)
773
774            remote_data = False
775            if not context.CLIARGS['offline']:
776                remote_data = self.api.lookup_role_by_name(role, False)
777
778            if remote_data:
779                role_info.update(remote_data)
780
781            if gr.metadata:
782                role_info.update(gr.metadata)
783
784            req = RoleRequirement()
785            role_spec = req.role_yaml_parse({'role': role})
786            if role_spec:
787                role_info.update(role_spec)
788
789            data = self._display_role_info(role_info)
790            # FIXME: This is broken in both 1.9 and 2.0 as
791            # _display_role_info() always returns something
792            if not data:
793                data = u"\n- the role %s was not found" % role
794
795        self.pager(data)
796
797    def execute_install(self):
798        """
799        Install one or more roles(``ansible-galaxy role install``), or one or more collections(``ansible-galaxy collection install``).
800        You can pass in a list (roles or collections) or use the file
801        option listed below (these are mutually exclusive). If you pass in a list, it
802        can be a name (which will be downloaded via the galaxy API and github), or it can be a local tar archive file.
803        """
804        if context.CLIARGS['type'] == 'collection':
805            collections = context.CLIARGS['args']
806            force = context.CLIARGS['force']
807            output_path = context.CLIARGS['collections_path']
808            ignore_certs = context.CLIARGS['ignore_certs']
809            ignore_errors = context.CLIARGS['ignore_errors']
810            requirements_file = context.CLIARGS['requirements']
811            no_deps = context.CLIARGS['no_deps']
812            force_deps = context.CLIARGS['force_with_deps']
813
814            if collections and requirements_file:
815                raise AnsibleError("The positional collection_name arg and --requirements-file are mutually exclusive.")
816            elif not collections and not requirements_file:
817                raise AnsibleError("You must specify a collection name or a requirements file.")
818
819            if requirements_file:
820                requirements_file = GalaxyCLI._resolve_path(requirements_file)
821                requirements = self._parse_requirements_file(requirements_file, allow_old_format=False)['collections']
822            else:
823                requirements = []
824                for collection_input in collections:
825                    requirement = None
826                    if os.path.isfile(to_bytes(collection_input, errors='surrogate_or_strict')) or \
827                            urlparse(collection_input).scheme.lower() in ['http', 'https']:
828                        # Arg is a file path or URL to a collection
829                        name = collection_input
830                    else:
831                        name, dummy, requirement = collection_input.partition(':')
832                    requirements.append((name, requirement or '*', None))
833
834            output_path = GalaxyCLI._resolve_path(output_path)
835            collections_path = C.COLLECTIONS_PATHS
836
837            if len([p for p in collections_path if p.startswith(output_path)]) == 0:
838                display.warning("The specified collections path '%s' is not part of the configured Ansible "
839                                "collections paths '%s'. The installed collection won't be picked up in an Ansible "
840                                "run." % (to_text(output_path), to_text(":".join(collections_path))))
841
842            if os.path.split(output_path)[1] != 'ansible_collections':
843                output_path = os.path.join(output_path, 'ansible_collections')
844
845            b_output_path = to_bytes(output_path, errors='surrogate_or_strict')
846            if not os.path.exists(b_output_path):
847                os.makedirs(b_output_path)
848
849            install_collections(requirements, output_path, self.api_servers, (not ignore_certs), ignore_errors,
850                                no_deps, force, force_deps)
851
852            return 0
853
854        role_file = context.CLIARGS['role_file']
855
856        if not context.CLIARGS['args'] and role_file is None:
857            # the user needs to specify one of either --role-file or specify a single user/role name
858            raise AnsibleOptionsError("- you must specify a user/role name or a roles file")
859
860        no_deps = context.CLIARGS['no_deps']
861        force_deps = context.CLIARGS['force_with_deps']
862
863        force = context.CLIARGS['force'] or force_deps
864
865        roles_left = []
866        if role_file:
867            if not (role_file.endswith('.yaml') or role_file.endswith('.yml')):
868                raise AnsibleError("Invalid role requirements file, it must end with a .yml or .yaml extension")
869
870            roles_left = self._parse_requirements_file(role_file)['roles']
871        else:
872            # roles were specified directly, so we'll just go out grab them
873            # (and their dependencies, unless the user doesn't want us to).
874            for rname in context.CLIARGS['args']:
875                role = RoleRequirement.role_yaml_parse(rname.strip())
876                roles_left.append(GalaxyRole(self.galaxy, self.api, **role))
877
878        for role in roles_left:
879            # only process roles in roles files when names matches if given
880            if role_file and context.CLIARGS['args'] and role.name not in context.CLIARGS['args']:
881                display.vvv('Skipping role %s' % role.name)
882                continue
883
884            display.vvv('Processing role %s ' % role.name)
885
886            # query the galaxy API for the role data
887
888            if role.install_info is not None:
889                if role.install_info['version'] != role.version or force:
890                    if force:
891                        display.display('- changing role %s from %s to %s' %
892                                        (role.name, role.install_info['version'], role.version or "unspecified"))
893                        role.remove()
894                    else:
895                        display.warning('- %s (%s) is already installed - use --force to change version to %s' %
896                                        (role.name, role.install_info['version'], role.version or "unspecified"))
897                        continue
898                else:
899                    if not force:
900                        display.display('- %s is already installed, skipping.' % str(role))
901                        continue
902
903            try:
904                installed = role.install()
905            except AnsibleError as e:
906                display.warning(u"- %s was NOT installed successfully: %s " % (role.name, to_text(e)))
907                self.exit_without_ignore()
908                continue
909
910            # install dependencies, if we want them
911            if not no_deps and installed:
912                if not role.metadata:
913                    display.warning("Meta file %s is empty. Skipping dependencies." % role.path)
914                else:
915                    role_dependencies = role.metadata.get('dependencies') or []
916                    for dep in role_dependencies:
917                        display.debug('Installing dep %s' % dep)
918                        dep_req = RoleRequirement()
919                        dep_info = dep_req.role_yaml_parse(dep)
920                        dep_role = GalaxyRole(self.galaxy, self.api, **dep_info)
921                        if '.' not in dep_role.name and '.' not in dep_role.src and dep_role.scm is None:
922                            # we know we can skip this, as it's not going to
923                            # be found on galaxy.ansible.com
924                            continue
925                        if dep_role.install_info is None:
926                            if dep_role not in roles_left:
927                                display.display('- adding dependency: %s' % to_text(dep_role))
928                                roles_left.append(dep_role)
929                            else:
930                                display.display('- dependency %s already pending installation.' % dep_role.name)
931                        else:
932                            if dep_role.install_info['version'] != dep_role.version:
933                                if force_deps:
934                                    display.display('- changing dependant role %s from %s to %s' %
935                                                    (dep_role.name, dep_role.install_info['version'], dep_role.version or "unspecified"))
936                                    dep_role.remove()
937                                    roles_left.append(dep_role)
938                                else:
939                                    display.warning('- dependency %s (%s) from role %s differs from already installed version (%s), skipping' %
940                                                    (to_text(dep_role), dep_role.version, role.name, dep_role.install_info['version']))
941                            else:
942                                if force_deps:
943                                    roles_left.append(dep_role)
944                                else:
945                                    display.display('- dependency %s is already installed, skipping.' % dep_role.name)
946
947            if not installed:
948                display.warning("- %s was NOT installed successfully." % role.name)
949                self.exit_without_ignore()
950
951        return 0
952
953    def execute_remove(self):
954        """
955        removes the list of roles passed as arguments from the local system.
956        """
957
958        if not context.CLIARGS['args']:
959            raise AnsibleOptionsError('- you must specify at least one role to remove.')
960
961        for role_name in context.CLIARGS['args']:
962            role = GalaxyRole(self.galaxy, self.api, role_name)
963            try:
964                if role.remove():
965                    display.display('- successfully removed %s' % role_name)
966                else:
967                    display.display('- %s is not installed, skipping.' % role_name)
968            except Exception as e:
969                raise AnsibleError("Failed to remove role %s: %s" % (role_name, to_native(e)))
970
971        return 0
972
973    def execute_list(self):
974        """
975        lists the roles installed on the local system or matches a single role passed as an argument.
976        """
977
978        def _display_role(gr):
979            install_info = gr.install_info
980            version = None
981            if install_info:
982                version = install_info.get("version", None)
983            if not version:
984                version = "(unknown version)"
985            display.display("- %s, %s" % (gr.name, version))
986
987        if context.CLIARGS['role']:
988            # show the requested role, if it exists
989            name = context.CLIARGS['role']
990            gr = GalaxyRole(self.galaxy, self.api, name)
991            if gr.metadata:
992                display.display('# %s' % os.path.dirname(gr.path))
993                _display_role(gr)
994            else:
995                display.display("- the role %s was not found" % name)
996        else:
997            # show all valid roles in the roles_path directory
998            roles_path = context.CLIARGS['roles_path']
999            path_found = False
1000            warnings = []
1001            for path in roles_path:
1002                role_path = os.path.expanduser(path)
1003                if not os.path.exists(role_path):
1004                    warnings.append("- the configured path %s does not exist." % role_path)
1005                    continue
1006                elif not os.path.isdir(role_path):
1007                    warnings.append("- the configured path %s, exists, but it is not a directory." % role_path)
1008                    continue
1009                display.display('# %s' % role_path)
1010                path_files = os.listdir(role_path)
1011                path_found = True
1012                for path_file in path_files:
1013                    gr = GalaxyRole(self.galaxy, self.api, path_file, path=path)
1014                    if gr.metadata:
1015                        _display_role(gr)
1016            for w in warnings:
1017                display.warning(w)
1018            if not path_found:
1019                raise AnsibleOptionsError("- None of the provided paths was usable. Please specify a valid path with --roles-path")
1020        return 0
1021
1022    def execute_publish(self):
1023        """
1024        Publish a collection into Ansible Galaxy. Requires the path to the collection tarball to publish.
1025        """
1026        collection_path = GalaxyCLI._resolve_path(context.CLIARGS['args'])
1027        wait = context.CLIARGS['wait']
1028        timeout = context.CLIARGS['import_timeout']
1029
1030        publish_collection(collection_path, self.api, wait, timeout)
1031
1032    def execute_search(self):
1033        ''' searches for roles on the Ansible Galaxy server'''
1034        page_size = 1000
1035        search = None
1036
1037        if context.CLIARGS['args']:
1038            search = '+'.join(context.CLIARGS['args'])
1039
1040        if not search and not context.CLIARGS['platforms'] and not context.CLIARGS['galaxy_tags'] and not context.CLIARGS['author']:
1041            raise AnsibleError("Invalid query. At least one search term, platform, galaxy tag or author must be provided.")
1042
1043        response = self.api.search_roles(search, platforms=context.CLIARGS['platforms'],
1044                                         tags=context.CLIARGS['galaxy_tags'], author=context.CLIARGS['author'], page_size=page_size)
1045
1046        if response['count'] == 0:
1047            display.display("No roles match your search.", color=C.COLOR_ERROR)
1048            return True
1049
1050        data = [u'']
1051
1052        if response['count'] > page_size:
1053            data.append(u"Found %d roles matching your search. Showing first %s." % (response['count'], page_size))
1054        else:
1055            data.append(u"Found %d roles matching your search:" % response['count'])
1056
1057        max_len = []
1058        for role in response['results']:
1059            max_len.append(len(role['username'] + '.' + role['name']))
1060        name_len = max(max_len)
1061        format_str = u" %%-%ds %%s" % name_len
1062        data.append(u'')
1063        data.append(format_str % (u"Name", u"Description"))
1064        data.append(format_str % (u"----", u"-----------"))
1065        for role in response['results']:
1066            data.append(format_str % (u'%s.%s' % (role['username'], role['name']), role['description']))
1067
1068        data = u'\n'.join(data)
1069        self.pager(data)
1070
1071        return True
1072
1073    def execute_import(self):
1074        """ used to import a role into Ansible Galaxy """
1075
1076        colors = {
1077            'INFO': 'normal',
1078            'WARNING': C.COLOR_WARN,
1079            'ERROR': C.COLOR_ERROR,
1080            'SUCCESS': C.COLOR_OK,
1081            'FAILED': C.COLOR_ERROR,
1082        }
1083
1084        github_user = to_text(context.CLIARGS['github_user'], errors='surrogate_or_strict')
1085        github_repo = to_text(context.CLIARGS['github_repo'], errors='surrogate_or_strict')
1086
1087        if context.CLIARGS['check_status']:
1088            task = self.api.get_import_task(github_user=github_user, github_repo=github_repo)
1089        else:
1090            # Submit an import request
1091            task = self.api.create_import_task(github_user, github_repo,
1092                                               reference=context.CLIARGS['reference'],
1093                                               role_name=context.CLIARGS['role_name'])
1094
1095            if len(task) > 1:
1096                # found multiple roles associated with github_user/github_repo
1097                display.display("WARNING: More than one Galaxy role associated with Github repo %s/%s." % (github_user, github_repo),
1098                                color='yellow')
1099                display.display("The following Galaxy roles are being updated:" + u'\n', color=C.COLOR_CHANGED)
1100                for t in task:
1101                    display.display('%s.%s' % (t['summary_fields']['role']['namespace'], t['summary_fields']['role']['name']), color=C.COLOR_CHANGED)
1102                display.display(u'\nTo properly namespace this role, remove each of the above and re-import %s/%s from scratch' % (github_user, github_repo),
1103                                color=C.COLOR_CHANGED)
1104                return 0
1105            # found a single role as expected
1106            display.display("Successfully submitted import request %d" % task[0]['id'])
1107            if not context.CLIARGS['wait']:
1108                display.display("Role name: %s" % task[0]['summary_fields']['role']['name'])
1109                display.display("Repo: %s/%s" % (task[0]['github_user'], task[0]['github_repo']))
1110
1111        if context.CLIARGS['check_status'] or context.CLIARGS['wait']:
1112            # Get the status of the import
1113            msg_list = []
1114            finished = False
1115            while not finished:
1116                task = self.api.get_import_task(task_id=task[0]['id'])
1117                for msg in task[0]['summary_fields']['task_messages']:
1118                    if msg['id'] not in msg_list:
1119                        display.display(msg['message_text'], color=colors[msg['message_type']])
1120                        msg_list.append(msg['id'])
1121                if task[0]['state'] in ['SUCCESS', 'FAILED']:
1122                    finished = True
1123                else:
1124                    time.sleep(10)
1125
1126        return 0
1127
1128    def execute_setup(self):
1129        """ Setup an integration from Github or Travis for Ansible Galaxy roles"""
1130
1131        if context.CLIARGS['setup_list']:
1132            # List existing integration secrets
1133            secrets = self.api.list_secrets()
1134            if len(secrets) == 0:
1135                # None found
1136                display.display("No integrations found.")
1137                return 0
1138            display.display(u'\n' + "ID         Source     Repo", color=C.COLOR_OK)
1139            display.display("---------- ---------- ----------", color=C.COLOR_OK)
1140            for secret in secrets:
1141                display.display("%-10s %-10s %s/%s" % (secret['id'], secret['source'], secret['github_user'],
1142                                                       secret['github_repo']), color=C.COLOR_OK)
1143            return 0
1144
1145        if context.CLIARGS['remove_id']:
1146            # Remove a secret
1147            self.api.remove_secret(context.CLIARGS['remove_id'])
1148            display.display("Secret removed. Integrations using this secret will not longer work.", color=C.COLOR_OK)
1149            return 0
1150
1151        source = context.CLIARGS['source']
1152        github_user = context.CLIARGS['github_user']
1153        github_repo = context.CLIARGS['github_repo']
1154        secret = context.CLIARGS['secret']
1155
1156        resp = self.api.add_secret(source, github_user, github_repo, secret)
1157        display.display("Added integration for %s %s/%s" % (resp['source'], resp['github_user'], resp['github_repo']))
1158
1159        return 0
1160
1161    def execute_delete(self):
1162        """ Delete a role from Ansible Galaxy. """
1163
1164        github_user = context.CLIARGS['github_user']
1165        github_repo = context.CLIARGS['github_repo']
1166        resp = self.api.delete_role(github_user, github_repo)
1167
1168        if len(resp['deleted_roles']) > 1:
1169            display.display("Deleted the following roles:")
1170            display.display("ID     User            Name")
1171            display.display("------ --------------- ----------")
1172            for role in resp['deleted_roles']:
1173                display.display("%-8s %-15s %s" % (role.id, role.namespace, role.name))
1174
1175        display.display(resp['status'])
1176
1177        return True
1178