1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
name: yaml
version_added: "2.4"
short_description: Uses a specific YAML file as an inventory source.
description:
- "YAML-based inventory, should start with the C(all) group and contain hosts/vars/children entries."
- Host entries can have sub-entries defined, which will be treated as variables.
- Vars entries are normal group vars.
- "Children are 'child groups', which can also have their own vars/hosts/children and so on."
- File MUST have a valid extension, defined in configuration.
notes:
- If you want to set vars for the C(all) group inside the inventory file, the C(all) group must be the first entry in the file.
- Enabled in configuration by default.
options:
yaml_extensions:
description: list of 'valid' extensions for files containing YAML
type: list
elements: string
default: ['.yaml', '.yml', '.json']
env:
- name: ANSIBLE_YAML_FILENAME_EXT
- name: ANSIBLE_INVENTORY_PLUGIN_EXTS
ini:
- key: yaml_valid_extensions
section: defaults
- section: inventory_plugin_yaml
key: yaml_valid_extensions
"""
EXAMPLES = """
all: # keys must be unique, i.e. only one 'hosts' per group
hosts:
test1:
test2:
host_var: value
vars:
group_all_var: value
children: # key order does not matter, indentation does
other_group:
children:
group_x:
hosts:
test5 # Note that one machine will work without a colon
#group_x:
# hosts:
# test5 # But this won't
# test7 #
group_y:
hosts:
test6: # So always use a colon
vars:
g2_var2: value3
hosts:
test4:
ansible_host: 127.0.0.1
last_group:
hosts:
test1 # same host as above, additional group membership
vars:
group_last_var: value
"""
import os
from collections.abc import MutableMapping
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.six import string_types
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.plugins.inventory import BaseFileInventoryPlugin
NoneType = type(None)
class InventoryModule(BaseFileInventoryPlugin):
NAME = 'yaml'
# implicit trust behavior is already added by the YAML parser invoked by the loader
def __init__(self):
super(InventoryModule, self).__init__()
def verify_file(self, path):
valid = False
if super(InventoryModule, self).verify_file(path):
file_name, ext = os.path.splitext(path)
if not ext or ext in self.get_option('yaml_extensions'):
valid = True
return valid
def parse(self, inventory, loader, path, cache=True):
""" parses the inventory file """
super(InventoryModule, self).parse(inventory, loader, path)
self.set_options()
try:
data = self.loader.load_from_file(path, cache='none', trusted_as_template=True)
except Exception as e:
raise AnsibleParserError(e)
if not data:
raise AnsibleParserError('Parsed empty YAML file')
elif not isinstance(data, MutableMapping):
raise AnsibleParserError('YAML inventory has invalid structure, it should be a dictionary, got: %s' % type(data))
elif data.get('plugin'):
raise AnsibleParserError('Plugin configuration YAML file, not YAML inventory')
# We expect top level keys to correspond to groups, iterate over them
# to get host, vars and subgroups (which we iterate over recursively)
if isinstance(data, MutableMapping):
for group_name in data:
self._parse_group(group_name, data[group_name])
else:
raise AnsibleParserError("Invalid data from file, expected dictionary and got:\n\n%s" % to_native(data))
def _parse_group(self, group, group_data):
if isinstance(group_data, (MutableMapping, NoneType)): # type: ignore[misc]
try:
group = self.inventory.add_group(group)
except AnsibleError as e:
raise AnsibleParserError("Unable to add group %s: %s" % (group, to_text(e)))
if group_data is not None:
# make sure they are dicts
for section in ['vars', 'children', 'hosts']:
if section in group_data:
# convert strings to dicts as these are allowed
if isinstance(group_data[section], string_types):
group_data[section] = {group_data[section]: None}
if not isinstance(group_data[section], (MutableMapping, NoneType)): # type: ignore[misc]
raise AnsibleParserError('Invalid "%s" entry for "%s" group, requires a dictionary, found "%s" instead.' %
(section, group, type(group_data[section])))
for key in group_data:
if not isinstance(group_data[key], (MutableMapping, NoneType)): # type: ignore[misc]
self.display.warning('Skipping key (%s) in group (%s) as it is not a mapping, it is a %s' % (key, group, type(group_data[key])))
continue
if isinstance(group_data[key], NoneType): # type: ignore[misc]
self.display.vvv('Skipping empty key (%s) in group (%s)' % (key, group))
elif key == 'vars':
for var in group_data[key]:
self.inventory.set_variable(group, var, group_data[key][var])
elif key == 'children':
for subgroup in group_data[key]:
subgroup = self._parse_group(subgroup, group_data[key][subgroup])
self.inventory.add_child(group, subgroup)
elif key == 'hosts':
for host_pattern in group_data[key]:
hosts, port = self._parse_host(host_pattern)
self._populate_host_vars(hosts, group_data[key][host_pattern] or {}, group, port)
else:
self.display.warning('Skipping unexpected key (%s) in group (%s), only "vars", "children" and "hosts" are valid' % (key, group))
else:
self.display.warning("Skipping '%s' as this is not a valid group definition" % group)
return group
def _parse_host(self, host_pattern):
"""
Each host key can be a pattern, try to process it and add variables as needed
"""
try:
(hostnames, port) = self._expand_hostpattern(host_pattern)
except TypeError:
raise AnsibleParserError(
f"Host pattern {host_pattern} must be a string. Enclose integers/floats in quotation marks."
)
return hostnames, port
|