Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #49754 minion zmq connecting to master configured explicitly as IPv6 address or IPv6 only master #49755

Merged
merged 47 commits into from
Jan 15, 2019
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4221e91
fix minion zmq connecting to master configured as IPv6 address
aphor Sep 24, 2018
d95a5fe
coverage for master host:port ipv4 and ipv6 config value support
aphor Dec 31, 2018
0a5cb60
fix parse_host_port() bad exception on hostname only arg
aphor Dec 31, 2018
947b54a
fix parse_host_port() parse error on hostname only arg
aphor Jan 1, 2019
53fdad3
fix parse_host_port() is_ip call
aphor Jan 1, 2019
3a993a6
avoid error on (redundant) is_ip check
aphor Jan 2, 2019
dde0677
avoid error on (redundant) is_ip check
aphor Jan 2, 2019
63f7474
pylint E741 triggers error in salt-pylint E8741 (unhandled)
aphor Jan 2, 2019
7b01545
fix minion zmq connecting to master configured as IPv6 address
aphor Sep 24, 2018
c3e4e24
coverage for master host:port ipv4 and ipv6 config value support
aphor Dec 31, 2018
9087af5
fix parse_host_port() parse error on hostname only arg
aphor Jan 1, 2019
4979812
debug ipaddress.ip_address TypeError
aphor Jan 4, 2019
194fdce
debug ipaddress.ip_address TypeError
aphor Jan 4, 2019
348ad98
fix indentation doh
aphor Jan 4, 2019
5163da4
fix horrible mistakes
aphor Jan 4, 2019
5ae668a
lint pep8
aphor Jan 5, 2019
efe0722
fix test setup
aphor Jan 5, 2019
0808956
fixup rebase merge goof
aphor Jan 5, 2019
0457cd7
test assertion correction
aphor Jan 5, 2019
883a089
fix good_host_ports iteration
aphor Jan 6, 2019
d7137ea
don't test invalid combination
aphor Jan 6, 2019
04492fd
lint pep8 whitespace
aphor Jan 6, 2019
8ada237
clear up lint, disambiguation
aphor Jan 6, 2019
c56abfb
typo in ipaddress.IPv6Address()
aphor Jan 6, 2019
630d145
checking for truthiness better than len
aphor Jan 6, 2019
0499280
remove bad extra test assertion inside exception handler
aphor Jan 8, 2019
479f37b
don't try to handle/log test exception
aphor Jan 8, 2019
65b99f5
minimize diff
aphor Jan 8, 2019
d5246d7
parsing logic error
aphor Jan 8, 2019
6b76754
wordsmithing
aphor Jan 8, 2019
68d405d
try to debug error in tests (revert me)
aphor Jan 8, 2019
016f4ea
avoid TypeError by not constructing an ip_address from an ip_address
aphor Jan 9, 2019
1287fd1
ip_address does not need str() and parse_host_port handles ipv6 or ipv4
aphor Jan 9, 2019
b4f9b44
Need more detail on test failure
aphor Jan 9, 2019
6878de0
expose error in tests
aphor Jan 8, 2019
3550b8f
Don't ip_bracket addresses returned by check_dns.
aphor Jan 10, 2019
ec1f7f3
syntax error bad parentheses
aphor Jan 10, 2019
ef859b7
socket.connect needs a tuple
aphor Jan 11, 2019
e9415d6
Merge branch '2018.3' into issues/49754
aphor Jan 11, 2019
0af7653
backport test improvements from develop
aphor Jan 11, 2019
f02625e
Merge branch 'issues/49754' of github.com:aphor/salt into issues/49754
aphor Jan 11, 2019
83572c3
Revert "backport test improvements from develop"
aphor Jan 11, 2019
9b56485
fix docstring quote style
aphor Jan 11, 2019
27aa957
These should not be considered destructive tests
s0undt3ch Jan 3, 2019
a2d9e4f
Make sure blackout tests clean up after themselves. Properly.
s0undt3ch Jan 3, 2019
a07f356
wordsmithing
aphor Jan 11, 2019
dea01b7
Revert "wordsmithing"
aphor Jan 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions salt/minion.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# pylint: disable=import-error,no-name-in-module,redefined-builtin
from salt.ext import six
from salt._compat import ipaddress
from salt.utils.network import parse_host_port
from salt.ext.six.moves import range
from salt.utils.zeromq import zmq, ZMQDefaultLoop, install_zmq, ZMQ_VERSION_INFO

