Browse code

draft add group merge priority and yaml inventory

* now you can specify a yaml invenotry file

* ansible_group_priority will now set this property on groups

* added example yaml inventory

* TODO: make group var merging depend on priority

groups, child/parent relationships should remain unchanged.

Brian Coca authored on 2016/04/08 05:22:36
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,43 @@
0
+# This is the default ansible 'hosts' file.
1
+#
2
+# It should live in /etc/ansible/hosts
3
+#
4
+#   - Comments begin with the '#' character
5
+#   - Blank lines are ignored
6
+#   - Top level entries are assumed to be groups
7
+#   - Hosts must be specified in a group's hosts:
8
+#     and they must be a key (: terminated)
9
+#   - groups can have children, hosts and vars keys
10
+#   - Anything defined under a hosts is assumed to be a var
11
+#   - You can enter hostnames or ip addresses
12
+#   - A hostname/ip can be a member of multiple groups
13
+# Ex 1: Ungrouped hosts, put in 'ungrouped' group
14
+##ungrouped:
15
+##  hosts:
16
+##      green.example.com:
17
+##          ansible_ssh_host: 191.168.100.32
18
+##      blue.example.com:
19
+##      192.168.100.1:
20
+##      192.168.100.10:
21
+
22
+# Ex 2: A collection of hosts belonging to the 'webservers' group
23
+
24
+##webservers:
25
+##  hosts:
26
+##      alpha.example.org:
27
+##      beta.example.org:
28
+##      192.168.1.100:
29
+##      192.168.1.110:
30
+
31
+# Ex 3: You can create hosts using ranges and add children groups and vars to a group
32
+# The child group can define anything you would normall add to a group
33
+
34
+##testing:
35
+##  hosts:
36
+##      www[001:006].example.com:
37
+##  vars:
38
+##      testing1: value1
39
+##  children:
40
+##      webservers:
41
+##          hosts:
42
+##              beta.example.org:
... ...
@@ -103,7 +103,7 @@ class Inventory(object):
103 103
         # Always create the 'all' and 'ungrouped' groups, even if host_list is
104 104
         # empty: in this case we will subsequently an the implicit 'localhost' to it.
105 105
 
106
-        ungrouped = Group(name='ungrouped')
106
+        ungrouped = Group('ungrouped')
107 107
         all = Group('all')
108 108
         all.add_child_group(ungrouped)
109 109
 
... ...
@@ -138,11 +138,12 @@ class Inventory(object):
138 138
 
139 139
         self._vars_plugins = [ x for x in vars_loader.all(self) ]
140 140
 
141
-        # get group vars from group_vars/ files and vars plugins
142
-        for group in self.groups.values():
141
+        # set group vars from group_vars/ files and vars plugins
142
+        for g in self.groups:
143
+            group = self.groups[g]
143 144
             group.vars = combine_vars(group.vars, self.get_group_variables(group.name))
144 145
 
145
-        # get host vars from host_vars/ files and vars plugins
146
+        # set host vars from host_vars/ files and vars plugins
146 147
         for host in self.get_hosts():
147 148
             host.vars = combine_vars(host.vars, self.get_host_variables(host.name))
148 149
 
... ...
@@ -24,12 +24,11 @@ import os
24 24
 
25 25
 from ansible import constants as C
26 26
 from ansible.errors import AnsibleError
27
-
28
-from ansible.inventory.host import Host
29
-from ansible.inventory.group import Group
30 27
 from ansible.utils.vars import combine_vars
31 28
 
29
+#FIXME: make into plugins
32 30
 from ansible.inventory.ini import InventoryParser as InventoryINIParser
31
+from ansible.inventory.yaml import InventoryParser as InventoryYAMLParser
33 32
 from ansible.inventory.script import InventoryScript
34 33
 
35 34
 __all__ = ['get_file_parser']
... ...
@@ -53,6 +52,8 @@ def get_file_parser(hostsfile, groups, loader):
53 53
     except:
54 54
         pass
55 55
 
56
+    #FIXME: make this 'plugin loop'
57
+    # script
56 58
     if loader.is_executable(hostsfile):
57 59
         try:
58 60
             parser = InventoryScript(loader=loader, groups=groups, filename=hostsfile)
... ...
@@ -62,6 +63,19 @@ def get_file_parser(hostsfile, groups, loader):
62 62
                             "If this is not supposed to be an executable script, correct this with `chmod -x %s`." % hostsfile)
63 63
             myerr.append(str(e))
64 64
 
65
+    # YAML/JSON
66
+    if not processed and os.path.splitext(hostsfile)[-1] in C.YAML_FILENAME_EXTENSIONS:
67
+        try:
68
+            parser = InventoryYAMLParser(loader=loader, groups=groups, filename=hostsfile)
69
+            processed = True
70
+        except Exception as e:
71
+            if shebang_present and not loader.is_executable(hostsfile):
72
+                myerr.append("The file %s looks like it should be an executable inventory script, but is not marked executable. " % hostsfile + \
73
+                              "Perhaps you want to correct this with `chmod +x %s`?" % hostsfile)
74
+            else:
75
+                myerr.append(str(e))
76
+
77
+    # ini
65 78
     if not processed:
