diff --git a/monitoring/mock_uss/server.py b/monitoring/mock_uss/server.py index 50810c5448..cb02bb190b 100644 --- a/monitoring/mock_uss/server.py +++ b/monitoring/mock_uss/server.py @@ -238,7 +238,11 @@ def _periodic_tasks_daemon_loop(self): with db as tx: periodic_task = tx.periodic_tasks[task_to_execute] periodic_task.executing = False - if periodic_task.period.timedelta.total_seconds() == 0: + if ( + "period" in periodic_task + and periodic_task.period + and periodic_task.period.timedelta.total_seconds() == 0 + ): periodic_task.last_execution_time = StringBasedDateTime( arrow.utcnow().datetime ) diff --git a/monitoring/mock_uss/tracer/README.md b/monitoring/mock_uss/tracer/README.md index 8243b378b5..e1a9655716 100644 --- a/monitoring/mock_uss/tracer/README.md +++ b/monitoring/mock_uss/tracer/README.md @@ -5,7 +5,7 @@ This diagnostic capability monitors UTM traffic in a specified area. This includes, when requested, remote ID Identification Service Areas, strategic deconfliction Operational Intents, and Constraints. This tool records data in a way not allowed in a standards-compliant production system, so should not be run -on a production system. +in a production environment. ## Polling Polling periodically queries the DSS regarding the objects of interest and notes @@ -39,10 +39,4 @@ Visit /tracer/logs to see a list of log entries recorded by tracer while the current session has been running. ## Invocation -From the repository root: - -```shell script -./monitoring/mock_uss/run_locally_tracer.sh -``` - -See that script for parameters that can be adjusted for a deployed system. +An instance of tracer-enabled mock_uss is brought up as part of the [local deployment](../README.md#local-deployment). It can also be deployed [with Google Cloud Platform](../deployment/gcp) when configured appropriately. diff --git a/monitoring/mock_uss/tracer/log_types.py b/monitoring/mock_uss/tracer/log_types.py new file mode 100644 index 0000000000..c5b315b3f4 --- /dev/null +++ b/monitoring/mock_uss/tracer/log_types.py @@ -0,0 +1,260 @@ +from abc import abstractmethod +import sys +from typing import Optional, Type + +from implicitdict import ImplicitDict, StringBasedDateTime +from monitoring.monitorlib.fetch import rid as rid_fetch, RequestDescription, summarize +from monitoring.monitorlib.fetch import scd as scd_fetch +from monitoring.monitorlib.fetch.rid import FetchedISAs +from monitoring.monitorlib.mutate import rid as rid_mutate +from monitoring.monitorlib.mutate import scd as scd_mutate + + +class TracerLogEntry(ImplicitDict): + """A log entry for a tracer event. + + All subclasses must be defined in this module. + """ + + object_type: str + """The type of log entry that this is (automatically populated according to concrete log entry class name.""" + + def __init__(self, *args, **kwargs): + kwargs = kwargs.copy() + kwargs["object_type"] = type(self).__name__ + super(TracerLogEntry, self).__init__(*args, **kwargs) + + @staticmethod + @abstractmethod + def prefix_code() -> str: + raise NotImplementedError() + + def human_info(self) -> dict: + """Reorganize the information in this log entry into human-consumable form. + + This form will be displayed to tracer viewers when viewing the log in a browser. + """ + return self + + @staticmethod + def entry_type(type_name: Optional[str]) -> Optional[Type]: + matches = [ + cls + for name, cls in sys.modules[__name__].__dict__.items() + if name == type_name + and isinstance(cls, type) + and issubclass(cls, TracerLogEntry) + ] + if not matches: + return None + if len(matches) > 1: + raise ValueError( + f"Multiple TracerLogEntry classes named `{type_name}` found" + ) + return matches[0] + + +class PollStart(TracerLogEntry): + """Log entry for when polling starts.""" + + @staticmethod + def prefix_code() -> str: + return "poll_start" + + config: dict + """Configuration used for polling.""" + + +class RIDSubscribe(TracerLogEntry): + """Log entry for establishing an RID subscription.""" + + @staticmethod + def prefix_code() -> str: + return "rid_subscribe" + + changed_subscription: rid_mutate.ChangedSubscription + """Subscription, as created in the DSS.""" + + +class RIDISANotification(TracerLogEntry): + """Log entry for an incoming RID ISA notification from another USS.""" + + @staticmethod + def prefix_code() -> str: + return "notify_isa" + + observation_area_id: str + """ID of observation area with which this notification is associated.""" + + request: RequestDescription + """Incoming notification request from other USS.""" + + +class RIDUnsubscribe(TracerLogEntry): + """Log entry for the removal of an RID subscription.""" + + @staticmethod + def prefix_code() -> str: + return "rid_unsubscribe" + + existing_subscription: rid_fetch.FetchedSubscription + """Subscription, as read from the DSS just before deletion.""" + + deleted_subscription: Optional[rid_mutate.ChangedSubscription] + """Subscription returned from DSS upon deletion.""" + + +class PollISAs(TracerLogEntry): + """Log entry for polling identification service areas from the DSS.""" + + @staticmethod + def prefix_code() -> str: + return "poll_isas" + + poll: FetchedISAs + """Result of polling ISAs.""" + + def human_info(self) -> dict: + return { + "summary": summarize.isas(self.poll), + "details": self, + } + + +class PollFlights(TracerLogEntry): + """Log entry for client-requested poll of all RID flights in an area.""" + + @staticmethod + def prefix_code() -> str: + return "clientrequest_pollflights" + + observation_area_id: str + """ID of the observation area for which the flight polling was requested.""" + + poll: rid_fetch.FetchedFlights + """All information relating to the RID flights fetched.""" + + def human_info(self) -> dict: + return { + "summary": summarize.flights(self.poll), + "details": self, + } + + +class SCDSubscribe(TracerLogEntry): + """Log entry for establishing an SCD subscription.""" + + @staticmethod + def prefix_code() -> str: + return "scd_subscribe" + + changed_subscription: scd_mutate.MutatedSubscription + """Subscription, as created in the DSS.""" + + +class OperationalIntentNotification(TracerLogEntry): + """Log entry for an incoming operational intent notification from another USS.""" + + @staticmethod + def prefix_code() -> str: + return "notify_op" + + observation_area_id: str + """ID of observation area with which this notification is associated.""" + + request: RequestDescription + """Incoming notification request from other USS.""" + + +class ConstraintNotification(TracerLogEntry): + """Log entry for an incoming constraint notification from another USS.""" + + @staticmethod + def prefix_code() -> str: + return "notify_constraint" + + observation_area_id: str + """ID of observation area with which this notification is associated.""" + + request: RequestDescription + """Incoming notification request from other USS.""" + + +class SCDUnsubscribe(TracerLogEntry): + """Log entry for the removal of an SCD subscription.""" + + @staticmethod + def prefix_code() -> str: + return "scd_unsubscribe" + + existing_subscription: scd_fetch.FetchedSubscription + """Subscription, as read from the DSS just before deletion.""" + + deleted_subscription: Optional[scd_mutate.MutatedSubscription] + """Subscription returned from DSS upon deletion.""" + + +class PollOperationalIntents(TracerLogEntry): + """Log entry for polling operational intents from DSS and managing USSs.""" + + @staticmethod + def prefix_code() -> str: + return "poll_ops" + + poll: scd_fetch.FetchedEntities + """Results from polling operational intents from DSS and managing USSs.""" + + def human_info(self) -> dict: + return { + "summary": summarize.entities(self.poll), + "details": self, + } + + +class PollConstraints(TracerLogEntry): + """Log entry for polling constraints from DSS and managing USSs.""" + + @staticmethod + def prefix_code() -> str: + return "poll_constraints" + + poll: scd_fetch.FetchedEntities + """Results from polling constraints from DSS and managing USSs.""" + + def human_info(self) -> dict: + return { + "summary": summarize.entities(self.poll), + "details": self, + } + + +class BadRoute(TracerLogEntry): + """Log entry for access to an undefined (bad) endpoint.""" + + @staticmethod + def prefix_code() -> str: + return "uss_badroute" + + request: RequestDescription + """Incoming notification request from other USS.""" + + +class ObservationAreaImportError(TracerLogEntry): + """Log entry for an error while attempting to import an operation area from subscriptions in the DSS.""" + + @staticmethod + def prefix_code() -> str: + return "import_obs_areas_error" + + rid_subscriptions: Optional[rid_fetch.FetchedSubscriptions] + """Result of attempting to fetch RID subscriptions""" + + +class TracerShutdown(TracerLogEntry): + """Log entry for when tracer shuts down.""" + + @staticmethod + def prefix_code() -> str: + return "tracer_stop" + + timestamp: StringBasedDateTime diff --git a/monitoring/mock_uss/tracer/routes/__init__.py b/monitoring/mock_uss/tracer/routes/__init__.py index 488e1843bf..9d38758a29 100644 --- a/monitoring/mock_uss/tracer/routes/__init__.py +++ b/monitoring/mock_uss/tracer/routes/__init__.py @@ -6,8 +6,9 @@ from termcolor import colored from monitoring.mock_uss import webapp +from monitoring.mock_uss.tracer import context +from monitoring.mock_uss.tracer.log_types import BadRoute from monitoring.monitorlib import fetch, versioning -from .. import context @webapp.route("/tracer/status") @@ -26,8 +27,7 @@ def tracer_status(): def tracer_catch_all(u_path) -> Tuple[str, int]: logger.debug(f"Handling tracer_catch_all from {os.getpid()}") req = fetch.describe_flask_request(flask.request) - req["endpoint"] = "catch_all" - log_name = context.tracer_logger.log_new("uss_badroute", req) + log_name = context.tracer_logger.log_new(BadRoute(request=req)) claims = req.token owner = claims.get("sub", "") diff --git a/monitoring/mock_uss/tracer/routes/observation_areas.py b/monitoring/mock_uss/tracer/routes/observation_areas.py index f430299f9d..880a69a01f 100644 --- a/monitoring/mock_uss/tracer/routes/observation_areas.py +++ b/monitoring/mock_uss/tracer/routes/observation_areas.py @@ -1,7 +1,7 @@ import os import uuid from datetime import datetime -from typing import Tuple, List +from typing import Tuple, List, Union import flask import s2sphere @@ -11,6 +11,10 @@ from monitoring.mock_uss import webapp from monitoring.mock_uss.tracer import context from monitoring.mock_uss.tracer.database import db +from monitoring.mock_uss.tracer.log_types import ( + ObservationAreaImportError, + TracerShutdown, +) from monitoring.mock_uss.tracer.observation_area_operations import ( redact_observation_area, delete_observation_area, @@ -34,7 +38,7 @@ @webapp.route("/tracer/observation_areas", methods=["GET"]) @ui_auth.login_required -def tracer_list_observation_areas() -> Tuple[str, int]: +def tracer_list_observation_areas() -> flask.Response: with db as tx: result = ListObservationAreasResponse( areas=[redact_observation_area(a) for a in tx.observation_areas.values()] @@ -44,7 +48,9 @@ def tracer_list_observation_areas() -> Tuple[str, int]: @webapp.route("/tracer/observation_areas/", methods=["PUT"]) @ui_auth.login_required(role="admin") -def tracer_upsert_observation_area(area_id: str) -> Tuple[str, int]: +def tracer_upsert_observation_area( + area_id: str, +) -> Union[Tuple[str, int], flask.Response]: try: req_body = flask.request.json if req_body is None: @@ -84,7 +90,9 @@ def tracer_upsert_observation_area(area_id: str) -> Tuple[str, int]: @webapp.route("/tracer/observation_areas/", methods=["DELETE"]) @ui_auth.login_required(role="admin") -def tracer_delete_observation_area(area_id: str) -> Tuple[str, int]: +def tracer_delete_observation_area( + area_id: str, +) -> Union[Tuple[str, int], flask.Response]: with db as tx: if area_id not in tx.observation_areas: return "Specified observation area not in system", 404 @@ -101,7 +109,7 @@ def tracer_delete_observation_area(area_id: str) -> Tuple[str, int]: @webapp.route("/tracer/observation_areas/import_requests", methods=["POST"]) @ui_auth.login_required(role="admin") -def tracer_import_observation_areas() -> Tuple[str, int]: +def tracer_import_observation_areas() -> Union[Tuple[str, int], flask.Response]: try: req_body = flask.request.json if req_body is None: @@ -141,7 +149,7 @@ def tracer_import_observation_areas() -> Tuple[str, int]: ) if not rid_subscriptions.success: context.tracer_logger.log_new( - "import_observation_areas_rid_error", rid_subscriptions + ObservationAreaImportError(rid_subscriptions=rid_subscriptions) ) return ( f"Could not retrieve F3411 subscriptions (code {rid_subscriptions.status_code})", @@ -219,8 +227,5 @@ def _shutdown(): logger.info("Observation areas cleanup complete.") context.tracer_logger.log_new( - "tracer_stop", - { - "timestamp": datetime.utcnow(), - }, + TracerShutdown(timestamp=StringBasedDateTime(datetime.utcnow())) ) diff --git a/monitoring/mock_uss/tracer/routes/rid.py b/monitoring/mock_uss/tracer/routes/rid.py index dd29962133..b53ab36572 100644 --- a/monitoring/mock_uss/tracer/routes/rid.py +++ b/monitoring/mock_uss/tracer/routes/rid.py @@ -7,6 +7,9 @@ from implicitdict import ImplicitDict from monitoring.mock_uss import webapp +from monitoring.mock_uss.tracer import context +from monitoring.mock_uss.tracer.log_types import RIDISANotification +from monitoring.mock_uss.tracer.template import _print_time_range from monitoring.monitorlib import fetch from monitoring.monitorlib.rid import RIDVersion from uas_standards.astm.f3411.v19.api import ( @@ -15,8 +18,6 @@ from uas_standards.astm.f3411.v22a.api import ( PutIdentificationServiceAreaNotificationParameters as PutIdentificationServiceAreaNotificationParametersV22a, ) -from .. import context -from ..template import _print_time_range RESULT = ("", 204) @@ -28,7 +29,7 @@ def tracer_rid_isa_notification_v19( observation_area_id: str, isa_id: str ) -> Tuple[str, int]: - return tracer_rid_isa_notification(isa_id, RIDVersion.f3411_19) + return tracer_rid_isa_notification(isa_id, observation_area_id, RIDVersion.f3411_19) @webapp.route( @@ -38,15 +39,20 @@ def tracer_rid_isa_notification_v19( def tracer_rid_isa_notification_v22a( observation_area_id: str, isa_id: str ) -> Tuple[str, int]: - return tracer_rid_isa_notification(isa_id, RIDVersion.f3411_22a) + return tracer_rid_isa_notification( + isa_id, observation_area_id, RIDVersion.f3411_22a + ) -def tracer_rid_isa_notification(id: str, rid_version: RIDVersion) -> Tuple[str, int]: +def tracer_rid_isa_notification( + isa_id: str, observation_area_id: str, rid_version: RIDVersion +) -> Tuple[str, int]: """Implements RID ISA notification receiver.""" logger.debug(f"Handling tracer_rid_isa_notification from {os.getpid()}") req = fetch.describe_flask_request(flask.request) - req["endpoint"] = "identification_service_areas" - log_name = context.tracer_logger.log_new("notify_isa", req) + log_name = context.tracer_logger.log_new( + RIDISANotification(observation_area_id=observation_area_id, request=req) + ) claims = req.token owner = claims.get("sub", "") @@ -84,13 +90,13 @@ def tracer_rid_isa_notification(id: str, rid_version: RIDVersion) -> Tuple[str, ) logger.info( - f"{label} {id} v{version} ({owner}) updated {time_range} -> {log_name}" + f"{label} {isa_id} v{version} ({owner}) updated {time_range} -> {log_name}" ) else: - logger.info(f"{label} {id} ({owner}) deleted -> {log_name}") + logger.info(f"{label} {isa_id} ({owner}) deleted -> {log_name}") except ValueError as err: logger.error( - f"{label} {id} ({owner}) unable to decode JSON: {err} -> {log_name}" + f"{label} {isa_id} ({owner}) unable to decode JSON: {err} -> {log_name}" ) return RESULT diff --git a/monitoring/mock_uss/tracer/routes/scd.py b/monitoring/mock_uss/tracer/routes/scd.py index baacbd2857..252213ba6d 100644 --- a/monitoring/mock_uss/tracer/routes/scd.py +++ b/monitoring/mock_uss/tracer/routes/scd.py @@ -6,9 +6,13 @@ from termcolor import colored from monitoring.mock_uss import webapp +from monitoring.mock_uss.tracer import context +from monitoring.mock_uss.tracer.log_types import ( + OperationalIntentNotification, + ConstraintNotification, +) +from monitoring.mock_uss.tracer.template import _print_time_range from monitoring.monitorlib import fetch, infrastructure -from . import context -from ..template import _print_time_range RESULT = ("", 204) @@ -21,8 +25,11 @@ def tracer_scd_v21_operation_notification(observation_area_id: str) -> Tuple[str """Implements SCD Operation notification receiver.""" logger.debug(f"Handling tracer_scd_v21_operation_notification from {os.getpid()}") req = fetch.describe_flask_request(flask.request) - req["endpoint"] = "operational_intents" - log_name = context.tracer_logger.log_new("notify_op", req) + log_name = context.tracer_logger.log_new( + OperationalIntentNotification( + observation_area_id=observation_area_id, request=req + ) + ) claims = req.token owner = claims.get("sub", "") @@ -83,8 +90,9 @@ def tracer_scd_v21_constraint_notification(observation_area_id: str) -> Tuple[st """Implements SCD Constraint notification receiver.""" logger.debug(f"Handling tracer_scd_v21_constraint_notification from {os.getpid()}") req = fetch.describe_flask_request(flask.request) - req["endpoint"] = "constraints" - log_name = context.tracer_logger.log_new("notify_constraint", req) + log_name = context.tracer_logger.log_new( + ConstraintNotification(observation_area_id=observation_area_id, request=req) + ) claims = infrastructure.get_token_claims({k: v for k, v in flask.request.headers}) owner = claims.get("sub", "") diff --git a/monitoring/mock_uss/tracer/routes/ui.py b/monitoring/mock_uss/tracer/routes/ui.py index ee549b6e99..3e5b8ff273 100644 --- a/monitoring/mock_uss/tracer/routes/ui.py +++ b/monitoring/mock_uss/tracer/routes/ui.py @@ -13,6 +13,7 @@ from monitoring.mock_uss import webapp from monitoring.mock_uss.tracer import context from monitoring.mock_uss.tracer.database import db +from monitoring.mock_uss.tracer.log_types import PollFlights, TracerLogEntry from monitoring.mock_uss.tracer.observation_areas import ObservationArea from monitoring.mock_uss.tracer.ui_auth import ui_auth from monitoring.monitorlib import fetch, geo, infrastructure @@ -120,26 +121,11 @@ def tracer_logs(log): else: obj = {"entries": objs} - object_type = obj.get("object_type", None) - if object_type == fetch.rid.FetchedISAs.__name__: - obj = { - "summary": summarize.isas(ImplicitDict.parse(obj, fetch.rid.FetchedISAs)), - "details": obj, - } - elif object_type == fetch.scd.FetchedEntities.__name__: - obj = { - "summary": summarize.entities( - ImplicitDict.parse(obj, fetch.scd.FetchedEntities) - ), - "details": obj, - } - elif object_type == fetch.rid.FetchedFlights.__name__: - obj = { - "summary": summarize.flights( - ImplicitDict.parse(obj, fetch.rid.FetchedFlights) - ), - "details": obj, - } + object_type_name = obj.get("object_type", None) + object_type = TracerLogEntry.entry_type(object_type_name) + if object_type: + parsed: TracerLogEntry = ImplicitDict.parse(obj, object_type) + obj = parsed.human_info() return flask.render_template( "tracer/log.html", @@ -236,14 +222,14 @@ def tracer_rid_request_poll(observation_area_id: str): rid_client = context.get_client(area.f3411.auth_spec, area.f3411.dss_base_url) flights_result = fetch.rid.all_flights( geo.make_latlng_rect(area.area.volume), - flask.request.form.get("include_recent_positions"), - flask.request.form.get("get_details"), + flask.request.form.get("include_recent_positions", type=bool), + flask.request.form.get("get_details", type=bool), area.f3411.rid_version, rid_client, - enhanced_details=flask.request.form.get("enhanced_details"), + enhanced_details=flask.request.form.get("enhanced_details", type=bool), ) log_name = context.tracer_logger.log_new( - "clientrequest_pollflights", flights_result + PollFlights(observation_area_id=observation_area_id, poll=flights_result) ) return flask.redirect(flask.url_for("tracer_logs", log=log_name)) diff --git a/monitoring/mock_uss/tracer/subscriptions.py b/monitoring/mock_uss/tracer/subscriptions.py index 170f63d625..15e1b097e8 100644 --- a/monitoring/mock_uss/tracer/subscriptions.py +++ b/monitoring/mock_uss/tracer/subscriptions.py @@ -5,6 +5,12 @@ from yaml.representer import Representer from monitoring.mock_uss.tracer import context +from monitoring.mock_uss.tracer.log_types import ( + RIDSubscribe, + SCDSubscribe, + RIDUnsubscribe, + SCDUnsubscribe, +) from monitoring.mock_uss.tracer.observation_areas import ObservationAreaID from monitoring.monitorlib import fetch import monitoring.monitorlib.fetch.rid @@ -20,11 +26,6 @@ yaml.add_representer(StringBasedDateTime, Representer.represent_str) -RID_SUBSCRIPTION_ID_CODE = "tracer RID Subscription" -SCD_SUBSCRIPTION_ID_CODE = "tracer SCD Subscription" -RID_SUBSCRIPTION_KEY = "subscription_rid" -SCD_SUBSCRIPTION_KEY = "subscription_scd" - class SubscriptionManagementError(RuntimeError): def __init__(self, msg): @@ -61,7 +62,7 @@ def subscribe_rid( rid_version=rid_version, utm_client=rid_client, ) - logger.log_new(f"{RID_SUBSCRIPTION_KEY}_create", create_result) + logger.log_new(RIDSubscribe(changed_subscription=create_result)) if not create_result.success: raise SubscriptionManagementError("Could not create RID Subscription") return subscription_id @@ -92,7 +93,7 @@ def subscribe_scd( area.volume.altitude_lower_wgs84_m(0), area.volume.altitude_upper_wgs84_m(3048), ) - logfile = logger.log_new(f"{SCD_SUBSCRIPTION_KEY}_create", create_result) + logfile = logger.log_new(SCDSubscribe(changed_subscription=create_result)) if not create_result.success: raise SubscriptionManagementError( "Could not create new SCD Subscription -> {}".format(logfile) @@ -105,8 +106,8 @@ def unsubscribe_rid( ) -> None: logger = context.tracer_logger existing_result = fetch.rid.subscription(subscription_id, rid_version, rid_client) - logfile = logger.log_new(f"{RID_SUBSCRIPTION_KEY}_get", existing_result) if existing_result.status_code != 404 and not existing_result.success: + logfile = logger.log_new(RIDUnsubscribe(existing_subscription=existing_result)) raise SubscriptionManagementError( "Could not query existing RID Subscription -> {}".format(logfile) ) @@ -118,7 +119,11 @@ def unsubscribe_rid( rid_version=rid_version, utm_client=rid_client, ) - logfile = logger.log_new(f"{RID_SUBSCRIPTION_KEY}_del", del_result) + logfile = logger.log_new( + RIDUnsubscribe( + existing_subscription=existing_result, deleted_subscription=del_result + ) + ) if not del_result.success: raise SubscriptionManagementError( "Could not delete existing RID Subscription -> {}".format(logfile) @@ -128,8 +133,8 @@ def unsubscribe_rid( def unsubscribe_scd(subscription_id: str, scd_client: UTMClientSession) -> None: logger = context.tracer_logger get_result = fetch.scd.get_subscription(scd_client, subscription_id) - logfile = logger.log_new(f"{SCD_SUBSCRIPTION_KEY}_get", get_result) if not get_result.success: + logfile = logger.log_new(SCDUnsubscribe(existing_subscription=get_result)) raise SubscriptionManagementError( "Could not query existing SCD Subscription -> {}".format(logfile) ) @@ -140,7 +145,11 @@ def unsubscribe_scd(subscription_id: str, scd_client: UTMClientSession) -> None: subscription_id, get_result.subscription.version, ) - logfile = logger.log_new(f"{SCD_SUBSCRIPTION_KEY}_del", del_result) + logfile = logger.log_new( + SCDUnsubscribe( + existing_subscription=get_result, deleted_subscription=del_result + ) + ) if not del_result.success: raise SubscriptionManagementError( "Could not delete existing SCD Subscription -> {}".format(logfile) diff --git a/monitoring/mock_uss/tracer/tracer_poll.py b/monitoring/mock_uss/tracer/tracer_poll.py index 4e29c6a8d0..16ec6ab82d 100755 --- a/monitoring/mock_uss/tracer/tracer_poll.py +++ b/monitoring/mock_uss/tracer/tracer_poll.py @@ -10,6 +10,12 @@ KEY_TRACER_KML_SERVER, KEY_TRACER_KML_FOLDER, ) +from monitoring.mock_uss.tracer.log_types import ( + PollOperationalIntents, + PollConstraints, + PollISAs, + PollStart, +) from monitoring.mock_uss.tracer.observation_areas import ( ObservationAreaID, ObservationArea, @@ -71,7 +77,7 @@ def _log_poll_start(logger): KEY_TRACER_KML_FOLDER: webapp.config[KEY_TRACER_KML_FOLDER], "code_version": versioning.get_code_version(), } - logger.log_new("poll_start", config) + logger.log_new(PollStart(config=config)) @webapp.periodic_task(TASK_POLL_OBSERVATION_AREAS) @@ -99,7 +105,6 @@ def poll_isas(area: ObservationArea, logger: tracerlog.Logger) -> None: rid_client = context.get_client(area.f3411.auth_spec, area.f3411.dss_base_url) box = get_latlngrect_vertices(make_latlng_rect(area.area.volume)) - log_name = "poll_isas" t0 = datetime.datetime.utcnow() result = fetch.rid.isas( box, @@ -125,13 +130,14 @@ def poll_isas(area: ObservationArea, logger: tracerlog.Logger) -> None: tx.need_line_break = True need_line_break = tx.need_line_break + log_entry = PollISAs(poll=result) if log_new: - logger.log_new(log_name, result) + logger.log_new(log_entry) if need_line_break: print() print(diff.isa_diff_text(last_result, result)) else: - logger.log_same(t0, t1, log_name) + logger.log_same(t0, t1, log_entry.prefix_code()) print_no_newline(".") @@ -139,7 +145,6 @@ def poll_ops( area: ObservationArea, scd_client: UTMClientSession, logger: tracerlog.Logger ) -> None: box = make_latlng_rect(area.area.volume) - log_name = "poll_ops" t0 = datetime.datetime.utcnow() if "operational_intents" not in context.scd_cache: context.scd_cache["operational_intents"]: Dict[ @@ -168,13 +173,14 @@ def poll_ops( tx.need_line_break = True need_line_break = tx.need_line_break + log_entry = PollOperationalIntents(poll=result) if log_new: - logger.log_new(log_name, result) + logger.log_new(log_entry) if need_line_break: print() print(diff.entity_diff_text(last_result, result)) else: - logger.log_same(t0, t1, log_name) + logger.log_same(t0, t1, log_entry.prefix_code()) print_no_newline(".") @@ -182,7 +188,6 @@ def poll_constraints( area: ObservationArea, scd_client: UTMClientSession, logger: tracerlog.Logger ) -> None: box = make_latlng_rect(area.area.volume) - log_name = "poll_constraints" t0 = datetime.datetime.utcnow() if "constraints" not in context.scd_cache: context.scd_cache["constraints"]: Dict[str, fetch.scd.FetchedEntity] = {} @@ -207,11 +212,12 @@ def poll_constraints( tx.need_line_break = True need_line_break = tx.need_line_break + log_entry = PollConstraints(poll=result) if log_new: - logger.log_new(log_name, result) + logger.log_new(log_entry) if need_line_break: print() print(diff.entity_diff_text(last_result, result)) else: - logger.log_same(t0, t1, log_name) + logger.log_same(t0, t1, log_entry.prefix_code()) print_no_newline(".") diff --git a/monitoring/mock_uss/tracer/tracerlog.py b/monitoring/mock_uss/tracer/tracerlog.py index 60a7004bf6..6e8615a20b 100644 --- a/monitoring/mock_uss/tracer/tracerlog.py +++ b/monitoring/mock_uss/tracer/tracerlog.py @@ -1,10 +1,10 @@ import datetime import json import os -from typing import Dict import yaml +from monitoring.mock_uss.tracer.log_types import TracerLogEntry from monitoring.monitorlib import infrastructure @@ -23,10 +23,10 @@ def log_same(self, t0: datetime.datetime, t1: datetime.datetime, code: str) -> N body = {"t0": t0.isoformat(), "t1": t1.isoformat(), "code": code} f.write(yaml.dump(body, explicit_start=True)) - def log_new(self, code: str, content: Dict) -> str: + def log_new(self, content: TracerLogEntry) -> str: n = len(os.listdir(self.log_path)) basename = "{:06d}_{}_{}".format( - n, datetime.datetime.now().strftime("%H%M%S_%f"), code + n, datetime.datetime.now().strftime("%H%M%S_%f"), content.prefix_code() ) logname = "{}.yaml".format(basename) fullname = os.path.join(self.log_path, logname) diff --git a/monitoring/monitorlib/kml/__init__.py b/monitoring/monitorlib/kml/__init__.py new file mode 100644 index 0000000000..fe08f60878 --- /dev/null +++ b/monitoring/monitorlib/kml/__init__.py @@ -0,0 +1 @@ +KML_NAMESPACE = {"kml": "http://www.opengis.net/kml/2.2"} diff --git a/monitoring/monitorlib/kml/f3548v21.py b/monitoring/monitorlib/kml/f3548v21.py new file mode 100644 index 0000000000..8bdef4148b --- /dev/null +++ b/monitoring/monitorlib/kml/f3548v21.py @@ -0,0 +1,92 @@ +from typing import List + +from pykml.factory import KML_ElementMaker as kml + +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.kml.generation import ( + GREEN, + TRANSLUCENT_GRAY, + TRANSLUCENT_GREEN, + YELLOW, + RED, + make_placemark_from_volume, +) +from monitoring.monitorlib.scd import priority_of +from uas_standards.astm.f3548.v21.api import ( + OperationalIntent, + QueryOperationalIntentReferenceParameters, + QueryOperationalIntentReferenceResponse, +) + + +def full_op_intent(op_intent: OperationalIntent) -> kml.Folder: + """Render operational intent information into Placemarks in a KML folder.""" + ref = op_intent.reference + details = op_intent.details + name = f"{ref.manager}'s P{priority_of(details)} {ref.state.value} {ref.id}[{ref.version}] @ {ref.ovn}" + folder = kml.Folder(kml.name(name)) + if "volumes" in details: + for i, v4_f3548 in enumerate(details.volumes): + v4 = Volume4D.from_f3548v21(v4_f3548) + folder.append( + make_placemark_from_volume( + v4, + name=f"Nominal volume {i}", + style_url=f"#F3548v21{ref.state.value}", + ) + ) + if "off_nominal_volumes" in details: + for i, v4_f3548 in enumerate(details.off_nominal_volumes): + v4 = Volume4D.from_f3548v21(v4_f3548) + folder.append( + make_placemark_from_volume( + v4, + name=f"Off-nominal volume {i}", + style_url=f"#F3548v21{ref.state.value}", + ) + ) + return folder + + +def op_intent_refs_query( + req: QueryOperationalIntentReferenceParameters, + resp: QueryOperationalIntentReferenceResponse, +) -> kml.Placemark: + """Render the area of interest and response from an operational intent references query into a KML Placemark.""" + v4 = Volume4D.from_f3548v21(req.area_of_interest) + items = "".join( + f"
  • {oi.manager}'s {oi.state.value} {oi.id}[{oi.version}]
  • " + for oi in resp.operational_intent_references + ) + description = ( + f"
      {items}
    " if items else "(no operational intent references found)" + ) + return make_placemark_from_volume( + v4, name="area_of_interest", style_url="#QueryArea", description=description + ) + + +def f3548v21_styles() -> List[kml.Style]: + """Provides KML styles according to F3548-21 operational intent states.""" + return [ + kml.Style( + kml.LineStyle(kml.color(GREEN), kml.width(3)), + kml.PolyStyle(kml.color(TRANSLUCENT_GRAY)), + id="F3548v21Accepted", + ), + kml.Style( + kml.LineStyle(kml.color(GREEN), kml.width(3)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="F3548v21Activated", + ), + kml.Style( + kml.LineStyle(kml.color(YELLOW), kml.width(5)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="F3548v21Nonconforming", + ), + kml.Style( + kml.LineStyle(kml.color(RED), kml.width(5)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="F3548v21Contingent", + ), + ] diff --git a/monitoring/monitorlib/kml/flight_planning.py b/monitoring/monitorlib/kml/flight_planning.py new file mode 100644 index 0000000000..091e027c71 --- /dev/null +++ b/monitoring/monitorlib/kml/flight_planning.py @@ -0,0 +1,65 @@ +from typing import List + +from pykml.factory import KML_ElementMaker as kml + +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.kml.generation import ( + GREEN, + TRANSLUCENT_GRAY, + TRANSLUCENT_GREEN, + YELLOW, + RED, + make_placemark_from_volume, +) +from uas_standards.interuss.automated_testing.flight_planning.v1.api import ( + UpsertFlightPlanRequest, + UpsertFlightPlanResponse, +) + + +def upsert_flight_plan( + req: UpsertFlightPlanRequest, resp: UpsertFlightPlanResponse +) -> kml.Folder: + """Render a flight planning action into a KML folder.""" + basic_info = req.flight_plan.basic_information + folder = kml.Folder( + kml.name( + f"Activity {resp.planning_result.value}, flight {resp.flight_plan_status.value}" + ) + ) + for i, v4_flight_planning in enumerate(basic_info.area): + v4 = Volume4D.from_flight_planning_api(v4_flight_planning) + folder.append( + make_placemark_from_volume( + v4, + name=f"Volume {i}", + style_url=f"#{basic_info.usage_state.value}_{basic_info.uas_state.value}", + ) + ) + return folder + + +def flight_planning_styles() -> List[kml.Style]: + """Provides KML styles with names in the form {FlightPlanState}_{AirspaceUsageState}.""" + return [ + kml.Style( + kml.LineStyle(kml.color(GREEN), kml.width(3)), + kml.PolyStyle(kml.color(TRANSLUCENT_GRAY)), + id="Planned_Nominal", + ), + kml.Style( + kml.LineStyle(kml.color(GREEN), kml.width(3)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="InUse_Nominal", + ), + kml.Style( + kml.LineStyle(kml.color(YELLOW), kml.width(5)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="InUse_OffNominal", + ), + kml.Style( + kml.LineStyle(kml.color(RED), kml.width(5)), + kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), + id="InUse_Contingent", + ), + ] diff --git a/monitoring/monitorlib/kml.py b/monitoring/monitorlib/kml/generation.py similarity index 52% rename from monitoring/monitorlib/kml.py rename to monitoring/monitorlib/kml/generation.py index b4e8ab9441..47a9e82e11 100644 --- a/monitoring/monitorlib/kml.py +++ b/monitoring/monitorlib/kml/generation.py @@ -1,9 +1,7 @@ import math -import re from typing import List, Optional, Union import s2sphere -from pykml import parser from pykml.factory import KML_ElementMaker as kml from monitoring.monitorlib.geo import ( @@ -12,13 +10,10 @@ DistanceUnits, egm96_geoid_offset, Radius, - EARTH_CIRCUMFERENCE_M, - LatLngPoint, + METERS_PER_FOOT, ) from monitoring.monitorlib.geotemporal import Volume4D -KML_NAMESPACE = {"kml": "http://www.opengis.net/kml/2.2"} -METERS_PER_FOOT = 0.3048 # Hexadecimal colors GREEN = "ff00c000" @@ -30,114 +25,6 @@ TRANSLUCENT_LIGHT_CYAN = "80ffffaa" -def get_kml_root(kml_obj, from_string=False): - if from_string: - content = parser.fromstring(kml_obj) - return content - content = parser.parse(kml_obj) - return content.getroot() - - -def get_folders(root): - return root.Document.Folder.Folder - - -def get_polygon_speed(polygon_name): - """Returns speed unit within a polygon.""" - result = re.search(r"\(([0-9.]+)\)", polygon_name) - return float(result.group(1)) if result else None - - -def get_folder_details(folder_elem): - speed_polygons = {} - alt_polygons = {} - operator_location = {} - coordinates = "" - for placemark in folder_elem.xpath(".//kml:Placemark", namespaces=KML_NAMESPACE): - placemark_name = str(placemark.name) - polygons = placemark.xpath(".//kml:Polygon", namespaces=KML_NAMESPACE) - - if placemark_name == "operator_location": - operator_point = folder_elem.xpath( - ".//kml:Placemark/kml:Point/kml:coordinates", namespaces=KML_NAMESPACE - )[0] - if operator_point: - operator_point = str(operator_point).split(",") - operator_location = {"lng": operator_point[0], "lat": operator_point[1]} - if polygons: - if placemark_name.startswith("alt:"): - polygon_coords = get_coordinates_from_kml( - polygons[0].outerBoundaryIs.LinearRing.coordinates - ) - alt_polygons.update({placemark_name: polygon_coords}) - if placemark_name.startswith("speed:"): - if not get_polygon_speed(placemark_name): - raise ValueError( - 'Could not determine Polygon speed from Placemark "{}"'.format( - placemark_name - ) - ) - polygon_coords = get_coordinates_from_kml( - polygons[0].outerBoundaryIs.LinearRing.coordinates - ) - speed_polygons.update({placemark_name: polygon_coords}) - - coords = placemark.xpath( - ".//kml:LineString/kml:coordinates", namespaces=KML_NAMESPACE - ) - if coords: - coordinates = coords - coordinates = get_coordinates_from_kml(coordinates) - return { - str(folder_elem.name): { - "description": get_folder_description(folder_elem), - "speed_polygons": speed_polygons, - "alt_polygons": alt_polygons, - "input_coordinates": coordinates, - "operator_location": operator_location, - } - } - - -def get_coordinates_from_kml(coordinates): - """Returns list of tuples of coordinates. - Args: - coordinates: coordinates element from KML. - """ - if coordinates: - return [ - tuple(float(x.strip()) for x in c.split(",")) - for c in str(coordinates[0]).split(" ") - if c.strip() - ] - - -def get_folder_description(folder_elem): - """Returns folder description from KML. - Args: - folder_elem: Folder element from KML. - """ - description = folder_elem.description - lines = [line for line in str(description).split("\n") if ":" in line] - values = {} - for line in lines: - cols = [col.strip() for col in line.split(":")] - if len(cols) == 2: - values[cols[0]] = cols[1] - return values - - -def get_kml_content(kml_file, from_string=False): - root = get_kml_root(kml_file, from_string) - folders = get_folders(root) - kml_content = {} - for folder in folders: - folder_details = get_folder_details(folder) - if folder_details: - kml_content.update(folder_details) - return kml_content - - def _altitude_mode_of(altitude: Altitude) -> str: if altitude.reference == AltitudeDatum.W84: return "absolute" @@ -297,32 +184,6 @@ def make_placemark_from_volume( return placemark -def flight_planning_styles() -> List[kml.Style]: - """Provides KML styles with names in the form {FlightPlanState}_{AirspaceUsageState}.""" - return [ - kml.Style( - kml.LineStyle(kml.color(GREEN), kml.width(3)), - kml.PolyStyle(kml.color(TRANSLUCENT_GRAY)), - id="Planned_Nominal", - ), - kml.Style( - kml.LineStyle(kml.color(GREEN), kml.width(3)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="InUse_Nominal", - ), - kml.Style( - kml.LineStyle(kml.color(YELLOW), kml.width(5)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="InUse_OffNominal", - ), - kml.Style( - kml.LineStyle(kml.color(RED), kml.width(5)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="InUse_Contingent", - ), - ] - - def query_styles() -> List[kml.Style]: """Provides KML styles for query areas.""" return [ @@ -332,29 +193,3 @@ def query_styles() -> List[kml.Style]: id="QueryArea", ), ] - - -def f3548v21_styles() -> List[kml.Style]: - """Provides KML styles according to F3548-21 operational intent states.""" - return [ - kml.Style( - kml.LineStyle(kml.color(GREEN), kml.width(3)), - kml.PolyStyle(kml.color(TRANSLUCENT_GRAY)), - id="F3548v21Accepted", - ), - kml.Style( - kml.LineStyle(kml.color(GREEN), kml.width(3)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="F3548v21Activated", - ), - kml.Style( - kml.LineStyle(kml.color(YELLOW), kml.width(5)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="F3548v21Nonconforming", - ), - kml.Style( - kml.LineStyle(kml.color(RED), kml.width(5)), - kml.PolyStyle(kml.color(TRANSLUCENT_GREEN)), - id="F3548v21Contingent", - ), - ] diff --git a/monitoring/monitorlib/kml/parsing.py b/monitoring/monitorlib/kml/parsing.py new file mode 100644 index 0000000000..f0c857b69e --- /dev/null +++ b/monitoring/monitorlib/kml/parsing.py @@ -0,0 +1,113 @@ +import re + +from pykml import parser + +from monitoring.monitorlib.kml import KML_NAMESPACE + + +def get_kml_root(kml_obj, from_string=False): + if from_string: + content = parser.fromstring(kml_obj) + return content + content = parser.parse(kml_obj) + return content.getroot() + + +def get_folders(root): + return root.Document.Folder.Folder + + +def get_polygon_speed(polygon_name): + """Returns speed unit within a polygon.""" + result = re.search(r"\(([0-9.]+)\)", polygon_name) + return float(result.group(1)) if result else None + + +def get_folder_details(folder_elem): + speed_polygons = {} + alt_polygons = {} + operator_location = {} + coordinates = "" + for placemark in folder_elem.xpath(".//kml:Placemark", namespaces=KML_NAMESPACE): + placemark_name = str(placemark.name) + polygons = placemark.xpath(".//kml:Polygon", namespaces=KML_NAMESPACE) + + if placemark_name == "operator_location": + operator_point = folder_elem.xpath( + ".//kml:Placemark/kml:Point/kml:coordinates", namespaces=KML_NAMESPACE + )[0] + if operator_point: + operator_point = str(operator_point).split(",") + operator_location = {"lng": operator_point[0], "lat": operator_point[1]} + if polygons: + if placemark_name.startswith("alt:"): + polygon_coords = get_coordinates_from_kml( + polygons[0].outerBoundaryIs.LinearRing.coordinates + ) + alt_polygons.update({placemark_name: polygon_coords}) + if placemark_name.startswith("speed:"): + if not get_polygon_speed(placemark_name): + raise ValueError( + 'Could not determine Polygon speed from Placemark "{}"'.format( + placemark_name + ) + ) + polygon_coords = get_coordinates_from_kml( + polygons[0].outerBoundaryIs.LinearRing.coordinates + ) + speed_polygons.update({placemark_name: polygon_coords}) + + coords = placemark.xpath( + ".//kml:LineString/kml:coordinates", namespaces=KML_NAMESPACE + ) + if coords: + coordinates = coords + coordinates = get_coordinates_from_kml(coordinates) + return { + str(folder_elem.name): { + "description": get_folder_description(folder_elem), + "speed_polygons": speed_polygons, + "alt_polygons": alt_polygons, + "input_coordinates": coordinates, + "operator_location": operator_location, + } + } + + +def get_coordinates_from_kml(coordinates): + """Returns list of tuples of coordinates. + Args: + coordinates: coordinates element from KML. + """ + if coordinates: + return [ + tuple(float(x.strip()) for x in c.split(",")) + for c in str(coordinates[0]).split(" ") + if c.strip() + ] + + +def get_folder_description(folder_elem): + """Returns folder description from KML. + Args: + folder_elem: Folder element from KML. + """ + description = folder_elem.description + lines = [line for line in str(description).split("\n") if ":" in line] + values = {} + for line in lines: + cols = [col.strip() for col in line.split(":")] + if len(cols) == 2: + values[cols[0]] = cols[1] + return values + + +def get_kml_content(kml_file, from_string=False): + root = get_kml_root(kml_file, from_string) + folders = get_folders(root) + kml_content = {} + for folder in folders: + folder_details = get_folder_details(folder) + if folder_details: + kml_content.update(folder_details) + return kml_content diff --git a/monitoring/uss_qualifier/reports/sequence_view/kml.py b/monitoring/uss_qualifier/reports/sequence_view/kml.py index d7ed661fd4..627e223dad 100644 --- a/monitoring/uss_qualifier/reports/sequence_view/kml.py +++ b/monitoring/uss_qualifier/reports/sequence_view/kml.py @@ -7,7 +7,16 @@ from pykml.factory import KML_ElementMaker as kml from pykml.util import format_xml_with_cdata -from monitoring.monitorlib.scd import priority_of +from monitoring.monitorlib.kml.f3548v21 import ( + f3548v21_styles, + full_op_intent, + op_intent_refs_query, +) +from monitoring.monitorlib.kml.flight_planning import ( + flight_planning_styles, + upsert_flight_plan, +) +from monitoring.monitorlib.kml.generation import query_styles from monitoring.uss_qualifier.reports.sequence_view.summary_types import TestedScenario from uas_standards.astm.f3548.v21.api import ( QueryOperationalIntentReferenceParameters, @@ -17,13 +26,6 @@ from monitoring.monitorlib.errors import stacktrace_string from monitoring.monitorlib.fetch import QueryType, Query -from monitoring.monitorlib.geotemporal import Volume4D -from monitoring.monitorlib.kml import ( - make_placemark_from_volume, - query_styles, - f3548v21_styles, - flight_planning_styles, -) from uas_standards.interuss.automated_testing.flight_planning.v1.api import ( UpsertFlightPlanRequest, UpsertFlightPlanResponse, @@ -153,9 +155,8 @@ def make_scenario_kml(scenario: TestedScenario) -> str: continue try: query_folder.extend(render_info.renderer(**kwargs)) - except TypeError as e: + except (TypeError, KeyError, ValueError) as e: msg = f"Error rendering {render_info.renderer.__name__}" - logger.warning(msg) query_folder.append( kml.Folder( kml.name(msg), @@ -175,72 +176,16 @@ def render_query_op_intent_references( req: QueryOperationalIntentReferenceParameters, resp: QueryOperationalIntentReferenceResponse, ): - if "area_of_interest" not in req or not req.area_of_interest: - return [ - kml.Folder(kml.name("Error: area_of_interest not specified in request")) - ] - v4 = Volume4D.from_f3548v21(req.area_of_interest) - items = "".join( - f"
  • {oi.manager}'s {oi.state.value} {oi.id}[{oi.version}]
  • " - for oi in resp.operational_intent_references - ) - description = ( - f"
      {items}
    " if items else "(no operational intent references found)" - ) - return [ - make_placemark_from_volume( - v4, name="area_of_interest", style_url="#QueryArea", description=description - ) - ] + return [op_intent_refs_query(req, resp)] @query_kml_renderer(QueryType.F3548v21USSGetOperationalIntentDetails) def render_get_op_intent_details(resp: GetOperationalIntentDetailsResponse): - ref = resp.operational_intent.reference - name = f"{ref.manager}'s P{priority_of(resp.operational_intent.details)} {ref.state.value} {ref.id}[{ref.version}] @ {ref.ovn}" - folder = kml.Folder(kml.name(name)) - if "volumes" in resp.operational_intent.details: - for i, v4_f3548 in enumerate(resp.operational_intent.details.volumes): - v4 = Volume4D.from_f3548v21(v4_f3548) - folder.append( - make_placemark_from_volume( - v4, - name=f"Nominal volume {i}", - style_url=f"#F3548v21{resp.operational_intent.reference.state.value}", - ) - ) - if "off_nominal_volumes" in resp.operational_intent.details: - for i, v4_f3548 in enumerate( - resp.operational_intent.details.off_nominal_volumes - ): - v4 = Volume4D.from_f3548v21(v4_f3548) - folder.append( - make_placemark_from_volume( - v4, - name=f"Off-nominal volume {i}", - style_url=f"#F3548v21{resp.operational_intent.reference.state.value}", - ) - ) - return [folder] + return [full_op_intent(resp.operational_intent)] @query_kml_renderer(QueryType.InterUSSFlightPlanningV1UpsertFlightPlan) def render_flight_planning_upsert_flight_plan( req: UpsertFlightPlanRequest, resp: UpsertFlightPlanResponse ): - folder = kml.Folder( - kml.name( - f"Activity {resp.planning_result.value}, flight {resp.flight_plan_status.value}" - ) - ) - basic_info = req.flight_plan.basic_information - for i, v4_flight_planning in enumerate(basic_info.area): - v4 = Volume4D.from_flight_planning_api(v4_flight_planning) - folder.append( - make_placemark_from_volume( - v4, - name=f"Volume {i}", - style_url=f"#{basic_info.usage_state.value}_{basic_info.uas_state.value}", - ) - ) - return [folder] + return [upsert_flight_plan(req, resp)] diff --git a/monitoring/uss_qualifier/resources/netrid/simulation/kml_flights.py b/monitoring/uss_qualifier/resources/netrid/simulation/kml_flights.py index bc28374073..4d8c631842 100755 --- a/monitoring/uss_qualifier/resources/netrid/simulation/kml_flights.py +++ b/monitoring/uss_qualifier/resources/netrid/simulation/kml_flights.py @@ -11,7 +11,7 @@ from implicitdict import StringBasedDateTime from monitoring.monitorlib.geo import flatten, unflatten -from monitoring.monitorlib import kml +from monitoring.monitorlib.kml.parsing import get_polygon_speed, get_kml_content from monitoring.uss_qualifier.resources.netrid.flight_data import ( FullFlightRecord, FlightRecordCollection, @@ -298,7 +298,7 @@ def get_interpolated_value(point, polygons, all_possible_values, round_value=Fal def get_speeds_from_speed_polygons(speed_polygons): - return [kml.get_polygon_speed(n) for n in list(speed_polygons)] + return [get_polygon_speed(n) for n in list(speed_polygons)] def get_flight_state_coordinates(flight_details): @@ -363,7 +363,7 @@ def get_flight_state_coordinates(flight_details): def get_flight_records( kml_content, reference_time, random_seed ) -> FlightRecordCollection: - kml_content = kml.get_kml_content(kml_content.encode("utf-8"), True) + kml_content = get_kml_content(kml_content.encode("utf-8"), True) flight_records = [] for flight_name, flight_details in kml_content.items(): flight_description = flight_details["description"] diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.md b/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.md new file mode 100644 index 0000000000..06c6e9e011 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.md @@ -0,0 +1,13 @@ +# Clear area validation test step fragment + +uss_qualifier verifies with the DSS that there are no operational intents remaining in the area. + +## 🛑 DSS responses check + +If the DSS fails to reply to a query concerning operational intent references in a given area, or fails to allow the deletion of +an operational intent from its own creator, it is in violation of **[astm.f3548.v21.DSS0005,1](../../../requirements/astm/f3548/v21.md)** +or **[astm.f3548.v21.DSS0005,2](../../../requirements/astm/f3548/v21.md)**, and this check will fail. + +## 🛑 Area is clear of op intents check + +If operational intents exist in the 4D area(s) that should be clear, then the current state of the test environment is not suitable to conduct tests so this check will fail. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.py new file mode 100644 index 0000000000..95d963219a --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/astm/utm/clear_area_validation.py @@ -0,0 +1,52 @@ +from typing import List + +from monitoring.monitorlib.fetch import QueryError +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance +from monitoring.uss_qualifier.scenarios.scenario import TestScenario +from uas_standards.astm.f3548.v21.api import OperationalIntentReference + + +def validate_clear_area( + scenario: TestScenario, + dss: DSSInstance, + areas: List[Volume4D], + ignore_self: bool, +) -> List[OperationalIntentReference]: + found_intents = [] + for area in areas: + with scenario.check("DSS responses", [dss.participant_id]) as check: + try: + op_intents, query = dss.find_op_intent(area.to_f3548v21()) + scenario.record_query(query) + except QueryError as e: + scenario.record_queries(e.queries) + query = e.queries[0] + check.record_failed( + summary="Error querying DSS for operational intents", + details=f"See query; {e}", + query_timestamps=[query.request.timestamp], + ) + found_intents.extend(op_intents) + + with scenario.check("Area is clear of op intents") as check: + if ignore_self: + uss_qualifier_sub = dss.client.auth_adapter.get_sub() + op_intents = [ + oi for oi in op_intents if oi.manager != uss_qualifier_sub + ] + if op_intents: + summary = f"{len(op_intents)} operational intent{'s' if len(op_intents) > 1 else ''} found in test area" + details = ( + "The following operational intents were observed even though the area was expected to be clear:\n" + + "\n".join( + f"* {oi.id} managed by {oi.manager}" for oi in op_intents + ) + ) + check.record_failed( + summary=summary, + details=details, + query_timestamps=[query.request.timestamp], + ) + + return found_intents diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md index ae7cdcf506..f1b43b2721 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md @@ -98,6 +98,12 @@ CMSA role in order to transition to the `Nonconforming` state in order to create DSSInstanceResource that provides access to a DSS instance where flight creation/sharing can be verified. +## Prerequisites check test case + +### [Verify area is clear test step](../../clear_area_validation.md) + +While this scenario assumes that the area used is already clear of any pre-existing flights (using, for instance, PrepareFlightPlanners scenario) in order to avoid a large number of area-clearing operations, the scenario will not proceed correctly if the area was left in a dirty state following a previous scenario that was supposed to leave the area clear. This test step verifies that the area is clear. + ## Attempt to plan flight into conflict test case ![Test case summary illustration](assets/attempt_to_plan_flight_into_conflict.svg) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py index e4d03763a6..b250c503ce 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py @@ -8,12 +8,15 @@ validate_flight_intent_templates, ExpectedFlightIntent, ) +from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( + validate_clear_area, +) from monitoring.uss_qualifier.suites.suite import ExecutionContext from uas_standards.astm.f3548.v21.api import ( OperationalIntentReference, ) from uas_standards.astm.f3548.v21.constants import Scope -from monitoring.monitorlib.geotemporal import Volume4DCollection +from monitoring.monitorlib.geotemporal import Volume4DCollection, Volume4D from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance @@ -192,6 +195,17 @@ def run(self, context: ExecutionContext): f"{self.control_uss.config.participant_id}", ) + self.begin_test_case("Prerequisites check") + self.begin_test_step("Verify area is clear") + validate_clear_area( + self, + self.dss, + [Volume4D.from_f3548v21(self._intents_extent)], + ignore_self=True, + ) + self.end_test_step() + self.end_test_case() + self.begin_test_case("Attempt to plan flight into conflict") _ = self._attempt_plan_flight_conflict() self.end_test_case() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py index fb30714858..4a5c6230a9 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/off_nominal_planning/down_uss.py @@ -265,15 +265,14 @@ def _plan_flight_conflict_planned(self): with self.check( "Rejected planning", [self.tested_uss.participant_id] ) as check: - check_details = ( - f"{self.tested_uss.participant_id} indicated {resp.result}" - + f' with notes "{resp.notes}"' - if "notes" in resp and resp.notes - else " with no notes" - ) + msg = f"{self.tested_uss.participant_id} indicated {resp.result}" + if "notes" in resp and resp.notes: + msg += f' with notes "{resp.notes}"' + else: + msg += " with no notes" check.record_failed( summary="Warning (not a failure): planning got rejected, USS may have been more conservative", - details=check_details, + details=msg, ) validator.expect_not_shared() self.end_test_step() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md index f897b7c6ac..bf61d6d7d5 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.md @@ -60,19 +60,9 @@ All USSs are requested to remove all flights from the area under test. **[interuss.automated_testing.flight_planning.ClearArea](../../../requirements/interuss/automated_testing/flight_planning.md)** -### Clear area validation test step +### [Clear area validation test step](clear_area_validation.md) -uss_qualifier verifies with the DSS that there are no operational intents remaining in the area - -#### 🛑 DSS responses check - -If the DSS fails to reply to a query concerning operational intent references in a given area, or fails to allow the deletion of -an operational intent from its own creator, it is in violation of **[astm.f3548.v21.DSS0005,1](../../../requirements/astm/f3548/v21.md)** -or **[astm.f3548.v21.DSS0005,2](../../../requirements/astm/f3548/v21.md)**, and this check will fail. - -#### 🛑 Area is clear of foreign op intents check - -If operational intents from foreign (non-uss_qualifier) users remain in the 4D area(s) following the preceding area clearing, then the current state of the test environment is not suitable to conduct tests so this check will fail. +This step examines whether any operational intents remain. If any foreign (other than uss_qualifier-owned) operational intents remain, then this step's checks will fail. If any uss_qualifier-owned operational intents remain, the checks for this step do not fail but instead we proceed to the next test case. If the area is clear, we skip the next test case. ## uss_qualifier preparation test case @@ -84,15 +74,6 @@ In addition to foreign flight planners, uss_qualifier may have left operational The operational intent references managed by uss_qualifier discovered in the previous test case are removed. -### Clear area validation test step - -uss_qualifier verifies with the DSS that there are no operational intents remaining in the area. - -#### 🛑 DSS responses check - -If the DSS fails to reply to a query concerning operational intent references in a given area, it is in violation of **[astm.f3548.v21.DSS0005,1](../../../requirements/astm/f3548/v21.md)** -or **[astm.f3548.v21.DSS0005,2](../../../requirements/astm/f3548/v21.md)**, and this check will fail. - -#### 🛑 Area is clear check +### [Clear area validation test step](clear_area_validation.md) -If any operational intents remain in the 4D area(s) following the preceding area clearing, then the current state of the test environment is not suitable to conduct tests so this check will fail. +After removing the operational intents of all flight planning participants previously, and just having attempted to remove uss_qualifier-owned operational intents, the area should now be fully clear. diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py index 1a10fa92c6..431ccd5a04 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/prep_planners.py @@ -7,6 +7,9 @@ FlightPlannersResource, FlightIntentsResource, ) +from monitoring.uss_qualifier.scenarios.astm.utm.clear_area_validation import ( + validate_clear_area, +) from monitoring.uss_qualifier.scenarios.astm.utm.dss.test_step_fragments import ( remove_op_intent, ) @@ -63,8 +66,11 @@ def run(self, context): self.end_test_step() self.begin_test_step("Clear area validation") - remaining_op_intents = self._validate_clear_area( - "Area is clear of foreign op intents", ignore_self=True + remaining_op_intents = validate_clear_area( + self, + self.dss, + self.areas, + ignore_self=True, ) self.end_test_step() @@ -78,54 +84,13 @@ def run(self, context): self.end_test_step() self.begin_test_step("Clear area validation") - self._validate_clear_area("Area is clear", ignore_self=False) + validate_clear_area(self, self.dss, self.areas, ignore_self=False) self.end_test_step() self.end_test_case() self.end_test_scenario() - def _validate_clear_area( - self, check_name: str, ignore_self: bool - ) -> List[OperationalIntentReference]: - found_intents = [] - for area in self.areas: - with self.check("DSS responses", [self.dss.participant_id]) as check: - try: - op_intents, query = self.dss.find_op_intent(area.to_f3548v21()) - self.record_query(query) - except QueryError as e: - self.record_queries(e.queries) - query = e.queries[0] - check.record_failed( - summary="Error querying DSS for operational intents", - details=f"See query; {e}", - query_timestamps=[query.request.timestamp], - ) - found_intents.extend(op_intents) - - with self.check(check_name) as check: - if ignore_self: - uss_qualifier_sub = self.dss.client.auth_adapter.get_sub() - op_intents = [ - oi for oi in op_intents if oi.manager != uss_qualifier_sub - ] - if op_intents: - summary = f"{len(op_intents)} operational intent{'s' if len(op_intents) > 1 else ''} found in cleared area" - details = ( - "The following operational intents were observed even after clearing the area:\n" - + "\n".join( - f"* {oi.id} managed by {oi.manager}" for oi in op_intents - ) - ) - check.record_failed( - summary=summary, - details=details, - query_timestamps=[query.request.timestamp], - ) - - return found_intents - def _remove_my_op_intents( self, my_op_intents: List[OperationalIntentReference] ) -> None: diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index cb316dbac5..38aa5bc7da 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -209,18 +209,15 @@ def modify_activated_flight_intent( resp.result == InjectFlightResponseResult.Rejected or resp.result == InjectFlightResponseResult.ConflictWithFlight ): - check_details = ( - f"{flight_planner.participant_id} indicated {resp.result}" - ) - check_details += ( - f' with notes "{resp.notes}"' - if "notes" in resp and resp.notes - else " with no notes" - ) + msg = f"{flight_planner.participant_id} indicated {resp.result}" + if "notes" in resp and resp.notes: + msg += f' with notes "{resp.notes}"' + else: + msg += " with no notes" check.record_failed( summary="Warning (not a failure): modification got rejected but a pre-existing conflict was present", severity=Severity.Low, - details=check_details, + details=msg, ) else: @@ -278,18 +275,17 @@ def submit_flight_intent( query_timestamps=[q.request.timestamp for q in e.queries], ) scenario.record_query(query) - check_details = ( - f'{flight_planner.participant_id} indicated {resp.result} rather than the expected {" or ".join(expected_results)}' - + f' with notes "{resp.notes}"' - if "notes" in resp and resp.notes - else " with no notes" - ) + msg = f'{flight_planner.participant_id} indicated {resp.result} rather than the expected {" or ".join(expected_results)}' + if "notes" in resp and resp.notes: + msg += f' with notes "{resp.notes}"' + else: + msg += " with no notes" if resp.result not in expected_results: check.record_failed( summary=f"Flight unexpectedly {resp.result}", severity=Severity.High, - details=check_details, + details=msg, query_timestamps=[query.request.timestamp], ) @@ -302,7 +298,7 @@ def submit_flight_intent( check.record_failed( summary=f"Flight unexpectedly {resp.result}", severity=Severity.High, - details=check_details, + details=msg, query_timestamps=[query.request.timestamp], ) @@ -535,14 +531,11 @@ def submit_flight( return resp, None result = (resp.activity_result, resp.flight_plan_status) - check_details = ( - f'{flight_planner.participant_id} indicated {result} rather than the expected {" or ".join([f"({expected_result[0]}, {expected_result[1]})" for expected_result in expected_results])}' - + ( - f' with notes "{resp.notes}"' - if "notes" in resp and resp.notes - else " with no notes" - ) - ) + msg = f'{flight_planner.participant_id} indicated {result} rather than the expected {" or ".join([f"({expected_result[0]}, {expected_result[1]})" for expected_result in expected_results])}' + if "notes" in resp and resp.notes: + msg += f' with notes "{resp.notes}"' + else: + msg += " with no notes" for unexpected_result, failed_test_check in failed_checks.items(): check_name = failed_test_check @@ -553,14 +546,14 @@ def submit_flight( if resp.activity_result == unexpected_result: specific_failed_check.record_failed( summary=f"Flight unexpectedly {result}", - details=check_details, + details=msg, query_timestamps=[query.request.timestamp], ) if result not in expected_results: check.record_failed( summary=f"Flight unexpectedly {result}", - details=check_details, + details=msg, query_timestamps=[query.request.timestamp], ) diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md index d5a8043b90..4995c0507f 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.md @@ -38,7 +38,7 @@ astm
    .f3548
    .v21
    DSS0005,1 Implemented - ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted + ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Nominal planning: not permitted conflict with equal priority
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted DSS0005,2 diff --git a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md index 300bb1a721..d3e178d854 100644 --- a/monitoring/uss_qualifier/suites/faa/uft/message_signing.md +++ b/monitoring/uss_qualifier/suites/faa/uft/message_signing.md @@ -21,7 +21,7 @@ astm
    .f3548
    .v21
    DSS0005,1 Implemented - ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted + ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Nominal planning: not permitted conflict with equal priority
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted DSS0005,2 diff --git a/monitoring/uss_qualifier/suites/uspace/flight_auth.md b/monitoring/uss_qualifier/suites/uspace/flight_auth.md index 02e77b87a7..9104a9a833 100644 --- a/monitoring/uss_qualifier/suites/uspace/flight_auth.md +++ b/monitoring/uss_qualifier/suites/uspace/flight_auth.md @@ -22,7 +22,7 @@ astm
    .f3548
    .v21
    DSS0005,1 Implemented - ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted + ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Nominal planning: not permitted conflict with equal priority
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted DSS0005,2 diff --git a/monitoring/uss_qualifier/suites/uspace/required_services.md b/monitoring/uss_qualifier/suites/uspace/required_services.md index 249dac8a41..e75431c5bd 100644 --- a/monitoring/uss_qualifier/suites/uspace/required_services.md +++ b/monitoring/uss_qualifier/suites/uspace/required_services.md @@ -457,7 +457,7 @@ astm
    .f3548
    .v21
    DSS0005,1 Implemented - ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted + ASTM F3548 flight planners preparation
    ASTM F3548-21 UTM DSS Operational Intent Reference Access Control
    ASTM SCD DSS: Interfaces authentication
    ASTM SCD DSS: Operational Intent Reference Key Validation
    ASTM SCD DSS: Operational Intent Reference Synchronization
    ASTM SCD DSS: Subscription Simple
    ASTM SCD DSS: Subscription Synchronization
    ASTM SCD DSS: Subscription Validation
    ASTM SCD DSS: Subscription and entity interaction
    Nominal planning: not permitted conflict with equal priority
    Off-Nominal planning: down USS
    Off-Nominal planning: down USS with equal priority conflicts not permitted DSS0005,2 diff --git a/monitoring/uss_qualifier/test_data/make_flight_intent_kml.py b/monitoring/uss_qualifier/test_data/make_flight_intent_kml.py index 8588758d9a..aa6639f41b 100644 --- a/monitoring/uss_qualifier/test_data/make_flight_intent_kml.py +++ b/monitoring/uss_qualifier/test_data/make_flight_intent_kml.py @@ -12,7 +12,8 @@ import yaml from implicitdict import ImplicitDict -from monitoring.monitorlib.kml import make_placemark_from_volume, flight_planning_styles +from monitoring.monitorlib.kml.flight_planning import flight_planning_styles +from monitoring.monitorlib.kml.generation import make_placemark_from_volume from monitoring.monitorlib.temporal import Time, TimeDuringTest from monitoring.uss_qualifier.fileio import load_dict_with_references, resolve_filename from monitoring.uss_qualifier.resources.flight_planning.flight_intent import (