Expand Down Expand Up @@ -243,27 +244,29 @@ def resolve_dns(opts, fallback=True):


def prep_ip_port(opts):
'''
parse host:port values from opts['master'] and return valid:
master: ip address or hostname as a string
master_port: (optional) master returner port as integer

e.g.:
- master: 'localhost:1234' -> {'master': 'localhost', 'master_port': 1234}
- master: '127.0.0.1:1234' -> {'master': '127.0.0.1', 'master_port' :1234}
- master: '[::1]:1234' -> {'master': '::1', 'master_port': 1234}
- master: 'fe80::a00:27ff:fedc:ba98' -> {'master': 'fe80::a00:27ff:fedc:ba98'}
'''
ret = {}
# Use given master IP if "ip_only" is set or if master_ip is an ipv6 address without
# a port specified. The is_ipv6 check returns False if brackets are used in the IP
# definition such as master: '[::1]:1234'.
if opts['master_uri_format'] == 'ip_only' or salt.utils.network.is_ipv6(opts['master']):
ret['master'] = opts['master']
if opts['master_uri_format'] == 'ip_only':
ret['master'] = ipaddress.ip_address(opts['master'])
else:
ip_port = opts['master'].rsplit(':', 1)
if len(ip_port) == 1:
# e.g. master: mysaltmaster
ret['master'] = ip_port[0]
else:
# e.g. master: localhost:1234
# e.g. master: 127.0.0.1:1234
# e.g. master: [::1]:1234
# Strip off brackets for ipv6 support
ret['master'] = ip_port[0].strip('[]')

# Cast port back to an int! Otherwise a TypeError is thrown
# on some of the socket calls elsewhere in the minion and utils code.
ret['master_port'] = int(ip_port[1])
host, port = parse_host_port(opts['master'])
ret = {'master': host}
if port:
ret.update({'master_port': port})

return ret


Expand Down
48 changes: 27 additions & 21 deletions salt/transport/zeromq.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import salt.transport.mixins.auth
from salt.ext import six
from salt.exceptions import SaltReqTimeoutError
from salt._compat import ipaddress

