Skip to content

Commit

Permalink
[mock_uss] Configure mock_uss locality at runtime (#239)
Browse files Browse the repository at this point in the history
Add configurable locality for mock_uss
  • Loading branch information
BenjaminPelletier authored Oct 11, 2023
1 parent a831fea commit a76b519
Show file tree
Hide file tree
Showing 36 changed files with 656 additions and 49 deletions.
2 changes: 2 additions & 0 deletions monitoring/mock_uss/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
webapp.config.get(config.KEY_TOKEN_PUBLIC_KEY),
webapp.config.get(config.KEY_TOKEN_AUDIENCE),
)

MOCK_USS_CONFIG_SCOPE = "interuss.mock_uss.configure"
5 changes: 1 addition & 4 deletions monitoring/mock_uss/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from monitoring.mock_uss import import_environment_variable
from monitoring.monitorlib import auth_validation
from monitoring.monitorlib.locality import Locality


KEY_TOKEN_PUBLIC_KEY = "MOCK_USS_PUBLIC_KEY"
Expand All @@ -27,7 +26,5 @@
mutator=lambda s: set(svc.strip().lower() for svc in s.split(",")),
)
import_environment_variable(KEY_DSS_URL, required=False)
import_environment_variable(
KEY_BEHAVIOR_LOCALITY, default="CHE", mutator=Locality.from_locale
)
import_environment_variable(KEY_BEHAVIOR_LOCALITY, default="US.IndustryCollaboration")
import_environment_variable(KEY_CODE_VERSION, default="Unknown")
Empty file.
29 changes: 29 additions & 0 deletions monitoring/mock_uss/dynamic_configuration/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json

from implicitdict import ImplicitDict
from monitoring.mock_uss import require_config_value, webapp
from monitoring.mock_uss.config import KEY_BEHAVIOR_LOCALITY
from monitoring.monitorlib.locality import Locality, LocalityCode
from monitoring.monitorlib.multiprocessing import SynchronizedValue


require_config_value(KEY_BEHAVIOR_LOCALITY)


class DynamicConfiguration(ImplicitDict):
locale: LocalityCode


db = SynchronizedValue(
DynamicConfiguration(locale=LocalityCode(webapp.config[KEY_BEHAVIOR_LOCALITY])),
decoder=lambda b: ImplicitDict.parse(
json.loads(b.decode("utf-8")), DynamicConfiguration
),
capacity_bytes=10000,
)


def get_locality() -> Locality:
with db as tx:
code = tx.locale
return Locality.from_locale(code)
48 changes: 48 additions & 0 deletions monitoring/mock_uss/dynamic_configuration/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Tuple

import flask
from implicitdict import ImplicitDict

from monitoring.mock_uss import webapp
from monitoring.mock_uss.auth import requires_scope, MOCK_USS_CONFIG_SCOPE
from monitoring.mock_uss.dynamic_configuration.configuration import db, get_locality
from monitoring.monitorlib.clients.mock_uss.locality import (
PutLocalityRequest,
GetLocalityResponse,
)
from monitoring.monitorlib.locality import Locality


@webapp.route("/configuration/locality", methods=["GET"])
def locality_get() -> Tuple[str, int]:
return flask.jsonify(
GetLocalityResponse(locality_code=get_locality().locality_code())
)


@webapp.route("/configuration/locality", methods=["PUT"])
@requires_scope([MOCK_USS_CONFIG_SCOPE]) # TODO: use separate public key for this
def locality_set() -> Tuple[str, int]:
"""Set the locality of the mock_uss."""
try:
json = flask.request.json
if json is None:
raise ValueError("Request did not contain a JSON payload")
req: PutLocalityRequest = ImplicitDict.parse(json, PutLocalityRequest)
except ValueError as e:
msg = f"Change locality unable to parse JSON: {str(e)}"
return msg, 400

# Make sure this is a valid locality
try:
Locality.from_locale(req.locality_code)
except ValueError as e:
msg = f"Invalid locality_code: {str(e)}"
return msg, 400

with db as tx:
tx.locale = req.locality_code

return flask.jsonify(
GetLocalityResponse(locality_code=get_locality().locality_code())
)
2 changes: 1 addition & 1 deletion monitoring/mock_uss/interaction_logging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def interaction_log_after_request(response):
datetime.datetime.utcnow() - flask.current_app.custom_profiler["start"]
).total_seconds()
# TODO: Make this configurable instead of hardcoding exactly these query types
if "/uss/v1/" in flask.request.url_rule.rule:
if flask.request.url_rule is not None and "/uss/v1/" in flask.request.url_rule.rule:
query = describe_flask_query(flask.request, response, elapsed_s)
log_interaction(QueryDirection.Incoming, query)
return response
3 changes: 3 additions & 0 deletions monitoring/mock_uss/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ def handle_exception(e):
flask.jsonify({"message": "Unhandled {}: {}".format(type(e).__name__, str(e))}),
500,
)


from .dynamic_configuration import routes
9 changes: 4 additions & 5 deletions monitoring/mock_uss/scdsc/routes_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import traceback
from datetime import datetime, timedelta
import time
from functools import wraps
from typing import List, Tuple
import uuid

