Browse code

Merge branch 'hostpatterns' of https://github.com/amenonsen/ansible into amenonsen-hostpatterns

James Cammarata authored on 2015/09/18 02:31:51
Showing 6 changed files
... ...
@@ -22,6 +22,10 @@ Major Changes:
22 22
 They will retain the value of `None`. To go back to the old behaviour, you can override
23 23
 the `null_representation` setting to an empty string in your config file or by setting the
24 24
 `ANSIBLE_NULL_REPRESENTATION` environment variable.
25
+* Use "pattern1,pattern2" to combine host matching patterns. The use of
26
+  ':' as a separator is deprecated (accepted with a warning) because it
27
+  conflicts with IPv6 addresses. The undocumented use of ';' as a
28
+  separator is no longer supported.
25 29
 * Backslashes used when specifying parameters in jinja2 expressions in YAML
26 30
 dicts sometimes needed to be escaped twice. This has been fixed so that
27 31
 escaping once works. Here's an example of how playbooks need to be modified:
... ...
@@ -253,8 +257,6 @@ Minor changes:
253 253
 
254 254
 * Many more tests. The new API makes things more testable and we took advantage of it.
255 255
 * big_ip modules now support turning off ssl certificate validation (use only for self-signed certificates).
256
-* Use "pattern1:pattern2" to combine host matching patterns. The undocumented
257
-use of semicolons or commas to combine patterns is no longer supported.
258 256
 * Use ``hosts: groupname[x:y]`` to select a subset of hosts in a group; the
259 257
 ``[x-y]`` range syntax is no longer supported. Note that ``[0:1]`` matches
260 258
 two hosts, i.e. the range is inclusive of its endpoints.
... ...
@@ -27,7 +27,7 @@ The following patterns are equivalent and target all hosts in the inventory::
27 27
 It is also possible to address a specific host or set of hosts by name::
28 28
 
29 29
     one.example.com
30
-    one.example.com:two.example.com
30
+    one.example.com, two.example.com
31 31
     192.168.1.50
32 32
     192.168.1.*
33 33
 
... ...
@@ -35,20 +35,20 @@ The following patterns address one or more groups.  Groups separated by a colon
35 35
 This means the host may be in either one group or the other::
36 36
 
37 37
     webservers
38
-    webservers:dbservers
38
+    webservers,dbservers
39 39
 
40 40
 You can exclude groups as well, for instance, all machines must be in the group webservers but not in the group phoenix::
41 41
 
42
-    webservers:!phoenix
42
+    webservers,!phoenix
43 43
 
44 44
 You can also specify the intersection of two groups.  This would mean the hosts must be in the group webservers and
45 45
 the host must also be in the group staging::
46 46
 
47
-    webservers:&staging
47
+    webservers,&staging
48 48
 
49 49
 You can do combinations::
50 50
 
51
-    webservers:dbservers:&staging:!phoenix
51
+    webservers,dbservers,&staging,!phoenix
52 52
 
53 53
 The above configuration means "all machines in the groups 'webservers' and 'dbservers' are to be managed if they are in
54 54
 the group 'staging' also, but the machines are not to be managed if they are in the group 'phoenix' ... whew!
... ...
@@ -56,7 +56,7 @@ the group 'staging' also, but the machines are not to be managed if they are in
56 56
 You can also use variables if you want to pass some group specifiers via the "-e" argument to ansible-playbook, but this
57 57
 is uncommonly used::
58 58
 
59
-    webservers:!{{excluded}}:&{{required}}
59
+    webservers,!{{excluded}},&{{required}}
60 60
 
61 61
 You also don't have to manage by strictly defined groups.  Individual host names, IPs and groups, can also be referenced using
62 62
 wildcards::
... ...
@@ -66,7 +66,7 @@ wildcards::
66 66
 
67 67
 It's also ok to mix wildcard patterns and groups at the same time::
68 68
 
69
-    one*.com:dbservers
69
+    one*.com,dbservers
70 70
 
71 71
 You can select a host or subset of hosts from a group by their position. For example, given the following group::
72 72
 
... ...
@@ -24,6 +24,7 @@ import os
24 24
 import sys
