summaryrefslogtreecommitdiffstats
path: root/lib/ansible/plugins/inventory/toml.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/plugins/inventory/toml.py')
-rw-r--r--lib/ansible/plugins/inventory/toml.py298
1 files changed, 298 insertions, 0 deletions
diff --git a/lib/ansible/plugins/inventory/toml.py b/lib/ansible/plugins/inventory/toml.py
new file mode 100644
index 0000000..f68b34a
--- /dev/null
+++ b/lib/ansible/plugins/inventory/toml.py
@@ -0,0 +1,298 @@
+# Copyright (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+ name: toml
+ version_added: "2.8"
+ short_description: Uses a specific TOML file as an inventory source.
+ description:
+ - TOML based inventory format
+ - File MUST have a valid '.toml' file extension
+ notes:
+ - >
+ Requires one of the following python libraries: 'toml', 'tomli', or 'tomllib'
+'''
+
+EXAMPLES = r'''# fmt: toml
+# Example 1
+[all.vars]
+has_java = false
+
+[web]
+children = [
+ "apache",
+ "nginx"
+]
+vars = { http_port = 8080, myvar = 23 }
+
+[web.hosts]
+host1 = {}
+host2 = { ansible_port = 222 }
+
+[apache.hosts]
+tomcat1 = {}
+tomcat2 = { myvar = 34 }
+tomcat3 = { mysecret = "03#pa33w0rd" }
+
+[nginx.hosts]
+jenkins1 = {}
+
+[nginx.vars]
+has_java = true
+
+# Example 2
+[all.vars]
+has_java = false
+
+[web]
+children = [
+ "apache",
+ "nginx"
+]
+
+[web.vars]
+http_port = 8080
+myvar = 23
+
+[web.hosts.host1]
+[web.hosts.host2]
+ansible_port = 222
+
+[apache.hosts.tomcat1]
+
+[apache.hosts.tomcat2]
+myvar = 34
+
+[apache.hosts.tomcat3]
+mysecret = "03#pa33w0rd"
+
+[nginx.hosts.jenkins1]
+
+[nginx.vars]
+has_java = true
+
+# Example 3
+[ungrouped.hosts]
+host1 = {}
+host2 = { ansible_host = "127.0.0.1", ansible_port = 44 }
+host3 = { ansible_host = "127.0.0.1", ansible_port = 45 }
+
+[g1.hosts]
+host4 = {}
+
+[g2.hosts]
+host4 = {}
+'''
+
+import os
+import typing as t
+
+from collections.abc import MutableMapping, MutableSequence
+from functools import partial
+
+from ansible.errors import AnsibleFileNotFound, AnsibleParserError, AnsibleRuntimeError
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.six import string_types, text_type
+from ansible.parsing.yaml.objects import AnsibleSequence, AnsibleUnicode
+from ansible.plugins.inventory import BaseFileInventoryPlugin
+from ansible.utils.display import Display
+from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes, AnsibleUnsafeText
+
+HAS_TOML = False
+try:
+ import toml
+ HAS_TOML = True
+except ImportError:
+ pass
+
+HAS_TOMLIW = False
+try:
+ import tomli_w # type: ignore[import]
+ HAS_TOMLIW = True
+except ImportError:
+ pass
+
+HAS_TOMLLIB = False
+try:
+ import tomllib # type: ignore[import]
+ HAS_TOMLLIB = True
+except ImportError:
+ try:
+ import tomli as tomllib # type: ignore[no-redef]
+ HAS_TOMLLIB = True
+ except ImportError:
+ pass
+
+display = Display()
+
+
+# dumps
+if HAS_TOML and hasattr(toml, 'TomlEncoder'):
+ # toml>=0.10.0
+ class AnsibleTomlEncoder(toml.TomlEncoder):
+ def __init__(self, *args, **kwargs):
+ super(AnsibleTomlEncoder, self).__init__(*args, **kwargs)
+ # Map our custom YAML object types to dump_funcs from ``toml``
+ self.dump_funcs.update({
+ AnsibleSequence: self.dump_funcs.get(list),
+ AnsibleUnicode: self.dump_funcs.get(str),
+ AnsibleUnsafeBytes: self.dump_funcs.get(str),
+ AnsibleUnsafeText: self.dump_funcs.get(str),
+ })
+ toml_dumps = partial(toml.dumps, encoder=AnsibleTomlEncoder()) # type: t.Callable[[t.Any], str]
+else:
+ # toml<0.10.0
+ # tomli-w
+ def toml_dumps(data): # type: (t.Any) -> str
+ if HAS_TOML:
+ return toml.dumps(convert_yaml_objects_to_native(data))
+ elif HAS_TOMLIW:
+ return tomli_w.dumps(convert_yaml_objects_to_native(data))
+ raise AnsibleRuntimeError(
+ 'The python "toml" or "tomli-w" library is required when using the TOML output format'
+ )
+
+# loads
+if HAS_TOML:
+ # prefer toml if installed, since it supports both encoding and decoding
+ toml_loads = toml.loads # type: ignore[assignment]
+ TOMLDecodeError = toml.TomlDecodeError # type: t.Any
+elif HAS_TOMLLIB:
+ toml_loads = tomllib.loads # type: ignore[assignment]
+ TOMLDecodeError = tomllib.TOMLDecodeError # type: t.Any # type: ignore[no-redef]
+
+
+def convert_yaml_objects_to_native(obj):
+ """Older versions of the ``toml`` python library, and tomllib, don't have
+ a pluggable way to tell the encoder about custom types, so we need to
+ ensure objects that we pass are native types.
+
+ Used with:
+ - ``toml<0.10.0`` where ``toml.TomlEncoder`` is missing
+ - ``tomli`` or ``tomllib``
+
+ This function recurses an object and ensures we cast any of the types from
+ ``ansible.parsing.yaml.objects`` into their native types, effectively cleansing
+ the data before we hand it over to the toml library.
+
+ This function doesn't directly check for the types from ``ansible.parsing.yaml.objects``
+ but instead checks for the types those objects inherit from, to offer more flexibility.
+ """
+ if isinstance(obj, dict):
+ return dict((k, convert_yaml_objects_to_native(v)) for k, v in obj.items())
+ elif isinstance(obj, list):
+ return [convert_yaml_objects_to_native(v) for v in obj]
+ elif isinstance(obj, text_type):
+ return text_type(obj)
+ else:
+ return obj
+
+
+class InventoryModule(BaseFileInventoryPlugin):
+ NAME = 'toml'
+
+ def _parse_group(self, group, group_data):
+ if group_data is not None and not isinstance(group_data, MutableMapping):
+ self.display.warning("Skipping '%s' as this is not a valid group definition" % group)
+ return
+
+ group = self.inventory.add_group(group)
+ if group_data is None:
+ return
+
+ for key, data in group_data.items():
+ if key == 'vars':
+ if not isinstance(data, MutableMapping):
+ raise AnsibleParserError(
+ 'Invalid "vars" entry for "%s" group, requires a dict, found "%s" instead.' %
+ (group, type(data))
+ )
+ for var, value in data.items():
+ self.inventory.set_variable(group, var, value)
+
+ elif key == 'children':
+ if not isinstance(data, MutableSequence):
+ raise AnsibleParserError(
+ 'Invalid "children" entry for "%s" group, requires a list, found "%s" instead.' %
+ (group, type(data))
+ )
+ for subgroup in data:
+ self._parse_group(subgroup, {})
+ self.inventory.add_child(group, subgroup)
+
+ elif key == 'hosts':
+ if not isinstance(data, MutableMapping):
+ raise AnsibleParserError(
+ 'Invalid "hosts" entry for "%s" group, requires a dict, found "%s" instead.' %
+ (group, type(data))
+ )
+ for host_pattern, value in data.items():
+ hosts, port = self._expand_hostpattern(host_pattern)
+ self._populate_host_vars(hosts, value, group, port)
+ else:
+ self.display.warning(
+ 'Skipping unexpected key "%s" in group "%s", only "vars", "children" and "hosts" are valid' %
+ (key, group)
+ )
+
+ def _load_file(self, file_name):
+ if not file_name or not isinstance(file_name, string_types):
+ raise AnsibleParserError("Invalid filename: '%s'" % to_native(file_name))
+
+ b_file_name = to_bytes(self.loader.path_dwim(file_name))
+ if not self.loader.path_exists(b_file_name):
+ raise AnsibleFileNotFound("Unable to retrieve file contents", file_name=file_name)
+
+ try:
+ (b_data, private) = self.loader._get_file_contents(file_name)
+ return toml_loads(to_text(b_data, errors='surrogate_or_strict'))
+ except TOMLDecodeError as e:
+ raise AnsibleParserError(
+ 'TOML file (%s) is invalid: %s' % (file_name, to_native(e)),
+ orig_exc=e
+ )
+ except (IOError, OSError) as e:
+ raise AnsibleParserError(
+ "An error occurred while trying to read the file '%s': %s" % (file_name, to_native(e)),
+ orig_exc=e
+ )
+ except Exception as e:
+ raise AnsibleParserError(
+ "An unexpected error occurred while parsing the file '%s': %s" % (file_name, to_native(e)),
+ orig_exc=e
+ )
+
+ def parse(self, inventory, loader, path, cache=True):
+ ''' parses the inventory file '''
+ if not HAS_TOMLLIB and not HAS_TOML:
+ # tomllib works here too, but we don't call it out in the error,
+ # since you either have it or not as part of cpython stdlib >= 3.11
+ raise AnsibleParserError(
+ 'The TOML inventory plugin requires the python "toml", or "tomli" library'
+ )
+
+ super(InventoryModule, self).parse(inventory, loader, path)
+ self.set_options()
+
+ try:
+ data = self._load_file(path)
+ except Exception as e:
+ raise AnsibleParserError(e)
+
+ if not data:
+ raise AnsibleParserError('Parsed empty TOML file')
+ elif data.get('plugin'):
+ raise AnsibleParserError('Plugin configuration TOML file, not TOML inventory')
+
+ for group_name in data:
+ self._parse_group(group_name, data[group_name])
+
+ def verify_file(self, path):
+ if super(InventoryModule, self).verify_file(path):
+ file_name, ext = os.path.splitext(path)
+ if ext == '.toml':
+ return True
+ return False