forked from interuss/monitoring
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Position Reporter - initial implementation
- Loading branch information
1 parent
7b9820b
commit ce82dbc
Showing
13 changed files
with
637 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Position Reporter | ||
|
||
Reports positions as per the position report plan sent to it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import inspect | ||
import os | ||
from typing import Optional, Callable, Any | ||
import logging | ||
from monitoring.position_reporter.server import PositionReporter | ||
from monitoring.monitorlib import auth | ||
|
||
webapp = PositionReporter(__name__) | ||
|
||
|
||
def import_environment_variable( | ||
var_name: str, | ||
required: bool = True, | ||
default: Optional[str] = None, | ||
mutator: Optional[Callable[[str], Any]] = None, | ||
) -> None: | ||
"""Import a value from a named environment variable into the webapp configuration. | ||
Args: | ||
var_name: Environment variable name (key). Also used as the webapp configuration key for that variable. | ||
required: Whether the variable must be specified by the user. If True, a ValueError will be raised if the | ||
variable is not specified by the user. If False, the webapp configuration will not be populated if no | ||
default is provided. If default is specified, the default value is treated as specification by the user. | ||
default: If the variable is not required, then use this value when it is not specified by the user. The default | ||
value should be the string from the environment variable rather than the output of the mutator, if present. | ||
mutator: If specified, apply this function to the string value of the environment variable to obtain the | ||
variable to actually store in the configuration. | ||
""" | ||
if var_name in os.environ: | ||
str_value = os.environ[var_name] | ||
elif default is not None: | ||
str_value = default | ||
elif required: | ||
stack = inspect.stack() | ||
raise ValueError( | ||
f"System cannot proceed because required environment variable '{var_name}' was not found. Required from {stack[1].filename}:{stack[1].lineno}" | ||
) | ||
else: | ||
str_value = None | ||
|
||
if str_value is not None: | ||
webapp.config[var_name] = str_value if mutator is None else mutator(str_value) | ||
|
||
|
||
def require_config_value(config_key: str) -> None: | ||
if config_key not in webapp.config: | ||
stack = inspect.stack() | ||
raise ValueError( | ||
f"System cannot proceed because required configuration key '{config_key}' was not found. Required from {stack[1].filename}:{stack[1].lineno}" | ||
) | ||
|
||
|
||
fmt = "%(asctime)s, %(filename)s:%(lineno)-4s | %(message)s" | ||
datefmt = "%Y-%m-%d:%H:%M:%S" | ||
# Configure the root logger | ||
logging.basicConfig( | ||
format=fmt, | ||
datefmt=datefmt, | ||
level=logging.INFO | ||
) | ||
|
||
AUTH_SPEC = "POS_REP_AUTH_SPEC" | ||
import_environment_variable(AUTH_SPEC) | ||
require_config_value(AUTH_SPEC) | ||
|
||
msg = ( | ||
"################################################################################\n" | ||
+ "################################ Configuration ################################\n" | ||
+ "\n".join("## {}: {}".format(key, webapp.config[key]) for key in webapp.config) | ||
+ "\n" | ||
+ "################################################################################" | ||
) | ||
logging.info("Configuration:\n" + msg) | ||
|
||
auth_client = auth.make_auth_adapter(webapp.config[AUTH_SPEC]) | ||
|
||
|
||
def get_auth_client(): | ||
return auth_client | ||
|
||
|
||
from monitoring.position_reporter import routes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
openapi: 3.0.2 | ||
info: | ||
title: Position Report Plan Interface | ||
version: 0.4.4 | ||
description: >- | ||
This interface is between uss_qualifier and position_reporter. | ||
Uss_qualifier posts position report plan to position reporter. Position reporter then send the position reports | ||
to the specified url of a participant USS. | ||
components: | ||
securitySchemes: | ||
Authority: | ||
type: oauth2 | ||
flows: | ||
clientCredentials: | ||
tokenUrl: https://auth.example.com/oauth/token | ||
scopes: | ||
test.flight_data.direct_automated_test: |- | ||
Test framework may determine the test-readiness of the Position Reporter. | ||
test.flight_data.position_report_plan: |- | ||
Test framework send the position report plan to the Position Reporter. | ||
description: |- | ||
Authorization from, or on behalf of, an authorization authority, augmenting standard strategic conflict detection or other similar authorization for the purpose of automated testing. | ||
This authority will issue access tokens that are JSON Web Tokens as defined in RFC 7519, using the `RS256` algorithm for the signature, and publish to all providers the public key for verifying that signature. | ||
The following fields must be included in the JWT claim for access tokens issued by this authority: | ||
* `exp`, with a time no further than 1 hour in the future. | ||
* `sub`, with unique ID of the client requesting the access token. | ||
* `scope`, with a string composed of a space-separated list of strings indicating the scopes granted, per RFC 6749. | ||
* `aud`, with the fully qualified domain name of the URL the access token will be used to access. For example, if a USS were querying the endpoint at https://uss.example.com:8888/flight_planning/v1/flight_plans/db41f454-b255-470e-98d6-4c5096a295a1, the access token included in the request should specify `"aud": "uss.example.com"`. | ||
Clients must provide these access tokens in an `Authorization` header in the form `Bearer <token>` in accordance with RFC 6750. | ||
schemas: | ||
FlightPlanID: | ||
description: >- | ||
String identifying a user flight plan. Format matches a version-4 UUID according to RFC 4122. | ||
maxLength: 36 | ||
minLength: 36 | ||
type: string | ||
format: uuid | ||
pattern: >- | ||
^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-4[0-9a-fA-F]{3}\-[8-b][0-9a-fA-F]{3}\-[0-9a-fA-F]{12}$ | ||
example: 03e5572a-f733-49af-bc14-8a18bd53ee39 | ||
|
||
StatusResponse: | ||
type: object | ||
required: | ||
- status | ||
properties: | ||
status: | ||
description: >- | ||
The status of this automated testing interface. | ||
- `Starting`: the interface is starting and the automated test driver should wait before sending requests. | ||
- `Ready`: the interface is ready to receive test requests. | ||
type: string | ||
enum: [Starting, Ready] | ||
example: Ready | ||
api_name: | ||
description: |- | ||
Indication of the API implemented at this URL. Must be "Position Report Plan Interface". | ||
type: string | ||
example: Position Report Plan Interface | ||
api_version: | ||
description: |- | ||
Indication of the API version implemented at this URL. Must be "v0.4.4" when implementing this version of the API. | ||
type: string | ||
example: v0.4.4 | ||
|
||
PositionReportPlan: | ||
description: >- | ||
Position report plan . | ||
required: | ||
- id | ||
- pos_url | ||
- positions | ||
type: object | ||
properties: | ||
latitude: | ||
type: number | ||
format: float | ||
maximum: 90 | ||
exclusiveMaximum: false | ||
minimum: -90 | ||
exclusiveMinimum: false | ||
longitude: | ||
type: number | ||
format: float | ||
minimum: -180 | ||
exclusiveMaximum: false | ||
maximum: 180 | ||
exclusiveMinimum: false | ||
altitude: | ||
type: number | ||
format: float | ||
minimum: -8000 | ||
exclusiveMinimum: false | ||
maximum: 100000 | ||
exclusiveMaximum: false | ||
offset_ms: | ||
description: >- | ||
Time offset, in milliseconds, from the previous position | ||
type: integer | ||
speed: | ||
type: number | ||
format: float | ||
track: | ||
type: number | ||
format: float | ||
|
||
PostPositionReportPlanRequest: | ||
description: >- | ||
Post the flight position plan to the client | ||
type: object | ||
required: | ||
- id | ||
- pos_url | ||
- positions | ||
properties: | ||
id: | ||
description: participant id of the USS to which the position reports are to be sent. | ||
type: string | ||
pos_url: | ||
description: >- | ||
The url to which position reports have to be sent | ||
type: string | ||
positions: | ||
type: array | ||
items: | ||
$ref: '#/components/schemas/PositionReportPlan' | ||
|
||
paths: | ||
/status: | ||
get: | ||
security: | ||
- Authority: | ||
- interuss.flight_planning.direct_automated_test | ||
responses: | ||
'200': | ||
content: | ||
application/json: | ||
schema: | ||
$ref: '#/components/schemas/StatusResponse' | ||
description: >- | ||
This automated testing interface is available and its status was retrieved successfully. | ||
'401': | ||
description: Bearer access token was not provided in Authorization header, token could not be decoded, or token was invalid. | ||
'403': | ||
description: The access token was decoded successfully but did not include a scope appropriate to this endpoint. | ||
'404': | ||
description: This automated testing interface is not available. | ||
summary: Status of this automated testing interface | ||
description: Get the status of this automated testing interface. | ||
operationId: GetStatus | ||
|
||
/position_report_plan/{flight_plan_id}: | ||
parameters: | ||
- name: flight_plan_id | ||
in: path | ||
required: true | ||
description: A UUID-formatted string identifying the user's flight plan intent. | ||
schema: | ||
$ref: '#/components/schemas/FlightPlanID' | ||
post: | ||
security: | ||
- Authority: | ||
- interuss.flight_data.position_report_plan | ||
requestBody: | ||
content: | ||
application/json: | ||
schema: | ||
$ref: '#/components/schemas/PostPositionReportPlanRequest' | ||
required: true | ||
responses: | ||
'200': | ||
description: Requested data was processed successfully | ||
'401': | ||
description: Bearer access token was not provided in Authorization header, token could not be decoded, or token was invalid. | ||
'403': | ||
description: The access token was decoded successfully but did not include a scope appropriate to this endpoint. | ||
'409': | ||
description: The request contains a duplicate request_id and the response for that request is not available, or another conflict condition occurred. | ||
|
||
summary: Post flight's position reports plan | ||
operationId: PostPositionReportPlan | ||
description: >- | ||
This endpoint simulates position reports being submitted for flight. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
version: '3.8' | ||
|
||
services: | ||
|
||
position_reporter: | ||
container_name: position_reporter_trial1 | ||
hostname: scdsc.position_reporter_trial1 | ||
image: interuss/monitoring | ||
command: position_reporter/start.sh | ||
environment: | ||
- POS_REP_AUTH_SPEC=DummyOAuth(http://oauth.authority.localutm:8085/token,uss1) | ||
- MOCK_USS_BASE_URL=http://scdsc.position_reporter_trial1 | ||
# TODO: remove interaction_logging once dedicated mock_uss is involved in tests | ||
- POS_REP_PORT=80 | ||
- MOCK_USS_PROXY_VALUES=x_for=1,x_proto=1,x_host=1,x_prefix=1,x_port=1 | ||
expose: | ||
- 80 | ||
ports: | ||
- 8074:80 | ||
user: "${UID_GID}" | ||
restart: always | ||
networks: | ||
- interop_ecosystem_network | ||
extra_hosts: | ||
- host.docker.internal:host-gateway | ||
|
||
|
||
networks: | ||
interop_ecosystem_network: | ||
external: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import os | ||
|
||
from gunicorn.arbiter import Arbiter | ||
from gunicorn.http import Request | ||
from gunicorn.http.wsgi import Response | ||
from gunicorn.workers.base import Worker | ||
import logging | ||
|
||
from monitoring.position_reporter import webapp | ||
|
||
|
||
def on_starting(server: Arbiter): | ||
"""gunicorn server hook called just before master process is initialized.""" | ||
logging.debug("on_starting") | ||
webapp.setup() | ||
|
||
|
||
def when_ready(server: Arbiter): | ||
"""gunicorn server hook called just after the server is started.""" | ||
logging.debug("when_ready") | ||
webapp.start_periodic_tasks_daemon() | ||
|
||
|
||
def _skip_logging(req: Request) -> bool: | ||
# Status endpoint is polled constantly for liveness; to avoid filling logs, we don't log it | ||
if req.path == "/status" and req.method == "GET": | ||
return True | ||
return False | ||
|
||
|
||
def pre_request(worker: Worker, req: Request): | ||
"""gunicorn server hook called just before a worker processes the request.""" | ||
if not _skip_logging(req): | ||
logging.debug( | ||
"gunicorn pre_request from worker {} (OS PID {}): {} {}".format( | ||
worker.pid, | ||
os.getpid(), | ||
req.method, | ||
req.path, | ||
) | ||
) | ||
|
||
|
||
def post_request(worker: Worker, req: Request, environ: dict, resp: Response): | ||
"""gunicorn server hook called after a worker processes the request.""" | ||
if not _skip_logging(req): | ||
logging.debug( | ||
"gunicorn post_request from worker {} (OS PID {}): {} {} -> {}".format( | ||
worker.pid, | ||
os.getpid(), | ||
req.method, | ||
req.path, | ||
resp.status_code, | ||
) | ||
) | ||
|
||
|
||
def worker_abort(worker: Worker): | ||
"""gunicorn server hook called when a worker received the SIGABRT signal.""" | ||
logging.debug( | ||
"gunicorn worker_abort from worker {} (OS PID {})".format(worker.pid, os.getpid()) | ||
) | ||
|
||
|
||
def on_exit(server: Arbiter): | ||
"""gunicorn server hook called just before exiting Gunicorn.""" | ||
logging.debug( | ||
f"on_exit from process {os.getpid()} with arbiter process {server.pid}" | ||
) | ||
webapp.shutdown(None, None) |
Empty file.
Oops, something went wrong.