Skip to content

Commit aee1555

Browse files
author
Alexander Gordeev
committed
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
1 parent 55ea7b8 commit aee1555

File tree

10 files changed

+59
-74
lines changed

10 files changed

+59
-74
lines changed

ironic_python_agent/agent.py

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import time
2020

2121
import pkg_resources
22-
from stevedore import driver
22+
from stevedore import extension
2323
from wsgiref import simple_server
2424

2525
from ironic_python_agent.api import app
@@ -38,15 +38,13 @@ def _time():
3838

3939

4040
class IronicPythonAgentStatus(encoding.Serializable):
41-
def __init__(self, mode, started_at, version):
42-
self.mode = mode
41+
def __init__(self, started_at, version):
4342
self.started_at = started_at
4443
self.version = version
4544

4645
def serialize(self):
4746
"""Turn the status into a dict."""
4847
return utils.get_ordereddict([
49-
('mode', self.mode),
5048
('started_at', self.started_at),
5149
('version', self.version),
5250
])
@@ -113,7 +111,6 @@ def __init__(self, api_url, advertise_address, listen_address):
113111
self.api_client = ironic_api_client.APIClient(self.api_url)
114112
self.listen_address = listen_address
115113
self.advertise_address = advertise_address
116-
self.mode_implementation = None
117114
self.version = pkg_resources.get_distribution('ironic-python-agent')\
118115
.version
119116
self.api = app.VersionSelectorApplication(self)
@@ -125,17 +122,15 @@ def __init__(self, api_url, advertise_address, listen_address):
125122
self.log = log.getLogger(__name__)
126123
self.started_at = None
127124
self.node = None
128-
129-
def get_mode_name(self):
130-
if self.mode_implementation:
131-
return self.mode_implementation.name
132-
else:
133-
return 'NONE'
125+
self.ext_mgr = extension.ExtensionManager(
126+
namespace='ironic_python_agent.extensions',
127+
invoke_on_load=True,
128+
propagate_map_exceptions=True,
129+
)
134130

135131
def get_status(self):
136132
"""Retrieve a serializable status."""
137133
return IronicPythonAgentStatus(
138-
mode=self.get_mode_name(),
139134
started_at=self.started_at,
140135
version=self.version
141136
)
@@ -162,35 +157,27 @@ def _split_command(self, command_name):
162157
command_parts = command_name.split('.', 1)
163158
if len(command_parts) != 2:
164159
raise errors.InvalidCommandError(
165-
'Command name must be of the form <mode>.<name>')
160+
'Command name must be of the form <extension>.<name>')
166161

167162
return (command_parts[0], command_parts[1])
168163

169-
def _verify_mode(self, mode_name, command_name):
170-
if not self.mode_implementation:
171-
try:
172-
self.mode_implementation = _load_mode_implementation(mode_name)
173-
except Exception:
174-
raise errors.InvalidCommandError(
175-
'Unknown mode: {0}'.format(mode_name))
176-
elif self.get_mode_name().lower() != mode_name:
177-
raise errors.InvalidCommandError(
178-
'Agent is already in {0} mode'.format(self.get_mode_name()))
179-
180164
def execute_command(self, command_name, **kwargs):
181165
"""Execute an agent command."""
182166
with self.command_lock:
183-
mode_part, command_part = self._split_command(command_name)
184-
self._verify_mode(mode_part, command_part)
167+
extension_part, command_part = self._split_command(command_name)
185168

186169
if len(self.command_results) > 0:
187170
last_command = self.command_results.values()[-1]
188171
if not last_command.is_done():
189172
raise errors.CommandExecutionError('agent is busy')
190173

191174
try:
192-
result = self.mode_implementation.execute(command_part,
193-
**kwargs)
175+
ext = self.ext_mgr[extension_part].obj
176+
result = ext.execute(command_part, **kwargs)
177+
except KeyError:
178+
# Extension Not found
179+
raise errors.RequestedObjectNotFoundError('Extension',
180+
extension_part)
194181
except errors.InvalidContentError as e:
195182
# Any command may raise a InvalidContentError which will be
196183
# returned to the caller directly.
@@ -232,16 +219,6 @@ def run(self):
232219
self.heartbeater.stop()
233220

234221

