diff --git a/monitoring/mock_uss/README.md b/monitoring/mock_uss/README.md index 91488cc899..f770ba3e0e 100644 --- a/monitoring/mock_uss/README.md +++ b/monitoring/mock_uss/README.md @@ -18,9 +18,10 @@ The available functionality sets are: * [`msgsigning`](msgsigning): [IETF HTTP Message Signatures](https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/) * [`riddp`](riddp): Remote ID Display Provider * [`ridsp`](ridsp): Remote ID Service Provider -* [`scdsc`](scdsc): ASTM F3548 strategic coordinator +* `scdsc`: Combination of [ASTM F3548-21](f3548v21) strategic conflict detection and [scd flight injection](scd_injection) +* [`flight_planning`](flight_planning): Exposes [InterUSS flight_planning automated testing API](https://github.com/interuss/automated_testing_interfaces/tree/main/flight_planning) * [`tracer`](tracer): Interoperability ecosystem tracer logger -* [`interaction_logging`](interaction_logging): Enables logging of the [interuss](https://github.com/astm-utm/Protocol/blob/master/utm.yaml) interactions between mock_uss and other uss participants +* [`interaction_logging`](interaction_logging): Enables logging of interactions between mock_uss and other uss participants ## Local deployment diff --git a/monitoring/mock_uss/__init__.py b/monitoring/mock_uss/__init__.py index 7733a0829b..4aa3c4ab35 100644 --- a/monitoring/mock_uss/__init__.py +++ b/monitoring/mock_uss/__init__.py @@ -81,8 +81,8 @@ def require_config_value(config_key: str) -> None: if SERVICE_SCDSC in webapp.config[config.KEY_SERVICES]: enabled_services.add(SERVICE_SCDSC) - from monitoring.mock_uss import scdsc - from monitoring.mock_uss.scdsc import routes as scdsc_routes + from monitoring.mock_uss.f3548v21 import routes_scd + from monitoring.mock_uss.scd_injection import routes as scd_injection_routes if SERVICE_MESSAGESIGNING in webapp.config[config.KEY_SERVICES]: enabled_services.add(SERVICE_MESSAGESIGNING) diff --git a/monitoring/mock_uss/f3548v21/README.md b/monitoring/mock_uss/f3548v21/README.md new file mode 100644 index 0000000000..9f7137ca84 --- /dev/null +++ b/monitoring/mock_uss/f3548v21/README.md @@ -0,0 +1,3 @@ +# mock_uss: ASTM F3548-21 + +[ASTM F3548-21](http://astm.org/f3548-21.html) standardizes UTM interoperability between USSs to achieve strategic coordination and communicate constraints. This folder enables [mock_uss](..) to comply with the Strategic Conflict Detection requirements from that standard. diff --git a/monitoring/mock_uss/scdsc/__init__.py b/monitoring/mock_uss/f3548v21/__init__.py similarity index 100% rename from monitoring/mock_uss/scdsc/__init__.py rename to monitoring/mock_uss/f3548v21/__init__.py diff --git a/monitoring/mock_uss/scdsc/flight_planning.py b/monitoring/mock_uss/f3548v21/flight_planning.py similarity index 88% rename from monitoring/mock_uss/scdsc/flight_planning.py rename to monitoring/mock_uss/f3548v21/flight_planning.py index d0c9457fa9..c477ce1067 100644 --- a/monitoring/mock_uss/scdsc/flight_planning.py +++ b/monitoring/mock_uss/f3548v21/flight_planning.py @@ -2,14 +2,16 @@ from typing import Optional, List, Callable import arrow + +from monitoring.uss_qualifier.resources.overrides import apply_overrides from uas_standards.astm.f3548.v21 import api as f3548_v21 +from uas_standards.astm.f3548.v21.api import OperationalIntentDetails, OperationalIntent from uas_standards.astm.f3548.v21.constants import OiMaxVertices, OiMaxPlanHorizonDays from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api -from monitoring.mock_uss.scdsc.database import FlightRecord +from monitoring.mock_uss.flights.database import FlightRecord from monitoring.monitorlib.geotemporal import Volume4DCollection from monitoring.monitorlib.locality import Locality -from monitoring.monitorlib.uspace import problems_with_flight_authorisation from uas_standards.interuss.automated_testing.scd.v1.api import OperationalIntentState @@ -17,19 +19,13 @@ class PlanningError(Exception): pass -def validate_request(req_body: scd_api.InjectFlightRequest, locality: Locality) -> None: +def validate_request(req_body: scd_api.InjectFlightRequest) -> None: """Raise a PlannerError if the request is not valid. Args: req_body: Information about the requested flight. locality: Jurisdictional requirements which the mock_uss should follow. """ - if locality.is_uspace_applicable(): - # Validate flight authorisation - problems = problems_with_flight_authorisation(req_body.flight_authorisation) - if problems: - raise PlanningError(", ".join(problems)) - # Validate max number of vertices nb_vertices = 0 for volume in ( @@ -219,3 +215,23 @@ def op_intent_transition_valid( else: return False + + +def op_intent_from_flightrecord(flight: FlightRecord, method: str) -> OperationalIntent: + ref = flight.op_intent.reference + details = OperationalIntentDetails( + volumes=flight.op_intent.details.volumes, + off_nominal_volumes=flight.op_intent.details.off_nominal_volumes, + priority=flight.op_intent.details.priority, + ) + op_intent = OperationalIntent(reference=ref, details=details) + if flight.mod_op_sharing_behavior: + mod_op_sharing_behavior = flight.mod_op_sharing_behavior + if mod_op_sharing_behavior.modify_sharing_methods is not None: + if method not in mod_op_sharing_behavior.modify_sharing_methods: + return op_intent + op_intent = apply_overrides( + op_intent, mod_op_sharing_behavior.modify_fields, parse_result=False + ) + + return op_intent diff --git a/monitoring/mock_uss/scdsc/routes_scdsc.py b/monitoring/mock_uss/f3548v21/routes_scd.py similarity index 66% rename from monitoring/mock_uss/scdsc/routes_scdsc.py rename to monitoring/mock_uss/f3548v21/routes_scd.py index 4b2157a51e..0bb78bc04d 100644 --- a/monitoring/mock_uss/scdsc/routes_scdsc.py +++ b/monitoring/mock_uss/f3548v21/routes_scd.py @@ -1,19 +1,12 @@ -import json - import flask -from implicitdict import ImplicitDict + +from monitoring.mock_uss.f3548v21.flight_planning import op_intent_from_flightrecord from monitoring.monitorlib import scd from monitoring.mock_uss import webapp from monitoring.mock_uss.auth import requires_scope -from monitoring.mock_uss.scdsc.database import db -from monitoring.mock_uss.scdsc.database import FlightRecord -from monitoring.uss_qualifier.resources.overrides import ( - apply_overrides, -) +from monitoring.mock_uss.flights.database import db from uas_standards.astm.f3548.v21.api import ( ErrorResponse, - OperationalIntent, - OperationalIntentDetails, GetOperationalIntentDetailsResponse, ) @@ -51,26 +44,6 @@ def scdsc_get_operational_intent_details(entityid: str): return flask.jsonify(response), 200 -def op_intent_from_flightrecord(flight: FlightRecord, method: str) -> OperationalIntent: - ref = flight.op_intent.reference - details = OperationalIntentDetails( - volumes=flight.op_intent.details.volumes, - off_nominal_volumes=flight.op_intent.details.off_nominal_volumes, - priority=flight.op_intent.details.priority, - ) - op_intent = OperationalIntent(reference=ref, details=details) - if flight.mod_op_sharing_behavior: - mod_op_sharing_behavior = flight.mod_op_sharing_behavior - if mod_op_sharing_behavior.modify_sharing_methods is not None: - if method not in mod_op_sharing_behavior.modify_sharing_methods: - return op_intent - op_intent = apply_overrides( - op_intent, mod_op_sharing_behavior.modify_fields, parse_result=False - ) - - return op_intent - - @webapp.route("/mock/scd/uss/v1/operational_intents", methods=["POST"]) @requires_scope(scd.SCOPE_SC) def scdsc_notify_operational_intent_details_changed(): diff --git a/monitoring/mock_uss/flight_planning/README.md b/monitoring/mock_uss/flight_planning/README.md new file mode 100644 index 0000000000..c62c7a9b2b --- /dev/null +++ b/monitoring/mock_uss/flight_planning/README.md @@ -0,0 +1,3 @@ +# mock_uss: flight_planner + +This folder contains materials implementing [InterUSS's flight_planning automated testing interface](https://github.com/interuss/automated_testing_interfaces/tree/main/flight_planning) by [mock_uss](..). diff --git a/monitoring/mock_uss/flight_planning/routes.py b/monitoring/mock_uss/flight_planning/routes.py index 54f21caf15..0c7d90cf97 100644 --- a/monitoring/mock_uss/flight_planning/routes.py +++ b/monitoring/mock_uss/flight_planning/routes.py @@ -6,7 +6,7 @@ from implicitdict import ImplicitDict from loguru import logger -from monitoring.mock_uss.scdsc.routes_injection import ( +from monitoring.mock_uss.scd_injection.routes_injection import ( inject_flight, lock_flight, release_flight_lock, diff --git a/monitoring/mock_uss/flights/README.md b/monitoring/mock_uss/flights/README.md new file mode 100644 index 0000000000..05f35b8307 --- /dev/null +++ b/monitoring/mock_uss/flights/README.md @@ -0,0 +1,3 @@ +# mock_uss: flights + +This folder contains materials related to generic handling of user-requested flights by [mock_uss](..). diff --git a/monitoring/mock_uss/flights/__init__.py b/monitoring/mock_uss/flights/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/mock_uss/scdsc/database.py b/monitoring/mock_uss/flights/database.py similarity index 93% rename from monitoring/mock_uss/scdsc/database.py rename to monitoring/mock_uss/flights/database.py index 94892eadbe..aefeb2b9c7 100644 --- a/monitoring/mock_uss/scdsc/database.py +++ b/monitoring/mock_uss/flights/database.py @@ -1,4 +1,5 @@ import json +from datetime import timedelta from typing import Dict, Optional from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo @@ -11,6 +12,8 @@ MockUssFlightBehavior, ) +DEADLOCK_TIMEOUT = timedelta(seconds=5) + class FlightRecord(ImplicitDict): """Representation of a flight in a USS""" diff --git a/monitoring/mock_uss/flights/planning.py b/monitoring/mock_uss/flights/planning.py new file mode 100644 index 0000000000..cc07cfd4bf --- /dev/null +++ b/monitoring/mock_uss/flights/planning.py @@ -0,0 +1,47 @@ +import time +from datetime import datetime +from typing import Callable + +from monitoring.mock_uss.flights.database import FlightRecord, db, DEADLOCK_TIMEOUT + + +def lock_flight(flight_id: str, log: Callable[[str], None]) -> FlightRecord: + # If this is a change to an existing flight, acquire lock to that flight + log(f"Acquiring lock for flight {flight_id}") + deadline = datetime.utcnow() + DEADLOCK_TIMEOUT + while True: + with db as tx: + if flight_id in tx.flights: + # This is an existing flight being modified + existing_flight = tx.flights[flight_id] + if existing_flight and not existing_flight.locked: + log("Existing flight locked for update") + existing_flight.locked = True + break + else: + log("Request is for a new flight (lock established)") + tx.flights[flight_id] = None + existing_flight = None + break + # We found an existing flight but it was locked; wait for it to become + # available + time.sleep(0.5) + + if datetime.utcnow() > deadline: + raise RuntimeError( + f"Deadlock in inject_flight while attempting to gain access to flight {flight_id}" + ) + return existing_flight + + +def release_flight_lock(flight_id: str, log: Callable[[str], None]) -> None: + with db as tx: + if flight_id in tx.flights: + if tx.flights[flight_id]: + # FlightRecord was a true existing flight + log(f"Releasing lock on existing flight_id {flight_id}") + tx.flights[flight_id].locked = False + else: + # FlightRecord was just a placeholder for a new flight + log(f"Releasing placeholder for existing flight_id {flight_id}") + del tx.flights[flight_id] diff --git a/monitoring/mock_uss/scd_injection/README.md b/monitoring/mock_uss/scd_injection/README.md new file mode 100644 index 0000000000..a6d634d284 --- /dev/null +++ b/monitoring/mock_uss/scd_injection/README.md @@ -0,0 +1,3 @@ +# mock_uss: scd_injection + +This folder contains material related to the deprecated [InterUSS scd automated testing interface](https://github.com/interuss/automated_testing_interfaces/tree/main/scd). diff --git a/monitoring/mock_uss/scd_injection/__init__.py b/monitoring/mock_uss/scd_injection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/mock_uss/scd_injection/routes.py b/monitoring/mock_uss/scd_injection/routes.py new file mode 100644 index 0000000000..837bd9d2cb --- /dev/null +++ b/monitoring/mock_uss/scd_injection/routes.py @@ -0,0 +1,6 @@ +from monitoring.mock_uss import webapp + + +@webapp.route("/scdsc/status") +def scdsc_status(): + return "scd flight injection API ok" diff --git a/monitoring/mock_uss/scdsc/routes_injection.py b/monitoring/mock_uss/scd_injection/routes_injection.py similarity index 91% rename from monitoring/mock_uss/scdsc/routes_injection.py rename to monitoring/mock_uss/scd_injection/routes_injection.py index 1622fa76cc..db7e8d7afe 100644 --- a/monitoring/mock_uss/scdsc/routes_injection.py +++ b/monitoring/mock_uss/scd_injection/routes_injection.py @@ -2,7 +2,7 @@ import traceback from datetime import datetime, timedelta import time -from typing import List, Tuple, Callable, Optional +from typing import List, Tuple, Optional import uuid import flask @@ -10,6 +10,8 @@ from loguru import logger import requests.exceptions +from monitoring.mock_uss.flights.planning import lock_flight, release_flight_lock +from monitoring.mock_uss.f3548v21 import utm_client from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo from uas_standards.astm.f3548.v21 import api from uas_standards.astm.f3548.v21.api import ( @@ -32,19 +34,20 @@ OperationalIntentState, ) -from monitoring.mock_uss import webapp, require_config_value +from monitoring.mock_uss import webapp, require_config_value, uspace from monitoring.mock_uss.auth import requires_scope from monitoring.mock_uss.config import KEY_BASE_URL from monitoring.mock_uss.dynamic_configuration.configuration import get_locality -from monitoring.mock_uss.scdsc import database, utm_client -from monitoring.mock_uss.scdsc.database import db, FlightRecord -from monitoring.mock_uss.scdsc.flight_planning import ( +from monitoring.mock_uss.flights import database +from monitoring.mock_uss.flights.database import db, FlightRecord +from monitoring.mock_uss.f3548v21.flight_planning import ( validate_request, check_for_disallowed_conflicts, PlanningError, + op_intent_from_flightrecord, op_intent_transition_valid, ) -from monitoring.mock_uss.scdsc.routes_scdsc import op_intent_from_flightrecord +import monitoring.mock_uss.uspace.flight_auth from monitoring.monitorlib import versioning from monitoring.monitorlib.clients import scd as scd_client from monitoring.monitorlib.fetch import QueryError @@ -188,48 +191,6 @@ def _mock_uss_flight_behavior_in_req( return None -def lock_flight(flight_id: str, log: Callable[[str], None]) -> FlightRecord: - # If this is a change to an existing flight, acquire lock to that flight - log(f"Acquiring lock for flight {flight_id}") - deadline = datetime.utcnow() + DEADLOCK_TIMEOUT - while True: - with db as tx: - if flight_id in tx.flights: - # This is an existing flight being modified - existing_flight = tx.flights[flight_id] - if existing_flight and not existing_flight.locked: - log("Existing flight locked for update") - existing_flight.locked = True - break - else: - log("Request is for a new flight (lock established)") - tx.flights[flight_id] = None - existing_flight = None - break - # We found an existing flight but it was locked; wait for it to become - # available - time.sleep(0.5) - - if datetime.utcnow() > deadline: - raise RuntimeError( - f"Deadlock in inject_flight while attempting to gain access to flight {flight_id}" - ) - return existing_flight - - -def release_flight_lock(flight_id: str, log: Callable[[str], None]) -> None: - with db as tx: - if flight_id in tx.flights: - if tx.flights[flight_id]: - # FlightRecord was a true existing flight - log(f"Releasing lock on existing flight_id {flight_id}") - tx.flights[flight_id].locked = False - else: - # FlightRecord was just a placeholder for a new flight - log(f"Releasing placeholder for existing flight_id {flight_id}") - del tx.flights[flight_id] - - def inject_flight( flight_id: str, req_body: MockUSSInjectFlightRequest, @@ -244,7 +205,9 @@ def log(msg: str): # Validate request log("Validating request") try: - validate_request(req_body, locality) + if locality.is_uspace_applicable(): + uspace.flight_auth.validate_request(req_body) + validate_request(req_body) except PlanningError as e: return ( InjectFlightResponse( diff --git a/monitoring/mock_uss/scdsc/README.md b/monitoring/mock_uss/scdsc/README.md deleted file mode 100644 index 1786fb2364..0000000000 --- a/monitoring/mock_uss/scdsc/README.md +++ /dev/null @@ -1 +0,0 @@ -[ASTM F3548-21](http://astm.org/f3548-21.html) standardizes UTM interoperability between USSs to achieve strategic coordination and communicate constraints. When this `scdsc` [mock_uss](..) functionality is enabled, mock_uss will behave like an F3548-21 strategic coordinator that accepts user flight planning attempts via the [InterUSS scd automated testing interface](../../../interfaces/automated_testing/scd) and attempts to establish and communicate an operational intent for those flights according to ASTM F3548-21. diff --git a/monitoring/mock_uss/scdsc/routes.py b/monitoring/mock_uss/scdsc/routes.py deleted file mode 100644 index 6c85debe12..0000000000 --- a/monitoring/mock_uss/scdsc/routes.py +++ /dev/null @@ -1,10 +0,0 @@ -from monitoring.mock_uss import webapp - - -@webapp.route("/scdsc/status") -def scdsc_status(): - return "Mock SCD strategic coordinator ok" - - -from . import routes_scdsc -from . import routes_injection diff --git a/monitoring/mock_uss/uspace/README.md b/monitoring/mock_uss/uspace/README.md new file mode 100644 index 0000000000..5bc1871b38 --- /dev/null +++ b/monitoring/mock_uss/uspace/README.md @@ -0,0 +1,3 @@ +# mock_uss: uspace + +This folder contains materials allowing mock_uss to emulate behaviors required in U-space. diff --git a/monitoring/mock_uss/uspace/__init__.py b/monitoring/mock_uss/uspace/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/mock_uss/uspace/flight_auth.py b/monitoring/mock_uss/uspace/flight_auth.py new file mode 100644 index 0000000000..5d6b3c989c --- /dev/null +++ b/monitoring/mock_uss/uspace/flight_auth.py @@ -0,0 +1,14 @@ +from monitoring.mock_uss.f3548v21.flight_planning import PlanningError +from monitoring.monitorlib.uspace import problems_with_flight_authorisation +from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api + + +def validate_request(req_body: scd_api.InjectFlightRequest) -> None: + """Raise a PlannerError if the request is not valid. + + Args: + req_body: Information about the requested flight. + """ + problems = problems_with_flight_authorisation(req_body.flight_authorisation) + if problems: + raise PlanningError(", ".join(problems))