From c36a01a43994f7f4a8cb0f44bd0859d41cea83cd Mon Sep 17 00:00:00 2001
From: Dmitry Tantsur <divius.inside@gmail.com>
Date: Thu, 11 Apr 2019 17:02:58 +0200
Subject: [PATCH] Publish baremetal endpoint via mdns

This change adds an option to publish the endpoint via mDNS on start
up and clean it up on tear down.

Story: #2005393
Task: #30383
Change-Id: I55d2e7718a23cde111eaac4e431588184cb16bda
---
 ironic/conductor/base_manager.py              | 20 +++++++++++
 ironic/conf/conductor.py                      |  3 ++
 .../tests/unit/conductor/test_base_manager.py | 36 +++++++++++++++++++
 lower-constraints.txt                         |  2 +-
 releasenotes/notes/mdns-a5f4034257139e31.yaml |  6 ++++
 requirements.txt                              |  2 +-
 tools/config/ironic-config-generator.conf     |  1 +
 7 files changed, 68 insertions(+), 2 deletions(-)
 create mode 100644 releasenotes/notes/mdns-a5f4034257139e31.yaml

diff --git a/ironic/conductor/base_manager.py b/ironic/conductor/base_manager.py
index c08c8702bd..31d9471927 100644
--- a/ironic/conductor/base_manager.py
+++ b/ironic/conductor/base_manager.py
@@ -19,6 +19,7 @@ import eventlet
 import futurist
 from futurist import periodics
 from futurist import rejection
+from ironic_lib import mdns
 from oslo_db import exception as db_exception
 from oslo_log import log
 from oslo_utils import excutils
@@ -39,6 +40,7 @@ from ironic.conductor import task_manager
 from ironic.conf import CONF
 from ironic.db import api as dbapi
 from ironic.drivers import base as driver_base
+from ironic.drivers.modules import deploy_utils
 from ironic import objects
 from ironic.objects import fields as obj_fields
 
@@ -78,6 +80,7 @@ class BaseConductorManager(object):
         self.sensors_notifier = rpc.get_sensors_notifier()
         self._started = False
         self._shutdown = None
