Merge pull request #66 from rackerlabs/JoshNang/heartbeat
Using Ironic vendor passthru for heartbeat
This commit is contained in:
		@@ -91,9 +91,9 @@ class TeethAgentHeartbeater(threading.Thread):
 | 
			
		||||
    def do_heartbeat(self):
 | 
			
		||||
        try:
 | 
			
		||||
            deadline = self.api.heartbeat(
 | 
			
		||||
                hardware_info=self.hardware.list_hardware_info(),
 | 
			
		||||
                version=self.agent.version,
 | 
			
		||||
                mode=self.agent.get_mode_name())
 | 
			
		||||
                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:
 | 
			
		||||
@@ -112,10 +112,11 @@ class TeethAgentHeartbeater(threading.Thread):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeethAgent(object):
 | 
			
		||||
    def __init__(self, api_url, listen_address, ipaddr):
 | 
			
		||||
    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.ipaddr = ipaddr
 | 
			
		||||
        self.advertise_address = advertise_address
 | 
			
		||||
        self.mode_implementation = None
 | 
			
		||||
        self.version = pkg_resources.get_distribution('teeth-agent').version
 | 
			
		||||
        self.api = app.VersionSelectorApplication(self)
 | 
			
		||||
@@ -125,6 +126,7 @@ class TeethAgent(object):
 | 
			
		||||
        self.command_lock = threading.Lock()
 | 
			
		||||
        self.log = log.getLogger(__name__)
 | 
			
		||||
        self.started_at = None
 | 
			
		||||
        self.node = None
 | 
			
		||||
 | 
			
		||||
    def get_mode_name(self):
 | 
			
		||||
        if self.mode_implementation:
 | 
			
		||||
@@ -143,6 +145,11 @@ class TeethAgent(object):
 | 
			
		||||
    def get_agent_mac_addr(self):
 | 
			
		||||
        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):
 | 
			
		||||
        return self.command_results.values()
 | 
			
		||||
 | 
			
		||||
@@ -204,6 +211,10 @@ class TeethAgent(object):
 | 
			
		||||
    def run(self):
 | 
			
		||||
        """Run the Teeth Agent."""
 | 
			
		||||
        self.started_at = _time()
 | 
			
		||||
        # Get the UUID so we can heartbeat to Ironic
 | 
			
		||||
        self.node = self.api_client.lookup_node(
 | 
			
		||||
            hardware_info=self.hardware.list_hardware_info(),
 | 
			
		||||
        )
 | 
			
		||||
        self.heartbeater.start()
 | 
			
		||||
        wsgi = simple_server.make_server(
 | 
			
		||||
            self.listen_address[0],
 | 
			
		||||
@@ -229,5 +240,12 @@ 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,
 | 
			
		||||
                advertise_host,
 | 
			
		||||
                advertise_port,
 | 
			
		||||
                listen_host,
 | 
			
		||||
                listen_port):
 | 
			
		||||
 | 
			
		||||
    return TeethAgent(api_url,
 | 
			
		||||
                      (advertise_host, advertise_port),
 | 
			
		||||
                      (listen_host, listen_port))
 | 
			
		||||
 
 | 
			
		||||
@@ -31,19 +31,25 @@ 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,
 | 
			
		||||
                        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('--advertise-host',
 | 
			
		||||
                        default='0.0.0.0',
 | 
			
		||||
                        type=str,
 | 
			
		||||
                        help='The host 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.')
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
    agent.build_agent(args.api_url,
 | 
			
		||||
                      args.advertise_host,
 | 
			
		||||
                      args.advertise_port,
 | 
			
		||||
                      args.listen_host,
 | 
			
		||||
                      args.listen_port,
 | 
			
		||||
                      args.ipaddr).run()
 | 
			
		||||
                      args.listen_port).run()
 | 
			
		||||
 
 | 
			
		||||