235-
def _load_mode_implementation(mode_name):
236-
mgr = driver.DriverManager(
237-
namespace='ironic_python_agent.modes',
238-
name=mode_name.lower(),
239-
invoke_on_load=True,
240-
invoke_args=[],
241-
)
242-
return mgr.driver
243-
244-
245222
def build_agent(api_url,
246223
advertise_host,
247224
advertise_port,

ironic_python_agent/api/controllers/v1/status.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@
2222

2323

2424
class AgentStatus(base.APIBase):
25-
mode = types.text
2625
started_at = base.MultiType(float)
2726
version = types.text
2827

2928
@classmethod
3029
def from_agent_status(cls, status):
3130
instance = cls()
32-
for field in ('mode', 'started_at', 'version'):
31+
for field in ('started_at', 'version'):
3332
setattr(instance, field, getattr(status, field))
3433
return instance
3534

ironic_python_agent/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,9 @@ def run(self):
114114
self.command_status = AgentCommandStatus.FAILED
115115

116116

117-
class BaseAgentMode(object):
117+
class BaseAgentExtension(object):
118118
def __init__(self, name):
119-
super(BaseAgentMode, self).__init__()
119+
super(BaseAgentExtension, self).__init__()
120120
self.log = log.getLogger(__name__)
121121
self.name = name
122122
self.command_map = {}

ironic_python_agent/decom.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
from ironic_python_agent import base
1818

1919

20-
class DecomMode(base.BaseAgentMode):
20+
class DecomExtension(base.BaseAgentExtension):
2121
def __init__(self):
22-
super(DecomMode, self).__init__('DECOM')
22+
super(DecomExtension, self).__init__('DECOM')

ironic_python_agent/standby.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ def _validate_image_info(image_info=None, **kwargs):
153153
'element.')
154154

155155

156-
class StandbyMode(base.BaseAgentMode):
156+
class StandbyExtension(base.BaseAgentExtension):
157157
def __init__(self):
158-
super(StandbyMode, self).__init__('STANDBY')
158+
super(StandbyExtension, self).__init__('STANDBY')
159159
self.command_map['cache_image'] = self.cache_image
160160
self.command_map['prepare_image'] = self.prepare_image
161161
self.command_map['run_image'] = self.run_image

ironic_python_agent/tests/agent.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import mock
2222
import pkg_resources
23+
from stevedore import extension
2324
from wsgiref import simple_server
2425

2526
from ironic_python_agent import agent
@@ -38,9 +39,9 @@ def foo_execute(*args, **kwargs):
3839
return 'command execution succeeded'
3940

4041

41-
class FakeMode(base.BaseAgentMode):
42+
class FakeExtension(base.BaseAgentExtension):
4243
def __init__(self):
43-
super(FakeMode, self).__init__('FAKE')
44+
super(FakeExtension, self).__init__('FAKE')
4445

4546

4647
class TestHeartbeater(unittest.TestCase):
@@ -145,9 +146,12 @@ def test_get_status(self):
145146

146147
def test_execute_command(self):
147148
do_something_impl = mock.Mock()
148-
self.agent.mode_implementation = FakeMode()
149-
command_map = self.agent.mode_implementation.command_map
150-
command_map['do_something'] = do_something_impl
149+
fake_extension = FakeExtension()
150+
fake_extension.command_map['do_something'] = do_something_impl
151+
self.agent.ext_mgr = extension.ExtensionManager.\
152+
make_test_instance([extension.Extension('fake', None,
153+
FakeExtension,
154+
fake_extension)])
151155

152156
self.agent.execute_command('fake.do_something', foo='bar')
153157
do_something_impl.assert_called_once_with('do_something', foo='bar')
@@ -158,6 +162,12 @@ def test_execute_invalid_command(self):
158162
'do_something',
159163
foo='bar')
160164

165+
def test_execute_unknown_command(self):
166+
self.assertRaises(errors.RequestedObjectNotFoundError,
167+
self.agent.execute_command,
168+
'fake.do_something',
169+
foo='bar')
170+
161171
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
162172
def test_run(self, wsgi_server_cls):
163173
wsgi_server = wsgi_server_cls.return_value

ironic_python_agent/tests/api.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,7 @@ def test_v1_root(self):
166166
self.assertTrue('commands' in data.keys())
167167

