Exemple #1
0
def get_rcv3_contents():
    """
    Get Rackspace Cloud Load Balancer contents as list of `RCv3Node`.
    """
    eff = service_request(ServiceType.RACKCONNECT_V3, 'GET',
                          'load_balancer_pools')

    def on_listing_pools(lblist_result):
        _, body = lblist_result
        return parallel([
            service_request(
                ServiceType.RACKCONNECT_V3, 'GET',
                append_segments('load_balancer_pools', lb_pool['id'],
                                'nodes')).on(
                                    partial(
                                        on_listing_nodes,
                                        RCv3Description(lb_id=lb_pool['id'])))
            for lb_pool in body
        ])

    def on_listing_nodes(rcv3_description, lbnodes_result):
        _, body = lbnodes_result
        return [
            RCv3Node(node_id=node['id'],
                     description=rcv3_description,
                     cloud_server_id=get_in(('cloud_server', 'id'), node))
            for node in body
        ]

    return eff.on(on_listing_pools).on(success=compose(list, concat),
                                       error=catch(NoSuchEndpoint,
                                                   lambda _: []))
Exemple #2
0
def delete_and_verify(server_id):
    """
    Check the status of the server to see if it's actually been deleted.
    Succeeds only if it has been either deleted (404) or acknowledged by Nova
    to be deleted (task_state = "deleted").

    Note that ``task_state`` is in the server details key
    ``OS-EXT-STS:task_state``, which is supported by Openstack but available
    only when looking at the extended status of a server.
    """

    def check_task_state((resp, server_blob)):
        if resp.code == 404:
            return
        server_details = server_blob['server']
        is_deleting = server_details.get("OS-EXT-STS:task_state", "")
        if is_deleting.strip().lower() != "deleting":
            raise UnexpectedServerStatus(server_id, is_deleting, "deleting")

    def verify((_type, error, traceback)):
        if error.code != 204:
            raise _type, error, traceback
        ver_eff = service_request(
            ServiceType.CLOUD_SERVERS, 'GET',
            append_segments('servers', server_id),
            success_pred=has_code(200, 404))
        return ver_eff.on(check_task_state)

    return service_request(
        ServiceType.CLOUD_SERVERS, 'DELETE',
        append_segments('servers', server_id),
        success_pred=has_code(404)).on(error=catch(APIError, verify))
Exemple #3
0
def get_rcv3_contents():
    """
    Get Rackspace Cloud Load Balancer contents as list of `RCv3Node`.
    """
    eff = service_request(ServiceType.RACKCONNECT_V3, 'GET',
                          'load_balancer_pools')

    def on_listing_pools(lblist_result):
        _, body = lblist_result
        return parallel([
            service_request(ServiceType.RACKCONNECT_V3, 'GET',
                            append_segments('load_balancer_pools',
                                            lb_pool['id'], 'nodes')).on(
                partial(on_listing_nodes,
                        RCv3Description(lb_id=lb_pool['id'])))
            for lb_pool in body
        ])

    def on_listing_nodes(rcv3_description, lbnodes_result):
        _, body = lbnodes_result
        return [
            RCv3Node(node_id=node['id'], description=rcv3_description,
                     cloud_server_id=get_in(('cloud_server', 'id'), node))
            for node in body
        ]

    return eff.on(on_listing_pools).on(
        success=compose(list, concat),
        error=catch(NoSuchEndpoint, lambda _: []))
Exemple #4
0
def delete_and_verify(server_id):
    """
    Check the status of the server to see if it's actually been deleted.
    Succeeds only if it has been either deleted (404) or acknowledged by Nova
    to be deleted (task_state = "deleted").

    Note that ``task_state`` is in the server details key
    ``OS-EXT-STS:task_state``, which is supported by Openstack but available
    only when looking at the extended status of a server.
    """

    def check_task_state((resp, server_blob)):
        if resp.code == 404:
            return
        server_details = server_blob['server']
        is_deleting = server_details.get("OS-EXT-STS:task_state", "")
        if is_deleting.strip().lower() != "deleting":
            raise UnexpectedServerStatus(server_id, is_deleting, "deleting")

    def verify((_type, error, traceback)):
        if error.code != 204:
            raise _type, error, traceback
        ver_eff = service_request(
            ServiceType.CLOUD_SERVERS, 'GET',
            append_segments('servers', server_id),
            success_pred=has_code(200, 404))
        return ver_eff.on(check_task_state)

    return service_request(
        ServiceType.CLOUD_SERVERS, 'DELETE',
        append_segments('servers', server_id),
        success_pred=has_code(404)).on(error=catch(APIError, verify))