+        self._zeroconf = None
 
     def init_host(self, admin_context=None):
         """Initialize the conductor host.
@@ -212,6 +215,9 @@ class BaseConductorManager(object):
         except exception.NoFreeConductorWorker:
             LOG.warning('Failed to start worker for resuming allocations.')
 
+        if CONF.conductor.enable_mdns:
+            self._publish_endpoint()
+
         self._started = True
 
     def _use_groups(self):
@@ -316,6 +322,11 @@ class BaseConductorManager(object):
         self._periodic_tasks.stop()
         self._periodic_tasks.wait()
         self._executor.shutdown(wait=True)
+
+        if self._zeroconf is not None:
+            self._zeroconf.close()
+            self._zeroconf = None
+
         self._started = False
 
     def _register_and_validate_hardware_interfaces(self, hardware_types):
@@ -564,3 +575,12 @@ class BaseConductorManager(object):
         for allocation in objects.Allocation.list(context, filters=filters):
             LOG.debug('Resuming unfinished allocation %s', allocation.uuid)
             allocations.do_allocate(context, allocation)
+
+    def _publish_endpoint(self):
+        params = {}
+        if CONF.debug:
+            params['ipa_debug'] = True
+        self._zeroconf = mdns.Zeroconf()
+        self._zeroconf.register_service('baremetal',
+                                        deploy_utils.get_ironic_api_url(),
+                                        params=params)
diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py
index 039574e4ea..05ecd20665 100644
--- a/ironic/conf/conductor.py
+++ b/ironic/conf/conductor.py
@@ -221,6 +221,9 @@ opts = [
                 mutable=True,
                 help=_('Allow deleting nodes which are in state '
                        '\'available\'. Defaults to True.')),
+    cfg.BoolOpt('enable_mdns', default=False,
+                help=_('Whether to enable publishing the ironic-inspector API '
+                       'endpoint via multicast DNS.')),
 ]
 
 
diff --git a/ironic/tests/unit/conductor/test_base_manager.py b/ironic/tests/unit/conductor/test_base_manager.py
index 0c619d912b..02bd171f92 100644
--- a/ironic/tests/unit/conductor/test_base_manager.py
+++ b/ironic/tests/unit/conductor/test_base_manager.py
@@ -18,6 +18,7 @@ import uuid
 import eventlet
 import futurist
 from futurist import periodics
+from ironic_lib import mdns
 import mock
 from oslo_config import cfg
 from oslo_db import exception as db_exception
@@ -32,6 +33,7 @@ from ironic.conductor import notification_utils
 from ironic.conductor import task_manager
 from ironic.drivers import fake_hardware
 from ironic.drivers import generic
+from ironic.drivers.modules import deploy_utils
 from ironic.drivers.modules import fake
 from ironic import objects
 from ironic.objects import fields
@@ -247,6 +249,40 @@ class StartStopTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
         self.service.del_host()
         self.assertTrue(self.service._shutdown)
 
+    @mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True)
+    @mock.patch.object(mdns, 'Zeroconf', autospec=True)
+    def test_start_with_mdns(self, mock_zc, mock_api_url):
+        CONF.set_override('debug', False)
+        CONF.set_override('enable_mdns', True, 'conductor')
+        self._start_service()
+        res = objects.Conductor.get_by_hostname(self.context, self.hostname)
+        self.assertEqual(self.hostname, res['hostname'])
+        mock_zc.return_value.register_service.assert_called_once_with(
+            'baremetal',
+            mock_api_url.return_value,
+            params={})
+
+    @mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True)
+    @mock.patch.object(mdns, 'Zeroconf', autospec=True)
+    def test_start_with_mdns_and_debug(self, mock_zc, mock_api_url):
+        CONF.set_override('debug', True)
+        CONF.set_override('enable_mdns', True, 'conductor')
+        self._start_service()
+        res = objects.Conductor.get_by_hostname(self.context, self.hostname)
+        self.assertEqual(self.hostname, res['hostname'])
+        mock_zc.return_value.register_service.assert_called_once_with(
+            'baremetal',
+            mock_api_url.return_value,
+            params={'ipa_debug': True})
+
+    def test_del_host_with_mdns(self):
+        mock_zc = mock.Mock(spec=mdns.Zeroconf)
+        self.service._zeroconf = mock_zc
+        self._start_service()
+        self.service.del_host()
+        mock_zc.close.assert_called_once_with()
+        self.assertIsNone(self.service._zeroconf)
+
 
 class CheckInterfacesTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
     def test__check_enabled_interfaces_success(self):
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 3676081f11..859c210448 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -38,7 +38,7 @@ greenlet==0.4.13
 hacking==1.0.0
 idna==2.6
 imagesize==1.0.0
-ironic-lib==2.15.0
+ironic-lib==2.17.0
 iso8601==0.1.11
 Jinja2==2.10
 jmespath==0.9.3
diff --git a/releasenotes/notes/mdns-a5f4034257139e31.yaml b/releasenotes/notes/mdns-a5f4034257139e31.yaml
new file mode 100644
index 0000000000..8bb4327e7e
--- /dev/null
+++ b/releasenotes/notes/mdns-a5f4034257139e31.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    A new option ``enable_mdns`` allows to enable publishing the baremetal
+    API endpoint via mDNS as specified in the `API SIG guideline
+    <http://specs.openstack.org/openstack/api-sig/guidelines/dns-sd.html>`_.
diff --git a/requirements.txt b/requirements.txt
index ab973569d8..be3c0992b1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,7 +11,7 @@ python-cinderclient>=3.3.0 # Apache-2.0
 python-neutronclient>=6.7.0 # Apache-2.0
 python-glanceclient>=2.8.0 # Apache-2.0
 keystoneauth1>=3.4.0 # Apache-2.0
-ironic-lib>=2.15.0 # Apache-2.0
+ironic-lib>=2.17.0 # Apache-2.0
 python-swiftclient>=3.2.0 # Apache-2.0
 pytz>=2013.6 # MIT
 stevedore>=1.20.0 # Apache-2.0
diff --git a/tools/config/ironic-config-generator.conf b/tools/config/ironic-config-generator.conf
index 5412c9159f..806bf7a03e 100644
--- a/tools/config/ironic-config-generator.conf
+++ b/tools/config/ironic-config-generator.conf
@@ -4,6 +4,7 @@ wrap_width = 62
 namespace = ironic
 namespace = ironic_lib.disk_utils
 namespace = ironic_lib.disk_partitioner
+namespace = ironic_lib.mdns
 namespace = ironic_lib.metrics
 namespace = ironic_lib.metrics_statsd
 namespace = ironic_lib.utils