Be systematic about parsing and validating hostnames and addresses

This adds a parse_address(pattern) utility function that returns
(host,port), and uses it wherever where we accept IPv4 and IPv6
addresses and hostnames (or host patterns): the inventory parser
the the add_host action plugin.

It also introduces a more extensive set of unit tests that supersedes
the old add_host unit tests (which didn't actually test add_host, but
only the parsing function).
This commit is contained in:
Abhijit Menon-Sen
2015-08-31 10:37:09 +05:30
parent 49803509b4
commit 065bb52109
7 changed files with 283 additions and 139 deletions

View File

@@ -33,6 +33,7 @@ from ansible.inventory.group import Group
from ansible.inventory.host import Host
from ansible.plugins import vars_loader
from ansible.utils.vars import combine_vars
from ansible.parsing.utils.addresses import parse_address
try:
from __main__ import display
@@ -83,27 +84,16 @@ class Inventory(object):
host_list = host_list.split(",")
host_list = [ h for h in host_list if h and h.strip() ]
self.parser = None
if host_list is None:
self.parser = None
pass
elif isinstance(host_list, list):
self.parser = None
all = Group('all')
self.groups = [ all ]
ipv6_re = re.compile('\[([a-f:A-F0-9]*[%[0-z]+]?)\](?::(\d+))?')
for x in host_list:
m = ipv6_re.match(x)
if m:
all.add_host(Host(m.groups()[0], m.groups()[1]))
else:
if ":" in x:
tokens = x.rsplit(":", 1)
# if there is ':' in the address, then this is an ipv6
if ':' in tokens[0]:
all.add_host(Host(x))
else:
all.add_host(Host(tokens[0], tokens[1]))
else:
all.add_host(Host(x))
for h in host_list:
(host, port) = parse_address(h, allow_ranges=False)
all.add_host(Host(host, port))
elif self._loader.path_exists(host_list):
#TODO: switch this to a plugin loader and a 'condition' per plugin on which it should be tried, restoring 'inventory pllugins'
if self._loader.is_directory(host_list):

View File

@@ -44,7 +44,7 @@ def detect_range(line = None):
Returnes True if the given line contains a pattern, else False.
'''
if 0 <= line.find("[") < line.find(":") < line.find("]"):
if '[' in line:
return True
else:
return False

View File

@@ -29,6 +29,7 @@ from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.inventory.expand_hosts import detect_range
from ansible.inventory.expand_hosts import expand_hostname_range
from ansible.parsing.utils.addresses import parse_address
from ansible.utils.unicode import to_unicode, to_bytes
class InventoryParser(object):
@@ -265,30 +266,20 @@ class InventoryParser(object):
optional port number that applies to all of them.
'''
# Is a port number specified?
#
# This may be a mandatory :NN suffix on any square-bracketed expression
# (IPv6 address, IPv4 address, host name, host pattern), or an optional
# :NN suffix on an IPv4 address, host name, or pattern. IPv6 addresses
# must be in square brackets if a port is specified.
# Can the given hostpattern be parsed as a host with an optional port
# specification?
port = None
(pattern, port) = parse_address(hostpattern, allow_ranges=True)
if not pattern:
self._raise_error("Can't parse '%s' as host[:port]" % hostpattern)
for type in ['bracketed_hostport', 'hostport']:
m = self.patterns[type].match(hostpattern)
if m:
(hostpattern, port) = m.groups()
continue
# Once we have separated the pattern, we expand it into list of one or
# more hostnames, depending on whether it contains any [x:y] ranges.
# Now we're left with just the pattern, which results in a list of one
# or more hostnames, depending on whether it contains any [x:y] ranges.
#
# FIXME: We could be more strict here about validation.
if detect_range(hostpattern):
hostnames = expand_hostname_range(hostpattern)
if detect_range(pattern):
hostnames = expand_hostname_range(pattern)
else:
hostnames = [hostpattern]
hostnames = [pattern]
return (hostnames, port)
@@ -374,29 +365,3 @@ class InventoryParser(object):
$ # end of the line
''', re.X
)
# The following patterns match the various ways in which a port number
# may be specified on an IPv6 address, IPv4 address, hostname, or host
# pattern. All of the above may be enclosed in square brackets with a
# mandatory :NN suffix; or all but the first may be given without any
# brackets but with an :NN suffix.
self.patterns['bracketed_hostport'] = re.compile(
r'''^
\[(.+)\] # [host identifier]
:([0-9]+) # :port number
$
''', re.X
)
self.patterns['hostport'] = re.compile(
r'''^
((?: # We want to match:
[^:\[\]] # (a non-range character
| # ...or...
\[[^\]]*\] # a complete bracketed expression)
)*) # repeated as many times as possible
:([0-9]+) # followed by a port number
$
''', re.X
)