2013-12-20 16:14:20 -08:00
|
|
|
"""
|
|
|
|
Copyright 2013 Rackspace, Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
"""
|
|
|
|
|
2014-01-05 21:46:49 -08:00
|
|
|
import json
|
2013-12-20 16:14:20 -08:00
|
|
|
import time
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
import mock
|
|
|
|
import pkg_resources
|
2014-03-07 15:36:22 -08:00
|
|
|
from wsgiref import simple_server
|
2013-12-20 16:14:20 -08:00
|
|
|
|
2014-01-05 21:46:49 -08:00
|
|
|
|
2014-01-15 15:23:16 -08:00
|
|
|
from teeth_agent import agent
|
2013-12-20 16:14:20 -08:00
|
|
|
from teeth_agent import base
|
2014-03-17 10:58:39 -07:00
|
|
|
from teeth_agent import encoding
|
2013-12-26 17:46:50 -08:00
|
|
|
from teeth_agent import errors
|
2014-03-05 15:37:57 -08:00
|
|
|
from teeth_agent import hardware
|
2013-12-20 16:14:20 -08:00
|
|
|
|
2014-01-05 21:46:49 -08:00
|
|
|
EXPECTED_ERROR = RuntimeError('command execution failed')
|
|
|
|
|
|
|
|
|
2014-02-06 11:04:44 -08:00
|
|
|
def foo_execute(*args, **kwargs):
|
2014-02-05 15:41:48 -08:00
|
|
|
if kwargs['fail']:
|
|
|
|
raise EXPECTED_ERROR
|
|
|
|
else:
|
|
|
|
return 'command execution succeeded'
|
2014-01-05 21:46:49 -08:00
|
|
|
|
|
|
|
|
2014-01-15 15:23:16 -08:00
|
|
|
class FakeMode(base.BaseAgentMode):
|
|
|
|
def __init__(self):
|
|
|
|
super(FakeMode, self).__init__('FAKE')
|
|
|
|
|
|
|
|
|
2014-01-07 23:46:12 -08:00
|
|
|
class TestHeartbeater(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
self.mock_agent = mock.Mock()
|
2014-01-15 15:23:16 -08:00
|
|
|
self.heartbeater = agent.TeethAgentHeartbeater(self.mock_agent)
|
2014-01-07 23:46:12 -08:00
|
|
|
self.heartbeater.api = mock.Mock()
|
2014-03-05 15:37:57 -08:00
|
|
|
self.heartbeater.hardware = mock.create_autospec(
|
|
|
|
hardware.HardwareManager)
|
2014-01-07 23:46:12 -08:00
|
|
|
self.heartbeater.stop_event = mock.Mock()
|
|
|
|
|
2014-03-17 15:17:27 -07:00
|
|
|
@mock.patch('teeth_agent.agent._time')
|
2014-01-07 23:46:12 -08:00
|
|
|
@mock.patch('random.uniform')
|
|
|
|
def test_heartbeat(self, mocked_uniform, mocked_time):
|
|
|
|
time_responses = []
|
|
|
|
uniform_responses = []
|
|
|
|
heartbeat_responses = []
|
|
|
|
wait_responses = []
|
|
|
|
expected_stop_event_calls = []
|
|
|
|
|
|
|
|
# FIRST RUN:
|
|
|
|
# initial delay is 0
|
|
|
|
expected_stop_event_calls.append(mock.call(0))
|
|
|
|
wait_responses.append(False)
|
|
|
|
# next heartbeat due at t=100
|
|
|
|
heartbeat_responses.append(100)
|
|
|
|
# random interval multiplier is 0.5
|
|
|
|
uniform_responses.append(0.5)
|
|
|
|
# time is now 50
|
|
|
|
time_responses.append(50)
|
|
|
|
|
|
|
|
# SECOND RUN:
|
|
|
|
# (100 - 50) * .5 = 25 (t becomes ~75)
|
|
|
|
expected_stop_event_calls.append(mock.call(25.0))
|
|
|
|
wait_responses.append(False)
|
2014-01-08 14:34:44 -08:00
|
|
|
# next heartbeat due at t=180
|
2014-01-07 23:46:12 -08:00
|
|
|
heartbeat_responses.append(180)
|
|
|
|
# random interval multiplier is 0.4
|
|
|
|
uniform_responses.append(0.4)
|
|
|
|
# time is now 80
|
|
|
|
time_responses.append(80)
|
|
|
|
|
|
|
|
# THIRD RUN:
|
|
|
|
# (180 - 80) * .4 = 40 (t becomes ~120)
|
|
|
|
expected_stop_event_calls.append(mock.call(40.0))
|
|
|
|
wait_responses.append(False)
|
|
|
|
# this heartbeat attempt fails
|
|
|
|
heartbeat_responses.append(Exception('uh oh!'))
|
|
|
|
# we check the time to generate a fake deadline, now t=125
|
|
|
|
time_responses.append(125)
|
|
|
|
# random interval multiplier is 0.5
|
|
|
|
uniform_responses.append(0.5)
|
|
|
|
# time is now 125.5
|
|
|
|
time_responses.append(125.5)
|
|
|
|
|
|
|
|
# FOURTH RUN:
|
|
|
|
# (125.5 - 125.0) * .5 = 0.25
|
|
|
|
expected_stop_event_calls.append(mock.call(0.25))
|
|
|
|
# Stop now
|
|
|
|
wait_responses.append(True)
|
|
|
|
|
|
|
|
# Hook it up and run it
|
|
|
|
mocked_time.side_effect = time_responses
|
|
|
|
mocked_uniform.side_effect = uniform_responses
|
|
|
|
self.heartbeater.api.heartbeat.side_effect = heartbeat_responses
|
|
|
|
self.heartbeater.stop_event.wait.side_effect = wait_responses
|
|
|
|
self.heartbeater.run()
|
|
|
|
|
|
|
|
# Validate expectations
|
|
|
|
self.assertEqual(self.heartbeater.stop_event.wait.call_args_list,
|
|
|
|
expected_stop_event_calls)
|
|
|
|
self.assertEqual(self.heartbeater.error_delay, 2.7)
|
|
|
|
|
|
|
|
|
2014-01-15 15:23:16 -08:00
|
|
|
class TestBaseAgent(unittest.TestCase):
|
2013-12-20 16:14:20 -08:00
|
|
|
def setUp(self):
|
2014-03-17 10:58:39 -07:00
|
|
|
self.encoder = encoding.RESTJSONEncoder(indent=4)
|
2014-01-15 15:23:16 -08:00
|
|
|
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
|
2014-03-19 14:01:49 -07:00
|
|
|
('203.0.113.1', 9990),
|
|
|
|
('192.0.2.1', 9999))
|
2013-12-20 16:14:20 -08:00
|
|
|
|
2014-01-05 21:46:49 -08:00
|
|
|
def assertEqualEncoded(self, a, b):
|
|
|
|
# Evidently JSONEncoder.default() can't handle None (??) so we have to
|
|
|
|
# use encode() to generate JSON, then json.loads() to get back a python
|
|
|
|
# object.
|
|
|
|
a_encoded = self.encoder.encode(a)
|
|
|
|
b_encoded = self.encoder.encode(b)
|
|
|
|
self.assertEqual(json.loads(a_encoded), json.loads(b_encoded))
|
|
|
|
|
2013-12-20 16:14:20 -08:00
|
|
|
def test_get_status(self):
|
|
|
|
started_at = time.time()
|
|
|
|
self.agent.started_at = started_at
|
|
|
|
|
|
|
|
status = self.agent.get_status()
|
2014-03-17 12:01:33 -07:00
|
|
|
self.assertTrue(isinstance(status, agent.TeethAgentStatus))
|
2013-12-20 16:14:20 -08:00
|
|
|
self.assertEqual(status.started_at, started_at)
|
|
|
|
self.assertEqual(status.version,
|
|
|
|
pkg_resources.get_distribution('teeth-agent').version)
|
|
|
|
|
|
|
|
def test_execute_command(self):
|
2013-12-25 21:00:30 -08:00
|
|
|
do_something_impl = mock.Mock()
|
2014-01-23 12:36:29 -08:00
|
|
|
self.agent.mode_implementation = FakeMode()
|
2014-01-21 10:42:43 -08:00
|
|
|
command_map = self.agent.mode_implementation.command_map
|
|
|
|
command_map['do_something'] = do_something_impl
|
2013-12-25 21:00:30 -08:00
|
|
|
|
2014-01-23 12:36:29 -08:00
|
|
|
self.agent.execute_command('fake.do_something', foo='bar')
|
2013-12-31 11:50:01 -08:00
|
|
|
do_something_impl.assert_called_once_with('do_something', foo='bar')
|
2013-12-20 16:14:20 -08:00
|
|
|
|
2013-12-26 17:46:50 -08:00
|
|
|
def test_execute_invalid_command(self):
|
|
|
|
self.assertRaises(errors.InvalidCommandError,
|
|
|
|
self.agent.execute_command,
|
|
|
|
'do_something',
|
|
|
|
foo='bar')
|
|
|
|
|
2014-03-07 15:36:22 -08:00
|
|
|
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
|
2014-01-10 13:49:58 -08:00
|
|
|
def test_run(self, wsgi_server_cls):
|
|
|
|
wsgi_server = wsgi_server_cls.return_value
|
|
|
|
wsgi_server.start.side_effect = KeyboardInterrupt()
|
|
|
|
|
2014-01-07 18:15:11 -08:00
|
|
|
self.agent.heartbeater = mock.Mock()
|
2014-03-18 10:36:33 -07:00
|
|
|
self.agent.api_client.lookup_node = mock.Mock()
|
2013-12-20 16:14:20 -08:00
|
|
|
self.agent.run()
|
2014-01-10 13:49:58 -08:00
|
|
|
|
2014-03-19 14:01:49 -07:00
|
|
|
listen_addr = ('192.0.2.1', 9999)
|
2014-03-07 15:36:22 -08:00
|
|
|
wsgi_server_cls.assert_called_once_with(
|
|
|
|
listen_addr[0],
|
|
|
|
listen_addr[1],
|
|
|
|
self.agent.api,
|
|
|
|
server_class=simple_server.WSGIServer)
|
2014-03-14 16:48:44 -07:00
|
|
|
wsgi_server.serve_forever.assert_called_once()
|
2014-01-10 13:49:58 -08:00
|
|
|
|
2014-01-07 18:15:11 -08:00
|
|
|
self.agent.heartbeater.start.assert_called_once_with()
|
2013-12-20 16:14:20 -08:00
|
|
|
|
2014-01-05 21:46:49 -08:00
|
|
|
def test_async_command_success(self):
|
2014-02-05 15:41:48 -08:00
|
|
|
result = base.AsyncCommandResult('foo_command', {'fail': False},
|
|
|
|
foo_execute)
|
2014-01-05 21:46:49 -08:00
|
|
|
expected_result = {
|
|
|
|
'id': result.id,
|
|
|
|
'command_name': 'foo_command',
|
|
|
|
'command_params': {
|
|
|
|
'fail': False,
|
|
|
|
},
|
|
|
|
'command_status': 'RUNNING',
|
|
|
|
'command_result': None,
|
|
|
|
'command_error': None,
|
|
|
|
}
|
|
|
|
self.assertEqualEncoded(result, expected_result)
|
|
|
|
|
|
|
|
result.start()
|
|
|
|
result.join()
|
|
|
|
|
|
|
|
expected_result['command_status'] = 'SUCCEEDED'
|
|
|
|
expected_result['command_result'] = 'command execution succeeded'
|
|
|
|
|
|
|
|
self.assertEqualEncoded(result, expected_result)
|
|
|
|
|
|
|
|
def test_async_command_failure(self):
|
2014-02-05 15:41:48 -08:00
|
|
|
result = base.AsyncCommandResult('foo_command', {'fail': True},
|
|
|
|
foo_execute)
|
2014-01-05 21:46:49 -08:00
|
|
|
expected_result = {
|
|
|
|
'id': result.id,
|
|
|
|
'command_name': 'foo_command',
|
|
|
|
'command_params': {
|
|
|
|
'fail': True,
|
|
|
|
},
|
|
|
|
'command_status': 'RUNNING',
|
|
|
|
'command_result': None,
|
|
|
|
'command_error': None,
|
|
|
|
}
|
|
|
|
self.assertEqualEncoded(result, expected_result)
|
|
|
|
|
|
|
|
result.start()
|
|
|
|
result.join()
|
|
|
|
|
|
|
|
expected_result['command_status'] = 'FAILED'
|
|
|
|
expected_result['command_error'] = errors.CommandExecutionError(
|
|
|
|
str(EXPECTED_ERROR))
|
|
|
|
|
|
|
|
self.assertEqualEncoded(result, expected_result)
|