25 25
 import re
26 26
 import stat
27
+import itertools
27 28
 
28 29
 from ansible import constants as C
29 30
 from ansible.errors import AnsibleError
... ...
@@ -149,23 +150,6 @@ class Inventory(object):
149 149
                 results.append(item)
150 150
         return results
151 151
 
152
-    def _split_pattern(self, pattern):
153
-        """
154
-        takes e.g. "webservers[0:5]:dbservers:others"
155
-        and returns ["webservers[0:5]", "dbservers", "others"]
156
-        """
157
-
158
-        term = re.compile(
159
-            r'''(?:             # We want to match something comprising:
160
-                    [^:\[\]]    # (anything other than ':', '[', or ']'
161
-                    |           # ...or...
162
-                    \[[^\]]*\]  # a single complete bracketed expression)
163
-                )*              # repeated as many times as possible
164
-            ''', re.X
165
-        )
166
-
167
-        return [x for x in term.findall(pattern) if x]
168
-
169 152
     def get_hosts(self, pattern="all", ignore_limits_and_restrictions=False):
170 153
         """ 
171 154
         Takes a pattern or list of patterns and returns a list of matching
... ...
@@ -173,14 +157,6 @@ class Inventory(object):
173 173
         or applied subsets
174 174
         """
175 175
 
176
-        # Enumerate all hosts matching the given pattern (which may be
177
-        # either a list of patterns or a string like 'pat1:pat2').
178
-        if isinstance(pattern, list):
179
-            pattern = ':'.join(pattern)
180
-
181
-        if ';' in pattern or ',' in pattern:
182
-            display.deprecated("Use ':' instead of ',' or ';' to separate host patterns", version=2.0, removed=True)
183
-
184 176
         patterns = self._split_pattern(pattern)
185 177
         hosts = self._evaluate_patterns(patterns)
186 178
 
... ...
@@ -197,6 +173,57 @@ class Inventory(object):
197 197
 
198 198
         return hosts
199 199
 
200
+    def _split_pattern(self, pattern):
201
+        """
202
+        Takes a string containing host patterns separated by commas (or a list
203
+        thereof) and returns a list of single patterns (which may not contain
204
+        commas). Whitespace is ignored.
205
+
206
+        Also accepts ':' as a separator for backwards compatibility, but it is
207
+        not recommended due to the conflict with IPv6 addresses and host ranges.
208
+
209
+        Example: 'a,b[1], c[2:3] , d' -> ['a', 'b[1]', 'c[2:3]', 'd']
210
+        """
211
+
212
+        if isinstance(pattern, list):
213
+            return list(itertools.chain(*map(self._split_pattern, pattern)))
214
+
215
+        if ';' in pattern:
216
+            display.deprecated("Use ',' instead of ':' or ';' to separate host patterns", version=2.0, removed=True)
217
+
218
+        # If it's got commas in it, we'll treat it as a straightforward
219
+        # comma-separated list of patterns.
220
+
221
+        elif ',' in pattern:
222
+            patterns = re.split('\s*,\s*', pattern)
223
+
224
+        # If it doesn't, it could still be a single pattern. This accounts for
225
+        # non-separator uses of colons: IPv6 addresses and [x:y] host ranges.
226
+
227
+        else:
228
+            (base, port) = parse_address(pattern, allow_ranges=True)
229
+            if base:
230
+                patterns = [pattern]
231
+
232
+            # The only other case we accept is a ':'-separated list of patterns.
233
+            # This mishandles IPv6 addresses, and is retained only for backwards
234
+            # compatibility.
235
+
236
+            else:
237
+                patterns = re.findall(
238
+                    r'''(?:             # We want to match something comprising:
239
+                            [^\s:\[\]]  # (anything other than whitespace or ':[]'
240
+                            |           # ...or...
241
+                            \[[^\]]*\]  # a single complete bracketed expression)
242
+                        )+              # occurring once or more
243
+                    ''', pattern, re.X
244
+                )
245
+
246
+                if len(patterns) > 1:
247
+                    display.deprecated("Use ',' instead of ':' or ';' to separate host patterns", version=2.0)
248
+
249
+        return [p.strip() for p in patterns]
250
+
200 251
     def _evaluate_patterns(self, patterns):
