Merge "Add a command to lock down the agent"

This commit is contained in:
Zuul 2024-11-21 16:20:33 +00:00 committed by Gerrit Code Review
commit 01639aab20
5 changed files with 118 additions and 0 deletions

View File

@ -36,6 +36,7 @@ from ironic_python_agent.extensions import base
from ironic_python_agent import hardware from ironic_python_agent import hardware
from ironic_python_agent import inspector from ironic_python_agent import inspector
from ironic_python_agent import ironic_api_client from ironic_python_agent import ironic_api_client
from ironic_python_agent import netutils
from ironic_python_agent import utils from ironic_python_agent import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -248,6 +249,9 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
self.hardware_initialization_delay = hardware_initialization_delay self.hardware_initialization_delay = hardware_initialization_delay
# IPA will stop serving requests and exit after this is set to False # IPA will stop serving requests and exit after this is set to False
self.serve_api = True self.serve_api = True
# Together with serve_api, this option allows locking down the system
# before IPA stops.
self.lockdown = False
self.agent_token = agent_token self.agent_token = agent_token
# Allows this to be turned on by the conductor while running, # Allows this to be turned on by the conductor while running,
# in the event of long running ramdisks where the conductor # in the event of long running ramdisks where the conductor
@ -568,3 +572,21 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
if not self.standalone and self.api_urls: if not self.standalone and self.api_urls:
self.heartbeater.stop() self.heartbeater.stop()
if self.lockdown:
self._lockdown_system()
LOG.info('System locked down, looping forever to avoid a service '
'restart')
while True:
time.sleep(100)
def _lockdown_system(self):
LOG.info('Locking down system after the API stopped')
# NOTE(dtantsur): not going through hardware managers here to minimize
# the amount of operations.
for iface in netutils.list_interfaces():
try:
utils.execute('ip', 'link', 'set', iface, 'down')
except Exception as exc:
LOG.warning('Could not bring down interface %s: %s',
iface, exc)

View File

@ -0,0 +1,29 @@
# 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.extensions import base
LOG = log.getLogger(__name__)
class SystemExtension(base.BaseAgentExtension):
# TODO(dtantsur): migrate (with deprecation) other system-wide commands
# from standby (power_off, run_image renamed into reboot, sync).
@base.sync_command('lockdown')
def lockdown(self):
"""Lock the agent down to prevent interactions with it."""
self.agent.lockdown = True
self.agent.serve_api = False

View File

@ -741,6 +741,65 @@ class TestBaseAgent(ironic_agent_base.IronicAgentTest):
self.assertTrue(mock_wait.called) self.assertTrue(mock_wait.called)
self.assertFalse(mock_dispatch.called) self.assertFalse(mock_dispatch.called)
@mock.patch.object(time, 'sleep', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(netutils, 'list_interfaces', autospec=True)
@mock.patch(
'ironic_python_agent.hardware_managers.cna._detect_cna_card',
mock.Mock())
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)
@mock.patch.object(agent.IronicPythonAgent,
'_wait_for_interface', autospec=True)
@mock.patch('oslo_service.wsgi.Server', autospec=True)
@mock.patch.object(hardware, 'get_managers', autospec=True)
def test_run_then_lockdown(self, mock_get_managers, mock_wsgi,
mock_wait, mock_dispatch, mock_interfaces,
mock_exec, mock_sleep):
CONF.set_override('inspection_callback_url', '')
wsgi_server = mock_wsgi.return_value
def set_serve_api():
self.agent.lockdown = True
self.agent.serve_api = False
wsgi_server.start.side_effect = set_serve_api
self.agent.heartbeater = mock.Mock()
self.agent.api_client.lookup_node = mock.Mock()
self.agent.api_client.lookup_node.return_value = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'config': {
'heartbeat_timeout': 300,
'agent_md5_checksum_enable': False
}
}
mock_interfaces.return_value = ['em1', 'em2']
class StopTesting(Exception):
"""Exception to exit the infinite loop."""
mock_sleep.side_effect = StopTesting
self.assertRaises(StopTesting, self.agent.run)
mock_wsgi.assert_called_once_with(CONF, 'ironic-python-agent',
app=self.agent.api,
host=mock.ANY, port=9999,
use_ssl=False)
wsgi_server.start.assert_called_once_with()
mock_wait.assert_called_once_with(mock.ANY)
self.assertEqual([mock.call('list_hardware_info'),
mock.call('wait_for_disks')],
mock_dispatch.call_args_list)
self.agent.heartbeater.start.assert_called_once_with()
self.agent.heartbeater.stop.assert_called_once_with()
mock_exec.assert_has_calls([
mock.call('ip', 'link', 'set', iface, 'down')
for iface in ['em1', 'em2']
])
@mock.patch.object(time, 'time', autospec=True) @mock.patch.object(time, 'time', autospec=True)
@mock.patch.object(time, 'sleep', autospec=True) @mock.patch.object(time, 'sleep', autospec=True)
@mock.patch.object(hardware, 'dispatch_to_managers', autospec=True) @mock.patch.object(hardware, 'dispatch_to_managers', autospec=True)

View File

@ -0,0 +1,7 @@
---
features:
- |
Adds a new API command ``system.lockdown``. When invoked, it stops the API,
the heartbeater and tries to disable all local network interfaces. More
actions may be added in the future to make the agent and the ramdisk
unusable after this command.

View File

@ -45,6 +45,7 @@ ironic_python_agent.extensions =
rescue = ironic_python_agent.extensions.rescue:RescueExtension rescue = ironic_python_agent.extensions.rescue:RescueExtension
poll = ironic_python_agent.extensions.poll:PollExtension poll = ironic_python_agent.extensions.poll:PollExtension
service = ironic_python_agent.extensions.service:ServiceExtension service = ironic_python_agent.extensions.service:ServiceExtension
system = ironic_python_agent.extensions.system:SystemExtension
ironic_python_agent.hardware_managers = ironic_python_agent.hardware_managers =
generic = ironic_python_agent.hardware:GenericHardwareManager generic = ironic_python_agent.hardware:GenericHardwareManager