Add standalone mode for IPA

This allows a developer to run IPA without an Ironic API. This can
be useful for testing (especially functional testing) or testing
integration of things like hardware managers.

Change-Id: I2dc49fbe306430bf5b05a36fe56de5275fc128b2
This commit is contained in:
Josh Gachnang
2014-12-15 17:29:14 -08:00
parent 86d4b41709
commit 417bf086a5
3 changed files with 70 additions and 15 deletions

View File

@@ -137,7 +137,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
def __init__(self, api_url, advertise_address, listen_address, def __init__(self, api_url, advertise_address, listen_address,
ip_lookup_attempts, ip_lookup_sleep, network_interface, ip_lookup_attempts, ip_lookup_sleep, network_interface,
lookup_timeout, lookup_interval, driver_name): lookup_timeout, lookup_interval, driver_name, standalone):
super(IronicPythonAgent, self).__init__() super(IronicPythonAgent, self).__init__()
self.ext_mgr = extension.ExtensionManager( self.ext_mgr = extension.ExtensionManager(
namespace='ironic_python_agent.extensions', namespace='ironic_python_agent.extensions',
@@ -166,6 +166,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
self.ip_lookup_attempts = ip_lookup_attempts self.ip_lookup_attempts = ip_lookup_attempts
self.ip_lookup_sleep = ip_lookup_sleep self.ip_lookup_sleep = ip_lookup_sleep
self.network_interface = network_interface self.network_interface = network_interface
self.standalone = standalone
def get_status(self): def get_status(self):
"""Retrieve a serializable status. """Retrieve a serializable status.
@@ -267,20 +268,22 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
result_id) result_id)
def force_heartbeat(self): def force_heartbeat(self):
self.heartbeater.force_heartbeat() if not self.standalone:
self.heartbeater.force_heartbeat()
def run(self): def run(self):
"""Run the Ironic Python Agent.""" """Run the Ironic Python Agent."""
# Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError # Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError
# if there is an issue (uncaught, restart agent) # if there is an issue (uncaught, restart agent)
self.started_at = _time() self.started_at = _time()
content = self.api_client.lookup_node( if not self.standalone:
content = self.api_client.lookup_node(
hardware_info=self.hardware.list_hardware_info(), hardware_info=self.hardware.list_hardware_info(),
timeout=self.lookup_timeout, timeout=self.lookup_timeout,
starting_interval=self.lookup_interval) starting_interval=self.lookup_interval)
self.node = content['node'] self.node = content['node']
self.heartbeat_timeout = content['heartbeat_timeout'] self.heartbeat_timeout = content['heartbeat_timeout']
wsgi = simple_server.make_server( wsgi = simple_server.make_server(
self.listen_address[0], self.listen_address[0],
@@ -288,12 +291,14 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
self.api, self.api,
server_class=simple_server.WSGIServer) server_class=simple_server.WSGIServer)
# Don't start heartbeating until the server is listening if not self.standalone:
self.heartbeater.start() # Don't start heartbeating until the server is listening
self.heartbeater.start()
try: try:
wsgi.serve_forever() wsgi.serve_forever()
except BaseException: except BaseException:
self.log.exception('shutting down') self.log.exception('shutting down')
self.heartbeater.stop() if not self.standalone:
self.heartbeater.stop()

View File

