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 time
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
from stevedore import driver
|
from stevedore import extension
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
from ironic_python_agent.api import app
|
from ironic_python_agent.api import app
|
||||||
@ -38,15 +38,13 @@ def _time():
|
|||||||
|
|
||||||
|
|
||||||
class IronicPythonAgentStatus(encoding.Serializable):
|
class IronicPythonAgentStatus(encoding.Serializable):
|
||||||
def __init__(self, mode, started_at, version):
|
def __init__(self, started_at, version):
|
||||||
self.mode = mode
|
|
||||||
self.started_at = started_at
|
self.started_at = started_at
|
||||||
self.version = version
|
self.version = version
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
"""Turn the status into a dict."""
|
"""Turn the status into a dict."""
|
||||||
return utils.get_ordereddict([
|
return utils.get_ordereddict([
|
||||||
('mode', self.mode),
|
|
||||||
('started_at', self.started_at),
|
('started_at', self.started_at),
|
||||||
('version', self.version),
|
('version', self.version),
|
||||||
])
|
])
|
||||||
@ -113,7 +111,6 @@ class IronicPythonAgent(object):
|
|||||||
self.api_client = ironic_api_client.APIClient(self.api_url)
|
self.api_client = ironic_api_client.APIClient(self.api_url)
|
||||||
self.listen_address = listen_address
|
self.listen_address = listen_address
|
||||||
self.advertise_address = advertise_address
|
self.advertise_address = advertise_address
|
||||||
self.mode_implementation = None
|
|
||||||
self.version = pkg_resources.get_distribution('ironic-python-agent')\
|
self.version = pkg_resources.get_distribution('ironic-python-agent')\
|
||||||
.version
|
.version
|
||||||
self.api = app.VersionSelectorApplication(self)
|
self.api = app.VersionSelectorApplication(self)
|
||||||
@ -125,17 +122,15 @@ class IronicPythonAgent(object):
|
|||||||
self.log = log.getLogger(__name__)
|
self.log = log.getLogger(__name__)
|
||||||
self.started_at = None
|
self.started_at = None
|
||||||
self.node = None
|
self.node = None
|
||||||
|
self.ext_mgr = extension.ExtensionManager(
|
||||||
def get_mode_name(self):
|
namespace='ironic_python_agent.extensions',
|
||||||
if self.mode_implementation:
|
invoke_on_load=True,
|
||||||
return self.mode_implementation.name
|
propagate_map_exceptions=True,
|
||||||
else:
|
)
|
||||||
return 'NONE'
|
|
||||||
|
|
||||||
def get_status(self):
|
def get_status(self):
|
||||||
"""Retrieve a serializable status."""
|
"""Retrieve a serializable status."""
|
||||||
return IronicPythonAgentStatus(
|
return IronicPythonAgentStatus(
|
||||||
mode=self.get_mode_name(),
|
|
||||||
started_at=self.started_at,
|
started_at=self.started_at,
|
||||||
version=self.version
|
version=self.version
|
||||||
)
|
)
|
||||||
@ -162,26 +157,14 @@ class IronicPythonAgent(object):
|
|||||||
command_parts = command_name.split('.', 1)
|
command_parts = command_name.split('.', 1)
|
||||||
if len(command_parts) != 2:
|
if len(command_parts) != 2:
|
||||||
raise errors.InvalidCommandError(
|
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])
|
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):
|
def execute_command(self, command_name, **kwargs):
|
||||||
"""Execute an agent command."""
|
"""Execute an agent command."""
|
||||||
with self.command_lock:
|
with self.command_lock:
|
||||||
mode_part, command_part = self._split_command(command_name)
|
extension_part, command_part = self._split_command(command_name)
|
||||||
self._verify_mode(mode_part, command_part)
|
|
||||||
|
|
||||||
if len(self.command_results) > 0:
|
if len(self.command_results) > 0:
|
||||||
last_command = self.command_results.values()[-1]
|
last_command = self.command_results.values()[-1]
|
||||||
@ -189,8 +172,12 @@ class IronicPythonAgent(object):
|
|||||||
raise errors.CommandExecutionError('agent is busy')
|
raise errors.CommandExecutionError('agent is busy')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.mode_implementation.execute(command_part,
|
ext = self.ext_mgr[extension_part].obj
|
||||||
**kwargs)
|
result = ext.execute(command_part, **kwargs)
|
||||||
|
except KeyError:
|
||||||
|
# Extension Not found
|
||||||
|
raise errors.RequestedObjectNotFoundError('Extension',
|
||||||
|
extension_part)
|
||||||
except errors.InvalidContentError as e:
|
except errors.InvalidContentError as e:
|
||||||
# Any command may raise a InvalidContentError which will be
|
# Any command may raise a InvalidContentError which will be
|
||||||
# returned to the caller directly.
|
# returned to the caller directly.
|
||||||
@ -232,16 +219,6 @@ class IronicPythonAgent(object):
|
|||||||
self.heartbeater.stop()
|
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,
|
def build_agent(api_url,
|
||||||
advertise_host,
|
advertise_host,
|
||||||
advertise_port,
|
advertise_port,
|
||||||
|
@ -22,14 +22,13 @@ from ironic_python_agent.api.controllers.v1 import base
|
|||||||
|
|
||||||
|
|
||||||
class AgentStatus(base.APIBase):
|
class AgentStatus(base.APIBase):
|
||||||
mode = types.text
|
|
||||||
started_at = base.MultiType(float)
|
started_at = base.MultiType(float)
|
||||||
version = types.text
|
version = types.text
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_agent_status(cls, status):
|
def from_agent_status(cls, status):
|
||||||
instance = cls()
|
instance = cls()
|
||||||
for field in ('mode', 'started_at', 'version'):
|
for field in ('started_at', 'version'):
|
||||||
setattr(instance, field, getattr(status, field))
|
setattr(instance, field, getattr(status, field))
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -114,9 +114,9 @@ class AsyncCommandResult(BaseCommandResult):
|
|||||||
self.command_status = AgentCommandStatus.FAILED
|
self.command_status = AgentCommandStatus.FAILED
|
||||||
|
|
||||||
|
|
||||||
class BaseAgentMode(object):
|
class BaseAgentExtension(object):
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super(BaseAgentMode, self).__init__()
|
super(BaseAgentExtension, self).__init__()
|
||||||
self.log = log.getLogger(__name__)
|
self.log = log.getLogger(__name__)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.command_map = {}
|
self.command_map = {}
|
||||||
|
@ -17,6 +17,6 @@ limitations under the License.
|
|||||||
from ironic_python_agent import base
|
from ironic_python_agent import base
|
||||||
|
|
||||||
|
|
||||||
class DecomMode(base.BaseAgentMode):
|
class DecomExtension(base.BaseAgentExtension):
|
||||||
def __init__(self):
|
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.')
|
'element.')
|
||||||
|
|
||||||
|
|
||||||
class StandbyMode(base.BaseAgentMode):
|
class StandbyExtension(base.BaseAgentExtension):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(StandbyMode, self).__init__('STANDBY')
|
super(StandbyExtension, self).__init__('STANDBY')
|
||||||
self.command_map['cache_image'] = self.cache_image
|
self.command_map['cache_image'] = self.cache_image
|
||||||
self.command_map['prepare_image'] = self.prepare_image
|
self.command_map['prepare_image'] = self.prepare_image
|
||||||
self.command_map['run_image'] = self.run_image
|
self.command_map['run_image'] = self.run_image
|
||||||
|
@ -20,6 +20,7 @@ import unittest
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
from stevedore import extension
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
from ironic_python_agent import agent
|
from ironic_python_agent import agent
|
||||||
@ -38,9 +39,9 @@ def foo_execute(*args, **kwargs):
|
|||||||
return 'command execution succeeded'
|
return 'command execution succeeded'
|
||||||
|
|
||||||
|
|
||||||
class FakeMode(base.BaseAgentMode):
|
class FakeExtension(base.BaseAgentExtension):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(FakeMode, self).__init__('FAKE')
|
super(FakeExtension, self).__init__('FAKE')
|
||||||
|
|
||||||
|
|
||||||
class TestHeartbeater(unittest.TestCase):
|
class TestHeartbeater(unittest.TestCase):
|
||||||
@ -145,9 +146,12 @@ class TestBaseAgent(unittest.TestCase):
|
|||||||
|
|
||||||
def test_execute_command(self):
|
def test_execute_command(self):
|
||||||
do_something_impl = mock.Mock()
|
do_something_impl = mock.Mock()
|
||||||
self.agent.mode_implementation = FakeMode()
|
fake_extension = FakeExtension()
|
||||||
command_map = self.agent.mode_implementation.command_map
|
fake_extension.command_map['do_something'] = do_something_impl
|
||||||
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')
|
self.agent.execute_command('fake.do_something', foo='bar')
|
||||||
do_something_impl.assert_called_once_with('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',
|
'do_something',
|
||||||
foo='bar')
|
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)
|
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
||||||
def test_run(self, wsgi_server_cls):
|
def test_run(self, wsgi_server_cls):
|
||||||
wsgi_server = wsgi_server_cls.return_value
|
wsgi_server = wsgi_server_cls.return_value
|
||||||
|
@ -166,8 +166,7 @@ class TestIronicAPI(unittest.TestCase):
|
|||||||
self.assertTrue('commands' in data.keys())
|
self.assertTrue('commands' in data.keys())
|
||||||
|
|
||||||
def test_get_agent_status(self):
|
def test_get_agent_status(self):
|
||||||
status = agent.IronicPythonAgentStatus('TEST_MODE',
|
status = agent.IronicPythonAgentStatus(time.time(),
|
||||||
time.time(),
|
|
||||||
'v72ac9')
|
'v72ac9')
|
||||||
self.mock_agent.get_status.return_value = status
|
self.mock_agent.get_status.return_value = status
|
||||||
|
|
||||||
@ -176,7 +175,6 @@ class TestIronicAPI(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json
|
data = response.json
|
||||||
self.assertEqual(data['mode'], status.mode)
|
|
||||||
self.assertEqual(data['started_at'], status.started_at)
|
self.assertEqual(data['started_at'], status.started_at)
|
||||||
self.assertEqual(data['version'], status.version)
|
self.assertEqual(data['version'], status.version)
|
||||||
|
|
||||||
|
@ -19,9 +19,9 @@ import unittest
|
|||||||
from ironic_python_agent import decom
|
from ironic_python_agent import decom
|
||||||
|
|
||||||
|
|
||||||
class TestDecomMode(unittest.TestCase):
|
class TestDecomExtension(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.agent_mode = decom.DecomMode()
|
self.agent_extension = decom.DecomExtension()
|
||||||
|
|
||||||
def test_decom_mode(self):
|
def test_decom_extension(self):
|
||||||
self.assertEqual(self.agent_mode.name, 'DECOM')
|
self.assertEqual(self.agent_extension.name, 'DECOM')
|
||||||
|
@ -21,12 +21,12 @@ from ironic_python_agent import errors
|
|||||||
from ironic_python_agent import standby
|
from ironic_python_agent import standby
|
||||||
|
|
||||||
|
|
||||||
class TestStandbyMode(unittest.TestCase):
|
class TestStandbyExtension(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.agent_mode = standby.StandbyMode()
|
self.agent_extension = standby.StandbyExtension()
|
||||||
|
|
||||||
def test_standby_mode(self):
|
def test_standby_extension(self):
|
||||||
self.assertEqual(self.agent_mode.name, 'STANDBY')
|
self.assertEqual(self.agent_extension.name, 'STANDBY')
|
||||||
|
|
||||||
def _build_fake_image_info(self):
|
def _build_fake_image_info(self):
|
||||||
return {
|
return {
|
||||||
@ -84,14 +84,14 @@ class TestStandbyMode(unittest.TestCase):
|
|||||||
invalid_info)
|
invalid_info)
|
||||||
|
|
||||||
def test_cache_image_success(self):
|
def test_cache_image_success(self):
|
||||||
result = self.agent_mode.cache_image(
|
result = self.agent_extension.cache_image(
|
||||||
'cache_image',
|
'cache_image',
|
||||||
image_info=self._build_fake_image_info())
|
image_info=self._build_fake_image_info())
|
||||||
result.join()
|
result.join()
|
||||||
|
|
||||||
def test_cache_image_invalid_image_list(self):
|
def test_cache_image_invalid_image_list(self):
|
||||||
self.assertRaises(errors.InvalidCommandParamsError,
|
self.assertRaises(errors.InvalidCommandParamsError,
|
||||||
self.agent_mode.cache_image,
|
self.agent_extension.cache_image,
|
||||||
'cache_image',
|
'cache_image',
|
||||||
image_info={'foo': 'bar'})
|
image_info={'foo': 'bar'})
|
||||||
|
|
||||||
@ -227,12 +227,13 @@ class TestStandbyMode(unittest.TestCase):
|
|||||||
write_mock.return_value = None
|
write_mock.return_value = None
|
||||||
manager_mock = hardware_mock.return_value
|
manager_mock = hardware_mock.return_value
|
||||||
manager_mock.get_os_install_device.return_value = 'manager'
|
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)
|
image_info=image_info)
|
||||||
async_result.join()
|
async_result.join()
|
||||||
download_mock.assert_called_once_with(image_info)
|
download_mock.assert_called_once_with(image_info)
|
||||||
write_mock.assert_called_once_with(image_info, 'manager')
|
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('SUCCEEDED', async_result.command_status)
|
||||||
self.assertEqual(None, async_result.command_result)
|
self.assertEqual(None, async_result.command_result)
|
||||||
|
|
||||||
@ -261,7 +262,7 @@ class TestStandbyMode(unittest.TestCase):
|
|||||||
configdrive_mock.return_value = None
|
configdrive_mock.return_value = None
|
||||||
configdrive_copy_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,
|
image_info=image_info,
|
||||||
metadata={},
|
metadata={},
|
||||||
files=[])
|
files=[])
|
||||||
@ -280,7 +281,7 @@ class TestStandbyMode(unittest.TestCase):
|
|||||||
configdrive_mock.reset_mock()
|
configdrive_mock.reset_mock()
|
||||||
configdrive_copy_mock.reset_mock()
|
configdrive_copy_mock.reset_mock()
|
||||||
# image is now cached, make sure download/write doesn't happen
|
# 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,
|
image_info=image_info,
|
||||||
metadata={},
|
metadata={},
|
||||||
files=[])
|
files=[])
|
||||||
@ -300,14 +301,14 @@ class TestStandbyMode(unittest.TestCase):
|
|||||||
command = ['/bin/bash', script]
|
command = ['/bin/bash', script]
|
||||||
call_mock.return_value = 0
|
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()
|
success_result.join()
|
||||||
call_mock.assert_called_once_with(command)
|
call_mock.assert_called_once_with(command)
|
||||||
|
|
||||||
call_mock.reset_mock()
|
call_mock.reset_mock()
|
||||||
call_mock.return_value = 1
|
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()
|
failed_result.join()
|
||||||
|
|
||||||
call_mock.assert_called_once_with(command)
|
call_mock.assert_called_once_with(command)
|
||||||
|
@ -18,9 +18,9 @@ packages =
|
|||||||
console_scripts =
|
console_scripts =
|
||||||
ironic-python-agent = ironic_python_agent.cmd.agent:run
|
ironic-python-agent = ironic_python_agent.cmd.agent:run
|
||||||
|
|
||||||
ironic_python_agent.modes =
|
ironic_python_agent.extensions =
|
||||||
standby = ironic_python_agent.standby:StandbyMode
|
standby = ironic_python_agent.standby:StandbyExtension
|
||||||
decom = ironic_python_agent.decom:DecomMode
|
decom = ironic_python_agent.decom:DecomExtension
|
||||||
|
|
||||||
ironic_python_agent.hardware_managers =
|
ironic_python_agent.hardware_managers =
|
||||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||||
@ -39,4 +39,4 @@ tag_date = 0
|
|||||||
tag_svn_revision = 0
|
tag_svn_revision = 0
|
||||||
|
|
||||||
[wheel]
|
[wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
Loading…
Reference in New Issue
Block a user