diff --git a/monitoring/uss_qualifier/configurations/dev/dss_probing.yaml b/monitoring/uss_qualifier/configurations/dev/dss_probing.yaml index 6ef5c7f17b..daaeefad8b 100644 --- a/monitoring/uss_qualifier/configurations/dev/dss_probing.yaml +++ b/monitoring/uss_qualifier/configurations/dev/dss_probing.yaml @@ -39,6 +39,18 @@ v1: rid_version: F3411-22a base_url: http://dss.uss2.localutm/rid/v2 has_private_address: true + utm_dss_instances_v21: # TODO discuss if this is the right place for this? The rest seems netrid specific + resource_type: resources.astm.f3548.DSSInstancesResource + dependencies: + auth_adapter: utm_auth + specification: + dss_instances: + - participant_id: uss1 + base_url: http://dss.uss1.localutm/rid/v2 # TODO: probably different base urls here? + has_private_address: true + - participant_id: uss2 + base_url: http://dss.uss2.localutm/rid/v2 + has_private_address: true id_generator: resource_type: resources.interuss.IDGeneratorResource dependencies: @@ -70,6 +82,7 @@ v1: resources: f3411v19_dss_instances: netrid_dss_instances_v19 f3411v22a_dss_instances: netrid_dss_instances_v22a + f3548v21_dss_instances: utm_dss_instances_v21 id_generator: id_generator service_area: service_area artifacts: diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index ed937b0694..4a5faf19b7 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -1,4 +1,4 @@ -from typing import Tuple, List +from typing import Tuple, List, Optional from urllib.parse import urlparse from implicitdict import ImplicitDict @@ -24,6 +24,9 @@ class DSSInstanceSpecification(ImplicitDict): base_url: str """Base URL for the DSS instance according to the ASTM F3548-21 API""" + has_private_address: Optional[bool] + """Whether this DSS instance is expected to have a private address that is not publicly addressable.""" + def __init__(self, *args, **kwargs): super().__init__(**kwargs) try: @@ -34,16 +37,20 @@ def __init__(self, *args, **kwargs): class DSSInstance(object): participant_id: str + has_private_address: bool = False client: infrastructure.UTMClientSession def __init__( self, participant_id: str, base_url: str, + has_private_address: Optional[bool], auth_adapter: infrastructure.AuthAdapter, ): self.participant_id = participant_id self._base_url = base_url + if has_private_address is not None: + self.has_private_address = has_private_address self.client = infrastructure.UTMClientSession(base_url, auth_adapter) def find_op_intent( @@ -92,5 +99,28 @@ def __init__( auth_adapter: AuthAdapterResource, ): self.dss = DSSInstance( - specification.participant_id, specification.base_url, auth_adapter.adapter + specification.participant_id, specification.base_url, specification.has_private_address, auth_adapter.adapter ) + + +class DSSInstancesSpecification(ImplicitDict): + dss_instances: List[DSSInstanceSpecification] + + +class DSSInstancesResource(Resource[DSSInstancesSpecification]): + dss_instances: List[DSSInstance] + + def __init__( + self, + specification: DSSInstancesSpecification, + auth_adapter: AuthAdapterResource, + ): + self.dss_instances = [ + DSSInstance( + s.participant_id, + s.base_url, + s.has_private_address, + auth_adapter.adapter, + ) + for s in specification.dss_instances + ] diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/TODO.md b/monitoring/uss_qualifier/scenarios/astm/utm/TODO.md new file mode 100644 index 0000000000..94aeb3c76b --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/TODO.md @@ -0,0 +1 @@ +TODO DSS0300 comes in here somewhere diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.md b/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.md new file mode 100644 index 0000000000..16ff85c363 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.md @@ -0,0 +1,31 @@ +# ASTM F3548-21 UTM DSS interoperability test scenario + +## Overview + +TODO: Complete with details once we check more than the prerequisites. + +This scenario currently only checks that all specified DSS instances are publicly addressable and reachable. + +## Resources + +### primary_dss_instance + +A resources.astm.f3548.v21.DSSInstanceResource containing the "primary" DSS instance for this scenario. + +### all_dss_instances + +A resources.astm.f3548.v21.DSSInstancesResource containing at least two DSS instances complying with ASTM F3548-21. + +## Prerequisites test case + +### Test environment requirements test step + +#### DSS instance is publicly addressable check + +As per **[astm.f3548.v21.DSS0300](../../../requirements/astm/f3548/v21.md)** the DSS instance should be publicly addressable. +As such, this check will fail if the resolved IP of the DSS host is a private IP address, unless that is explicitly +expected. + +#### DSS instance is reachable check +As per **[astm.f3548.v21.DSS0300](../../../requirements/astm/f3548/v21.md)** the DSS instance should be publicly addressable. +As such, this check will fail if the DSS is not reachable with a dummy query. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.py new file mode 100644 index 0000000000..e5fa560234 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss_interoperability.py @@ -0,0 +1,119 @@ +import ipaddress +import socket +import time +import uuid +from dataclasses import dataclass +import datetime +from enum import Enum +from typing import List, Dict, Optional +from urllib.parse import urlparse + +import s2sphere + +from monitoring.monitorlib.fetch.rid import ISA +from monitoring.uss_qualifier.common_data_definitions import Severity +from monitoring.uss_qualifier.resources.astm.f3411.dss import ( + DSSInstancesResource, + DSSInstanceResource, +) +from monitoring.uss_qualifier.scenarios.astm.netrid.dss_wrapper import DSSWrapper +from monitoring.uss_qualifier.scenarios.scenario import GenericTestScenario + +VERTICES: List[s2sphere.LatLng] = [ + s2sphere.LatLng.from_degrees(lng=130.6205, lat=-23.6558), + s2sphere.LatLng.from_degrees(lng=130.6301, lat=-23.6898), + s2sphere.LatLng.from_degrees(lng=130.6700, lat=-23.6709), + s2sphere.LatLng.from_degrees(lng=130.6466, lat=-23.6407), +] +SHORT_WAIT_SEC = 5 + + +class EntityType(str, Enum): + ISA = "ISA" + Sub = "Sub" + + +@dataclass +class TestEntity(object): + type: EntityType + uuid: str + version: Optional[str] = None + + +class DSSInteroperability(GenericTestScenario): + _dss_primary: DSSWrapper + _dss_others: List[DSSWrapper] + _context: Dict[str, TestEntity] + + def __init__( + self, + primary_dss_instance: DSSInstanceResource, + all_dss_instances: DSSInstancesResource, + ): + super().__init__() + self._dss_primary = DSSWrapper(self, primary_dss_instance.dss_instance) + self._dss_others = [ + DSSWrapper(self, dss) + for dss in all_dss_instances.dss_instances + if not dss.is_same_as(primary_dss_instance.dss_instance) + ] + self._context: Dict[str, TestEntity] = {} + + def _new_isa(self, name: str) -> TestEntity: + self._context[name] = TestEntity(EntityType.ISA, str(uuid.uuid4())) + return self._context[name] + + def _new_sub(self, name: str) -> TestEntity: + self._context[name] = TestEntity(EntityType.Sub, str(uuid.uuid4())) + return self._context[name] + + def _get_entities_by_prefix(self, prefix: str) -> Dict[str, TestEntity]: + all_entities = dict() + for name, entity in self._context.items(): + if name.startswith(prefix): + all_entities[entity.uuid] = entity + return all_entities + + def run(self): + + self.record_note("dss_instances", f"Provided DSS instances: {[self._dss_primary] + self._dss_others}") + self.begin_test_scenario() + + self.begin_test_case("Prerequisites") + + self.begin_test_step("Test environment requirements") + self._test_env_reqs() + self.end_test_step() + + self.end_test_case() + + self.end_test_scenario() + + def _test_env_reqs(self): + for dss in [self._dss_primary] + self._dss_others: + with self.check( + "DSS instance is publicly addressable", [dss.participant_id] + ) as check: + parsed_url = urlparse(dss.base_url) + ip_addr = socket.gethostbyname(parsed_url.hostname) + + if dss.has_private_address: + self.record_note( + f"{dss.participant_id}_private_address", + f"DSS instance (URL: {dss.base_url}, netloc: {parsed_url.netloc}, resolved IP: {ip_addr}) is declared as explicitly having a private address, skipping check", + ) + elif ipaddress.ip_address(ip_addr).is_private: + check.record_failed( + summary=f"DSS host {parsed_url.netloc} is not publicly addressable", + severity=Severity.Medium, + participants=[dss.participant_id], + details=f"DSS (URL: {dss.base_url}, netloc: {parsed_url.netloc}, resolved IP: {ip_addr}) is not publicly addressable", + ) + + with self.check("DSS instance is reachable", [dss.participant_id]) as check: + # dummy search query + dss.search_subs(check, VERTICES) + + def cleanup(self): + self.begin_cleanup() + self.end_cleanup() diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml index 51380c2746..298bd41c79 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml @@ -2,90 +2,96 @@ name: ASTM F3548-21 resources: flight_planners: resources.flight_planning.FlightPlannersResource dss: resources.astm.f3548.v21.DSSInstanceResource + all_dss_instances: resources.astm.f3548.DSSInstancesResource? conflicting_flights: resources.flight_planning.FlightIntentsResource priority_preemption_flights: resources.flight_planning.FlightIntentsResource invalid_flight_intents: resources.flight_planning.FlightIntentsResource nominal_planning_selector: resources.flight_planning.FlightPlannerCombinationSelectorResource? priority_planning_selector: resources.flight_planning.FlightPlannerCombinationSelectorResource? actions: -- action_generator: - generator_type: action_generators.flight_planning.FlightPlannerCombinations +- test_scenario: # TODO confirm this should go here + scenario_type: scenarios.astm.utm.DSSInteroperability resources: - flight_planners: flight_planners - flight_intent_validation_selector: flight_intent_validation_selector? - invalid_flight_intents: invalid_flight_intents - dss: dss - specification: - action_to_repeat: - test_scenario: - scenario_type: scenarios.astm.utm.FlightIntentValidation - resources: - flight_intents: invalid_flight_intents - tested_uss: uss1 - dss: dss - on_failure: Continue - combination_selector_source: flight_intent_validation_selector - flight_planners_source: flight_planners - roles: - - uss1 - on_failure: Continue -- action_generator: - generator_type: action_generators.flight_planning.FlightPlannerCombinations - resources: - flight_planners: flight_planners - priority_planning_selector: priority_planning_selector? - priority_preemption_flights: priority_preemption_flights - dss: dss - specification: - action_to_repeat: - test_scenario: - scenario_type: scenarios.astm.utm.ConflictHigherPriority - resources: - flight_intents: priority_preemption_flights - tested_uss: uss1 - control_uss: uss2 - dss: dss - on_failure: Continue - combination_selector_source: priority_planning_selector - flight_planners_source: flight_planners - roles: - - uss1 - - uss2 - on_failure: Continue -- action_generator: - generator_type: action_generators.flight_planning.FlightPlannerCombinations - resources: - flight_planners: flight_planners - priority_planning_selector: priority_planning_selector? - priority_preemption_flights: priority_preemption_flights - dss: dss - specification: - action_to_repeat: - test_scenario: - scenario_type: scenarios.astm.utm.ConflictEqualPriorityNotPermitted - resources: - flight_intents: priority_preemption_flights - tested_uss: uss1 - control_uss: uss2 - dss: dss - on_failure: Continue - combination_selector_source: priority_planning_selector - flight_planners_source: flight_planners - roles: - - uss1 - - uss2 - on_failure: Continue -participant_verifiable_capabilities: - - id: scd - name: Strategic Conflict Detection - description: Participant fulfills testable requirements necessary to perform the Strategic Conflict Detection role. - verification_condition: - requirements_checked: - checked: - requirement_sets: - - "astm.f3548.v21.scd#Automated verification" - - id: no_failures - name: Fails No Existing Tests - description: Until testing coverage is sufficiently complete to verify all intended requirements for particular roles, this capability may be useful in communicating that the participant has passed all existing applicable tests for their role and capabilities. This capability should be removed once test coverage extends to all intended requirements. - verification_condition: - no_failed_checks: {} + primary_dss_instance: dss + all_dss_instances: all_dss_instances +#- action_generator: +# generator_type: action_generators.flight_planning.FlightPlannerCombinations +# resources: +# flight_planners: flight_planners +# flight_intent_validation_selector: flight_intent_validation_selector? +# invalid_flight_intents: invalid_flight_intents +# dss: dss +# specification: +# action_to_repeat: +# test_scenario: +# scenario_type: scenarios.astm.utm.FlightIntentValidation +# resources: +# flight_intents: invalid_flight_intents +# tested_uss: uss1 +# dss: dss +# on_failure: Continue +# combination_selector_source: flight_intent_validation_selector +# flight_planners_source: flight_planners +# roles: +# - uss1 +# on_failure: Continue +#- action_generator: +# generator_type: action_generators.flight_planning.FlightPlannerCombinations +# resources: +# flight_planners: flight_planners +# priority_planning_selector: priority_planning_selector? +# priority_preemption_flights: priority_preemption_flights +# dss: dss +# specification: +# action_to_repeat: +# test_scenario: +# scenario_type: scenarios.astm.utm.ConflictHigherPriority +# resources: +# flight_intents: priority_preemption_flights +# tested_uss: uss1 +# control_uss: uss2 +# dss: dss +# on_failure: Continue +# combination_selector_source: priority_planning_selector +# flight_planners_source: flight_planners +# roles: +# - uss1 +# - uss2 +# on_failure: Continue +#- action_generator: +# generator_type: action_generators.flight_planning.FlightPlannerCombinations +# resources: +# flight_planners: flight_planners +# priority_planning_selector: priority_planning_selector? +# priority_preemption_flights: priority_preemption_flights +# dss: dss +# specification: +# action_to_repeat: +# test_scenario: +# scenario_type: scenarios.astm.utm.ConflictEqualPriorityNotPermitted +# resources: +# flight_intents: priority_preemption_flights +# tested_uss: uss1 +# control_uss: uss2 +# dss: dss +# on_failure: Continue +# combination_selector_source: priority_planning_selector +# flight_planners_source: flight_planners +# roles: +# - uss1 +# - uss2 +# on_failure: Continue +#participant_verifiable_capabilities: +# - id: scd +# name: Strategic Conflict Detection +# description: Participant fulfills testable requirements necessary to perform the Strategic Conflict Detection role. +# verification_condition: +# requirements_checked: +# checked: +# requirement_sets: +# - "astm.f3548.v21.scd#Automated verification" +# - id: no_failures +# name: Fails No Existing Tests +# description: Until testing coverage is sufficiently complete to verify all intended requirements for particular roles, this capability may be useful in communicating that the participant has passed all existing applicable tests for their role and capabilities. This capability should be removed once test coverage extends to all intended requirements. +# verification_condition: +# no_failed_checks: {} diff --git a/monitoring/uss_qualifier/suites/interuss/dss/all_tests.yaml b/monitoring/uss_qualifier/suites/interuss/dss/all_tests.yaml index b8fad053be..0c33ce19c7 100644 --- a/monitoring/uss_qualifier/suites/interuss/dss/all_tests.yaml +++ b/monitoring/uss_qualifier/suites/interuss/dss/all_tests.yaml @@ -2,6 +2,7 @@ name: ASTM DSS tests resources: f3411v19_dss_instances: resources.astm.f3411.DSSInstancesResource f3411v22a_dss_instances: resources.astm.f3411.DSSInstancesResource + f3548v21_dss_instances: resources.astm.f3548.v21.DSSInstancesResource id_generator: resources.interuss.IDGeneratorResource service_area: resources.netrid.ServiceAreaResource actions: