diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index aabb03bc50..f9889b6d12 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -7,7 +7,7 @@ from monitoring.monitorlib import infrastructure, fetch from monitoring.monitorlib.fetch import QueryType -from monitoring.monitorlib.scd import SCOPE_SC, SCOPE_AA +from monitoring.monitorlib.scd import SCOPE_SC, SCOPE_AA, SCOPE_CM_SA from monitoring.uss_qualifier.resources.resource import Resource from monitoring.uss_qualifier.resources.communications import AuthAdapterResource from uas_standards.astm.f3548.v21.api import ( @@ -30,6 +30,8 @@ GetOperationalIntentReferenceResponse, OPERATIONS, OperationID, + GetOperationalIntentTelemetryResponse, + VehicleTelemetry, ) # A base URL for a USS that is not expected to be ever called @@ -164,6 +166,29 @@ def get_full_op_intent_without_validation( return result, query + def get_op_intent_telemetry( + self, + op_intent_ref: OperationalIntentReference, + uss_participant_id: Optional[str] = None, + ) -> Tuple[Optional[VehicleTelemetry], fetch.Query]: + op = OPERATIONS[OperationID.GetOperationalIntentTelemetry] + query = fetch.query_and_describe( + self.client, + op.verb, + f"{op_intent_ref.uss_base_url}{op.path.format(entityid=op_intent_ref.id)}", + QueryType.F3548v21USSGetOperationalIntentTelemetry, + uss_participant_id, + scope=SCOPE_CM_SA, + ) + if query.status_code == 200: + result: GetOperationalIntentTelemetryResponse = ImplicitDict.parse( + query.response.json, GetOperationalIntentTelemetryResponse + ) + telemetry = result.telemetry if "telemetry" in result else None + return telemetry, query + else: + return None, query + def put_op_intent( self, extents: List[Volume4D], diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index a760e0943b..f46b8979a1 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -5,7 +5,6 @@ from implicitdict import ImplicitDict from monitoring.monitorlib import schema_validation, fetch from monitoring.monitorlib.clients.flight_planning.client import FlightPlannerClient -from monitoring.monitorlib.geotemporal import Volume4DCollection from uas_standards.astm.f3548.v21.api import ( OperationalIntentState, Volume4D, @@ -31,11 +30,6 @@ ) from uas_standards.interuss.automated_testing.scd.v1.api import InjectFlightRequest -OI_DATA_FORMAT = "Operational intent details data format" -OI_CORRECT_DETAILS = "Correct operational intent details" -OFF_NOM_VOLS = "Off-nominal volumes" -VERTICES = "Vertices" - class OpIntentValidator(object): """ @@ -165,109 +159,26 @@ def expect_shared( :returns: the shared operational intent reference. None if skipped because not found. """ - oi_ref = self._operational_intent_shared_check(flight_intent, skip_if_not_found) - - oi_full, oi_full_query = self._dss.get_full_op_intent( - oi_ref, self._flight_planner.participant_id - ) - self._scenario.record_query(oi_full_query) - self._operational_intent_retrievable_check(oi_full_query, oi_ref.id) - - validation_failures = self._evaluate_op_intent_validation(oi_full_query) + if isinstance(flight_intent, InjectFlightRequest): + flight_intent = FlightInfo.from_scd_inject_flight_request(flight_intent) - with self._scenario.check( - OI_DATA_FORMAT, - [self._flight_planner.participant_id], - ) as check: - data_format_fail = ( - self._expected_validation_failure_found( - validation_failures, OpIntentValidationFailureType.DataFormat - ) - if validation_failures - else None - ) - if data_format_fail: - errors = data_format_fail.errors - check.record_failed( - summary="Operational intent details response failed schema validation", - severity=Severity.Medium, - details="The response received from querying operational intent details failed validation against the required OpenAPI schema:\n" - + "\n".join( - f"At {e.json_path} in the response: {e.message}" for e in errors - ), - query_timestamps=[oi_full_query.request.timestamp], - ) - - with self._scenario.check( - OI_CORRECT_DETAILS, [self._flight_planner.participant_id] - ) as check: - priority = ( - flight_intent.operational_intent.priority - if isinstance(flight_intent, InjectFlightRequest) - else flight_intent.astm_f3548_21.priority - ) - if isinstance(flight_intent, InjectFlightRequest): - priority = flight_intent.operational_intent.priority - vols = Volume4DCollection.from_interuss_scd_api( - flight_intent.operational_intent.volumes - + flight_intent.operational_intent.off_nominal_volumes - ) - elif isinstance(flight_intent, FlightInfo): - priority = flight_intent.astm_f3548_21.priority - vols = flight_intent.basic_information.area - - error_text = validate_op_intent_details( - oi_full.details, - priority, - vols.bounding_volume.to_f3548v21(), - ) - if error_text: - check.record_failed( - summary="Operational intent details do not match user flight intent", - severity=Severity.High, - details=error_text, - query_timestamps=[oi_full_query.request.timestamp], - ) + self._begin_step() + oi_ref = self._operational_intent_shared_check(flight_intent, skip_if_not_found) + if oi_ref is None: + self._scenario.end_test_step() + return None - with self._scenario.check( - OFF_NOM_VOLS, [self._flight_planner.participant_id] - ) as check: - off_nom_vol_fail = ( - self._expected_validation_failure_found( - validation_failures, - OpIntentValidationFailureType.NominalWithOffNominalVolumes, - ) - if validation_failures - else None - ) - if off_nom_vol_fail: - check.record_failed( - summary="Accepted or Activated operational intents are not allowed off-nominal volumes", - severity=Severity.Medium, - details=off_nom_vol_fail.error_text, - query_timestamps=[oi_full_query.request.timestamp], - ) + self._check_op_intent_details(flight_intent, oi_ref) - with self._scenario.check( - VERTICES, [self._flight_planner.participant_id] - ) as check: - vertices_fail = ( - self._expected_validation_failure_found( - validation_failures, OpIntentValidationFailureType.VertexCount - ) - if validation_failures - else None - ) - if vertices_fail: - check.record_failed( - summary="Too many vertices", - severity=Severity.Medium, - details=vertices_fail.error_text, - query_timestamps=[oi_full_query.request.timestamp], - ) + # Check telemetry if intent is off-nominal + if flight_intent.basic_information.uas_state in { + UasState.OffNominal, + UasState.Contingent, + }: + self._check_op_intent_telemetry(oi_ref) self._scenario.end_test_step() - return oi_full.reference + return oi_ref def expect_shared_with_invalid_data( self, @@ -287,8 +198,14 @@ def expect_shared_with_invalid_data( :returns: the shared operational intent reference. None if skipped because not found. """ + if isinstance(flight_intent, InjectFlightRequest): + flight_intent = FlightInfo.from_scd_inject_flight_request(flight_intent) + self._begin_step() oi_ref = self._operational_intent_shared_check(flight_intent, skip_if_not_found) + if oi_ref is None: + self._scenario.end_test_step() + return None goidr_json, oi_full_query = self._dss.get_full_op_intent_without_validation( oi_ref, self._flight_planner.participant_id @@ -335,11 +252,9 @@ def _operational_intent_retrievable_check( def _operational_intent_shared_check( self, - flight_intent: Union[InjectFlightRequest | FlightInfo], + flight_intent: FlightInfo, skip_if_not_found: bool, - ) -> OperationalIntentReference: - - self._begin_step() + ) -> Optional[OperationalIntentReference]: with self._scenario.check( "Operational intent shared correctly", [self._flight_planner.participant_id] @@ -360,7 +275,6 @@ def _operational_intent_shared_check( f"{self._flight_planner.participant_id} skipped step", f"No new operational intent was found in DSS, instructed to skip test step '{self._test_step}'.", ) - self._scenario.end_test_step() return None oi_ref = self._new_oi_ref @@ -373,23 +287,11 @@ def _operational_intent_shared_check( if modified_oi_ref is None: if not skip_if_not_found: if ( - (isinstance(flight_intent, InjectFlightRequest)) - and ( - flight_intent.operational_intent.state - == OperationalIntentState.Activated - ) - ) or ( - isinstance(flight_intent, FlightInfo) - and ( - ( - flight_intent.basic_information.uas_state - == UasState.Nominal - ) - and ( - flight_intent.basic_information.usage_state - == AirspaceUsageState.InUse - ) - ) + flight_intent.basic_information.uas_state + == UasState.Nominal + ) and ( + flight_intent.basic_information.usage_state + == AirspaceUsageState.InUse ): with self._scenario.check( "Operational intent for active flight not deleted", @@ -415,7 +317,6 @@ def _operational_intent_shared_check( f"{self._flight_planner.participant_id} skipped step", f"Operational intent reference with ID {self._orig_oi_ref.id} not found in DSS, instructed to skip test step '{self._test_step}'.", ) - self._scenario.end_test_step() return None oi_ref = modified_oi_ref @@ -432,6 +333,118 @@ def _operational_intent_shared_check( return oi_ref + def _check_op_intent_details( + self, flight_intent: FlightInfo, oi_ref: OperationalIntentReference + ): + oi_full, oi_full_query = self._dss.get_full_op_intent( + oi_ref, self._flight_planner.participant_id + ) + self._scenario.record_query(oi_full_query) + self._operational_intent_retrievable_check(oi_full_query, oi_ref.id) + + validation_failures = self._evaluate_op_intent_validation(oi_full_query) + with self._scenario.check( + "Operational intent details data format", + [self._flight_planner.participant_id], + ) as check: + data_format_fail = ( + self._expected_validation_failure_found( + validation_failures, OpIntentValidationFailureType.DataFormat + ) + if validation_failures + else None + ) + if data_format_fail: + errors = data_format_fail.errors + check.record_failed( + summary="Operational intent details response failed schema validation", + severity=Severity.Medium, + details="The response received from querying operational intent details failed validation against the required OpenAPI schema:\n" + + "\n".join( + f"At {e.json_path} in the response: {e.message}" for e in errors + ), + query_timestamps=[oi_full_query.request.timestamp], + ) + + with self._scenario.check( + "Correct operational intent details", [self._flight_planner.participant_id] + ) as check: + error_text = validate_op_intent_details( + oi_full.details, + flight_intent.astm_f3548_21.priority, + flight_intent.basic_information.area.bounding_volume.to_f3548v21(), + ) + if error_text: + check.record_failed( + summary="Operational intent details do not match user flight intent", + severity=Severity.High, + details=error_text, + query_timestamps=[oi_full_query.request.timestamp], + ) + + with self._scenario.check( + "Off-nominal volumes", [self._flight_planner.participant_id] + ) as check: + off_nom_vol_fail = ( + self._expected_validation_failure_found( + validation_failures, + OpIntentValidationFailureType.NominalWithOffNominalVolumes, + ) + if validation_failures + else None + ) + if off_nom_vol_fail: + check.record_failed( + summary="Accepted or Activated operational intents are not allowed off-nominal volumes", + severity=Severity.Medium, + details=off_nom_vol_fail.error_text, + query_timestamps=[oi_full_query.request.timestamp], + ) + + with self._scenario.check( + "Vertices", [self._flight_planner.participant_id] + ) as check: + vertices_fail = ( + self._expected_validation_failure_found( + validation_failures, OpIntentValidationFailureType.VertexCount + ) + if validation_failures + else None + ) + if vertices_fail: + check.record_failed( + summary="Too many vertices", + severity=Severity.Medium, + details=vertices_fail.error_text, + query_timestamps=[oi_full_query.request.timestamp], + ) + + def _check_op_intent_telemetry(self, oi_ref: OperationalIntentReference): + oi_tel, oi_tel_query = self._dss.get_op_intent_telemetry( + oi_ref, self._flight_planner.participant_id + ) + self._scenario.record_query(oi_tel_query) + + with self._scenario.check( + "Operational intent telemetry retrievable", + [self._flight_planner.participant_id], + ) as check: + if oi_tel_query.status_code not in {200, 412}: + check.record_failed( + summary="Operational intent telemetry could not be retrieved from USS", + severity=Severity.High, + details=f"Received status code {oi_tel_query.status_code} from {self._flight_planner.participant_id} when querying for telemetry of operational intent {oi_ref.id}", + query_timestamps=[oi_tel_query.request.timestamp], + ) + + if oi_tel is None: + check.record_failed( + summary="Warning (not a failure): USS indicated that no operational intent telemetry was available", + severity=Severity.Low, + details=f"Received status code {oi_tel_query.status_code} from {self._flight_planner.participant_id} when querying for details of operational intent {oi_ref.id}", + query_timestamps=[oi_tel_query.request.timestamp], + ) + def _evaluate_op_intent_validation( self, oi_full_query: fetch.Query ) -> Set[OpIntentValidationFailure]: @@ -485,13 +498,17 @@ def volume_vertices(v4): f"Operational intent {oi_full.reference.id} had too many total vertices - {n_vertices}", ) validation_failures.add( - validation_failure_type=OpIntentValidationFailureType.VertexCount, - error_text=details, + OpIntentValidationFailure( + validation_failure_type=OpIntentValidationFailureType.VertexCount, + error_text=details, + ) ) except (KeyError, ValueError) as e: validation_failures.add( - validation_failure_type=OpIntentValidationFailureType.DataFormat, - error_text=e, + OpIntentValidationFailure( + validation_failure_type=OpIntentValidationFailureType.DataFormat, + error_text=e, + ) ) return validation_failures diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md index f7e77daf67..3cad3f78f9 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md @@ -35,3 +35,9 @@ If the operational intent details reported by the USS do not match the user's fl ## ⚠️ Vertices check **[astm.f3548.v21.OPIN0020](../../../requirements/astm/f3548/v21.md)** + +## 🛑 Operational intent telemetry retrievable check + +If the operational intent is in an off-nominal state and that its telemetry cannot be retrieved from the USS, this check will fail per **[astm.f3548.v21.SCD0100](../../../requirements/astm/f3548/v21.md)**. + +The USS may explicitly indicate that no telemetry is available for this operational intent, in which case, as a warning, this check will fail with a low severity. diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index 69f6505dd9..4f55c911c0 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -31,7 +31,7 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Off-Nominal planning: down USS with equal priority conflicts not permitted
Validation of operational intents @@ -161,6 +161,11 @@ Implemented Data Validation of GET operational intents by USS + + SCD0100 + Implemented + Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + USS0005 Implemented diff --git a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md index f1fc60c97e..100289dc27 100644 --- a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md +++ b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md @@ -18,7 +18,7 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Off-Nominal planning: down USS with equal priority conflicts not permitted
Validation of operational intents @@ -148,6 +148,11 @@ Implemented Data Validation of GET operational intents by USS + + SCD0100 + Implemented + Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + USS0005 Implemented diff --git a/monitoring/uss_qualifier/suites/uspace/flight_auth.md b/monitoring/uss_qualifier/suites/uspace/flight_auth.md index 21ea7f935c..2af8101183 100644 --- a/monitoring/uss_qualifier/suites/uspace/flight_auth.md +++ b/monitoring/uss_qualifier/suites/uspace/flight_auth.md @@ -19,7 +19,7 @@ Checked in - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Off-Nominal planning: down USS with equal priority conflicts not permitted
Validation of operational intents @@ -149,6 +149,11 @@ Implemented Data Validation of GET operational intents by USS + + SCD0100 + Implemented + Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + USS0005 Implemented diff --git a/monitoring/uss_qualifier/suites/uspace/required_services.md b/monitoring/uss_qualifier/suites/uspace/required_services.md index c4a3ed480a..b51cf67cfa 100644 --- a/monitoring/uss_qualifier/suites/uspace/required_services.md +++ b/monitoring/uss_qualifier/suites/uspace/required_services.md @@ -454,7 +454,7 @@ ASTM NetRID DSS: Concurrent Requests
ASTM NetRID DSS: ISA Expiry
ASTM NetRID DSS: ISA Subscription Interactions
ASTM NetRID DSS: Simple ISA
ASTM NetRID DSS: Submitted ISA Validations
ASTM NetRID DSS: Subscription Simple
ASTM NetRID DSS: Subscription Validation
ASTM NetRID DSS: Token Validation - astm
.f3548
.v21
+ astm
.f3548
.v21
DSS0005 Implemented ASTM F3548 flight planners preparation
Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Off-Nominal planning: down USS with equal priority conflicts not permitted
Validation of operational intents @@ -584,6 +584,11 @@ Implemented Data Validation of GET operational intents by USS + + SCD0100 + Implemented + Data Validation of GET operational intents by USS
Nominal planning: conflict with higher priority
Nominal planning: not permitted conflict with equal priority
Off-Nominal planning: down USS
Validation of operational intents + USS0005 Implemented