66 79
         try:
67 80
             parser = InventoryINIParser(loader=loader, groups=groups, filename=hostsfile)
... ...
@@ -18,7 +18,6 @@ from __future__ import (absolute_import, division, print_function)
18 18
 __metaclass__ = type
19 19
 
20 20
 from ansible.errors import AnsibleError
21
-from ansible.utils.debug import debug
22 21
 
23 22
 class Group:
24 23
     ''' a group of ansible hosts '''
... ...
@@ -34,6 +33,7 @@ class Group:
34 34
         self.child_groups = []
35 35
         self.parent_groups = []
36 36
         self._hosts_cache = None
37
+        self.priority = 1
37 38
 
38 39
         #self.clear_hosts_cache()
39 40
         #if self.name is None:
... ...
@@ -162,3 +162,10 @@ class Group:
162 162
 
163 163
         return self._get_ancestors().values()
164 164
 
165
+    def set_priority(self, priority):
166
+        try:
167
+            self.priority = int(priority)
168
+        except TypeError:
169
+            #FIXME: warn about invalid priority
170
+            pass
171
+
... ...
@@ -143,7 +143,10 @@ class InventoryParser(object):
143 143
             # applied to the current group.
144 144
             elif state == 'vars':
145 145
                 (k, v) = self._parse_variable_definition(line)
146
-                self.groups[groupname].set_variable(k, v)
146
+                if k != 'ansible_group_priority':
147
+                    self.groups[groupname].set_variable(k, v)
148
+                else:
149
+                    self.groups[groupname].set_priority(v)
147 150
 
148 151
             # [groupname:children] contains subgroup names that must be
149 152
             # added as children of the current group. The subgroup names
