def test_big_operation_search(scd_api, scd_session): with open('./scd/resources/op_big_operation.json', 'r') as f: req = json.load(f) dt = datetime.datetime.utcnow() - scd.start_of([req['area_of_interest']]) req['area_of_interest'] = scd.offset_time([req['area_of_interest']], dt)[0] resp = scd_session.post('/operational_intent_references/query', json=req) assert resp.status_code == 400, resp.content
def test_big_operation_search(scd_session): """ This test reproduces a case where a search resulted in 503 because the underlying gRPC backend had crashed. """ with open('./scd/resources/op_big_operation.json', 'r') as f: req = json.load(f) dt = datetime.datetime.utcnow() - scd.start_of([req['area_of_interest']]) req['area_of_interest'] = scd.offset_time([req['area_of_interest']], dt)[0] resp = scd_session.post('/operation_references/query', json=req) assert resp.status_code == 400, resp.content
def test_missing_conflicted_operation(ids, scd_api, scd_session): # Emplace the initial version of Operation 1 with open('./scd/resources/op_missing_initial.yaml', 'r') as f: req = yaml.full_load(f) dt = datetime.datetime.utcnow() - scd.start_of(req['extents']) req['extents'] = scd.offset_time(req['extents'], dt) resp = scd_session.put('/operational_intent_references/{}'.format( ids(OP_TYPE)), json=req) assert resp.status_code == 200, resp.content ovn1a = resp.json()['operational_intent_reference']['ovn'] sub_id = resp.json()['operational_intent_reference']['subscription_id'] # Emplace the pre-existing Operation that conflicted in the original observation with open('./scd/resources/op_missing_preexisting_unknown.yaml', 'r') as f: req = yaml.full_load(f) req['extents'] = scd.offset_time(req['extents'], dt) req['key'] = [ovn1a] resp = scd_session.put('/operational_intent_references/{}'.format( ids(OP_TYPE2)), json=req) assert resp.status_code == 200, resp.content # Attempt to update Operation 1 without OVN for the pre-existing Operation with open('./scd/resources/op_missing_update.json', 'r') as f: req = json.load(f) req['extents'] = scd.offset_time(req['extents'], dt) req['key'] = [ovn1a] req['subscription_id'] = sub_id resp = scd_session.put('/operational_intent_references/{}/{}'.format( ids(OP_TYPE), ovn1a), json=req) assert resp.status_code == 409, resp.content # checking entity conflicts conflicts = [] data = resp.json() assert 'missing_operational_intents' in data assert ids(OP_TYPE2) in [ intent['id'] for intent in data['missing_operational_intents'] ], resp.content # Perform an area-based query on the area occupied by Operation 1 with open('./scd/resources/op_missing_query.json', 'r') as f: req = json.load(f) req['area_of_interest'] = scd.offset_time([req['area_of_interest']], dt)[0] resp = scd_session.post('/operational_intent_references/query', json=req) assert resp.status_code == 200, resp.content ops = [op['id'] for op in resp.json()['operational_intent_references']] assert ids(OP_TYPE) in ops, resp.content # ids(OP_ID2) not expected here because its ceiling is <575m whereas query floor is # >591m. assert ids(OP_TYPE2) not in ops, resp.content
def test_missing_conflicted_operation(scd_session): """ This test reproduces a case where a conflicting Operation did not appear in a follow-up area-based query for Operations. """ # Emplace the initial version of Operation 1 with open('./scd/resources/op_missing_initial.yaml', 'r') as f: req = yaml.full_load(f) dt = datetime.datetime.utcnow() - scd.start_of(req['extents']) req['extents'] = scd.offset_time(req['extents'], dt) resp = scd_session.put('/operation_references/{}'.format(OP_ID), json=req) assert resp.status_code == 200, resp.content ovn1a = resp.json()['operation_reference']['ovn'] sub_id = resp.json()['operation_reference']['subscription_id'] # Emplace the pre-existing Operation that conflicted in the original observation with open('./scd/resources/op_missing_preexisting_unknown.yaml', 'r') as f: req = yaml.full_load(f) req['extents'] = scd.offset_time(req['extents'], dt) req['key'] = [ovn1a] resp = scd_session.put('/operation_references/{}'.format(OP_ID2), json=req) assert resp.status_code == 200, resp.content # Attempt to update Operation 1 without OVN for the pre-existing Operation with open('./scd/resources/op_missing_update.json', 'r') as f: req = json.load(f) req['extents'] = scd.offset_time(req['extents'], dt) req['key'] = [ovn1a] req['subscription_id'] = sub_id resp = scd_session.put('/operation_references/{}'.format(OP_ID), json=req) assert resp.status_code == 409, resp.content conflicts = [] for conflict in resp.json()['entity_conflicts']: if conflict.get('operation_reference', None): conflicts.append(conflict['operation_reference']['id']) assert OP_ID2 in conflicts, resp.content # Perform an area-based query on the area occupied by Operation 1 with open('./scd/resources/op_missing_query.json', 'r') as f: req = json.load(f) req['area_of_interest'] = scd.offset_time([req['area_of_interest']], dt)[0] resp = scd_session.post('/operation_references/query', json=req) assert resp.status_code == 200, resp.content ops = [op['id'] for op in resp.json()['operation_references']] assert OP_ID in ops, resp.content # OP_ID2 not expected here because its ceiling is <575m whereas query floor is # >591m. assert OP_ID2 not in ops, resp.content
def _verify_operational_intent(runner, step: TestStep, step_ref: TestStepReference, target: TestTarget, resp: InjectFlightResponse, flight_id: str) -> None: # Verify that the operation actually does exist in the system # Check the DSS for a reference op_intent = step.inject_flight.test_injection.operational_intent vol4s = op_intent.volumes + op_intent.off_nominal_volumes rect = scd.rect_bounds_of(vol4s) t0 = scd.start_of(vol4s) t1 = scd.end_of(vol4s) alts = scd.meter_altitude_bounds_of(vol4s) op_refs = fetch_scd.operational_intent_references(runner.dss_target.client, rect, t0, t1, alts[0], alts[1]) dss_interaction_id = runner.report_recorder.capture_interaction(step_ref, op_refs, 'Check if injected operational intent exists in DSS') if not op_refs.success: # The DSS call didn't even succeed issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=[], severity=Severity.Critical, subject=None, summary='Error querying operational intent references from DSS', details=op_refs.error, interactions=[dss_interaction_id]) raise TestRunnerError(issue.summary, issue) try: ImplicitDict.parse(op_refs.json_result, scd.QueryOperationalIntentReferenceResponse) except ValueError as e: # The DSS returned an invalid result issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=[], severity=Severity.Critical, subject=None, summary='Error in operational intent reference data format from DSS', details=str(e), interactions=[dss_interaction_id]) raise TestRunnerError(issue.summary, issue) if resp.operational_intent_id not in op_refs.references_by_id: # The expected operational intent reference wasn't present issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=['F3548-21 USS0005'], severity=Severity.High, subject=IssueSubject(subject_type=SubjectType.OperationalIntent, subject=resp.operational_intent_id), summary='Operational intent not present in DSS', details='When queried for the operational intent {} for flight {} in the DSS, it was not found in the Volume4D of interest'.format(resp.operational_intent_id, flight_id), interactions=[dss_interaction_id]) raise TestRunnerError(issue.summary, issue) op_ref = op_refs.references_by_id[resp.operational_intent_id] # Check the USS for the operational intent details op = fetch_scd.operational_intent(op_ref['uss_base_url'], resp.operational_intent_id, runner.dss_target.client) uss_interaction_id = runner.report_recorder.capture_interaction(step_ref, op, 'Inspect operational intent details') if not op.success: # The USS call didn't succeed issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=['F3548-21 USS0105'], severity=Severity.High, subject=IssueSubject(subject_type=SubjectType.OperationalIntent, subject=resp.operational_intent_id), summary='Error querying operational intent details from USS', details=op.error, interactions=[uss_interaction_id]) raise TestRunnerError(issue.summary, issue) try: op_resp: scd.GetOperationalIntentDetailsResponse = ImplicitDict.parse(op.json_result, scd.GetOperationalIntentDetailsResponse) except ValueError as e: # The USS returned an invalid result issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=['F3548-21 USS0105'], severity=Severity.High, subject=IssueSubject(subject_type=SubjectType.OperationalIntent, subject=resp.operational_intent_id), summary='Error in operational intent data format from USS', details=str(e), interactions=[uss_interaction_id]) raise TestRunnerError(issue.summary, issue) # Check that the USS is providing reasonable details resp_vol4s = op_resp.operational_intent.details.volumes + op_resp.operational_intent.details.off_nominal_volumes resp_alts = scd.meter_altitude_bounds_of(resp_vol4s) resp_start = scd.start_of(resp_vol4s) resp_end = scd.end_of(resp_vol4s) error_text = None if resp_alts[0] > alts[0] + NUMERIC_PRECISION: error_text = 'Lower altitude specified by USS in operational intent details ({} m WGS84) is above the lower altitude in the injected flight ({} m WGS84)'.format(resp_alts[0], alts[0]) elif resp_alts[1] < alts[1] - NUMERIC_PRECISION: error_text = 'Upper altitude specified by USS in operational intent details ({} m WGS84) is below the upper altitude in the injected flight ({} m WGS84)'.format(resp_alts[1], alts[1]) elif resp_start > t0 + datetime.timedelta(seconds=NUMERIC_PRECISION): error_text = 'Start time specified by USS in operational intent details ({}) is past the start time of the injected flight ({})'.format(resp_start.isoformat(), t0.isoformat()) elif resp_end < t1 - datetime.timedelta(seconds=NUMERIC_PRECISION): error_text = 'End time specified by USS in operational intent details ({}) is prior to the end time of the injected flight ({})'.format(resp_end.isoformat(), t1.isoformat()) if error_text: # The USS's flight details are incorrect issue = Issue( context=runner.context, check_code='CREATED_FLIGHT_EXISTS_IN_SYSTEM', uss_role=step.inject_flight.injection_target.uss_role, target=target.name, relevant_requirements=[], severity=Severity.High, subject=IssueSubject(subject_type=SubjectType.OperationalIntent, subject=resp.operational_intent_id), summary='Operational intent details does not match injected flight', details=error_text, interactions=[uss_interaction_id]) raise TestRunnerError(issue.summary, issue)
def inject_flight(flight_id: str) -> Tuple[str, int]: """Implements flight injection in SCD automated testing injection API.""" try: json = flask.request.json if json is None: raise ValueError('Request did not contain a JSON payload') req_body: InjectFlightRequest = ImplicitDict.parse( json, InjectFlightRequest) except ValueError as e: msg = 'Create flight {} unable to parse JSON: {}'.format(flight_id, e) return msg, 400 if webapp.config[config.KEY_BEHAVIOR_LOCALITY].is_uspace_applicable: # Validate flight authorisation flight_auth = req_body.flight_authorisation if not flight_auth.uas_serial_number.valid: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid serial number')) if not flight_auth.operator_id.valid: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid operator ID')) if flight_auth.uas_class == scd_injection_api.UASClass.Other: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid UAS class')) if flight_auth.operation_category == scd_injection_api.OperationCategory.Unknown: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid operation category')) if flight_auth.endurance_minutes < 1 or flight_auth.endurance_minutes > 10 * 24 * 60: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid endurance')) if sum(1 if len(m) > 0 else 0 for m in flight_auth.connectivity_methods) == 0: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid connectivity methods')) if sum(1 if len(m) > 0 else 0 for m in flight_auth.identification_technologies) == 0: return flask.jsonify( InjectFlightResponse( result=InjectFlightResult.Rejected, notes='Invalid identification technologies')) try: urlparse(flight_auth.emergency_procedure_url) except ValueError: return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Rejected, notes='Invalid emergency procedure URL')) # Check for operational intents in the DSS start_time = scd.start_of(req_body.operational_intent.volumes) end_time = scd.end_of(req_body.operational_intent.volumes) area = scd.rect_bounds_of(req_body.operational_intent.volumes) alt_lo, alt_hi = scd.meter_altitude_bounds_of( req_body.operational_intent.volumes) vol4 = scd.make_vol4(start_time, end_time, alt_lo, alt_hi, polygon=scd.make_polygon(latlngrect=area)) try: op_intents = query_operational_intents(vol4) except (ValueError, scd_client.OperationError, requests.exceptions.ConnectionError, ConnectionError) as e: notes = 'Error querying operational intents: {}'.format(e) return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Failed, notes=notes)), 200 # Check for intersections v1 = req_body.operational_intent.volumes for op_intent in op_intents: if req_body.operational_intent.priority > op_intent.details.priority: continue if webapp.config[ config. KEY_BEHAVIOR_LOCALITY].allow_same_priority_intersections: continue v2a = op_intent.details.volumes v2b = op_intent.details.off_nominal_volumes if scd.vol4s_intersect(v1, v2a) or scd.vol4s_intersect(v1, v2b): notes = 'Requested flight intersected {}\'s operational intent {}'.format( op_intent.reference.manager, op_intent.reference.id) return flask.jsonify( InjectFlightResponse( result=InjectFlightResult.ConflictWithFlight, notes=notes)), 200 # Create operational intent in DSS base_url = '{}/mock/scd'.format(webapp.config[config.KEY_BASE_URL]) req = scd.PutOperationalIntentReferenceParameters( extents=req_body.operational_intent.volumes, key=[op.reference.ovn for op in op_intents], state=req_body.operational_intent.state, uss_base_url=base_url, new_subscription=scd.ImplicitSubscriptionParameters( uss_base_url=base_url)) id = str(uuid.uuid4()) try: result = scd_client.create_operational_intent_reference( resources.utm_client, id, req) except (ValueError, scd_client.OperationError, requests.exceptions.ConnectionError, ConnectionError) as e: notes = 'Error creating operational intent: {}'.format(e) return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Failed, notes=notes)), 200 scd_client.notify_subscribers( resources.utm_client, result.operational_intent_reference.id, scd.OperationalIntent(reference=result.operational_intent_reference, details=req_body.operational_intent), result.subscribers) # Store flight in database record = database.FlightRecord( op_intent_reference=result.operational_intent_reference, op_intent_injection=req_body.operational_intent, flight_authorisation=req_body.flight_authorisation) with db as tx: tx.flights[flight_id] = record return flask.jsonify( InjectFlightResponse(result=InjectFlightResult.Planned, operational_intent_id=id))
def clear_area() -> Tuple[str, int]: try: json = flask.request.json if json is None: raise ValueError('Request did not contain a JSON payload') req = ImplicitDict.parse(json, ClearAreaRequest) except ValueError as e: msg = 'Unable to parse ClearAreaRequest JSON request: {}'.format(e) return msg, 400 # Find operational intents in the DSS start_time = scd.start_of([req.extent]) end_time = scd.end_of([req.extent]) area = scd.rect_bounds_of([req.extent]) alt_lo, alt_hi = scd.meter_altitude_bounds_of([req.extent]) vol4 = scd.make_vol4(start_time, end_time, alt_lo, alt_hi, polygon=scd.make_polygon(latlngrect=area)) try: op_intent_refs = scd_client.query_operational_intent_references( resources.utm_client, vol4) except (ValueError, scd_client.OperationError, requests.exceptions.ConnectionError, ConnectionError) as e: msg = 'Error querying operational intents: {}'.format(e) return flask.jsonify( ClearAreaResponse(outcome=ClearAreaOutcome( success=False, message=msg, timestamp=StringBasedDateTime(datetime.utcnow())), request=req)), 200 # Try to delete every operational intent found dss_deletion_results = {} deleted = set() for op_intent_ref in op_intent_refs: try: scd_client.delete_operational_intent_reference( resources.utm_client, op_intent_ref.id, op_intent_ref.ovn) dss_deletion_results[op_intent_ref.id] = 'Deleted from DSS' deleted.add(op_intent_ref.id) except scd_client.OperationError as e: dss_deletion_results[op_intent_ref.id] = str(e) # Delete corresponding flight injections and cached operational intents with db as tx: flights_to_delete = [] for flight_id, record in tx.flights.items(): if record.op_intent_reference.id in deleted: flights_to_delete.append(flight_id) for flight_id in flights_to_delete: del tx.flights[flight_id] cache_deletions = [] for op_intent_id in deleted: if op_intent_id in tx.cached_operations: del tx.cached_operations[op_intent_id] cache_deletions.append(op_intent_id) msg = yaml.dump({ 'dss_deletions': dss_deletion_results, 'flight_deletions': flights_to_delete, 'cache_deletions': cache_deletions, }) return flask.jsonify( ClearAreaResponse(outcome=ClearAreaOutcome( success=True, message=msg, timestamp=StringBasedDateTime(datetime.utcnow())), request=req)), 200