Last active
March 30, 2017 19:43
-
-
Save cchurch/3b6027d3816c0ea3efe42b55ba924c09 to your computer and use it in GitHub Desktop.
Standalone ansible-inventory script based on https://github.com/bcoca/ansible/blob/a689d6a54bcaff2e686c95f0933a9b3519e9c209/lib/ansible/cli/inventory.py (variables defined under group_vars are not listed separately but still included in host variables)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# (c) 2017, Brian Coca <[email protected]> | |
# | |
# 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 import constants as C | |
from ansible.cli import CLI | |
from ansible.errors import AnsibleOptionsError | |
from ansible.inventory import Inventory | |
from ansible.parsing.dataloader import DataLoader | |
from ansible.vars import VariableManager | |
try: | |
from __main__ import display | |
except ImportError: | |
from ansible.utils.display import Display | |
display = Display() | |
INTERNAL_VARS = frozenset([ '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 | |
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') | |
try: | |
super(InventoryCLI, self).parse() | |
except Exception as e: | |
if 'Need to implement!' not in e.args[0]: | |
raise | |
# --- Start of 2.3+ super(InventoryCLI, self).parse() --- | |
self.options, self.args = self.parser.parse_args(self.args[1:]) | |
if hasattr(self.options, 'tags') and not self.options.tags: | |
# optparse defaults does not do what's expected | |
self.options.tags = ['all'] | |
if hasattr(self.options, 'tags') and self.options.tags: | |
if not C.MERGE_MULTIPLE_CLI_TAGS: | |
if len(self.options.tags) > 1: | |
display.deprecated('Specifying --tags multiple times on the command line currently uses the last specified value. In 2.4, values will be merged instead. Set merge_multiple_cli_tags=True in ansible.cfg to get this behavior now.', version=2.5, removed=False) | |
self.options.tags = [self.options.tags[-1]] | |
tags = set() | |
for tag_set in self.options.tags: | |
for tag in tag_set.split(u','): | |
tags.add(tag.strip()) | |
self.options.tags = list(tags) | |
if hasattr(self.options, 'skip_tags') and self.options.skip_tags: | |
if not C.MERGE_MULTIPLE_CLI_TAGS: | |
if len(self.options.skip_tags) > 1: | |
display.deprecated('Specifying --skip-tags multiple times on the command line currently uses the last specified value. In 2.4, values will be merged instead. Set merge_multiple_cli_tags=True in ansible.cfg to get this behavior now.', version=2.5, removed=False) | |
self.options.skip_tags = [self.options.skip_tags[-1]] | |
skip_tags = set() | |
for tag_set in self.options.skip_tags: | |
for tag in tag_set.split(u','): | |
skip_tags.add(tag.strip()) | |
self.options.skip_tags = list(skip_tags) | |
# --- End of 2.3+ 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 | |
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.vm.get_vars(self.loader, 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.inventory.get_group('all') | |
if self.options.yaml: | |
results = self.yaml_inventory(top) | |
else: | |
results = self.json_inventory(top) | |
results = self.dump(results) | |
if results: | |
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 _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.inventory.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.vm.get_vars(self.loader, 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.vm.get_vars(self.loader, 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) | |
if __name__ == '__main__': | |
import imp | |
import subprocess | |
import sys | |
with open(__file__) as f: | |
imp.load_source('ansible.cli.inventory', __file__ + '.py', f) | |
ansible_path = subprocess.check_output(['which', 'ansible']).strip() | |
sys.argv[0] = 'ansible-inventory' | |
execfile(ansible_path) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment