Skip to content

Commit

Permalink
[monitorlib, uss_qualifier] Add flight_planning client and resource (#…
Browse files Browse the repository at this point in the history
…284)

* Add flight_planning client and resource

* Fix message signing

* Check execution style
  • Loading branch information
BenjaminPelletier authored Oct 23, 2023
1 parent 726592d commit 8a39765
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 37 deletions.
175 changes: 175 additions & 0 deletions monitoring/monitorlib/clients/flight_planning/client_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import uuid

from implicitdict import ImplicitDict
from monitoring.monitorlib.clients.flight_planning.client import (
FlightPlannerClient,
PlanningActivityError,
)
from monitoring.monitorlib.clients.flight_planning.test_preparation import (
TestPreparationActivityResponse,
)

from monitoring.monitorlib.clients.flight_planning.flight_info import (
FlightInfo,
FlightID,
ExecutionStyle,
)
from monitoring.monitorlib.clients.flight_planning.planning import (
PlanningActivityResponse,
)
from monitoring.monitorlib.fetch import query_and_describe
from monitoring.monitorlib.geotemporal import Volume4D
from monitoring.monitorlib.infrastructure import UTMClientSession

from uas_standards.interuss.automated_testing.flight_planning.v1 import api
from uas_standards.interuss.automated_testing.flight_planning.v1.constants import Scope


class V1FlightPlannerClient(FlightPlannerClient):
_session: UTMClientSession

def __init__(self, session: UTMClientSession):
self._session = session

def _inject(
self,
flight_plan_id: FlightID,
flight_info: FlightInfo,
execution_style: ExecutionStyle,
) -> PlanningActivityResponse:
flight_plan = ImplicitDict.parse(flight_info, api.FlightPlan)
req = api.UpsertFlightPlanRequest(
flight_plan=flight_plan,
execution_style=execution_style,
request_id=str(uuid.uuid4()),
)

op = api.OPERATIONS[api.OperationID.UpsertFlightPlan]
url = op.path.format(flight_plan_id=flight_plan_id)
query = query_and_describe(
self._session, op.verb, url, json=req, scope=Scope.Plan
)
if query.status_code != 200 and query.status_code != 201:
raise PlanningActivityError(
f"Attempt to plan flight returned status {query.status_code} rather than 200 as expected",
query,
)
try:
resp: api.UpsertFlightPlanResponse = ImplicitDict.parse(
query.response.json, api.UpsertFlightPlanResponse
)
except ValueError as e:
raise PlanningActivityError(
f"Response to plan flight could not be parsed: {str(e)}", query
)

response = PlanningActivityResponse(
flight_id=flight_plan_id,
queries=[query],
activity_result=resp.planning_result,
flight_plan_status=resp.flight_plan_status,
)
return response

def try_plan_flight(
self, flight_info: FlightInfo, execution_style: ExecutionStyle
) -> PlanningActivityResponse:
return self._inject(str(uuid.uuid4()), flight_info, execution_style)

def try_update_flight(
self,
flight_id: FlightID,
updated_flight_info: FlightInfo,
execution_style: ExecutionStyle,
) -> PlanningActivityResponse:
return self._inject(flight_id, updated_flight_info, execution_style)

def try_end_flight(
self, flight_id: FlightID, execution_style: ExecutionStyle
) -> PlanningActivityResponse:
if execution_style != ExecutionStyle.IfAllowed:
raise NotImplementedError(
"Only IfAllowed execution style is currently allowed"
)
op = api.OPERATIONS[api.OperationID.DeleteFlightPlan]
url = op.path.format(flight_plan_id=flight_id)
query = query_and_describe(self._session, op.verb, url, scope=Scope.Plan)
if query.status_code != 200:
raise PlanningActivityError(
f"Attempt to delete flight plan returned status {query.status_code} rather than 200 as expected",
query,
)
try:
resp: api.DeleteFlightPlanResponse = ImplicitDict.parse(
query.response.json, api.DeleteFlightPlanResponse
)
except ValueError as e:
raise PlanningActivityError(
f"Response to delete flight plan could not be parsed: {str(e)}", query
)

response = PlanningActivityResponse(
flight_id=flight_id,
queries=[query],
activity_result=resp.planning_result,
flight_plan_status=resp.flight_plan_status,
)
return response

def report_readiness(self) -> TestPreparationActivityResponse:
op = api.OPERATIONS[api.OperationID.GetStatus]
query = query_and_describe(
self._session, op.verb, op.path, scope=Scope.DirectAutomatedTest
)
if query.status_code != 200:
raise PlanningActivityError(
f"Attempt to get interface status returned status {query.status_code} rather than 200 as expected",
query,
)
try:
resp: api.StatusResponse = ImplicitDict.parse(
query.response.json, api.StatusResponse
)
except ValueError as e:
raise PlanningActivityError(
f"Response to get interface status could not be parsed: {str(e)}", query
)

