Merge branch 'amenonsen-hostrange' into devel

pull/12146/head
James Cammarata 9 years ago
commit 0880687f9d

@ -68,13 +68,19 @@ It's also ok to mix wildcard patterns and groups at the same time::
one*.com:dbservers
As an advanced usage, you can also select the numbered server in a group::
webservers[0]
You can select a host or subset of hosts from a group by their position. For example, given the following group::
Or a range of servers in a group::
[webservers]
cobweb
webbing
weber
webservers[0:25]
You can refer to hosts within the group by adding a subscript to the group name:
webservers[0] # == cobweb
webservers[-1] # == weber
webservers[0:1] # == webservers[0]:webservers[1]
# == cobweb:webbing
Most people don't specify patterns as regular expressions, but you can. Just start the pattern with a '~'::

@ -132,7 +132,6 @@ class Inventory(object):
for host in self.get_hosts():
host.vars = combine_vars(host.vars, self.get_host_variables(host.name))
def _match(self, str, pattern_str):
try:
if pattern_str.startswith('~'):
@ -205,6 +204,23 @@ class Inventory(object):
return hosts
def _split_pattern(self, pattern):
"""
takes e.g. "webservers[0:5]:dbservers:others"
and returns ["webservers[0:5]", "dbservers", "others"]
"""
term = re.compile(
r'''(?: # We want to match something comprising:
[^:\[\]] # (anything other than ':', '[', or ']'
| # ...or...
\[[^\]]*\] # a single complete bracketed expression)
)* # repeated as many times as possible
''', re.X
)
return [x for x in term.findall(pattern) if x]
def _evaluate_patterns(self, patterns):
"""
Takes a list of patterns and returns a list of matching host names,
@ -251,103 +267,125 @@ class Inventory(object):
def _match_one_pattern(self, pattern):
"""
Takes a single pattern (i.e., not "p1:p2") and returns a list of
matching hosts names. Does not take negatives or intersections
into account.
Takes a single pattern and returns a list of matching host names.
Ignores intersection (&) and exclusion (!) specifiers.
The pattern may be:
1. A regex starting with ~, e.g. '~[abc]*'
2. A shell glob pattern with ?/*/[chars]/[!chars], e.g. 'foo'
3. An ordinary word that matches itself only, e.g. 'foo'
The pattern is matched using the following rules:
1. If it's 'all', it matches all hosts in all groups.
2. Otherwise, for each known group name:
(a) if it matches the group name, the results include all hosts
in the group or any of its children.
(b) otherwise, if it matches any hosts in the group, the results
include the matching hosts.
This means that 'foo*' may match one or more groups (thus including all
hosts therein) but also hosts in other groups.
The built-in groups 'all' and 'ungrouped' are special. No pattern can
match these group names (though 'all' behaves as though it matches, as
described above). The word 'ungrouped' can match a host of that name,
and patterns like 'ungr*' and 'al*' can match either hosts or groups
other than all and ungrouped.
If the pattern matches one or more group names according to these rules,
it may have an optional range suffix to select a subset of the results.
This is allowed only if the pattern is not a regex, i.e. '~foo[1]' does
not work (the [1] is interpreted as part of the regex), but 'foo*[1]'
would work if 'foo*' matched the name of one or more groups.
Duplicate matches are always eliminated from the results.
"""
if pattern in self._pattern_cache:
return self._pattern_cache[pattern]
if pattern.startswith("&") or pattern.startswith("!"):
pattern = pattern[1:]
(name, enumeration_details) = self._enumeration_info(pattern)
hpat = self._hosts_in_unenumerated_pattern(name)
result = self._apply_ranges(pattern, hpat)
self._pattern_cache[pattern] = result
return result
if pattern not in self._pattern_cache:
(expr, slice) = self._split_subscript(pattern)
hosts = self._enumerate_matches(expr)
try:
hosts = self._apply_subscript(hosts, slice)
except IndexError:
raise AnsibleError("No hosts matched the subscripted pattern '%s'" % pattern)
self._pattern_cache[pattern] = hosts
def _enumeration_info(self, pattern):
return self._pattern_cache[pattern]
def _split_subscript(self, pattern):
"""
returns (pattern, limits) taking a regular pattern and finding out
which parts of it correspond to start/stop offsets. limits is
a tuple of (start, stop) or None
Takes a pattern, checks if it has a subscript, and returns the pattern
without the subscript and a (start,end) tuple representing the given
subscript (or None if there is no subscript).
Validates that the subscript is in the right syntax, but doesn't make
sure the actual indices make sense in context.
"""
# Do not parse regexes for enumeration info
if pattern.startswith('~'):
return (pattern, None)
# The regex used to match on the range, which can be [x] or [x-y].
pattern_re = re.compile("^(.*)\[([-]?[0-9]+)(?:(?:-)([0-9]+))?\](.*)$")
m = pattern_re.match(pattern)
# We want a pattern followed by an integer or range subscript.
# (We can't be more restrictive about the expression because the
# fnmatch semantics permit [\[:\]] to occur.)
pattern_with_subscript = re.compile(
r'''^
(.+) # A pattern expression ending with...
\[(?: # A [subscript] expression comprising:
(-?[0-9]+) # A single positive or negative number
| # Or a numeric range
([0-9]+)([:-])([0-9]+)
)\]
$
''', re.X
)
subscript = None
m = pattern_with_subscript.match(pattern)
if m:
(target, first, last, rest) = m.groups()
first = int(first)
if last:
if first < 0:
raise AnsibleError("invalid range: negative indices cannot be used as the first item in a range")
last = int(last)
(pattern, idx, start, sep, end) = m.groups()
if idx:
subscript = (int(idx), None)
else:
last = first
return (target, (first, last))
else:
return (pattern, None)
subscript = (int(start), int(end))
if sep == '-':
display.deprecated("Use [x:y] inclusive subscripts instead of [x-y]", version=2.0, removed=True)
def _apply_ranges(self, pat, hosts):
return (pattern, subscript)
def _apply_subscript(self, hosts, subscript):
"""
given a pattern like foo, that matches hosts, return all of hosts
given a pattern like foo[0:5], where foo matches hosts, return the first 6 hosts
Takes a list of hosts and a (start,end) tuple and returns the subset of
hosts based on the subscript (which may be None to return all hosts).
"""
# If there are no hosts to select from, just return the
# empty set. This prevents trying to do selections on an empty set.
# issue#6258
if not hosts:
if not hosts or not subscript:
return hosts
(loose_pattern, limits) = self._enumeration_info(pat)
if not limits:
return hosts
(start, end) = subscript
(left, right) = limits
if left == '':
left = 0
if right == '':
right = 0
left=int(left)
right=int(right)
try:
if left != right:
return hosts[left:right]
else:
return [ hosts[left] ]
except IndexError:
raise AnsibleError("no hosts matching the pattern '%s' were found" % pat)
def _create_implicit_localhost(self, pattern):
new_host = Host(pattern)
new_host.set_variable("ansible_python_interpreter", sys.executable)
new_host.set_variable("ansible_connection", "local")
new_host.ipv4_address = '127.0.0.1'
ungrouped = self.get_group("ungrouped")
if ungrouped is None:
self.add_group(Group('ungrouped'))
ungrouped = self.get_group('ungrouped')
self.get_group('all').add_child_group(ungrouped)
ungrouped.add_host(new_host)
return new_host
if end:
return hosts[start:end+1]
else:
return [ hosts[start] ]
def _hosts_in_unenumerated_pattern(self, pattern):
""" Get all host names matching the pattern """
def _enumerate_matches(self, pattern):
"""
Returns a list of host names matching the given pattern according to the
rules explained above in _match_one_pattern.
"""
results = []
hosts = []
hostnames = set()
# ignore any negative checks here, this is handled elsewhere
pattern = pattern.replace("!","").replace("&", "")
def __append_host_to_results(host):
if host.name not in hostnames:
hostnames.add(host.name)
@ -372,6 +410,20 @@ class Inventory(object):
results.append(new_host)
return results
def _create_implicit_localhost(self, pattern):
new_host = Host(pattern)
new_host.set_variable("ansible_python_interpreter", sys.executable)
new_host.set_variable("ansible_connection", "local")
new_host.ipv4_address = '127.0.0.1'
ungrouped = self.get_group("ungrouped")
if ungrouped is None:
self.add_group(Group('ungrouped'))
ungrouped = self.get_group('ungrouped')
self.get_group('all').add_child_group(ungrouped)
ungrouped.add_host(new_host)
return new_host
def clear_pattern_cache(self):
''' called exclusively by the add_host plugin to allow patterns to be recalculated '''
self._pattern_cache = {}

Loading…
Cancel
Save