Add support code for multicast DNS service discovery
To simplify standalone configuration, we need an ability for IPA to discovery ironic and ironic-inspector on the local network. This change adds the support code for using multicast DNS (RFC 6762) with DNS service discovery (RFC 6763) to publish and discover OpenStack services as proposed in the API SIG guideline https://review.opendev.org/651222. Change-Id: Iaf3422331238884412ce608c0667de7891945f98 Story: #2005393 Task: #30432
This commit is contained in:
parent
8a798680e1
commit
c90d008a5b
@ -103,3 +103,11 @@ class FileSystemNotSupported(IronicException):
|
|||||||
|
|
||||||
class InvalidMetricConfig(IronicException):
|
class InvalidMetricConfig(IronicException):
|
||||||
message = _("Invalid value for metrics config option: %(reason)s")
|
message = _("Invalid value for metrics config option: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceLookupFailure(IronicException):
|
||||||
|
message = _("Cannot find %(service)s service through multicast")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceRegistrationFailure(IronicException):
|
||||||
|
message = _("Cannot register %(service)s service: %(error)s")
|
||||||
|
254
ironic_lib/mdns.py
Normal file
254
ironic_lib/mdns.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Multicast DNS implementation for API discovery.
|
||||||
|
|
||||||
|
This implementation follows RFC 6763 as clarified by the API SIG guideline
|
||||||
|
https://review.opendev.org/651222.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from six.moves.urllib import parse
|
||||||
|
import zeroconf
|
||||||
|
|
||||||
|
from ironic_lib.common.i18n import _
|
||||||
|
from ironic_lib import exception
|
||||||
|
|
||||||
|
|
||||||
|
opts = [
|
||||||
|
cfg.IntOpt('registration_attempts',
|
||||||
|
min=1, default=5,
|
||||||
|
help='Number of attempts to register a service. Currently '
|
||||||
|
'has to be larger than 1 because of race conditions '
|
||||||
|
'in the zeroconf library.'),
|
||||||
|
cfg.IntOpt('lookup_attempts',
|
||||||
|
min=1, default=3,
|
||||||
|
help='Number of attempts to lookup a service.'),
|
||||||
|
cfg.DictOpt('params',
|
||||||
|
default={},
|
||||||
|
help='Additional parameters to pass for the registered '
|
||||||
|
'service.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
opt_group = cfg.OptGroup(name='mdns', title='Options for multicast DNS')
|
||||||
|
CONF.register_group(opt_group)
|
||||||
|
CONF.register_opts(opts, opt_group)
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_MDNS_DOMAIN = '_openstack._tcp.local.'
|
||||||
|
_endpoint = collections.namedtuple('Endpoint',
|
||||||
|
['ip', 'hostname', 'port', 'params'])
|
||||||
|
|
||||||
|
|
||||||
|
class Zeroconf(object):
|
||||||
|
"""Multicast DNS implementation client and server.
|
||||||
|
|
||||||
|
Uses threading internally, so there is no start method. It starts
|
||||||
|
automatically on creation.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
The underlying library does not yet support IPv6.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize and start the mDNS server."""
|
||||||
|
self._zc = zeroconf.Zeroconf()
|
||||||
|
self._registered = []
|
||||||
|
|
||||||
|
def register_service(self, service_type, endpoint, params=None):
|
||||||
|
"""Register a service.
|
||||||
|
|
||||||
|
This call announces the new services via multicast and instructs the
|
||||||
|
built-in server to respond to queries about it.
|
||||||
|
|
||||||
|
:param service_type: OpenStack service type, e.g. "baremetal".
|
||||||
|
:param endpoint: full endpoint to reach the service.
|
||||||
|
:param params: optional properties as a dictionary.
|
||||||
|
:raises: :exc:`.ServiceRegistrationFailure` if the service cannot be
|
||||||
|
registered, e.g. because of conflicts.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed = _parse_endpoint(endpoint)
|
||||||
|
except socket.error as ex:
|
||||||
|
msg = (_("Cannot resolve the host name of %(endpoint)s: "
|
||||||
|
"%(error)s. Hint: only IPv4 is supported for now.") %
|
||||||
|
{'endpoint': endpoint, 'error': ex})
|
||||||
|
raise exception.ServiceRegistrationFailure(
|
||||||
|
service=service_type, error=msg)
|
||||||
|
|
||||||
|
all_params = CONF.mdns.params.copy()
|
||||||
|
if params:
|
||||||
|
all_params.update(params)
|
||||||
|
all_params.update(parsed.params)
|
||||||
|
|
||||||
|
# TODO(dtantsur): allow overriding TTL values via configuration when
|
||||||
|
# https://github.com/jstasiak/python-zeroconf/commit/ecc021b7a3cec863eed5a3f71a1f28e3026c25b0
|
||||||
|
# is released.
|
||||||
|
info = zeroconf.ServiceInfo(_MDNS_DOMAIN,
|
||||||
|
'%s.%s' % (service_type, _MDNS_DOMAIN),
|
||||||
|
parsed.ip, parsed.port,
|
||||||
|
properties=all_params,
|
||||||
|
server=parsed.hostname)
|
||||||
|
|
||||||
|
LOG.debug('Registering %s via mDNS', info)
|
||||||
|
# Work around a potential race condition in the registration code:
|
||||||
|
# https://github.com/jstasiak/python-zeroconf/issues/163
|
||||||
|
delay = 0.1
|
||||||
|
try:
|
||||||
|
for attempt in range(CONF.mdns.registration_attempts):
|
||||||
|
try:
|
||||||
|
self._zc.register_service(info)
|
||||||
|
except zeroconf.NonUniqueNameException:
|
||||||
|
LOG.debug('Could not register %s - conflict', info)
|
||||||
|
if attempt == CONF.mdns.registration_attempts - 1:
|
||||||
|
raise
|
||||||
|
# reset the cache to purge learned records and retry
|
||||||
|
self._zc.cache = zeroconf.DNSCache()
|
||||||
|
time.sleep(delay)
|
||||||
|
delay *= 2
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except zeroconf.Error as exc:
|
||||||
|
raise exception.ServiceRegistrationFailure(
|
||||||
|
service=service_type, error=exc)
|
||||||
|
|
||||||
|
self._registered.append(info)
|
||||||
|
|
||||||
|
def get_endpoint(self, service_type):
|
||||||
|
"""Get an endpoint and its properties from mDNS.
|
||||||
|
|
||||||
|
If the requested endpoint is already in the built-in server cache, and
|
||||||
|
its TTL is not exceeded, the cached value is returned.
|
||||||
|
|
||||||
|
:param service_type: OpenStack service type.
|
||||||
|
:returns: tuple (endpoint URL, properties as a dict).
|
||||||
|
:raises: :exc:`.ServiceLookupFailure` if the service cannot be found.
|
||||||
|
"""
|
||||||
|
delay = 0.1
|
||||||
|
for attempt in range(CONF.mdns.lookup_attempts):
|
||||||
|
name = '%s.%s' % (service_type, _MDNS_DOMAIN)
|
||||||
|
info = self._zc.get_service_info(name, name)
|
||||||
|
if info is not None:
|
||||||
|
break
|
||||||
|
elif attempt == CONF.mdns.lookup_attempts - 1:
|
||||||
|
raise exception.ServiceLookupFailure(service=service_type)
|
||||||
|
else:
|
||||||
|
time.sleep(delay)
|
||||||
|
delay *= 2
|
||||||
|
|
||||||
|
# TODO(dtantsur): IPv6 support
|
||||||
|
address = socket.inet_ntoa(info.address)
|
||||||
|
properties = info.properties.copy()
|
||||||
|
path = properties.pop('path', '')
|
||||||
|
protocol = properties.pop('protocol', None)
|
||||||
|
if not protocol:
|
||||||
|
if info.port == 80:
|
||||||
|
protocol = 'http'
|
||||||
|
else:
|
||||||
|
protocol = 'https'
|
||||||
|
|
||||||
|
if info.server.endswith('.local.'):
|
||||||
|
# Local hostname means that the catalog lists an IP address,
|
||||||
|
# so use it
|
||||||
|
host = address
|
||||||
|
else:
|
||||||
|
# Otherwise use the provided hostname.
|
||||||
|
host = info.server.rstrip('.')
|
||||||
|
|
||||||
|
return ('{proto}://{host}:{port}{path}'.format(proto=protocol,
|
||||||
|
host=host,
|
||||||
|
port=info.port,
|
||||||
|
path=path),
|
||||||
|
properties)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Shut down mDNS and unregister services.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
If another server is running for the same services, it will
|
||||||
|
re-register them immediately.
|
||||||
|
"""
|
||||||
|
for info in self._registered:
|
||||||
|
try:
|
||||||
|
self._zc.unregister_service(info)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception('Cound not unregister mDNS service %s', info)
|
||||||
|
self._zc.close()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_endpoint(service_type):
|
||||||
|
"""Get an endpoint and its properties from mDNS.
|
||||||
|
|
||||||
|
If the requested endpoint is already in the built-in server cache, and
|
||||||
|
its TTL is not exceeded, the cached value is returned.
|
||||||
|
|
||||||
|
:param service_type: OpenStack service type.
|
||||||
|
:returns: tuple (endpoint URL, properties as a dict).
|
||||||
|
:raises: :exc:`.ServiceLookupFailure` if the service cannot be found.
|
||||||
|
"""
|
||||||
|
with Zeroconf() as zc:
|
||||||
|
return zc.get_endpoint(service_type)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_endpoint(endpoint):
|
||||||
|
params = {}
|
||||||
|
url = parse.urlparse(endpoint)
|
||||||
|
port = url.port
|
||||||
|
|
||||||
|
if port is None:
|
||||||
|
if url.scheme == 'https':
|
||||||
|
port = 443
|
||||||
|
else:
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
hostname = url.hostname
|
||||||
|
# FIXME(dtantsur): the zeroconf library does not support IPv6, use IPv4
|
||||||
|
# only resolving for now.
|
||||||
|
ip = socket.gethostbyname(hostname)
|
||||||
|
if ip == hostname:
|
||||||
|
# we need a host name for the service record. if what we have in
|
||||||
|
# the catalog is an IP address, use the local hostname instead
|
||||||
|
hostname = None
|
||||||
|
# zeroconf requires addresses in network format (and see above re IPv6)
|
||||||
|
ip = socket.inet_aton(ip)
|
||||||
|
|
||||||
|
# avoid storing information that can be derived from existing data
|
||||||
|
if url.path not in ('', '/'):
|
||||||
|
params['path'] = url.path
|
||||||
|
|
||||||
|
if (not (port == 80 and url.scheme == 'http')
|
||||||
|
and not (port == 443 and url.scheme == 'https')):
|
||||||
|
params['protocol'] = url.scheme
|
||||||
|
|
||||||
|
# zeroconf is pretty picky about having the trailing dot
|
||||||
|
if hostname is not None and not hostname.endswith('.'):
|
||||||
|
hostname += '.'
|
||||||
|
|
||||||
|
return _endpoint(ip, hostname, port, params)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
"""Entry point for oslo-config-generator."""
|
||||||
|
return [('mdns', opts)]
|
220
ironic_lib/tests/test_mdns.py
Normal file
220
ironic_lib/tests/test_mdns.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
|
import zeroconf
|
||||||
|
|
||||||
|
from ironic_lib import exception
|
||||||
|
from ironic_lib import mdns
|
||||||
|
from ironic_lib.tests import base
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(zeroconf, 'Zeroconf', autospec=True)
|
||||||
|
class RegisterServiceTestCase(base.IronicLibTestCase):
|
||||||
|
|
||||||
|
def test_ok(self, mock_zc):
|
||||||
|
zc = mdns.Zeroconf()
|
||||||
|
zc.register_service('baremetal', 'https://127.0.0.1/baremetal')
|
||||||
|
mock_zc.return_value.register_service.assert_called_once_with(mock.ANY)
|
||||||
|
info = mock_zc.return_value.register_service.call_args[0][0]
|
||||||
|
self.assertEqual('_openstack._tcp.local.', info.type)
|
||||||
|
self.assertEqual('baremetal._openstack._tcp.local.', info.name)
|
||||||
|
self.assertEqual('127.0.0.1', socket.inet_ntoa(info.address))
|
||||||
|
self.assertEqual({'path': '/baremetal'}, info.properties)
|
||||||
|
|
||||||
|
def test_with_params(self, mock_zc):
|
||||||
|
CONF.set_override('params', {'answer': 'none', 'foo': 'bar'},
|
||||||
|
group='mdns')
|
||||||
|
zc = mdns.Zeroconf()
|
||||||
|
zc.register_service('baremetal', 'https://127.0.0.1/baremetal',
|
||||||
|
params={'answer': 42})
|
||||||
|
mock_zc.return_value.register_service.assert_called_once_with(mock.ANY)
|
||||||
|
info = mock_zc.return_value.register_service.call_args[0][0]
|
||||||
|
self.assertEqual('_openstack._tcp.local.', info.type)
|
||||||
|
self.assertEqual('baremetal._openstack._tcp.local.', info.name)
|
||||||
|
self.assertEqual('127.0.0.1', socket.inet_ntoa(info.address))
|
||||||
|
self.assertEqual({'path': '/baremetal',
|
||||||
|
'answer': 42,
|
||||||
|
'foo': 'bar'},
|
||||||
|
info.properties)
|
||||||
|
|
||||||
|
@mock.patch.object(mdns.time, 'sleep', autospec=True)
|
||||||
|
def test_with_race(self, mock_sleep, mock_zc):
|
||||||
|
mock_zc.return_value.register_service.side_effect = [
|
||||||
|
zeroconf.NonUniqueNameException,
|
||||||
|
zeroconf.NonUniqueNameException,
|
||||||
|
zeroconf.NonUniqueNameException,
|
||||||
|
None
|
||||||
|
]
|
||||||
|
zc = mdns.Zeroconf()
|
||||||
|
zc.register_service('baremetal', 'https://127.0.0.1/baremetal')
|
||||||
|
mock_zc.return_value.register_service.assert_called_with(mock.ANY)
|
||||||
|
self.assertEqual(4, mock_zc.return_value.register_service.call_count)
|
||||||
|
mock_sleep.assert_has_calls([mock.call(i) for i in (0.1, 0.2, 0.4)])
|
||||||
|
|
||||||
|
@mock.patch.object(mdns.time, 'sleep', autospec=True)
|
||||||
|
def test_failure(self, mock_sleep, mock_zc):
|
||||||
|
mock_zc.return_value.register_service.side_effect = (
|
||||||
|
zeroconf.NonUniqueNameException
|
||||||
|
)
|
||||||
|
zc = mdns.Zeroconf()
|
||||||
|
self.assertRaises(exception.ServiceRegistrationFailure,
|
||||||
|
zc.register_service,
|
||||||
|
'baremetal', 'https://127.0.0.1/baremetal')
|
||||||
|
mock_zc.return_value.register_service.assert_called_with(mock.ANY)
|
||||||
|
self.assertEqual(CONF.mdns.registration_attempts,
|
||||||
|
mock_zc.return_value.register_service.call_count)
|
||||||
|
self.assertEqual(CONF.mdns.registration_attempts - 1,
|
||||||
|
mock_sleep.call_count)
|
||||||
|
|
||||||
|
|
||||||
|
class ParseEndpointTestCase(base.IronicLibTestCase):
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
endpoint = mdns._parse_endpoint('http://127.0.0.1')
|
||||||
|
self.assertEqual('127.0.0.1', socket.inet_ntoa(endpoint.ip))
|
||||||
|
self.assertEqual(80, endpoint.port)
|
||||||
|
self.assertEqual({}, endpoint.params)
|
||||||
|
self.assertIsNone(endpoint.hostname)
|
||||||
|
|
||||||
|
def test_simple_https(self):
|
||||||
|
endpoint = mdns._parse_endpoint('https://127.0.0.1')
|
||||||
|
self.assertEqual('127.0.0.1', socket.inet_ntoa(endpoint.ip))
|
||||||
|
self.assertEqual(443, endpoint.port)
|
||||||
|
self.assertEqual({}, endpoint.params)
|
||||||
|
self.assertIsNone(endpoint.hostname)
|
||||||
|
|
||||||
|
def test_with_path_and_port(self):
|
||||||
|
endpoint = mdns._parse_endpoint('http://127.0.0.1:8080/bm')
|
||||||
|
self.assertEqual('127.0.0.1', socket.inet_ntoa(endpoint.ip))
|
||||||
|
self.assertEqual(8080, endpoint.port)
|
||||||
|
self.assertEqual({'path': '/bm', 'protocol': 'http'}, endpoint.params)
|
||||||
|
self.assertIsNone(endpoint.hostname)
|
||||||
|
|
||||||
|
@mock.patch.object(socket, 'gethostbyname', autospec=True)
|
||||||
|
def test_resolve(self, mock_resolve):
|
||||||
|
mock_resolve.return_value = '1.2.3.4'
|
||||||
|
endpoint = mdns._parse_endpoint('http://example.com')
|
||||||
|
self.assertEqual('1.2.3.4', socket.inet_ntoa(endpoint.ip))
|
||||||
|
self.assertEqual(80, endpoint.port)
|
||||||
|
self.assertEqual({}, endpoint.params)
|
||||||
|
self.assertEqual('example.com.', endpoint.hostname)
|
||||||
|
mock_resolve.assert_called_once_with('example.com')
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('zeroconf.Zeroconf', autospec=True)
|
||||||
|
class GetEndpointTestCase(base.IronicLibTestCase):
|
||||||
|
def test_simple(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=80,
|
||||||
|
properties={}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('http://192.168.1.1:80', endp)
|
||||||
|
self.assertEqual({}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
mock_zc.return_value.close.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_https(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=443,
|
||||||
|
properties={}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('https://192.168.1.1:443', endp)
|
||||||
|
self.assertEqual({}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_custom_port_and_path(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=8080,
|
||||||
|
properties={'path': '/baremetal'}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('https://192.168.1.1:8080/baremetal', endp)
|
||||||
|
self.assertEqual({}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_custom_port_path_and_protocol(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=8080,
|
||||||
|
properties={'path': '/baremetal', 'protocol': 'http'}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('http://192.168.1.1:8080/baremetal', endp)
|
||||||
|
self.assertEqual({}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_params(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=80,
|
||||||
|
properties={'ipa_debug': True}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('http://192.168.1.1:80', endp)
|
||||||
|
self.assertEqual({'ipa_debug': True}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_with_server(self, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = mock.Mock(
|
||||||
|
address=socket.inet_aton('192.168.1.1'),
|
||||||
|
port=443,
|
||||||
|
server='openstack.example.com.',
|
||||||
|
properties={}
|
||||||
|
)
|
||||||
|
|
||||||
|
endp, params = mdns.get_endpoint('baremetal')
|
||||||
|
self.assertEqual('https://openstack.example.com:443', endp)
|
||||||
|
self.assertEqual({}, params)
|
||||||
|
mock_zc.return_value.get_service_info.assert_called_once_with(
|
||||||
|
'baremetal._openstack._tcp.local.',
|
||||||
|
'baremetal._openstack._tcp.local.'
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('time.sleep', autospec=True)
|
||||||
|
def test_not_found(self, mock_sleep, mock_zc):
|
||||||
|
mock_zc.return_value.get_service_info.return_value = None
|
||||||
|
|
||||||
|
self.assertRaisesRegex(exception.ServiceLookupFailure,
|
||||||
|
'baremetal service',
|
||||||
|
mdns.get_endpoint, 'baremetal')
|
||||||
|
self.assertEqual(CONF.mdns.lookup_attempts - 1, mock_sleep.call_count)
|
@ -73,3 +73,4 @@ traceback2==1.4.0
|
|||||||
unittest2==1.1.0
|
unittest2==1.1.0
|
||||||
WebOb==1.7.1
|
WebOb==1.7.1
|
||||||
wrapt==1.7.0
|
wrapt==1.7.0
|
||||||
|
zeroconf==0.19.1
|
||||||
|
@ -12,3 +12,5 @@ oslo.utils>=3.33.0 # Apache-2.0
|
|||||||
requests>=2.14.2 # Apache-2.0
|
requests>=2.14.2 # Apache-2.0
|
||||||
six>=1.10.0 # MIT
|
six>=1.10.0 # MIT
|
||||||
oslo.log>=3.36.0 # Apache-2.0
|
oslo.log>=3.36.0 # Apache-2.0
|
||||||
|
zeroconf>=0.19.1,<0.20;python_version=='2.7' # LGPL
|
||||||
|
zeroconf>=0.19.1;python_version>='3.0' # LGPL
|
||||||
|
@ -26,6 +26,7 @@ packages =
|
|||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
ironic_lib.disk_partitioner = ironic_lib.disk_partitioner:list_opts
|
ironic_lib.disk_partitioner = ironic_lib.disk_partitioner:list_opts
|
||||||
ironic_lib.disk_utils = ironic_lib.disk_utils:list_opts
|
ironic_lib.disk_utils = ironic_lib.disk_utils:list_opts
|
||||||
|
ironic_lib.mdns = ironic_lib.mdns:list_opts
|
||||||
ironic_lib.utils = ironic_lib.utils:list_opts
|
ironic_lib.utils = ironic_lib.utils:list_opts
|
||||||
ironic_lib.metrics = ironic_lib.metrics_utils:list_opts
|
ironic_lib.metrics = ironic_lib.metrics_utils:list_opts
|
||||||
ironic_lib.metrics_statsd = ironic_lib.metrics_statsd:list_opts
|
ironic_lib.metrics_statsd = ironic_lib.metrics_statsd:list_opts
|
||||||
|
Loading…
Reference in New Issue
Block a user