allow for non standard hostnames

* Changed parse_addresses to throw exceptions instead of passing None
* Switched callers to trap and pass through the original values.
* Added very verbose notice
* Look at deprecating this and possibly validate at plugin instead
fixes #13608
This commit is contained in:
Brian Coca 2015-12-21 13:06:48 -05:00
parent 593d80c63d
commit 75e94e0cba
5 changed files with 49 additions and 29 deletions

View file

@ -109,7 +109,12 @@ class Inventory(object):
pass pass
elif isinstance(host_list, list): elif isinstance(host_list, list):
for h in host_list: for h in host_list:
(host, port) = parse_address(h, allow_ranges=False) try:
(host, port) = parse_address(h, allow_ranges=False)
except AnsibleError as e:
display.vvv("Unable to parse address from hostname, leaving unchanged: %s" % to_string(e))
host = h
port = None
all.add_host(Host(host, port)) all.add_host(Host(host, port))
elif self._loader.path_exists(host_list): 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' #TODO: switch this to a plugin loader and a 'condition' per plugin on which it should be tried, restoring 'inventory pllugins'
@ -228,15 +233,13 @@ class Inventory(object):
# If it doesn't, it could still be a single pattern. This accounts for # If it doesn't, it could still be a single pattern. This accounts for
# non-separator uses of colons: IPv6 addresses and [x:y] host ranges. # non-separator uses of colons: IPv6 addresses and [x:y] host ranges.
else: else:
(base, port) = parse_address(pattern, allow_ranges=True) try:
if base: (base, port) = parse_address(pattern, allow_ranges=True)
patterns = [pattern] patterns = [pattern]
except:
# The only other case we accept is a ':'-separated list of patterns. # The only other case we accept is a ':'-separated list of patterns.
# This mishandles IPv6 addresses, and is retained only for backwards # This mishandles IPv6 addresses, and is retained only for backwards
# compatibility. # compatibility.
else:
patterns = re.findall( patterns = re.findall(
r'''(?: # We want to match something comprising: r'''(?: # We want to match something comprising:
[^\s:\[\]] # (anything other than whitespace or ':[]' [^\s:\[\]] # (anything other than whitespace or ':[]'

View file

@ -23,7 +23,7 @@ import ast
import re import re
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError, AnsibleParserError
from ansible.inventory.host import Host from ansible.inventory.host import Host
from ansible.inventory.group import Group from ansible.inventory.group import Group
from ansible.inventory.expand_hosts import detect_range from ansible.inventory.expand_hosts import detect_range
@ -264,9 +264,12 @@ class InventoryParser(object):
# Can the given hostpattern be parsed as a host with an optional port # Can the given hostpattern be parsed as a host with an optional port
# specification? # specification?
(pattern, port) = parse_address(hostpattern, allow_ranges=True) try:
if not pattern: (pattern, port) = parse_address(hostpattern, allow_ranges=True)
self._raise_error("Can't parse '%s' as host[:port]" % hostpattern) except:
# not a recognizable host pattern
pattern = hostpattern
port = None
# Once we have separated the pattern, we expand it into list of one or # 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. # more hostnames, depending on whether it contains any [x:y] ranges.

View file

@ -20,6 +20,7 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import re import re
from ansible.errors import AnsibleParserError, AnsibleError
# Components that match a numeric or alphanumeric begin:end or begin:end:step # Components that match a numeric or alphanumeric begin:end or begin:end:step
# range expression inside square brackets. # range expression inside square brackets.
@ -162,6 +163,7 @@ patterns = {
$ $
'''.format(label=label), re.X|re.I|re.UNICODE '''.format(label=label), re.X|re.I|re.UNICODE
), ),
} }
def parse_address(address, allow_ranges=False): def parse_address(address, allow_ranges=False):
@ -183,8 +185,8 @@ def parse_address(address, allow_ranges=False):
# First, we extract the port number if one is specified. # First, we extract the port number if one is specified.
port = None port = None
for type in ['bracketed_hostport', 'hostport']: for matching in ['bracketed_hostport', 'hostport']:
m = patterns[type].match(address) m = patterns[matching].match(address)
if m: if m:
(address, port) = m.groups() (address, port) = m.groups()
port = int(port) port = int(port)
@ -194,22 +196,20 @@ def parse_address(address, allow_ranges=False):
# numeric ranges, or a hostname with alphanumeric ranges. # numeric ranges, or a hostname with alphanumeric ranges.
host = None host = None
for type in ['ipv4', 'ipv6', 'hostname']: for matching in ['ipv4', 'ipv6', 'hostname']:
m = patterns[type].match(address) m = patterns[matching].match(address)
if m: if m:
host = address host = address
continue continue
# If it isn't any of the above, we don't understand it. # If it isn't any of the above, we don't understand it.
if not host: if not host:
return (None, None) raise AnsibleError("Not a valid network hostname: %s" % address)
# If we get to this point, we know that any included ranges are valid. If
# the caller is prepared to handle them, all is well. Otherwise we treat
# it as a parse failure.
# If we get to this point, we know that any included ranges are valid.
# If the caller is prepared to handle them, all is well.
# Otherwise we treat it as a parse failure.
if not allow_ranges and '[' in host: if not allow_ranges and '[' in host:
return (None, None) raise AnsibleParserError("Detected range in host but was asked to ignore ranges")
return (host, port) return (host, port)

View file

@ -53,9 +53,13 @@ class ActionModule(ActionBase):
new_name = self._task.args.get('name', self._task.args.get('hostname', None)) new_name = self._task.args.get('name', self._task.args.get('hostname', None))
display.vv("creating host via 'add_host': hostname=%s" % new_name) display.vv("creating host via 'add_host': hostname=%s" % new_name)
name, port = parse_address(new_name, allow_ranges=False) try:
if not name: name, port = parse_address(new_name, allow_ranges=False)
raise AnsibleError("Invalid inventory hostname: %s" % new_name) except:
# not a parsable hostname, but might still be usable
name = new_name
port = None
if port: if port:
self._task.args['ansible_ssh_port'] = port self._task.args['ansible_ssh_port'] = port

View file

@ -71,7 +71,12 @@ class TestParseAddress(unittest.TestCase):
for t in self.tests: for t in self.tests:
test = self.tests[t] test = self.tests[t]
(host, port) = parse_address(t) try:
(host, port) = parse_address(t)
except:
host = None
port = None
assert host == test[0] assert host == test[0]
assert port == test[1] assert port == test[1]
@ -79,6 +84,11 @@ class TestParseAddress(unittest.TestCase):
for t in self.range_tests: for t in self.range_tests:
test = self.range_tests[t] test = self.range_tests[t]
(host, port) = parse_address(t, allow_ranges=True) try:
(host, port) = parse_address(t, allow_ranges=True)
except:
host = None
port = None
assert host == test[0] assert host == test[0]
assert port == test[1] assert port == test[1]