From 696606f6826394a32e9d09647f2ab1a8653bf8cb Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 18 Jun 2019 18:26:13 -0700 Subject: [PATCH] manual introspection trigger command Change-Id: I64e66682c1e54f6edc260a22f46f5f6df8e85af1 Story: 2005896 Task: 33756 --- doc/source/admin/how_it_works.rst | 27 ++++++ ironic_python_agent/agent.py | 9 +- ironic_python_agent/cmd/inspect.py | 30 +++++++ ironic_python_agent/config.py | 12 +++ ironic_python_agent/inspect.py | 86 +++++++++++++++++++ ironic_python_agent/inspector.py | 4 +- .../tests/unit/test_inspector.py | 4 +- ...manual-introspection-b04b5c25f5e004ac.yaml | 13 +++ setup.cfg | 1 + 9 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 ironic_python_agent/cmd/inspect.py create mode 100644 ironic_python_agent/inspect.py create mode 100644 releasenotes/notes/manual-introspection-b04b5c25f5e004ac.yaml diff --git a/doc/source/admin/how_it_works.rst b/doc/source/admin/how_it_works.rst index aeb25ad28..d93c878c3 100644 --- a/doc/source/admin/how_it_works.rst +++ b/doc/source/admin/how_it_works.rst @@ -60,6 +60,33 @@ full endpoint of Ironic Inspector, for example:: Make sure your DHCP environment is set to boot IPA by default. +For the cases where the infrastructure operator and cloud user are the same, +an additional tool exists that can be installed alongside the agent inside +a running instance. This is the ``ironic-collect-introspection-data`` +command which allows for a node in ``ACTIVE`` state to publish updated +introspection data to ironic-inspector. This ability requires ironic-inspector +to be configured with ``[processing]permit_active_introspection`` set to +``True``. For example:: + + ironic-collect-introspection-data --inspection_callback_url http://IP:5050/v1/continue + +Alternatively, this command may also be used with multicast DNS +functionality to identify the `Ironic Inspector`_ service endpoint. +For example:: + + ironic-collect-introspection-data --inspection_callback_url mdns + +An additional daemon mode may be useful for some operators who wish to receive +regular updates, in the form of the ``[DEFAULT]introspection_daemon`` boolean +configuration option. +For example:: + + ironic-collect-introspection-data --inspection_callback_url mdns --introspection_daemon + +The above command will attempt to connect to introspection and will then enter +a loop to publish every 300 seconds. This can be tuned with the +``[DEFAULT]introspection_daemon_post_interval`` configuration option. + .. _Ironic Inspector: https://docs.openstack.org/ironic-inspector/ Hardware Inventory diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py index 390afce8e..4641b82e3 100644 --- a/ironic_python_agent/agent.py +++ b/ironic_python_agent/agent.py @@ -388,8 +388,13 @@ class IronicPythonAgent(base.ExecuteCommandMixin): # lookup will fail due to unknown MAC. uuid = None if cfg.CONF.inspection_callback_url: - uuid = inspector.inspect() - + try: + # Attempt inspection. This may fail, and previously + # an error would be logged. + uuid = inspector.inspect() + except errors.InspectionError as e: + LOG.error('Failed to perform inspection: %(err)s', + {'error': e}) if self.api_url: self._wait_for_interface() content = self.api_client.lookup_node( diff --git a/ironic_python_agent/cmd/inspect.py b/ironic_python_agent/cmd/inspect.py new file mode 100644 index 000000000..af747f7e0 --- /dev/null +++ b/ironic_python_agent/cmd/inspect.py @@ -0,0 +1,30 @@ +# Copyright 2013 Rackspace, Inc. +# +# 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 sys + +from oslo_config import cfg +from oslo_log import log + +from ironic_python_agent import inspect as inspection + +CONF = cfg.CONF + + +def run(): + """Entrypoint for IronicPythonAgent.""" + log.register_options(CONF) + CONF(args=sys.argv[1:]) + log.setup(CONF, 'ironic-python-agent') + inspection.IronicInspection().run() diff --git a/ironic_python_agent/config.py b/ironic_python_agent/config.py index 5bd1cdc2d..2b65bcfea 100644 --- a/ironic_python_agent/config.py +++ b/ironic_python_agent/config.py @@ -217,6 +217,18 @@ cli_opts = [ 'Must be provided together with "certfile" option. ' 'Default is to not present any client certificates to ' 'the server.'), + cfg.BoolOpt('introspection_daemon', + default=False, + help='When the ``ironic-collect-introspection-data`` ' + 'command is executed, continue running as ' + 'a background process and continue to post data ' + 'to the bare metal inspection service.'), + cfg.IntOpt('introspection_daemon_post_interval', + default=300, + help='The interval in seconds by which to transmit data to ' + 'the bare metal introspection service when the ' + '``ironic-collect-introspection-data`` program is ' + 'executing in daemon mode.'), ] CONF.register_cli_opts(cli_opts) diff --git a/ironic_python_agent/inspect.py b/ironic_python_agent/inspect.py new file mode 100644 index 000000000..b8b8fd73b --- /dev/null +++ b/ironic_python_agent/inspect.py @@ -0,0 +1,86 @@ +# 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 os +import select +import threading + +from ironic_lib import exception +from oslo_config import cfg +from oslo_log import log + +from ironic_python_agent import errors +from ironic_python_agent import inspector + +LOG = log.getLogger(__name__) + + +class IronicInspection(threading.Thread): + """Class for manual inspection functionality.""" + + def __init__(self): + super(IronicInspection, self).__init__() + if bool(cfg.CONF.keyfile) != bool(cfg.CONF.certfile): + LOG.warning("Only one of 'keyfile' and 'certfile' options is " + "defined in config file. Its value will be ignored.") + + def _run(self): + try: + daemon_mode = cfg.CONF.introspection_daemon + post_interval = cfg.CONF.introspection_daemon_post_interval + + inspector.inspect() + if not daemon_mode: + # No reason to continue unless we're in daemon mode. + return + + self.reader, self.writer = os.pipe() + p = select.poll() + p.register(self.reader) + + try: + while daemon_mode: + LOG.info('Sleeping until next check-in.') + # TODO(TheJulia): It would likely be good to introduce + # some jitter into this at some point... + if p.poll(post_interval * 1000): + if os.read(self.reader, 1).decode() == 'a': + break + try: + inspector.inspect() + except errors.InspectionError as e: + # Failures happen, no reason to exit as + # the failure could be intermittent. + LOG.warning('Error reporting introspection ' + 'data: %(err)s', + {'err': e}) + except exception.ServiceLookupFailure as e: + # Likely a mDNS lookup failure. We should + # keep retrying. + LOG.error('Error looking up introspection ' + 'endpoint: %(err)s', + {'err': e}) + + finally: + os.close(self.reader) + os.close(self.writer) + self.reader = None + self.writer = None + except errors.InspectionError as e: + msg = "Inspection failed: %s" % e + raise errors.InspectionError(msg) + + def run(self): + """Run Inspection.""" + if not cfg.CONF.inspection_callback_url: + cfg.CONF.set_override('inspection_callback_url', 'mdns') + self._run() diff --git a/ironic_python_agent/inspector.py b/ironic_python_agent/inspector.py index 54de65d07..6b6458619 100644 --- a/ironic_python_agent/inspector.py +++ b/ironic_python_agent/inspector.py @@ -104,8 +104,8 @@ def inspect(): failures.raise_if_needed() if resp is None: - LOG.info('stopping inspection, as inspector returned an error') - return + raise errors.InspectionError('stopping inspection, as inspector ' + 'returned an error') LOG.info('inspection finished successfully') return resp.get('uuid') diff --git a/ironic_python_agent/tests/unit/test_inspector.py b/ironic_python_agent/tests/unit/test_inspector.py index 501e8c1e5..10f9f9e41 100644 --- a/ironic_python_agent/tests/unit/test_inspector.py +++ b/ironic_python_agent/tests/unit/test_inspector.py @@ -137,11 +137,11 @@ class TestInspect(base.IronicAgentTest): mock_call.return_value = None mock_ext_mgr.return_value = [self.mock_ext] - result = inspector.inspect() + self.assertRaises(errors.InspectionError, + inspector.inspect) self.mock_collect.assert_called_with_failure() mock_call.assert_called_with_failure() - self.assertIsNone(result) @mock.patch.object(requests, 'post', autospec=True) diff --git a/releasenotes/notes/manual-introspection-b04b5c25f5e004ac.yaml b/releasenotes/notes/manual-introspection-b04b5c25f5e004ac.yaml new file mode 100644 index 000000000..5918ee917 --- /dev/null +++ b/releasenotes/notes/manual-introspection-b04b5c25f5e004ac.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Adds a new CLI command ``ironic-collect-introspection-data`` to enable + manually publishing into the ``baremetal-introspection`` service. + Executing this command on a system unknown to the Bare Metal service + will likely result in the machine becoming registered to Ironic, and + as such this command should be used with caution. + + If the capability to update introspection data for running machines + has been enabled in the Bare Metal introspection service, then an + operator may use this command in the ``active`` or ``rescue`` states + to update introspection data. diff --git a/setup.cfg b/setup.cfg index ad246ff6f..61e53e004 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,7 @@ oslo.config.opts = console_scripts = ironic-python-agent = ironic_python_agent.cmd.agent:run + ironic-collect-introspection-data = ironic_python_agent.cmd.inspect:run ironic_python_agent.extensions = standby = ironic_python_agent.extensions.standby:StandbyExtension