From 6ffd9c3025473385e9780adbb7da834081b4d466 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 22 Aug 2015 02:28:03 -0400 Subject: [PATCH] draft galaxy cli search TODO: paging results --- lib/ansible/cli/galaxy.py | 182 ++++++++++++++++++++++---------------- lib/ansible/galaxy/api.py | 35 +++++++- 2 files changed, 137 insertions(+), 80 deletions(-) diff --git a/lib/ansible/cli/galaxy.py b/lib/ansible/cli/galaxy.py index ddc10794c09..5988e1fbc35 100644 --- a/lib/ansible/cli/galaxy.py +++ b/lib/ansible/cli/galaxy.py @@ -19,23 +19,14 @@ # ######################################################################## -import datetime -import json import os import os.path -import shutil -import subprocess import sys -import tarfile -import tempfile -import urllib -import urllib2 import yaml from collections import defaultdict from distutils.version import LooseVersion from jinja2 import Environment -from optparse import OptionParser import ansible.constants as C import ansible.utils @@ -46,12 +37,12 @@ from ansible.galaxy import Galaxy from ansible.galaxy.api import GalaxyAPI from ansible.galaxy.role import GalaxyRole from ansible.playbook.role.requirement import RoleRequirement -from ansible.utils.display import Display + class GalaxyCLI(CLI): - VALID_ACTIONS = ("init", "info", "install", "list", "remove") - SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" ) + VALID_ACTIONS = ("init", "info", "install", "list", "remove", "search") + SKIP_INFO_KEYS = ("name", "description", "readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" ) def __init__(self, args, display=None): @@ -72,47 +63,46 @@ class GalaxyCLI(CLI): # options specific to actions if self.action == "info": - self.parser.set_usage("usage: %prog info [options] role_name[,version]") + self.parser.set_usage("usage: %prog info [options] role_name[,version]") elif self.action == "init": - self.parser.set_usage("usage: %prog init [options] role_name") - self.parser.add_option( - '-p', '--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( + self.parser.set_usage("usage: %prog init [options] role_name") + self.parser.add_option('-p', '--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( '--offline', dest='offline', default=False, action='store_true', help="Don't query the galaxy API when creating roles") elif self.action == "install": - self.parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]") - self.parser.add_option( - '-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False, + self.parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]") + self.parser.add_option('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False, help='Ignore errors and continue with the next specified role.') - self.parser.add_option( - '-n', '--no-deps', dest='no_deps', action='store_true', default=False, + 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', + self.parser.add_option('-r', '--role-file', dest='role_file', help='A file containing a list of roles to be imported') elif self.action == "remove": - self.parser.set_usage("usage: %prog remove role1 role2 ...") + self.parser.set_usage("usage: %prog remove role1 role2 ...") elif self.action == "list": - self.parser.set_usage("usage: %prog list [role_name]") + self.parser.set_usage("usage: %prog list [role_name]") + elif self.action == "search": + self.parser.add_option('-P', '--platforms', dest='platforms', + help='list of OS platforms to filter by') + self.parser.add_option('-C', '--categories', dest='categories', + help='list of categories to filter by') + self.parser.set_usage("usage: %prog search [] [-C ] [-P platform]") # options that apply to more than one action if self.action != "init": - self.parser.add_option( - '-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH, + self.parser.add_option('-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH, help='The path to the directory containing your roles. ' 'The default is the roles_path configured in your ' 'ansible.cfg file (/etc/ansible/roles if not configured)') - if self.action in ("info","init","install"): - self.parser.add_option( '-s', '--server', dest='api_server', default="https://galaxy.ansible.com", + if self.action in ("info","init","install","search"): + self.parser.add_option('-s', '--server', dest='api_server', default="https://galaxy.ansible.com", help='The API server destination') if self.action in ("init","install"): - self.parser.add_option( - '-f', '--force', dest='force', action='store_true', default=False, + self.parser.add_option('-f', '--force', dest='force', action='store_true', default=False, help='Force overwriting an existing role') # get options, args and galaxy object @@ -127,7 +117,7 @@ class GalaxyCLI(CLI): super(GalaxyCLI, self).run() # if not offline, get connect to galaxy api - if self.action in ("info","install") or (self.action == 'init' and not self.options.offline): + if self.action in ("info","install", "search") or (self.action == 'init' and not self.options.offline): api_server = self.options.api_server self.api = GalaxyAPI(self.galaxy, api_server) if not self.api: @@ -156,6 +146,65 @@ class GalaxyCLI(CLI): if not self.get_opt("ignore_errors", False): raise AnsibleError('- you can use --ignore-errors to skip failed roles and finish processing the list.') + def parse_requirements_files(self, role): + if 'role' in role: + # Old style: {role: "galaxy.role,version,name", other_vars: "here" } + role_info = role_spec_parse(role['role']) + if isinstance(role_info, dict): + # Warning: Slight change in behaviour here. name may be being + # overloaded. Previously, name was only a parameter to the role. + # Now it is both a parameter to the role and the name that + # ansible-galaxy will install under on the local system. + if 'name' in role and 'name' in role_info: + del role_info['name'] + role.update(role_info) + else: + # New style: { src: 'galaxy.role,version,name', other_vars: "here" } + if 'github.com' in role["src"] and 'http' in role["src"] and '+' not in role["src"] and not role["src"].endswith('.tar.gz'): + role["src"] = "git+" + role["src"] + + if '+' in role["src"]: + (scm, src) = role["src"].split('+') + role["scm"] = scm + role["src"] = src + + if 'name' not in role: + role["name"] = GalaxyRole.url_to_spec(role["src"]) + + if 'version' not in role: + role['version'] = '' + + if 'scm' not in role: + role['scm'] = None + + return role + + + def _display_role_info(self, role_info): + + text = "\nRole: %s \n" % role_info['name'] + text += "\tdescription: %s \n" % role_info['description'] + + for k in sorted(role_info.keys()): + + if k in self.SKIP_INFO_KEYS: + continue + + if isinstance(role_info[k], dict): + text += "\t%s: \n" % (k) + for key in sorted(role_info[k].keys()): + if key in self.SKIP_INFO_KEYS: + continue + text += "\t\t%s: %s\n" % (key, role_info[k][key]) + else: + text += "\t%s: %s\n" % (k, role_info[k]) + + return text + +############################ +# execute actions +############################ + def execute_init(self): """ Executes the init action, which creates the skeleton framework @@ -249,6 +298,7 @@ class GalaxyCLI(CLI): roles_path = self.get_opt("roles_path") + data = '' for role in self.args: role_info = {} @@ -277,23 +327,12 @@ class GalaxyCLI(CLI): if role_spec: role_info.update(role_spec) - if role_info: - self.display.display("- %s:" % (role)) - for k in sorted(role_info.keys()): - - if k in self.SKIP_INFO_KEYS: - continue - - if isinstance(role_info[k], dict): - self.display.display("\t%s: " % (k)) - for key in sorted(role_info[k].keys()): - if key in self.SKIP_INFO_KEYS: - continue - self.display.display("\t\t%s: %s" % (key, role_info[k][key])) - else: - self.display.display("\t%s: %s" % (k, role_info[k])) + data += self._display_role_info(role_info) + if data: + data += "\n- %s:" % (role) else: - self.display.display("- the role %s was not found" % role) + data += "\n- the role %s was not found" % role + self.pager(data) def execute_install(self): """ @@ -497,35 +536,22 @@ class GalaxyCLI(CLI): self.display.display("- %s, %s" % (path_file, version)) return 0 - def parse_requirements_files(self, role): - if 'role' in role: - # Old style: {role: "galaxy.role,version,name", other_vars: "here" } - role_info = role_spec_parse(role['role']) - if isinstance(role_info, dict): - # Warning: Slight change in behaviour here. name may be being - # overloaded. Previously, name was only a parameter to the role. - # Now it is both a parameter to the role and the name that - # ansible-galaxy will install under on the local system. - if 'name' in role and 'name' in role_info: - del role_info['name'] - role.update(role_info) - else: - # New style: { src: 'galaxy.role,version,name', other_vars: "here" } - if 'github.com' in role["src"] and 'http' in role["src"] and '+' not in role["src"] and not role["src"].endswith('.tar.gz'): - role["src"] = "git+" + role["src"] + def execute_search(self): - if '+' in role["src"]: - (scm, src) = role["src"].split('+') - role["scm"] = scm - role["src"] = src + search = None + if len(self.args) > 1: + raise AnsibleOptionsError("At most a single search term is allowed.") + elif len(self.args) == 1: + search = self.args.pop() - if 'name' not in role: - role["name"] = GalaxyRole.url_to_spec(role["src"]) + response = self.api.search_roles(search, self.options.platforms, self.options.categories) - if 'version' not in role: - role['version'] = '' + if 'count' in response: + self.galaxy.display.display("Found %d roles matching your search:\n" % response['count']) - if 'scm' not in role: - role['scm'] = None + data = '' + if 'results' in response: + for role in response['results']: + data += self._display_role_info(role) - return role + self.pager(data) diff --git a/lib/ansible/galaxy/api.py b/lib/ansible/galaxy/api.py index f14afc52d3a..b6f6c3bca26 100644 --- a/lib/ansible/galaxy/api.py +++ b/lib/ansible/galaxy/api.py @@ -21,10 +21,10 @@ # ######################################################################## import json -from urllib2 import urlopen, quote as urlquote +from urllib2 import urlopen, quote as urlquote, HTTPError from urlparse import urlparse -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleOptionsError class GalaxyAPI(object): ''' This class is meant to be used as a API client for an Ansible Galaxy server ''' @@ -139,3 +139,34 @@ class GalaxyAPI(object): return results except Exception as error: raise AnsibleError("Failed to download the %s list: %s" % (what, str(error))) + + def search_roles(self, search, platforms=None, categories=None): + + search_url = self.baseurl + '/roles/?page=1' + + if search: + search_url += '&search=' + urlquote(search) + + if categories is None: + categories = [] + elif isinstance(categories, basestring): + categories = categories.split(',') + + for cat in categories: + search_url += '&chain__categories__name=' + urlquote(cat) + + if platforms is None: + platforms = [] + elif isinstance(platforms, basestring): + platforms = platforms.split(',') + + for plat in platforms: + search_url += '&chain__platforms__name=' + urlquote(plat) + + self.galaxy.display.debug("Executing query: %s" % search_url) + try: + data = json.load(urlopen(search_url)) + except HTTPError as e: + raise AnsibleError("Unsuccessful request to server: %s" % str(e)) + + return data