From ce2e27630fa7e3996b39296ae64f1fc1fef5b145 Mon Sep 17 00:00:00 2001 From: Hans Trompert Date: Tue, 3 Sep 2024 16:02:18 +0200 Subject: [PATCH] add modify reservation unit tests --- src/supa/job/reserve.py | 7 +- tests/conftest.py | 137 +++++++++++++++++++++++ tests/connection/provider/test_server.py | 105 ++++++++++++++++- tests/job/test_reserve.py | 87 ++++++++++++++ 4 files changed, 334 insertions(+), 2 deletions(-) diff --git a/src/supa/job/reserve.py b/src/supa/job/reserve.py index fa1f4076..f8382250 100644 --- a/src/supa/job/reserve.py +++ b/src/supa/job/reserve.py @@ -524,6 +524,11 @@ def __call__(self) -> None: dpsm.current_state == DataPlaneStateMachine.Activated or dpsm.current_state == DataPlaneStateMachine.AutoEnd ): + self.log.info( + "modify bandwidth on connection", + old_bandwidth=old_bandwidth, + new_bandwidth=new_bandwidth, + ) if circuit_id := backend.modify(**connection_to_dict(connection)): connection.circuit_id = circuit_id @@ -555,7 +560,7 @@ def __call__(self) -> None: self.log.info("Cancel previous auto end") scheduler.remove_job(job_id=AutoEndJob(self.connection_id).job_id) if schedule_auto_end: - self.log.info("Schedule auto end", job="AutoEndJob", end_time=new_end_time.isoformat()) + self.log.info("Schedule new auto end", job="AutoEndJob", end_time=new_end_time.isoformat()) scheduler.add_job(job := AutoEndJob(self.connection_id), trigger=job.trigger(), id=job.job_id) register_result(request, ResultType.ReserveCommitConfirmed) self.log.debug("Sending message", method="ReserveCommitConfirmed", request_message=request) diff --git a/tests/conftest.py b/tests/conftest.py index 226bf95d..b4f76e13 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,7 @@ from supa.grpc_nsi import connection_provider_pb2_grpc from supa.job.dataplane import AutoEndJob, AutoStartJob from supa.job.reserve import ReserveTimeoutJob +from supa.util.timestamp import NO_END_DATE from supa.util.type import RequestType @@ -494,6 +495,21 @@ def auto_end(connection_id: Column) -> Generator: pass # job already removed from job store +@pytest.fixture +def auto_end_job(connection_id: Column) -> Generator: + """Run AutoEndtJob for connection_id.""" + from supa import scheduler + + job_handle = scheduler.add_job(job := AutoEndJob(connection_id), trigger=job.trigger(), id=job.job_id) + + yield None + + try: + job_handle.remove() + except JobLookupError: + pass # job already removed from job store + + @pytest.fixture def deactivating(connection_id: Column) -> None: """Set data plane state machine of reservation identified by connection_id to state Deactivating.""" @@ -522,3 +538,124 @@ def flag_reservation_timeout(connection_id: Column) -> None: with db_session() as session: reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() reservation.reservation_timeout = True + + +@pytest.fixture +def start_now(connection_id: Column) -> None: + """Set reservation start time to now.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.schedule.start_time = datetime.now(timezone.utc) + + +@pytest.fixture +def no_end_time(connection_id: Column) -> None: + """Set reservation start time to now.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.schedule.end_time = NO_END_DATE + + +def p2p_criteria_from_p2_criteria(p2p_criteria: P2PCriteria) -> P2PCriteria: + """Create deepcopy of given P2PCriteria object with version set to 1.""" + return P2PCriteria( + version=1, + bandwidth=p2p_criteria.bandwidth, + symmetric=p2p_criteria.symmetric, + src_domain=p2p_criteria.src_domain, + src_topology=p2p_criteria.src_topology, + src_stp_id=p2p_criteria.src_stp_id, + src_vlans=p2p_criteria.src_vlans, + src_selected_vlan=p2p_criteria.src_selected_vlan, + dst_domain=p2p_criteria.dst_domain, + dst_topology=p2p_criteria.dst_topology, + dst_stp_id=p2p_criteria.dst_stp_id, + dst_vlans=p2p_criteria.dst_vlans, + dst_selected_vlan=p2p_criteria.dst_selected_vlan, + ) + + +@pytest.fixture +def modified_start_time(connection_id: Column) -> None: + """Add Schedule with modified start time on connection set to Provisioned and AutoStart.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.data_plane_state = DataPlaneStateMachine.AutoStart.value + reservation.provision_state = ProvisionStateMachine.Provisioned.value + reservation.schedules.append( + Schedule( + version=1, + start_time=reservation.schedule.start_time + timedelta(minutes=1), + end_time=reservation.schedule.end_time, + ) + ) + reservation.p2p_criteria_list.append(p2p_criteria_from_p2_criteria(reservation.p2p_criteria)) + reservation.version = reservation.version + 1 + + +@pytest.fixture +def modified_no_end_time(connection_id: Column) -> None: + """Add Schedule with no end time on connection set to Provisioned and AutoEnd.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.data_plane_state = DataPlaneStateMachine.AutoEnd.value + reservation.provision_state = ProvisionStateMachine.Provisioned.value + reservation.schedules.append( + Schedule( + version=1, + start_time=reservation.schedule.start_time, + end_time=NO_END_DATE, + ) + ) + reservation.p2p_criteria_list.append(p2p_criteria_from_p2_criteria(reservation.p2p_criteria)) + reservation.version = reservation.version + 1 + + +@pytest.fixture +def modified_end_time(connection_id: Column) -> None: + """Add Schedule with end time of 30 minutes in the future on connection set to Provisioned and Activated.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.data_plane_state = DataPlaneStateMachine.Activated.value + reservation.provision_state = ProvisionStateMachine.Provisioned.value + reservation.schedules.append( + Schedule( + version=1, + start_time=reservation.schedule.start_time, + end_time=datetime.now(timezone.utc) + timedelta(minutes=30), + ) + ) + reservation.p2p_criteria_list.append(p2p_criteria_from_p2_criteria(reservation.p2p_criteria)) + reservation.version = reservation.version + 1 + + +@pytest.fixture +def modified_bandwidth(connection_id: Column) -> None: + """Add P2PCriteria with modified bandwidth on connection set to Provisioned and Activated.""" + from supa.db.session import db_session + + with db_session() as session: + reservation = session.query(Reservation).filter(Reservation.connection_id == connection_id).one() + reservation.data_plane_state = DataPlaneStateMachine.Activated.value + reservation.provision_state = ProvisionStateMachine.Provisioned.value + reservation.schedules.append( + Schedule( + version=1, + start_time=reservation.schedule.start_time, + end_time=reservation.schedule.end_time, + ) + ) + new_p2p_criteria = p2p_criteria_from_p2_criteria(reservation.p2p_criteria) + new_p2p_criteria.bandwidth = new_p2p_criteria.bandwidth + 10 + reservation.p2p_criteria_list.append(new_p2p_criteria) + reservation.version = reservation.version + 1 diff --git a/tests/connection/provider/test_server.py b/tests/connection/provider/test_server.py index a1cc7367..828bdd6a 100644 --- a/tests/connection/provider/test_server.py +++ b/tests/connection/provider/test_server.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta, timezone from json import dumps from typing import Any -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest from google.protobuf.json_format import Parse @@ -65,6 +65,7 @@ def pb_reservation_request_criteria(pb_schedule: Schedule, pb_ptps: PointToPoint reservation_request_criteria.schedule.CopyFrom(pb_schedule) reservation_request_criteria.service_type = const.SERVICE_TYPE reservation_request_criteria.ptps.CopyFrom(pb_ptps) + reservation_request_criteria.version = 1 return reservation_request_criteria @@ -184,6 +185,108 @@ def test_reserve_request_end_time_in_past(pb_reserve_request_end_time_in_past: R assert "End time lies in the past" in caplog.text +def test_reserve_modify(pb_reserve_request: ReserveRequest, connection_id: UUID, connection: None, caplog: Any) -> None: + """Test the connection provider Reserve Modify happy path.""" + service = ConnectionProviderService() + mock_context = unittest.mock.create_autospec(spec=ServicerContext) + request_correlation_id = pb_reserve_request.header.correlation_id + # add existing connection_id to reservation to mark this a modify request + pb_reserve_request.connection_id = str(connection_id) + reserve_response = service.Reserve(pb_reserve_request, mock_context) + assert request_correlation_id == reserve_response.header.correlation_id + assert not reserve_response.header.reply_to + assert reserve_response.connection_id == str(connection_id) + assert not reserve_response.HasField("service_exception") + assert "modify reservation" in caplog.text + assert "Schedule reserve" in caplog.text + assert "Schedule reserve timeout" in caplog.text + + +def test_reserve_modify_illegal_version( + pb_reserve_request: ReserveRequest, connection_id: UUID, connection: None, caplog: Any +) -> None: + """Test the connection provider Reserve Modify happy path.""" + service = ConnectionProviderService() + mock_context = unittest.mock.create_autospec(spec=ServicerContext) + request_correlation_id = pb_reserve_request.header.correlation_id + # add existing connection_id to reservation to mark this a modify request + pb_reserve_request.connection_id = str(connection_id) + # criteria version may only be incremented by 1 + pb_reserve_request.criteria.version = pb_reserve_request.criteria.version + 2 + reserve_response = service.Reserve(pb_reserve_request, mock_context) + assert request_correlation_id == reserve_response.header.correlation_id + assert not reserve_response.header.reply_to + assert not reserve_response.connection_id + assert reserve_response.HasField("service_exception") + assert reserve_response.service_exception.error_id == "00102" + assert reserve_response.service_exception.connection_id == str(connection_id) + assert "version may only be incremented by 1" in caplog.text + + +def test_reserve_modify_unknown_connection_id( + pb_reserve_request: ReserveRequest, connection: None, caplog: Any +) -> None: + """Test the connection provider Reserve Modify happy path.""" + service = ConnectionProviderService() + mock_context = unittest.mock.create_autospec(spec=ServicerContext) + request_correlation_id = pb_reserve_request.header.correlation_id + # add unknown connection_id to this modify request + non_existing_connection_id = str(uuid4()) + pb_reserve_request.connection_id = str(non_existing_connection_id) + reserve_response = service.Reserve(pb_reserve_request, mock_context) + assert request_correlation_id == reserve_response.header.correlation_id + assert not reserve_response.header.reply_to + assert not reserve_response.connection_id + assert reserve_response.HasField("service_exception") + assert reserve_response.service_exception.connection_id == non_existing_connection_id + assert reserve_response.service_exception.error_id == "00203" + assert "Connection ID does not exist" in caplog.text + assert reserve_response.service_exception.variables[0].type == "connectionId" + assert reserve_response.service_exception.variables[0].value == non_existing_connection_id + + +def test_reserve_modify_reservation_already_started( + pb_reserve_request: ReserveRequest, connection_id: UUID, start_now: None, connection: None, caplog: Any +) -> None: + """Test the connection provider Reserve Modify happy path.""" + service = ConnectionProviderService() + mock_context = unittest.mock.create_autospec(spec=ServicerContext) + request_correlation_id = pb_reserve_request.header.correlation_id + # add existing connection_id to reservation to mark this a modify request + pb_reserve_request.connection_id = str(connection_id) + # change start time to 1 minute in te future + pb_reserve_request.criteria.schedule.start_time.FromDatetime(datetime.now(timezone.utc) + timedelta(minutes=1)) + reserve_response = service.Reserve(pb_reserve_request, mock_context) + assert request_correlation_id == reserve_response.header.correlation_id + assert not reserve_response.header.reply_to + assert not reserve_response.connection_id + assert reserve_response.HasField("service_exception") + assert reserve_response.service_exception.connection_id == str(connection_id) + assert reserve_response.service_exception.error_id == "00102" + assert "cannot change start time when reservation already started" in caplog.text + + +def test_reserve_modify_invalid_transition( + pb_reserve_request: ReserveRequest, connection_id: UUID, reserve_held: None, connection: None, caplog: Any +) -> None: + """Test the connection provider Reserve Modify happy path.""" + service = ConnectionProviderService() + mock_context = unittest.mock.create_autospec(spec=ServicerContext) + request_correlation_id = pb_reserve_request.header.correlation_id + # add existing connection_id to reservation to mark this a modify request + pb_reserve_request.connection_id = str(connection_id) + reserve_response = service.Reserve(pb_reserve_request, mock_context) + assert request_correlation_id == reserve_response.header.correlation_id + assert not reserve_response.header.reply_to + assert not reserve_response.connection_id + assert reserve_response.HasField("service_exception") + assert reserve_response.service_exception.connection_id == str(connection_id) + assert reserve_response.service_exception.error_id == "00201" + assert "Can't reserve_request when in ReserveHeld" in caplog.text + assert reserve_response.service_exception.variables[0].type == "connectionId" + assert reserve_response.service_exception.variables[0].value == str(connection_id) + + def test_reserve_commit(pb_reserve_commit_request: GenericRequest, reserve_held: None, caplog: Any) -> None: """Test the connection provider ReserveCommit happy path.""" service = ConnectionProviderService() diff --git a/tests/job/test_reserve.py b/tests/job/test_reserve.py index e4c42485..8c5a1bb5 100644 --- a/tests/job/test_reserve.py +++ b/tests/job/test_reserve.py @@ -205,6 +205,93 @@ def test_reserve_commit_job_reserve_commit_confirmed( assert state_machine.is_reserve_start(connection_id) +def test_reserve_commit_modified_start_time( + connection_id: UUID, + connection: None, + reserve_committing: None, + modified_start_time: None, + auto_start_job: None, + get_stub: None, + caplog: Any, +) -> None: + """Test ReserveCommitJob to reschedule AutoStartJob when start time is modified. + + Verify (see fake_servicer) that a ReserveCommitJob will + ...... + """ + reserve_commit_job = ReserveCommitJob(connection_id) + reserve_commit_job.__call__() + assert state_machine.is_reserve_start(connection_id) + assert "Reschedule auto start" in caplog.text + + +def test_reserve_commit_modified_no_end_time( + connection_id: UUID, + connection: None, + reserve_committing: None, + modified_no_end_time: None, + auto_end_job: None, + get_stub: None, + caplog: Any, +) -> None: + """Test ReserveCommitJob to reschedule AutoStartJob when start time is modified. + + Verify (see fake_servicer) that a ReserveCommitJob will + transition reservation state machine to ReserveStart, + and transition data plane state machine from AutoEnd to Activated, + and that the previous auto end job is canceled. + """ + reserve_commit_job = ReserveCommitJob(connection_id) + reserve_commit_job.__call__() + assert state_machine.is_reserve_start(connection_id) + assert state_machine.is_activated(connection_id) + assert "Cancel previous auto end" in caplog.text + + +def test_reserve_commit_modified_set_end_time( + connection_id: UUID, + connection: None, + reserve_committing: None, + modified_end_time: None, + get_stub: None, + caplog: Any, +) -> None: + """Test ReserveCommitJob to schedule AutoEndJob when end time is set on connection without end time. + + Verify (see fake_servicer) that a ReserveCommitJob will + transition reservation state machine to ReserveStart, + and transition data plane state machine from Activated to AutoEnd, + and that a auto end job is scheduled. + """ + reserve_commit_job = ReserveCommitJob(connection_id) + reserve_commit_job.__call__() + assert state_machine.is_reserve_start(connection_id) + assert state_machine.is_auto_end(connection_id) + assert "Schedule new auto end" in caplog.text + + +def test_reserve_commit_modified_change_bandwidth( + connection_id: UUID, + connection: None, + reserve_committing: None, + modified_bandwidth: None, + get_stub: None, + caplog: Any, +) -> None: + """Test ReserveCommitJob to call modify() on backend of activated connection without end time. + + Verify (see fake_servicer) that a ReserveCommitJob will + transition reservation state machine to ReserveStart, + and that modify() is called on the backend. + """ + reserve_commit_job = ReserveCommitJob(connection_id) + reserve_commit_job.__call__() + assert state_machine.is_reserve_start(connection_id) + assert state_machine.is_activated(connection_id) + assert "modify bandwidth on connection" in caplog.text + assert "Modify resources in NRM" in caplog.text + + def test_reserve_commit_job_recover(connection_id: UUID, reserve_committing: None, get_stub: None, caplog: Any) -> None: """Test ReserveCommitJob to recover reservations in state ReserveCommitting.""" reserve_commit_job = ReserveCommitJob(connection_id)