import flask
from implicitdict import ImplicitDict, StringBasedDateTime
from loguru import logger
import requests.exceptions
from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api

from monitoring.mock_uss.dynamic_configuration.configuration import get_locality
from uas_standards.interuss.automated_testing.scd.v1.api import (
InjectFlightRequest,
InjectFlightResponse,
Expand Down Expand Up @@ -42,7 +42,7 @@
from monitoring.mock_uss.scdsc.routes_scdsc import op_intent_from_flightrecord
from monitoring.monitorlib.geo import Polygon
from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection
from monitoring.mock_uss.config import KEY_BASE_URL, KEY_BEHAVIOR_LOCALITY
from monitoring.mock_uss.config import KEY_BASE_URL
from monitoring.monitorlib import versioning
from monitoring.monitorlib.clients import scd as scd_client
from monitoring.monitorlib.fetch import QueryError
Expand All @@ -56,7 +56,6 @@


require_config_value(KEY_BASE_URL)
require_config_value(KEY_BEHAVIOR_LOCALITY)

DEADLOCK_TIMEOUT = timedelta(seconds=5)

Expand Down Expand Up @@ -181,7 +180,7 @@ def scdsc_inject_flight(flight_id: str) -> Tuple[str, int]:

def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, int]:
pid = os.getpid()
locality = webapp.config[KEY_BEHAVIOR_LOCALITY]
locality = get_locality()

def log(msg: str):
logger.debug(f"[inject_flight/{pid}:{flight_id}] {msg}")
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions monitoring/monitorlib/clients/mock_uss/locality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from implicitdict import ImplicitDict

from monitoring.monitorlib.locality import LocalityCode


class PutLocalityRequest(ImplicitDict):
"""API object to request a change in locality"""

locality_code: LocalityCode


class GetLocalityResponse(ImplicitDict):
"""API object defining a response indicating locality"""

locality_code: LocalityCode
7 changes: 2 additions & 5 deletions monitoring/monitorlib/infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,8 @@ def adjust_request_kwargs(self, kwargs):
def auth(
prepared_request: requests.PreparedRequest,
) -> requests.PreparedRequest:
if not scopes:
raise ValueError(
"All tests must specify auth scope for all session requests. Either specify as an argument for each individual HTTP call, or decorate the test with @default_scope."
)
self.auth_adapter.add_headers(prepared_request, scopes)
if scopes:
self.auth_adapter.add_headers(prepared_request, scopes)
return prepared_request

kwargs["auth"] = auth
Expand Down
37 changes: 27 additions & 10 deletions monitoring/monitorlib/locality.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
class Locality(ABC):
_NOT_IMPLEMENTED_MSG = "All methods of base Locality class must be implemented by each specific subclass"

@classmethod
def locality_code(cls) -> str:
raise NotImplementedError(
"locality_code classmethod must be overridden by each specific subclass"
)

@abstractmethod
def is_uspace_applicable(self) -> bool:
"""Returns true iff U-space rules apply to this locality"""
raise NotImplementedError(Locality._NOT_IMPLEMENTED_MSG)

@abstractmethod
def allows_same_priority_intersections(self, priority: int) -> bool:
"""Returns true iff locality allows intersections between two operations at this priority level"""
"""Returns true iff locality allows intersections between two operations at this priority level for ASTM F3548-21"""
raise NotImplementedError(Locality._NOT_IMPLEMENTED_MSG)

def __str__(self):
Expand All @@ -28,25 +34,36 @@ def __str__(self):
def from_locale(locality_code: LocalityCode) -> LocalityType:
current_module = sys.modules[__name__]
for name, obj in inspect.getmembers(current_module, inspect.isclass):
if name == locality_code:
if not issubclass(obj, Locality):
raise ValueError(
f"Locality '{name}' is not a subclass of the Locality abstract base class"
)
return obj()
if issubclass(obj, Locality) and obj != Locality:
if obj.locality_code() == locality_code:
return obj()
raise ValueError(
f"Could not find Locality implementation for Locality code '{locality_code}' (expected to find a subclass of the Locality astract base class named {locality_code})"
f"Could not find Locality implementation for Locality code '{locality_code}' (expected to find a subclass of the Locality astract base class where classmethod locality_code returns '{locality_code}')"
)


LocalityType = TypeVar("LocalityType", bound=Locality)


class CHE(Locality):
"""Switzerland"""
class Switzerland(Locality):
@classmethod
def locality_code(cls) -> str:
return "CHE"

def is_uspace_applicable(self) -> bool:
return True

def allows_same_priority_intersections(self, priority: int) -> bool:
return False


class UnitedStatesIndustryCollaboration(Locality):
@classmethod
def locality_code(cls) -> str:
return "US.IndustryCollaboration"

def is_uspace_applicable(self) -> bool:
return False

def allows_same_priority_intersections(self, priority: int) -> bool:
return False
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .with_locality import WithLocality
Loading

0 comments on commit a76b519

Please sign in to comment.