isar 1.15.0__py3-none-any.whl → 1.34.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isar/__init__.py +2 -5
- isar/apis/api.py +159 -66
- isar/apis/models/__init__.py +0 -1
- isar/apis/models/models.py +22 -12
- isar/apis/models/start_mission_definition.py +128 -123
- isar/apis/robot_control/robot_controller.py +41 -0
- isar/apis/schedule/scheduling_controller.py +135 -121
- isar/apis/security/authentication.py +5 -5
- isar/config/certs/ca-cert.pem +32 -32
- isar/config/keyvault/keyvault_service.py +1 -2
- isar/config/log.py +47 -39
- isar/config/logging.conf +16 -31
- isar/config/open_telemetry.py +102 -0
- isar/config/predefined_mission_definition/default_exr.json +49 -0
- isar/config/predefined_mission_definition/default_mission.json +1 -5
- isar/config/predefined_mission_definition/default_turtlebot.json +4 -11
- isar/config/predefined_missions/default.json +67 -87
- isar/config/predefined_missions/default_extra_capabilities.json +107 -0
- isar/config/settings.py +119 -142
- isar/eventhandlers/eventhandler.py +123 -0
- isar/mission_planner/local_planner.py +6 -20
- isar/mission_planner/mission_planner_interface.py +1 -1
- isar/models/events.py +184 -0
- isar/models/status.py +18 -0
- isar/modules.py +118 -205
- isar/robot/robot.py +377 -0
- isar/robot/robot_battery.py +60 -0
- isar/robot/robot_monitor_mission.py +357 -0
- isar/robot/robot_pause_mission.py +74 -0
- isar/robot/robot_resume_mission.py +67 -0
- isar/robot/robot_start_mission.py +66 -0
- isar/robot/robot_status.py +61 -0
- isar/robot/robot_stop_mission.py +68 -0
- isar/robot/robot_upload_inspection.py +75 -0
- isar/script.py +171 -0
- isar/services/service_connections/mqtt/mqtt_client.py +47 -11
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +32 -0
- isar/services/service_connections/mqtt/robot_info_publisher.py +4 -3
- isar/services/service_connections/persistent_memory.py +69 -0
- isar/services/utilities/mqtt_utilities.py +93 -0
- isar/services/utilities/robot_utilities.py +20 -0
- isar/services/utilities/scheduling_utilities.py +393 -65
- isar/state_machine/state_machine.py +227 -486
- isar/state_machine/states/__init__.py +0 -7
- isar/state_machine/states/await_next_mission.py +114 -0
- isar/state_machine/states/blocked_protective_stop.py +60 -0
- isar/state_machine/states/going_to_lockdown.py +95 -0
- isar/state_machine/states/going_to_recharging.py +92 -0
- isar/state_machine/states/home.py +115 -0
- isar/state_machine/states/intervention_needed.py +77 -0
- isar/state_machine/states/lockdown.py +38 -0
- isar/state_machine/states/maintenance.py +36 -0
- isar/state_machine/states/monitor.py +137 -166
- isar/state_machine/states/offline.py +60 -0
- isar/state_machine/states/paused.py +92 -23
- isar/state_machine/states/pausing.py +48 -0
- isar/state_machine/states/pausing_return_home.py +48 -0
- isar/state_machine/states/recharging.py +80 -0
- isar/state_machine/states/resuming.py +57 -0
- isar/state_machine/states/resuming_return_home.py +64 -0
- isar/state_machine/states/return_home_paused.py +109 -0
- isar/state_machine/states/returning_home.py +217 -0
- isar/state_machine/states/stopping.py +61 -0
- isar/state_machine/states/stopping_due_to_maintenance.py +61 -0
- isar/state_machine/states/stopping_go_to_lockdown.py +60 -0
- isar/state_machine/states/stopping_go_to_recharge.py +51 -0
- isar/state_machine/states/stopping_return_home.py +77 -0
- isar/state_machine/states/unknown_status.py +72 -0
- isar/state_machine/states_enum.py +22 -5
- isar/state_machine/transitions/mission.py +192 -0
- isar/state_machine/transitions/return_home.py +106 -0
- isar/state_machine/transitions/robot_status.py +80 -0
- isar/state_machine/utils/common_event_handlers.py +73 -0
- isar/storage/blob_storage.py +71 -45
- isar/storage/local_storage.py +28 -14
- isar/storage/storage_interface.py +28 -6
- isar/storage/uploader.py +184 -55
- isar/storage/utilities.py +35 -27
- isar-1.34.9.dist-info/METADATA +496 -0
- isar-1.34.9.dist-info/RECORD +135 -0
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
- isar-1.34.9.dist-info/entry_points.txt +3 -0
- robot_interface/models/exceptions/__init__.py +0 -7
- robot_interface/models/exceptions/robot_exceptions.py +274 -4
- robot_interface/models/initialize/__init__.py +0 -1
- robot_interface/models/inspection/__init__.py +0 -13
- robot_interface/models/inspection/inspection.py +43 -34
- robot_interface/models/mission/mission.py +18 -14
- robot_interface/models/mission/status.py +20 -25
- robot_interface/models/mission/task.py +156 -92
- robot_interface/models/robots/battery_state.py +6 -0
- robot_interface/models/robots/media.py +13 -0
- robot_interface/models/robots/robot_model.py +7 -7
- robot_interface/robot_interface.py +135 -66
- robot_interface/telemetry/mqtt_client.py +84 -12
- robot_interface/telemetry/payloads.py +111 -12
- robot_interface/utilities/json_service.py +7 -1
- isar/config/predefined_missions/default_turtlebot.json +0 -110
- isar/config/predefined_poses/__init__.py +0 -0
- isar/config/predefined_poses/predefined_poses.py +0 -616
- isar/config/settings.env +0 -26
- isar/mission_planner/sequential_task_selector.py +0 -23
- isar/mission_planner/task_selector_interface.py +0 -31
- isar/models/communication/__init__.py +0 -0
- isar/models/communication/message.py +0 -12
- isar/models/communication/queues/__init__.py +0 -4
- isar/models/communication/queues/queue_io.py +0 -12
- isar/models/communication/queues/queue_timeout_error.py +0 -2
- isar/models/communication/queues/queues.py +0 -19
- isar/models/communication/queues/status_queue.py +0 -20
- isar/models/mission_metadata/__init__.py +0 -0
- isar/services/readers/__init__.py +0 -0
- isar/services/readers/base_reader.py +0 -37
- isar/services/service_connections/mqtt/robot_status_publisher.py +0 -93
- isar/services/service_connections/stid/__init__.py +0 -0
- isar/services/service_connections/stid/stid_service.py +0 -45
- isar/services/utilities/queue_utilities.py +0 -39
- isar/state_machine/states/idle.py +0 -40
- isar/state_machine/states/initialize.py +0 -60
- isar/state_machine/states/initiate.py +0 -129
- isar/state_machine/states/off.py +0 -18
- isar/state_machine/states/stop.py +0 -78
- isar/storage/slimm_storage.py +0 -181
- isar-1.15.0.dist-info/METADATA +0 -417
- isar-1.15.0.dist-info/RECORD +0 -113
- robot_interface/models/initialize/initialize_params.py +0 -9
- robot_interface/models/mission/step.py +0 -211
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
- {isar-1.15.0.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from http import HTTPStatus
|
|
3
|
-
from typing import List, Optional
|
|
4
3
|
|
|
5
|
-
from alitra import Pose
|
|
6
4
|
from fastapi import Body, HTTPException, Path
|
|
7
|
-
from
|
|
5
|
+
from opentelemetry import trace
|
|
8
6
|
|
|
9
|
-
from isar.apis.models import InputPose, StartMissionResponse
|
|
10
7
|
from isar.apis.models.models import (
|
|
11
8
|
ControlMissionResponse,
|
|
12
|
-
|
|
9
|
+
StartMissionResponse,
|
|
13
10
|
TaskResponse,
|
|
14
11
|
)
|
|
15
12
|
from isar.apis.models.start_mission_definition import (
|
|
16
13
|
StartMissionDefinition,
|
|
14
|
+
StopMissionDefinition,
|
|
17
15
|
to_isar_mission,
|
|
18
16
|
)
|
|
19
|
-
from isar.config.settings import robot_settings
|
|
17
|
+
from isar.config.settings import robot_settings
|
|
20
18
|
from isar.mission_planner.mission_planner_interface import MissionPlannerError
|
|
21
19
|
from isar.services.utilities.scheduling_utilities import SchedulingUtilities
|
|
22
20
|
from isar.state_machine.states_enum import States
|
|
23
21
|
from robot_interface.models.mission.mission import Mission
|
|
24
|
-
from robot_interface.models.mission.
|
|
25
|
-
|
|
22
|
+
from robot_interface.models.mission.task import TASKS, InspectionTask
|
|
23
|
+
|
|
24
|
+
tracer = trace.get_tracer(__name__)
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
class SchedulingController:
|
|
29
|
-
@inject
|
|
30
28
|
def __init__(
|
|
31
29
|
self,
|
|
32
30
|
scheduling_utilities: SchedulingUtilities,
|
|
@@ -34,54 +32,33 @@ class SchedulingController:
|
|
|
34
32
|
self.scheduling_utilities: SchedulingUtilities = scheduling_utilities
|
|
35
33
|
self.logger = logging.getLogger("api")
|
|
36
34
|
|
|
35
|
+
@tracer.start_as_current_span("start_mission_by_id")
|
|
37
36
|
def start_mission_by_id(
|
|
38
37
|
self,
|
|
39
38
|
mission_id: str = Path(
|
|
40
|
-
...,
|
|
41
39
|
alias="id",
|
|
42
40
|
title="Mission ID",
|
|
43
41
|
description="ID-number for predefined mission",
|
|
44
42
|
),
|
|
45
|
-
initial_pose: Optional[InputPose] = Body(
|
|
46
|
-
default=None,
|
|
47
|
-
description="The starting point of the mission. Used for initial "
|
|
48
|
-
"localization of robot",
|
|
49
|
-
embed=True,
|
|
50
|
-
),
|
|
51
|
-
return_pose: Optional[InputPose] = Body(
|
|
52
|
-
default=None,
|
|
53
|
-
description="End pose of the mission. The robot return to the specified "
|
|
54
|
-
"pose after finishing all inspections",
|
|
55
|
-
embed=True,
|
|
56
|
-
),
|
|
57
43
|
) -> StartMissionResponse:
|
|
58
|
-
self.logger.info(
|
|
44
|
+
self.logger.info("Received request to start mission with id %s", mission_id)
|
|
59
45
|
|
|
60
46
|
state: States = self.scheduling_utilities.get_state()
|
|
61
47
|
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
|
|
62
48
|
|
|
63
49
|
mission: Mission = self.scheduling_utilities.get_mission(mission_id)
|
|
64
|
-
if return_pose:
|
|
65
|
-
pose: Pose = return_pose.to_alitra_pose()
|
|
66
|
-
step: DriveToPose = DriveToPose(pose=pose)
|
|
67
|
-
mission.tasks.append(Task(steps=[step]))
|
|
68
50
|
|
|
69
51
|
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
70
52
|
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
71
53
|
)
|
|
72
54
|
|
|
73
|
-
|
|
74
|
-
initial_pose.to_alitra_pose() if initial_pose else None
|
|
75
|
-
)
|
|
55
|
+
self.logger.info("Starting mission with ISAR Mission ID: '%s'", mission.id)
|
|
76
56
|
|
|
77
|
-
self.
|
|
78
|
-
|
|
79
|
-
self.scheduling_utilities.start_mission(
|
|
80
|
-
mission=mission, initial_pose=initial_pose_alitra
|
|
81
|
-
)
|
|
57
|
+
self.scheduling_utilities.start_mission(mission=mission)
|
|
82
58
|
|
|
83
59
|
return self._api_response(mission)
|
|
84
60
|
|
|
61
|
+
@tracer.start_as_current_span("start_mission")
|
|
85
62
|
def start_mission(
|
|
86
63
|
self,
|
|
87
64
|
mission_definition: StartMissionDefinition = Body(
|
|
@@ -90,35 +67,26 @@ class SchedulingController:
|
|
|
90
67
|
title="Mission Definition",
|
|
91
68
|
description="Description of the mission in json format",
|
|
92
69
|
),
|
|
93
|
-
initial_pose: Optional[InputPose] = Body(
|
|
94
|
-
default=None,
|
|
95
|
-
description="The starting point of the mission. Used for initial "
|
|
96
|
-
"localization of robot",
|
|
97
|
-
embed=True,
|
|
98
|
-
),
|
|
99
|
-
return_pose: Optional[InputPose] = Body(
|
|
100
|
-
default=None,
|
|
101
|
-
description="End pose of the mission. The robot return to the specified "
|
|
102
|
-
"pose after finishing all inspections",
|
|
103
|
-
embed=True,
|
|
104
|
-
),
|
|
105
70
|
) -> StartMissionResponse:
|
|
106
71
|
self.logger.info("Received request to start new mission")
|
|
107
72
|
|
|
108
73
|
if not mission_definition:
|
|
109
|
-
|
|
74
|
+
error_message_no_mission_definition: str = (
|
|
110
75
|
"Unprocessable entity - 'mission_definition' empty or invalid"
|
|
111
76
|
)
|
|
112
|
-
self.logger.error(
|
|
77
|
+
self.logger.error(error_message_no_mission_definition)
|
|
113
78
|
raise HTTPException(
|
|
114
|
-
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
79
|
+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
80
|
+
detail=error_message_no_mission_definition,
|
|
115
81
|
)
|
|
116
82
|
|
|
117
83
|
state: States = self.scheduling_utilities.get_state()
|
|
118
84
|
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
|
|
119
85
|
|
|
120
86
|
try:
|
|
121
|
-
mission: Mission = to_isar_mission(
|
|
87
|
+
mission: Mission = to_isar_mission(
|
|
88
|
+
start_mission_definition=mission_definition
|
|
89
|
+
)
|
|
122
90
|
except MissionPlannerError as e:
|
|
123
91
|
error_message = f"Bad Request - Cannot create ISAR mission: {e}"
|
|
124
92
|
self.logger.warning(error_message)
|
|
@@ -130,21 +98,23 @@ class SchedulingController:
|
|
|
130
98
|
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
131
99
|
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
132
100
|
)
|
|
133
|
-
if return_pose:
|
|
134
|
-
pose: Pose = return_pose.to_alitra_pose()
|
|
135
|
-
step: DriveToPose = DriveToPose(pose=pose)
|
|
136
|
-
mission.tasks.append(Task(steps=[step]))
|
|
137
101
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)
|
|
102
|
+
self.logger.info("Starting mission: %s", mission.id)
|
|
103
|
+
self.scheduling_utilities.start_mission(mission=mission)
|
|
104
|
+
return self._api_response(mission)
|
|
105
|
+
|
|
106
|
+
@tracer.start_as_current_span("return_home")
|
|
107
|
+
def return_home(self) -> None:
|
|
108
|
+
self.logger.info("Received request to return home")
|
|
141
109
|
|
|
142
|
-
self.
|
|
143
|
-
self.scheduling_utilities.
|
|
144
|
-
|
|
110
|
+
state: States = self.scheduling_utilities.get_state()
|
|
111
|
+
self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
|
|
112
|
+
state
|
|
145
113
|
)
|
|
146
|
-
return self._api_response(mission)
|
|
147
114
|
|
|
115
|
+
self.scheduling_utilities.return_home()
|
|
116
|
+
|
|
117
|
+
@tracer.start_as_current_span("pause_mission")
|
|
148
118
|
def pause_mission(self) -> ControlMissionResponse:
|
|
149
119
|
self.logger.info("Received request to pause current mission")
|
|
150
120
|
|
|
@@ -152,7 +122,7 @@ class SchedulingController:
|
|
|
152
122
|
|
|
153
123
|
if state not in [
|
|
154
124
|
States.Monitor,
|
|
155
|
-
States.
|
|
125
|
+
States.ReturningHome,
|
|
156
126
|
]:
|
|
157
127
|
error_message = (
|
|
158
128
|
f"Conflict - Pause command received in invalid state - State: {state}"
|
|
@@ -168,12 +138,13 @@ class SchedulingController:
|
|
|
168
138
|
)
|
|
169
139
|
return pause_mission_response
|
|
170
140
|
|
|
141
|
+
@tracer.start_as_current_span("resume_mission")
|
|
171
142
|
def resume_mission(self) -> ControlMissionResponse:
|
|
172
143
|
self.logger.info("Received request to resume current mission")
|
|
173
144
|
|
|
174
145
|
state: States = self.scheduling_utilities.get_state()
|
|
175
146
|
|
|
176
|
-
if state
|
|
147
|
+
if state not in [States.Paused, States.ReturnHomePaused]:
|
|
177
148
|
error_message = (
|
|
178
149
|
f"Conflict - Resume command received in invalid state - State: {state}"
|
|
179
150
|
)
|
|
@@ -185,12 +156,29 @@ class SchedulingController:
|
|
|
185
156
|
)
|
|
186
157
|
return resume_mission_response
|
|
187
158
|
|
|
188
|
-
|
|
159
|
+
@tracer.start_as_current_span("stop_mission")
|
|
160
|
+
def stop_mission(
|
|
161
|
+
self,
|
|
162
|
+
mission_id: StopMissionDefinition = Body(
|
|
163
|
+
default=None,
|
|
164
|
+
embed=True,
|
|
165
|
+
title="Mission ID to stop",
|
|
166
|
+
description="The mission ID of the mission being stopped, in json format",
|
|
167
|
+
),
|
|
168
|
+
) -> ControlMissionResponse:
|
|
169
|
+
|
|
189
170
|
self.logger.info("Received request to stop current mission")
|
|
190
171
|
|
|
191
172
|
state: States = self.scheduling_utilities.get_state()
|
|
192
173
|
|
|
193
|
-
if
|
|
174
|
+
if (
|
|
175
|
+
state == States.UnknownStatus
|
|
176
|
+
or state == States.Stopping
|
|
177
|
+
or state == States.BlockedProtectiveStop
|
|
178
|
+
or state == States.Offline
|
|
179
|
+
or state == States.Home
|
|
180
|
+
or state == States.ReturningHome
|
|
181
|
+
):
|
|
194
182
|
error_message = (
|
|
195
183
|
f"Conflict - Stop command received in invalid state - State: {state}"
|
|
196
184
|
)
|
|
@@ -198,71 +186,94 @@ class SchedulingController:
|
|
|
198
186
|
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
199
187
|
|
|
200
188
|
stop_mission_response: ControlMissionResponse = (
|
|
201
|
-
self.scheduling_utilities.stop_mission()
|
|
189
|
+
self.scheduling_utilities.stop_mission(mission_id.mission_id)
|
|
202
190
|
)
|
|
203
191
|
return stop_mission_response
|
|
204
192
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
default=None,
|
|
209
|
-
title="Target Pose",
|
|
210
|
-
description="The target pose for the drive_to step",
|
|
211
|
-
),
|
|
212
|
-
) -> StartMissionResponse:
|
|
213
|
-
self.logger.info("Received request to start new drive-to mission")
|
|
193
|
+
@tracer.start_as_current_span("release_intervention_needed")
|
|
194
|
+
def release_intervention_needed(self) -> None:
|
|
195
|
+
self.logger.info("Received request to release intervention needed state")
|
|
214
196
|
|
|
215
197
|
state: States = self.scheduling_utilities.get_state()
|
|
216
198
|
|
|
217
|
-
|
|
199
|
+
if state != States.InterventionNeeded:
|
|
200
|
+
error_message = f"Conflict - Release intervention needed command received in invalid state - State: {state}"
|
|
201
|
+
self.logger.warning(error_message)
|
|
202
|
+
raise HTTPException(
|
|
203
|
+
status_code=HTTPStatus.CONFLICT,
|
|
204
|
+
detail=error_message,
|
|
205
|
+
)
|
|
218
206
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
mission: Mission = Mission(tasks=[Task(steps=[step])])
|
|
207
|
+
self.scheduling_utilities.release_intervention_needed()
|
|
208
|
+
self.logger.info("Released intervention needed state successfully")
|
|
222
209
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
226
|
-
self.scheduling_utilities.start_mission(mission=mission, initial_pose=None)
|
|
227
|
-
return self._api_response(mission)
|
|
210
|
+
@tracer.start_as_current_span("lockdown")
|
|
211
|
+
def lockdown(self) -> None:
|
|
212
|
+
self.logger.info("Received request to lockdown robot")
|
|
228
213
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
214
|
+
state: States = self.scheduling_utilities.get_state()
|
|
215
|
+
|
|
216
|
+
if state == States.Lockdown:
|
|
217
|
+
error_message = "Conflict - Lockdown command received in lockdown state"
|
|
218
|
+
self.logger.warning(error_message)
|
|
219
|
+
raise HTTPException(
|
|
220
|
+
status_code=HTTPStatus.CONFLICT,
|
|
221
|
+
detail=error_message,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self.scheduling_utilities.lock_down_robot()
|
|
225
|
+
self.logger.info("Lockdown started successfully")
|
|
226
|
+
|
|
227
|
+
@tracer.start_as_current_span("release_lockdown")
|
|
228
|
+
def release_lockdown(self) -> None:
|
|
229
|
+
self.logger.info("Received request to release robot lockdown")
|
|
239
230
|
|
|
240
231
|
state: States = self.scheduling_utilities.get_state()
|
|
241
232
|
|
|
242
|
-
|
|
233
|
+
if state != States.Lockdown:
|
|
234
|
+
error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
|
|
235
|
+
self.logger.warning(error_message)
|
|
236
|
+
raise HTTPException(
|
|
237
|
+
status_code=HTTPStatus.CONFLICT,
|
|
238
|
+
detail=error_message,
|
|
239
|
+
)
|
|
243
240
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
mission: Mission = Mission(tasks=[Task(steps=[step])])
|
|
241
|
+
self.scheduling_utilities.release_robot_lockdown()
|
|
242
|
+
self.logger.info("Released lockdown successfully")
|
|
247
243
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
self.scheduling_utilities.start_mission(
|
|
252
|
-
mission=mission,
|
|
253
|
-
initial_pose=None,
|
|
254
|
-
)
|
|
255
|
-
return self._api_response(mission)
|
|
244
|
+
@tracer.start_as_current_span("maintenance_mode")
|
|
245
|
+
def set_maintenance_mode(self) -> None:
|
|
246
|
+
self.logger.info("Received request to set maintenance_mode")
|
|
256
247
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
248
|
+
state: States = self.scheduling_utilities.get_state()
|
|
249
|
+
|
|
250
|
+
if state == States.Maintenance or state == States.StoppingDueToMaintenance:
|
|
251
|
+
message = f"Conflict - Call to set maintenance mode was given while in state {state}."
|
|
252
|
+
self.logger.info(message)
|
|
253
|
+
raise HTTPException(
|
|
254
|
+
status_code=HTTPStatus.CONFLICT,
|
|
255
|
+
detail=message,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
self.scheduling_utilities.set_maintenance_mode()
|
|
259
|
+
self.logger.info("Maintenance mode has been set")
|
|
260
|
+
|
|
261
|
+
@tracer.start_as_current_span("release_maintenance_mode")
|
|
262
|
+
def release_maintenance_mode(self) -> None:
|
|
263
|
+
self.logger.info("Received request to release robot from maintenance mode")
|
|
264
|
+
|
|
265
|
+
state: States = self.scheduling_utilities.get_state()
|
|
266
|
+
|
|
267
|
+
if state != States.Maintenance:
|
|
268
|
+
message = f"Conflict - Release maintenance mode command received in invalid state - State: {state}"
|
|
269
|
+
self.logger.info(message)
|
|
270
|
+
raise HTTPException(
|
|
271
|
+
status_code=HTTPStatus.CONFLICT,
|
|
272
|
+
detail=message,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self.scheduling_utilities.release_maintenance_mode()
|
|
276
|
+
self.logger.info("Maintenance mode successfully released")
|
|
266
277
|
|
|
267
278
|
def _api_response(self, mission: Mission) -> StartMissionResponse:
|
|
268
279
|
return StartMissionResponse(
|
|
@@ -270,9 +281,12 @@ class SchedulingController:
|
|
|
270
281
|
tasks=[self._task_api_response(task) for task in mission.tasks],
|
|
271
282
|
)
|
|
272
283
|
|
|
273
|
-
def _task_api_response(self, task:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
def _task_api_response(self, task: TASKS) -> TaskResponse:
|
|
285
|
+
if isinstance(task, InspectionTask):
|
|
286
|
+
inspection_id = task.inspection_id
|
|
287
|
+
else:
|
|
288
|
+
inspection_id = None
|
|
277
289
|
|
|
278
|
-
return TaskResponse(
|
|
290
|
+
return TaskResponse(
|
|
291
|
+
id=task.id, tag_id=task.tag_id, inspection_id=inspection_id, type=task.type
|
|
292
|
+
)
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
from fastapi import Depends
|
|
4
4
|
from fastapi.security.base import SecurityBase
|
|
5
5
|
from fastapi_azure_auth import SingleTenantAzureAuthorizationCodeBearer
|
|
6
|
-
from fastapi_azure_auth.exceptions import
|
|
6
|
+
from fastapi_azure_auth.exceptions import InvalidAuthHttp
|
|
7
7
|
from fastapi_azure_auth.user import User
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
@@ -35,7 +35,7 @@ async def validate_has_role(user: User = Depends(azure_scheme)) -> None:
|
|
|
35
35
|
Raises a 403 authorization error if not.
|
|
36
36
|
"""
|
|
37
37
|
if settings.REQUIRED_ROLE not in user.roles:
|
|
38
|
-
raise
|
|
38
|
+
raise InvalidAuthHttp(
|
|
39
39
|
"Current user does not possess the required role for this endpoint"
|
|
40
40
|
)
|
|
41
41
|
|
|
@@ -48,9 +48,9 @@ class Authenticator:
|
|
|
48
48
|
self.logger = logging.getLogger("api")
|
|
49
49
|
self.authentication_enabled: bool = authentication_enabled
|
|
50
50
|
enabled_string = "enabled" if self.authentication_enabled else "disabled"
|
|
51
|
-
self.logger.info(
|
|
51
|
+
self.logger.info("API authentication is %s", enabled_string)
|
|
52
52
|
|
|
53
|
-
def should_authenticate(self):
|
|
53
|
+
def should_authenticate(self) -> bool:
|
|
54
54
|
return self.authentication_enabled
|
|
55
55
|
|
|
56
56
|
def get_scheme(self):
|
|
@@ -58,7 +58,7 @@ class Authenticator:
|
|
|
58
58
|
return validate_has_role
|
|
59
59
|
return NoSecurity
|
|
60
60
|
|
|
61
|
-
async def load_config(self):
|
|
61
|
+
async def load_config(self) -> None:
|
|
62
62
|
"""
|
|
63
63
|
Load OpenID config on startup.
|
|
64
64
|
"""
|
isar/config/certs/ca-cert.pem
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
MIIGGzCCBAOgAwIBAgIUCVuS8tL7R2bdjRJznkk1NN0oUa8wDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwgZwxCzAJBgNVBAYTAk5PMQ8wDQYDVQQIDAZCZXJnZW4xDzANBgNVBAcMBkJl
|
|
4
4
|
cmdlbjEUMBIGA1UECgwLRXFfUm9ib3RpY3MxETAPBgNVBAsMCFJvYm90aWNzMRww
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
5
|
+
GgYDVQQDDBNSb2JvdGljc19TZWxmU2lnbmVkMSQwIgYJKoZIhvcNAQkBFhVmZ19y
|
|
6
|
+
b2JvdHNAZXF1aW5vci5jb20wHhcNMjQxMjExMDczNzAzWhcNMjUxMjExMDczNzAz
|
|
7
|
+
WjCBnDELMAkGA1UEBhMCTk8xDzANBgNVBAgMBkJlcmdlbjEPMA0GA1UEBwwGQmVy
|
|
8
|
+
Z2VuMRQwEgYDVQQKDAtFcV9Sb2JvdGljczERMA8GA1UECwwIUm9ib3RpY3MxHDAa
|
|
9
|
+
BgNVBAMME1JvYm90aWNzX1NlbGZTaWduZWQxJDAiBgkqhkiG9w0BCQEWFWZnX3Jv
|
|
10
|
+
Ym90c0BlcXVpbm9yLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
|
|
11
|
+
ALVlT8EjA8UpXdARJM6YfhH+9DEyytb0SJYOEm9jWL7X0G5eMbNfKtfFj/BLQBBw
|
|
12
|
+
GDqyvhFa8SO56zM3Yi1Lir41JkpOKf2zJpNaL1syPxOA87MAm43UN/p2Mo6QAYpC
|
|
13
|
+
lJzmeNoagLFi/kIzGajeUwISOf//Zej0jK/9CpuvvNVHz4wRk4Ap9qtgrM9qhlNq
|
|
14
|
+
rZL4ZQODnvDZ/nwAYwEh9O2sbRr73aiHFcbxNAU2XxtT5UwbZMHom2Xic2Dmubqn
|
|
15
|
+
cWFU+C4upEqPvgYU/hxia/rkhHyicXmdXT6IzjVxPbrlPBHoKVodJOCQ3UoBQl5X
|
|
16
|
+
caHCluKOblA/yX+vEhv2h/m8XOtWiq0Jxnx+6D7B1piZfQnnNVSBT0bOYASb2cpn
|
|
17
|
+
8rJE3RBdBwiqjKTotVTZ8PAS/GY0Gz3GoMAUCvIGA02+/cO9ENls9D79/86lgiCQ
|
|
18
|
+
UqyteNNmo+oMqKHBw9Jmeejw1kbpnJN1abc270+qCT17aeX8yiMza5CyUTQTtgFI
|
|
19
|
+
62xIYaHWhN7GrxOvoMU8X0G1HINwjS5heIhu5bND/26dLZqV3aBUWwb4ql5QIce2
|
|
20
|
+
q5BQKmMqQLZF6J0aFL9dKJv5u3ioPFmiYPsPiRlwusPOoOFCiMt2kqZE3h3wxu6V
|
|
21
|
+
cXWdj0sH+dgxuGYTxrLuoSru4Xv/HPXh15mVY0/eI+u/AgMBAAGjUzBRMB0GA1Ud
|
|
22
|
+
DgQWBBQW2Ms6M3iadKVLOnraMlx6Exm8KjAfBgNVHSMEGDAWgBQW2Ms6M3iadKVL
|
|
23
|
+
OnraMlx6Exm8KjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQB0
|
|
24
|
+
XorfCLmVUN3aQWBouwG0fXHBqi3d4RA0vPsi8w+R/dOuHnLBLlHAiOkCMqW23bLs
|
|
25
|
+
ir2UVqpGXcp5keKGeCTj+bmdqhnS/B9eoaeA8np/zDPH834v+Qv7JA5ch50gYrL7
|
|
26
|
+
llF/02e5m0egP2xx9V7k90Du8OBm7xQgfbyrUcB2laC+loPyQiHMF24CU4ESfC4K
|
|
27
|
+
DBXOZm8Kd6+fvzEs8XDLKwjNb9MocHIJikysZTncxfhgVPaU5KCqF3afHkCkZGzn
|
|
28
|
+
Z+yMBfWbZ2WtM3lfwmM7z5NCTSqSFucW5+kw/OnzS3nQLyz2Q4nh4ApojsI3v+FH
|
|
29
|
+
xz8aCITPbVwKAHGYO9jbk9x8LlkzDPCCzBH7dGKLKaQSCoD+IOwig1cI7pM77WeI
|
|
30
|
+
BNC0g2ZCuLdb7c37/kT/1xGrIGUrpDsO34ZWW7E2X8kQPkAgqMNp0uXcnvIoNHO1
|
|
31
|
+
eVL08Om+7WbwK7wS3te5IWDq4rRWVW4Jv709w8z6porr3W8ipdtUoYqbgZI8jOiW
|
|
32
|
+
osY4UVcRwQ4S1oF5pNRzXlZAPwAnaDipglxHBiy9/ARS6HKpOCbmUHbj+goXYNyZ
|
|
33
|
+
ps8TcYc9LFOq4HBE4KKbV971jf/wQy1nVcm2qSbOJ9MPHM5BG22m1iVhSYbLPgE2
|
|
34
|
+
SBZvCS+voD8MqpBB1tPcvVqlPb1pFxf1lOc5QV3uOA==
|
|
35
35
|
-----END CERTIFICATE-----
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import traceback
|
|
3
2
|
from typing import Union
|
|
4
3
|
|
|
5
4
|
from azure.core.exceptions import (
|
|
@@ -66,7 +65,7 @@ class Keyvault:
|
|
|
66
65
|
raise KeyvaultError # type: ignore
|
|
67
66
|
|
|
68
67
|
def get_secret_client(self) -> SecretClient:
|
|
69
|
-
if self.client
|
|
68
|
+
if self.client is None:
|
|
70
69
|
try:
|
|
71
70
|
credential: Union[ClientSecretCredential, DefaultAzureCredential]
|
|
72
71
|
if self.client_id and self.client_secret and self.tenant_id:
|
isar/config/log.py
CHANGED
|
@@ -1,63 +1,71 @@
|
|
|
1
|
-
import importlib.resources as pkg_resources
|
|
2
1
|
import logging
|
|
3
2
|
import logging.config
|
|
3
|
+
import os
|
|
4
|
+
from importlib.resources import as_file, files
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
6
|
-
from opencensus.ext.azure.log_exporter import AzureLogHandler
|
|
7
7
|
from uvicorn.logging import ColourizedFormatter
|
|
8
8
|
|
|
9
|
-
from isar.config.configuration_error import ConfigurationError
|
|
10
|
-
from isar.config.keyvault.keyvault_error import KeyvaultError
|
|
11
|
-
from isar.config.keyvault.keyvault_service import Keyvault
|
|
12
9
|
from isar.config.settings import settings
|
|
13
10
|
|
|
11
|
+
from .settings import Settings
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
log_config = yaml.safe_load(open(path))
|
|
13
|
+
|
|
14
|
+
def setup_loggers() -> None:
|
|
15
|
+
log_config = load_log_config()
|
|
19
16
|
|
|
20
17
|
logging.config.dictConfig(log_config)
|
|
21
18
|
|
|
19
|
+
env_log_levels = {}
|
|
20
|
+
for env_var, value in os.environ.items():
|
|
21
|
+
if env_var.endswith("_LOG_LEVEL"):
|
|
22
|
+
log_name = env_var.split("_LOG_LEVEL")[0].lower()
|
|
23
|
+
env_log_levels[log_name] = value.upper()
|
|
24
|
+
|
|
22
25
|
handlers = []
|
|
23
26
|
if settings.LOG_HANDLER_LOCAL_ENABLED:
|
|
24
|
-
handlers.append(configure_console_handler(log_config=log_config))
|
|
25
|
-
if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
|
|
26
27
|
handlers.append(
|
|
27
|
-
|
|
28
|
+
configure_console_handler(log_config=log_config, settings=settings)
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
for log_handler in handlers:
|
|
31
|
-
for
|
|
32
|
-
logging.getLogger(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
for logger_name, logger_config in log_config["loggers"].items():
|
|
33
|
+
logger = logging.getLogger(logger_name)
|
|
34
|
+
logger.addHandler(log_handler)
|
|
35
|
+
if "level" in logger_config:
|
|
36
|
+
logger.setLevel(logger_config["level"])
|
|
37
|
+
if logger_name in env_log_levels:
|
|
38
|
+
logger.setLevel(env_log_levels[logger_name])
|
|
39
|
+
root_logger = logging.getLogger()
|
|
40
|
+
root_logger.addHandler(log_handler)
|
|
41
|
+
if "level" in log_config.get("root", {}):
|
|
42
|
+
root_logger.setLevel(log_config["root"]["level"])
|
|
36
43
|
|
|
37
|
-
def configure_console_handler(log_config: dict) -> logging.Handler:
|
|
38
|
-
handler = logging.StreamHandler()
|
|
39
|
-
handler.setLevel(log_config["root"]["level"])
|
|
40
|
-
handler.setFormatter(
|
|
41
|
-
ColourizedFormatter(
|
|
42
|
-
log_config["formatters"]["colourized"]["format"],
|
|
43
|
-
style="{",
|
|
44
|
-
use_colors=True,
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
return handler
|
|
48
44
|
|
|
45
|
+
def load_log_config():
|
|
46
|
+
source = files("isar").joinpath("config").joinpath("logging.conf")
|
|
47
|
+
with as_file(source) as f:
|
|
48
|
+
log_config = yaml.safe_load(open(f))
|
|
49
|
+
return log_config
|
|
49
50
|
|
|
50
|
-
def configure_azure_handler(log_config: dict, keyvault: Keyvault) -> logging.Handler:
|
|
51
|
-
connection_string: str
|
|
52
|
-
try:
|
|
53
|
-
connection_string = keyvault.get_secret(
|
|
54
|
-
"application-insights-connection-string"
|
|
55
|
-
).value
|
|
56
|
-
except KeyvaultError:
|
|
57
|
-
message: str = f"CRITICAL ERROR: Missing connection string for Application Insights in key vault '{keyvault.name}'."
|
|
58
|
-
print(f"\n{message} \n")
|
|
59
|
-
raise ConfigurationError(message)
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
def configure_console_handler(log_config: dict, settings: Settings) -> logging.Handler:
|
|
53
|
+
handler = logging.StreamHandler()
|
|
62
54
|
handler.setLevel(log_config["root"]["level"])
|
|
55
|
+
if settings.DEBUG_LOG_FORMATTER:
|
|
56
|
+
handler.setFormatter(
|
|
57
|
+
ColourizedFormatter(
|
|
58
|
+
log_config["formatters"]["debug-formatter"]["format"],
|
|
59
|
+
style="{",
|
|
60
|
+
use_colors=True,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
handler.setFormatter(
|
|
65
|
+
ColourizedFormatter(
|
|
66
|
+
log_config["formatters"]["colourized"]["format"],
|
|
67
|
+
style="{",
|
|
68
|
+
use_colors=True,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
63
71
|
return handler
|