if resp.status == api.StatusResponseStatus.Ready:
errors = []
elif resp.status == api.StatusResponseStatus.Starting:
errors = ["Flight planning v1 interface is still starting (not ready)"]
else:
errors = [f"Unrecognized status '{resp.status}'"]

return TestPreparationActivityResponse(errors=errors, queries=[query])

def clear_area(self, area: Volume4D) -> TestPreparationActivityResponse:
req = api.ClearAreaRequest(
request_id=str(uuid.uuid4()), extent=area.to_interuss_scd_api()
)

op = api.OPERATIONS[api.OperationID.ClearArea]
query = query_and_describe(
self._session, op.verb, op.path, json=req, scope=Scope.DirectAutomatedTest
)
if query.status_code != 200:
raise PlanningActivityError(
f"Attempt to clear area returned status {query.status_code} rather than 200 as expected",
query,
)
try:
resp: api.ClearAreaResponse = ImplicitDict.parse(
query.response.json, api.ClearAreaResponse
)
except ValueError as e:
raise PlanningActivityError(
f"Response to clear area could not be parsed: {str(e)}", query
)

if resp.outcome.success:
errors = None
else:
errors = [f"[{resp.outcome.timestamp}]: {resp.outcome.message}"]

return TestPreparationActivityResponse(errors=errors, queries=[query])
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ v1:
auth_adapter: utm_auth
specification:
flight_planners:
# uss1 is the mock_uss directly exposing scdsc functionality
# uss1 is the mock_uss directly exposing flight planning functionality
- participant_id: uss1
injection_base_url: http://scdsc.uss1.localutm/scdsc
# uss2 is another mock_uss directly exposing scdsc functionality
scd_injection_base_url: http://scdsc.uss1.localutm/scdsc
# uss2 is another mock_uss directly exposing flight planning functionality
- participant_id: uss2
injection_base_url: http://scdsc.uss2.localutm/scdsc
scd_injection_base_url: http://scdsc.uss2.localutm/scdsc

# Details of conflicting flights (used in nominal planning scenario)
conflicting_flights:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ all_flight_planners:
specification:
flight_planners:
- participant_id: uss1
injection_base_url: http://scdsc.uss1.localutm/scdsc
scd_injection_base_url: http://scdsc.uss1.localutm/scdsc
local_debug: true

- participant_id: uss2
injection_base_url: http://scdsc.uss2.localutm/scdsc
scd_injection_base_url: http://scdsc.uss2.localutm/scdsc
local_debug: true

uss1_flight_planner:
Expand All @@ -134,7 +134,7 @@ uss1_flight_planner:
specification:
flight_planner:
participant_id: uss1
injection_base_url: http://scdsc.uss1.localutm/scdsc
scd_injection_base_url: http://scdsc.uss1.localutm/scdsc
local_debug: true

uss2_flight_planner:
Expand All @@ -145,7 +145,7 @@ uss2_flight_planner:
specification:
flight_planner:
participant_id: uss2
injection_base_url: http://scdsc.uss2.localutm/scdsc
scd_injection_base_url: http://scdsc.uss2.localutm/scdsc
local_debug: true

# ===== F3548 =====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ all_flight_planners:
specification:
flight_planners:
- participant_id: uss1
injection_base_url: http://localhost:8074/scdsc
scd_injection_base_url: http://localhost:8074/scdsc
local_debug: true

- participant_id: uss2
injection_base_url: http://localhost:8094/scdsc
scd_injection_base_url: http://localhost:8094/scdsc
local_debug: true

uss1_flight_planner:
Expand All @@ -134,7 +134,7 @@ uss1_flight_planner:
specification:
flight_planner:
participant_id: uss1
injection_base_url: http://localhost:8074/scdsc
scd_injection_base_url: http://localhost:8074/scdsc
local_debug: true

uss2_flight_planner:
Expand All @@ -145,7 +145,7 @@ uss2_flight_planner:
specification:
flight_planner:
participant_id: uss2
injection_base_url: http://localhost:8094/scdsc
scd_injection_base_url: http://localhost:8094/scdsc
local_debug: true