168168
def test_get_agent_status(self):
169-
status = agent.IronicPythonAgentStatus('TEST_MODE',
170-
time.time(),
169+
status = agent.IronicPythonAgentStatus(time.time(),
171170
'v72ac9')
172171
self.mock_agent.get_status.return_value = status
173172

@@ -176,7 +175,6 @@ def test_get_agent_status(self):
176175

177176
self.assertEqual(response.status_code, 200)
178177
data = response.json
179-
self.assertEqual(data['mode'], status.mode)
180178
self.assertEqual(data['started_at'], status.started_at)
181179
self.assertEqual(data['version'], status.version)
182180

ironic_python_agent/tests/decom.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
from ironic_python_agent import decom
2020

2121

22-
class TestDecomMode(unittest.TestCase):
22+
class TestDecomExtension(unittest.TestCase):
2323
def setUp(self):
24-
self.agent_mode = decom.DecomMode()
24+
self.agent_extension = decom.DecomExtension()
2525

26-
def test_decom_mode(self):
27-
self.assertEqual(self.agent_mode.name, 'DECOM')
26+
def test_decom_extension(self):
27+
self.assertEqual(self.agent_extension.name, 'DECOM')

ironic_python_agent/tests/standby.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
from ironic_python_agent import standby
2222

2323

24-
class TestStandbyMode(unittest.TestCase):
24+
class TestStandbyExtension(unittest.TestCase):
2525
def setUp(self):
26-
self.agent_mode = standby.StandbyMode()
26+
self.agent_extension = standby.StandbyExtension()
2727

28-
def test_standby_mode(self):
29-
self.assertEqual(self.agent_mode.name, 'STANDBY')
28+
def test_standby_extension(self):
29+
self.assertEqual(self.agent_extension.name, 'STANDBY')
3030

3131
def _build_fake_image_info(self):
3232
return {
@@ -84,14 +84,14 @@ def test_validate_image_info_empty_hashes(self):
8484
invalid_info)
8585

8686
def test_cache_image_success(self):
87-
result = self.agent_mode.cache_image(
87+
result = self.agent_extension.cache_image(
8888
'cache_image',
8989
image_info=self._build_fake_image_info())
9090
result.join()
9191

9292
def test_cache_image_invalid_image_list(self):
9393
self.assertRaises(errors.InvalidCommandParamsError,
94-
self.agent_mode.cache_image,
94+
self.agent_extension.cache_image,
9595
'cache_image',
9696
image_info={'foo': 'bar'})
9797

@@ -227,12 +227,13 @@ def test_cache_image(self, download_mock, write_mock, hardware_mock):
227227
write_mock.return_value = None
228228
manager_mock = hardware_mock.return_value
229229
manager_mock.get_os_install_device.return_value = 'manager'
230-
async_result = self.agent_mode.cache_image('cache_image',
230+
async_result = self.agent_extension.cache_image('cache_image',
231231
image_info=image_info)
232232
async_result.join()
233233
download_mock.assert_called_once_with(image_info)
234234
write_mock.assert_called_once_with(image_info, 'manager')
235-
self.assertEqual(self.agent_mode.cached_image_id, image_info['id'])
235+
self.assertEqual(self.agent_extension.cached_image_id,
236+
image_info['id'])
236237
self.assertEqual('SUCCEEDED', async_result.command_status)
237238
self.assertEqual(None, async_result.command_result)
238239

@@ -261,7 +262,7 @@ def test_prepare_image(self,
261262
configdrive_mock.return_value = None
262263
configdrive_copy_mock.return_value = None
263264

264-
async_result = self.agent_mode.prepare_image('prepare_image',
265+
async_result = self.agent_extension.prepare_image('prepare_image',
265266
image_info=image_info,
266267
metadata={},
267268
files=[])
@@ -280,7 +281,7 @@ def test_prepare_image(self,
280281
configdrive_mock.reset_mock()
281282
configdrive_copy_mock.reset_mock()
282283
# image is now cached, make sure download/write doesn't happen
283-
async_result = self.agent_mode.prepare_image('prepare_image',
284+
async_result = self.agent_extension.prepare_image('prepare_image',
284285
image_info=image_info,
285286
metadata={},
286287
files=[])
@@ -300,14 +301,14 @@ def test_run_image(self, call_mock):
300301
command = ['/bin/bash', script]
301302
call_mock.return_value = 0
302303

303-
success_result = self.agent_mode.run_image('run_image')
304+
success_result = self.agent_extension.run_image('run_image')
304305
success_result.join()
305306
call_mock.assert_called_once_with(command)
306307

307308
call_mock.reset_mock()
308309
call_mock.return_value = 1
309310

310-
failed_result = self.agent_mode.run_image('run_image')
311+
failed_result = self.agent_extension.run_image('run_image')
311312
failed_result.join()
312313

313314
call_mock.assert_called_once_with(command)

setup.cfg

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ packages =
1818
console_scripts =
1919
ironic-python-agent = ironic_python_agent.cmd.agent:run
2020

21-
ironic_python_agent.modes =
22-
standby = ironic_python_agent.standby:StandbyMode
23-
decom = ironic_python_agent.decom:DecomMode
21+
ironic_python_agent.extensions =
22+
standby = ironic_python_agent.standby:StandbyExtension
23+
decom = ironic_python_agent.decom:DecomExtension
2424

2525
ironic_python_agent.hardware_managers =
2626
generic = ironic_python_agent.hardware:GenericHardwareManager
@@ -39,4 +39,4 @@ tag_date = 0
3939
tag_svn_revision = 0
4040

4141
[wheel]
42-
universal = 1
42+
universal = 1

0 commit comments

Comments
 (0)