diff --git a/monitoring/monitorlib/clients/flight_planning/client_v1.py b/monitoring/monitorlib/clients/flight_planning/client_v1.py new file mode 100644 index 0000000000..c64eb5039f --- /dev/null +++ b/monitoring/monitorlib/clients/flight_planning/client_v1.py @@ -0,0 +1,175 @@ +import uuid + +from implicitdict import ImplicitDict +from monitoring.monitorlib.clients.flight_planning.client import ( + FlightPlannerClient, + PlanningActivityError, +) +from monitoring.monitorlib.clients.flight_planning.test_preparation import ( + TestPreparationActivityResponse, +) + +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + FlightInfo, + FlightID, + ExecutionStyle, +) +from monitoring.monitorlib.clients.flight_planning.planning import ( + PlanningActivityResponse, +) +from monitoring.monitorlib.fetch import query_and_describe +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.infrastructure import UTMClientSession + +from uas_standards.interuss.automated_testing.flight_planning.v1 import api +from uas_standards.interuss.automated_testing.flight_planning.v1.constants import Scope + + +class V1FlightPlannerClient(FlightPlannerClient): + _session: UTMClientSession + + def __init__(self, session: UTMClientSession): + self._session = session + + def _inject( + self, + flight_plan_id: FlightID, + flight_info: FlightInfo, + execution_style: ExecutionStyle, + ) -> PlanningActivityResponse: + flight_plan = ImplicitDict.parse(flight_info, api.FlightPlan) + req = api.UpsertFlightPlanRequest( + flight_plan=flight_plan, + execution_style=execution_style, + request_id=str(uuid.uuid4()), + ) + + op = api.OPERATIONS[api.OperationID.UpsertFlightPlan] + url = op.path.format(flight_plan_id=flight_plan_id) + query = query_and_describe( + self._session, op.verb, url, json=req, scope=Scope.Plan + ) + if query.status_code != 200 and query.status_code != 201: + raise PlanningActivityError( + f"Attempt to plan flight returned status {query.status_code} rather than 200 as expected", + query, + ) + try: + resp: api.UpsertFlightPlanResponse = ImplicitDict.parse( + query.response.json, api.UpsertFlightPlanResponse + ) + except ValueError as e: + raise PlanningActivityError( + f"Response to plan flight could not be parsed: {str(e)}", query + ) + + response = PlanningActivityResponse( + flight_id=flight_plan_id, + queries=[query], + activity_result=resp.planning_result, + flight_plan_status=resp.flight_plan_status, + ) + return response + + def try_plan_flight( + self, flight_info: FlightInfo, execution_style: ExecutionStyle + ) -> PlanningActivityResponse: + return self._inject(str(uuid.uuid4()), flight_info, execution_style) + + def try_update_flight( + self, + flight_id: FlightID, + updated_flight_info: FlightInfo, + execution_style: ExecutionStyle, + ) -> PlanningActivityResponse: + return self._inject(flight_id, updated_flight_info, execution_style) + + def try_end_flight( + self, flight_id: FlightID, execution_style: ExecutionStyle + ) -> PlanningActivityResponse: + if execution_style != ExecutionStyle.IfAllowed: + raise NotImplementedError( + "Only IfAllowed execution style is currently allowed" + ) + op = api.OPERATIONS[api.OperationID.DeleteFlightPlan] + url = op.path.format(flight_plan_id=flight_id) + query = query_and_describe(self._session, op.verb, url, scope=Scope.Plan) + if query.status_code != 200: + raise PlanningActivityError( + f"Attempt to delete flight plan returned status {query.status_code} rather than 200 as expected", + query, + ) + try: + resp: api.DeleteFlightPlanResponse = ImplicitDict.parse( + query.response.json, api.DeleteFlightPlanResponse + ) + except ValueError as e: + raise PlanningActivityError( + f"Response to delete flight plan could not be parsed: {str(e)}", query + ) + + response = PlanningActivityResponse( + flight_id=flight_id, + queries=[query], + activity_result=resp.planning_result, + flight_plan_status=resp.flight_plan_status, + ) + return response + + def report_readiness(self) -> TestPreparationActivityResponse: + op = api.OPERATIONS[api.OperationID.GetStatus] + query = query_and_describe( + self._session, op.verb, op.path, scope=Scope.DirectAutomatedTest + ) + if query.status_code != 200: + raise PlanningActivityError( + f"Attempt to get interface status returned status {query.status_code} rather than 200 as expected", + query, + ) + try: + resp: api.StatusResponse = ImplicitDict.parse( + query.response.json, api.StatusResponse + ) + except ValueError as e: + raise PlanningActivityError( + f"Response to get interface status could not be parsed: {str(e)}", query + ) + + if resp.status == api.StatusResponseStatus.Ready: + errors = [] + elif resp.status == api.StatusResponseStatus.Starting: + errors = ["Flight planning v1 interface is still starting (not ready)"] + else: + errors = [f"Unrecognized status '{resp.status}'"] + + return TestPreparationActivityResponse(errors=errors, queries=[query]) + + def clear_area(self, area: Volume4D) -> TestPreparationActivityResponse: + req = api.ClearAreaRequest( + request_id=str(uuid.uuid4()), extent=area.to_interuss_scd_api() + ) + + op = api.OPERATIONS[api.OperationID.ClearArea] + query = query_and_describe( + self._session, op.verb, op.path, json=req, scope=Scope.DirectAutomatedTest + ) + if query.status_code != 200: + raise PlanningActivityError( + f"Attempt to clear area returned status {query.status_code} rather than 200 as expected", + query, + ) + try: + resp: api.ClearAreaResponse = ImplicitDict.parse( + query.response.json, api.ClearAreaResponse + ) + except ValueError as e: + raise PlanningActivityError( + f"Response to clear area could not be parsed: {str(e)}", query + ) + + if resp.outcome.success: + errors = None + else: + errors = [f"[{resp.outcome.timestamp}]: {resp.outcome.message}"] + + return TestPreparationActivityResponse(errors=errors, queries=[query]) diff --git a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml index cc74bc3a10..e87ccd0343 100644 --- a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml +++ b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml @@ -40,12 +40,12 @@ v1: auth_adapter: utm_auth specification: flight_planners: - # uss1 is the mock_uss directly exposing scdsc functionality + # uss1 is the mock_uss directly exposing flight planning functionality - participant_id: uss1 - injection_base_url: http://scdsc.uss1.localutm/scdsc - # uss2 is another mock_uss directly exposing scdsc functionality + scd_injection_base_url: http://scdsc.uss1.localutm/scdsc + # uss2 is another mock_uss directly exposing flight planning functionality - participant_id: uss2 - injection_base_url: http://scdsc.uss2.localutm/scdsc + scd_injection_base_url: http://scdsc.uss2.localutm/scdsc # Details of conflicting flights (used in nominal planning scenario) conflicting_flights: diff --git a/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml b/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml index 00111b7788..3070efcafd 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml @@ -119,11 +119,11 @@ all_flight_planners: specification: flight_planners: - participant_id: uss1 - injection_base_url: http://scdsc.uss1.localutm/scdsc + scd_injection_base_url: http://scdsc.uss1.localutm/scdsc local_debug: true - participant_id: uss2 - injection_base_url: http://scdsc.uss2.localutm/scdsc + scd_injection_base_url: http://scdsc.uss2.localutm/scdsc local_debug: true uss1_flight_planner: @@ -134,7 +134,7 @@ uss1_flight_planner: specification: flight_planner: participant_id: uss1 - injection_base_url: http://scdsc.uss1.localutm/scdsc + scd_injection_base_url: http://scdsc.uss1.localutm/scdsc local_debug: true uss2_flight_planner: @@ -145,7 +145,7 @@ uss2_flight_planner: specification: flight_planner: participant_id: uss2 - injection_base_url: http://scdsc.uss2.localutm/scdsc + scd_injection_base_url: http://scdsc.uss2.localutm/scdsc local_debug: true # ===== F3548 ===== diff --git a/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml b/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml index 637b2ff4ef..64971f949e 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml @@ -119,11 +119,11 @@ all_flight_planners: specification: flight_planners: - participant_id: uss1 - injection_base_url: http://localhost:8074/scdsc + scd_injection_base_url: http://localhost:8074/scdsc local_debug: true - participant_id: uss2 - injection_base_url: http://localhost:8094/scdsc + scd_injection_base_url: http://localhost:8094/scdsc local_debug: true uss1_flight_planner: @@ -134,7 +134,7 @@ uss1_flight_planner: specification: flight_planner: participant_id: uss1 - injection_base_url: http://localhost:8074/scdsc + scd_injection_base_url: http://localhost:8074/scdsc local_debug: true uss2_flight_planner: @@ -145,7 +145,7 @@ uss2_flight_planner: specification: flight_planner: participant_id: uss2 - injection_base_url: http://localhost:8094/scdsc + scd_injection_base_url: http://localhost:8094/scdsc local_debug: true # ===== F3548 ===== diff --git a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml index dfb2ec8491..dde36e218e 100644 --- a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml +++ b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml @@ -21,11 +21,11 @@ v1: specification: flight_planners: - participant_id: uss1 - injection_base_url: http://host.docker.internal:8074/scdsc + scd_injection_base_url: http://host.docker.internal:8074/scdsc - participant_id: uss2 - injection_base_url: http://host.docker.internal:8074/scdsc + scd_injection_base_url: http://host.docker.internal:8074/scdsc - participant_id: mock_uss - injection_base_url: http://host.docker.internal:8074/scdsc + scd_injection_base_url: http://host.docker.internal:8074/scdsc mock_uss: resource_type: resources.interuss.mock_uss.client.MockUSSResource dependencies: diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py b/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py index fae5d9eb01..accd64aaac 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py @@ -9,6 +9,9 @@ from monitoring.monitorlib.clients.flight_planning.client_scd import ( SCDFlightPlannerClient, ) +from monitoring.monitorlib.clients.flight_planning.client_v1 import ( + V1FlightPlannerClient, +) from monitoring.monitorlib.clients.flight_planning.flight_info import ( ExecutionStyle, FlightInfo, @@ -47,20 +50,33 @@ class FlightPlannerConfiguration(ImplicitDict): participant_id: str """ID of the flight planner into which test data can be injected""" - injection_base_url: str - """Base URL for the flight planner's implementation of the interfaces/automated-testing/scd/scd.yaml API""" + scd_injection_base_url: Optional[str] + """Base URL for the flight planner's implementation of the interfaces/automated_testing/scd/v1/scd.yaml API""" + + v1_base_url: Optional[str] + """Base URL for the flight planner's implementation of the interfaces/automated_testing/flight_planning/v1/flight_planning.yaml API""" timeout_seconds: Optional[float] = None """Number of seconds to allow for requests to this flight planner. If None, use default.""" def __init__(self, *args, **kwargs): super().__init__(**kwargs) - try: - urlparse(self.injection_base_url) - except ValueError: + if "v1_base_url" not in self and "scd_injection_base_url" not in self: raise ValueError( - "FlightPlannerConfiguration.injection_base_url must be a URL" + "One of `scd_injection_base_url` or `v1_base_url` must be specified" ) + if "scd_injection_base_url" in self and self.scd_injection_base_url: + try: + urlparse(self.scd_injection_base_url) + except ValueError: + raise ValueError( + "FlightPlannerConfiguration.scd_injection_base_url must be a URL" + ) + if "v1_base_url" in self and self.v1_base_url: + try: + urlparse(self.v1_base_url) + except ValueError: + raise ValueError("FlightPlannerConfiguration.v1_base_url must be a URL") class FlightPlanner: @@ -74,17 +90,23 @@ def __init__( auth_adapter: infrastructure.AuthAdapter, ): self.config = config - session = infrastructure.UTMClientSession( - self.config.injection_base_url, auth_adapter, config.timeout_seconds - ) - self.scd_client = SCDFlightPlannerClient(session) + if "scd_injection_base_url" in config and config.scd_injection_base_url: + session = infrastructure.UTMClientSession( + self.config.scd_injection_base_url, auth_adapter, config.timeout_seconds + ) + self.client = SCDFlightPlannerClient(session) + elif "v1_base_url" in config and config.v1_base_url: + session = infrastructure.UTMClientSession( + self.config.v1_base_url, auth_adapter, config.timeout_seconds + ) + self.client = V1FlightPlannerClient(session) # Flights injected by this target. self.created_flight_ids: Set[str] = set() def __repr__(self): return "FlightPlanner({}, {})".format( - self.config.participant_id, self.config.injection_base_url + self.config.participant_id, self.config.scd_injection_base_url ) @property @@ -145,7 +167,7 @@ def request_flight( if not flight_id: try: - resp = self.scd_client.try_plan_flight( + resp = self.client.try_plan_flight( flight_info, ExecutionStyle.IfAllowed ) except PlanningActivityError as e: @@ -153,7 +175,7 @@ def request_flight( flight_id = resp.flight_id else: try: - resp = self.scd_client.try_update_flight( + resp = self.client.try_update_flight( flight_id, flight_info, ExecutionStyle.IfAllowed ) except PlanningActivityError as e: @@ -193,7 +215,7 @@ def cleanup_flight( self, flight_id: str ) -> Tuple[DeleteFlightResponse, fetch.Query]: try: - resp = self.scd_client.try_end_flight(flight_id, ExecutionStyle.IfAllowed) + resp = self.client.try_end_flight(flight_id, ExecutionStyle.IfAllowed) except PlanningActivityError as e: raise QueryError(str(e), e.queries) @@ -214,14 +236,14 @@ def cleanup_flight( def get_readiness(self) -> Tuple[Optional[str], Query]: try: - resp = self.scd_client.report_readiness() + resp = self.client.report_readiness() except PlanningActivityError as e: return str(e), e.queries[0] return None, resp.queries[0] def clear_area(self, extent: Volume4D) -> Tuple[ClearAreaResponse, fetch.Query]: try: - resp = self.scd_client.clear_area(extent) + resp = self.client.clear_area(extent) except PlanningActivityError as e: raise QueryError(str(e), e.queries) success = False if resp.errors else True diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/record_planners.py b/monitoring/uss_qualifier/scenarios/flight_planning/record_planners.py index 39e39d8466..68c4aa2234 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/record_planners.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/record_planners.py @@ -15,7 +15,7 @@ def run(self): self.record_note( "Available flight planners", "\n".join( - f"* {fp.config.participant_id}: {fp.config.injection_base_url}" + f"* {fp.config.participant_id}: {fp.config.scd_injection_base_url}" for fp in self._flight_planners.flight_planners ), ) diff --git a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planner/FlightPlannerConfiguration.json b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planner/FlightPlannerConfiguration.json index 13e2532ed0..e6f7b535be 100644 --- a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planner/FlightPlannerConfiguration.json +++ b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_planner/FlightPlannerConfiguration.json @@ -7,24 +7,33 @@ "description": "Path to content that replaces the $ref", "type": "string" }, - "injection_base_url": { - "description": "Base URL for the flight planner's implementation of the interfaces/automated-testing/scd/scd.yaml API", - "type": "string" - }, "participant_id": { "description": "ID of the flight planner into which test data can be injected", "type": "string" }, + "scd_injection_base_url": { + "description": "Base URL for the flight planner's implementation of the interfaces/automated_testing/scd/v1/scd.yaml API", + "type": [ + "string", + "null" + ] + }, "timeout_seconds": { "description": "Number of seconds to allow for requests to this flight planner. If None, use default.", "type": [ "number", "null" ] + }, + "v1_base_url": { + "description": "Base URL for the flight planner's implementation of the interfaces/automated_testing/flight_planning/v1/flight_planning.yaml API", + "type": [ + "string", + "null" + ] } }, "required": [ - "injection_base_url", "participant_id" ], "type": "object"