def test_vlanranges_non_sensical_values() -> None: # noqa: D103 # Negative values, however, are an error with pytest.raises(ValueError): VlanRanges("-10") with pytest.raises(ValueError): VlanRanges("fubar")
def test_vlanranges_str_repr() -> None: # noqa: D103 vr = VlanRanges("10-14,4,200-256") # `str` version of VlanRanges should be suitable value for constructor, # resulting in equivalent object assert vr == VlanRanges(str(vr)) # `repr` version of VlanRanges should be valid Python code r # esulting in an equivalent object vr_from_repr = eval(repr(vr), globals(), locals()) # noqa: S307 Use of possibly insecure function assert vr_from_repr == vr
def test_vlanranges_subset_operator() -> None: # noqa: D103 vr = VlanRanges("10-20") assert not VlanRanges("8-9") < vr assert not VlanRanges("9-10") < vr assert not VlanRanges("21-23") < vr assert not VlanRanges("20-23") < vr assert not VlanRanges("10-20") < vr assert VlanRanges("11-19") < vr assert not VlanRanges("0-3,10,20,21-28") < vr assert not VlanRanges("0-3,20") < vr assert not VlanRanges("0-3,9,21,22-30") < vr
def test_stp_vlan_ranges() -> None: # noqa: D103 stp = parse_stp( "urn:ogf:network:netherlight.net:2013:production7:netherlight-of-1?vlan=200-500,1779-1799" ) assert stp.vlan_ranges == VlanRanges("200-500,1779-1799") stp = parse_stp( "urn:ogf:network:netherlight.net:2013:production7:netherlight-of-1?vlan=1799" ) assert stp.vlan_ranges == VlanRanges(1799) stp = parse_stp( "urn:ogf:network:netherlight.net:2013:production7:netherlight-of-1") assert stp.vlan_ranges == VlanRanges()
def vlan_ranges(self) -> VlanRanges: """Return the vlan ranges specified on the STP. A single If no vlan ranges where specified on the STP, this will return an "empty" :class:`~supa.util.vlan.VlanRanges` object. Such an object will evaluate to False in a boolean context. Returns: A :class:`~supa.util.vlan.VlanRanges` object. """ if self.labels is not None and self.labels.startswith("vlan="): return VlanRanges(self.labels[len("vlan="):]) # noqa: E203 return VlanRanges()
def test_vlanranges_in() -> None: # noqa: D103 vr = VlanRanges("10-20") assert 10 in vr assert 20 in vr assert 9 not in vr assert 21 not in vr assert 0 not in vr
def test_vlanranges_union() -> None: # noqa: D103 vr = VlanRanges("10-20") # with iterable assert vr == VlanRanges().union(VlanRanges("10-15"), {16, 17, 18}, {19}, VlanRanges(20)) # single arg assert vr == VlanRanges("10-19").union(VlanRanges(20))
def add(port_id: UUID, name: str, vlans: str, remote_stp: Optional[str], bandwidth: int, enabled: bool) -> None: """Add Orchestrator port to SuPA.""" init_app(with_scheduler=False) # Safe to import, now that `init_app()` has been called from supa.db.model import Port from supa.db.session import db_session port = Port( port_id=port_id, name=name, vlans=str(VlanRanges(vlans)), remote_stp=remote_stp, bandwidth=bandwidth, enabled=enabled, ) with db_session() as session: session.add(port)
def _port_resources_in_use( self, session: orm.Session) -> Dict[str, PortResources]: """Calculate port resources in use for active reservations that overlap with ours. Active reservations being those that: - are currently being held - have been committed and not yet been terminated. Overlap as in: their start times and end times overlap with ours. The bandwidth in use is calculated per port. Eg, if a port is used in two active reservations, (one reservation for a connection with a bandwidth of 100 Mbps and another with a bandwidth of 400 Mbps) the bandwidth in use for the port will be: 100 + 400 = 500 Mbps. Similarly for the VLANs in use. Given the same port used in two active reservations (one reservation where the port has a VLAN of 100 and another one where the port has a VLAN of 105), the VLANs in use for the port will be: VlanRanges([100, 105]) Args: session: A SQLAlchemy session to construct and run the DB query Returns: A dict mapping port (names) to their port resources. """ # To calculate the active overlapping reservation we need to perform a self-join. # One part of the join is for our (current) reservation. # The other part is for joining the overlapping ones with our (current) reservation. CurrentReservation = aliased(Reservation, name="cr") overlap_active = ( # The other part session.query(Reservation).join(( CurrentReservation, # Do they overlap? and_( CurrentReservation.start_time < Reservation.end_time, CurrentReservation.end_time > Reservation.start_time, ), )).filter( # Only select active reservations or_( and_( Reservation.reservation_state == ReservationStateMachine.ReserveStart.name, Reservation.provisioning_state.isnot(None), Reservation.lifecycle_state == LifecycleStateMachine.Created.name, ), Reservation.reservation_state == ReservationStateMachine.ReserveHeld.name, )) # And only those that overlap with our reservation. .filter(CurrentReservation.connection_id == self.connection_id )).subquery() OverlappingActiveReservation = aliased(Reservation, overlap_active, name="oar") # To map ports to resources (bandwidth and vlan) in use # we need to unpivot the two pair of port columns from the reservations table into separate rows. # Eg, from: # # row 1: connection_id, ..., src_port, src_selected_vlan, dst_port, .dst_selected_vlan .. # # to: # # row 1: connection_id, port, vlan <-- former src_port, src_selected_vlan # row 2: connection_id, port, vlan <-- former dst_port, dst_selected_vlan src_port = session.query( Reservation.connection_id.label("connection_id"), Reservation.src_port.label("port"), Reservation.src_selected_vlan.label("vlan"), ) dst_port = session.query( Reservation.connection_id, Reservation.dst_port.label("port"), Reservation.dst_selected_vlan.label("vlan"), ) ports = src_port.union(dst_port).subquery() # With the 'hard' work done for us in two subqueries, # calculating the port resources (bandwidth, VLANs) in use is now relatively straightforward. port_resources_in_use = ( session.query( ports.c.port, func.sum( OverlappingActiveReservation.bandwidth).label("bandwidth"), func.group_concat(ports.c.vlan, ",").label("vlans"), # yes, plural! ).select_from(OverlappingActiveReservation).join( ports, OverlappingActiveReservation.connection_id == ports.c.connection_id).filter( ports.c.port.in_(( OverlappingActiveReservation.src_port, OverlappingActiveReservation.dst_port, ))).group_by(ports.c.port).all()) return { rec.port: PortResources(bandwidth=rec.bandwidth, vlans=VlanRanges(rec.vlans)) for rec in port_resources_in_use }
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 test_vlanranges_union_operator() -> None: # noqa: D103 vr = VlanRanges("10-20") assert vr | VlanRanges("8-9") == VlanRanges("8-20") assert vr | VlanRanges("9-10") == VlanRanges("9-20") assert vr | VlanRanges("21-23") == VlanRanges("10-23") assert vr | VlanRanges("20-23") == VlanRanges("10-23") assert vr | VlanRanges("10-20") == VlanRanges("10-20") assert vr | VlanRanges("11-19") == VlanRanges("10-20") assert vr | VlanRanges("0-3,10,20,21-28") == VlanRanges("0-3,10-28") assert vr | VlanRanges("0-3,20") == VlanRanges("0-3,10-20") assert vr | VlanRanges("0-3,9,21,22-30") == VlanRanges("0-3,9-30")
def test_vlanranges_instantiation() -> None: # noqa: D103 assert VlanRanges() == VlanRanges([]) assert VlanRanges(None) == VlanRanges([]) assert VlanRanges("") == VlanRanges([]) assert VlanRanges(4) == VlanRanges("4") assert VlanRanges("0") == VlanRanges([(0, 0)]) == VlanRanges([[0]]) assert VlanRanges("2,4,8") == VlanRanges([(2, 2), (4, 4), (8, 8)]) == VlanRanges([[2], [4], [8]]) assert VlanRanges("80-120") == VlanRanges([(80, 120)]) == VlanRanges([[80, 120]]) assert VlanRanges("10,12-16") == VlanRanges([(10, 10), (12, 16)]) == VlanRanges([[10], [12, 16]]) # String interpretation is quite flexible, # allowing extra whitespace assert VlanRanges(" 4 , 6- 10") == VlanRanges("4,6-10") # Overlapping ranges will be normalized assert VlanRanges("4,6-9,7-10") == VlanRanges("4,6-10") assert VlanRanges([[4], [6, 9], [7, 10]]) == VlanRanges([[4], [6, 10]]) # Not all non-ranges are an error perse assert VlanRanges("10-1") == VlanRanges("")
def test_vlanranges_substract() -> None: # noqa: D103 vr = VlanRanges("10-20") assert vr - VlanRanges("8-9") == VlanRanges("10-20") assert vr - VlanRanges("9-10") == VlanRanges("11-20") assert vr - VlanRanges("21-23") == VlanRanges("10-20") assert vr - VlanRanges("20-23") == VlanRanges("10-19") assert vr - VlanRanges("10-20") == VlanRanges("") assert vr - VlanRanges("11-19") == VlanRanges("10,20") assert vr - VlanRanges("0-3,10,20,21-28") == VlanRanges("11-19") assert vr - VlanRanges("0-3,20") == VlanRanges("10-19") assert vr - VlanRanges("0-3,9,21,22-30") == VlanRanges("10-20")
def test_vlanranges_repr() -> None: # noqa: D103 vr = VlanRanges("3,4-10") vr2 = VlanRanges(str(vr)) assert vr == vr2
def test_vlanranges_hash() -> None: # noqa: D103 vr = VlanRanges("10-14,4,200-256") # Just making sure it doesn't raise an exception. # Which, BTW will be raised, # should the internal representation be changed to a mutable data structure. assert hash(vr)
def test_vlanranges_symmetric_difference_operator() -> None: # noqa: D103 vr = VlanRanges("10-20") assert vr ^ VlanRanges("8-9") == VlanRanges("8-20") assert vr ^ VlanRanges("9-10") == VlanRanges("9,11-20") assert vr ^ VlanRanges("21-23") == VlanRanges("10-23") assert vr ^ VlanRanges("20-23") == VlanRanges("10-19,21-23") assert vr ^ VlanRanges("10-20") == VlanRanges("") assert vr ^ VlanRanges("11-19") == VlanRanges("10,20") assert vr ^ VlanRanges("0-3,10,20,21-28") == VlanRanges("0-3,11-19,21-28") assert vr ^ VlanRanges("0-3,20") == VlanRanges("0-3,10-19") assert vr ^ VlanRanges("0-3,9,21,22-30") == VlanRanges("0-3,9-30")