Merge "Adds poll mode deployment support"
This commit is contained in:
commit
bfb395837d
@ -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 '
|
||||
|
@ -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'),
|
||||
|
43
ironic_python_agent/extensions/poll.py
Normal file
43
ironic_python_agent/extensions/poll.py
Normal file
@ -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)
|
59
ironic_python_agent/tests/unit/extensions/test_poll.py
Normal file
59
ironic_python_agent/tests/unit/extensions/test_poll.py
Normal file
@ -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)
|
@ -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()
|
||||
|
6
releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml
Normal file
6
releasenotes/notes/poll-mode-063bd36b2b18bffb.yaml
Normal file
@ -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.
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user