Skip to content

Commit

Permalink
Position Reporter - initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
punamverma committed Dec 19, 2024
1 parent 7b9820b commit ce82dbc
Show file tree
Hide file tree
Showing 13 changed files with 637 additions and 0 deletions.
3 changes: 3 additions & 0 deletions monitoring/position_reporter/README.md
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.
82 changes: 82 additions & 0 deletions monitoring/position_reporter/__init__.py
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
195 changes: 195 additions & 0 deletions monitoring/position_reporter/api/pos_report_plan.yaml
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.
30 changes: 30 additions & 0 deletions monitoring/position_reporter/docker-compose.yaml
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
70 changes: 70 additions & 0 deletions monitoring/position_reporter/gunicorn.conf.py
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.
Loading

0 comments on commit ce82dbc

Please sign in to comment.