Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
 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
Exemplo n.º 3
0
 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
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
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))
Exemplo n.º 6
0
 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
Exemplo n.º 7
0
 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
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
    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))
Exemplo n.º 10
0
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))
Exemplo n.º 11
0
 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
Exemplo n.º 12
0
 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)
Exemplo n.º 13
0
 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
Exemplo n.º 14
0
 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
Exemplo n.º 15
0
 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)
Exemplo n.º 16
0
 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))
Exemplo n.º 17
0
    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")
Exemplo n.º 18
0
 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))
Exemplo n.º 19
0
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)))
Exemplo n.º 20
0
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)))
Exemplo n.º 21
0
 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
Exemplo n.º 22
0
    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")
Exemplo n.º 23
0
 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)
Exemplo n.º 24
0
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,
        )
    )
Exemplo n.º 25
0
 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)
Exemplo n.º 26
0
 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)
Exemplo n.º 27
0
 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)
Exemplo n.º 28
0
    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)
Exemplo n.º 29
0
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)
Exemplo n.º 30
0
    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))
Exemplo n.º 31
0
    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))
Exemplo n.º 32
0
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))
Exemplo n.º 33
0
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))
Exemplo n.º 34
0
 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)
Exemplo n.º 35
0
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)
Exemplo n.º 36
0
 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)
Exemplo n.º 37
0
 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)))
Exemplo n.º 38
0
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))
Exemplo n.º 39
0
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']))
Exemplo n.º 40
0
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))
Exemplo n.º 41
0
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,
    )
Exemplo n.º 42
0
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']))
Exemplo n.º 43
0
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))
Exemplo n.º 44
0
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']))
Exemplo n.º 45
0
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']))
Exemplo n.º 46
0
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))
Exemplo n.º 47
0
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))
Exemplo n.º 48
0
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))
Exemplo n.º 49
0
    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")
Exemplo n.º 50
0
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))
Exemplo n.º 51
0
    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))
Exemplo n.º 52
0
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))
Exemplo n.º 53
0
 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
             )
         )
     )
Exemplo n.º 54
0
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))
Exemplo n.º 55
0
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)
Exemplo n.º 56
0
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))
Exemplo n.º 57
0
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))