@@ -128,8 +128,7 @@ APARAMS = _get_agent_params()
cli_opts = [ cli_opts = [
cfg.StrOpt('api_url', cfg.StrOpt('api_url',
required=('ipa-api-url' not in APARAMS), default=APARAMS.get('ipa-api-url', 'http://127.0.0.1:6835'),
default=APARAMS.get('ipa-api-url'),
deprecated_name='api-url', deprecated_name='api-url',
help='URL of the Ironic API'), help='URL of the Ironic API'),
@@ -195,7 +194,12 @@ cli_opts = [
cfg.FloatOpt('lldp_timeout', cfg.FloatOpt('lldp_timeout',
default=APARAMS.get('lldp-timeout', 30.0), default=APARAMS.get('lldp-timeout', 30.0),
help='The amount of seconds to wait for LLDP packets.') help='The amount of seconds to wait for LLDP packets.'),
cfg.BoolOpt('standalone',
default=False,
help='Note: for debugging only. Start the Agent but suppress '
'any calls to Ironic API.'),
] ]
CONF.register_cli_opts(cli_opts) CONF.register_cli_opts(cli_opts)
@@ -204,7 +208,6 @@ CONF.register_cli_opts(cli_opts)
def run(): def run():
CONF() CONF()
log.setup('ironic-python-agent') log.setup('ironic-python-agent')
agent.IronicPythonAgent(CONF.api_url, agent.IronicPythonAgent(CONF.api_url,
(CONF.advertise_host, CONF.advertise_port), (CONF.advertise_host, CONF.advertise_port),
(CONF.listen_host, CONF.listen_port), (CONF.listen_host, CONF.listen_port),
@@ -213,4 +216,5 @@ def run():
CONF.network_interface, CONF.network_interface,
CONF.lookup_timeout, CONF.lookup_timeout,
CONF.lookup_interval, CONF.lookup_interval,
CONF.driver_name).run() CONF.driver_name,
CONF.standalone).run()

View File

@@ -146,7 +146,8 @@ class TestBaseAgent(test_base.BaseTestCase):
'eth0', 'eth0',
300, 300,
1, 1,
'agent_ipmitool') 'agent_ipmitool',
False)
self.agent.ext_mgr = extension.ExtensionManager.\ self.agent.ext_mgr = extension.ExtensionManager.\
make_test_instance([extension.Extension('fake', None, make_test_instance([extension.Extension('fake', None,
FakeExtension, FakeExtension,
@@ -210,7 +211,8 @@ class TestBaseAgent(test_base.BaseTestCase):
None, None,
300, 300,
1, 1,
'agent_ipmitool') 'agent_ipmitool',
False)
homeless_agent.hardware = mock.Mock() homeless_agent.hardware = mock.Mock()
mock_list_net = homeless_agent.hardware.list_network_interfaces mock_list_net = homeless_agent.hardware.list_network_interfaces
@@ -303,6 +305,50 @@ class TestBaseAgent(test_base.BaseTestCase):
self.agent.get_node_uuid) self.agent.get_node_uuid)
class TestAgentStandalone(test_base.BaseTestCase):
def setUp(self):
super(TestAgentStandalone, self).setUp()
self.agent = agent.IronicPythonAgent('https://fake_api.example.'
'org:8081/',
('203.0.113.1', 9990),
('192.0.2.1', 9999),
3,
10,
'eth0',
300,
1,
'agent_ipmitool',
True)
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
@mock.patch.object(hardware.HardwareManager, 'list_hardware_info')
def test_run(self, mocked_list_hardware, wsgi_server_cls):
wsgi_server = wsgi_server_cls.return_value
wsgi_server.start.side_effect = KeyboardInterrupt()
self.agent.heartbeater = mock.Mock()
self.agent.api_client.lookup_node = mock.Mock()
self.agent.api_client.lookup_node.return_value = {
'node': {
'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
},
'heartbeat_timeout': 300
}
self.agent.run()
listen_addr = ('192.0.2.1', 9999)
wsgi_server_cls.assert_called_once_with(
listen_addr[0],
listen_addr[1],
self.agent.api,
server_class=simple_server.WSGIServer)
wsgi_server.serve_forever.assert_called_once()
self.assertFalse(self.agent.heartbeater.called)
self.assertFalse(self.agent.api_client.lookup_node.called)
class TestAgentCmd(test_base.BaseTestCase): class TestAgentCmd(test_base.BaseTestCase):
@mock.patch('ironic_python_agent.openstack.common.log.getLogger') @mock.patch('ironic_python_agent.openstack.common.log.getLogger')
@mock.patch(OPEN_FUNCTION_NAME) @mock.patch(OPEN_FUNCTION_NAME)