def last_operation(self, instance_id: str, operation_data: Optional[str], **kwargs) -> LastOperation: """ Further readings `CF Broker API#LastOperation <https://docs.cloudfoundry.org/services/api.html#polling>`_ :param instance_id: Instance id provided by the platform :param operation_data: Operation data received from async operation :param kwargs: May contain additional information, improves compatibility with upstream versions :rtype: LastOperation """ instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrInstanceDoesNotExist if not operation_data: raise errors.ErrBadRequest(msg="Missing operation ID") operation = instance.operations.filter_by( id=int(operation_data)).first() if not operation: raise errors.ErrBadRequest( msg= f"Invalid operation id {operation_data} for service {instance_id}" ) return LastOperation( state=Operation.States(operation.state), description=operation.step_description, )
def deprovision( self, instance_id: str, details: DeprovisionDetails, async_allowed: bool, **kwargs, ) -> DeprovisionServiceSpec: if not async_allowed: raise errors.ErrAsyncRequired() instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrInstanceDoesNotExist operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.DEPROVISION.value, ) db.session.add(operation) db.session.commit() queue_all_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) return DeprovisionServiceSpec(is_async=True, operation=str(operation.id))
def provision(self, instance_id: str, details: ProvisionDetails, async_allowed: bool, **kwargs) -> ProvisionedServiceSpec: self.logger.info("starting provision request") if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} domain_names = parse_domain_options(params) if not domain_names: raise errors.ErrBadRequest("'domains' parameter required.") self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") if not config.IGNORE_DUPLICATE_DOMAINS: validators.UniqueDomains(domain_names).validate() if details.plan_id == CDN_PLAN_ID: instance = provision_cdn_instance(instance_id, domain_names, params) queue = queue_all_cdn_provision_tasks_for_operation elif details.plan_id == ALB_PLAN_ID: instance = ALBServiceInstance(id=instance_id, domain_names=domain_names) queue = queue_all_alb_provision_tasks_for_operation elif details.plan_id == MIGRATION_PLAN_ID: instance = MigrationServiceInstance(id=instance_id, domain_names=domain_names) db.session.add(instance) db.session.commit() return ProvisionedServiceSpec( state=ProvisionState.SUCCESSFUL_CREATED) else: raise NotImplementedError() self.logger.info("setting origin hostname") self.logger.info("creating operation") operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.PROVISION.value, step_description="Queuing tasks", ) db.session.add(instance) db.session.add(operation) self.logger.info("committing db session") db.session.commit() self.logger.info("queueing tasks") queue(operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) self.logger.info("all done. Returning provisioned service spec") return ProvisionedServiceSpec(state=ProvisionState.IS_ASYNC, operation=str(operation.id))
def provision(self, instance_id: str, details: ProvisionDetails, async_allowed: bool, **kwargs) -> ProvisionedServiceSpec: self.logger.info("starting provision request") if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} if params.get("domains"): domain_names = [ d.strip().lower() for d in params["domains"].split(",") ] else: raise errors.ErrBadRequest("'domains' parameter required.") self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") validators.UniqueDomains(domain_names).validate() instance = ServiceInstance(id=instance_id, domain_names=domain_names) self.logger.info("setting origin hostname") instance.cloudfront_origin_hostname = params.get( "origin", config.DEFAULT_CLOUDFRONT_ORIGIN) instance.cloudfront_origin_path = params.get("path", "") self.logger.info("creating operation") operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.PROVISION.value, ) db.session.add(instance) db.session.add(operation) self.logger.info("committing db session") db.session.commit() self.logger.info("queueing tasks") queue_all_provision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) self.logger.info("all done. Returning provisioned service spec") return ProvisionedServiceSpec(state=ProvisionState.IS_ASYNC, operation=str(operation.id))
def deprovision( self, instance_id: str, details: DeprovisionDetails, async_allowed: bool, **kwargs, ) -> DeprovisionServiceSpec: if not async_allowed: raise errors.ErrAsyncRequired() instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrInstanceDoesNotExist operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.DEPROVISION.value, step_description="Queuing tasks", ) db.session.add(operation) db.session.commit() if details.plan_id == CDN_PLAN_ID: queue_all_cdn_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) elif details.plan_id == ALB_PLAN_ID: queue_all_alb_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) elif details.plan_id == MIGRATION_PLAN_ID: for o in instance.operations: if o.action == Operation.Actions.UPDATE.value: raise errors.ErrBadRequest( msg="Can't delete migration with update operations") queue_all_migration_deprovision_tasks_for_operation( operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) else: raise NotImplementedError() return DeprovisionServiceSpec(is_async=True, operation=str(operation.id))
def scan_for_expiring_certs(): with huey.huey.flask_app.app_context(): logger.info("Scanning for expired certificates") # TODO: skip SIs with active operations certificates = Certificate.query.filter( Certificate.expires_at - datetime.timedelta(days=30) < datetime.datetime.now() ).all() instances = [ c.service_instance for c in certificates if not c.service_instance.deactivated_at and not c.service_instance.has_active_operations() ] cdn_renewals = [] alb_renewals = [] for instance in instances: if instance.has_active_operations(): continue logger.info("Instance %s needs renewal", instance.id) renewal = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.RENEW.value, step_description="Queuing tasks", ) db.session.add(renewal) if instance.instance_type == "cdn_service_instance": cdn_renewals.append(renewal) else: alb_renewals.append(renewal) db.session.commit() for renewal in cdn_renewals: queue_all_cdn_renewal_tasks_for_operation(renewal.id) for renewal in alb_renewals: queue_all_alb_renewal_tasks_for_operation(renewal.id) renew_instances = cdn_renewals + alb_renewals # n.b. this return is only for testing - huey ignores it. return [instance.service_instance_id for instance in renew_instances]
def update(self, instance_id: str, details: UpdateDetails, async_allowed: bool, **kwargs) -> UpdateServiceSpec: if not async_allowed: raise errors.ErrAsyncRequired() params = details.parameters or {} instance = ServiceInstance.query.get(instance_id) if not instance: raise errors.ErrBadRequest("Service instance does not exist") if instance.deactivated_at: raise errors.ErrBadRequest( "Cannot update instance because it was already canceled") if instance.has_active_operations(): raise errors.ErrBadRequest( "Instance has an active operation in progress") domain_names = parse_domain_options(params) noop = True if domain_names is not None: self.logger.info("validating CNAMEs") validators.CNAME(domain_names).validate() self.logger.info("validating unique domains") validators.UniqueDomains(domain_names).validate(instance) noop = noop and (sorted(domain_names) == sorted( instance.domain_names)) if instance.instance_type == "cdn_service_instance" and noop: instance.new_certificate = instance.current_certificate instance.domain_names = domain_names if instance.instance_type == "cdn_service_instance": # N.B. we're using "param" in params rather than # params.get("param") because the OSBAPI spec # requires we do not mess with params that were not # specified, so unset and set to None have different meanings noop = False if details.plan_id != CDN_PLAN_ID: raise ClientError("Updating service plan is not supported") if "origin" in params: if params["origin"]: origin_hostname = params["origin"] validators.Hostname(origin_hostname).validate() else: origin_hostname = config.DEFAULT_CLOUDFRONT_ORIGIN instance.cloudfront_origin_hostname = origin_hostname if "path" in params: if params["path"]: cloudfront_origin_path = params["path"] else: cloudfront_origin_path = "" instance.cloudfront_origin_path = cloudfront_origin_path if "forward_cookies" in params: forward_cookie_policy, forwarded_cookies = parse_cookie_options( params) instance.forward_cookie_policy = forward_cookie_policy instance.forwarded_cookies = forwarded_cookies if "forward_headers" in params: forwarded_headers = parse_header_options(params) else: # .copy() so sqlalchemy recognizes the field has changed forwarded_headers = instance.forwarded_headers.copy() if instance.cloudfront_origin_hostname == config.DEFAULT_CLOUDFRONT_ORIGIN: forwarded_headers.append("HOST") forwarded_headers = normalize_header_list(forwarded_headers) instance.forwarded_headers = forwarded_headers if "insecure_origin" in params: origin_protocol_policy = "https-only" if params["insecure_origin"]: if (instance.cloudfront_origin_hostname == config.DEFAULT_CLOUDFRONT_ORIGIN): raise errors.ErrBadRequest( "Cannot use insecure_origin with default origin") origin_protocol_policy = "http-only" instance.origin_protocol_policy = origin_protocol_policy if "error_responses" in params: instance.error_responses = params["error_responses"] validators.ErrorResponseConfig( instance.error_responses).validate() queue = queue_all_cdn_update_tasks_for_operation elif instance.instance_type == "alb_service_instance": if details.plan_id != ALB_PLAN_ID: raise ClientError("Updating service plan is not supported") queue = queue_all_alb_update_tasks_for_operation elif instance.instance_type == "migration_service_instance": if details.plan_id == CDN_PLAN_ID: noop = False validate_migration_to_cdn_params(params) instance = change_instance_type(instance, CDNServiceInstance, db.session) update_cdn_params_for_migration(instance, params) db.session.add(instance.current_certificate) queue = queue_all_cdn_broker_migration_tasks_for_operation elif details.plan_id == ALB_PLAN_ID: noop = False validate_migration_to_alb_params(params) instance = change_instance_type(instance, ALBServiceInstance, db.session) update_alb_params_for_migration(instance, params) db.session.add(instance.current_certificate) queue = queue_all_domain_broker_migration_tasks_for_operation else: raise ClientError( "Updating to this service plan is not supported") if noop: return UpdateServiceSpec(False) operation = Operation( state=Operation.States.IN_PROGRESS.value, service_instance=instance, action=Operation.Actions.UPDATE.value, step_description="Queuing tasks", ) db.session.add(operation) db.session.add(instance) db.session.commit() queue(operation.id, cf_logging.FRAMEWORK.context.get_correlation_id()) return UpdateServiceSpec(True, operation=str(operation.id))