1# Copyright: (c) 2017, Brian Coca <bcoca@ansible.com> 2# Copyright: (c) 2018, Ansible Project 3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4 5from __future__ import (absolute_import, division, print_function) 6__metaclass__ = type 7 8import sys 9 10import argparse 11from operator import attrgetter 12 13from ansible import constants as C 14from ansible import context 15from ansible.cli import CLI 16from ansible.cli.arguments import option_helpers as opt_help 17from ansible.errors import AnsibleError, AnsibleOptionsError 18from ansible.module_utils._text import to_bytes, to_native, to_text 19from ansible.utils.vars import combine_vars 20from ansible.utils.display import Display 21from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_path 22 23display = Display() 24 25INTERNAL_VARS = frozenset(['ansible_diff_mode', 26 'ansible_config_file', 27 'ansible_facts', 28 'ansible_forks', 29 'ansible_inventory_sources', 30 'ansible_limit', 31 'ansible_playbook_python', 32 'ansible_run_tags', 33 'ansible_skip_tags', 34 'ansible_verbosity', 35 'ansible_version', 36 'inventory_dir', 37 'inventory_file', 38 'inventory_hostname', 39 'inventory_hostname_short', 40 'groups', 41 'group_names', 42 'omit', 43 'playbook_dir', ]) 44 45 46class InventoryCLI(CLI): 47 ''' used to display or dump the configured inventory as Ansible sees it ''' 48 49 ARGUMENTS = {'host': 'The name of a host to match in the inventory, relevant when using --list', 50 'group': 'The name of a group in the inventory, relevant when using --graph', } 51 52 def __init__(self, args): 53 54 super(InventoryCLI, self).__init__(args) 55 self.vm = None 56 self.loader = None 57 self.inventory = None 58 59 def init_parser(self): 60 super(InventoryCLI, self).init_parser( 61 usage='usage: %prog [options] [host|group]', 62 epilog='Show Ansible inventory information, by default it uses the inventory script JSON format') 63 64 opt_help.add_inventory_options(self.parser) 65 opt_help.add_vault_options(self.parser) 66 opt_help.add_basedir_options(self.parser) 67 opt_help.add_runtask_options(self.parser) 68 69 # remove unused default options 70 self.parser.add_argument('-l', '--limit', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument, nargs='?') 71 self.parser.add_argument('--list-hosts', help=argparse.SUPPRESS, action=opt_help.UnrecognizedArgument) 72 73 self.parser.add_argument('args', metavar='host|group', nargs='?') 74 75 # Actions 76 action_group = self.parser.add_argument_group("Actions", "One of following must be used on invocation, ONLY ONE!") 77 action_group.add_argument("--list", action="store_true", default=False, dest='list', help='Output all hosts info, works as inventory script') 78 action_group.add_argument("--host", action="store", default=None, dest='host', help='Output specific host info, works as inventory script') 79 action_group.add_argument("--graph", action="store_true", default=False, dest='graph', 80 help='create inventory graph, if supplying pattern it must be a valid group name') 81 self.parser.add_argument_group(action_group) 82 83 # graph 84 self.parser.add_argument("-y", "--yaml", action="store_true", default=False, dest='yaml', 85 help='Use YAML format instead of default JSON, ignored for --graph') 86 self.parser.add_argument('--toml', action='store_true', default=False, dest='toml', 87 help='Use TOML format instead of default JSON, ignored for --graph') 88 self.parser.add_argument("--vars", action="store_true", default=False, dest='show_vars', 89 help='Add vars to graph display, ignored unless used with --graph') 90 91 # list 92 self.parser.add_argument("--export", action="store_true", default=C.INVENTORY_EXPORT, dest='export', 93 help="When doing an --list, represent in a way that is optimized for export," 94 "not as an accurate representation of how Ansible has processed it") 95 self.parser.add_argument('--output', default=None, dest='output_file', 96 help="When doing --list, send the inventory to a file instead of to the screen") 97 # self.parser.add_argument("--ignore-vars-plugins", action="store_true", default=False, dest='ignore_vars_plugins', 98 # help="When doing an --list, skip vars data from vars plugins, by default, this would include group_vars/ and host_vars/") 99 100 def post_process_args(self, options): 101 options = super(InventoryCLI, self).post_process_args(options) 102 103 display.verbosity = options.verbosity 104 self.validate_conflicts(options) 105 106 # there can be only one! and, at least, one! 107 used = 0 108 for opt in (options.list, options.host, options.graph): 109 if opt: 110 used += 1 111 if used == 0: 112 raise AnsibleOptionsError("No action selected, at least one of --host, --graph or --list needs to be specified.") 113 elif used > 1: 114 raise AnsibleOptionsError("Conflicting options used, only one of --host, --graph or --list can be used at the same time.") 115 116 # set host pattern to default if not supplied 117 if options.args: 118 options.pattern = options.args 119 else: 120 options.pattern = 'all' 121 122 return options 123 124 def run(self): 125 126 super(InventoryCLI, self).run() 127 128 # Initialize needed objects 129 self.loader, self.inventory, self.vm = self._play_prereqs() 130 131 results = None 132 if context.CLIARGS['host']: 133 hosts = self.inventory.get_hosts(context.CLIARGS['host']) 134 if len(hosts) != 1: 135 raise AnsibleOptionsError("You must pass a single valid host to --host parameter") 136 137 myvars = self._get_host_variables(host=hosts[0]) 138 139 # FIXME: should we template first? 140 results = self.dump(myvars) 141 142 elif context.CLIARGS['graph']: 143 results = self.inventory_graph() 144 elif context.CLIARGS['list']: 145 top = self._get_group('all') 146 if context.CLIARGS['yaml']: 147 results = self.yaml_inventory(top) 148 elif context.CLIARGS['toml']: 149 results = self.toml_inventory(top) 150 else: 151 results = self.json_inventory(top) 152 results = self.dump(results) 153 154 if results: 155 outfile = context.CLIARGS['output_file'] 156 if outfile is None: 157 # FIXME: pager? 158 display.display(results) 159 else: 160 try: 161 with open(to_bytes(outfile), 'wb') as f: 162 f.write(to_bytes(results)) 163 except (OSError, IOError) as e: 164 raise AnsibleError('Unable to write to destination file (%s): %s' % (to_native(outfile), to_native(e))) 165 sys.exit(0) 166 167 sys.exit(1) 168 169 @staticmethod 170 def dump(stuff): 171 172 if context.CLIARGS['yaml']: 173 import yaml 174 from ansible.parsing.yaml.dumper import AnsibleDumper 175 results = to_text(yaml.dump(stuff, Dumper=AnsibleDumper, default_flow_style=False, allow_unicode=True)) 176 elif context.CLIARGS['toml']: 177 from ansible.plugins.inventory.toml import toml_dumps, HAS_TOML 178 if not HAS_TOML: 179 raise AnsibleError( 180 'The python "toml" library is required when using the TOML output format' 181 ) 182 results = toml_dumps(stuff) 183 else: 184 import json 185 from ansible.parsing.ajson import AnsibleJSONEncoder 186 try: 187 results = json.dumps(stuff, cls=AnsibleJSONEncoder, sort_keys=True, indent=4, preprocess_unsafe=True, ensure_ascii=False) 188 except TypeError as e: 189 results = json.dumps(stuff, cls=AnsibleJSONEncoder, sort_keys=False, indent=4, preprocess_unsafe=True, ensure_ascii=False) 190 display.warning("Could not sort JSON output due to issues while sorting keys: %s" % to_native(e)) 191 192 return results 193 194 def _get_group_variables(self, group): 195 196 # get info from inventory source 197 res = group.get_vars() 198 199 # Always load vars plugins 200 res = combine_vars(res, get_vars_from_inventory_sources(self.loader, self.inventory._sources, [group], 'all')) 201 if context.CLIARGS['basedir']: 202 res = combine_vars(res, get_vars_from_path(self.loader, context.CLIARGS['basedir'], [group], 'all')) 203 204 if group.priority != 1: 205 res['ansible_group_priority'] = group.priority 206 207 return self._remove_internal(res) 208 209 def _get_host_variables(self, host): 210 211 if context.CLIARGS['export']: 212 # only get vars defined directly host 213 hostvars = host.get_vars() 214 215 # Always load vars plugins 216 hostvars = combine_vars(hostvars, get_vars_from_inventory_sources(self.loader, self.inventory._sources, [host], 'all')) 217 if context.CLIARGS['basedir']: 218 hostvars = combine_vars(hostvars, get_vars_from_path(self.loader, context.CLIARGS['basedir'], [host], 'all')) 219 else: 220 # get all vars flattened by host, but skip magic hostvars 221 hostvars = self.vm.get_vars(host=host, include_hostvars=False, stage='all') 222 223 return self._remove_internal(hostvars) 224 225 def _get_group(self, gname): 226 group = self.inventory.groups.get(gname) 227 return group 228 229 @staticmethod 230 def _remove_internal(dump): 231 232 for internal in INTERNAL_VARS: 233 if internal in dump: 234 del dump[internal] 235 236 return dump 237 238 @staticmethod 239 def _remove_empty(dump): 240 # remove empty keys 241 for x in ('hosts', 'vars', 'children'): 242 if x in dump and not dump[x]: 243 del dump[x] 244 245 @staticmethod 246 def _show_vars(dump, depth): 247 result = [] 248 for (name, val) in sorted(dump.items()): 249 result.append(InventoryCLI._graph_name('{%s = %s}' % (name, val), depth)) 250 return result 251 252 @staticmethod 253 def _graph_name(name, depth=0): 254 if depth: 255 name = " |" * (depth) + "--%s" % name 256 return name 257 258 def _graph_group(self, group, depth=0): 259 260 result = [self._graph_name('@%s:' % group.name, depth)] 261 depth = depth + 1 262 for kid in sorted(group.child_groups, key=attrgetter('name')): 263 result.extend(self._graph_group(kid, depth)) 264 265 if group.name != 'all': 266 for host in sorted(group.hosts, key=attrgetter('name')): 267 result.append(self._graph_name(host.name, depth)) 268 if context.CLIARGS['show_vars']: 269 result.extend(self._show_vars(self._get_host_variables(host), depth + 1)) 270 271 if context.CLIARGS['show_vars']: 272 result.extend(self._show_vars(self._get_group_variables(group), depth)) 273 274 return result 275 276 def inventory_graph(self): 277 278 start_at = self._get_group(context.CLIARGS['pattern']) 279 if start_at: 280 return '\n'.join(self._graph_group(start_at)) 281 else: 282 raise AnsibleOptionsError("Pattern must be valid group name when using --graph") 283 284 def json_inventory(self, top): 285 286 seen = set() 287 288 def format_group(group): 289 results = {} 290 results[group.name] = {} 291 if group.name != 'all': 292 results[group.name]['hosts'] = [h.name for h in sorted(group.hosts, key=attrgetter('name'))] 293 results[group.name]['children'] = [] 294 for subgroup in sorted(group.child_groups, key=attrgetter('name')): 295 results[group.name]['children'].append(subgroup.name) 296 if subgroup.name not in seen: 297 results.update(format_group(subgroup)) 298 seen.add(subgroup.name) 299 if context.CLIARGS['export']: 300 results[group.name]['vars'] = self._get_group_variables(group) 301 302 self._remove_empty(results[group.name]) 303 if not results[group.name]: 304 del results[group.name] 305 306 return results 307 308 results = format_group(top) 309 310 # populate meta 311 results['_meta'] = {'hostvars': {}} 312 hosts = self.inventory.get_hosts() 313 for host in hosts: 314 hvars = self._get_host_variables(host) 315 if hvars: 316 results['_meta']['hostvars'][host.name] = hvars 317 318 return results 319 320 def yaml_inventory(self, top): 321 322 seen = [] 323 324 def format_group(group): 325 results = {} 326 327 # initialize group + vars 328 results[group.name] = {} 329 330 # subgroups 331 results[group.name]['children'] = {} 332 for subgroup in sorted(group.child_groups, key=attrgetter('name')): 333 if subgroup.name != 'all': 334 results[group.name]['children'].update(format_group(subgroup)) 335 336 # hosts for group 337 results[group.name]['hosts'] = {} 338 if group.name != 'all': 339 for h in sorted(group.hosts, key=attrgetter('name')): 340 myvars = {} 341 if h.name not in seen: # avoid defining host vars more than once 342 seen.append(h.name) 343 myvars = self._get_host_variables(host=h) 344 results[group.name]['hosts'][h.name] = myvars 345 346 if context.CLIARGS['export']: 347 gvars = self._get_group_variables(group) 348 if gvars: 349 results[group.name]['vars'] = gvars 350 351 self._remove_empty(results[group.name]) 352 353 return results 354 355 return format_group(top) 356 357 def toml_inventory(self, top): 358 seen = set() 359 has_ungrouped = bool(next(g.hosts for g in top.child_groups if g.name == 'ungrouped')) 360 361 def format_group(group): 362 results = {} 363 results[group.name] = {} 364 365 results[group.name]['children'] = [] 366 for subgroup in sorted(group.child_groups, key=attrgetter('name')): 367 if subgroup.name == 'ungrouped' and not has_ungrouped: 368 continue 369 if group.name != 'all': 370 results[group.name]['children'].append(subgroup.name) 371 results.update(format_group(subgroup)) 372 373 if group.name != 'all': 374 for host in sorted(group.hosts, key=attrgetter('name')): 375 if host.name not in seen: 376 seen.add(host.name) 377 host_vars = self._get_host_variables(host=host) 378 else: 379 host_vars = {} 380 try: 381 results[group.name]['hosts'][host.name] = host_vars 382 except KeyError: 383 results[group.name]['hosts'] = {host.name: host_vars} 384 385 if context.CLIARGS['export']: 386 results[group.name]['vars'] = self._get_group_variables(group) 387 388 self._remove_empty(results[group.name]) 389 if not results[group.name]: 390 del results[group.name] 391 392 return results 393 394 results = format_group(top) 395 396 return results 397