150 153
new file mode 100644
... ...
@@ -0,0 +1,200 @@
0
+# Copyright 2016 RedHat, inc
1
+#
2
+# This file is part of Ansible
3
+#
4
+# Ansible is free software: you can redistribute it and/or modify
5
+# it under the terms of the GNU General Public License as published by
6
+# the Free Software Foundation, either version 3 of the License, or
7
+# (at your option) any later version.
8
+#
9
+# Ansible is distributed in the hope that it will be useful,
10
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
+# GNU General Public License for more details.
13
+#
14
+# You should have received a copy of the GNU General Public License
15
+# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
16
+
17
+#############################################
18
+from __future__ import (absolute_import, division, print_function)
19
+__metaclass__ = type
20
+
21
+import re
22
+
23
+from ansible import constants as C
24
+from ansible.inventory.host import Host
25
+from ansible.inventory.group import Group
26
+from ansible.inventory.expand_hosts import detect_range
27
+from ansible.inventory.expand_hosts import expand_hostname_range
28
+from ansible.parsing.utils.addresses import parse_address
29
+
30
+class InventoryParser(object):
31
+    """
32
+    Takes an INI-format inventory file and builds a list of groups and subgroups
33
+    with their associated hosts and variable settings.
34
+    """
35
+
36
+    def __init__(self, loader, groups, filename=C.DEFAULT_HOST_LIST):
37
+        self._loader = loader
38
+        self.filename = filename
39
+
40
+        # Start with an empty host list and whatever groups we're passed in
41
+        # (which should include the default 'all' and 'ungrouped' groups).
42
+
43
+        self.hosts = {}
44
+        self.patterns = {}
45
+        self.groups = groups
46
+
47
+        # Read in the hosts, groups, and variables defined in the
48
+        # inventory file.
49
+        data = loader.load_from_file(filename)
50
+
51
+        self._parse(data)
52
+
53
+    def _parse(self, data):
54
+        '''
55
+        Populates self.groups from the given array of lines. Raises an error on
56
+        any parse failure.
57
+        '''
58
+
59
+        self._compile_patterns()
60
+
61
+        # We expect top level keys to correspond to groups, iterate over them
62
+        # to get host, vars and subgroups (which we iterate over recursivelly)
63
+        for group_name in data.keys():
64
+            self._parse_groups(group_name, data[group_name])
65
+
66
+        # Finally, add all top-level groups as children of 'all'.
67
+        # We exclude ungrouped here because it was already added as a child of
68
+        # 'all' at the time it was created.
69
+        for group in self.groups.values():
70
+            if group.depth == 0 and group.name not in ('all', 'ungrouped'):
71
+                self.groups['all'].add_child_group(Group(group_name))
72
+
73
+    def _parse_groups(self, group, group_data):
74
+
75
+        if group not in self.groups:
76
+            self.groups[group] = Group(name=group)
77
+
78
+        if isinstance(group_data, dict):
79
+            if 'vars' in group_data:
80
+                for var in group_data['vars']:
81
+                    if var != 'ansible_group_priority':
82
+                        self.groups[group].set_variable(var, group_data['vars'][var])
83
+                    else:
84
+                        self.groups[group].set_priority(group_data['vars'][var])
85
+
86
+            if 'children' in group_data:
87
+                for subgroup in group_data['children']:
88
+                    self._parse_groups(subgroup, group_data['children'][subgroup])
89
+                    self.groups[group].add_child_group(self.groups[subgroup])
90
+
91
+            if 'hosts' in group_data:
92
+                for host_pattern in group_data['hosts']:
93
+                    hosts = self._parse_host(host_pattern, group_data['hosts'][host_pattern])
94
+                    for h in hosts:
95
+                        self.groups[group].add_host(h)
96
+
97
+
98
+    def _parse_host(self, host_pattern, host_data):
99
+        '''
100
+        Each host key can be a pattern, try to process it and add variables as needed
101
+        '''
102
+        (hostnames, port) = self._expand_hostpattern(host_pattern)
103
+        hosts = self._Hosts(hostnames, port)
104
+
105
+        if isinstance(host_data, dict):
106
+            for k in host_data:
107
+                for h in hosts:
108
+                    h.set_variable(k, host_data[k])
109
+                    if k in ['ansible_host', 'ansible_ssh_host']:
110
+                        h.address = host_data[k]
111
+        return hosts
112
+
113
+    def _expand_hostpattern(self, hostpattern):
114
+        '''
115
+        Takes a single host pattern and returns a list of hostnames and an
116
+        optional port number that applies to all of them.
117
+        '''
118
+
119
+        # Can the given hostpattern be parsed as a host with an optional port
120
+        # specification?
121
+
122
+        try:
123
+            (pattern, port) = parse_address(hostpattern, allow_ranges=True)
124
+        except:
125
+            # not a recognizable host pattern
126
+            pattern = hostpattern
127
+            port = None
128
+
129
+        # Once we have separated the pattern, we expand it into list of one or
130
+        # more hostnames, depending on whether it contains any [x:y] ranges.
131
+
132
+        if detect_range(pattern):
133
+            hostnames = expand_hostname_range(pattern)
134
+        else:
135
+            hostnames = [pattern]
136
+
137
+        return (hostnames, port)
138
+
139
+    def _Hosts(self, hostnames, port):
140
+        '''
141
+        Takes a list of hostnames and a port (which may be None) and returns a
142
+        list of Hosts (without recreating anything in self.hosts).
143
+        '''
144
+
145
+        hosts = []
146
+
147
+        # Note that we decide whether or not to create a Host based solely on
148
+        # the (non-)existence of its hostname in self.hosts. This means that one
149
+        # cannot add both "foo:22" and "foo:23" to the inventory.
150
+
151
+        for hn in hostnames:
152
+            if hn not in self.hosts:
153
+                self.hosts[hn] = Host(name=hn, port=port)
154
+            hosts.append(self.hosts[hn])
155
+
156
+        return hosts
157
+
158
+    def get_host_variables(self, host):
159
+        return {}
160
+
161
+    def _compile_patterns(self):
162
+        '''
163
+        Compiles the regular expressions required to parse the inventory and
164
+        stores them in self.patterns.
165
+        '''
166
+
167
+        # Section names are square-bracketed expressions at the beginning of a
168
+        # line, comprising (1) a group name optionally followed by (2) a tag
169
+        # that specifies the contents of the section. We ignore any trailing
170
+        # whitespace and/or comments. For example:
171
+        #
172
+        # [groupname]
173
+        # [somegroup:vars]
174
+        # [naughty:children] # only get coal in their stockings
175
+
176
+        self.patterns['section'] = re.compile(
177
+            r'''^\[
178
+                    ([^:\]\s]+)             # group name (see groupname below)
179
+                    (?::(\w+))?             # optional : and tag name
180
+                \]
181
+                \s*                         # ignore trailing whitespace
182
+                (?:\#.*)?                   # and/or a comment till the
183
+                $                           # end of the line
184
+            ''', re.X
185
+        )
186
+
187
+        # FIXME: What are the real restrictions on group names, or rather, what
188
+        # should they be? At the moment, they must be non-empty sequences of non
189
+        # whitespace characters excluding ':' and ']', but we should define more
190
+        # precise rules in order to support better diagnostics.
191
+
192
+        self.patterns['groupname'] = re.compile(
193
+            r'''^
194
+                ([^:\]\s]+)
195
+                \s*                         # ignore trailing whitespace
196
+                (?:\#.*)?                   # and/or a comment till the
197
+                $                           # end of the line
198
+            ''', re.X
199
+        )