... | ... |
@@ -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)) |