Merge pull request #66 from rackerlabs/JoshNang/heartbeat
Using Ironic vendor passthru for heartbeat
This commit is contained in:
commit
6e366520bb
teeth_agent
@ -91,9 +91,9 @@ class TeethAgentHeartbeater(threading.Thread):
|
|||||||
def do_heartbeat(self):
|
def do_heartbeat(self):
|
||||||
try:
|
try:
|
||||||
deadline = self.api.heartbeat(
|
deadline = self.api.heartbeat(
|
||||||
hardware_info=self.hardware.list_hardware_info(),
|
uuid=self.agent.get_node_uuid(),
|
||||||
version=self.agent.version,
|
advertise_address=self.agent.advertise_address
|
||||||
mode=self.agent.get_mode_name())
|
)
|
||||||
self.error_delay = self.initial_delay
|
self.error_delay = self.initial_delay
|
||||||
self.log.info('heartbeat successful')
|
self.log.info('heartbeat successful')
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -112,10 +112,11 @@ class TeethAgentHeartbeater(threading.Thread):
|
|||||||
|
|
||||||
|
|
||||||
class TeethAgent(object):
|
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_url = api_url
|
||||||
|
self.api_client = overlord_agent_api.APIClient(self.api_url)
|
||||||
self.listen_address = listen_address
|
self.listen_address = listen_address
|
||||||
self.ipaddr = ipaddr
|
self.advertise_address = advertise_address
|
||||||
self.mode_implementation = None
|
self.mode_implementation = None
|
||||||
self.version = pkg_resources.get_distribution('teeth-agent').version
|
self.version = pkg_resources.get_distribution('teeth-agent').version
|
||||||
self.api = app.VersionSelectorApplication(self)
|
self.api = app.VersionSelectorApplication(self)
|
||||||
@ -125,6 +126,7 @@ class TeethAgent(object):
|
|||||||
self.command_lock = threading.Lock()
|
self.command_lock = threading.Lock()
|
||||||
self.log = log.getLogger(__name__)
|
self.log = log.getLogger(__name__)
|
||||||
self.started_at = None
|
self.started_at = None
|
||||||
|
self.node = None
|
||||||
|
|
||||||
def get_mode_name(self):
|
def get_mode_name(self):
|
||||||
if self.mode_implementation:
|
if self.mode_implementation:
|
||||||
@ -143,6 +145,11 @@ class TeethAgent(object):
|
|||||||
def get_agent_mac_addr(self):
|
def get_agent_mac_addr(self):
|
||||||
return self.hardware.get_primary_mac_address()
|
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):
|
def list_command_results(self):
|
||||||
return self.command_results.values()
|
return self.command_results.values()
|
||||||
|
|
||||||
@ -204,6 +211,10 @@ class TeethAgent(object):
|
|||||||
def run(self):
|
def run(self):
|
||||||
"""Run the Teeth Agent."""
|
"""Run the Teeth Agent."""
|
||||||
self.started_at = _time()
|
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()
|
self.heartbeater.start()
|
||||||
wsgi = simple_server.make_server(
|
wsgi = simple_server.make_server(
|
||||||
self.listen_address[0],
|
self.listen_address[0],
|
||||||
@ -229,5 +240,12 @@ def _load_mode_implementation(mode_name):
|
|||||||
return mgr.driver
|
return mgr.driver
|
||||||
|
|
||||||
|
|
||||||
def build_agent(api_url, listen_host, listen_port, ipaddr):
|
def build_agent(api_url,
|
||||||
return TeethAgent(api_url, (listen_host, listen_port), ipaddr)
|
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',
|
parser.add_argument('--listen-host',
|
||||||
default='0.0.0.0',
|
default='0.0.0.0',
|
||||||
type=str,
|
type=str,
|
||||||
help=('The IP address to listen on.'))
|
help='The IP address to listen on.')
|
||||||
|
|
||||||
parser.add_argument('--listen-port',
|
parser.add_argument('--listen-port',
|
||||||
default=9999,
|
default=9999,
|
||||||
type=int,
|
type=int,
|
||||||
help='The port to listen on')
|
help='The port to listen on')
|
||||||
|
parser.add_argument('--advertise-host',
|
||||||
parser.add_argument('--ipaddr',
|
default='0.0.0.0',
|
||||||
required=True,
|
type=str,
|
||||||
help='The external IP address to advertise to ironic')
|
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()
|
args = parser.parse_args()
|
||||||
agent.build_agent(args.api_url,
|
agent.build_agent(args.api_url,
|
||||||
|
args.advertise_host,
|
||||||
|
args.advertise_port,
|
||||||
args.listen_host,
|
args.listen_host,
|
||||||
args.listen_port,
|
args.listen_port).run()
|
||||||
args.ipaddr).run()
|
|
||||||
|
@ -110,6 +110,17 @@ class HeartbeatError(OverlordAPIError):
|
|||||||
super(HeartbeatError, self).__init__(details)
|
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):
|
class ImageDownloadError(RESTError):
|
||||||
"""Error raised when an image cannot be downloaded."""
|
"""Error raised when an image cannot be downloaded."""
|
||||||
|
|
||||||
|
@ -46,17 +46,16 @@ class APIClient(object):
|
|||||||
headers=request_headers,
|
headers=request_headers,
|
||||||
data=data)
|
data=data)
|
||||||
|
|
||||||
def heartbeat(self, hardware_info, mode, version):
|
def heartbeat(self, uuid, advertise_address):
|
||||||
path = '/{api_version}/agents'.format(api_version=self.api_version)
|
path = '/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat'.format(
|
||||||
|
api_version=self.api_version,
|
||||||
|
uuid=uuid
|
||||||
|
)
|
||||||
data = {
|
data = {
|
||||||
'hardware': hardware_info,
|
'agent_url': self._get_agent_url(advertise_address)
|
||||||
'mode': mode,
|
|
||||||
'version': version,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._request('PUT', path, data=data)
|
response = self._request('POST', path, data=data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise errors.HeartbeatError(str(e))
|
raise errors.HeartbeatError(str(e))
|
||||||
|
|
||||||
@ -71,18 +70,36 @@ class APIClient(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise errors.HeartbeatError('Invalid Heartbeat-Before header')
|
raise errors.HeartbeatError('Invalid Heartbeat-Before header')
|
||||||
|
|
||||||
def get_configuration(self, mac_addr):
|
def lookup_node(self, hardware_info):
|
||||||
path = '/{api_version}/agents/{mac_addr}/configuration'.format(
|
path = '/{api_version}/drivers/teeth/lookup'.format(
|
||||||
api_version=self.api_version,
|
api_version=self.api_version
|
||||||
mac_addr=mac_addr)
|
)
|
||||||
|
# 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:
|
if response.status_code != requests.codes.OK:
|
||||||
msg = 'Invalid status code: {0}'.format(response.status_code)
|
msg = 'Invalid status code: {0}'.format(response.status_code)
|
||||||
raise errors.OverlordAPIError(msg)
|
raise errors.LookupNodeError(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.loads(response.content)
|
content = json.loads(response.content)
|
||||||
except Exception as e:
|
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):
|
def setUp(self):
|
||||||
self.encoder = encoding.RESTJSONEncoder(indent=4)
|
self.encoder = encoding.RESTJSONEncoder(indent=4)
|
||||||
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
|
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
|
||||||
('localhost', 9999),
|
('203.0.113.1', 9990),
|
||||||
'192.168.1.1')
|
('192.0.2.1', 9999))
|
||||||
|
|
||||||
def assertEqualEncoded(self, a, b):
|
def assertEqualEncoded(self, a, b):
|
||||||
# Evidently JSONEncoder.default() can't handle None (??) so we have to
|
# 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()
|
wsgi_server.start.side_effect = KeyboardInterrupt()
|
||||||
|
|
||||||
self.agent.heartbeater = mock.Mock()
|
self.agent.heartbeater = mock.Mock()
|
||||||
|
self.agent.api_client.lookup_node = mock.Mock()
|
||||||
self.agent.run()
|
self.agent.run()
|
||||||
|
|
||||||
listen_addr = ('localhost', 9999)
|
listen_addr = ('192.0.2.1', 9999)
|
||||||
wsgi_server_cls.assert_called_once_with(
|
wsgi_server_cls.assert_called_once_with(
|
||||||
listen_addr[0],
|
listen_addr[0],
|
||||||
listen_addr[1],
|
listen_addr[1],
|
||||||
|
@ -32,9 +32,9 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
self.api_client = overlord_agent_api.APIClient(API_URL)
|
self.api_client = overlord_agent_api.APIClient(API_URL)
|
||||||
self.hardware_info = [
|
self.hardware_info = [
|
||||||
hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS,
|
hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS,
|
||||||
'a:b:c:d'),
|
'aa:bb:cc:dd:ee:ff'),
|
||||||
hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS,
|
hardware.HardwareInfo(hardware.HardwareType.MAC_ADDRESS,
|
||||||
'0:1:2:3'),
|
'ff:ee:dd:cc:bb:aa'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_successful_heartbeat(self):
|
def test_successful_heartbeat(self):
|
||||||
@ -47,30 +47,17 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
self.api_client.session.request.return_value = response
|
self.api_client.session.request.return_value = response
|
||||||
|
|
||||||
heartbeat_before = self.api_client.heartbeat(
|
heartbeat_before = self.api_client.heartbeat(
|
||||||
hardware_info=self.hardware_info,
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
version='15',
|
advertise_address=('192.0.2.1', '9999')
|
||||||
mode='STANDBY')
|
)
|
||||||
|
|
||||||
self.assertEqual(heartbeat_before, expected_heartbeat_before)
|
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]
|
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/agents')
|
self.assertEqual(request_args[1], API_URL + heartbeat_path)
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
def test_heartbeat_requests_exception(self):
|
def test_heartbeat_requests_exception(self):
|
||||||
self.api_client.session.request = mock.Mock()
|
self.api_client.session.request = mock.Mock()
|
||||||
@ -78,9 +65,8 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(errors.HeartbeatError,
|
self.assertRaises(errors.HeartbeatError,
|
||||||
self.api_client.heartbeat,
|
self.api_client.heartbeat,
|
||||||
hardware_info=self.hardware_info,
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
version='15',
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
mode='STANDBY')
|
|
||||||
|
|
||||||
def test_heartbeat_invalid_status_code(self):
|
def test_heartbeat_invalid_status_code(self):
|
||||||
response = httmock.response(status_code=404)
|
response = httmock.response(status_code=404)
|
||||||
@ -89,9 +75,8 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(errors.HeartbeatError,
|
self.assertRaises(errors.HeartbeatError,
|
||||||
self.api_client.heartbeat,
|
self.api_client.heartbeat,
|
||||||
hardware_info=self.hardware_info,
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
version='15',
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
mode='STANDBY')
|
|
||||||
|
|
||||||
def test_heartbeat_missing_heartbeat_before_header(self):
|
def test_heartbeat_missing_heartbeat_before_header(self):
|
||||||
response = httmock.response(status_code=204)
|
response = httmock.response(status_code=204)
|
||||||
@ -100,9 +85,8 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(errors.HeartbeatError,
|
self.assertRaises(errors.HeartbeatError,
|
||||||
self.api_client.heartbeat,
|
self.api_client.heartbeat,
|
||||||
hardware_info=self.hardware_info,
|
uuid='deadbeef-dabb-ad00-b105-f00d00bab10c',
|
||||||
version='15',
|
advertise_address=('192.0.2.1', '9999'))
|
||||||
mode='STANDBY')
|
|
||||||
|
|
||||||
def test_heartbeat_invalid_heartbeat_before_header(self):
|
def test_heartbeat_invalid_heartbeat_before_header(self):
|
||||||
response = httmock.response(status_code=204, headers={
|
response = httmock.response(status_code=204, headers={
|
||||||
@ -113,6 +97,75 @@ class TestBaseTeethAgent(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(errors.HeartbeatError,
|
self.assertRaises(errors.HeartbeatError,
|
||||||
self.api_client.heartbeat,
|
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,
|
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,
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user