summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-07-22 19:53:31 +0000
committerGerrit Code Review <review@openstack.org>2020-07-22 19:53:31 +0000
commitbfb395837dddb244b268166008efed4a19e5fc1f (patch)
tree2bbc645ed22ab9a4a1132b0ddd7f814e13052b25
parentc66bec0f43cfeb371a8ac97df3eddca3853191dc (diff)
parent61c95554ff07420f8c57e97b9b4a5af909258012 (diff)
downloadironic-python-agent-bfb395837dddb244b268166008efed4a19e5fc1f.tar.gz
Merge "Adds poll mode deployment support"
-rw-r--r--ironic_python_agent/agent.py91
-rw-r--r--ironic_python_agent/config.py7
-rw-r--r--ironic_python_agent/extensions/poll.py43
-rw-r--r--ironic_python_agent/tests/unit/extensions/test_poll.py59
-rw-r--r--ironic_python_agent/tests/unit/test_agent.py1
-rw-r--r--releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml6
-rw-r--r--setup.cfg1
7 files changed, 161 insertions, 47 deletions
diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py
index 207b9a45..5907a201 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 84566d85..d1c4dc95 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 00000000..1d20647e
--- /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 00000000..b144d104
--- /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 6c461ab4..1a2b75a8 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 00000000..dd971d91
--- /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 47c50c25..5e67ccfc 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