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    # remember to enable this inventory plugin in the ansible.cfg before using
42    # View the output using `ansible-inventory -i inventory.config --list`
43    plugin: generator
44    hosts:
45        name: "{{ operation }}_{{ application }}_{{ environment }}_runner"
46        parents:
47          - name: "{{ operation }}_{{ application }}_{{ environment }}"
48            parents:
49              - name: "{{ operation }}_{{ application }}"
50                parents:
51                  - name: "{{ operation }}"
52                  - name: "{{ application }}"
53              - name: "{{ application }}_{{ environment }}"
54                parents:
55                  - name: "{{ application }}"
56                    vars:
57                      application: "{{ application }}"
58                  - name: "{{ environment }}"
59                    vars:
60                      environment: "{{ environment }}"
61          - name: runner
62    layers:
63        operation:
64            - build
65            - launch
66        environment:
67            - dev
68            - test
69            - prod
70        application:
71            - web
72            - api
73'''
74
75import os
76
77from itertools import product
78
79from ansible import constants as C
80from ansible.errors import AnsibleParserError
81from ansible.plugins.inventory import BaseInventoryPlugin
82
83
84class InventoryModule(BaseInventoryPlugin):
85    """ constructs groups and vars using Jinja2 template expressions """
86
87    NAME = 'generator'
88
89    def __init__(self):
90
91        super(InventoryModule, self).__init__()
92
93    def verify_file(self, path):
94
95        valid = False
96        if super(InventoryModule, self).verify_file(path):
97            file_name, ext = os.path.splitext(path)
98
99            if not ext or ext in ['.config'] + C.YAML_FILENAME_EXTENSIONS:
100                valid = True
101
102        return valid
103
104    def template(self, pattern, variables):
105        self.templar.available_variables = variables
106        return self.templar.do_template(pattern)
107
108    def add_parents(self, inventory, child, parents, template_vars):
109        for parent in parents:
110            try:
111                groupname = self.template(parent['name'], template_vars)
112            except (AttributeError, ValueError):
113                raise AnsibleParserError("Element %s has a parent with no name element" % child['name'])
114            if groupname not in inventory.groups:
115                inventory.add_group(groupname)
116            group = inventory.groups[groupname]
117            for (k, v) in parent.get('vars', {}).items():
118                group.set_variable(k, self.template(v, template_vars))
119            inventory.add_child(groupname, child)
120            self.add_parents(inventory, groupname, parent.get('parents', []), template_vars)
121
122    def parse(self, inventory, loader, path, cache=False):
123        ''' parses the inventory file '''
124
125        super(InventoryModule, self).parse(inventory, loader, path, cache=cache)
126
127        config = self._read_config_data(path)
128
129        template_inputs = product(*config['layers'].values())
130        for item in template_inputs:
131            template_vars = dict()
132            for i, key in enumerate(config['layers'].keys()):
133                template_vars[key] = item[i]
134            host = self.template(config['hosts']['name'], template_vars)
135            inventory.add_host(host)
136            self.add_parents(inventory, host, config['hosts'].get('parents', []), template_vars)
137