def _fake_perform(self, dispatcher, effect): """ A test double for :func:`txeffect.perform`. :param dispatcher: The Effect dispatcher. :param effect: The effect to "execute". """ self.assertIdentical(dispatcher, self.dispatcher) self.assertIs(type(effect), Effect) tenant_scope = effect.intent self.assertEqual(tenant_scope.tenant_id, 'thetenantid') req = tenant_scope.effect.intent self.assertEqual(req.service_type, ServiceType.RACKCONNECT_V3) self.assertEqual(req.data, [{'load_balancer_pool': {'id': 'lb_id'}, 'cloud_server': {'id': 'server_id'}}]) self.assertEqual(req.url, 'load_balancer_pools/nodes') self.assertEqual(req.headers, None) # The method is either POST (add) or DELETE (remove). self.assertIn(req.method, ["POST", "DELETE"]) if req.method == "POST": self.assertEqual(req.success_pred, has_code(201)) # http://docs.rcv3.apiary.io/#post-%2Fv3%2F{tenant_id} # %2Fload_balancer_pools%2Fnodes return succeed(self.post_result) elif req.method == "DELETE": self.assertEqual(req.success_pred, has_code(204, 409)) # http://docs.rcv3.apiary.io/#delete-%2Fv3%2F{tenant_id} # %2Fload_balancer_pools%2Fnode return succeed(self.del_result)
def _create_stack_intent(self, data): return service_request( ServiceType.CLOUD_ORCHESTRATION, 'POST', 'stacks', data=data, success_pred=has_code(201), reauth_codes=(401,)).intent
def _delete_stack_intent(self, stack_name, stack_id): return service_request( ServiceType.CLOUD_ORCHESTRATION, 'DELETE', 'stacks/{0}/{1}'.format(stack_name, stack_id), success_pred=has_code(204), reauth_codes=(401,), json_response=False).intent
def test_publish_autoscale_event(self): """ Publish an event to cloudfeeds. Successfully handle non-JSON data. """ _log = object() eff = cf.publish_autoscale_event({'event': 'stuff'}, log=_log) expected = service_request( ServiceType.CLOUD_FEEDS, 'POST', 'autoscale/events', headers={'content-type': ['application/vnd.rackspace.atom+json']}, data={'event': 'stuff'}, log=_log, success_pred=has_code(201), json_response=False) # success dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(stub_pure_response('<this is xml>', 201)))]) resp, body = sync_perform(dispatcher, eff) self.assertEqual(body, '<this is xml>') # Add regression test that 202 should be an API error because this # is a bug in CF dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(stub_pure_response('<this is xml>', 202)))]) self.assertRaises(APIError, sync_perform, dispatcher, eff)
def get_server_details(server_id): """ Get details for one particular server. :ivar str server_id: a Nova server ID. Succeed on 200. :return: a `tuple` of (:obj:`twisted.web.client.Response`, JSON `dict`) :raise: :class:`NoSuchServer`, :class:`NovaRateLimitError`, :class:`NovaComputeFaultError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_SERVERS, 'GET', append_segments('servers', server_id), success_pred=has_code(200)) @only_json_api_errors def _parse_known_errors(code, json_body): other_errors = [ (404, ('itemNotFound', 'message'), None, partial(NoSuchServerError, server_id=six.text_type(server_id))), ] match_errors(_nova_standard_errors + other_errors, code, json_body) return eff.on(error=_parse_known_errors).on( log_success_response('request-one-server-details', identity))
def _create_stack_intent(self, data): return service_request(ServiceType.CLOUD_ORCHESTRATION, 'POST', 'stacks', data=data, success_pred=has_code(201), reauth_codes=(401, )).intent
def _delete_stack_intent(self, stack_name, stack_id): return service_request(ServiceType.CLOUD_ORCHESTRATION, 'DELETE', 'stacks/{0}/{1}'.format(stack_name, stack_id), success_pred=has_code(204), reauth_codes=(401, ), json_response=False).intent
def change_clb_node(lb_id, node_id, condition, weight, _type="PRIMARY"): """ Generate effect to change a node on a load balancer. :param str lb_id: The load balancer ID to add the nodes to :param str node_id: The node id to change. :param str condition: The condition to change to: one of "ENABLED", "DRAINING", or "DISABLED" :param int weight: The weight to change to. :param str _type: The type to change the CLB node to. :return: :class:`ServiceRequest` effect :raises: :class:`CLBImmutableError`, :class:`CLBDeletedError`, :class:`NoSuchCLBError`, :class:`NoSuchCLBNodeError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', append_segments('loadbalancers', lb_id, 'nodes', node_id), data={'node': { 'condition': condition, 'weight': weight, 'type': _type}}, success_pred=has_code(202)) @_only_json_api_errors def _parse_known_errors(code, json_body): _process_clb_api_error(code, json_body, lb_id) _match_errors( _expand_clb_matches( [(404, _CLB_NO_SUCH_NODE_PATTERN, NoSuchCLBNodeError)], lb_id=lb_id, node_id=node_id), code, json_body) return eff.on(error=_parse_known_errors)
def test_change_clb_node_default_type(self): """ Produce a request for modifying a node on a load balancer with the default type, which returns a successful result on 202. """ eff = change_clb_node(lb_id=self.lb_id, node_id='1234', condition="DRAINING", weight=50) expected = service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', 'loadbalancers/{0}/nodes/1234'.format( self.lb_id), data={ 'node': { 'condition': 'DRAINING', 'weight': 50, 'type': 'PRIMARY' } }, success_pred=has_code(202)) dispatcher = EQFDispatcher([ (expected.intent, service_request_eqf(stub_pure_response('', 202))) ]) self.assertEqual(sync_perform(dispatcher, eff), stub_pure_response(None, 202))
def get_server_details(server_id): """ Get details for one particular server. :ivar str server_id: a Nova server ID. Succeed on 200. :return: a `tuple` of (:obj:`twisted.web.client.Response`, JSON `dict`) :raise: :class:`NoSuchServer`, :class:`NovaRateLimitError`, :class:`NovaComputeFaultError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_SERVERS, 'GET', append_segments('servers', server_id), success_pred=has_code(200)) @_only_json_api_errors def _parse_known_errors(code, json_body): other_errors = [ (404, ('itemNotFound', 'message'), None, partial(NoSuchServerError, server_id=six.text_type(server_id))), ] _match_errors(_nova_standard_errors + other_errors, code, json_body) return eff.on(error=_parse_known_errors).on( log_success_response('request-one-server-details', identity))
def _check_stack_intent(self, stack_name, stack_id): return service_request( ServiceType.CLOUD_ORCHESTRATION, 'POST', 'stacks/{0}/{1}/actions'.format(stack_name, stack_id), data={'check': None}, success_pred=has_code(200, 201), reauth_codes=(401,), json_response=False).intent
def test_error(self): """ :func:`add_error_handling` ostensibly invokes :func:`check_response`. """ response = stub_pure_response("", code=404) request_fn = add_error_handling(has_code(200), stub_request(response)) eff = request_fn('GET', '/xyzzy') self.assertRaises(APIError, resolve_stubs, eff)
def _update_stack_intent(self, stack_name, stack_id, stack_args): return service_request(ServiceType.CLOUD_ORCHESTRATION, 'PUT', 'stacks/{0}/{1}'.format(stack_name, stack_id), data=stack_args, success_pred=has_code(202), reauth_codes=(401, ), json_response=False).intent
def _update_stack_intent(self, stack_name, stack_id, stack_args): return service_request( ServiceType.CLOUD_ORCHESTRATION, 'PUT', 'stacks/{0}/{1}'.format(stack_name, stack_id), data=stack_args, success_pred=has_code(202), reauth_codes=(401,), json_response=False).intent
def expected_node_removal_req(self, nodes=(1, 2)): """ :return: Expected effect for a node removal request. """ return service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'DELETE', 'loadbalancers/{}/nodes'.format(self.lb_id), params={'id': map(str, nodes)}, success_pred=has_code(202))
def test_add_clb_nodes(self): """ Produce a request for adding nodes to a load balancer, which returns a successful result on a 202. Parse the common CLB errors, and a :class:`CLBDuplicateNodesError`. """ nodes = [{"address": "1.1.1.1", "port": 80, "condition": "ENABLED"}, {"address": "1.1.1.2", "port": 80, "condition": "ENABLED"}, {"address": "1.1.1.5", "port": 81, "condition": "ENABLED"}] eff = add_clb_nodes(lb_id=self.lb_id, nodes=nodes) expected = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'POST', 'loadbalancers/{0}/nodes'.format(self.lb_id), data={'nodes': nodes}, success_pred=has_code(202)) # success seq = [ (expected.intent, lambda i: stub_json_response({}, 202, {})), (log_intent('request-add-clb-nodes', {}), lambda _: None)] self.assertEqual(perform_sequence(seq, eff), (StubResponse(202, {}), {})) # CLBDuplicateNodesError failure msg = ("Duplicate nodes detected. One or more nodes already " "configured on load balancer.") duplicate_nodes = stub_pure_response( json.dumps({'message': msg, 'code': 422}), 422) dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(duplicate_nodes))]) with self.assertRaises(CLBDuplicateNodesError) as cm: sync_perform(dispatcher, eff) self.assertEqual( cm.exception, CLBDuplicateNodesError(msg, lb_id=six.text_type(self.lb_id))) # CLBNodeLimitError failure msg = "Nodes must not exceed 25 per load balancer." limit = stub_pure_response( json.dumps({'message': msg, 'code': 413}), 413) dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(limit))]) with self.assertRaises(CLBNodeLimitError) as cm: sync_perform(dispatcher, eff) self.assertEqual( cm.exception, CLBNodeLimitError(msg, lb_id=six.text_type(self.lb_id), node_limit=25)) # all the common failures assert_parses_common_clb_errors(self, expected.intent, eff, "123456")
def expected_node_removal_req(self, nodes=(1, 2)): """ :return: Expected effect for a node removal request. """ return service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'DELETE', 'loadbalancers/{}/nodes'.format(self.lb_id), params={'id': map(str, nodes)}, success_pred=has_code(202))
def create_server(server_args): """ Create a server using Nova. :ivar dict server_args: The dictionary to pass to Nova specifying how the server should be built. Succeed on 202, and only reauthenticate on 401 because 403s may be terminal errors. :return: a `tuple` of (:obj:`twisted.web.client.Response`, JSON `dict`) :raise: :class:`CreateServerConfigurationError`, :class:`CreateServerOverQuoteError`, :class:`NovaRateLimitError`, :class:`NovaComputFaultError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_SERVERS, 'POST', 'servers', data=server_args, success_pred=has_code(202), reauth_codes=(401,)) @only_json_api_errors def _parse_known_json_errors(code, json_body): other_errors = [ (400, ('badRequest', 'message'), None, CreateServerConfigurationError), (403, ('forbidden', 'message'), _NOVA_403_QUOTA, CreateServerOverQuoteError) ] match_errors(_nova_standard_errors + other_errors, code, json_body) def _parse_known_string_errors(api_error_exc_info): api_error = api_error_exc_info[1] if api_error.code == 403: for pat in (_NOVA_403_RACKCONNECT_NETWORK_REQUIRED, _NOVA_403_NO_PUBLIC_NETWORK, _NOVA_403_PUBLIC_SERVICENET_BOTH_REQUIRED): m = pat.match(api_error.body) if m: raise CreateServerConfigurationError(m.groups()[0]) six.reraise(*api_error_exc_info) def _remove_admin_pass_for_logging(response): return {'server': { k: v for k, v in response['server'].items() if k != "adminPass" }} return (eff .on(error=catch(APIError, _parse_known_string_errors)) .on(error=_parse_known_json_errors) .on(log_success_response('request-create-server', _remove_admin_pass_for_logging)))
def create_server(server_args): """ Create a server using Nova. :ivar dict server_args: The dictionary to pass to Nova specifying how the server should be built. Succeed on 202, and only reauthenticate on 401 because 403s may be terminal errors. :return: a `tuple` of (:obj:`twisted.web.client.Response`, JSON `dict`) :raise: :class:`CreateServerConfigurationError`, :class:`CreateServerOverQuoteError`, :class:`NovaRateLimitError`, :class:`NovaComputFaultError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_SERVERS, 'POST', 'servers', data=server_args, success_pred=has_code(202), reauth_codes=(401,)) @_only_json_api_errors def _parse_known_json_errors(code, json_body): other_errors = [ (400, ('badRequest', 'message'), None, CreateServerConfigurationError), (403, ('forbidden', 'message'), _NOVA_403_QUOTA, CreateServerOverQuoteError) ] _match_errors(_nova_standard_errors + other_errors, code, json_body) def _parse_known_string_errors(api_error_exc_info): api_error = api_error_exc_info[1] if api_error.code == 403: for pat in (_NOVA_403_RACKCONNECT_NETWORK_REQUIRED, _NOVA_403_NO_PUBLIC_NETWORK, _NOVA_403_PUBLIC_SERVICENET_BOTH_REQUIRED): m = pat.match(api_error.body) if m: raise CreateServerConfigurationError(m.groups()[0]) six.reraise(*api_error_exc_info) def _remove_admin_pass_for_logging(response): return {'server': { k: v for k, v in response['server'].items() if k != "adminPass" }} return (eff .on(error=catch(APIError, _parse_known_string_errors)) .on(error=_parse_known_json_errors) .on(log_success_response('request-create-server', _remove_admin_pass_for_logging)))
def _check_stack_intent(self, stack_name, stack_id): return service_request(ServiceType.CLOUD_ORCHESTRATION, 'POST', 'stacks/{0}/{1}/actions'.format( stack_name, stack_id), data={ 'check': None }, success_pred=has_code(200, 201), reauth_codes=(401, ), json_response=False).intent
def test_change_clb_node(self): """ Produce a request for modifying a node on a load balancer, which returns a successful result on 202. Parse the common CLB errors, and :class:`NoSuchCLBNodeError`. """ eff = change_clb_node(lb_id=self.lb_id, node_id='1234', condition="DRAINING", weight=50, _type='SECONDARY') expected = service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', 'loadbalancers/{0}/nodes/1234'.format( self.lb_id), data={ 'node': { 'condition': 'DRAINING', 'weight': 50, 'type': 'SECONDARY' } }, success_pred=has_code(202)) # success dispatcher = EQFDispatcher([ (expected.intent, service_request_eqf(stub_pure_response('', 202))) ]) self.assertEqual(sync_perform(dispatcher, eff), stub_pure_response(None, 202)) # NoSuchCLBNode failure msg = "Node with id #1234 not found for loadbalancer #{0}".format( self.lb_id) no_such_node = stub_pure_response( json.dumps({ 'message': msg, 'code': 404 }), 404) dispatcher = EQFDispatcher([(expected.intent, service_request_eqf(no_such_node))]) with self.assertRaises(NoSuchCLBNodeError) as cm: sync_perform(dispatcher, eff) self.assertEqual( cm.exception, NoSuchCLBNodeError(msg, lb_id=six.text_type(self.lb_id), node_id=u'1234')) # all the common failures assert_parses_common_clb_errors(self, expected.intent, eff, "123456")
def _setup_for_get_server_details(self): """ Produce the data needed to test :obj:`get_server_details`: a tuple of (server_id, expected_effect, real_effect) """ server_id = unicode(uuid4()) real = get_server_details(server_id=server_id) expected = service_request(ServiceType.CLOUD_SERVERS, 'GET', 'servers/{0}'.format(server_id), success_pred=has_code(200)) return (server_id, expected, real)
def service_request( service_type, method, url, headers=None, data=None, params=None, log=None, reauth_codes=(401, 403), success_pred=has_code(200), json_response=True, ): """ Make an HTTP request to a Rackspace service, with a bunch of awesome behavior! :param otter.constants.ServiceType service_type: The service against which the request should be made. :param bytes method: HTTP method :param url: partial URL (appended to service endpoint) :param dict headers: base headers; will have auth headers added. :param data: JSON-able object or None. :param params: dict of query param ids to lists of values, or a list of tuples of query key to query value. :param log: log to send request info to. :param sequence success_pred: A predicate of responses which determines if a response indicates success or failure. :param sequence reauth_codes: HTTP codes upon which to invalidate the auth cache. :param bool json_response: Specifies whether the response should be parsed as JSON. :param bool parse_errors: Whether to parse :class:`APIError` :raise APIError: Raised asynchronously when the response HTTP code is not in success_codes. :return: Effect of :obj:`ServiceRequest`, resulting in a tuple of (:obj:`twisted.web.client.Response`, JSON-parsed HTTP response body). """ return Effect( ServiceRequest( service_type=service_type, method=method, url=url, headers=headers, data=data, params=params, log=log, reauth_codes=reauth_codes, success_pred=success_pred, json_response=json_response, ) )
def _setup_for_create_server(self): """ Produce the data needed to test :obj:`create_server`: a tuple of (expected_effect, real_effect) """ real = create_server({'server': 'args'}) expected = service_request(ServiceType.CLOUD_SERVERS, 'POST', 'servers', data={'server': 'args'}, reauth_codes=(401, ), success_pred=has_code(202)) return (expected, real)
def _setup_for_get_server_details(self): """ Produce the data needed to test :obj:`get_server_details`: a tuple of (server_id, expected_effect, real_effect) """ server_id = unicode(uuid4()) real = get_server_details(server_id=server_id) expected = service_request( ServiceType.CLOUD_SERVERS, 'GET', 'servers/{0}'.format(server_id), success_pred=has_code(200)) return (server_id, expected, real)
def _setup_for_create_server(self): """ Produce the data needed to test :obj:`create_server`: a tuple of (expected_effect, real_effect) """ real = create_server({'server': 'args'}) expected = service_request( ServiceType.CLOUD_SERVERS, 'POST', 'servers', data={'server': 'args'}, reauth_codes=(401,), success_pred=has_code(202)) return (expected, real)
def _fake_perform(self, dispatcher, effect): """ A test double for :func:`txeffect.perform`. :param dispatcher: The Effect dispatcher. :param effect: The effect to "execute". """ self.assertIdentical(dispatcher, self.dispatcher) self.assertIs(type(effect), Effect) tenant_scope = effect.intent self.assertEqual(tenant_scope.tenant_id, 'thetenantid') req = tenant_scope.effect.intent self.assertEqual(req.service_type, ServiceType.RACKCONNECT_V3) self.assertEqual(req.data, [{ 'load_balancer_pool': { 'id': 'lb_id' }, 'cloud_server': { 'id': 'server_id' } }]) self.assertEqual(req.url, 'load_balancer_pools/nodes') self.assertEqual(req.headers, None) # The method is either POST (add) or DELETE (remove). self.assertIn(req.method, ["POST", "DELETE"]) if req.method == "POST": self.assertEqual(req.success_pred, has_code(201)) # http://docs.rcv3.apiary.io/#post-%2Fv3%2F{tenant_id} # %2Fload_balancer_pools%2Fnodes return succeed(self.post_result) elif req.method == "DELETE": self.assertEqual(req.success_pred, has_code(204, 409)) # http://docs.rcv3.apiary.io/#delete-%2Fv3%2F{tenant_id} # %2Fload_balancer_pools%2Fnode return succeed(self.del_result)
def publish_to_cloudfeeds(event, log=None): """ Publish an event dictionary to cloudfeeds. """ return service_request( ServiceType.CLOUD_FEEDS, 'POST', append_segments('autoscale', 'events'), # note: if we actually wanted a JSON response instead of XML, # we'd have to pass the header: # 'accept': ['application/vnd.rackspace.atom+json'], headers={ 'content-type': ['application/vnd.rackspace.atom+json']}, data=event, log=log, success_pred=has_code(201), json_response=False)
def test_has_code(self): """ The predicate returns :data:`True` if the given response is in the successful code list, :data:`False` otherwise. """ pred = has_code(200, 204) def check_for_code(code): return pred(*stub_pure_response(None, code)) self.assertTrue(check_for_code(200)) self.assertTrue(check_for_code(204)) self.assertFalse(check_for_code(400)) self.assertFalse(check_for_code(500))
def remove_clb_nodes(lb_id, node_ids): """ Remove multiple nodes from a load balancer. :param str lb_id: A load balancer ID. :param node_ids: iterable of node IDs. :return: Effect of None. Succeeds on 202. This function will handle the case where *some* of the nodes are valid and some aren't, by retrying deleting only the valid ones. """ node_ids = list(node_ids) partial = None if len(node_ids) > CLB_BATCH_DELETE_LIMIT: not_removing = node_ids[CLB_BATCH_DELETE_LIMIT:] node_ids = node_ids[:CLB_BATCH_DELETE_LIMIT] partial = CLBPartialNodesRemoved(six.text_type(lb_id), map(six.text_type, not_removing), map(six.text_type, node_ids)) eff = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'DELETE', append_segments('loadbalancers', lb_id, 'nodes'), params={'id': map(str, node_ids)}, success_pred=has_code(202)) def check_invalid_nodes(exc_info): code = exc_info[1].code body = exc_info[1].body if code == 400: message = try_json_with_keys( body, ["validationErrors", "messages", 0]) if message is not None: match = _CLB_NODE_REMOVED_PATTERN.match(message) if match: removed = concat([group.split(',') for group in match.groups()]) return remove_clb_nodes(lb_id, set(node_ids) - set(removed)) six.reraise(*exc_info) return eff.on( error=catch(APIError, check_invalid_nodes) ).on( error=_only_json_api_errors( lambda c, b: _process_clb_api_error(c, b, lb_id)) ).on(success=lambda _: None if partial is None else raise_(partial))
def bulk_add(lb_node_pairs): """ Bulk add RCv3 LB Nodes. If RCv3 returns error about a pair being already a member, it retries the remaining pairs *provided* there are no other errors :param list lb_node_pairs: List of (lb_id, node_id) tuples :return: Effect of response body ``dict`` when succeeds with 201 or None when all pairs are already members. Otherwise raises `BulkErrors` or `UnknownBulkResponse` """ pairs = [(normalize_lb_id(l), n) for l, n in lb_node_pairs] eff = _rackconnect_bulk_request(pairs, "POST", success_pred=has_code(201, 409)) return eff.on(_check_bulk_add(pairs))
def _setup_for_set_nova_metadata_item(self): """ Produce the data needed to test :obj:`set_nova_metadata_item`: a tuple of (server_id, expected_effect, real_effect) """ server_id = unicode(uuid4()) real = set_nova_metadata_item(server_id=server_id, key='k', value='v') expected = service_request( ServiceType.CLOUD_SERVERS, 'PUT', 'servers/{0}/metadata/k'.format(server_id), data={'meta': {'k': 'v'}}, reauth_codes=(401,), success_pred=has_code(200)) return (server_id, expected, real)
def publish_autoscale_event(event, log=None): """ Publish event dictionary to autoscale feed """ return service_request( ServiceType.CLOUD_FEEDS, 'POST', append_segments('autoscale', 'events'), # note: if we actually wanted a JSON response instead of XML, # we'd have to pass the header: # 'accept': ['application/vnd.rackspace.atom+json'], headers={'content-type': ['application/vnd.rackspace.atom+json']}, data=event, log=log, success_pred=has_code(201), json_response=False)
def _setup_for_set_nova_metadata_item(self): """ Produce the data needed to test :obj:`set_nova_metadata_item`: a tuple of (server_id, expected_effect, real_effect) """ server_id = unicode(uuid4()) real = set_nova_metadata_item(server_id=server_id, key='k', value='v') expected = service_request(ServiceType.CLOUD_SERVERS, 'PUT', 'servers/{0}/metadata/k'.format(server_id), data={'meta': { 'k': 'v' }}, reauth_codes=(401, ), success_pred=has_code(200)) return (server_id, expected, real)
def test_defaults(self): """Default arguments are populated.""" eff = service_request(ServiceType.CLOUD_SERVERS, 'GET', 'foo') self.assertEqual( eff, Effect( ServiceRequest(service_type=ServiceType.CLOUD_SERVERS, method='GET', url='foo', headers=None, data=None, params=None, log=None, reauth_codes=(401, 403), success_pred=has_code(200), json_response=True)))
def service_request(service_type, method, url, headers=None, data=None, params=None, log=None, reauth_codes=(401, 403), success_pred=has_code(200), json_response=True): """ Make an HTTP request to a Rackspace service, with a bunch of awesome behavior! :param otter.constants.ServiceType service_type: The service against which the request should be made. :param bytes method: HTTP method :param url: partial URL (appended to service endpoint) :param dict headers: base headers; will have auth headers added. :param data: JSON-able object or None. :param params: dict of query param ids to lists of values, or a list of tuples of query key to query value. :param log: log to send request info to. :param sequence success_pred: A predicate of responses which determines if a response indicates success or failure. :param sequence reauth_codes: HTTP codes upon which to invalidate the auth cache. :param bool json_response: Specifies whether the response should be parsed as JSON. :param bool parse_errors: Whether to parse :class:`APIError` :raise APIError: Raised asynchronously when the response HTTP code is not in success_codes. :return: Effect of :obj:`ServiceRequest`, resulting in a JSON-parsed HTTP response body. """ return Effect( ServiceRequest(service_type=service_type, method=method, url=url, headers=headers, data=data, params=params, log=log, reauth_codes=reauth_codes, success_pred=success_pred, json_response=json_response))
def list_stacks_all(parameters=None): """ List Heat stacks. :param dict parameters: Query parameters to include. :return: List of stack details JSON. """ eff = service_request( ServiceType.CLOUD_ORCHESTRATION, 'GET', 'stacks', success_pred=has_code(200), reauth_codes=(401,), params=parameters) return (eff.on(log_success_response('request-list-stacks-all', identity)) .on(lambda (response, body): body['stacks']))
def remove_clb_nodes(lb_id, node_ids): """ Remove multiple nodes from a load balancer. :param str lb_id: A load balancer ID. :param node_ids: iterable of node IDs. :return: Effect of None. Succeeds on 202. This function will handle the case where *some* of the nodes are valid and some aren't, by retrying deleting only the valid ones. """ node_ids = list(node_ids) partial = None if len(node_ids) > CLB_BATCH_DELETE_LIMIT: not_removing = node_ids[CLB_BATCH_DELETE_LIMIT:] node_ids = node_ids[:CLB_BATCH_DELETE_LIMIT] partial = CLBPartialNodesRemoved(six.text_type(lb_id), map(six.text_type, not_removing), map(six.text_type, node_ids)) eff = service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'DELETE', append_segments('loadbalancers', lb_id, 'nodes'), params={'id': map(str, node_ids)}, success_pred=has_code(202)) def check_invalid_nodes(exc_info): code = exc_info[1].code body = exc_info[1].body if code == 400: message = try_json_with_keys(body, ["validationErrors", "messages", 0]) if message is not None: match = _CLB_NODE_REMOVED_PATTERN.match(message) if match: removed = concat( [group.split(',') for group in match.groups()]) return remove_clb_nodes(lb_id, set(node_ids) - set(removed)) six.reraise(*exc_info) return eff.on(error=catch(APIError, check_invalid_nodes)).on( error=only_json_api_errors( lambda c, b: _process_clb_api_error(c, b, lb_id))).on( success=lambda _: None if partial is None else raise_(partial))
def publish_autoscale_event(event, log=None): """ Publish event dictionary to autoscale feed """ return service_request( ServiceType.CLOUD_FEEDS, "POST", append_segments("autoscale", "events"), # note: if we actually wanted a JSON response instead of XML, # we'd have to pass the header: # 'accept': ['application/vnd.rackspace.atom+json'], headers={"content-type": ["application/vnd.rackspace.atom+json"]}, data=event, log=log, success_pred=has_code(201), json_response=False, )
def add_clb_nodes(lb_id, nodes): """ Generate effect to add one or more nodes to a load balancer. Note: This is not correctly documented in the load balancer documentation - it is documented as "Add Node" (singular), but the examples show multiple nodes being added. :param str lb_id: The load balancer ID to add the nodes to :param list nodes: A list of node dictionaries that each look like:: { "address": "valid ip address", "port": 80, "condition": "ENABLED", "weight": 1, "type": "PRIMARY" } (weight and type are optional) :return: :class:`ServiceRequest` effect :raises: :class:`CLBImmutableError`, :class:`CLBDeletedError`, :class:`NoSuchCLBError`, :class:`CLBDuplicateNodesError`, :class:`APIError` """ eff = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'POST', append_segments('loadbalancers', lb_id, 'nodes'), data={'nodes': nodes}, success_pred=has_code(202)) @_only_json_api_errors def _parse_known_errors(code, json_body): mappings = _expand_clb_matches( [(422, _CLB_DUPLICATE_NODES_PATTERN, CLBDuplicateNodesError)], lb_id) _match_errors(mappings, code, json_body) _process_clb_api_error(code, json_body, lb_id) process_nodelimit_error(code, json_body, lb_id) return eff.on(error=_parse_known_errors).on( log_success_response('request-add-clb-nodes', identity))
def create_stack(stack_args): """ Create a stack using Heat. :param dict stack_args: The dictionary to pass to Heat specifying how the stack should be built. :return: JSON `dict` """ eff = service_request( ServiceType.CLOUD_ORCHESTRATION, 'POST', 'stacks', data=stack_args, success_pred=has_code(201), reauth_codes=(401,)) return (eff.on(log_success_response('request-create-stack', identity)) .on(lambda (response, body): body['stack']))
def add_clb_nodes(lb_id, nodes): """ Generate effect to add one or more nodes to a load balancer. Note: This is not correctly documented in the load balancer documentation - it is documented as "Add Node" (singular), but the examples show multiple nodes being added. :param str lb_id: The load balancer ID to add the nodes to :param list nodes: A list of node dictionaries that each look like:: { "address": "valid ip address", "port": 80, "condition": "ENABLED", "weight": 1, "type": "PRIMARY" } (weight and type are optional) :return: :class:`ServiceRequest` effect :raises: :class:`CLBImmutableError`, :class:`CLBDeletedError`, :class:`NoSuchCLBError`, :class:`CLBDuplicateNodesError`, :class:`APIError` """ eff = service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'POST', append_segments('loadbalancers', lb_id, 'nodes'), data={'nodes': nodes}, success_pred=has_code(202)) @only_json_api_errors def _parse_known_errors(code, json_body): mappings = _expand_clb_matches( [(422, _CLB_DUPLICATE_NODES_PATTERN, CLBDuplicateNodesError)], lb_id) match_errors(mappings, code, json_body) _process_clb_api_error(code, json_body, lb_id) process_nodelimit_error(code, json_body, lb_id) return eff.on(error=_parse_known_errors).on( log_success_response('request-add-clb-nodes', identity))
def delete_stack(stack_name, stack_id): """ Delete a stack using Heat. :param string stack_name: The name of the stack. :param string stack_id: The id of the stack. :return: `None` """ eff = service_request( ServiceType.CLOUD_ORCHESTRATION, 'DELETE', append_segments('stacks', stack_name, stack_id), success_pred=has_code(204), reauth_codes=(401,), json_response=False) return (eff.on(log_success_response('request-delete-stack', identity, log_as_json=False)) .on(lambda _: None))
def test_change_clb_node(self): """ Produce a request for modifying a node on a load balancer, which returns a successful result on 202. Parse the common CLB errors, and :class:`NoSuchCLBNodeError`. """ eff = change_clb_node(lb_id=self.lb_id, node_id='1234', condition="DRAINING", weight=50, _type='SECONDARY') expected = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', 'loadbalancers/{0}/nodes/1234'.format(self.lb_id), data={'node': {'condition': 'DRAINING', 'weight': 50, 'type': 'SECONDARY'}}, success_pred=has_code(202)) # success dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(stub_pure_response('', 202)))]) self.assertEqual(sync_perform(dispatcher, eff), stub_pure_response(None, 202)) # NoSuchCLBNode failure msg = "Node with id #1234 not found for loadbalancer #{0}".format( self.lb_id) no_such_node = stub_pure_response( json.dumps({'message': msg, 'code': 404}), 404) dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(no_such_node))]) with self.assertRaises(NoSuchCLBNodeError) as cm: sync_perform(dispatcher, eff) self.assertEqual( cm.exception, NoSuchCLBNodeError(msg, lb_id=six.text_type(self.lb_id), node_id=u'1234')) # all the common failures assert_parses_common_clb_errors(self, expected.intent, eff, "123456")
def bulk_delete(lb_node_pairs): """ Bulk delete RCv3 LB Nodes. If RCv3 returns error about a pair not being a member or server or lb not existing it retries the remaining pairs *provided* there are no LBInactive errors. Otherwise `BulkErrors` with `LBInactive` errors in it is raised TODO: Ideally its outside the scope of this function to decide whether to retry on LB and Server does not exist error. There should be a parameter for this: lb_deleted_ok, server_deleted_ok? :param list lb_node_pairs: List of (lb_id, node_id) tuples :return: Effect of response body dict when succeeds or Effect of None if all nodes are already deleted. Otherwise raises `BulkErrors` or `UnknownBulkResponse` """ pairs = [(normalize_lb_id(l), n) for l, n in lb_node_pairs] eff = _rackconnect_bulk_request(pairs, "DELETE", success_pred=has_code(204, 409)) return eff.on(_check_bulk_delete(pairs))
def test_change_clb_node_default_type(self): """ Produce a request for modifying a node on a load balancer with the default type, which returns a successful result on 202. """ eff = change_clb_node(lb_id=self.lb_id, node_id='1234', condition="DRAINING", weight=50) expected = service_request( ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', 'loadbalancers/{0}/nodes/1234'.format(self.lb_id), data={'node': {'condition': 'DRAINING', 'weight': 50, 'type': 'PRIMARY'}}, success_pred=has_code(202)) dispatcher = EQFDispatcher([( expected.intent, service_request_eqf(stub_pure_response('', 202)))]) self.assertEqual(sync_perform(dispatcher, eff), stub_pure_response(None, 202))
def check_stack(stack_name, stack_id): """ Check a stack using Heat. :param string stack_name: The name of the stack. :param string stack_id: The id of the stack. :return: `None` """ eff = service_request( ServiceType.CLOUD_ORCHESTRATION, 'POST', append_segments('stacks', stack_name, stack_id, 'actions'), data={'check': None}, success_pred=has_code(200, 201), reauth_codes=(401,), json_response=False) return (eff.on(log_success_response('request-check-stack', identity, log_as_json=False)) .on(lambda _: None))
def test_defaults(self): """Default arguments are populated.""" eff = service_request(ServiceType.CLOUD_SERVERS, 'GET', 'foo') self.assertEqual( eff, Effect( ServiceRequest( service_type=ServiceType.CLOUD_SERVERS, method='GET', url='foo', headers=None, data=None, params=None, log=None, reauth_codes=(401, 403), success_pred=has_code(200), json_response=True ) ) )
def change_clb_node(lb_id, node_id, condition, weight, _type="PRIMARY"): """ Generate effect to change a node on a load balancer. :param str lb_id: The load balancer ID to add the nodes to :param str node_id: The node id to change. :param str condition: The condition to change to: one of "ENABLED", "DRAINING", or "DISABLED" :param int weight: The weight to change to. :param str _type: The type to change the CLB node to. :return: :class:`ServiceRequest` effect :raises: :class:`CLBImmutableError`, :class:`CLBDeletedError`, :class:`NoSuchCLBError`, :class:`NoSuchCLBNodeError`, :class:`APIError` """ eff = service_request(ServiceType.CLOUD_LOAD_BALANCERS, 'PUT', append_segments('loadbalancers', lb_id, 'nodes', node_id), data={ 'node': { 'condition': condition, 'weight': weight, 'type': _type } }, success_pred=has_code(202)) @only_json_api_errors def _parse_known_errors(code, json_body): _process_clb_api_error(code, json_body, lb_id) match_errors( _expand_clb_matches( [(404, _CLB_NO_SUCH_NODE_PATTERN, NoSuchCLBNodeError)], lb_id=lb_id, node_id=node_id), code, json_body) return eff.on(error=_parse_known_errors)
def update_stack(stack_name, stack_id, stack_args): """ Update a stack using Heat. :param string stack_name: The name of the stack. :param string stack_id: The id of the stack. :param dict stack_args: The dictionary to pass to Heat specifying how the stack should be updated. :return: `None` """ eff = service_request( ServiceType.CLOUD_ORCHESTRATION, 'PUT', append_segments('stacks', stack_name, stack_id), data=stack_args, success_pred=has_code(202), reauth_codes=(401,), json_response=False) return (eff.on(log_success_response('request-update-stack', identity, log_as_json=False)) .on(lambda _: None))
def set_nova_metadata_item(server_id, key, value): """ Set metadata key/value item on the given server. :ivar str server_id: a Nova server ID. :ivar str key: The metadata key to set (<=256 characters) :ivar str value: The value to assign to the metadata key (<=256 characters) Succeed on 200. :return: a `tuple` of (:obj:`twisted.web.client.Response`, JSON `dict`) :raise: :class:`NoSuchServer`, :class:`MetadataOverLimit`, :class:`NovaRateLimitError`, :class:`NovaComputeFaultError`, :class:`APIError` """ eff = service_request(ServiceType.CLOUD_SERVERS, 'PUT', append_segments('servers', server_id, 'metadata', key), data={'meta': { key: value }}, reauth_codes=(401, ), success_pred=has_code(200)) @_only_json_api_errors def _parse_known_errors(code, json_body): other_errors = [ (404, ('itemNotFound', 'message'), None, partial(NoSuchServerError, server_id=six.text_type(server_id))), (403, ('forbidden', 'message'), _MAX_METADATA_PATTERN, partial(ServerMetadataOverLimitError, server_id=six.text_type(server_id))), ] _match_errors(_nova_standard_errors + other_errors, code, json_body) return eff.on(error=_parse_known_errors).on( log_success_response('request-set-metadata-item', identity))