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
This commit is contained in:
parent
55ea7b8edd
commit
aee1555156
@ -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 <mode>.<name>')
|
||||
'Command name must be of the form <extension>.<name>')
|
||||
|
||||
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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = {}
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
universal = 1
|
||||
|
Loading…
Reference in New Issue
Block a user