560 lines
23 KiB
Python
Executable file
560 lines
23 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
########################################################################
|
|
#
|
|
# (C) 2013, James Cammarata <jcammarata@ansible.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible 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.
|
|
#
|
|
# Ansible 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/>.
|
|
#
|
|
########################################################################
|
|
|
|
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
|
|
import ansible.galaxy
|
|
from ansible.errors import AnsibleError
|
|
|
|
class Cli(object):
|
|
|
|
VALID_ACTIONS = ("init", "info", "install", "list", "remove")
|
|
SKIP_INFO_KEYS = ("platforms","readme_html", "related", "summary_fields", "average_aw_composite", "average_aw_score", "url" )
|
|
|
|
def __init__(self):
|
|
|
|
if display is None:
|
|
self.display = Display()
|
|
else:
|
|
self.display = display
|
|
self.action = None
|
|
|
|
def set_action(args):
|
|
"""
|
|
Get the action the user wants to execute from the
|
|
sys argv list.
|
|
"""
|
|
for i in range(0,len(args)):
|
|
arg = args[i]
|
|
if arg in VALID_ACTIONS:
|
|
del args[i]
|
|
self.action = arg
|
|
|
|
|
|
def parse(self):
|
|
''' create an options parser for bin/ansible '''
|
|
usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(VALID_ACTIONS)
|
|
epilog = "\nSee '%s <command> --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0])
|
|
OptionParser.format_epilog = lambda self, formatter: self.epilog
|
|
parser = OptionParser(usage=usage, epilog=epilog)
|
|
|
|
if not self.action:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
# options specific to actions
|
|
if self.action == "info":
|
|
parser.set_usage("usage: %prog info [options] role_name[,version]")
|
|
elif self.action == "init":
|
|
parser.set_usage("usage: %prog init [options] role_name")
|
|
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.')
|
|
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":
|
|
parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]")
|
|
parser.add_option(
|
|
'-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False,
|
|
help='Ignore errors and continue with the next specified role.')
|
|
parser.add_option(
|
|
'-n', '--no-deps', dest='no_deps', action='store_true', default=False,
|
|
help='Don\'t download roles listed as dependencies')
|
|
parser.add_option(
|
|
'-r', '--role-file', dest='role_file',
|
|
help='A file containing a list of roles to be imported')
|
|
elif self.action == "remove":
|
|
parser.set_usage("usage: %prog remove role1 role2 ...")
|
|
elif self.action == "list":
|
|
parser.set_usage("usage: %prog list [role_name]")
|
|
|
|
# options that apply to more than one action
|
|
if self.action != "init":
|
|
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"):
|
|
parser.add_option(
|
|
'-s', '--server', dest='api_server', default="galaxy.ansible.com",
|
|
help='The API server destination')
|
|
|
|
if self.action in ("init","install"):
|
|
parser.add_option(
|
|
'-f', '--force', dest='force', action='store_true', default=False,
|
|
help='Force overwriting an existing role')
|
|
|
|
# done, return the parser
|
|
options, args = parser.parse_args()
|
|
|
|
if len(args) == 0 or len(args) > 1:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
display.verbosity = options.verbosity
|
|
|
|
return (options, args)
|
|
|
|
def run(options, args):
|
|
|
|
# execute the desired action
|
|
fn = getattr(self, "execute_%s" % self.action)
|
|
fn(args, options)
|
|
|
|
def get_opt(options, k, defval=""):
|
|
"""
|
|
Returns an option from an Optparse values instance.
|
|
"""
|
|
try:
|
|
data = getattr(options, k)
|
|
except:
|
|
return defval
|
|
if k == "roles_path":
|
|
if os.pathsep in data:
|
|
data = data.split(os.pathsep)[0]
|
|
return data
|
|
|
|
def exit_without_ignore(options, rc=1):
|
|
"""
|
|
Exits with the specified return code unless the
|
|
option --ignore-errors was specified
|
|
"""
|
|
|
|
if not get_opt(options, "ignore_errors", False):
|
|
print '- you can use --ignore-errors to skip failed roles.'
|
|
sys.exit(rc)
|
|
|
|
|
|
|
|
def execute_init(args, options, parser):
|
|
"""
|
|
Executes the init action, which creates the skeleton framework
|
|
of a role that complies with the galaxy metadata format.
|
|
"""
|
|
|
|
init_path = get_opt(options, 'init_path', './')
|
|
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
|
force = get_opt(options, 'force', False)
|
|
offline = get_opt(options, 'offline', False)
|
|
|
|
if not offline:
|
|
api_config = api_get_config(api_server)
|
|
if not api_config:
|
|
print "- the API server (%s) is not responding, please try again later." % api_server
|
|
sys.exit(1)
|
|
|
|
try:
|
|
role_name = args.pop(0).strip()
|
|
if role_name == "":
|
|
raise Exception("")
|
|
role_path = os.path.join(init_path, role_name)
|
|
if os.path.exists(role_path):
|
|
if os.path.isfile(role_path):
|
|
print "- the path %s already exists, but is a file - aborting" % role_path
|
|
sys.exit(1)
|
|
elif not force:
|
|
print "- the directory %s already exists." % role_path
|
|
print " 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."
|
|
sys.exit(1)
|
|
except Exception, e:
|
|
parser.print_help()
|
|
print "- no role name specified for init"
|
|
sys.exit(1)
|
|
|
|
ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars')
|
|
|
|
# create the default README.md
|
|
if not os.path.exists(role_path):
|
|
os.makedirs(role_path)
|
|
readme_path = os.path.join(role_path, "README.md")
|
|
f = open(readme_path, "wb")
|
|
f.write(default_readme_template)
|
|
f.close
|
|
|
|
for dir in ROLE_DIRS:
|
|
dir_path = os.path.join(init_path, role_name, dir)
|
|
main_yml_path = os.path.join(dir_path, 'main.yml')
|
|
# create the directory if it doesn't exist already
|
|
if not os.path.exists(dir_path):
|
|
os.makedirs(dir_path)
|
|
|
|
# now create the main.yml file for that directory
|
|
if dir == "meta":
|
|
# create a skeleton meta/main.yml with a valid galaxy_info
|
|
# datastructure in place, plus with all of the available
|
|
# tags/platforms included (but commented out) and the
|
|
# dependencies section
|
|
platforms = []
|
|
if not offline:
|
|
platforms = api_get_list(api_server, "platforms") or []
|
|
categories = []
|
|
if not offline:
|
|
categories = api_get_list(api_server, "categories") or []
|
|
|
|
# group the list of platforms from the api based
|
|
# on their names, with the release field being
|
|
# appended to a list of versions
|
|
platform_groups = defaultdict(list)
|
|
for platform in platforms:
|
|
platform_groups[platform['name']].append(platform['release'])
|
|
platform_groups[platform['name']].sort()
|
|
|
|
inject = dict(
|
|
author = 'your name',
|
|
company = 'your company (optional)',
|
|
license = 'license (GPLv2, CC-BY, etc)',
|
|
issue_tracker_url = 'http://example.com/issue/tracker',
|
|
min_ansible_version = '1.2',
|
|
platforms = platform_groups,
|
|
categories = categories,
|
|
)
|
|
rendered_meta = Environment().from_string(default_meta_template).render(inject)
|
|
f = open(main_yml_path, 'w')
|
|
f.write(rendered_meta)
|
|
f.close()
|
|
pass
|
|
elif dir not in ('files','templates'):
|
|
# just write a (mostly) empty YAML file for main.yml
|
|
f = open(main_yml_path, 'w')
|
|
f.write('---\n# %s file for %s\n' % (dir,role_name))
|
|
f.close()
|
|
print "- %s was created successfully" % role_name
|
|
|
|
def execute_info(args, options, parser):
|
|
"""
|
|
Executes the info action. This action prints out detailed
|
|
information about an installed role as well as info available
|
|
from the galaxy API.
|
|
"""
|
|
|
|
if len(args) == 0:
|
|
# the user needs to specify a role
|
|
parser.print_help()
|
|
print "- you must specify a user/role name"
|
|
sys.exit(1)
|
|
|
|
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
|
api_config = api_get_config(api_server)
|
|
roles_path = get_opt(options, "roles_path")
|
|
|
|
for role in args:
|
|
|
|
role_info = {}
|
|
|
|
install_info = get_galaxy_install_info(role, options)
|
|
if install_info:
|
|
if 'version' in install_info:
|
|
install_info['intalled_version'] = install_info['version']
|
|
del install_info['version']
|
|
role_info.update(install_info)
|
|
|
|
remote_data = api_lookup_role_by_name(api_server, role, False)
|
|
if remote_data:
|
|
role_info.update(remote_data)
|
|
|
|
metadata = get_role_metadata(role, options)
|
|
if metadata:
|
|
role_info.update(metadata)
|
|
|
|
role_spec = ansible.utils.role_spec_parse(role)
|
|
if role_spec:
|
|
role_info.update(role_spec)
|
|
|
|
if role_info:
|
|
print "- %s:" % (role)
|
|
for k in sorted(role_info.keys()):
|
|
|
|
if k in SKIP_INFO_KEYS:
|
|
continue
|
|
|
|
if isinstance(role_info[k], dict):
|
|
print "\t%s: " % (k)
|
|
for key in sorted(role_info[k].keys()):
|
|
if key in SKIP_INFO_KEYS:
|
|
continue
|
|
print "\t\t%s: %s" % (key, role_info[k][key])
|
|
else:
|
|
print "\t%s: %s" % (k, role_info[k])
|
|
else:
|
|
print "- the role %s was not found" % role
|
|
|
|
def execute_install(args, options, parser):
|
|
"""
|
|
Executes the installation action. The args list contains the
|
|
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.
|
|
"""
|
|
|
|
role_file = get_opt(options, "role_file", None)
|
|
|
|
if len(args) == 0 and role_file is None:
|
|
# the user needs to specify one of either --role-file
|
|
# or specify a single user/role name
|
|
parser.print_help()
|
|
print "- you must specify a user/role name or a roles file"
|
|
sys.exit()
|
|
elif len(args) == 1 and not role_file is None:
|
|
# using a role file is mutually exclusive of specifying
|
|
# the role name on the command line
|
|
parser.print_help()
|
|
print "- please specify a user/role name, or a roles file, but not both"
|
|
sys.exit(1)
|
|
|
|
api_server = get_opt(options, "api_server", "galaxy.ansible.com")
|
|
no_deps = get_opt(options, "no_deps", False)
|
|
roles_path = get_opt(options, "roles_path")
|
|
|
|
roles_done = []
|
|
if role_file:
|
|
f = open(role_file, 'r')
|
|
if role_file.endswith('.yaml') or role_file.endswith('.yml'):
|
|
roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f))
|
|
else:
|
|
# roles listed in a file, one per line
|
|
roles_left = map(ansible.utils.role_spec_parse, f.readlines())
|
|
f.close()
|
|
else:
|
|
# roles were specified directly, so we'll just go out grab them
|
|
# (and their dependencies, unless the user doesn't want us to).
|
|
roles_left = map(ansible.utils.role_spec_parse, args)
|
|
|
|
while len(roles_left) > 0:
|
|
# query the galaxy API for the role data
|
|
role_data = None
|
|
role = roles_left.pop(0)
|
|
role_src = role.get("src")
|
|
role_scm = role.get("scm")
|
|
role_path = role.get("path")
|
|
|
|
if role_path:
|
|
options.roles_path = role_path
|
|
else:
|
|
options.roles_path = roles_path
|
|
|
|
if os.path.isfile(role_src):
|
|
# installing a local tar.gz
|
|
tmp_file = role_src
|
|
else:
|
|
if role_scm:
|
|
# create tar file from scm url
|
|
tmp_file = scm_archive_role(role_scm, role_src, role.get("version"), role.get("name"))
|
|
elif '://' in role_src:
|
|
# just download a URL - version will probably be in the URL
|
|
tmp_file = fetch_role(role_src, None, None, options)
|
|
else:
|
|
# installing from galaxy
|
|
api_config = api_get_config(api_server)
|
|
if not api_config:
|
|
print "- the API server (%s) is not responding, please try again later." % api_server
|
|
sys.exit(1)
|
|
|
|
role_data = api_lookup_role_by_name(api_server, role_src)
|
|
if not role_data:
|
|
print "- sorry, %s was not found on %s." % (role_src, api_server)
|
|
exit_without_ignore(options)
|
|
continue
|
|
|
|
role_versions = api_fetch_role_related(api_server, 'versions', role_data['id'])
|
|
if "version" not in role or role['version'] == '':
|
|
# convert the version names to LooseVersion objects
|
|
# and sort them to get the latest version. If there
|
|
# are no versions in the list, we'll grab the head
|
|
# of the master branch
|
|
if len(role_versions) > 0:
|
|
loose_versions = [LooseVersion(a.get('name',None)) for a in role_versions]
|
|
loose_versions.sort()
|
|
role["version"] = str(loose_versions[-1])
|
|
else:
|
|
role["version"] = 'master'
|
|
elif role['version'] != 'master':
|
|
if role_versions and role["version"] not in [a.get('name', None) for a in role_versions]:
|
|
print 'role is %s' % role
|
|
print "- the specified version (%s) was not found in the list of available versions (%s)." % (role['version'], role_versions)
|
|
exit_without_ignore(options)
|
|
continue
|
|
|
|
# download the role. if --no-deps was specified, we stop here,
|
|
# otherwise we recursively grab roles and all of their deps.
|
|
tmp_file = fetch_role(role_src, role["version"], role_data, options)
|
|
installed = False
|
|
if tmp_file:
|
|
installed = install_role(role.get("name"), role.get("version"), tmp_file, options)
|
|
# we're done with the temp file, clean it up
|
|
if tmp_file != role_src:
|
|
os.unlink(tmp_file)
|
|
# install dependencies, if we want them
|
|
if not no_deps and installed:
|
|
if not role_data:
|
|
role_data = get_role_metadata(role.get("name"), options)
|
|
role_dependencies = role_data['dependencies']
|
|
else:
|
|
role_dependencies = role_data['summary_fields']['dependencies'] # api_fetch_role_related(api_server, 'dependencies', role_data['id'])
|
|
for dep in role_dependencies:
|
|
if isinstance(dep, basestring):
|
|
dep = ansible.utils.role_spec_parse(dep)
|
|
else:
|
|
dep = ansible.utils.role_yaml_parse(dep)
|
|
if not get_role_metadata(dep["name"], options):
|
|
if dep not in roles_left:
|
|
print '- adding dependency: %s' % dep["name"]
|
|
roles_left.append(dep)
|
|
else:
|
|
print '- dependency %s already pending installation.' % dep["name"]
|
|
else:
|
|
print '- dependency %s is already installed, skipping.' % dep["name"]
|
|
if not tmp_file or not installed:
|
|
print "- %s was NOT installed successfully." % role.get("name")
|
|
exit_without_ignore(options)
|
|
sys.exit(0)
|
|
|
|
def execute_remove(args, options, parser):
|
|
"""
|
|
Executes the remove action. The args list contains the list
|
|
of roles to be removed. This list can contain more than one role.
|
|
"""
|
|
|
|
if len(args) == 0:
|
|
parser.print_help()
|
|
print '- you must specify at least one role to remove.'
|
|
sys.exit()
|
|
|
|
for role in args:
|
|
if get_role_metadata(role, options):
|
|
if remove_role(role, options):
|
|
print '- successfully removed %s' % role
|
|
else:
|
|
print "- failed to remove role: %s" % role
|
|
else:
|
|
print '- %s is not installed, skipping.' % role
|
|
sys.exit(0)
|
|
|
|
def execute_list(args, options, parser):
|
|
"""
|
|
Executes the list action. The args list can contain zero
|
|
or one role. If one is specified, only that role will be
|
|
shown, otherwise all roles in the specified directory will
|
|
be shown.
|
|
"""
|
|
|
|
if len(args) > 1:
|
|
print "- please specify only one role to list, or specify no roles to see a full list"
|
|
sys.exit(1)
|
|
|
|
if len(args) == 1:
|
|
# show only the request role, if it exists
|
|
role_name = args[0]
|
|
metadata = get_role_metadata(role_name, options)
|
|
if metadata:
|
|
install_info = get_galaxy_install_info(role_name, options)
|
|
version = None
|
|
if install_info:
|
|
version = install_info.get("version", None)
|
|
if not version:
|
|
version = "(unknown version)"
|
|
# show some more info about single roles here
|
|
print "- %s, %s" % (role_name, version)
|
|
else:
|
|
print "- the role %s was not found" % role_name
|
|
else:
|
|
# show all valid roles in the roles_path directory
|
|
roles_path = get_opt(options, 'roles_path')
|
|
roles_path = os.path.expanduser(roles_path)
|
|
if not os.path.exists(roles_path):
|
|
parser.print_help()
|
|
print "- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path
|
|
sys.exit(1)
|
|
elif not os.path.isdir(roles_path):
|
|
print "- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
path_files = os.listdir(roles_path)
|
|
for path_file in path_files:
|
|
if get_role_metadata(path_file, options):
|
|
install_info = get_galaxy_install_info(path_file, options)
|
|
version = None
|
|
if install_info:
|
|
version = install_info.get("version", None)
|
|
if not version:
|
|
version = "(unknown version)"
|
|
print "- %s, %s" % (path_file, version)
|
|
sys.exit(0)
|
|
|
|
#-------------------------------------------------------------------------------------
|
|
# The main entry point
|
|
#-------------------------------------------------------------------------------------
|
|
|
|
#def main():
|
|
# # parse the CLI options
|
|
# action = get_action(sys.argv)
|
|
# parser = build_option_parser(action)
|
|
# (options, args) = parser.parse_args()
|
|
#
|
|
# # execute the desired action
|
|
# if 1: #try:
|
|
# fn = globals()["execute_%s" % action]
|
|
# fn(args, options, parser)
|
|
# #except KeyError, e:
|
|
# # print "- error: %s is not a valid action. Valid actions are: %s" % (action, ", ".join(VALID_ACTIONS))
|
|
# # sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
display = Display()
|
|
|
|
try:
|
|
cli = Cli(display=display)
|
|
cli.set_action(sys.argv)
|
|
(options, args) = cli.parse()
|
|
sys.exit(cli.run(options, args))
|
|
except AnsibleError as e:
|
|
display.error(str(e))
|
|
sys.exit(1)
|
|
except KeyboardInterrupt:
|
|
display.error("interrupted")
|
|
sys.exit(1)
|