mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-26 23:00:36 +00:00
217 lines
8.0 KiB
Python
217 lines
8.0 KiB
Python
# Copyright 2015 Abhijit Menon-Sen <ams@2ndQuadrant.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# Make coding more python3-ish
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import re
|
|
from ansible.errors import AnsibleParserError, AnsibleError
|
|
|
|
# Components that match a numeric or alphanumeric begin:end or begin:end:step
|
|
# range expression inside square brackets.
|
|
|
|
numeric_range = r'''
|
|
\[
|
|
(?:[0-9]+:[0-9]+) # numeric begin:end
|
|
(?::[0-9]+)? # numeric :step (optional)
|
|
\]
|
|
'''
|
|
|
|
hexadecimal_range = r'''
|
|
\[
|
|
(?:[0-9a-f]+:[0-9a-f]+) # hexadecimal begin:end
|
|
(?::[0-9]+)? # numeric :step (optional)
|
|
\]
|
|
'''
|
|
|
|
alphanumeric_range = r'''
|
|
\[
|
|
(?:
|
|
[a-z]:[a-z]| # one-char alphabetic range
|
|
[0-9]+:[0-9]+ # ...or a numeric one
|
|
)
|
|
(?::[0-9]+)? # numeric :step (optional)
|
|
\]
|
|
'''
|
|
|
|
# Components that match a 16-bit portion of an IPv6 address in hexadecimal
|
|
# notation (0..ffff) or an 8-bit portion of an IPv4 address in decimal notation
|
|
# (0..255) or an [x:y(:z)] numeric range.
|
|
|
|
ipv6_component = r'''
|
|
(?:
|
|
[0-9a-f]{{1,4}}| # 0..ffff
|
|
{range} # or a numeric range
|
|
)
|
|
'''.format(range=hexadecimal_range)
|
|
|
|
ipv4_component = r'''
|
|
(?:
|
|
[01]?[0-9]{{1,2}}| # 0..199
|
|
2[0-4][0-9]| # 200..249
|
|
25[0-5]| # 250..255
|
|
{range} # or a numeric range
|
|
)
|
|
'''.format(range=numeric_range)
|
|
|
|
# A hostname label, e.g. 'foo' in 'foo.example.com'. Consists of alphanumeric
|
|
# characters plus dashes (and underscores) or valid ranges. The label may not
|
|
# start or end with a hyphen or an underscore. This is interpolated into the
|
|
# hostname pattern below. We don't try to enforce the 63-char length limit.
|
|
|
|
label = r'''
|
|
(?:[\w]|{range}) # Starts with an alphanumeric or a range
|
|
(?:[\w_-]|{range})* # Then zero or more of the same or [_-]
|
|
(?<![_-]) # ...as long as it didn't end with [_-]
|
|
'''.format(range=alphanumeric_range)
|
|
|
|
patterns = {
|
|
# This matches a square-bracketed expression with a port specification. What
|
|
# is inside the square brackets is validated later.
|
|
|
|
'bracketed_hostport': re.compile(
|
|
r'''^
|
|
\[(.+)\] # [host identifier]
|
|
:([0-9]+) # :port number
|
|
$
|
|
''', re.X
|
|
),
|
|
|
|
# This matches a bare IPv4 address or hostname (or host pattern including
|
|
# [x:y(:z)] ranges) with a port specification.
|
|
|
|
'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
|
|
),
|
|
|
|
# This matches an IPv4 address, but also permits range expressions.
|
|
|
|
'ipv4': re.compile(
|
|
r'''^
|
|
(?:{i4}\.){{3}}{i4} # Three parts followed by dots plus one
|
|
$
|
|
'''.format(i4=ipv4_component), re.X | re.I
|
|
),
|
|
|
|
# This matches an IPv6 address, but also permits range expressions.
|
|
#
|
|
# This expression looks complex, but it really only spells out the various
|
|
# combinations in which the basic unit of an IPv6 address (0..ffff) can be
|
|
# written, from :: to 1:2:3:4:5:6:7:8, plus the IPv4-in-IPv6 variants such
|
|
# as ::ffff:192.0.2.3.
|
|
#
|
|
# Note that we can't just use ipaddress.ip_address() because we also have to
|
|
# accept ranges in place of each component.
|
|
|
|
'ipv6': re.compile(
|
|
r'''^
|
|
(?:{0}:){{7}}{0}| # uncompressed: 1:2:3:4:5:6:7:8
|
|
(?:{0}:){{1,6}}:| # compressed variants, which are all
|
|
(?:{0}:)(?::{0}){{1,6}}| # a::b for various lengths of a,b
|
|
(?:{0}:){{2}}(?::{0}){{1,5}}|
|
|
(?:{0}:){{3}}(?::{0}){{1,4}}|
|
|
(?:{0}:){{4}}(?::{0}){{1,3}}|
|
|
(?:{0}:){{5}}(?::{0}){{1,2}}|
|
|
(?:{0}:){{6}}(?::{0})| # ...all with 2 <= a+b <= 7
|
|
:(?::{0}){{1,6}}| # ::ffff(:ffff...)
|
|
{0}?::| # ffff::, ::
|
|
# ipv4-in-ipv6 variants
|
|
(?:0:){{6}}(?:{0}\.){{3}}{0}|
|
|
::(?:ffff:)?(?:{0}\.){{3}}{0}|
|
|
(?:0:){{5}}ffff:(?:{0}\.){{3}}{0}
|
|
$
|
|
'''.format(ipv6_component), re.X | re.I
|
|
),
|
|
|
|
# This matches a hostname or host pattern including [x:y(:z)] ranges.
|
|
#
|
|
# We roughly follow DNS rules here, but also allow ranges (and underscores).
|
|
# In the past, no systematic rules were enforced about inventory hostnames,
|
|
# but the parsing context (e.g. shlex.split(), fnmatch.fnmatch()) excluded
|
|
# various metacharacters anyway.
|
|
#
|
|
# We don't enforce DNS length restrictions here (63 characters per label,
|
|
# 253 characters total) or make any attempt to process IDNs.
|
|
|
|
'hostname': re.compile(
|
|
r'''^
|
|
{label} # We must have at least one label
|
|
(?:\.{label})* # Followed by zero or more .labels
|
|
$
|
|
'''.format(label=label), re.X | re.I | re.UNICODE
|
|
),
|
|
|
|
}
|
|
|
|
|
|
def parse_address(address, allow_ranges=False):
|
|
"""
|
|
Takes a string and returns a (host, port) tuple. If the host is None, then
|
|
the string could not be parsed as a host identifier with an optional port
|
|
specification. If the port is None, then no port was specified.
|
|
|
|
The host identifier may be a hostname (qualified or not), an IPv4 address,
|
|
or an IPv6 address. If allow_ranges is True, then any of those may contain
|
|
[x:y] range specifications, e.g. foo[1:3] or foo[0:5]-bar[x-z].
|
|
|
|
The port number is an optional :NN suffix on an IPv4 address or host name,
|
|
or a mandatory :NN suffix on any square-bracketed expression: IPv6 address,
|
|
IPv4 address, or host name. (This means the only way to specify a port for
|
|
an IPv6 address is to enclose it in square brackets.)
|
|
"""
|
|
|
|
# First, we extract the port number if one is specified.
|
|
|
|
port = None
|
|
for matching in ['bracketed_hostport', 'hostport']:
|
|
m = patterns[matching].match(address)
|
|
if m:
|
|
(address, port) = m.groups()
|
|
port = int(port)
|
|
continue
|
|
|
|
# What we're left with now must be an IPv4 or IPv6 address, possibly with
|
|
# numeric ranges, or a hostname with alphanumeric ranges.
|
|
|
|
host = None
|
|
for matching in ['ipv4', 'ipv6', 'hostname']:
|
|
m = patterns[matching].match(address)
|
|
if m:
|
|
host = address
|
|
continue
|
|
|
|
# If it isn't any of the above, we don't understand it.
|
|
if not host:
|
|
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 not allow_ranges and '[' in host:
|
|
raise AnsibleParserError("Detected range in host but was asked to ignore ranges")
|
|
|
|
return (host, port)
|