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 _: []))
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))
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 _: []))
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)
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 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))
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))
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))
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 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))
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 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) )
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)
def gone(r): return catch(CLBNotFoundError, lambda exc: r)