Exemple #5
0
def _only_json_api_errors(f):
    """
    Helper function so that we only catch APIErrors with bodies that can be
    parsed into JSON.

    Should decorate a function that expects two parameters: http status code
    and JSON body.

    If the decorated function cannot parse the error (either because it's not
    JSON or not recognized), reraise the error.
    """

    @wraps(f)
    def try_parsing(api_error_exc_info):
        api_error = api_error_exc_info[1]
        try:
            body = json.loads(api_error.body)
        except (ValueError, TypeError):
            pass
        else:
            f(api_error.code, body)

        six.reraise(*api_error_exc_info)

    return catch(APIError, try_parsing)
Exemple #6
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)))
Exemple #7
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)))
Exemple #8
0
 def as_effect(self):
     """
     Produce a :obj:`Effect` to add some nodes to some RCv3 load
     balancers.
     """
     eff = rcv3.bulk_add(self.lb_node_pairs)
     return eff.on(
         success=lambda _: (StepResult.RETRY, [ErrorReason.String(
             'must re-gather after LB add in order to update the '
             'active cache')]),
         error=catch(rcv3.BulkErrors, _handle_bulk_add_errors))
Exemple #9
0
 def as_effect(self):
     """
     Produce a :obj:`Effect` to add some nodes to some RCv3 load
     balancers.
     """
     eff = rcv3.bulk_add(self.lb_node_pairs)
     return eff.on(success=lambda _: (StepResult.RETRY, [
         ErrorReason.String(
             'must re-gather after LB add in order to update the '
             'active cache')
     ]),
                   error=catch(rcv3.BulkErrors, _handle_bulk_add_errors))
Exemple #10
0
    def release_eff(self):
        """
        Effect implementation of ``release``.

        :return: ``Effect`` of ``None``
        """
        def reset_node(_):
            self._node = None

        if self._node is not None:
            return Effect(DeleteNode(path=self._node, version=-1)).on(
                success=reset_node, error=catch(NoNodeError, reset_node))
        else:
            return Effect(Constant(None))
Exemple #11
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))
Exemple #12
0
    def release_eff(self):
        """
        Effect implementation of ``release``.

        :return: ``Effect`` of ``None``
        """

        def reset_node(_):
            self._node = None

        if self._node is not None:
            return Effect(DeleteNode(path=self._node, version=-1)).on(
                success=reset_node, error=catch(NoNodeError, reset_node)
            )
        else:
            return Effect(Constant(None))
Exemple #13
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))
Exemple #14
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 = map(str, node_ids)
    eff = service_request(
        ServiceType.CLOUD_LOAD_BALANCERS,
        "DELETE",
        append_segments("loadbalancers", lb_id, "nodes"),
        params={"id": 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)
    )
Exemple #15
0
def only_json_api_errors(f):
    """
    Helper function so that we only catch APIErrors with bodies that can be
    parsed into JSON.

    Should decorate a function that expects two parameters: http status code
    and JSON body.

    If the decorated function cannot parse the error (either because it's not
    JSON or not recognized), reraise the error.
    """
    @wraps(f)
    def try_parsing(api_error_exc_info):
        api_error = api_error_exc_info[1]
        try:
            body = json.loads(api_error.body)
        except (ValueError, TypeError):
            pass
        else:
            f(api_error.code, body)

        six.reraise(*api_error_exc_info)

    return catch(APIError, try_parsing)
Exemple #16
0
 def gone(r):
     return catch(CLBNotFoundError, lambda exc: r)
Exemple #17
0
 def gone(r):
     return catch(CLBNotFoundError, lambda exc: r)