# ===== F3548 =====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ v1:
specification:
flight_planners:
- participant_id: uss1
injection_base_url: http://host.docker.internal:8074/scdsc
scd_injection_base_url: http://host.docker.internal:8074/scdsc
- participant_id: uss2
injection_base_url: http://host.docker.internal:8074/scdsc
scd_injection_base_url: http://host.docker.internal:8074/scdsc
- participant_id: mock_uss
injection_base_url: http://host.docker.internal:8074/scdsc
scd_injection_base_url: http://host.docker.internal:8074/scdsc
mock_uss:
resource_type: resources.interuss.mock_uss.client.MockUSSResource
dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from monitoring.monitorlib.clients.flight_planning.client_scd import (
SCDFlightPlannerClient,
)
from monitoring.monitorlib.clients.flight_planning.client_v1 import (
V1FlightPlannerClient,
)
from monitoring.monitorlib.clients.flight_planning.flight_info import (
ExecutionStyle,
FlightInfo,
Expand Down Expand Up @@ -47,20 +50,33 @@ class FlightPlannerConfiguration(ImplicitDict):
participant_id: str
"""ID of the flight planner into which test data can be injected"""

injection_base_url: str
"""Base URL for the flight planner's implementation of the interfaces/automated-testing/scd/scd.yaml API"""
scd_injection_base_url: Optional[str]
"""Base URL for the flight planner's implementation of the interfaces/automated_testing/scd/v1/scd.yaml API"""

v1_base_url: Optional[str]
"""Base URL for the flight planner's implementation of the interfaces/automated_testing/flight_planning/v1/flight_planning.yaml API"""

timeout_seconds: Optional[float] = None
"""Number of seconds to allow for requests to this flight planner. If None, use default."""

def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
try:
urlparse(self.injection_base_url)
except ValueError:
if "v1_base_url" not in self and "scd_injection_base_url" not in self:
raise ValueError(
"FlightPlannerConfiguration.injection_base_url must be a URL"
"One of `scd_injection_base_url` or `v1_base_url` must be specified"
)
if "scd_injection_base_url" in self and self.scd_injection_base_url:
try:
urlparse(self.scd_injection_base_url)
except ValueError:
raise ValueError(
"FlightPlannerConfiguration.scd_injection_base_url must be a URL"
)
if "v1_base_url" in self and self.v1_base_url:
try:
urlparse(self.v1_base_url)
except ValueError:
raise ValueError("FlightPlannerConfiguration.v1_base_url must be a URL")


class FlightPlanner:
Expand All @@ -74,17 +90,23 @@ def __init__(
auth_adapter: infrastructure.AuthAdapter,
):
self.config = config
session = infrastructure.UTMClientSession(
self.config.injection_base_url, auth_adapter, config.timeout_seconds
)
self.scd_client = SCDFlightPlannerClient(session)
if "scd_injection_base_url" in config and config.scd_injection_base_url:
session = infrastructure.UTMClientSession(
self.config.scd_injection_base_url, auth_adapter, config.timeout_seconds
)
self.client = SCDFlightPlannerClient(session)
elif "v1_base_url" in config and config.v1_base_url:
session = infrastructure.UTMClientSession(
self.config.v1_base_url, auth_adapter, config.timeout_seconds
)
self.client = V1FlightPlannerClient(session)

# Flights injected by this target.
self.created_flight_ids: Set[str] = set()

def __repr__(self):
return "FlightPlanner({}, {})".format(
self.config.participant_id, self.config.injection_base_url
self.config.participant_id, self.config.scd_injection_base_url
)

@property
Expand Down Expand Up @@ -145,15 +167,15 @@ def request_flight(

if not flight_id:
try:
resp = self.scd_client.try_plan_flight(
resp = self.client.try_plan_flight(
flight_info, ExecutionStyle.IfAllowed
)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)
flight_id = resp.flight_id
else:
try:
resp = self.scd_client.try_update_flight(
resp = self.client.try_update_flight(
flight_id, flight_info, ExecutionStyle.IfAllowed
)
except PlanningActivityError as e:
Expand Down Expand Up @@ -193,7 +215,7 @@ def cleanup_flight(
self, flight_id: str
) -> Tuple[DeleteFlightResponse, fetch.Query]:
try:
resp = self.scd_client.try_end_flight(flight_id, ExecutionStyle.IfAllowed)
resp = self.client.try_end_flight(flight_id, ExecutionStyle.IfAllowed)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)

Expand All @@ -214,14 +236,14 @@ def cleanup_flight(

def get_readiness(self) -> Tuple[Optional[str], Query]:
try:
resp = self.scd_client.report_readiness()
resp = self.client.report_readiness()
except PlanningActivityError as e:
return str(e), e.queries[0]
return None, resp.queries[0]

def clear_area(self, extent: Volume4D) -> Tuple[ClearAreaResponse, fetch.Query]:
try:
resp = self.scd_client.clear_area(extent)
resp = self.client.clear_area(extent)
except PlanningActivityError as e:
raise QueryError(str(e), e.queries)
success = False if resp.errors else True
Expand Down
Loading

0 comments on commit 8a39765

Please sign in to comment.