from salt.utils.zeromq import zmq, ZMQDefaultLoop, install_zmq, ZMQ_VERSION_INFO, LIBZMQ_VERSION_INFO
import zmq.error
Expand Down Expand Up @@ -71,33 +72,38 @@ def _get_master_uri(master_ip,
'''
Return the ZeroMQ URI to connect the Minion to the Master.
It supports different source IP / port, given the ZeroMQ syntax:

// Connecting using a IP address and bind to an IP address
rc = zmq_connect(socket, "tcp://192.168.1.17:5555;192.168.1.1:5555"); assert (rc == 0);

Source: http://api.zeromq.org/4-1:zmq-tcp
'''
if LIBZMQ_VERSION_INFO >= (4, 1, 6) and ZMQ_VERSION_INFO >= (16, 0, 1):
# The source:port syntax for ZeroMQ has been added in libzmq 4.1.6
# which is included in the pyzmq wheels starting with 16.0.1.
if source_ip or source_port:
from salt.utils.zeromq import ip_bracket

master_uri = 'tcp://{master_ip}:{master_port}'.format(
master_ip=ip_bracket(master_ip), master_port=master_port)

if source_ip or source_port:
if LIBZMQ_VERSION_INFO >= (4, 1, 6) and ZMQ_VERSION_INFO >= (16, 0, 1):
# The source:port syntax for ZeroMQ has been added in libzmq 4.1.6
# which is included in the pyzmq wheels starting with 16.0.1.
if source_ip and source_port:
return 'tcp://{source_ip}:{source_port};{master_ip}:{master_port}'.format(
source_ip=source_ip, source_port=source_port,
master_ip=master_ip, master_port=master_port)
master_uri = 'tcp://{source_ip}:{source_port};{master_ip}:{master_port}'.format(
source_ip=ip_bracket(source_ip), source_port=source_port,
master_ip=ip_bracket(master_ip), master_port=master_port)
elif source_ip and not source_port:
return 'tcp://{source_ip}:0;{master_ip}:{master_port}'.format(
source_ip=source_ip,
master_ip=master_ip, master_port=master_port)
elif not source_ip and source_port:
return 'tcp://0.0.0.0:{source_port};{master_ip}:{master_port}'.format(
source_port=source_port,
master_ip=master_ip, master_port=master_port)
if source_ip or source_port:
log.warning('Unable to connect to the Master using a specific source IP / port')
log.warning('Consider upgrading to pyzmq >= 16.0.1 and libzmq >= 4.1.6')
return 'tcp://{master_ip}:{master_port}'.format(
master_ip=master_ip, master_port=master_port)
master_uri = 'tcp://{source_ip}:0;{master_ip}:{master_port}'.format(
source_ip=ip_bracket(source_ip),
master_ip=ip_bracket(master_ip), master_port=master_port)
elif source_port and not source_ip:
ip_any = '0.0.0.0' if ipaddress.ip_address(master_ip).version == 4 else ip_bracket('::')
master_uri = 'tcp://{ip_any}:{source_port};{master_ip}:{master_port}'.format(
ip_any=ip_any, source_port=source_port,
master_ip=ip_bracket(master_ip), master_port=master_port)
else:
log.warning('Unable to connect to the Master using a specific source IP / port')
log.warning('Consider upgrading to pyzmq >= 16.0.1 and libzmq >= 4.1.6')
log.warning('Specific source IP / port for connecting to master returner port: configuraion ignored')

return master_uri


class AsyncZeroMQReqChannel(salt.transport.client.ReqChannel):
Expand Down
66 changes: 59 additions & 7 deletions salt/utils/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@


def sanitize_host(host):
'''
"""
Sanitize host string.
'''
return ''.join([
c for c in host[0:255] if c in (ascii_letters + digits + '.-')
])
https://tools.ietf.org/html/rfc1123#section-2.1
"""
RFC952_characters = ascii_letters + digits + ".-"
return "".join([c for c in host[0:255] if c in RFC952_characters])


def isportopen(host, port):
Expand Down Expand Up @@ -1870,14 +1870,14 @@ def dns_check(addr, port, safe=False, ipv6=None):
if h[0] == socket.AF_INET6 and ipv6 is False:
continue

candidate_addr = salt.utils.zeromq.ip_bracket(h[4][0])
candidate_addr = h[4][0]

if h[0] != socket.AF_INET6 or ipv6 is not None:
candidates.append(candidate_addr)

try:
s = socket.socket(h[0], socket.SOCK_STREAM)
s.connect((candidate_addr.strip('[]'), port))
s.connect((candidate_addr), port))
s.close()

resolved = candidate_addr
Expand Down Expand Up @@ -1906,3 +1906,55 @@ def dns_check(addr, port, safe=False, ipv6=None):
raise SaltClientError()
raise SaltSystemExit(code=42, msg=err)
return resolved


def parse_host_port(host_port):
"""
Takes a string argument specifying host or host:port.

Returns a (hostname, port) or (ip_address, port) tuple. If no port is given,
the second (port) element of the returned tuple will be None.

host:port argument, for example, is accepted in the forms of:
- hostname
- hostname:1234
- hostname.domain.tld
- hostname.domain.tld:5678
- [1234::5]:5678
- 1234::5
- 10.11.12.13:4567
- 10.11.12.13
"""
host, port = None, None # default

_s_ = host_port[:]
if _s_[0] == "[":
if "]" in host_port:
host, _s_ = _s_.lstrip("[").rsplit("]", 1)
host = ipaddress.IPv6Address(host)
if _s_[0] == ":":
port = int(_s_.lstrip(":"))
else:
if len(_s_) > 1:
raise ValueError('found ambiguous "{}" port in "{}"'.format(_s_, host_port))
else:
if _s_.count(":") == 1:
host, _hostport_separator_, port = _s_.partition(":")
try:
port = int(port)
except ValueError as _e_:
log.error('host_port "%s" port value "%s" is not an integer.', host_port, port)
raise _e_
else:
host = _s_
try:
if not isinstance(host, ipaddress._BaseAddress):
host_ip = ipaddress.ip_address(host)
host = host_ip
except ValueError:
log.debug('"%s" Not an IP address? Assuming it is a hostname.', host)
if host != sanitize_host(host):
log.error('bad hostname: "%s"', host)
raise ValueError('bad hostname: "{}"'.format(host))

return host, port
6 changes: 3 additions & 3 deletions salt/utils/zeromq.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging
import tornado.ioloop
from salt.exceptions import SaltSystemExit
from salt._compat import ipaddress

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -82,6 +83,5 @@ def ip_bracket(addr):
Convert IP address representation to ZMQ (URL) format. ZMQ expects
brackets around IPv6 literals, since they are used in URLs.
'''
if addr and ':' in addr and not addr.startswith('['):
return '[{0}]'.format(addr)
return addr
addr = ipaddress.ip_address(addr)
return ('[{}]' if addr.version == 6 else '{}').format(addr)
16 changes: 16 additions & 0 deletions tests/unit/transport/test_zeromq.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,15 @@ def test_master_uri(self):
'''
test _get_master_uri method
'''

m_ip = '127.0.0.1'
m_port = 4505
s_ip = '111.1.0.1'
s_port = 4058

m_ip6 = '1234:5678::9abc'
s_ip6 = '1234:5678::1:9abc'

with patch('salt.transport.zeromq.LIBZMQ_VERSION_INFO', (4, 1, 6)), \
patch('salt.transport.zeromq.ZMQ_VERSION_INFO', (16, 0, 1)):
# pass in both source_ip and source_port
Expand All @@ -330,15 +334,27 @@ def test_master_uri(self):
source_ip=s_ip,
source_port=s_port) == 'tcp://{0}:{1};{2}:{3}'.format(s_ip, s_port, m_ip, m_port)

assert salt.transport.zeromq._get_master_uri(master_ip=m_ip6,
master_port=m_port,
source_ip=s_ip6,
source_port=s_port) == 'tcp://[{0}]:{1};[{2}]:{3}'.format(s_ip6, s_port, m_ip6, m_port)

# source ip and source_port empty
assert salt.transport.zeromq._get_master_uri(master_ip=m_ip,
master_port=m_port) == 'tcp://{0}:{1}'.format(m_ip, m_port)

assert salt.transport.zeromq._get_master_uri(master_ip=m_ip6,
master_port=m_port) == 'tcp://[{0}]:{1}'.format(m_ip6, m_port)

# pass in only source_ip
assert salt.transport.zeromq._get_master_uri(master_ip=m_ip,
master_port=m_port,
source_ip=s_ip) == 'tcp://{0}:0;{1}:{2}'.format(s_ip, m_ip, m_port)

assert salt.transport.zeromq._get_master_uri(master_ip=m_ip6,
master_port=m_port,
source_ip=s_ip6) == 'tcp://[{0}]:0;[{1}]:{2}'.format(s_ip6, m_ip6, m_port)

# pass in only source_port
assert salt.transport.zeromq._get_master_uri(master_ip=m_ip,
master_port=m_port,
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/utils/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

# Import salt libs
import salt.utils.network as network
from salt._compat import ipaddress

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -202,6 +203,35 @@ def test_is_ipv6(self):
self.assertFalse(network.is_ipv6('10.0.1.2'))
self.assertFalse(network.is_ipv6('2001.0db8.85a3.0000.0000.8a2e.0370.7334'))

def test_parse_host_port(self):
_ip = ipaddress.ip_address
good_host_ports = {
'10.10.0.3': (_ip('10.10.0.3'), None),
'10.10.0.3:1234': (_ip('10.10.0.3'), 1234),
'2001:0db8:85a3::8a2e:0370:7334': (_ip('2001:0db8:85a3::8a2e:0370:7334'), None),
'[2001:0db8:85a3::8a2e:0370:7334]:1234': (_ip('2001:0db8:85a3::8a2e:0370:7334'), 1234),
'2001:0db8:85a3::7334': (_ip('2001:0db8:85a3::7334'), None),
'[2001:0db8:85a3::7334]:1234': (_ip('2001:0db8:85a3::7334'), 1234)
}
bad_host_ports = [
'10.10.0.3/24',
'10.10.0.3::1234',
'2001:0db8:0370:7334',
'2001:0db8:0370::7334]:1234',
'2001:0db8:0370:0:a:b:c:d:1234'
]
for host_port, assertion_value in good_host_ports.items():
host = port = None
host, port = network.parse_host_port(host_port)
self.assertEqual((host, port), assertion_value)

for host_port in bad_host_ports:
try:
self.assertRaises(ValueError, network.parse_host_port, host_port)
except AssertionError as _e_:
log.error('bad host_port value: "%s" failed to trigger ValueError exception', host_port)
raise _e_

def test_is_subnet(self):
for subnet_data in (IPV4_SUBNETS, IPV6_SUBNETS):
for item in subnet_data[True]:
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/utils/validate/test_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_ipv6_addr(self):
Test IPv6 address validation
'''
true_addrs = [
'::',
'::1',
'::1/32',
'::1/32',
Expand All @@ -62,6 +63,8 @@ def test_ipv6_addr(self):
'::1/0',
'::1/32d',
'::1/129',
'2a03:4000:c:10aa:1017:f00d:aaaa:a:4506',
'2a03::1::2',
]

for addr in true_addrs:
Expand Down