diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py index 207b9a45b..5907a2016 100644 --- a/ironic_python_agent/agent.py +++ b/ironic_python_agent/agent.py @@ -172,7 +172,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin): "defined in config file. Its value will be ignored.") self.ext_mgr = base.init_ext_manager(self) self.api_url = api_url - if not self.api_url or self.api_url == 'mdns': + if (not self.api_url or self.api_url == 'mdns') and not standalone: try: self.api_url, params = mdns.get_endpoint('baremetal') except lib_exc.ServiceLookupFailure: @@ -380,6 +380,52 @@ class IronicPythonAgent(base.ExecuteCommandMixin): LOG.info('Caught keyboard interrupt, exiting') self.api.stop() + def process_lookup_data(self, content): + """Update agent configuration from lookup data.""" + + self.node = content['node'] + LOG.info('Lookup succeeded, node UUID is %s', + self.node['uuid']) + hardware.cache_node(self.node) + self.heartbeat_timeout = content['config']['heartbeat_timeout'] + + # Update config with values from Ironic + config = content.get('config', {}) + if config.get('metrics'): + for opt, val in config.items(): + setattr(cfg.CONF.metrics, opt, val) + if config.get('metrics_statsd'): + for opt, val in config.items(): + setattr(cfg.CONF.metrics_statsd, opt, val) + if config.get('agent_token_required'): + self.agent_token_required = True + token = config.get('agent_token') + if token: + if len(token) >= 32: + LOG.debug('Agent token recorded as designated by ' + 'the ironic installation.') + self.agent_token = token + # set with-in the API client. + if not self.standalone: + self.api_client.agent_token = token + elif token == '******': + LOG.warning('The agent token has already been ' + 'retrieved. IPA may not operate as ' + 'intended and the deployment may fail ' + 'depending on settings in the ironic ' + 'deployment.') + if not self.agent_token and self.agent_token_required: + LOG.error('Ironic is signaling that agent tokens ' + 'are required, however we do not have ' + 'a token on file. ' + 'This is likely **FATAL**.') + else: + LOG.info('An invalid token was received.') + if self.agent_token and not self.standalone: + # Explicitly set the token in our API client before + # starting heartbeat operations. + self.api_client.agent_token = self.agent_token + def run(self): """Run the Ironic Python Agent.""" LOG.info('Starting ironic-python-agent version: %s', @@ -421,49 +467,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin): timeout=self.lookup_timeout, starting_interval=self.lookup_interval, node_uuid=uuid) - LOG.debug('Received lookup results: %s', content) - self.node = content['node'] - LOG.info('Lookup succeeded, node UUID is %s', - self.node['uuid']) - hardware.cache_node(self.node) - self.heartbeat_timeout = content['config']['heartbeat_timeout'] - - # Update config with values from Ironic - config = content.get('config', {}) - if config.get('metrics'): - for opt, val in config.items(): - setattr(cfg.CONF.metrics, opt, val) - if config.get('metrics_statsd'): - for opt, val in config.items(): - setattr(cfg.CONF.metrics_statsd, opt, val) - if config.get('agent_token_required'): - self.agent_token_required = True - token = config.get('agent_token') - if token: - if len(token) >= 32: - LOG.debug('Agent token recorded as designated by ' - 'the ironic installation.') - self.agent_token = token - # set with-in the API client. - self.api_client.agent_token = token - elif token == '******': - LOG.warning('The agent token has already been ' - 'retrieved. IPA may not operate as ' - 'intended and the deployment may fail ' - 'depending on settings in the ironic ' - 'deployment.') - if not self.agent_token and self.agent_token_required: - LOG.error('Ironic is signaling that agent tokens ' - 'are required, however we do not have ' - 'a token on file. ' - 'This is likely **FATAL**.') - else: - LOG.info('An invalid token was received.') - if self.agent_token: - # Explicitly set the token in our API client before - # starting heartbeat operations. - self.api_client.agent_token = self.agent_token + self.process_lookup_data(content) elif cfg.CONF.inspection_callback_url: LOG.info('No ipa-api-url configured, Heartbeat and lookup ' diff --git a/ironic_python_agent/config.py b/ironic_python_agent/config.py index 84566d859..d1c4dc954 100644 --- a/ironic_python_agent/config.py +++ b/ironic_python_agent/config.py @@ -119,10 +119,9 @@ cli_opts = [ cfg.BoolOpt('standalone', default=APARAMS.get('ipa-standalone', False), - help='Note: for debugging only. Start the Agent but suppress ' - 'any calls to Ironic API. ' - 'Can be supplied as "ipa-standalone" ' - 'kernel parameter.'), + help='Start the Agent but suppress any calls to Ironic API, ' + 'the agent runs on this mode for poll mode deployment. ' + 'Can be supplied as "ipa-standalone" kernel parameter.'), cfg.StrOpt('inspection_callback_url', default=APARAMS.get('ipa-inspection-callback-url'), diff --git a/ironic_python_agent/extensions/poll.py b/ironic_python_agent/extensions/poll.py new file mode 100644 index 000000000..1d20647eb --- /dev/null +++ b/ironic_python_agent/extensions/poll.py @@ -0,0 +1,43 @@ +# 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. + +from oslo_log import log + +from ironic_python_agent import errors +from ironic_python_agent.extensions import base +from ironic_python_agent import hardware + +LOG = log.getLogger(__name__) + + +class PollExtension(base.BaseAgentExtension): + + @base.sync_command('get_hardware_info') + def get_hardware_info(self): + """Get the hardware information where IPA is running.""" + hardware_info = hardware.dispatch_to_managers('list_hardware_info') + return hardware_info + + @base.sync_command('set_node_info') + def set_node_info(self, node_info=None): + """Set node lookup data when IPA is running at passive mode. + + :param node_info: A dictionary contains the information of the node + where IPA is running. + """ + if not self.agent.standalone: + error_msg = ('Node lookup data can only be set when the Ironic ' + 'Python Agent is running in standalone mode.') + LOG.error(error_msg) + raise errors.InvalidCommandError(error_msg) + LOG.debug('Received lookup results: %s', node_info) + self.agent.process_lookup_data(node_info) diff --git a/ironic_python_agent/tests/unit/extensions/test_poll.py b/ironic_python_agent/tests/unit/extensions/test_poll.py new file mode 100644 index 000000000..b144d1047 --- /dev/null +++ b/ironic_python_agent/tests/unit/extensions/test_poll.py @@ -0,0 +1,59 @@ +# 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. + +from unittest import mock + +from ironic_python_agent import agent +from ironic_python_agent import errors +from ironic_python_agent.extensions import poll +from ironic_python_agent import hardware +from ironic_python_agent.tests.unit import base + + +class TestPollExtension(base.IronicAgentTest): + def setUp(self): + super(TestPollExtension, self).setUp() + self.mock_agent = mock.Mock(spec=agent.IronicPythonAgent) + self.agent_extension = poll.PollExtension(agent=self.mock_agent) + self.fake_cpu = hardware.CPU(model_name='fuzzypickles', + frequency=1024, + count=1, + architecture='generic', + flags='') + + @mock.patch.object(hardware, 'dispatch_to_managers', + autospec=True) + def test_get_hardware_info_success(self, mock_dispatch): + mock_dispatch.return_value = {'foo': 'bar'} + result = self.agent_extension.get_hardware_info() + mock_dispatch.assert_called_once_with('list_hardware_info') + self.assertEqual({'foo': 'bar'}, result.command_result) + self.assertEqual('SUCCEEDED', result.command_status) + + def test_set_node_info_success(self): + self.mock_agent.standalone = True + node_info = {'node': {'uuid': 'fake-node', 'properties': {}}, + 'config': {'agent_token_required': True, + 'agent_token': 'blah' * 8}} + result = self.agent_extension.set_node_info(node_info=node_info) + self.mock_agent.process_lookup_data.assert_called_once_with(node_info) + self.assertEqual('SUCCEEDED', result.command_status) + + def test_set_node_info_not_standalone(self): + self.mock_agent.standalone = False + node_info = {'node': {'uuid': 'fake-node', 'properties': {}}, + 'config': {'agent_token_required': True, + 'agent_token': 'blah' * 8}} + self.assertRaises(errors.InvalidCommandError, + self.agent_extension.set_node_info, + node_info=node_info) + self.assertFalse(self.mock_agent.process_lookup_data.called) diff --git a/ironic_python_agent/tests/unit/test_agent.py b/ironic_python_agent/tests/unit/test_agent.py index 6c461ab4c..1a2b75a84 100644 --- a/ironic_python_agent/tests/unit/test_agent.py +++ b/ironic_python_agent/tests/unit/test_agent.py @@ -774,6 +774,7 @@ class TestAgentStandalone(ironic_agent_base.IronicAgentTest): wsgi_server_request.start.side_effect = set_serve_api self.agent.heartbeater = mock.Mock() + self.agent.api_client = mock.Mock() self.agent.api_client.lookup_node = mock.Mock() self.agent.run() diff --git a/releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml b/releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml new file mode 100644 index 000000000..dd971d91e --- /dev/null +++ b/releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds a Poll extension which provides the ability to retrieve hardware + information as well as set node data from API. This feature is required + for poll mode deployment driven by ironic. diff --git a/setup.cfg b/setup.cfg index 47c50c259..5e67ccfcd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ ironic_python_agent.extensions = image = ironic_python_agent.extensions.image:ImageExtension log = ironic_python_agent.extensions.log:LogExtension rescue = ironic_python_agent.extensions.rescue:RescueExtension + poll = ironic_python_agent.extensions.poll:PollExtension ironic_python_agent.hardware_managers = generic = ironic_python_agent.hardware:GenericHardwareManager