From aee1555156706bfebd37d98d6d315229ee024cc4 Mon Sep 17 00:00:00 2001 From: Alexander Gordeev Date: Tue, 25 Mar 2014 18:00:10 +0400 Subject: [PATCH] Get rid of modes. Introduce pluggable extensions Allow multiple extensions to be loaded by switching to ExtensionManager from stevedore. Remove any reference to modes. Change-Id: Ic160478625226b4dd17bd68b3d37f3b05823e519 --- ironic_python_agent/agent.py | 53 ++++++------------- .../api/controllers/v1/status.py | 3 +- ironic_python_agent/base.py | 4 +- ironic_python_agent/decom.py | 4 +- ironic_python_agent/standby.py | 4 +- ironic_python_agent/tests/agent.py | 20 +++++-- ironic_python_agent/tests/api.py | 4 +- ironic_python_agent/tests/decom.py | 8 +-- ironic_python_agent/tests/standby.py | 25 ++++----- setup.cfg | 8 +-- 10 files changed, 59 insertions(+), 74 deletions(-) diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py index bab3449ee..a3fe39400 100644 --- a/ironic_python_agent/agent.py +++ b/ironic_python_agent/agent.py @@ -19,7 +19,7 @@ import threading import time import pkg_resources -from stevedore import driver +from stevedore import extension from wsgiref import simple_server from ironic_python_agent.api import app @@ -38,15 +38,13 @@ def _time(): class IronicPythonAgentStatus(encoding.Serializable): - def __init__(self, mode, started_at, version): - self.mode = mode + def __init__(self, started_at, version): self.started_at = started_at self.version = version def serialize(self): """Turn the status into a dict.""" return utils.get_ordereddict([ - ('mode', self.mode), ('started_at', self.started_at), ('version', self.version), ]) @@ -113,7 +111,6 @@ class IronicPythonAgent(object): self.api_client = ironic_api_client.APIClient(self.api_url) self.listen_address = listen_address self.advertise_address = advertise_address - self.mode_implementation = None self.version = pkg_resources.get_distribution('ironic-python-agent')\ .version self.api = app.VersionSelectorApplication(self) @@ -125,17 +122,15 @@ class IronicPythonAgent(object): self.log = log.getLogger(__name__) self.started_at = None self.node = None - - def get_mode_name(self): - if self.mode_implementation: - return self.mode_implementation.name - else: - return 'NONE' + self.ext_mgr = extension.ExtensionManager( + namespace='ironic_python_agent.extensions', + invoke_on_load=True, + propagate_map_exceptions=True, + ) def get_status(self): """Retrieve a serializable status.""" return IronicPythonAgentStatus( - mode=self.get_mode_name(), started_at=self.started_at, version=self.version ) @@ -162,26 +157,14 @@ class IronicPythonAgent(object): command_parts = command_name.split('.', 1) if len(command_parts) != 2: raise errors.InvalidCommandError( - 'Command name must be of the form .') + 'Command name must be of the form .') return (command_parts[0], command_parts[1]) - def _verify_mode(self, mode_name, command_name): - if not self.mode_implementation: - try: - self.mode_implementation = _load_mode_implementation(mode_name) - except Exception: - raise errors.InvalidCommandError( - 'Unknown mode: {0}'.format(mode_name)) - elif self.get_mode_name().lower() != mode_name: - raise errors.InvalidCommandError( - 'Agent is already in {0} mode'.format(self.get_mode_name())) - def execute_command(self, command_name, **kwargs): """Execute an agent command.""" with self.command_lock: - mode_part, command_part = self._split_command(command_name) - self._verify_mode(mode_part, command_part) + extension_part, command_part = self._split_command(command_name) if len(self.command_results) > 0: last_command = self.command_results.values()[-1] @@ -189,8 +172,12 @@ class IronicPythonAgent(object): raise errors.CommandExecutionError('agent is busy') try: - result = self.mode_implementation.execute(command_part, - **kwargs) + ext = self.ext_mgr[extension_part].obj + result = ext.execute(command_part, **kwargs) + except KeyError: + # Extension Not found + raise errors.RequestedObjectNotFoundError('Extension', + extension_part) except errors.InvalidContentError as e: # Any command may raise a InvalidContentError which will be # returned to the caller directly. @@ -232,16 +219,6 @@ class IronicPythonAgent(object): self.heartbeater.stop() -def _load_mode_implementation(mode_name): - mgr = driver.DriverManager( - namespace='ironic_python_agent.modes', - name=mode_name.lower(), - invoke_on_load=True, - invoke_args=[], - ) - return mgr.driver - - def build_agent(api_url, advertise_host, advertise_port, diff --git a/ironic_python_agent/api/controllers/v1/status.py b/ironic_python_agent/api/controllers/v1/status.py index 7916f09c3..0c7c152b7 100644 --- a/ironic_python_agent/api/controllers/v1/status.py +++ b/ironic_python_agent/api/controllers/v1/status.py @@ -22,14 +22,13 @@ from ironic_python_agent.api.controllers.v1 import base class AgentStatus(base.APIBase): - mode = types.text started_at = base.MultiType(float) version = types.text @classmethod def from_agent_status(cls, status): instance = cls() - for field in ('mode', 'started_at', 'version'): + for field in ('started_at', 'version'): setattr(instance, field, getattr(status, field)) return instance diff --git a/ironic_python_agent/base.py b/ironic_python_agent/base.py index 473c0444d..4401e4e7f 100644 --- a/ironic_python_agent/base.py +++ b/ironic_python_agent/base.py @@ -114,9 +114,9 @@ class AsyncCommandResult(BaseCommandResult): self.command_status = AgentCommandStatus.FAILED -class BaseAgentMode(object): +class BaseAgentExtension(object): def __init__(self, name): - super(BaseAgentMode, self).__init__() + super(BaseAgentExtension, self).__init__() self.log = log.getLogger(__name__) self.name = name self.command_map = {} diff --git a/ironic_python_agent/decom.py b/ironic_python_agent/decom.py index ac841ae10..6dde9be09 100644 --- a/ironic_python_agent/decom.py +++ b/ironic_python_agent/decom.py @@ -17,6 +17,6 @@ limitations under the License. from ironic_python_agent import base -class DecomMode(base.BaseAgentMode): +class DecomExtension(base.BaseAgentExtension): def __init__(self): - super(DecomMode, self).__init__('DECOM') + super(DecomExtension, self).__init__('DECOM') diff --git a/ironic_python_agent/standby.py b/ironic_python_agent/standby.py index c19c03938..8bdbc8bf4 100644 --- a/ironic_python_agent/standby.py +++ b/ironic_python_agent/standby.py @@ -153,9 +153,9 @@ def _validate_image_info(image_info=None, **kwargs): 'element.') -class StandbyMode(base.BaseAgentMode): +class StandbyExtension(base.BaseAgentExtension): def __init__(self): - super(StandbyMode, self).__init__('STANDBY') + super(StandbyExtension, self).__init__('STANDBY') self.command_map['cache_image'] = self.cache_image self.command_map['prepare_image'] = self.prepare_image self.command_map['run_image'] = self.run_image diff --git a/ironic_python_agent/tests/agent.py b/ironic_python_agent/tests/agent.py index 9d3badf1d..0e697d601 100644 --- a/ironic_python_agent/tests/agent.py +++ b/ironic_python_agent/tests/agent.py @@ -20,6 +20,7 @@ import unittest import mock import pkg_resources +from stevedore import extension from wsgiref import simple_server from ironic_python_agent import agent @@ -38,9 +39,9 @@ def foo_execute(*args, **kwargs): return 'command execution succeeded' -class FakeMode(base.BaseAgentMode): +class FakeExtension(base.BaseAgentExtension): def __init__(self): - super(FakeMode, self).__init__('FAKE') + super(FakeExtension, self).__init__('FAKE') class TestHeartbeater(unittest.TestCase): @@ -145,9 +146,12 @@ class TestBaseAgent(unittest.TestCase): def test_execute_command(self): do_something_impl = mock.Mock() - self.agent.mode_implementation = FakeMode() - command_map = self.agent.mode_implementation.command_map - command_map['do_something'] = do_something_impl + fake_extension = FakeExtension() + fake_extension.command_map['do_something'] = do_something_impl + self.agent.ext_mgr = extension.ExtensionManager.\ + make_test_instance([extension.Extension('fake', None, + FakeExtension, + fake_extension)]) self.agent.execute_command('fake.do_something', foo='bar') do_something_impl.assert_called_once_with('do_something', foo='bar') @@ -158,6 +162,12 @@ class TestBaseAgent(unittest.TestCase): 'do_something', foo='bar') + def test_execute_unknown_command(self): + self.assertRaises(errors.RequestedObjectNotFoundError, + self.agent.execute_command, + 'fake.do_something', + foo='bar') + @mock.patch('wsgiref.simple_server.make_server', autospec=True) def test_run(self, wsgi_server_cls): wsgi_server = wsgi_server_cls.return_value diff --git a/ironic_python_agent/tests/api.py b/ironic_python_agent/tests/api.py index 08f9b0fc2..f4e253457 100644 --- a/ironic_python_agent/tests/api.py +++ b/ironic_python_agent/tests/api.py @@ -166,8 +166,7 @@ class TestIronicAPI(unittest.TestCase): self.assertTrue('commands' in data.keys()) def test_get_agent_status(self): - status = agent.IronicPythonAgentStatus('TEST_MODE', - time.time(), + status = agent.IronicPythonAgentStatus(time.time(), 'v72ac9') self.mock_agent.get_status.return_value = status @@ -176,7 +175,6 @@ class TestIronicAPI(unittest.TestCase): self.assertEqual(response.status_code, 200) data = response.json - self.assertEqual(data['mode'], status.mode) self.assertEqual(data['started_at'], status.started_at) self.assertEqual(data['version'], status.version) diff --git a/ironic_python_agent/tests/decom.py b/ironic_python_agent/tests/decom.py index d55909718..737cd7971 100644 --- a/ironic_python_agent/tests/decom.py +++ b/ironic_python_agent/tests/decom.py @@ -19,9 +19,9 @@ import unittest from ironic_python_agent import decom -class TestDecomMode(unittest.TestCase): +class TestDecomExtension(unittest.TestCase): def setUp(self): - self.agent_mode = decom.DecomMode() + self.agent_extension = decom.DecomExtension() - def test_decom_mode(self): - self.assertEqual(self.agent_mode.name, 'DECOM') + def test_decom_extension(self): + self.assertEqual(self.agent_extension.name, 'DECOM') diff --git a/ironic_python_agent/tests/standby.py b/ironic_python_agent/tests/standby.py index bfaa896c5..c88ccf4a8 100644 --- a/ironic_python_agent/tests/standby.py +++ b/ironic_python_agent/tests/standby.py @@ -21,12 +21,12 @@ from ironic_python_agent import errors from ironic_python_agent import standby -class TestStandbyMode(unittest.TestCase): +class TestStandbyExtension(unittest.TestCase): def setUp(self): - self.agent_mode = standby.StandbyMode() + self.agent_extension = standby.StandbyExtension() - def test_standby_mode(self): - self.assertEqual(self.agent_mode.name, 'STANDBY') + def test_standby_extension(self): + self.assertEqual(self.agent_extension.name, 'STANDBY') def _build_fake_image_info(self): return { @@ -84,14 +84,14 @@ class TestStandbyMode(unittest.TestCase): invalid_info) def test_cache_image_success(self): - result = self.agent_mode.cache_image( + result = self.agent_extension.cache_image( 'cache_image', image_info=self._build_fake_image_info()) result.join() def test_cache_image_invalid_image_list(self): self.assertRaises(errors.InvalidCommandParamsError, - self.agent_mode.cache_image, + self.agent_extension.cache_image, 'cache_image', image_info={'foo': 'bar'}) @@ -227,12 +227,13 @@ class TestStandbyMode(unittest.TestCase): write_mock.return_value = None manager_mock = hardware_mock.return_value manager_mock.get_os_install_device.return_value = 'manager' - async_result = self.agent_mode.cache_image('cache_image', + async_result = self.agent_extension.cache_image('cache_image', image_info=image_info) async_result.join() download_mock.assert_called_once_with(image_info) write_mock.assert_called_once_with(image_info, 'manager') - self.assertEqual(self.agent_mode.cached_image_id, image_info['id']) + self.assertEqual(self.agent_extension.cached_image_id, + image_info['id']) self.assertEqual('SUCCEEDED', async_result.command_status) self.assertEqual(None, async_result.command_result) @@ -261,7 +262,7 @@ class TestStandbyMode(unittest.TestCase): configdrive_mock.return_value = None configdrive_copy_mock.return_value = None - async_result = self.agent_mode.prepare_image('prepare_image', + async_result = self.agent_extension.prepare_image('prepare_image', image_info=image_info, metadata={}, files=[]) @@ -280,7 +281,7 @@ class TestStandbyMode(unittest.TestCase): configdrive_mock.reset_mock() configdrive_copy_mock.reset_mock() # image is now cached, make sure download/write doesn't happen - async_result = self.agent_mode.prepare_image('prepare_image', + async_result = self.agent_extension.prepare_image('prepare_image', image_info=image_info, metadata={}, files=[]) @@ -300,14 +301,14 @@ class TestStandbyMode(unittest.TestCase): command = ['/bin/bash', script] call_mock.return_value = 0 - success_result = self.agent_mode.run_image('run_image') + success_result = self.agent_extension.run_image('run_image') success_result.join() call_mock.assert_called_once_with(command) call_mock.reset_mock() call_mock.return_value = 1 - failed_result = self.agent_mode.run_image('run_image') + failed_result = self.agent_extension.run_image('run_image') failed_result.join() call_mock.assert_called_once_with(command) diff --git a/setup.cfg b/setup.cfg index 22b441f64..7ab4a8043 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,9 +18,9 @@ packages = console_scripts = ironic-python-agent = ironic_python_agent.cmd.agent:run -ironic_python_agent.modes = - standby = ironic_python_agent.standby:StandbyMode - decom = ironic_python_agent.decom:DecomMode +ironic_python_agent.extensions = + standby = ironic_python_agent.standby:StandbyExtension + decom = ironic_python_agent.decom:DecomExtension ironic_python_agent.hardware_managers = generic = ironic_python_agent.hardware:GenericHardwareManager @@ -39,4 +39,4 @@ tag_date = 0 tag_svn_revision = 0 [wheel] -universal = 1 \ No newline at end of file +universal = 1