def _check_plan_id(broker: ServiceBroker, plan_id) -> bool: """ Checks that the plan_id exists in the catalog :return: boolean """ for service in ensure_list(broker.catalog()): for plan in service.plans: if plan.id == plan_id: return True return False
def catalog(self) -> List[Service]: services = [] for broker in self._service_brokers: services.extend(ensure_list(broker.catalog())) return services
def _service_ids(broker: ServiceBroker) -> Set[str]: return {service.id for service in ensure_list(broker.catalog())}
def get_blueprint(service_brokers: Union[List[ServiceBroker], ServiceBroker], broker_credentials: Union[None, List[BrokerCredentials], BrokerCredentials], logger: logging.Logger) -> Blueprint: """ Returns the blueprint with service broker api. :param service_brokers: Services that this broker exposes :param broker_credentials: Optional Usernames and passwords that will be required to communicate with service broker :param logger: Used for api logs. This will not influence Flasks logging behavior. :return: Blueprint to register with Flask app instance """ openbroker = Blueprint('open_broker', __name__) service_brokers = ensure_list(service_brokers) # Apply filters logger.debug("Apply print_request filter for debugging") openbroker.before_request(print_request) if DISABLE_VERSION_CHECK: logger.warning( "Minimum API version is not checked, this can cause illegal contracts between service broker and platform!" ) else: logger.debug("Apply check_version filter for version %s" % str(MIN_VERSION)) openbroker.before_request(check_version) logger.debug("Apply check_originating_identity filter") openbroker.before_request(check_originating_identity) if broker_credentials is not None: broker_credentials = ensure_list(broker_credentials) logger.debug("Apply check_auth filter with {} credentials".format( len(broker_credentials))) openbroker.before_request(get_auth_filter(broker_credentials)) def get_broker_by_id(service_id: str): for service in service_brokers: if service.service_id() == service_id: return service raise KeyError('Service {} not found'.format(service_id)) def add_service_id_to_async_response(response, service_id: str): if response.is_async: if response.operation is None: response.operation = service_id else: response.operation = ' '.join((service_id, response.operation)) @openbroker.errorhandler(Exception) def error_handler(e): logger.exception(e) return to_json_response(ErrorResponse( description=str(e))), HTTPStatus.INTERNAL_SERVER_ERROR @openbroker.route("/v2/catalog", methods=['GET']) def catalog(): """ :return: Catalog of broker (List of services) """ return to_json_response( CatalogResponse(list(s.catalog() for s in service_brokers))) @openbroker.route("/v2/service_instances/<instance_id>", methods=['PUT']) @requires_application_json def provision(instance_id): try: accepts_incomplete = 'true' == request.args.get( "accepts_incomplete", 'false') provision_details = ProvisionDetails(**json.loads(request.data)) provision_details.originating_identity = request.originating_identity provision_details.authorization_username = request.authorization.username broker = get_broker_by_id(provision_details.service_id) if not broker.check_plan_id(provision_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response( ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = broker.provision(instance_id, provision_details, accepts_incomplete) add_service_id_to_async_response(result, broker.service_id()) except errors.ErrInstanceAlreadyExists as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.CONFLICT except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response( ErrorResponse( error="AsyncRequired", description= "This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.state == ProvisionState.IS_ASYNC: return to_json_response( ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.ACCEPTED elif result.state == ProvisionState.IDENTICAL_ALREADY_EXISTS: return to_json_response( ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.OK elif result.state == ProvisionState.SUCCESSFUL_CREATED: return to_json_response( ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.CREATED else: raise errors.ServiceException( 'IllegalState, ProvisioningState unknown.') @openbroker.route("/v2/service_instances/<instance_id>", methods=['PATCH']) @requires_application_json def update(instance_id): try: accepts_incomplete = 'true' == request.args.get( "accepts_incomplete", 'false') update_details = UpdateDetails(**json.loads(request.data)) update_details.originating_identity = request.originating_identity update_details.authorization_username = request.authorization.username broker = get_broker_by_id(update_details.service_id) if not broker.check_plan_id(update_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response( ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = broker.update(instance_id, update_details, accepts_incomplete) add_service_id_to_async_response(result, broker.service_id()) except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response( ErrorResponse( error="AsyncRequired", description= "This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.is_async: return to_json_response(UpdateResponse( result.operation)), HTTPStatus.ACCEPTED else: return to_json_response(EmptyResponse()), HTTPStatus.OK @openbroker.route( "/v2/service_instances/<instance_id>/service_bindings/<binding_id>", methods=['PUT']) @requires_application_json def bind(instance_id, binding_id): try: binding_details = BindDetails(**json.loads(request.data)) binding_details.originating_identity = request.originating_identity binding_details.authorization_username = request.authorization.username broker = get_broker_by_id(binding_details.service_id) if not broker.check_plan_id(binding_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response( ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = broker.bind(instance_id, binding_id, binding_details) except errors.ErrBindingAlreadyExists as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.CONFLICT except errors.ErrAppGuidNotProvided as e: logger.exception(e) return to_json_response( ErrorResponse( error="RequiresApp", description= "This service supports generation of credentials through binding an application only." )), HTTPStatus.UNPROCESSABLE_ENTITY response = BindResponse(credentials=result.credentials, syslog_drain_url=result.syslog_drain_url, route_service_url=result.route_service_url, volume_mounts=result.volume_mounts) if result.state == BindState.SUCCESSFUL_BOUND: return to_json_response(response), HTTPStatus.CREATED elif result.state == BindState.IDENTICAL_ALREADY_EXISTS: return to_json_response(response), HTTPStatus.OK else: raise errors.ServiceException('IllegalState, BindState unknown.') @openbroker.route( "/v2/service_instances/<instance_id>/service_bindings/<binding_id>", methods=['DELETE']) def unbind(instance_id, binding_id): try: plan_id = request.args["plan_id"] service_id = request.args["service_id"] unbind_details = UnbindDetails(plan_id, service_id) unbind_details.originating_identity = request.originating_identity unbind_details.authorization_username = request.authorization.username broker = get_broker_by_id(unbind_details.service_id) if not broker.check_plan_id(unbind_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError) as e: logger.exception(e) return to_json_response( ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: broker.unbind(instance_id, binding_id, unbind_details) except errors.ErrBindingDoesNotExist as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.GONE return to_json_response(EmptyResponse()), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>", methods=['DELETE']) def deprovision(instance_id): try: plan_id = request.args["plan_id"] service_id = request.args["service_id"] accepts_incomplete = 'true' == request.args.get( "accepts_incomplete", 'false') deprovision_details = DeprovisionDetails(plan_id, service_id) deprovision_details.originating_identity = request.originating_identity deprovision_details.authorization_username = request.authorization.username broker = get_broker_by_id(deprovision_details.service_id) if not broker.check_plan_id(deprovision_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError) as e: logger.exception(e) return to_json_response( ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = broker.deprovision(instance_id, deprovision_details, accepts_incomplete) add_service_id_to_async_response(result, broker.service_id()) except errors.ErrInstanceDoesNotExist as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.GONE except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response( ErrorResponse( error="AsyncRequired", description= "This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.is_async: return to_json_response(DeprovisionResponse( result.operation)), HTTPStatus.ACCEPTED else: return to_json_response(EmptyResponse()), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>/last_operation", methods=['GET']) def last_operation(instance_id): # Not required # service_id = request.args.get("service_id", None) # plan_id = request.args.get("plan_id", None) operation_data = request.args.get("operation", None) data = operation_data.split(' ', maxsplit=1) service_id = data[0] if len(data) == 2: operation_data = data[1] else: operation_data = None broker = get_broker_by_id(service_id) result = broker.last_operation(instance_id, operation_data) return to_json_response( LastOperationResponse(result.state, result.description)), HTTPStatus.OK return openbroker
def get_blueprint(service_broker: ServiceBroker, broker_credentials: Union[None, List[BrokerCredentials], BrokerCredentials], logger: logging.Logger) -> Blueprint: """ Returns the blueprint with service broker api. :param service_broker: Services that this broker exposes :param broker_credentials: Optional Usernames and passwords that will be required to communicate with service broker :param logger: Used for api logs. This will not influence Flasks logging behavior. :return: Blueprint to register with Flask app instance """ openbroker = Blueprint('open_broker', __name__) # Apply filters logger.debug("Apply print_request filter for debugging") openbroker.before_request(print_request) if DISABLE_VERSION_CHECK: logger.warning( "Minimum API version is not checked, this can cause illegal contracts between service broker and platform!" ) else: logger.debug("Apply check_version filter for version %s" % str(MIN_VERSION)) openbroker.before_request(check_version) logger.debug("Apply check_originating_identity filter") openbroker.before_request(check_originating_identity) if broker_credentials is not None: broker_credentials = ensure_list(broker_credentials) logger.debug("Apply check_auth filter with {} credentials".format(len(broker_credentials))) openbroker.before_request(get_auth_filter(broker_credentials)) def extract_authorization_username(request: Request): if request.authorization is not None: return request.authorization.username else: return None @openbroker.errorhandler(Exception) def error_handler(e): logger.exception(e) return to_json_response(ErrorResponse( description=constants.DEFAULT_EXCEPTION_ERROR_MESSAGE )), HTTPStatus.INTERNAL_SERVER_ERROR @openbroker.errorhandler(NotImplementedError) def error_handler_not_implemented(e): logger.exception(e) return to_json_response(ErrorResponse( description=constants.DEFAULT_NOT_IMPLEMENTED_ERROR_MESSAGE )), HTTPStatus.NOT_IMPLEMENTED @openbroker.errorhandler(errors.ErrBadRequest) def error_handler_bad_request(e): logger.exception(e) return to_json_response(ErrorResponse( description=constants.DEFAULT_BAD_REQUEST_ERROR_MESSAGE )), HTTPStatus.BAD_REQUEST @openbroker.route("/v2/catalog", methods=['GET']) def catalog(): """ :return: Catalog of broker (List of services) """ catalog = ensure_list(service_broker.catalog()) return to_json_response(CatalogResponse(list(catalog))) @openbroker.route("/v2/service_instances/<instance_id>", methods=['PUT']) @requires_application_json def provision(instance_id): try: accepts_incomplete = 'true' == request.args.get("accepts_incomplete", 'false') provision_details = ProvisionDetails(**json.loads(request.data)) provision_details.originating_identity = request.originating_identity provision_details.authorization_username = extract_authorization_username(request) if not _check_plan_id(service_broker, provision_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response(ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = service_broker.provision(instance_id, provision_details, accepts_incomplete) except errors.ErrInstanceAlreadyExists as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.CONFLICT except errors.ErrInvalidParameters as e: return to_json_response(ErrorResponse('InvalidParameters', str(e))), HTTPStatus.BAD_REQUEST except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response(ErrorResponse( error="AsyncRequired", description="This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.state == ProvisionState.IS_ASYNC: return to_json_response(ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.ACCEPTED elif result.state == ProvisionState.IDENTICAL_ALREADY_EXISTS: return to_json_response(ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.OK elif result.state == ProvisionState.SUCCESSFUL_CREATED: return to_json_response(ProvisioningResponse(result.dashboard_url, result.operation)), HTTPStatus.CREATED else: raise errors.ServiceException('IllegalState, ProvisioningState unknown.') @openbroker.route("/v2/service_instances/<instance_id>", methods=['PATCH']) @requires_application_json def update(instance_id): try: accepts_incomplete = 'true' == request.args.get("accepts_incomplete", 'false') update_details = UpdateDetails(**json.loads(request.data)) update_details.originating_identity = request.originating_identity update_details.authorization_username = extract_authorization_username(request) plan_id = update_details.plan_id if plan_id and not _check_plan_id(service_broker, plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response(ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = service_broker.update(instance_id, update_details, accepts_incomplete) except errors.ErrInvalidParameters as e: return to_json_response(ErrorResponse('InvalidParameters', str(e))), HTTPStatus.BAD_REQUEST except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response(ErrorResponse( error="AsyncRequired", description="This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.is_async: return to_json_response(UpdateResponse(result.operation, result.dashboard_url)), HTTPStatus.ACCEPTED else: return to_json_response(UpdateResponse(None, result.dashboard_url)), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>/service_bindings/<binding_id>", methods=['PUT']) @requires_application_json def bind(instance_id, binding_id): try: accepts_incomplete = 'true' == request.args.get("accepts_incomplete", 'false') binding_details = BindDetails(**json.loads(request.data)) binding_details.originating_identity = request.originating_identity binding_details.authorization_username = extract_authorization_username(request) if not _check_plan_id(service_broker, binding_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError, JSONDecodeError) as e: logger.exception(e) return to_json_response(ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = service_broker.bind(instance_id, binding_id, binding_details, accepts_incomplete) except errors.ErrBindingAlreadyExists as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.CONFLICT except errors.ErrAppGuidNotProvided as e: logger.exception(e) return to_json_response(ErrorResponse( error="RequiresApp", description="This service supports generation of credentials through binding an application only." )), HTTPStatus.UNPROCESSABLE_ENTITY response = BindResponse( credentials=result.credentials, syslog_drain_url=result.syslog_drain_url, route_service_url=result.route_service_url, volume_mounts=result.volume_mounts ) if result.state == BindState.SUCCESSFUL_BOUND: return to_json_response(response), HTTPStatus.CREATED elif result.state == BindState.IDENTICAL_ALREADY_EXISTS: return to_json_response(response), HTTPStatus.OK elif result.state == BindState.IS_ASYNC: return to_json_response(BindResponse(operation=result.operation)), HTTPStatus.ACCEPTED else: raise errors.ServiceException('IllegalState, BindState unknown.') @openbroker.route("/v2/service_instances/<instance_id>/service_bindings/<binding_id>", methods=['DELETE']) def unbind(instance_id, binding_id): try: accepts_incomplete = 'true' == request.args.get("accepts_incomplete", 'false') plan_id = request.args["plan_id"] service_id = request.args["service_id"] unbind_details = UnbindDetails(service_id=service_id, plan_id=plan_id) unbind_details.originating_identity = request.originating_identity unbind_details.authorization_username = extract_authorization_username(request) if not _check_plan_id(service_broker, unbind_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError) as e: logger.exception(e) return to_json_response(ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = service_broker.unbind(instance_id, binding_id, unbind_details, accepts_incomplete) except errors.ErrBindingDoesNotExist as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.GONE if result.is_async: return to_json_response(UnbindResponse(result.operation)), HTTPStatus.ACCEPTED else: return to_json_response(EmptyResponse()), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>", methods=['DELETE']) def deprovision(instance_id): try: plan_id = request.args["plan_id"] service_id = request.args["service_id"] accepts_incomplete = 'true' == request.args.get("accepts_incomplete", 'false') deprovision_details = DeprovisionDetails(service_id=service_id, plan_id=plan_id) deprovision_details.originating_identity = request.originating_identity deprovision_details.authorization_username = extract_authorization_username(request) if not _check_plan_id(service_broker, deprovision_details.plan_id): raise TypeError('plan_id not found in this service.') except (TypeError, KeyError) as e: logger.exception(e) return to_json_response(ErrorResponse(description=str(e))), HTTPStatus.BAD_REQUEST try: result = service_broker.deprovision(instance_id, deprovision_details, accepts_incomplete) except errors.ErrInstanceDoesNotExist as e: logger.exception(e) return to_json_response(EmptyResponse()), HTTPStatus.GONE except errors.ErrAsyncRequired as e: logger.exception(e) return to_json_response(ErrorResponse( error="AsyncRequired", description="This service plan requires client support for asynchronous service operations." )), HTTPStatus.UNPROCESSABLE_ENTITY if result.is_async: return to_json_response(DeprovisionResponse(result.operation)), HTTPStatus.ACCEPTED else: return to_json_response(EmptyResponse()), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>/last_operation", methods=['GET']) def last_operation(instance_id): # TODO: forward them # service_id = request.args.get("service_id", None) # plan_id = request.args.get("plan_id", None) operation_data = request.args.get("operation", None) try: result = service_broker.last_operation(instance_id, operation_data) return to_json_response(LastOperationResponse(result.state, result.description)), HTTPStatus.OK except errors.ErrInstanceDoesNotExist: return to_json_response(LastOperationResponse(OperationState.SUCCEEDED, '')), HTTPStatus.GONE @openbroker.route("/v2/service_instances/<instance_id>/service_bindings/<binding_id>/last_operation", methods=['GET']) def last_binding_operation(instance_id, binding_id): # TODO: forward them # service_id = request.args.get("service_id", None) # plan_id = request.args.get("plan_id", None) operation_data = request.args.get("operation", None) result = service_broker.last_binding_operation(instance_id, binding_id, operation_data) return to_json_response(LastOperationResponse(result.state, result.description)), HTTPStatus.OK @openbroker.route("/v2/service_instances/<instance_id>", methods=['GET']) def get_instance(instance_id): try: result = service_broker.get_instance(instance_id) response = GetInstanceResponse( service_id=result.service_id, plan_id=result.plan_id, dashboard_url=result.dashboard_url, parameters=result.parameters, ) return to_json_response(response), HTTPStatus.OK except errors.ErrInstanceDoesNotExist: return to_json_response(EmptyResponse()), HTTPStatus.NOT_FOUND except errors.ErrConcurrentInstanceAccess: error_response = ErrorResponse(error='ConcurrencyError', description='The Service Broker does not support concurrent requests that mutate the same resource.') return to_json_response(error_response), HTTPStatus.UNPROCESSABLE_ENTITY @openbroker.route("/v2/service_instances/<instance_id>/service_bindings/<binding_id>", methods=['GET']) def get_binding(instance_id, binding_id): try: result = service_broker.get_binding(instance_id, binding_id) response = GetBindingResponse( credentials=result.credentials, syslog_drain_url=result.syslog_drain_url, route_service_url=result.route_service_url, volume_mounts=result.volume_mounts, parameters=result.parameters, ) return to_json_response(response), HTTPStatus.OK except errors.ErrBindingDoesNotExist: return to_json_response(EmptyResponse()), HTTPStatus.NOT_FOUND return openbroker
def catalog(): """ :return: Catalog of broker (List of services) """ catalog = ensure_list(service_broker.catalog()) return to_json_response(CatalogResponse(list(catalog)))