draft galaxy cli search

TODO: paging results
This commit is contained in:
Brian Coca 2015-08-22 02:28:03 -04:00
parent e282309f6d
commit 6ffd9c3025
2 changed files with 137 additions and 80 deletions

View file

@ -19,23 +19,14 @@
# #
######################################################################## ########################################################################
import datetime
import json
import os import os
import os.path import os.path
import shutil
import subprocess
import sys import sys
import tarfile
import tempfile
import urllib
import urllib2
import yaml import yaml
from collections import defaultdict from collections import defaultdict
from distutils.version import LooseVersion from distutils.version import LooseVersion
from jinja2 import Environment from jinja2 import Environment
from optparse import OptionParser
import ansible.constants as C import ansible.constants as C
import ansible.utils import ansible.utils
@ -46,12 +37,12 @@ from ansible.galaxy import Galaxy
from ansible.galaxy.api import GalaxyAPI from ansible.galaxy.api import GalaxyAPI
from ansible.galaxy.role import GalaxyRole from ansible.galaxy.role import GalaxyRole
from ansible.playbook.role.requirement import RoleRequirement from ansible.playbook.role.requirement import RoleRequirement
from ansible.utils.display import Display
class GalaxyCLI(CLI): class GalaxyCLI(CLI):
VALID_ACTIONS = ("init", "info", "install", "list", "remove") VALID_ACTIONS = ("init", "info", "install", "list", "remove", "search")
SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" ) SKIP_INFO_KEYS = ("name", "description", "readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
def __init__(self, args, display=None): def __init__(self, args, display=None):
@ -75,44 +66,43 @@ class GalaxyCLI(CLI):
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": elif self.action == "init":
self.parser.set_usage("usage: %prog init [options] role_name") self.parser.set_usage("usage: %prog init [options] role_name")
self.parser.add_option( self.parser.add_option('-p', '--init-path', dest='init_path', default="./",
'-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.')
help='The path in which the skeleton role will be created. '
'The default is the current working directory.')
self.parser.add_option( self.parser.add_option(
'--offline', dest='offline', default=False, action='store_true', '--offline', dest='offline', default=False, action='store_true',
help="Don't query the galaxy API when creating roles") help="Don't query the galaxy API when creating roles")
elif self.action == "install": 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.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
self.parser.add_option( self.parser.add_option('-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
'-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
help='Ignore errors and continue with the next specified role.') help='Ignore errors and continue with the next specified role.')
self.parser.add_option( self.parser.add_option('-n', '--no-deps', dest='no_deps', action='store_true', default=False,
'-n', '--no-deps', dest='no_deps', action='store_true', default=False,
help='Don\'t download roles listed as dependencies') help='Don\'t download roles listed as dependencies')
self.parser.add_option( self.parser.add_option('-r', '--role-file', dest='role_file',
'-r', '--role-file', dest='role_file',
help='A file containing a list of roles to be imported') help='A file containing a list of roles to be imported')
elif self.action == "remove": 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": 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 [<search_term>] [-C <category1,category2>] [-P platform]")
# options that apply to more than one action # options that apply to more than one action
if self.action != "init": if self.action != "init":
self.parser.add_option( self.parser.add_option('-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH,
'-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH,
help='The path to the directory containing your roles. ' help='The path to the directory containing your roles. '
'The default is the roles_path configured in your ' 'The default is the roles_path configured in your '
'ansible.cfg file (/etc/ansible/roles if not configured)') 'ansible.cfg file (/etc/ansible/roles if not configured)')
if self.action in ("info","init","install"): if self.action in ("info","init","install","search"):
self.parser.add_option('-s', '--server', dest='api_server', default="https://galaxy.ansible.com", self.parser.add_option('-s', '--server', dest='api_server', default="https://galaxy.ansible.com",
help='The API server destination') help='The API server destination')
if self.action in ("init","install"): if self.action in ("init","install"):
self.parser.add_option( self.parser.add_option('-f', '--force', dest='force', action='store_true', default=False,
'-f', '--force', dest='force', action='store_true', default=False,
help='Force overwriting an existing role') help='Force overwriting an existing role')
# get options, args and galaxy object # get options, args and galaxy object
@ -127,7 +117,7 @@ class GalaxyCLI(CLI):
super(GalaxyCLI, self).run() super(GalaxyCLI, self).run()
# if not offline, get connect to galaxy api # 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 api_server = self.options.api_server
self.api = GalaxyAPI(self.galaxy, api_server) self.api = GalaxyAPI(self.galaxy, api_server)
if not self.api: if not self.api:
@ -156,6 +146,65 @@ class GalaxyCLI(CLI):
if not self.get_opt("ignore_errors", False): if not self.get_opt("ignore_errors", False):
raise AnsibleError('- you can use --ignore-errors to skip failed roles and finish processing the list.') 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): def execute_init(self):
""" """
Executes the init action, which creates the skeleton framework Executes the init action, which creates the skeleton framework
@ -249,6 +298,7 @@ class GalaxyCLI(CLI):
roles_path = self.get_opt("roles_path") roles_path = self.get_opt("roles_path")
data = ''
for role in self.args: for role in self.args:
role_info = {} role_info = {}
@ -277,23 +327,12 @@ class GalaxyCLI(CLI):
if role_spec: if role_spec:
role_info.update(role_spec) role_info.update(role_spec)
if role_info: data += self._display_role_info(role_info)
self.display.display("- %s:" % (role)) if data:
for k in sorted(role_info.keys()): data += "\n- %s:" % (role)
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: else:
self.display.display("\t%s: %s" % (k, role_info[k])) data += "\n- the role %s was not found" % role
else: self.pager(data)
self.display.display("- the role %s was not found" % role)
def execute_install(self): def execute_install(self):
""" """
@ -497,35 +536,22 @@ class GalaxyCLI(CLI):
self.display.display("- %s, %s" % (path_file, version)) self.display.display("- %s, %s" % (path_file, version))
return 0 return 0
def parse_requirements_files(self, role): def execute_search(self):
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"]: search = None
(scm, src) = role["src"].split('+') if len(self.args) > 1:
role["scm"] = scm raise AnsibleOptionsError("At most a single search term is allowed.")
role["src"] = src elif len(self.args) == 1:
search = self.args.pop()
if 'name' not in role: response = self.api.search_roles(search, self.options.platforms, self.options.categories)
role["name"] = GalaxyRole.url_to_spec(role["src"])
if 'version' not in role: if 'count' in response:
role['version'] = '' self.galaxy.display.display("Found %d roles matching your search:\n" % response['count'])
if 'scm' not in role: data = ''
role['scm'] = None if 'results' in response:
for role in response['results']:
data += self._display_role_info(role)
return role self.pager(data)

View file

@ -21,10 +21,10 @@
# #
######################################################################## ########################################################################
import json import json
from urllib2 import urlopen, quote as urlquote from urllib2 import urlopen, quote as urlquote, HTTPError
from urlparse import urlparse from urlparse import urlparse
from ansible.errors import AnsibleError from ansible.errors import AnsibleError, AnsibleOptionsError
class GalaxyAPI(object): class GalaxyAPI(object):
''' This class is meant to be used as a API client for an Ansible Galaxy server ''' ''' 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 return results
except Exception as error: except Exception as error:
raise AnsibleError("Failed to download the %s list: %s" % (what, str(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