summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Coca <bcoca@users.noreply.github.com>2017-08-15 16:45:54 -0400
committerGitHub <noreply@github.com>2017-08-15 16:45:54 -0400
commitc27248db6026d929dcb564170413403c97465534 (patch)
tree3ca5b0f7e3fd22018e52b0b7d5b0650443f7eda3
parent178cf6e82e9b446a498c80211302049eae5aae70 (diff)
downloadansible-c27248db6026d929dcb564170413403c97465534.tar.gz
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
l---------bin/ansible-inventory1
-rw-r--r--lib/ansible/cli/inventory.py299
2 files changed, 300 insertions, 0 deletions
diff --git a/bin/ansible-inventory b/bin/ansible-inventory
new file mode 120000
index 0000000000..cabb1f519a
--- /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 0000000000..f0dba1171a
--- /dev/null
+++ b/lib/ansible/cli/inventory.py
@@ -0,0 +1,299 @@
+# (c) 2017, Brian Coca <bcoca@ansible.com>
+#
+# 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/>.
+#
+
+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)