From fe4113fb5a2a47a33bf3ff559969fe9227e9b6fe Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Thu, 13 Mar 2014 16:52:45 -0700 Subject: [PATCH 01/14] Using Ironic vendor passthru for heartbeat/config --- teeth_agent/agent.py | 16 ++++++++++++- teeth_agent/errors.py | 9 ++++++++ teeth_agent/overlord_agent_api.py | 30 ++++++++++++++++++------- teeth_agent/tests/agent.py | 1 + teeth_agent/tests/overlord_agent_api.py | 19 +++++++++++----- 5 files changed, 60 insertions(+), 15 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index b14863941..f67c52e1b 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -15,6 +15,7 @@ limitations under the License. """ import random +import socket import threading import time @@ -87,8 +88,9 @@ class TeethAgentHeartbeater(threading.Thread): try: deadline = self.api.heartbeat( hardware_info=self.hardware.list_hardware_info(), + mode=self.agent.get_mode_name(), version=self.agent.version, - mode=self.agent.get_mode_name()) + uuid=self.agent.get_node_uuid()) self.error_delay = self.initial_delay self.log.info('heartbeat successful') except Exception as e: @@ -109,6 +111,7 @@ class TeethAgentHeartbeater(threading.Thread): class TeethAgent(object): def __init__(self, api_url, listen_address): self.api_url = api_url + self.api_client = overlord_agent_api.APIClient(self.api_url) self.listen_address = listen_address self.mode_implementation = None self.version = pkg_resources.get_distribution('teeth-agent').version @@ -119,6 +122,8 @@ class TeethAgent(object): self.command_lock = threading.Lock() self.log = structlog.get_logger() self.started_at = None + self.configuration = None + self.content = None def get_mode_name(self): if self.mode_implementation: @@ -137,6 +142,12 @@ class TeethAgent(object): def get_agent_mac_addr(self): return self.hardware.get_primary_mac_address() + def get_all_mac_addrs(self): + return self.hardware.list_hardware_info() + + def get_node_uuid(self): + return self.content['node']['uuid'] + def list_command_results(self): return self.command_results.values() @@ -195,6 +206,9 @@ class TeethAgent(object): def run(self): """Run the Teeth Agent.""" self.started_at = time.time() + # Get the UUID so we can heartbeat to Ironic + mac_addresses = self.get_all_mac_addrs() + self.configuration = self.api_client.get_configuration(mac_addresses) self.heartbeater.start() server = wsgiserver.CherryPyWSGIServer(self.listen_address, self.api) diff --git a/teeth_agent/errors.py b/teeth_agent/errors.py index 3d0f870b5..c8731e2f6 100644 --- a/teeth_agent/errors.py +++ b/teeth_agent/errors.py @@ -71,6 +71,15 @@ class HeartbeatError(OverlordAPIError): super(HeartbeatError, self).__init__(details) +class ConfigurationError(OverlordAPIError): + """Error raised when the configuration lookup to the agent API fails.""" + + message = 'Error getting configuration from agent API.' + + def __init__(self, details): + super(ConfigurationError, self).__init__(details) + + class ImageDownloadError(errors.RESTError): """Error raised when an image cannot be downloaded.""" diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 1ef250a32..30bbd3a0d 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -30,6 +30,7 @@ class APIClient(object): self.session = requests.Session() self.encoder = encoding.RESTJSONEncoder( encoding.SerializationViews.PUBLIC) + self.uuid = None def _request(self, method, path, data=None): request_url = '{api_url}{path}'.format(api_url=self.api_url, path=path) @@ -47,13 +48,16 @@ class APIClient(object): headers=request_headers, data=data) - def heartbeat(self, hardware_info, mode, version): - path = '/{api_version}/agents'.format(api_version=self.api_version) - + def heartbeat(self, hardware_info, mode, version, uuid): + path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format( + api_version=self.api_version, + uuid=uuid + ) data = { 'hardware': hardware_info, 'mode': mode, 'version': version, + 'agent_url': self.api_url } try: @@ -73,17 +77,27 @@ class APIClient(object): raise errors.HeartbeatError('Invalid Heartbeat-Before header') def get_configuration(self, mac_addr): - path = '/{api_version}/agents/{mac_addr}/configuration'.format( - api_version=self.api_version, - mac_addr=mac_addr) + path = '/{api_version}/drivers/teeth/lookup'.format( + api_version=self.api_version) + data = { + 'mac_addresses': [mac_addr] + } - response = self._request('GET', path) + try: + response = self._request('POST', path, data=data) + except Exception as e: + raise errors.ConfigurationError(str(e)) if response.status_code != requests.codes.OK: msg = 'Invalid status code: {0}'.format(response.status_code) raise errors.OverlordAPIError(msg) try: - return json.loads(response.content) + content = json.loads(response.content) except Exception as e: raise errors.OverlordAPIError('Error decoding response: ' + str(e)) + + if 'node' not in content or 'uuid' not in content['node']: + raise errors.OverlordAPIError('Got invalid data from the API: ' + + content) + return content \ No newline at end of file diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index f1d4ed20c..1a40299bb 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -162,6 +162,7 @@ class TestBaseAgent(unittest.TestCase): wsgi_server.start.side_effect = KeyboardInterrupt() self.agent.heartbeater = mock.Mock() + self.agent.api_client.get_configuration = mock.Mock() self.agent.run() listen_addr = ('localhost', 9999) diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 3abbc9675..74ce06e80 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -49,13 +49,15 @@ class TestBaseTeethAgent(unittest.TestCase): heartbeat_before = self.api_client.heartbeat( hardware_info=self.hardware_info, version='15', - mode='STANDBY') + mode='STANDBY', + uuid='fake-uuid') self.assertEqual(heartbeat_before, expected_heartbeat_before) request_args = self.api_client.session.request.call_args[0] self.assertEqual(request_args[0], 'PUT') - self.assertEqual(request_args[1], API_URL + 'v1/agents') + self.assertEqual(request_args[1], API_URL + 'v1/nodes/fake-uuid/vendor' + '_passthru/heartbeat') data = self.api_client.session.request.call_args[1]['data'] content = json.loads(data) @@ -71,6 +73,7 @@ class TestBaseTeethAgent(unittest.TestCase): 'id': '0:1:2:3', }, ]) + self.assertEqual(content['agent_url'], API_URL.rstrip('/')) def test_heartbeat_requests_exception(self): self.api_client.session.request = mock.Mock() @@ -80,7 +83,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.heartbeat, hardware_info=self.hardware_info, version='15', - mode='STANDBY') + mode='STANDBY', + uuid='fake-uuid') def test_heartbeat_invalid_status_code(self): response = httmock.response(status_code=404) @@ -91,7 +95,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.heartbeat, hardware_info=self.hardware_info, version='15', - mode='STANDBY') + mode='STANDBY', + uuid='fake-uuid') def test_heartbeat_missing_heartbeat_before_header(self): response = httmock.response(status_code=204) @@ -102,7 +107,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.heartbeat, hardware_info=self.hardware_info, version='15', - mode='STANDBY') + mode='STANDBY', + uuid='fake-uuid') def test_heartbeat_invalid_heartbeat_before_header(self): response = httmock.response(status_code=204, headers={ @@ -115,4 +121,5 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.heartbeat, hardware_info=self.hardware_info, version='15', - mode='STANDBY') + mode='STANDBY', + uuid='fake-uuid') From dcfaf82c5a25a42adc087bf406881f4ecd856501 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Fri, 14 Mar 2014 16:40:51 -0700 Subject: [PATCH 02/14] All posted data goes in get_configuration --- teeth_agent/agent.py | 15 +-- teeth_agent/overlord_agent_api.py | 34 +++--- teeth_agent/tests/overlord_agent_api.py | 156 ++++++++++++++++-------- 3 files changed, 135 insertions(+), 70 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index f67c52e1b..d444ad05e 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -15,7 +15,6 @@ limitations under the License. """ import random -import socket import threading import time @@ -86,11 +85,7 @@ class TeethAgentHeartbeater(threading.Thread): def do_heartbeat(self): try: - deadline = self.api.heartbeat( - hardware_info=self.hardware.list_hardware_info(), - mode=self.agent.get_mode_name(), - version=self.agent.version, - uuid=self.agent.get_node_uuid()) + deadline = self.api.heartbeat(uuid=self.agent.get_node_uuid()) self.error_delay = self.initial_delay self.log.info('heartbeat successful') except Exception as e: @@ -208,7 +203,13 @@ class TeethAgent(object): self.started_at = time.time() # Get the UUID so we can heartbeat to Ironic mac_addresses = self.get_all_mac_addrs() - self.configuration = self.api_client.get_configuration(mac_addresses) + self.configuration = self.api_client.get_configuration( + mac_addresses, + ipaddr=self.ipaddr, + hardware_info=self.hardware.list_hardware_info(), + mode=self.get_mode_name(), + version=self.version, + ) self.heartbeater.start() server = wsgiserver.CherryPyWSGIServer(self.listen_address, self.api) diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 30bbd3a0d..800ce949a 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -15,6 +15,7 @@ limitations under the License. """ import json +import os import requests from teeth_rest import encoding @@ -48,20 +49,14 @@ class APIClient(object): headers=request_headers, data=data) - def heartbeat(self, hardware_info, mode, version, uuid): + def heartbeat(self, uuid): path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format( api_version=self.api_version, uuid=uuid ) - data = { - 'hardware': hardware_info, - 'mode': mode, - 'version': version, - 'agent_url': self.api_url - } try: - response = self._request('PUT', path, data=data) + response = self._request('POST', path) except Exception as e: raise errors.HeartbeatError(str(e)) @@ -76,11 +71,16 @@ class APIClient(object): except Exception: raise errors.HeartbeatError('Invalid Heartbeat-Before header') - def get_configuration(self, mac_addr): + def get_configuration(self, mac_addrs, ipaddr, hardware_info, mode, + version): path = '/{api_version}/drivers/teeth/lookup'.format( api_version=self.api_version) data = { - 'mac_addresses': [mac_addr] + 'mac_addresses': mac_addrs, + 'agent_url': self._get_agent_url(ipaddr), + 'hardware': hardware_info, + 'mode': mode, + 'version': version, } try: @@ -90,14 +90,18 @@ class APIClient(object): if response.status_code != requests.codes.OK: msg = 'Invalid status code: {0}'.format(response.status_code) - raise errors.OverlordAPIError(msg) + raise errors.ConfigurationError(msg) try: content = json.loads(response.content) except Exception as e: - raise errors.OverlordAPIError('Error decoding response: ' + str(e)) + raise errors.ConfigurationError('Error decoding response: ' + + str(e)) if 'node' not in content or 'uuid' not in content['node']: - raise errors.OverlordAPIError('Got invalid data from the API: ' + - content) - return content \ No newline at end of file + raise errors.ConfigurationError('Got invalid data from the API: ' + '{0}'.format(content)) + return content + + def _get_agent_url(self, ipaddr): + return "http://{0}:9999".format(ipaddr) \ No newline at end of file diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 74ce06e80..4cc82ec99 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -46,19 +46,74 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - heartbeat_before = self.api_client.heartbeat( - hardware_info=self.hardware_info, - version='15', - mode='STANDBY', - uuid='fake-uuid') + heartbeat_before = self.api_client.heartbeat(uuid='fake-uuid') self.assertEqual(heartbeat_before, expected_heartbeat_before) request_args = self.api_client.session.request.call_args[0] - self.assertEqual(request_args[0], 'PUT') + self.assertEqual(request_args[0], 'POST') self.assertEqual(request_args[1], API_URL + 'v1/nodes/fake-uuid/vendor' '_passthru/heartbeat') + def test_heartbeat_requests_exception(self): + self.api_client.session.request = mock.Mock() + self.api_client.session.request.side_effect = Exception('api is down!') + + self.assertRaises(errors.HeartbeatError, + self.api_client.heartbeat, + uuid='fake-uuid') + + def test_heartbeat_invalid_status_code(self): + response = httmock.response(status_code=404) + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertRaises(errors.HeartbeatError, + self.api_client.heartbeat, + uuid='fake-uuid') + + def test_heartbeat_missing_heartbeat_before_header(self): + response = httmock.response(status_code=204) + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertRaises(errors.HeartbeatError, + self.api_client.heartbeat, + uuid='fake-uuid') + + def test_heartbeat_invalid_heartbeat_before_header(self): + response = httmock.response(status_code=204, headers={ + 'Heartbeat-Before': 'tomorrow', + }) + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertRaises(errors.HeartbeatError, + self.api_client.heartbeat, + uuid='fake-uuid') + + def test_get_configuration(self): + response = httmock.response(status_code=200, content={ + 'node': { + 'uuid': 'fake-uuid' + } + }) + + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.api_client.get_configuration( + mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], + ipaddr='42.42.42.42', + hardware_info=self.hardware_info, + version='15', + mode='STANDBY', + ) + + request_args = self.api_client.session.request.call_args[0] + self.assertEqual(request_args[0], 'POST') + self.assertEqual(request_args[1], API_URL + 'v1/drivers/teeth/lookup') + data = self.api_client.session.request.call_args[1]['data'] content = json.loads(data) self.assertEqual(content['mode'], 'STANDBY') @@ -73,53 +128,58 @@ class TestBaseTeethAgent(unittest.TestCase): 'id': '0:1:2:3', }, ]) - self.assertEqual(content['agent_url'], API_URL.rstrip('/')) + self.assertEqual(content['agent_url'], 'http://42.42.42.42:9999') - def test_heartbeat_requests_exception(self): - self.api_client.session.request = mock.Mock() - self.api_client.session.request.side_effect = Exception('api is down!') - - self.assertRaises(errors.HeartbeatError, - self.api_client.heartbeat, - hardware_info=self.hardware_info, - version='15', - mode='STANDBY', - uuid='fake-uuid') - - def test_heartbeat_invalid_status_code(self): - response = httmock.response(status_code=404) - self.api_client.session.request = mock.Mock() - self.api_client.session.request.return_value = response - - self.assertRaises(errors.HeartbeatError, - self.api_client.heartbeat, - hardware_info=self.hardware_info, - version='15', - mode='STANDBY', - uuid='fake-uuid') - - def test_heartbeat_missing_heartbeat_before_header(self): - response = httmock.response(status_code=204) - self.api_client.session.request = mock.Mock() - self.api_client.session.request.return_value = response - - self.assertRaises(errors.HeartbeatError, - self.api_client.heartbeat, - hardware_info=self.hardware_info, - version='15', - mode='STANDBY', - uuid='fake-uuid') - - def test_heartbeat_invalid_heartbeat_before_header(self): - response = httmock.response(status_code=204, headers={ - 'Heartbeat-Before': 'tomorrow', + def test_get_configuration_bad_response_code(self): + response = httmock.response(status_code=400, content={ + 'node': { + 'uuid': 'fake-uuid' + } }) + self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - self.assertRaises(errors.HeartbeatError, - self.api_client.heartbeat, + self.assertRaises(errors.ConfigurationError, + self.api_client.get_configuration, + mac_addrs=['aa:bb:cc:dd:ee:ff', + '42:42:42:42:42:42'], + ipaddr='42.42.42.42', hardware_info=self.hardware_info, version='15', mode='STANDBY', - uuid='fake-uuid') + ) + + def test_get_configuration_bad_response_data(self): + response = httmock.response(status_code=200, content={'a'}) + + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertRaises(errors.ConfigurationError, + self.api_client.get_configuration, + mac_addrs=['aa:bb:cc:dd:ee:ff', + '42:42:42:42:42:42'], + ipaddr='42.42.42.42', + hardware_info=self.hardware_info, + version='15', + mode='STANDBY', + ) + + def test_get_configuration_bad_response_body(self): + response = httmock.response(status_code=200, content={ + 'node_node': 'also_not_node' + }) + + self.api_client.session.request = mock.Mock() + self.api_client.session.request.return_value = response + + self.assertRaises(errors.ConfigurationError, + self.api_client.get_configuration, + mac_addrs=['aa:bb:cc:dd:ee:ff', + '42:42:42:42:42:42'], + ipaddr='42.42.42.42', + hardware_info=self.hardware_info, + version='15', + mode='STANDBY', + ) \ No newline at end of file From 367a44e0d12c1b04f5b644c80e5c35b3e04f4cff Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Fri, 14 Mar 2014 16:45:22 -0700 Subject: [PATCH 03/14] Cleaning up errors --- teeth_agent/overlord_agent_api.py | 3 +-- teeth_agent/tests/overlord_agent_api.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 800ce949a..e06f69709 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -15,7 +15,6 @@ limitations under the License. """ import json -import os import requests from teeth_rest import encoding @@ -104,4 +103,4 @@ class APIClient(object): return content def _get_agent_url(self, ipaddr): - return "http://{0}:9999".format(ipaddr) \ No newline at end of file + return "http://{0}:9999".format(ipaddr) diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 4cc82ec99..5d61cef2b 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -151,7 +151,7 @@ class TestBaseTeethAgent(unittest.TestCase): ) def test_get_configuration_bad_response_data(self): - response = httmock.response(status_code=200, content={'a'}) + response = httmock.response(status_code=200, content='a') self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response @@ -182,4 +182,4 @@ class TestBaseTeethAgent(unittest.TestCase): hardware_info=self.hardware_info, version='15', mode='STANDBY', - ) \ No newline at end of file + ) From 2a4c6113746508911954c7b20235fafaaa5d4638 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Mon, 17 Mar 2014 16:24:44 -0700 Subject: [PATCH 04/14] Retaining configuration error, changing message --- teeth_agent/errors.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/teeth_agent/errors.py b/teeth_agent/errors.py index b38626e8c..b9a856831 100644 --- a/teeth_agent/errors.py +++ b/teeth_agent/errors.py @@ -110,8 +110,16 @@ class HeartbeatError(OverlordAPIError): super(HeartbeatError, self).__init__(details) -class ImageDownloadError(RESTError): +class ConfigurationError(OverlordAPIError): + """Error raised when the configuration lookup to the agent API fails.""" + message = 'Error getting configuration from Ironic.' + + def __init__(self, details): + super(ConfigurationError, self).__init__(details) + + +class ImageDownloadError(RESTError): """Error raised when an image cannot be downloaded.""" message = 'Error downloading image.' From 4174fa43114fe16b5c2861bc7d18ff5534015b6f Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Mon, 17 Mar 2014 16:43:23 -0700 Subject: [PATCH 05/14] Retaining configuration error, changing message --- teeth_agent/errors.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/teeth_agent/errors.py b/teeth_agent/errors.py index b38626e8c..b9a856831 100644 --- a/teeth_agent/errors.py +++ b/teeth_agent/errors.py @@ -110,8 +110,16 @@ class HeartbeatError(OverlordAPIError): super(HeartbeatError, self).__init__(details) -class ImageDownloadError(RESTError): +class ConfigurationError(OverlordAPIError): + """Error raised when the configuration lookup to the agent API fails.""" + message = 'Error getting configuration from Ironic.' + + def __init__(self, details): + super(ConfigurationError, self).__init__(details) + + +class ImageDownloadError(RESTError): """Error raised when an image cannot be downloaded.""" message = 'Error downloading image.' From 22f3ccd3dc557418e338b33aaac2f477475298df Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Tue, 18 Mar 2014 10:36:33 -0700 Subject: [PATCH 06/14] Renaming get_configuration to lookup_node --- teeth_agent/agent.py | 2 +- teeth_agent/overlord_agent_api.py | 2 +- teeth_agent/tests/agent.py | 2 +- teeth_agent/tests/overlord_agent_api.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index aecca1c75..bc0c20f71 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -212,7 +212,7 @@ class TeethAgent(object): self.started_at = _time() # Get the UUID so we can heartbeat to Ironic mac_addresses = self.get_all_mac_addrs() - self.configuration = self.api_client.get_configuration( + self.configuration = self.api_client.lookup_node( mac_addresses, ipaddr=self.ipaddr, hardware_info=self.hardware.list_hardware_info(), diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 21984faf0..ff37c2f8d 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -69,7 +69,7 @@ class APIClient(object): except Exception: raise errors.HeartbeatError('Invalid Heartbeat-Before header') - def get_configuration(self, mac_addrs, ipaddr, hardware_info, mode, + def lookup_node(self, mac_addrs, ipaddr, hardware_info, mode, version): path = '/{api_version}/drivers/teeth/lookup'.format( api_version=self.api_version) diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index cd880eb8a..4d57c05a2 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -162,7 +162,7 @@ class TestBaseAgent(unittest.TestCase): wsgi_server.start.side_effect = KeyboardInterrupt() self.agent.heartbeater = mock.Mock() - self.agent.api_client.get_configuration = mock.Mock() + self.agent.api_client.lookup_node = mock.Mock() self.agent.run() listen_addr = ('localhost', 9999) diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 5d61cef2b..57cdf7d81 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -92,7 +92,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.heartbeat, uuid='fake-uuid') - def test_get_configuration(self): + def test_lookup_node(self): response = httmock.response(status_code=200, content={ 'node': { 'uuid': 'fake-uuid' @@ -102,7 +102,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - self.api_client.get_configuration( + self.api_client.lookup_node( mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], ipaddr='42.42.42.42', hardware_info=self.hardware_info, @@ -130,7 +130,7 @@ class TestBaseTeethAgent(unittest.TestCase): ]) self.assertEqual(content['agent_url'], 'http://42.42.42.42:9999') - def test_get_configuration_bad_response_code(self): + def test_lookup_node_bad_response_code(self): response = httmock.response(status_code=400, content={ 'node': { 'uuid': 'fake-uuid' @@ -141,7 +141,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request.return_value = response self.assertRaises(errors.ConfigurationError, - self.api_client.get_configuration, + self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], ipaddr='42.42.42.42', @@ -150,14 +150,14 @@ class TestBaseTeethAgent(unittest.TestCase): mode='STANDBY', ) - def test_get_configuration_bad_response_data(self): + def test_lookup_node_bad_response_data(self): response = httmock.response(status_code=200, content='a') self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response self.assertRaises(errors.ConfigurationError, - self.api_client.get_configuration, + self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], ipaddr='42.42.42.42', @@ -166,7 +166,7 @@ class TestBaseTeethAgent(unittest.TestCase): mode='STANDBY', ) - def test_get_configuration_bad_response_body(self): + def test_lookup_node_bad_response_body(self): response = httmock.response(status_code=200, content={ 'node_node': 'also_not_node' }) @@ -175,7 +175,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request.return_value = response self.assertRaises(errors.ConfigurationError, - self.api_client.get_configuration, + self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], ipaddr='42.42.42.42', From 3f08a0ce7efb0e50578cf8c0449f630c2586f8f8 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Tue, 18 Mar 2014 10:39:47 -0700 Subject: [PATCH 07/14] Renaming ConfigurationError to LookupNodeError --- teeth_agent/errors.py | 8 +++++--- teeth_agent/overlord_agent_api.py | 8 ++++---- teeth_agent/tests/overlord_agent_api.py | 6 +++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/teeth_agent/errors.py b/teeth_agent/errors.py index b9a856831..6ba45316c 100644 --- a/teeth_agent/errors.py +++ b/teeth_agent/errors.py @@ -110,13 +110,15 @@ class HeartbeatError(OverlordAPIError): super(HeartbeatError, self).__init__(details) -class ConfigurationError(OverlordAPIError): - """Error raised when the configuration lookup to the agent API fails.""" +class LookupNodeError(OverlordAPIError): + """Error raised when the node configuration lookup to the Ironic API + fails. + """ message = 'Error getting configuration from Ironic.' def __init__(self, details): - super(ConfigurationError, self).__init__(details) + super(LookupNodeError, self).__init__(details) class ImageDownloadError(RESTError): diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index ff37c2f8d..c66a78b3a 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -84,20 +84,20 @@ class APIClient(object): try: response = self._request('POST', path, data=data) except Exception as e: - raise errors.ConfigurationError(str(e)) + raise errors.LookupNodeError(str(e)) if response.status_code != requests.codes.OK: msg = 'Invalid status code: {0}'.format(response.status_code) - raise errors.ConfigurationError(msg) + raise errors.LookupNodeError(msg) try: content = json.loads(response.content) except Exception as e: - raise errors.ConfigurationError('Error decoding response: ' + raise errors.LookupNodeError('Error decoding response: ' + str(e)) if 'node' not in content or 'uuid' not in content['node']: - raise errors.ConfigurationError('Got invalid data from the API: ' + raise errors.LookupNodeError('Got invalid data from the API: ' '{0}'.format(content)) return content diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 57cdf7d81..0227c7ee6 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -140,7 +140,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - self.assertRaises(errors.ConfigurationError, + self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], @@ -156,7 +156,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - self.assertRaises(errors.ConfigurationError, + self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], @@ -174,7 +174,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - self.assertRaises(errors.ConfigurationError, + self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], From 947b783b6b70dc11e0a80a8fc797e22277225de8 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Tue, 18 Mar 2014 12:45:36 -0700 Subject: [PATCH 08/14] Consolidate content and configuration as node --- teeth_agent/agent.py | 11 +++-------- teeth_agent/overlord_agent_api.py | 11 ++++------- teeth_agent/tests/overlord_agent_api.py | 19 +------------------ 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index bc0c20f71..e4798b9a0 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -123,8 +123,7 @@ class TeethAgent(object): self.command_lock = threading.Lock() self.log = log.getLogger(__name__) self.started_at = None - self.configuration = None - self.content = None + self.node = None def get_mode_name(self): if self.mode_implementation: @@ -147,7 +146,7 @@ class TeethAgent(object): return self.hardware.list_hardware_info() def get_node_uuid(self): - return self.content['node']['uuid'] + return self.node['uuid'] def list_command_results(self): return self.command_results.values() @@ -211,13 +210,9 @@ class TeethAgent(object): """Run the Teeth Agent.""" self.started_at = _time() # Get the UUID so we can heartbeat to Ironic - mac_addresses = self.get_all_mac_addrs() - self.configuration = self.api_client.lookup_node( - mac_addresses, + self.node = self.api_client.lookup_node( ipaddr=self.ipaddr, hardware_info=self.hardware.list_hardware_info(), - mode=self.get_mode_name(), - version=self.version, ) self.heartbeater.start() wsgi = simple_server.make_server( diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index c66a78b3a..240aec091 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -69,16 +69,13 @@ class APIClient(object): except Exception: raise errors.HeartbeatError('Invalid Heartbeat-Before header') - def lookup_node(self, mac_addrs, ipaddr, hardware_info, mode, - version): + def lookup_node(self, ipaddr, hardware_info): path = '/{api_version}/drivers/teeth/lookup'.format( - api_version=self.api_version) + api_version=self.api_version + ) data = { - 'mac_addresses': mac_addrs, 'agent_url': self._get_agent_url(ipaddr), 'hardware': hardware_info, - 'mode': mode, - 'version': version, } try: @@ -99,7 +96,7 @@ class APIClient(object): if 'node' not in content or 'uuid' not in content['node']: raise errors.LookupNodeError('Got invalid data from the API: ' '{0}'.format(content)) - return content + return content['node'] def _get_agent_url(self, ipaddr): return "http://{0}:9999".format(ipaddr) diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 0227c7ee6..a8d8c4553 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -103,11 +103,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request.return_value = response self.api_client.lookup_node( - mac_addrs=['aa:bb:cc:dd:ee:ff', '42:42:42:42:42:42'], ipaddr='42.42.42.42', hardware_info=self.hardware_info, - version='15', - mode='STANDBY', ) request_args = self.api_client.session.request.call_args[0] @@ -116,8 +113,6 @@ class TestBaseTeethAgent(unittest.TestCase): data = self.api_client.session.request.call_args[1]['data'] content = json.loads(data) - self.assertEqual(content['mode'], 'STANDBY') - self.assertEqual(content['version'], '15') self.assertEqual(content['hardware'], [ { 'type': 'mac_address', @@ -142,12 +137,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - mac_addrs=['aa:bb:cc:dd:ee:ff', - '42:42:42:42:42:42'], ipaddr='42.42.42.42', hardware_info=self.hardware_info, - version='15', - mode='STANDBY', ) def test_lookup_node_bad_response_data(self): @@ -158,12 +149,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - mac_addrs=['aa:bb:cc:dd:ee:ff', - '42:42:42:42:42:42'], ipaddr='42.42.42.42', - hardware_info=self.hardware_info, - version='15', - mode='STANDBY', + hardware_info=self.hardware_info ) def test_lookup_node_bad_response_body(self): @@ -176,10 +163,6 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - mac_addrs=['aa:bb:cc:dd:ee:ff', - '42:42:42:42:42:42'], ipaddr='42.42.42.42', hardware_info=self.hardware_info, - version='15', - mode='STANDBY', ) From 1ad55bc78941f1f188a4a6a9e33fda42248dff32 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Tue, 18 Mar 2014 15:24:20 -0700 Subject: [PATCH 09/14] Minor fixes and sending agent_url with heartbeat The driver won't be able to save anything during the first node lookup, so sending the URL then won't be useful. --- teeth_agent/agent.py | 7 ++----- teeth_agent/cmd/agent.py | 7 ++++++- teeth_agent/overlord_agent_api.py | 16 ++++++++-------- teeth_agent/tests/overlord_agent_api.py | 25 +++++++++++++++---------- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index e4798b9a0..2cd8050a4 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -109,11 +109,12 @@ class TeethAgentHeartbeater(threading.Thread): class TeethAgent(object): - def __init__(self, api_url, listen_address, ipaddr): + def __init__(self, api_url, listen_address, ipaddr, port=9999): self.api_url = api_url self.api_client = overlord_agent_api.APIClient(self.api_url) self.listen_address = listen_address self.ipaddr = ipaddr + self.port = port self.mode_implementation = None self.version = pkg_resources.get_distribution('teeth-agent').version self.api = app.VersionSelectorApplication(self) @@ -142,9 +143,6 @@ class TeethAgent(object): def get_agent_mac_addr(self): return self.hardware.get_primary_mac_address() - def get_all_mac_addrs(self): - return self.hardware.list_hardware_info() - def get_node_uuid(self): return self.node['uuid'] @@ -211,7 +209,6 @@ class TeethAgent(object): self.started_at = _time() # Get the UUID so we can heartbeat to Ironic self.node = self.api_client.lookup_node( - ipaddr=self.ipaddr, hardware_info=self.hardware.list_hardware_info(), ) self.heartbeater.start() diff --git a/teeth_agent/cmd/agent.py b/teeth_agent/cmd/agent.py index 7075693a5..fd1c87be0 100644 --- a/teeth_agent/cmd/agent.py +++ b/teeth_agent/cmd/agent.py @@ -42,8 +42,13 @@ def run(): required=True, help='The external IP address to advertise to ironic') + parser.add_argument('--port', + required=False, + help='The external port the agent is listening on') + args = parser.parse_args() agent.build_agent(args.api_url, args.listen_host, args.listen_port, - args.ipaddr).run() + args.ipaddr, + args.port).run() diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 240aec091..0e096d001 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -29,7 +29,6 @@ class APIClient(object): self.api_url = api_url.rstrip('/') self.session = requests.Session() self.encoder = encoding.RESTJSONEncoder() - self.uuid = None def _request(self, method, path, data=None): request_url = '{api_url}{path}'.format(api_url=self.api_url, path=path) @@ -47,14 +46,16 @@ class APIClient(object): headers=request_headers, data=data) - def heartbeat(self, uuid): + def heartbeat(self, uuid, ipaddr, port): path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format( api_version=self.api_version, uuid=uuid ) - + data = { + 'agent_url': self._get_agent_url(ipaddr, port) + } try: - response = self._request('POST', path) + response = self._request('POST', path, data=data) except Exception as e: raise errors.HeartbeatError(str(e)) @@ -69,12 +70,11 @@ class APIClient(object): except Exception: raise errors.HeartbeatError('Invalid Heartbeat-Before header') - def lookup_node(self, ipaddr, hardware_info): + def lookup_node(self, hardware_info): path = '/{api_version}/drivers/teeth/lookup'.format( api_version=self.api_version ) data = { - 'agent_url': self._get_agent_url(ipaddr), 'hardware': hardware_info, } @@ -98,5 +98,5 @@ class APIClient(object): '{0}'.format(content)) return content['node'] - def _get_agent_url(self, ipaddr): - return "http://{0}:9999".format(ipaddr) + def _get_agent_url(self, ipaddr, port): + return "http://{0}:{1}".format(ipaddr, port) diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index a8d8c4553..3bbde34a2 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -46,7 +46,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - heartbeat_before = self.api_client.heartbeat(uuid='fake-uuid') + heartbeat_before = self.api_client.heartbeat(uuid='fake-uuid', + ipaddr='42.42.42.42', + port=9999) self.assertEqual(heartbeat_before, expected_heartbeat_before) @@ -61,7 +63,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid') + uuid='fake-uuid', + ipaddr='42.42.42.42', + port=9999) def test_heartbeat_invalid_status_code(self): response = httmock.response(status_code=404) @@ -70,7 +74,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid') + uuid='fake-uuid', + ipaddr='42.42.42.42', + port=9999) def test_heartbeat_missing_heartbeat_before_header(self): response = httmock.response(status_code=204) @@ -79,7 +85,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid') + uuid='fake-uuid', + ipaddr='42.42.42.42', + port=9999) def test_heartbeat_invalid_heartbeat_before_header(self): response = httmock.response(status_code=204, headers={ @@ -90,7 +98,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid') + uuid='fake-uuid', + ipaddr='42.42.42.42', + port=9999) def test_lookup_node(self): response = httmock.response(status_code=200, content={ @@ -103,7 +113,6 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request.return_value = response self.api_client.lookup_node( - ipaddr='42.42.42.42', hardware_info=self.hardware_info, ) @@ -123,7 +132,6 @@ class TestBaseTeethAgent(unittest.TestCase): 'id': '0:1:2:3', }, ]) - self.assertEqual(content['agent_url'], 'http://42.42.42.42:9999') def test_lookup_node_bad_response_code(self): response = httmock.response(status_code=400, content={ @@ -137,7 +145,6 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - ipaddr='42.42.42.42', hardware_info=self.hardware_info, ) @@ -149,7 +156,6 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - ipaddr='42.42.42.42', hardware_info=self.hardware_info ) @@ -163,6 +169,5 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.LookupNodeError, self.api_client.lookup_node, - ipaddr='42.42.42.42', hardware_info=self.hardware_info, ) From 56a0ed37a4edc918d274667e62502f9ef3849b24 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 19 Mar 2014 11:14:45 -0700 Subject: [PATCH 10/14] Removing host/port, already in listen_address --- teeth_agent/agent.py | 8 +++----- teeth_agent/cmd/agent.py | 12 +----------- teeth_agent/overlord_agent_api.py | 8 ++++---- teeth_agent/tests/agent.py | 3 +-- teeth_agent/tests/overlord_agent_api.py | 19 ++++++++----------- 5 files changed, 17 insertions(+), 33 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index 2cd8050a4..b28bd256b 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -109,12 +109,10 @@ class TeethAgentHeartbeater(threading.Thread): class TeethAgent(object): - def __init__(self, api_url, listen_address, ipaddr, port=9999): + def __init__(self, api_url, listen_address): self.api_url = api_url self.api_client = overlord_agent_api.APIClient(self.api_url) self.listen_address = listen_address - self.ipaddr = ipaddr - self.port = port self.mode_implementation = None self.version = pkg_resources.get_distribution('teeth-agent').version self.api = app.VersionSelectorApplication(self) @@ -236,5 +234,5 @@ def _load_mode_implementation(mode_name): return mgr.driver -def build_agent(api_url, listen_host, listen_port, ipaddr): - return TeethAgent(api_url, (listen_host, listen_port), ipaddr) +def build_agent(api_url, listen_host, listen_port): + return TeethAgent(api_url, (listen_host, listen_port)) diff --git a/teeth_agent/cmd/agent.py b/teeth_agent/cmd/agent.py index fd1c87be0..83124f6d3 100644 --- a/teeth_agent/cmd/agent.py +++ b/teeth_agent/cmd/agent.py @@ -38,17 +38,7 @@ def run(): type=int, help='The port to listen on') - parser.add_argument('--ipaddr', - required=True, - help='The external IP address to advertise to ironic') - - parser.add_argument('--port', - required=False, - help='The external port the agent is listening on') - args = parser.parse_args() agent.build_agent(args.api_url, args.listen_host, - args.listen_port, - args.ipaddr, - args.port).run() + args.listen_port).run() diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 0e096d001..b233876a4 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -46,13 +46,13 @@ class APIClient(object): headers=request_headers, data=data) - def heartbeat(self, uuid, ipaddr, port): + def heartbeat(self, uuid, listen_address): path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format( api_version=self.api_version, uuid=uuid ) data = { - 'agent_url': self._get_agent_url(ipaddr, port) + 'agent_url': self._get_agent_url(listen_address) } try: response = self._request('POST', path, data=data) @@ -98,5 +98,5 @@ class APIClient(object): '{0}'.format(content)) return content['node'] - def _get_agent_url(self, ipaddr, port): - return "http://{0}:{1}".format(ipaddr, port) + def _get_agent_url(self, listen_address): + return "http://{0}:{1}".format(listen_address[0], listen_address[1]) diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index 4d57c05a2..4ea141fa5 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -120,8 +120,7 @@ class TestBaseAgent(unittest.TestCase): def setUp(self): self.encoder = encoding.RESTJSONEncoder(indent=4) self.agent = agent.TeethAgent('https://fake_api.example.org:8081/', - ('localhost', 9999), - '192.168.1.1') + ('localhost', 9999)) def assertEqualEncoded(self, a, b): # Evidently JSONEncoder.default() can't handle None (??) so we have to diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 3bbde34a2..7187270d3 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -46,9 +46,10 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request = mock.Mock() self.api_client.session.request.return_value = response - heartbeat_before = self.api_client.heartbeat(uuid='fake-uuid', - ipaddr='42.42.42.42', - port=9999) + heartbeat_before = self.api_client.heartbeat( + uuid='fake-uuid', + listen_address=('42.42.42.42', '9999') + ) self.assertEqual(heartbeat_before, expected_heartbeat_before) @@ -64,8 +65,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='fake-uuid', - ipaddr='42.42.42.42', - port=9999) + listen_address=('42.42.42.42', '9999')) def test_heartbeat_invalid_status_code(self): response = httmock.response(status_code=404) @@ -75,8 +75,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='fake-uuid', - ipaddr='42.42.42.42', - port=9999) + listen_address=('42.42.42.42', '9999')) def test_heartbeat_missing_heartbeat_before_header(self): response = httmock.response(status_code=204) @@ -86,8 +85,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='fake-uuid', - ipaddr='42.42.42.42', - port=9999) + listen_address=('42.42.42.42', '9999')) def test_heartbeat_invalid_heartbeat_before_header(self): response = httmock.response(status_code=204, headers={ @@ -99,8 +97,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='fake-uuid', - ipaddr='42.42.42.42', - port=9999) + listen_address=('42.42.42.42', '9999')) def test_lookup_node(self): response = httmock.response(status_code=200, content={ From d0323f3ec1668f35a3153628d733678eb9dcf5d0 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 19 Mar 2014 13:55:56 -0700 Subject: [PATCH 11/14] Using advertise_address, using valid uuid/macs --- teeth_agent/agent.py | 21 +++++++++++--- teeth_agent/cmd/agent.py | 22 ++++++++++++--- teeth_agent/overlord_agent_api.py | 11 +++++--- teeth_agent/tests/agent.py | 5 ++-- teeth_agent/tests/overlord_agent_api.py | 37 +++++++++++++------------ 5 files changed, 64 insertions(+), 32 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index b28bd256b..d6bffa85c 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -90,7 +90,10 @@ class TeethAgentHeartbeater(threading.Thread): def do_heartbeat(self): try: - deadline = self.api.heartbeat(uuid=self.agent.get_node_uuid()) + deadline = self.api.heartbeat( + uuid=self.agent.get_node_uuid(), + advertise_address=self.agent.advertise_address + ) self.error_delay = self.initial_delay self.log.info('heartbeat successful') except Exception: @@ -109,10 +112,11 @@ class TeethAgentHeartbeater(threading.Thread): class TeethAgent(object): - def __init__(self, api_url, listen_address): + def __init__(self, api_url, advertise_address, listen_address): self.api_url = api_url self.api_client = overlord_agent_api.APIClient(self.api_url) self.listen_address = listen_address + self.advertise_address = advertise_address self.mode_implementation = None self.version = pkg_resources.get_distribution('teeth-agent').version self.api = app.VersionSelectorApplication(self) @@ -142,6 +146,8 @@ class TeethAgent(object): return self.hardware.get_primary_mac_address() def get_node_uuid(self): + if 'uuid' not in self.node: + errors.HeartbeatError('Tried to heartbeat without node UUID.') return self.node['uuid'] def list_command_results(self): @@ -234,5 +240,12 @@ def _load_mode_implementation(mode_name): return mgr.driver -def build_agent(api_url, listen_host, listen_port): - return TeethAgent(api_url, (listen_host, listen_port)) +def build_agent(api_url, + advertise_host, + advertise_port, + listen_host, + listen_port): + + return TeethAgent(api_url=api_url, + advertise_address=(advertise_host, advertise_port), + listen_address=(listen_host, listen_port)) diff --git a/teeth_agent/cmd/agent.py b/teeth_agent/cmd/agent.py index 83124f6d3..f6e1a6148 100644 --- a/teeth_agent/cmd/agent.py +++ b/teeth_agent/cmd/agent.py @@ -37,8 +37,22 @@ def run(): default=9999, type=int, help='The port to listen on') - + parser.add_argument('--advertise-host', + default='0.0.0.0', + type=str, + help=('The port to tell Ironic to reply and send ' + 'commands to. This is different than ' + 'listen-host because Docker will have a ' + 'different internal IP than the host IP that ' + 'Ironic will be communicating with.')) + parser.add_argument('--advertise-port', + default=9999, + type=int, + help=('The port to tell Ironic to reply and send ' + 'commands to.')) args = parser.parse_args() - agent.build_agent(args.api_url, - args.listen_host, - args.listen_port).run() + agent.build_agent(api_url=args.api_url, + advertise_host=args.advertise_host, + advertise_port=args.advertise_port, + listen_host=args.listen_host, + listen_port=args.listen_port).run() diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index b233876a4..fda86b22b 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -46,13 +46,13 @@ class APIClient(object): headers=request_headers, data=data) - def heartbeat(self, uuid, listen_address): + def heartbeat(self, uuid, advertise_address): path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format( api_version=self.api_version, uuid=uuid ) data = { - 'agent_url': self._get_agent_url(listen_address) + 'agent_url': self._get_agent_url(advertise_address) } try: response = self._request('POST', path, data=data) @@ -74,6 +74,8 @@ class APIClient(object): path = '/{api_version}/drivers/teeth/lookup'.format( api_version=self.api_version ) + # This hardware won't be saved on the node currently, because of how + # driver_vendor_passthru is implemented (no node saving). data = { 'hardware': hardware_info, } @@ -98,5 +100,6 @@ class APIClient(object): '{0}'.format(content)) return content['node'] - def _get_agent_url(self, listen_address): - return "http://{0}:{1}".format(listen_address[0], listen_address[1]) + def _get_agent_url(self, advertise_address): + return 'http://{0}:{1}'.format(advertise_address[0], + advertise_address[1]) diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index 4ea141fa5..a032a021d 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -120,7 +120,8 @@ class TestBaseAgent(unittest.TestCase): def setUp(self): self.encoder = encoding.RESTJSONEncoder(indent=4) self.agent = agent.TeethAgent('https://fake_api.example.org:8081/', - ('localhost', 9999)) + ('31.41.59.26', 9990), + ('42.42.42.42', 9999)) def assertEqualEncoded(self, a, b): # Evidently JSONEncoder.default() can't handle None (??) so we have to @@ -164,7 +165,7 @@ class TestBaseAgent(unittest.TestCase): self.agent.api_client.lookup_node = mock.Mock() self.agent.run() - listen_addr = ('localhost', 9999) + listen_addr = ('42.42.42.42', 9999) wsgi_server_cls.assert_called_once_with( listen_addr[0], listen_addr[1], diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 7187270d3..32a8e0acb 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -32,9 +32,9 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client = overlord_agent_api.APIClient(API_URL) self.hardware_info = [ hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS, - 'a:b:c:d'), + 'aa:bb:cc:dd:ee:ff'), hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS, - '0:1:2:3'), + 'ff:ee:dd:cc:bb:aa'), ] def test_successful_heartbeat(self): @@ -47,16 +47,17 @@ class TestBaseTeethAgent(unittest.TestCase): self.api_client.session.request.return_value = response heartbeat_before = self.api_client.heartbeat( - uuid='fake-uuid', - listen_address=('42.42.42.42', '9999') + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('42.42.42.42', '9999') ) self.assertEqual(heartbeat_before, expected_heartbeat_before) + heartbeat_path = 'v1/nodes/deadbeef-dabb-ad00-b105-f00d00bab10c/' \ + 'vendor_passthru/heartbeat' request_args = self.api_client.session.request.call_args[0] self.assertEqual(request_args[0], 'POST') - self.assertEqual(request_args[1], API_URL + 'v1/nodes/fake-uuid/vendor' - '_passthru/heartbeat') + self.assertEqual(request_args[1], API_URL + heartbeat_path) def test_heartbeat_requests_exception(self): self.api_client.session.request = mock.Mock() @@ -64,8 +65,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid', - listen_address=('42.42.42.42', '9999')) + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('42.42.42.42', '9999')) def test_heartbeat_invalid_status_code(self): response = httmock.response(status_code=404) @@ -74,8 +75,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid', - listen_address=('42.42.42.42', '9999')) + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('42.42.42.42', '9999')) def test_heartbeat_missing_heartbeat_before_header(self): response = httmock.response(status_code=204) @@ -84,8 +85,8 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid', - listen_address=('42.42.42.42', '9999')) + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('42.42.42.42', '9999')) def test_heartbeat_invalid_heartbeat_before_header(self): response = httmock.response(status_code=204, headers={ @@ -96,13 +97,13 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, - uuid='fake-uuid', - listen_address=('42.42.42.42', '9999')) + uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', + advertise_address=('42.42.42.42', '9999')) def test_lookup_node(self): response = httmock.response(status_code=200, content={ 'node': { - 'uuid': 'fake-uuid' + 'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c' } }) @@ -122,18 +123,18 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertEqual(content['hardware'], [ { 'type': 'mac_address', - 'id': 'a:b:c:d', + 'id': 'aa:bb:cc:dd:ee:ff', }, { 'type': 'mac_address', - 'id': '0:1:2:3', + 'id': 'ff:ee:dd:cc:bb:aa', }, ]) def test_lookup_node_bad_response_code(self): response = httmock.response(status_code=400, content={ 'node': { - 'uuid': 'fake-uuid' + 'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c' } }) From 85234b702c6aee6c3929cdb145d5edc19a90c3bf Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 19 Mar 2014 14:01:49 -0700 Subject: [PATCH 12/14] Using RFC5737 example ip addrs --- teeth_agent/tests/agent.py | 6 +++--- teeth_agent/tests/overlord_agent_api.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index a032a021d..8f90e7a6a 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -120,8 +120,8 @@ class TestBaseAgent(unittest.TestCase): def setUp(self): self.encoder = encoding.RESTJSONEncoder(indent=4) self.agent = agent.TeethAgent('https://fake_api.example.org:8081/', - ('31.41.59.26', 9990), - ('42.42.42.42', 9999)) + ('203.0.113.1', 9990), + ('192.0.2.1', 9999)) def assertEqualEncoded(self, a, b): # Evidently JSONEncoder.default() can't handle None (??) so we have to @@ -165,7 +165,7 @@ class TestBaseAgent(unittest.TestCase): self.agent.api_client.lookup_node = mock.Mock() self.agent.run() - listen_addr = ('42.42.42.42', 9999) + listen_addr = ('192.0.2.1', 9999) wsgi_server_cls.assert_called_once_with( listen_addr[0], listen_addr[1], diff --git a/teeth_agent/tests/overlord_agent_api.py b/teeth_agent/tests/overlord_agent_api.py index 32a8e0acb..20f92ad78 100644 --- a/teeth_agent/tests/overlord_agent_api.py +++ b/teeth_agent/tests/overlord_agent_api.py @@ -48,7 +48,7 @@ class TestBaseTeethAgent(unittest.TestCase): heartbeat_before = self.api_client.heartbeat( uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', - advertise_address=('42.42.42.42', '9999') + advertise_address=('192.0.2.1', '9999') ) self.assertEqual(heartbeat_before, expected_heartbeat_before) @@ -66,7 +66,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', - advertise_address=('42.42.42.42', '9999')) + advertise_address=('192.0.2.1', '9999')) def test_heartbeat_invalid_status_code(self): response = httmock.response(status_code=404) @@ -76,7 +76,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', - advertise_address=('42.42.42.42', '9999')) + advertise_address=('192.0.2.1', '9999')) def test_heartbeat_missing_heartbeat_before_header(self): response = httmock.response(status_code=204) @@ -86,7 +86,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', - advertise_address=('42.42.42.42', '9999')) + advertise_address=('192.0.2.1', '9999')) def test_heartbeat_invalid_heartbeat_before_header(self): response = httmock.response(status_code=204, headers={ @@ -98,7 +98,7 @@ class TestBaseTeethAgent(unittest.TestCase): self.assertRaises(errors.HeartbeatError, self.api_client.heartbeat, uuid='deadbeef-dabb-ad00-b105-f00d00bab10c', - advertise_address=('42.42.42.42', '9999')) + advertise_address=('192.0.2.1', '9999')) def test_lookup_node(self): response = httmock.response(status_code=200, content={ From b28d69cd319801be7586840b82ec9723f04e7ae4 Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 19 Mar 2014 14:29:57 -0700 Subject: [PATCH 13/14] Generalizing help message for command args --- teeth_agent/cmd/agent.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/teeth_agent/cmd/agent.py b/teeth_agent/cmd/agent.py index f6e1a6148..8544c58a3 100644 --- a/teeth_agent/cmd/agent.py +++ b/teeth_agent/cmd/agent.py @@ -31,7 +31,7 @@ def run(): parser.add_argument('--listen-host', default='0.0.0.0', type=str, - help=('The IP address to listen on.')) + help='The IP address to listen on.') parser.add_argument('--listen-port', default=9999, @@ -40,16 +40,13 @@ def run(): parser.add_argument('--advertise-host', default='0.0.0.0', type=str, - help=('The port to tell Ironic to reply and send ' - 'commands to. This is different than ' - 'listen-host because Docker will have a ' - 'different internal IP than the host IP that ' - 'Ironic will be communicating with.')) + help='The port to tell Ironic to reply and send ' + 'commands to.') parser.add_argument('--advertise-port', default=9999, type=int, - help=('The port to tell Ironic to reply and send ' - 'commands to.')) + help='The port to tell Ironic to reply and send ' + 'commands to.') args = parser.parse_args() agent.build_agent(api_url=args.api_url, advertise_host=args.advertise_host, From a0b92f28ccace5e762556986159c8fdaa3b49c8c Mon Sep 17 00:00:00 2001 From: Josh Gachnang Date: Wed, 19 Mar 2014 14:58:11 -0700 Subject: [PATCH 14/14] Fixing kwarg vs posarg --- teeth_agent/agent.py | 6 +++--- teeth_agent/cmd/agent.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index d6bffa85c..261a48771 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -246,6 +246,6 @@ def build_agent(api_url, listen_host, listen_port): - return TeethAgent(api_url=api_url, - advertise_address=(advertise_host, advertise_port), - listen_address=(listen_host, listen_port)) + return TeethAgent(api_url, + (advertise_host, advertise_port), + (listen_host, listen_port)) diff --git a/teeth_agent/cmd/agent.py b/teeth_agent/cmd/agent.py index 8544c58a3..a30b8a9c6 100644 --- a/teeth_agent/cmd/agent.py +++ b/teeth_agent/cmd/agent.py @@ -40,7 +40,7 @@ def run(): parser.add_argument('--advertise-host', default='0.0.0.0', type=str, - help='The port to tell Ironic to reply and send ' + help='The host to tell Ironic to reply and send ' 'commands to.') parser.add_argument('--advertise-port', default=9999, @@ -48,8 +48,8 @@ def run(): help='The port to tell Ironic to reply and send ' 'commands to.') args = parser.parse_args() - agent.build_agent(api_url=args.api_url, - advertise_host=args.advertise_host, - advertise_port=args.advertise_port, - listen_host=args.listen_host, - listen_port=args.listen_port).run() + agent.build_agent(args.api_url, + args.advertise_host, + args.advertise_port, + args.listen_host, + args.listen_port).run()