201 252
         """
202 253
         Takes a list of patterns and returns a list of matching host names,
... ...
@@ -249,7 +276,7 @@ class Inventory(object):
249 249
         The pattern may be:
250 250
 
251 251
             1. A regex starting with ~, e.g. '~[abc]*'
252
-            2. A shell glob pattern with ?/*/[chars]/[!chars], e.g. 'foo'
252
+            2. A shell glob pattern with ?/*/[chars]/[!chars], e.g. 'foo*'
253 253
             3. An ordinary word that matches itself only, e.g. 'foo'
254 254
 
255 255
         The pattern is matched using the following rules:
... ...
@@ -122,7 +122,7 @@ patterns = {
122 122
         r'''^
123 123
             (?:{0}:){{7}}{0}|           # uncompressed: 1:2:3:4:5:6:7:8
124 124
             (?:{0}:){{1,6}}:|           # compressed variants, which are all
125
-            (?:{0}:)(?:{0}){{1,6}}|     # a::b for various lengths of a,b
125
+            (?:{0}:)(?::{0}){{1,6}}|    # a::b for various lengths of a,b
126 126
             (?:{0}:){{2}}(?::{0}){{1,5}}|
127 127
             (?:{0}:){{3}}(?::{0}){{1,4}}|
128 128
             (?:{0}:){{4}}(?::{0}){{1,3}}|
129 129
new file mode 100644
... ...
@@ -0,0 +1,21 @@
0
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
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
+# Make coding more python3-ish
18
+from __future__ import (absolute_import, division, print_function)
19
+__metaclass__ = type
20
+
0 21
new file mode 100644
... ...
@@ -0,0 +1,69 @@
0
+# Copyright 2015 Abhijit Menon-Sen <ams@2ndQuadrant.com>
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
+# Make coding more python3-ish
18
+from __future__ import (absolute_import, division, print_function)
19
+__metaclass__ = type
20
+
21
+from ansible.compat.tests import unittest
22
+from ansible.compat.tests.mock import patch, MagicMock
23
+
24
+from ansible.errors import AnsibleError, AnsibleParserError
25
+from ansible.inventory import Inventory
26
+from ansible.vars import VariableManager
27
+
28
+from units.mock.loader import DictDataLoader
29
+
30
+class TestInventory(unittest.TestCase):
31
+
32
+    patterns = {
33
+        'a': ['a'],
34
+        'a, b': ['a', 'b'],
35
+        'a , b': ['a', 'b'],
36
+        ' a,b ,c[1:2] ': ['a', 'b', 'c[1:2]'],
37
+        '9a01:7f8:191:7701::9': ['9a01:7f8:191:7701::9'],
38
+        '9a01:7f8:191:7701::9,9a01:7f8:191:7701::9': ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9'],
39
+        '9a01:7f8:191:7701::9,9a01:7f8:191:7701::9,foo': ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9','foo'],
40
+        'foo[1:2]': ['foo[1:2]'],
41
+        'a::b': ['a::b'],
42
+        'a:b': ['a', 'b'],
43
+        ' a : b ': ['a', 'b'],
44
+        'foo:bar:baz[1:2]': ['foo', 'bar', 'baz[1:2]'],
45
+    }
46
+    pattern_lists = [
47
+        [['a'], ['a']],
48
+        [['a', 'b'], ['a', 'b']],
49
+        [['a, b'], ['a', 'b']],
50
+        [['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9,foo'],
51
+         ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9','foo']]
52
+    ]
53
+
54
+
55
+    def setUp(self):
56
+        v = VariableManager()
57
+        fake_loader = DictDataLoader({})
58
+
59
+        self.i = Inventory(loader=fake_loader, variable_manager=v, host_list='')
60
+
61
+    def test_split_patterns(self):
62
+
63
+        for p in self.patterns:
64
+            r = self.patterns[p]
65
+            self.assertEqual(r, self.i._split_pattern(p))
66
+
67
+        for p, r in self.pattern_lists:
68
+            self.assertEqual(r, self.i._split_pattern(p))