def __call__(self) -> None: """Commit reservation request. If the reservation can be committed a ReserveCommitConfirmed message will be send to the NSA/AG. If for whatever reason the reservation cannot be committed a ReserveCommitFailed message will be sent instead. If the reservation state machine is not in the correct state for a ReserveCommit an NSI error is returned leaving the state machine unchanged. """ self.log.info("Committing reservation") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) rsm = ReservationStateMachine(reservation, state_field="reservation_state") try: if reservation.reservation_timeout: # we use this column because the reserve state machine is actually missing a state raise NsiException( GenericServiceError, "Cannot commit a timed out reservation.") else: # # TODO: If there is a Network Resource Manager that needs to be contacted # to commit the reservation request then this is the place. # If this is a recovered job then try to recover the reservation state # from the NRM. # pass except NsiException as nsi_exc: self.log.info("Reserve commit failed.", reason=nsi_exc.text) rsm.reserve_commit_failed() self._send_reserve_commit_failed(session, nsi_exc) except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) rsm.reserve_commit_failed() nsi_exc = NsiException(GenericInternalError, str(exc)) # type: ignore[misc] self._send_reserve_commit_failed(session, nsi_exc) # type: ignore[misc] else: # create new psm just before the reserveCommitConfirmed, # this will set initial provision_state on reservation psm = ProvisionStateMachine( reservation, state_field="provision_state") # noqa: F841 rsm.reserve_commit_confirmed() self._send_reserve_commit_confirmed(session)
def __call__(self) -> None: """Provision reservation request. The reservation will be provisioned and a ProvisionConfirmed message will be sent to the NSA/AG. If the provision state machine is not in the correct state for a Provision an NSI exception is returned leaving the state machine unchanged. """ self.log.info("Provisioning reservation") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) psm = ProvisionStateMachine(reservation, state_field="provision_state") dpsm = DataPlaneStateMachine(reservation, state_field="data_plane_state") try: # # TODO: If there is a Network Resource Manager that needs to be contacted # to provision the reservation request then this is the place. # If this is a recovered job then try to recover the reservation state # from the NRM. # pass except NsiException as nsi_exc: self.log.info("Provision failed.", reason=nsi_exc.text) send_error( to_header(reservation), nsi_exc, self.connection_id, ) except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) send_error( to_header(reservation), NsiException( GenericInternalError, str(exc), { Variable.PROVISION_STATE: reservation.provsion_state, Variable.CONNECTION_ID: str(self.connection_id), }, ), self.connection_id, ) else: from supa import scheduler psm.provision_confirmed() dpsm.data_plane_up() scheduler.add_job(job := ActivateJob(self.connection_id), trigger=job.trigger()) self._send_provision_confirmed(session)
def __call__(self) -> None: """Commit Reservation.""" self.log.info("Committing reservation") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) if reservation is None: raise NsiException( ReservationNonExistent, str(self.connection_id), {Variable.CONNECTION_ID: str(self.connection_id)}) try: rsm = ReservationStateMachine(reservation, state_field="reservation_state") rsm.reserve_commit_request() except NsiException as nsi_exc: self.log.info("Reserve commit failed.", reason=nsi_exc.text) rsm.reserve_commit_failed() self._send_reserve_commit_failed(session, nsi_exc) except TransitionNotAllowed as tna: self.log.info("Invalid state transition", reason=str(tna)) rsm.reserve_commit_failed() nsi_exc = NsiException( InvalidTransition, str(tna), { Variable.RESERVATION_STATE: reservation.reservation_state }, ) # type: ignore[misc] self._send_reserve_commit_failed(session, nsi_exc) # type: ignore[misc] except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) rsm.reserve_commit_failed() nsi_exc = NsiException(GenericInternalError, str(exc)) # type: ignore[misc] self._send_reserve_commit_failed(session, nsi_exc) # type: ignore[misc] else: self._send_reserve_commit_confirmed(session)
def __call__(self) -> None: """Abort reservation request. The reservation will be aborted and a ReserveAbortConfirmed message will be sent to the NSA/AG. If the reservation state machine is not in the correct state for a ReserveCommit an NSI error is returned leaving the state machine unchanged. """ self.log.info("Aborting reservation") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) rsm = ReservationStateMachine(reservation, state_field="reservation_state") try: # # TODO: If there is a Network Resource Manager that needs to be contacted # to abort the reservation request then this is the place. # If this is a recovered job then try to recover the reservation state # from the NRM. # pass except NsiException as nsi_exc: self.log.info("Reserve abort failed.", reason=nsi_exc.text) send_error( to_header(reservation), nsi_exc, self.connection_id, ) except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) send_error( to_header(reservation), NsiException( GenericInternalError, str(exc), { Variable.RESERVATION_STATE: reservation.reservation_state, Variable.CONNECTION_ID: str(self.connection_id), }, ), self.connection_id, ) else: rsm.reserve_abort_confirmed() self._send_reserve_abort_confirmed(session)
def __call__(self) -> None: """Activate data plane request. Activate the data plane according to the reservation criteria and send a data plane status notitication to the NSA/AG. If the data plane state machine is not in the correct state for a Activate an NSI exception is returned leaving the state machine unchanged. """ self.log.info("Activating data plane") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) dpsm = DataPlaneStateMachine(reservation, state_field="data_plane_state") try: # # TODO: Call the Network Resource Manager to activate the data plane. # If this is a recovered job then try to recover the data plane state # from the NRM. # pass except NsiException as nsi_exc: self.log.info("Data plane activation failed", reason=nsi_exc.text) send_error( to_header(reservation), nsi_exc, self.connection_id, ) except Exception as exc: self.log.exception("Unexpected error occurred", reason=str(exc)) send_error( to_header(reservation), NsiException( GenericInternalError, str(exc), { Variable.CONNECTION_ID: str(self.connection_id), }, ), self.connection_id, ) else: dpsm.data_plane_activated() send_data_plane_state_change(session, self.connection_id)
def __call__(self) -> None: """Check reservation request. If the reservation can be made a ReserveConfirmed message will be send to the NSA/AG. If not, a ReserveFailed message will be send instead. """ self.log.info("Checking reservation request.") from supa.db.session import db_session with db_session() as session: reservation: Reservation = (session.query(Reservation).options( joinedload(Reservation.parameters), joinedload(Reservation.path_trace).joinedload( PathTrace.paths).joinedload(Path.segments).joinedload( Segment.stps), ).get(self.connection_id)) rsm = ReservationStateMachine(reservation, state_field="reservation_state") port_resources_in_use = self._port_resources_in_use(session) try: if rsm.current_state != ReservationStateMachine.ReserveChecking: raise NsiException( InvalidTransition, rsm.current_state.name, {Variable.RESERVATION_STATE: rsm.current_state.value}, ) if reservation.src_port == reservation.dst_port: raise NsiException( # Not sure if this is the correct error to use. # As its descriptive text refers to path computation # it suggests its an error typically returned by an aggregator. # On the other hand it is the only error related to a path/connection as a whole # and that is what is at issue here. NoServiceplanePathFound, "source and destination ports are the same", { Variable.PROVIDER_NSA: settings.nsa_id, Variable.SOURCE_STP: str(reservation.src_stp()), Variable.DEST_STP: str(reservation.dst_stp()), }, ) for target, var in (("src", Variable.SOURCE_STP), ("dst", Variable.DEST_STP)): # Dynamic attribute lookups as we want to use the same code for # both src and dst ports/stps res_port = getattr(reservation, f"{target}_port") stp = str( getattr(reservation, f"{target}_stp")()) # <-- mind the func call domain = getattr(reservation, f"{target}_domain") network_type = getattr(reservation, f"{target}_network_type") requested_vlans = VlanRanges( getattr(reservation, f"{target}_vlans")) port = session.query(Port).filter( Port.name == res_port).one_or_none() if (port is None or not port.enabled or domain != settings. domain # only process requests for our domain or network_type != settings. network_type # only process requests for our network ): raise NsiException(UnknownStp, stp, {var: stp}) if not requested_vlans: raise NsiException(InvalidLabelFormat, "missing VLANs label on STP", {var: stp}) if port.name in port_resources_in_use: bandwidth_available = port.bandwidth - port_resources_in_use[ port.name].bandwidth available_vlans = VlanRanges( port.vlans) - port_resources_in_use[ port.name].vlans else: bandwidth_available = port.bandwidth available_vlans = VlanRanges(port.vlans) if bandwidth_available < reservation.bandwidth: raise NsiException( CapacityUnavailable, f"requested: {format_bandwidth(reservation.bandwidth)}, " f"available: {format_bandwidth(bandwidth_available)}", { Variable.CAPACITY: str(reservation.bandwidth), var: stp, }, ) if not available_vlans: raise NsiException(StpUnavailable, "all VLANs in use", {var: stp}) candidate_vlans = requested_vlans & available_vlans if not candidate_vlans: raise NsiException( StpUnavailable, f"no matching VLAN found (requested: {requested_vlans!s}, available: {available_vlans!s}", {var: stp}, ) selected_vlan = random.choice(list(candidate_vlans)) setattr(reservation, f"{target}_selected_vlan", selected_vlan) except NsiException as nsi_exc: self.log.info("Reservation failed.", reason=nsi_exc.text) rsm.reserve_failed() self._send_reserve_failed(session, nsi_exc) except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) rsm.reserve_failed() nsi_exc = NsiException(GenericInternalError, str(exc)) # type: ignore[misc] self._send_reserve_failed(session, nsi_exc) # type: ignore[misc] else: rsm.reserve_confirmed() self._send_reserve_confirmed(session)
def Reserve(self, pb_reserve_request: ReserveRequest, context: ServicerContext) -> ReserveResponse: """Request new reservation, or modify existing reservation. The reserve message is sent from an RA to a PA when a new reservation is being requested, or a modification to an existing reservation is required. The :class:`ReserveResponse` indicates that the PA has accepted the reservation request for processing and has assigned it the returned connectionId. The original ``connection_id`` will be returned for the :class:`ReserveResponse`` of a modification. A :class:`ReserveConfirmed` or :class:`ReserveFailed` message will be sent asynchronously to the RA when reserve operation has completed processing. Args: pb_reserve_request: All the details about the requested reservation. context: gRPC server context object. Returns: A :class:`ReserveResponse` message containing the PA assigned ``connection_id`` for this reservation request. This value will be unique within the context of the PA. """ log = logger.bind(method="Reserve") log.debug("Received message.", request_message=pb_reserve_request) # Sanity check on start and end time, in case of problem return ServiceException pb_criteria: ReservationRequestCriteria = pb_reserve_request.criteria pb_schedule: Schedule = pb_criteria.schedule start_time = as_utc_timestamp(pb_schedule.start_time) end_time = as_utc_timestamp(pb_schedule.end_time) extra_info = "" if is_specified(end_time): if end_time <= start_time: extra_info = f"End time cannot come before start time. {start_time=!s}, {end_time=!s}" elif end_time <= current_timestamp(): extra_info = f"End time lies in the past. {end_time=!s}" if extra_info: log.info(extra_info) reserve_response = ReserveResponse( header=to_response_header(pb_reserve_request.header), service_exception=to_service_exception( NsiException(MissingParameter, extra_info, {Variable.END_TIME: end_time.isoformat()})), ) log.debug("Sending response.", response_message=reserve_response) return reserve_response if not pb_reserve_request.connection_id: # new reservation pb_header: Header = pb_reserve_request.header pb_ptps: PointToPointService = pb_criteria.ptps pb_path_trace: PathTrace = pb_header.path_trace reservation = model.Reservation( correlation_id=UUID(pb_header.correlation_id), protocol_version=pb_header.protocol_version, requester_nsa=pb_header.requester_nsa, provider_nsa=pb_header.provider_nsa, reply_to=pb_header.reply_to if pb_header.reply_to else None, session_security_attributes=pb_header. session_security_attributes if pb_header.session_security_attributes else None, global_reservation_id=pb_reserve_request.global_reservation_id, description=pb_reserve_request.description if pb_reserve_request.description else None, version=pb_criteria.version, ) if is_specified(start_time): reservation.start_time = start_time if is_specified(end_time): reservation.end_time = end_time reservation.bandwidth = pb_ptps.capacity reservation.directionality = Directionality.Name( pb_ptps.directionality) reservation.symmetric = pb_ptps.symmetric_path src_stp = parse_stp(pb_ptps.source_stp) reservation.src_domain = src_stp.domain reservation.src_network_type = src_stp.network_type reservation.src_port = src_stp.port reservation.src_vlans = str(src_stp.vlan_ranges) dst_stp = parse_stp(pb_ptps.dest_stp) reservation.dst_domain = dst_stp.domain reservation.dst_network_type = dst_stp.network_type reservation.dst_port = dst_stp.port reservation.dst_vlans = str(dst_stp.vlan_ranges) for k, v in pb_ptps.parameters.items(): reservation.parameters.append(model.Parameter(key=k, value=v)) if pb_path_trace.id: path_trace = model.PathTrace( path_trace_id=pb_path_trace.id, ag_connection_id=pb_path_trace.connection_id) for pb_path in pb_path_trace.paths: path = model.Path() for pb_segment in pb_path.segments: segment = model.Segment( segment_id=pb_segment.id, upa_connection_id=pb_segment.connection_id) for pb_stp in pb_segment.stps: segment.stps.append(model.Stp(stp_id=pb_stp)) path.segments.append(segment) path_trace.paths.append(path) reservation.path_trace = path_trace rsm = ReservationStateMachine(reservation, state_field="reservation_state") rsm.reserve_request() from supa.db.session import db_session with db_session() as session: session.add(reservation) session.flush( ) # Let DB (actually SQLAlchemy) generate the connection_id for us. connection_id = reservation.connection_id # Can't reference it outside of the session, hence new var. log = log.bind(connection_id=str(connection_id)) else: log = log.bind(connection_id=pb_reserve_request.connection_id) # TODO modify reservation (else clause) from supa import scheduler job: Job scheduler.add_job(job := ReserveJob(connection_id), trigger=job.trigger()) reserve_response = ReserveResponse(header=to_response_header( pb_reserve_request.header), connection_id=str(connection_id)) # # TODO: - make timeout delta configurable # - add reservation version to timeout job so we do not accidentally timeout a modify # log.info("Schedule reserve timeout", connection_id=str(connection_id), timeout=30) scheduler.add_job(job := ReserveTimeoutJob(connection_id), trigger=job.trigger()) log.debug("Sending response.", response_message=reserve_response) return reserve_response
def Provision(self, pb_provision_request: ProvisionRequest, context: ServicerContext) -> ProvisionResponse: """Provision reservation. Check if the connection ID exists, if the provision state machine exists (as an indication that the reservation was committed, and if the provision state machine transition is allowed, all real work for provisioning the reservation is done asynchronously by :class:`~supa.job.reserve.ProvisionJob` Args: pb_provision_request: Basically the connection id wrapped in a request like object context: gRPC server context object. Returns: A response telling the caller we have received its provision request. """ connection_id = UUID(pb_provision_request.connection_id) log = logger.bind(method="Provision", connection_id=str(connection_id)) log.debug("Received message.", request_message=pb_provision_request) from supa.db.session import db_session with db_session() as session: reservation = (session.query( model.Reservation).filter(model.Reservation.connection_id == connection_id).one_or_none()) if reservation is None: log.info("Connection ID does not exist") provision_response = ProvisionResponse( header=to_response_header(pb_provision_request.header), service_exception=to_service_exception( NsiException( ReservationNonExistent, str(connection_id), {Variable.CONNECTION_ID: str(connection_id)}), connection_id, ), ) elif not reservation.provision_state: log.info("First version of reservation not committed yet") provision_response = ProvisionResponse( header=to_response_header(pb_provision_request.header), service_exception=to_service_exception( NsiException( InvalidTransition, "First version of reservation not committed yet", { Variable.RESERVATION_STATE: reservation.reservation_state, Variable.CONNECTION_ID: str(connection_id), }, ), connection_id, ), ) else: try: rsm = ProvisionStateMachine(reservation, state_field="provision_state") rsm.provision_request() except TransitionNotAllowed as tna: log.info("Not scheduling ProvisionJob", reason=str(tna)) provision_response = ProvisionResponse( header=to_response_header(pb_provision_request.header), service_exception=to_service_exception( NsiException( InvalidTransition, str(tna), { Variable.PROVISION_STATE: reservation.provision_state, Variable.CONNECTION_ID: str(connection_id), }, ), connection_id, ), ) else: from supa import scheduler scheduler.add_job(job := ProvisionJob(connection_id), trigger=job.trigger()) provision_response = ProvisionResponse( header=to_response_header(pb_provision_request.header)) log.debug("Sending response.", response_message=provision_response) return provision_response
def __call__(self) -> None: """Timeout reservation request. The reservation will be timed out if the ReservationStateMachine is still in the ReserveHeld state and a ReserveTimeoutNotification message will be sent to the NSA/AG. If another transition already moved the state beyond ReserveHeld then this is practically a no-op. """ self.log.info("Timeout reservation") from supa.db.session import db_session with db_session() as session: reservation = (session.query(Reservation).filter( Reservation.connection_id == self.connection_id).one_or_none()) rsm = ReservationStateMachine(reservation, state_field="reservation_state") try: rsm.reserve_timeout_notification() # # TODO: If there is a Network Resource Manager that needs to be contacted # to timeout the reservation request then this is the place. # If this is a recovered job then try to recover the reservation state # from the NRM. # except TransitionNotAllowed: # Reservation is already in another state turning this into a no-op. self.log.info( "Reservation not timed out", state=rsm.current_state.identifier, connection_id=str(self.connection_id), ) except NsiException as nsi_exc: self.log.info("Reserve timeout failed.", reason=nsi_exc.text) send_error( to_header(reservation), nsi_exc, self.connection_id, ) except Exception as exc: self.log.exception("Unexpected error occurred.", reason=str(exc)) send_error( to_header(reservation), NsiException( GenericInternalError, str(exc), { Variable.RESERVATION_STATE: reservation.reservation_state, Variable.CONNECTION_ID: str(self.connection_id), }, ), self.connection_id, ) else: # # TODO: release reserved resources(?) # self.log.debug( "setting reservation.reservation_timeout to true in db") reservation.reservation_timeout = True self._send_reserve_timeout_notification(session)