Exemplo n.º 1
0
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")
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
    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()
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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))
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
    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
        }
Exemplo n.º 10
0
    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)
Exemplo n.º 11
0
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")
Exemplo n.º 12
0
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("")
Exemplo n.º 13
0
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")
Exemplo n.º 14
0
def test_vlanranges_repr() -> None:  # noqa: D103
    vr = VlanRanges("3,4-10")
    vr2 = VlanRanges(str(vr))
    assert vr == vr2
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
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")