@@ -110,6 +110,17 @@ class HeartbeatError(OverlordAPIError):
 | 
			
		||||
        super(HeartbeatError, self).__init__(details)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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(LookupNodeError, self).__init__(details)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageDownloadError(RESTError):
 | 
			
		||||
    """Error raised when an image cannot be downloaded."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,17 +46,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, uuid, advertise_address):
 | 
			
		||||
        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._get_agent_url(advertise_address)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            response = self._request('PUT', path, data=data)
 | 
			
		||||
            response = self._request('POST', path, data=data)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            raise errors.HeartbeatError(str(e))
 | 
			
		||||
 | 
			
		||||
@@ -71,18 +70,36 @@ class APIClient(object):
 | 
			
		||||
        except Exception:
 | 
			
		||||
            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)
 | 
			
		||||
    def lookup_node(self, hardware_info):
 | 
			
		||||
        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,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        response = self._request('GET', path)
 | 
			
		||||
        try:
 | 
			
		||||
            response = self._request('POST', path, data=data)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            raise errors.LookupNodeError(str(e))
 | 
			
		||||
 | 
			
		||||
        if response.status_code != requests.codes.OK:
 | 
			
		||||
            msg = 'Invalid status code: {0}'.format(response.status_code)
 | 
			
		||||
            raise errors.OverlordAPIError(msg)
 | 
			
		||||
            raise errors.LookupNodeError(msg)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return json.loads(response.content)
 | 
			
		||||
            content = json.loads(response.content)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            raise errors.OverlordAPIError('Error decoding response: ' + str(e))
 | 
			
		||||
            raise errors.LookupNodeError('Error decoding response: '
 | 
			
		||||
                                            + str(e))
 | 
			
		||||
 | 
			
		||||
        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['node']
 | 
			
		||||
 | 
			
		||||
    def _get_agent_url(self, advertise_address):
 | 
			
		||||
        return 'http://{0}:{1}'.format(advertise_address[0],
 | 
			
		||||
                                       advertise_address[1])
 | 
			
		||||
 
 | 
			
		||||
@@ -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/',
 | 
			
		||||
                                      ('localhost', 9999),
 | 
			
		||||
                                      '192.168.1.1')
 | 
			
		||||
                                      ('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
 | 
			
		||||
@@ -162,9 +162,10 @@ class TestBaseAgent(unittest.TestCase):
 | 
			
		||||
        wsgi_server.start.side_effect = KeyboardInterrupt()
 | 
			
		||||
 | 
			
		||||
        self.agent.heartbeater = mock.Mock()
 | 
			
		||||
        self.agent.api_client.lookup_node = mock.Mock()
 | 
			
		||||
        self.agent.run()
 | 
			
		||||
 | 
			
		||||
        listen_addr = ('localhost', 9999)
 | 
			
		||||
        listen_addr = ('192.0.2.1', 9999)
 | 
			
		||||
        wsgi_server_cls.assert_called_once_with(
 | 
			
		||||
            listen_addr[0],
 | 
			
		||||
            listen_addr[1],
 | 
			
		||||
 
 | 
			
		||||
@@ -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,30 +47,17 @@ class TestBaseTeethAgent(unittest.TestCase):
 | 
			
		||||
        self.api_client.session.request.return_value = response
 | 
			
		||||
 | 
			
		||||
        heartbeat_before = self.api_client.heartbeat(
 | 
			
		||||
            hardware_info=self.hardware_info,
 | 
			
		||||
            version='15',
 | 
			
		||||
            mode='STANDBY')
 | 
			
		||||
            uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
 | 
			
		||||
            advertise_address=('192.0.2.1', '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], 'PUT')
 | 
			
		||||
        self.assertEqual(request_args[1], API_URL + 'v1/agents')
 | 
			
		||||
 | 
			
		||||
        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',
 | 
			
		||||
                'id': 'a:b:c:d',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                'type': 'mac_address',
 | 
			
		||||
                'id': '0:1:2:3',
 | 
			
		||||
            },
 | 
			
		||||
        ])
 | 
			
		||||
        self.assertEqual(request_args[0], 'POST')
 | 
			
		||||
        self.assertEqual(request_args[1], API_URL + heartbeat_path)
 | 
			
		||||
 | 
			
		||||
    def test_heartbeat_requests_exception(self):
 | 
			
		||||
        self.api_client.session.request = mock.Mock()
 | 
			
		||||
@@ -78,9 +65,8 @@ class TestBaseTeethAgent(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(errors.HeartbeatError,
 | 
			
		||||
                          self.api_client.heartbeat,
 | 
			
		||||
                          hardware_info=self.hardware_info,
 | 
			
		||||
                          version='15',
 | 
			
		||||
                          mode='STANDBY')
 | 
			
		||||
                          uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
 | 
			
		||||
                          advertise_address=('192.0.2.1', '9999'))
 | 
			
		||||
 | 
			
		||||
    def test_heartbeat_invalid_status_code(self):
 | 
			
		||||
        response = httmock.response(status_code=404)
 | 
			
		||||
@@ -89,9 +75,8 @@ class TestBaseTeethAgent(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(errors.HeartbeatError,
 | 
			
		||||
                          self.api_client.heartbeat,
 | 
			
		||||
                          hardware_info=self.hardware_info,
 | 
			
		||||
                          version='15',
 | 
			
		||||
                          mode='STANDBY')
 | 
			
		||||
                          uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
 | 
			
		||||
                          advertise_address=('192.0.2.1', '9999'))
 | 
			
		||||
 | 
			
		||||
    def test_heartbeat_missing_heartbeat_before_header(self):
 | 
			
		||||
        response = httmock.response(status_code=204)
 | 
			
		||||
@@ -100,9 +85,8 @@ class TestBaseTeethAgent(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(errors.HeartbeatError,
 | 
			
		||||
                          self.api_client.heartbeat,
 | 
			
		||||
                          hardware_info=self.hardware_info,
 | 
			
		||||
                          version='15',
 | 
			
		||||
                          mode='STANDBY')
 | 
			
		||||
                          uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
 | 
			
		||||
                          advertise_address=('192.0.2.1', '9999'))
 | 
			
		||||
 | 
			
		||||
    def test_heartbeat_invalid_heartbeat_before_header(self):
 | 
			
		||||
        response = httmock.response(status_code=204, headers={
 | 
			
		||||
@@ -113,6 +97,75 @@ class TestBaseTeethAgent(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(errors.HeartbeatError,
 | 
			
		||||
                          self.api_client.heartbeat,
 | 
			
		||||
                          uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
 | 
			
		||||
                          advertise_address=('192.0.2.1', '9999'))
 | 
			
		||||
 | 
			
		||||
    def test_lookup_node(self):
 | 
			
		||||
        response = httmock.response(status_code=200, content={
 | 
			
		||||
            'node': {
 | 
			
		||||
                'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.api_client.session.request = mock.Mock()
 | 
			
		||||
        self.api_client.session.request.return_value = response
 | 
			
		||||
 | 
			
		||||
        self.api_client.lookup_node(
 | 
			
		||||
            hardware_info=self.hardware_info,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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['hardware'], [
 | 
			
		||||
            {
 | 
			
		||||
                'type': 'mac_address',
 | 
			
		||||
                'id': 'aa:bb:cc:dd:ee:ff',
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                'type': 'mac_address',
 | 
			
		||||
                'id': 'ff:ee:dd:cc:bb:aa',
 | 
			
		||||
            },
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
    def test_lookup_node_bad_response_code(self):
 | 
			
		||||
        response = httmock.response(status_code=400, content={
 | 
			
		||||
            'node': {
 | 
			
		||||
                'uuid': 'deadbeef-dabb-ad00-b105-f00d00bab10c'
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        self.api_client.session.request = mock.Mock()
 | 
			
		||||
        self.api_client.session.request.return_value = response
 | 
			
		||||
 | 
			
		||||
        self.assertRaises(errors.LookupNodeError,
 | 
			
		||||
                          self.api_client.lookup_node,
 | 
			
		||||
                          hardware_info=self.hardware_info,
 | 
			
		||||
                          version='15',
 | 
			
		||||
                          mode='STANDBY')
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    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.LookupNodeError,
 | 
			
		||||
                          self.api_client.lookup_node,
 | 
			
		||||
                          hardware_info=self.hardware_info
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_lookup_node_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.LookupNodeError,
 | 
			
		||||
                          self.api_client.lookup_node,
 | 
			
		||||
                          hardware_info=self.hardware_info,
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user