summaryrefslogtreecommitdiffstats
path: root/lib/ansible/plugins/inventory/constructed.py
blob: dd630c66cd26069415d202ca0109cf9f45fe399d (plain)
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
# 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 (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    name: constructed
    version_added: "2.4"
    short_description: Uses Jinja2 to construct vars and groups based on existing inventory.
    description:
        - Uses a YAML configuration file with a valid YAML or C(.config) extension to define var expressions and group conditionals
        - The Jinja2 conditionals that qualify a host for membership.
        - The Jinja2 expressions are calculated and assigned to the variables
        - Only variables already available from previous inventories or the fact cache can be used for templating.
        - When I(strict) is False, failed expressions will be ignored (assumes vars were missing).
    options:
        plugin:
            description: token that ensures this is a source file for the 'constructed' plugin.
            required: True
            choices: ['ansible.builtin.constructed', 'constructed']
        use_vars_plugins:
            description:
                - Normally, for performance reasons, vars plugins get executed after the inventory sources complete the base inventory,
                  this option allows for getting vars related to hosts/groups from those plugins.
                - The host_group_vars (enabled by default) 'vars plugin' is the one responsible for reading host_vars/ and group_vars/ directories.
                - This will execute all vars plugins, even those that are not supposed to execute at the 'inventory' stage.
                  See vars plugins docs for details on 'stage'.
            required: false
            default: false
            type: boolean
            version_added: '2.11'
    extends_documentation_fragment:
      - constructed
'''

EXAMPLES = r'''
    # inventory.config file in YAML format
    plugin: ansible.builtin.constructed
    strict: False
    compose:
        var_sum: var1 + var2

        # this variable will only be set if I have a persistent fact cache enabled (and have non expired facts)
        # `strict: False` will skip this instead of producing an error if it is missing facts.
        server_type: "ansible_hostname | regex_replace ('(.{6})(.{2}).*', '\\2')"
    groups:
        # simple name matching
        webservers: inventory_hostname.startswith('web')

        # using ec2 'tags' (assumes aws inventory)
        development: "'devel' in (ec2_tags|list)"

        # using other host properties populated in inventory
        private_only: not (public_dns_name is defined or ip_address is defined)

        # complex group membership
        multi_group: (group_names | intersect(['alpha', 'beta', 'omega'])) | length >= 2

    keyed_groups:
        # this creates a group per distro (distro_CentOS, distro_Debian) and assigns the hosts that have matching values to it,
        # using the default separator "_"
        - prefix: distro
          key: ansible_distribution

        # the following examples assume the first inventory is from the `aws_ec2` plugin
        # this creates a group per ec2 architecture and assign hosts to the matching ones (arch_x86_64, arch_sparc, etc)
        - prefix: arch
          key: architecture

        # this creates a group per ec2 region like "us_west_1"
        - prefix: ""
          separator: ""
          key: placement.region

        # this creates a common parent group for all ec2 availability zones
        - key: placement.availability_zone
          parent_group: all_ec2_zones
'''

import os

from ansible import constants as C
from ansible.errors import AnsibleParserError, AnsibleOptionsError
from ansible.inventory.helpers import get_group_vars
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.module_utils._text import to_native
from ansible.utils.vars import combine_vars
from ansible.vars.fact_cache import FactCache
from ansible.vars.plugins import get_vars_from_inventory_sources


class InventoryModule(BaseInventoryPlugin, Constructable):
    """ constructs groups and vars using Jinja2 template expressions """

    NAME = 'constructed'

    def __init__(self):

        super(InventoryModule, self).__init__()

        self._cache = FactCache()

    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 ['.config'] + C.YAML_FILENAME_EXTENSIONS:
                valid = True

        return valid

    def get_all_host_vars(self, host, loader, sources):
        ''' requires host object '''
        return combine_vars(self.host_groupvars(host, loader, sources), self.host_vars(host, loader, sources))

    def host_groupvars(self, host, loader, sources):
        ''' requires host object '''
        gvars = get_group_vars(host.get_groups())

        if self.get_option('use_vars_plugins'):
            gvars = combine_vars(gvars, get_vars_from_inventory_sources(loader, sources, host.get_groups(), 'all'))

        return gvars

    def host_vars(self, host, loader, sources):
        ''' requires host object '''
        hvars = host.get_vars()

        if self.get_option('use_vars_plugins'):
            hvars = combine_vars(hvars, get_vars_from_inventory_sources(loader, sources, [host], 'all'))

        return hvars

    def parse(self, inventory, loader, path, cache=False):
        ''' parses the inventory file '''

        super(InventoryModule, self).parse(inventory, loader, path, cache=cache)

        self._read_config_data(path)

        sources = []
        try:
            sources = inventory.processed_sources
        except AttributeError:
            if self.get_option('use_vars_plugins'):
                raise AnsibleOptionsError("The option use_vars_plugins requires ansible >= 2.11.")

        strict = self.get_option('strict')
        fact_cache = FactCache()
        try:
            # Go over hosts (less var copies)
            for host in inventory.hosts:

                # get available variables to templar
                hostvars = self.get_all_host_vars(inventory.hosts[host], loader, sources)
                if host in fact_cache:  # adds facts if cache is active
                    hostvars = combine_vars(hostvars, fact_cache[host])

                # create composite vars
                self._set_composite_vars(self.get_option('compose'), hostvars, host, strict=strict)

                # refetch host vars in case new ones have been created above
                hostvars = self.get_all_host_vars(inventory.hosts[host], loader, sources)
                if host in self._cache:  # adds facts if cache is active
                    hostvars = combine_vars(hostvars, self._cache[host])

                # constructed groups based on conditionals
                self._add_host_to_composed_groups(self.get_option('groups'), hostvars, host, strict=strict, fetch_hostvars=False)

                # constructed groups based variable values
                self._add_host_to_keyed_groups(self.get_option('keyed_groups'), hostvars, host, strict=strict, fetch_hostvars=False)

        except Exception as e:
            raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)), orig_exc=e)