From c27248db6026d929dcb564170413403c97465534 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 15 Aug 2017 16:45:54 -0400 Subject: [PATCH] ansible-inventory cli tool (#22477) * ansible-inventory cli tool added vars dump to graph made yaml inventory dump actual yaml inventory format cleaner dump fixed graph, no dump needed add pulling in host/group vars pep indentation crime added docstring for manpage autodoc remove ansible_facts from output added api compat layer allow import from new and old APIs better conditional for <2.4 pe4+p4 test stuck on OS X (seems popular today) but passes rest, merging anyways --- bin/ansible-inventory | 1 + lib/ansible/cli/inventory.py | 299 +++++++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 120000 bin/ansible-inventory create mode 100644 lib/ansible/cli/inventory.py diff --git a/bin/ansible-inventory b/bin/ansible-inventory new file mode 120000 index 00000000000..cabb1f519aa --- /dev/null +++ b/bin/ansible-inventory @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/lib/ansible/cli/inventory.py b/lib/ansible/cli/inventory.py new file mode 100644 index 00000000000..f0dba1171aa --- /dev/null +++ b/lib/ansible/cli/inventory.py @@ -0,0 +1,299 @@ +# (c) 2017, Brian Coca +# +# 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 . +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import optparse +from operator import attrgetter + +from ansible.cli import CLI +from ansible.errors import AnsibleOptionsError +from ansible.parsing.dataloader import DataLoader + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +INTERNAL_VARS = frozenset(['ansible_facts', 'ansible_version', + 'ansible_playbook_python', + 'inventory_dir', + 'inventory_file', + 'inventory_hostname', + 'inventory_hostname_short', + 'groups', + 'group_names', + 'omit', + 'playbook_dir', ]) + + +class InventoryCLI(CLI): + ''' used to display or dump the configured inventory as Ansible sees it ''' + + ARGUMENTS = {'host': 'The name of a host to match in the inventory, relevant when using --list', + 'group': 'The name of a group in the inventory, relevant when using --graph', } + + def __init__(self, args): + + super(InventoryCLI, self).__init__(args) + self.vm = None + self.loader = None + self.inventory = None + + self._new_api = True + + def parse(self): + + self.parser = CLI.base_parser( + usage='usage: %prog [options] [host|group]', + epilog='Show Ansible inventory information, by default it uses the inventory script JSON format', + inventory_opts=True, + vault_opts=True + ) + + # Actions + action_group = optparse.OptionGroup(self.parser, "Actions", "One of following must be used on invocation, ONLY ONE!") + action_group.add_option("--list", action="store_true", default=False, dest='list', help='Output all hosts info, works as inventory script') + action_group.add_option("--host", action="store", default=None, dest='host', help='Output specific host info, works as inventory script') + action_group.add_option("--graph", action="store_true", default=False, dest='graph', + help='create inventory graph, if supplying pattern it must be a valid group name') + self.parser.add_option_group(action_group) + + # Options + self.parser.add_option("-y", "--yaml", action="store_true", default=False, dest='yaml', + help='Use YAML format instead of default JSON, ignored for --graph') + self.parser.add_option("--vars", action="store_true", default=False, dest='show_vars', + help='Add vars to graph display, ignored unless used with --graph') + + super(InventoryCLI, self).parse() + + display.verbosity = self.options.verbosity + + self.validate_conflicts(vault_opts=True) + + # there can be only one! and, at least, one! + used = 0 + for opt in (self.options.list, self.options.host, self.options.graph): + if opt: + used += 1 + if used == 0: + raise AnsibleOptionsError("No action selected, at least one of --host, --graph or --list needs to be specified.") + elif used > 1: + raise AnsibleOptionsError("Conflicting options used, only one of --host, --graph or --list can be used at the same time.") + + # set host pattern to default if not supplied + if len(self.args) > 0: + self.options.pattern = self.args[0] + else: + self.options.pattern = 'all' + + def run(self): + + results = None + + super(InventoryCLI, self).run() + + # Initialize needed objects + if getattr(self, '_play_prereqs', False): + self.loader, self.inventory, self.vm = self._play_prereqs(self.options) + else: + # fallback to pre 2.4 way of initialzing + from ansible.vars import VariableManager + from ansible.inventory import Inventory + + self._new_api = False + self.loader = DataLoader() + self.vm = VariableManager() + + # use vault if needed + if self.options.vault_password_file: + vault_pass = CLI.read_vault_password_file(self.options.vault_password_file, loader=self.loader) + elif self.options.ask_vault_pass: + vault_pass = self.ask_vault_passwords() + else: + vault_pass = None + + if vault_pass: + self.loader.set_vault_password(vault_pass) + # actually get inventory and vars + + self.inventory = Inventory(loader=self.loader, variable_manager=self.vm, host_list=self.options.inventory) + self.vm.set_inventory(self.inventory) + + if self.options.host: + hosts = self.inventory.get_hosts(self.options.host) + if len(hosts) != 1: + raise AnsibleOptionsError("You must pass a single valid host to --hosts parameter") + + myvars = self._get_host_variables(host=hosts[0]) + self._remove_internal(myvars) + + # FIXME: should we template first? + results = self.dump(myvars) + + elif self.options.graph: + results = self.inventory_graph() + elif self.options.list: + top = self._get_group('all') + if self.options.yaml: + results = self.yaml_inventory(top) + else: + results = self.json_inventory(top) + results = self.dump(results) + + if results: + # FIXME: pager? + display.display(results) + exit(0) + + exit(1) + + def dump(self, stuff): + + if self.options.yaml: + import yaml + from ansible.parsing.yaml.dumper import AnsibleDumper + results = yaml.dump(stuff, Dumper=AnsibleDumper, default_flow_style=False) + else: + import json + results = json.dumps(stuff, sort_keys=True, indent=4) + + return results + + def _get_host_variables(self, host): + if self._new_api: + hostvars = self.vm.get_vars(host=host) + else: + hostvars = self.vm.get_vars(self.loader, host=host) + return hostvars + + def _get_group(self, gname): + if self._new_api: + group = self.inventory.groups.get(gname) + else: + group = self.inventory.get_group(gname) + return group + + def _remove_internal(self, dump): + + for internal in INTERNAL_VARS: + if internal in dump: + del dump[internal] + + def _remove_empty(self, dump): + # remove empty keys + for x in ('hosts', 'vars', 'children'): + if x in dump and not dump[x]: + del dump[x] + + def _show_vars(self, dump, depth): + result = [] + self._remove_internal(dump) + if self.options.show_vars: + for (name, val) in sorted(dump.items()): + result.append(self._graph_name('{%s = %s}' % (name, val), depth + 1)) + return result + + def _graph_name(self, name, depth=0): + if depth: + name = " |" * (depth) + "--%s" % name + return name + + def _graph_group(self, group, depth=0): + + result = [self._graph_name('@%s:' % group.name, depth)] + depth = depth + 1 + for kid in sorted(group.child_groups, key=attrgetter('name')): + result.extend(self._graph_group(kid, depth)) + + if group.name != 'all': + for host in sorted(group.hosts, key=attrgetter('name')): + result.append(self._graph_name(host.name, depth)) + result.extend(self._show_vars(host.get_vars(), depth)) + + result.extend(self._show_vars(group.get_vars(), depth)) + + return result + + def inventory_graph(self): + + start_at = self._get_group(self.options.pattern) + if start_at: + return '\n'.join(self._graph_group(start_at)) + else: + raise AnsibleOptionsError("Pattern must be valid group name when using --graph") + + def json_inventory(self, top): + + def format_group(group): + results = {} + results[group.name] = {} + if group.name != 'all': + results[group.name]['hosts'] = [h.name for h in sorted(group.hosts, key=attrgetter('name'))] + results[group.name]['vars'] = group.get_vars() + results[group.name]['children'] = [] + for subgroup in sorted(group.child_groups, key=attrgetter('name')): + results[group.name]['children'].append(subgroup.name) + results.update(format_group(subgroup)) + + self._remove_empty(results[group.name]) + return results + + results = format_group(top) + + # populate meta + results['_meta'] = {'hostvars': {}} + hosts = self.inventory.get_hosts() + for host in hosts: + results['_meta']['hostvars'][host.name] = self._get_host_variables(host=host) + self._remove_internal(results['_meta']['hostvars'][host.name]) + + return results + + def yaml_inventory(self, top): + + seen = [] + + def format_group(group): + results = {} + + # initialize group + vars + results[group.name] = {} + results[group.name]['vars'] = group.get_vars() + + # subgroups + results[group.name]['children'] = {} + for subgroup in sorted(group.child_groups, key=attrgetter('name')): + if subgroup.name != 'all': + results[group.name]['children'].update(format_group(subgroup)) + + # hosts for group + results[group.name]['hosts'] = {} + if group.name != 'all': + for h in sorted(group.hosts, key=attrgetter('name')): + myvars = {} + if h.name not in seen: # avoid defining host vars more than once + seen.append(h.name) + myvars = self._get_host_variables(host=h) + self._remove_internal(myvars) + results[group.name]['hosts'][h.name] = myvars + + self._remove_empty(results[group.name]) + return results + + return format_group(top)