1# Copyright (c) 2017 Ansible Project
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4from __future__ import (absolute_import, division, print_function)
5__metaclass__ = type
6
7DOCUMENTATION = '''
8    name: generator
9    plugin_type: inventory
10    version_added: "2.6"
11    short_description: Uses Jinja2 to construct hosts and groups from patterns
12    description:
13        - Uses a YAML configuration file with a valid YAML or C(.config) extension to define var expressions and group conditionals
14        - Create a template pattern that describes each host, and then use independent configuration layers
15        - Every element of every layer is combined to create a host for every layer combination
16        - Parent groups can be defined with reference to hosts and other groups using the same template variables
17    options:
18      plugin:
19         description: token that ensures this is a source file for the 'generator' plugin.
20         required: True
21         choices: ['generator']
22      hosts:
23        description:
24          - The C(name) key is a template used to generate
25            hostnames based on the C(layers) option. Each variable in the name is expanded to create a
26            cartesian product of all possible layer combinations.
27          - The C(parents) are a list of parent groups that the host belongs to. Each C(parent) item
28            contains a C(name) key, again expanded from the template, and an optional C(parents) key
29            that lists its parents.
30          - Parents can also contain C(vars), which is a dictionary of vars that
31            is then always set for that variable. This can provide easy access to the group name. E.g
32            set an C(application) variable that is set to the value of the C(application) layer name.
33      layers:
34        description:
35          - A dictionary of layers, with the key being the layer name, used as a variable name in the C(host)
36            C(name) and C(parents) keys. Each layer value is a list of possible values for that layer.
37'''
38
39EXAMPLES = '''
40    # inventory.config file in YAML format
41    plugin: generator
42    strict: False
43    hosts:
44        name: "{{ operation }}-{{ application }}-{{ environment }}-runner"
45        parents:
46          - name: "{{ operation }}-{{ application }}-{{ environment }}"
47            parents:
48              - name: "{{ operation }}-{{ application }}"
49                parents:
50                  - name: "{{ operation }}"
51                  - name: "{{ application }}"
52              - name: "{{ application }}-{{ environment }}"
53                parents:
54                  - name: "{{ application }}"
55                    vars:
56                      application: "{{ application }}"
57                  - name: "{{ environment }}"
58                    vars:
59                      environment: "{{ environment }}"
60          - name: runner
61    layers:
62        operation:
63            - build
64            - launch
65        environment:
66            - dev
67            - test
68            - prod
69        application:
70            - web
71            - api
72'''
73
74import os
75
76from itertools import product
77
78from ansible import constants as C
79from ansible.errors import AnsibleParserError
80from ansible.plugins.inventory import BaseInventoryPlugin
81
82
83class InventoryModule(BaseInventoryPlugin):
84    """ constructs groups and vars using Jinja2 template expressions """
85
86    NAME = 'generator'
87
88    def __init__(self):
89
90        super(InventoryModule, self).__init__()
91
92    def verify_file(self, path):
93
94        valid = False
95        if super(InventoryModule, self).verify_file(path):
96            file_name, ext = os.path.splitext(path)
97
98            if not ext or ext in ['.config'] + C.YAML_FILENAME_EXTENSIONS:
99                valid = True
100
101        return valid
102
103    def template(self, pattern, variables):
104        self.templar.available_variables = variables
105        return self.templar.do_template(pattern)
106
107    def add_parents(self, inventory, child, parents, template_vars):
108        for parent in parents:
109            try:
110                groupname = self.template(parent['name'], template_vars)
111            except (AttributeError, ValueError):
112                raise AnsibleParserError("Element %s has a parent with no name element" % child['name'])
113            if groupname not in inventory.groups:
114                inventory.add_group(groupname)
115            group = inventory.groups[groupname]
116            for (k, v) in parent.get('vars', {}).items():
117                group.set_variable(k, self.template(v, template_vars))
118            inventory.add_child(groupname, child)
119            self.add_parents(inventory, groupname, parent.get('parents', []), template_vars)
120
121    def parse(self, inventory, loader, path, cache=False):
122        ''' parses the inventory file '''
123
124        super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
125
126        config = self._read_config_data(path)
127
128        template_inputs = product(*config['layers'].values())
129        for item in template_inputs:
130            template_vars = dict()
131            for i, key in enumerate(config['layers'].keys()):
132                template_vars[key] = item[i]
133            host = self.template(config['hosts']['name'], template_vars)
134            inventory.add_host(host)
135            self.add_parents(inventory, host, config['hosts'].get('parents', []), template_vars)
136