def test_error_per_tenant(self): """ When a request for servers fails, the associated effect results in None, and an error is logged. """ log = mock_log() log.err.return_value = None groups = { "t1": [{ 'tenantId': 't1', 'groupId': 'g1', 'desired': 0 }], "t2": [{ 'tenantId': 't2', 'groupId': 'g2', 'desired': 0 }] } effs = get_all_metrics_effects(groups, log) results = [] for eff in effs: if eff.intent.tenant_id == 't1': results.append(resolve_effect(eff, {})) elif eff.intent.tenant_id == 't2': err = (ZeroDivisionError, ZeroDivisionError('foo bar'), None) results.append(resolve_effect(eff, err, is_error=True)) self.assertEqual( results, [None, [GroupMetrics('t1', 'g1', desired=0, actual=0, pending=0)]]) log.err.assert_called_once_with( CheckFailureValue(ZeroDivisionError('foo bar')))
def test_delete_and_verify_verify_404(self): """ :func:`delete_and_verify` gets server details after successful delete and succeeds if get server details returns 404 """ eff = delete_and_verify('sid') eff = resolve_effect( eff, service_request_error_response(APIError(204, {})), is_error=True) r = resolve_effect(eff, (StubResponse(404, {}), {"itemNotFound": {}})) self.assertIsNone(r)
def test_no_json_parsing_on_error(self): """ Whatever ``json_response`` is set to, it is ignored, if the response does not pass the success predicate (because errors may just be HTML or otherwise not JSON parsable, even if the success response would have been). """ svcreq = service_request(ServiceType.CLOUD_SERVERS, "GET", "servers", json_response=True).intent eff = self._concrete(svcreq) next_eff = resolve_authenticate(eff) stub_response = stub_pure_response("THIS IS A FAILURE", 500) with self.assertRaises(APIError) as cm: resolve_effect(next_eff, stub_response) self.assertEqual(cm.exception.body, "THIS IS A FAILURE")
def test_get_all_metrics(self): """ Metrics are returned based on the requests done to get server info. """ # Maybe this could use a parameterized "get_all_scaling_group_servers" # call to avoid needing to stub the nova responses, but it seems okay. servers_t1 = { 'g1': ([_server('g1', 'ACTIVE')] * 3 + [_server('g1', 'BUILD')] * 2), 'g2': [_server('g2', 'ACTIVE')]} servers_t2 = { 'g4': [_server('g4', 'ACTIVE'), _server('g4', 'BUILD')]} groups = { "t1": [{'tenantId': 't1', 'groupId': 'g1', 'desired': 3}, {'tenantId': 't1', 'groupId': 'g2', 'desired': 4}], "t2": [{'tenantId': 't2', 'groupId': 'g4', 'desired': 2}]} tenant_servers = {'t1': servers_t1, 't2': servers_t2} effs = get_all_metrics_effects(groups, mock_log()) # All the effs are wrapped in TenantScopes to indicate the tenant # of ServiceRequests made under them. We use that tenant to get the # stubbed result of get_all_scaling_group_servers. results = [ resolve_effect(eff, tenant_servers[eff.intent.tenant_id]) for eff in effs] self.assertEqual( set(reduce(operator.add, results)), set([GroupMetrics('t1', 'g1', desired=3, actual=3, pending=2), GroupMetrics('t1', 'g2', desired=4, actual=1, pending=0), GroupMetrics('t2', 'g4', desired=2, actual=1, pending=1)]))
def _assert_create_server_with_errs_has_status(self, exceptions, status): """ Helper function to make a :class:`CreateServer` effect, and resolve it with the provided exceptions, asserting that the result is the provided status, with the reason being the exception. """ eff = CreateServer( server_config=freeze({'server': {'flavorRef': '1'}})).as_effect() eff = resolve_effect(eff, 'random-name') for exc in exceptions: self.assertEqual( resolve_effect(eff, service_request_error_response(exc), is_error=True), (status, [ErrorReason.Exception( matches(ContainsAll([type(exc), exc])))]) )
def test_delete_and_verify_del_404(self): """ :func:`delete_and_verify` invokes server delete and succeeds on 404 """ eff = delete_and_verify('sid') self.assertEqual( eff.intent, service_request( ServiceType.CLOUD_SERVERS, 'DELETE', 'servers/sid', success_pred=has_code(404)).intent) self.assertEqual(resolve_effect(eff, (ANY, {})), (ANY, {}))
def test_should_retry(self): """ When called and can_retry returns True, a Delay based on next_interval is executed and the ultimate result is True. """ sdar = ShouldDelayAndRetry(can_retry=lambda f: True, next_interval=lambda f: 1.5) eff = sdar(get_exc_info()) next_eff = _perform_func_intent(eff) self.assertEqual(next_eff.intent, Delay(delay=1.5)) self.assertEqual(resolve_effect(next_eff, None), True)
def test_invalidate_on_auth_error_code(self): """ Upon authentication error, the auth cache is invalidated. """ eff = self._concrete(self.svcreq) next_eff = resolve_authenticate(eff) # When the HTTP response is an auth error, the auth cache is # invalidated, by way of the next effect: invalidate_eff = resolve_effect(next_eff, stub_pure_response("", 401)) expected_intent = InvalidateToken(self.authenticator, 1) self.assertEqual(invalidate_eff.intent, expected_intent) self.assertRaises(APIError, resolve_effect, invalidate_eff, None)
def test_delete_and_verify_verifies(self): """ :func:`delete_and_verify` verifies if the server task_state has changed to "deleting" after successful delete server call and succeeds if that has happened. The details call succeeds if it returns 404 """ eff = delete_and_verify('sid') eff = resolve_effect( eff, service_request_error_response(APIError(204, {})), is_error=True) self.assertEqual( eff.intent, service_request( ServiceType.CLOUD_SERVERS, 'GET', 'servers/sid', success_pred=has_code(200, 404)).intent) r = resolve_effect( eff, (StubResponse(200, {}), {'server': {"OS-EXT-STS:task_state": 'deleting'}})) self.assertIsNone(r)
def test_no_json_response(self): """ ``json_response`` can be set to :data:`False` to get the response object and the plaintext body of the response. """ svcreq = service_request(ServiceType.CLOUD_SERVERS, "GET", "servers", json_response=False).intent eff = self._concrete(svcreq) next_eff = resolve_authenticate(eff) stub_response = stub_pure_response("foo") result = resolve_effect(next_eff, stub_response) self.assertEqual(result, stub_response)
def resolve_service_request(service_request_intent): eff = concretize_service_request( authenticator=object(), log=object(), service_configs=make_service_configs(), throttler=lambda stype, method, tid: None, tenant_id='000000', service_request=service_request_intent) # "authenticate" eff = resolve_authenticate(eff) # make request return resolve_effect(eff, stub_response)
def test_error_per_tenant(self): """ When a request for servers fails, the associated effect results in None, and an error is logged. """ log = mock_log() log.err.return_value = None groups = { "t1": [{"tenantId": "t1", "groupId": "g1", "desired": 0}], "t2": [{"tenantId": "t2", "groupId": "g2", "desired": 0}], } effs = get_all_metrics_effects(groups, log) results = [] for eff in effs: if eff.intent.tenant_id == "t1": results.append(resolve_effect(eff, {})) elif eff.intent.tenant_id == "t2": err = (ZeroDivisionError, ZeroDivisionError("foo bar"), None) results.append(resolve_effect(eff, err, is_error=True)) self.assertEqual(results, [None, [GroupMetrics("t1", "g1", desired=0, actual=0, pending=0)]]) log.err.assert_called_once_with(CheckFailureValue(ZeroDivisionError("foo bar")))
def test_get_all_metrics(self): """ Metrics are returned based on the requests done to get server info. """ # Maybe this could use a parameterized "get_all_scaling_group_servers" # call to avoid needing to stub the nova responses, but it seems okay. servers_t1 = { 'g1': ([_server('g1', 'ACTIVE')] * 3 + [_server('g1', 'BUILD')] * 2), 'g2': [_server('g2', 'ACTIVE')] } servers_t2 = {'g4': [_server('g4', 'ACTIVE'), _server('g4', 'BUILD')]} groups = { "t1": [{ 'tenantId': 't1', 'groupId': 'g1', 'desired': 3 }, { 'tenantId': 't1', 'groupId': 'g2', 'desired': 4 }], "t2": [{ 'tenantId': 't2', 'groupId': 'g4', 'desired': 2 }] } tenant_servers = {'t1': servers_t1, 't2': servers_t2} effs = get_all_metrics_effects(groups, mock_log()) # All the effs are wrapped in TenantScopes to indicate the tenant # of ServiceRequests made under them. We use that tenant to get the # stubbed result of get_all_scaling_group_servers. results = [ resolve_effect(eff, tenant_servers[eff.intent.tenant_id]) for eff in effs ] self.assertEqual( set(reduce(operator.add, results)), set([ GroupMetrics('t1', 'g1', desired=3, actual=3, pending=2), GroupMetrics('t1', 'g2', desired=4, actual=1, pending=0), GroupMetrics('t2', 'g4', desired=2, actual=1, pending=1) ]))
def test_delete_and_verify_verify_unexpectedstatus(self): """ :func:`delete_and_verify` raises `UnexpectedServerStatus` error if server status returned after deleting is not "deleting" """ eff = delete_and_verify('sid') eff = resolve_effect( eff, service_request_error_response(APIError(204, {})), is_error=True) self.assertRaises( UnexpectedServerStatus, resolve_effect, eff, (StubResponse(200, {}), {'server': {"OS-EXT-STS:task_state": 'bad'}}) )
def test_json(self): """ JSON-serializable requests are dumped before being sent, and JSON-serialized responses are parsed. """ input_json = {"a": 1} output_json = {"b": 2} svcreq = service_request(ServiceType.CLOUD_SERVERS, "GET", "servers", data=input_json).intent eff = self._concrete(svcreq) # Input is serialized next_eff = resolve_authenticate(eff) self.assertEqual(next_eff.intent.data, json.dumps(input_json)) # Output is parsed response, body = stub_pure_response(json.dumps(output_json)) result = resolve_effect(next_eff, (response, body)) self.assertEqual(result, (response, output_json))
def test_delete_server(self, mock_dav): """ :obj:`DeleteServer.as_effect` calls `delete_and_verify` with retries. It returns SUCCESS on completion and RETRY on failure """ mock_dav.side_effect = lambda sid: Effect(sid) eff = DeleteServer(server_id='abc123').as_effect() self.assertIsInstance(eff.intent, Retry) self.assertEqual( eff.intent.should_retry, ShouldDelayAndRetry(can_retry=retry_times(3), next_interval=exponential_backoff_interval(2))) self.assertEqual(eff.intent.effect.intent, 'abc123') self.assertEqual( resolve_effect(eff, (None, {})), (StepResult.RETRY, [ErrorReason.String('must re-gather after deletion in order to ' 'update the active cache')]))
def test_create_server_request_with_name(self): """ :obj:`CreateServer.as_effect` produces a request for creating a server. If the name is given, a randomly generated suffix is appended to the server name. """ create = CreateServer( server_config=freeze({'server': {'name': 'myserver', 'flavorRef': '1'}})) eff = create.as_effect() self.assertEqual(eff.intent, Func(generate_server_name)) eff = resolve_effect(eff, 'random-name') self.assertEqual( eff.intent, service_request( ServiceType.CLOUD_SERVERS, 'POST', 'servers', data={'server': {'name': 'myserver-random-name', 'flavorRef': '1'}}, success_pred=has_code(202), reauth_codes=(401,)).intent)
def test_create_server_noname(self): """ :obj:`CreateServer.as_effect`, when no name is provided in the launch config, will generate the name will from scratch. This only verifies intent; result reporting is tested in :meth:`test_create_server`. """ create = CreateServer( server_config=freeze({'server': {'flavorRef': '1'}})) eff = create.as_effect() self.assertEqual(eff.intent, Func(generate_server_name)) eff = resolve_effect(eff, 'random-name') self.assertEqual( eff.intent, service_request( ServiceType.CLOUD_SERVERS, 'POST', 'servers', data={'server': {'name': 'random-name', 'flavorRef': '1'}}, success_pred=has_code(202), reauth_codes=(401,)).intent)
def resolve_authenticate(eff, token='token'): """Resolve an Authenticate effect with test data.""" return resolve_effect(eff, (token, fake_service_catalog))