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:
Alexander Gordeev 2014-03-25 18:00:10 +04:00
parent 55ea7b8edd
commit aee1555156
10 changed files with 59 additions and 74 deletions

View File

@ -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,

View File

@ -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

View File

@ -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 = {}

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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