isar 1.20.2__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/apis/api.py +148 -76
- isar/apis/models/__init__.py +0 -1
- isar/apis/models/models.py +21 -11
- isar/apis/models/start_mission_definition.py +110 -168
- isar/apis/robot_control/robot_controller.py +41 -0
- isar/apis/schedule/scheduling_controller.py +124 -162
- isar/apis/security/authentication.py +5 -5
- isar/config/certs/ca-cert.pem +33 -31
- isar/config/keyvault/keyvault_service.py +1 -1
- isar/config/log.py +45 -40
- isar/config/logging.conf +16 -31
- isar/config/open_telemetry.py +102 -0
- isar/config/predefined_mission_definition/default_exr.json +0 -2
- 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 +76 -111
- 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 -199
- 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 +57 -40
- isar/services/service_connections/mqtt/mqtt_client.py +47 -11
- isar/services/service_connections/mqtt/robot_heartbeat_publisher.py +5 -2
- isar/services/service_connections/mqtt/robot_info_publisher.py +3 -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 +219 -538
- isar/state_machine/states/__init__.py +0 -8
- 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 -247
- isar/state_machine/states/offline.py +51 -53
- 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 +21 -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 +70 -52
- isar/storage/local_storage.py +25 -12
- isar/storage/storage_interface.py +28 -7
- isar/storage/uploader.py +174 -55
- isar/storage/utilities.py +32 -29
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/METADATA +73 -110
- isar-1.34.9.dist-info/RECORD +135 -0
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/WHEEL +1 -1
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/entry_points.txt +1 -0
- robot_interface/models/exceptions/robot_exceptions.py +91 -41
- robot_interface/models/initialize/__init__.py +0 -1
- robot_interface/models/inspection/__init__.py +0 -13
- robot_interface/models/inspection/inspection.py +42 -33
- robot_interface/models/mission/mission.py +14 -15
- robot_interface/models/mission/status.py +20 -26
- robot_interface/models/mission/task.py +154 -121
- 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 +119 -84
- robot_interface/telemetry/mqtt_client.py +74 -12
- robot_interface/telemetry/payloads.py +91 -13
- 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 -25
- 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/stid/__init__.py +0 -0
- isar/services/utilities/queue_utilities.py +0 -39
- isar/state_machine/states/idle.py +0 -85
- isar/state_machine/states/initialize.py +0 -71
- isar/state_machine/states/initiate.py +0 -142
- isar/state_machine/states/off.py +0 -18
- isar/state_machine/states/stop.py +0 -95
- isar/storage/slimm_storage.py +0 -191
- isar-1.20.2.dist-info/RECORD +0 -116
- robot_interface/models/initialize/initialize_params.py +0 -9
- robot_interface/models/mission/step.py +0 -234
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info/licenses}/LICENSE +0 -0
- {isar-1.20.2.dist-info → isar-1.34.9.dist-info}/top_level.txt +0 -0
|
@@ -1,37 +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
|
-
|
|
26
|
-
|
|
27
|
-
MoveArm,
|
|
28
|
-
ReturnToHome,
|
|
29
|
-
)
|
|
30
|
-
from robot_interface.models.mission.task import Task
|
|
22
|
+
from robot_interface.models.mission.task import TASKS, InspectionTask
|
|
23
|
+
|
|
24
|
+
tracer = trace.get_tracer(__name__)
|
|
31
25
|
|
|
32
26
|
|
|
33
27
|
class SchedulingController:
|
|
34
|
-
@inject
|
|
35
28
|
def __init__(
|
|
36
29
|
self,
|
|
37
30
|
scheduling_utilities: SchedulingUtilities,
|
|
@@ -39,6 +32,7 @@ class SchedulingController:
|
|
|
39
32
|
self.scheduling_utilities: SchedulingUtilities = scheduling_utilities
|
|
40
33
|
self.logger = logging.getLogger("api")
|
|
41
34
|
|
|
35
|
+
@tracer.start_as_current_span("start_mission_by_id")
|
|
42
36
|
def start_mission_by_id(
|
|
43
37
|
self,
|
|
44
38
|
mission_id: str = Path(
|
|
@@ -46,46 +40,25 @@ class SchedulingController:
|
|
|
46
40
|
title="Mission ID",
|
|
47
41
|
description="ID-number for predefined mission",
|
|
48
42
|
),
|
|
49
|
-
initial_pose: Optional[InputPose] = Body(
|
|
50
|
-
default=None,
|
|
51
|
-
description="The starting point of the mission. Used for initial "
|
|
52
|
-
"localization of robot",
|
|
53
|
-
embed=True,
|
|
54
|
-
),
|
|
55
|
-
return_pose: Optional[InputPose] = Body(
|
|
56
|
-
default=None,
|
|
57
|
-
description="End pose of the mission. The robot return to the specified "
|
|
58
|
-
"pose after finishing all inspections",
|
|
59
|
-
embed=True,
|
|
60
|
-
),
|
|
61
43
|
) -> StartMissionResponse:
|
|
62
|
-
self.logger.info(
|
|
44
|
+
self.logger.info("Received request to start mission with id %s", mission_id)
|
|
63
45
|
|
|
64
46
|
state: States = self.scheduling_utilities.get_state()
|
|
65
47
|
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
|
|
66
48
|
|
|
67
49
|
mission: Mission = self.scheduling_utilities.get_mission(mission_id)
|
|
68
|
-
if return_pose:
|
|
69
|
-
pose: Pose = return_pose.to_alitra_pose()
|
|
70
|
-
step: DriveToPose = DriveToPose(pose=pose)
|
|
71
|
-
mission.tasks.append(Task(steps=[step]))
|
|
72
50
|
|
|
73
51
|
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
74
52
|
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
75
53
|
)
|
|
76
54
|
|
|
77
|
-
|
|
78
|
-
initial_pose.to_alitra_pose() if initial_pose else None
|
|
79
|
-
)
|
|
55
|
+
self.logger.info("Starting mission with ISAR Mission ID: '%s'", mission.id)
|
|
80
56
|
|
|
81
|
-
self.
|
|
82
|
-
|
|
83
|
-
self.scheduling_utilities.start_mission(
|
|
84
|
-
mission=mission, initial_pose=initial_pose_alitra
|
|
85
|
-
)
|
|
57
|
+
self.scheduling_utilities.start_mission(mission=mission)
|
|
86
58
|
|
|
87
59
|
return self._api_response(mission)
|
|
88
60
|
|
|
61
|
+
@tracer.start_as_current_span("start_mission")
|
|
89
62
|
def start_mission(
|
|
90
63
|
self,
|
|
91
64
|
mission_definition: StartMissionDefinition = Body(
|
|
@@ -94,35 +67,26 @@ class SchedulingController:
|
|
|
94
67
|
title="Mission Definition",
|
|
95
68
|
description="Description of the mission in json format",
|
|
96
69
|
),
|
|
97
|
-
initial_pose: Optional[InputPose] = Body(
|
|
98
|
-
default=None,
|
|
99
|
-
description="The starting point of the mission. Used for initial "
|
|
100
|
-
"localization of robot",
|
|
101
|
-
embed=True,
|
|
102
|
-
),
|
|
103
|
-
return_pose: Optional[InputPose] = Body(
|
|
104
|
-
default=None,
|
|
105
|
-
description="End pose of the mission. The robot return to the specified "
|
|
106
|
-
"pose after finishing all inspections",
|
|
107
|
-
embed=True,
|
|
108
|
-
),
|
|
109
70
|
) -> StartMissionResponse:
|
|
110
71
|
self.logger.info("Received request to start new mission")
|
|
111
72
|
|
|
112
73
|
if not mission_definition:
|
|
113
|
-
|
|
74
|
+
error_message_no_mission_definition: str = (
|
|
114
75
|
"Unprocessable entity - 'mission_definition' empty or invalid"
|
|
115
76
|
)
|
|
116
|
-
self.logger.error(
|
|
77
|
+
self.logger.error(error_message_no_mission_definition)
|
|
117
78
|
raise HTTPException(
|
|
118
|
-
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
79
|
+
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
80
|
+
detail=error_message_no_mission_definition,
|
|
119
81
|
)
|
|
120
82
|
|
|
121
83
|
state: States = self.scheduling_utilities.get_state()
|
|
122
84
|
self.scheduling_utilities.verify_state_machine_ready_to_receive_mission(state)
|
|
123
85
|
|
|
124
86
|
try:
|
|
125
|
-
mission: Mission = to_isar_mission(
|
|
87
|
+
mission: Mission = to_isar_mission(
|
|
88
|
+
start_mission_definition=mission_definition
|
|
89
|
+
)
|
|
126
90
|
except MissionPlannerError as e:
|
|
127
91
|
error_message = f"Bad Request - Cannot create ISAR mission: {e}"
|
|
128
92
|
self.logger.warning(error_message)
|
|
@@ -134,21 +98,23 @@ class SchedulingController:
|
|
|
134
98
|
self.scheduling_utilities.verify_robot_capable_of_mission(
|
|
135
99
|
mission=mission, robot_capabilities=robot_settings.CAPABILITIES
|
|
136
100
|
)
|
|
137
|
-
if return_pose:
|
|
138
|
-
pose: Pose = return_pose.to_alitra_pose()
|
|
139
|
-
step: DriveToPose = DriveToPose(pose=pose)
|
|
140
|
-
mission.tasks.append(Task(steps=[step]))
|
|
141
101
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
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")
|
|
145
109
|
|
|
146
|
-
self.
|
|
147
|
-
self.scheduling_utilities.
|
|
148
|
-
|
|
110
|
+
state: States = self.scheduling_utilities.get_state()
|
|
111
|
+
self.scheduling_utilities.verify_state_machine_ready_to_receive_return_home_mission(
|
|
112
|
+
state
|
|
149
113
|
)
|
|
150
|
-
return self._api_response(mission)
|
|
151
114
|
|
|
115
|
+
self.scheduling_utilities.return_home()
|
|
116
|
+
|
|
117
|
+
@tracer.start_as_current_span("pause_mission")
|
|
152
118
|
def pause_mission(self) -> ControlMissionResponse:
|
|
153
119
|
self.logger.info("Received request to pause current mission")
|
|
154
120
|
|
|
@@ -156,7 +122,7 @@ class SchedulingController:
|
|
|
156
122
|
|
|
157
123
|
if state not in [
|
|
158
124
|
States.Monitor,
|
|
159
|
-
States.
|
|
125
|
+
States.ReturningHome,
|
|
160
126
|
]:
|
|
161
127
|
error_message = (
|
|
162
128
|
f"Conflict - Pause command received in invalid state - State: {state}"
|
|
@@ -172,12 +138,13 @@ class SchedulingController:
|
|
|
172
138
|
)
|
|
173
139
|
return pause_mission_response
|
|
174
140
|
|
|
141
|
+
@tracer.start_as_current_span("resume_mission")
|
|
175
142
|
def resume_mission(self) -> ControlMissionResponse:
|
|
176
143
|
self.logger.info("Received request to resume current mission")
|
|
177
144
|
|
|
178
145
|
state: States = self.scheduling_utilities.get_state()
|
|
179
146
|
|
|
180
|
-
if state
|
|
147
|
+
if state not in [States.Paused, States.ReturnHomePaused]:
|
|
181
148
|
error_message = (
|
|
182
149
|
f"Conflict - Resume command received in invalid state - State: {state}"
|
|
183
150
|
)
|
|
@@ -189,12 +156,29 @@ class SchedulingController:
|
|
|
189
156
|
)
|
|
190
157
|
return resume_mission_response
|
|
191
158
|
|
|
192
|
-
|
|
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
|
+
|
|
193
170
|
self.logger.info("Received request to stop current mission")
|
|
194
171
|
|
|
195
172
|
state: States = self.scheduling_utilities.get_state()
|
|
196
173
|
|
|
197
|
-
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
|
+
):
|
|
198
182
|
error_message = (
|
|
199
183
|
f"Conflict - Stop command received in invalid state - State: {state}"
|
|
200
184
|
)
|
|
@@ -202,119 +186,94 @@ class SchedulingController:
|
|
|
202
186
|
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=error_message)
|
|
203
187
|
|
|
204
188
|
stop_mission_response: ControlMissionResponse = (
|
|
205
|
-
self.scheduling_utilities.stop_mission()
|
|
189
|
+
self.scheduling_utilities.stop_mission(mission_id.mission_id)
|
|
206
190
|
)
|
|
207
191
|
return stop_mission_response
|
|
208
192
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
default=None,
|
|
213
|
-
title="Target Pose",
|
|
214
|
-
description="The target pose for the drive_to step",
|
|
215
|
-
),
|
|
216
|
-
) -> StartMissionResponse:
|
|
217
|
-
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")
|
|
218
196
|
|
|
219
197
|
state: States = self.scheduling_utilities.get_state()
|
|
220
198
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
+
)
|
|
226
206
|
|
|
227
|
-
self.
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
self.scheduling_utilities.start_mission(mission=mission, initial_pose=None)
|
|
231
|
-
return self._api_response(mission)
|
|
207
|
+
self.scheduling_utilities.release_intervention_needed()
|
|
208
|
+
self.logger.info("Released intervention needed state successfully")
|
|
232
209
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
default=None,
|
|
237
|
-
embed=True,
|
|
238
|
-
title="Localization Pose",
|
|
239
|
-
description="The current position of the robot",
|
|
240
|
-
),
|
|
241
|
-
) -> StartMissionResponse:
|
|
242
|
-
self.logger.info("Received request to start new localization mission")
|
|
210
|
+
@tracer.start_as_current_span("lockdown")
|
|
211
|
+
def lockdown(self) -> None:
|
|
212
|
+
self.logger.info("Received request to lockdown robot")
|
|
243
213
|
|
|
244
214
|
state: States = self.scheduling_utilities.get_state()
|
|
245
215
|
|
|
246
|
-
|
|
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
|
+
)
|
|
247
223
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
mission: Mission = Mission(tasks=[Task(steps=[step])])
|
|
224
|
+
self.scheduling_utilities.lock_down_robot()
|
|
225
|
+
self.logger.info("Lockdown started successfully")
|
|
251
226
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
self.scheduling_utilities.start_mission(
|
|
256
|
-
mission=mission,
|
|
257
|
-
initial_pose=None,
|
|
258
|
-
)
|
|
259
|
-
return self._api_response(mission)
|
|
227
|
+
@tracer.start_as_current_span("release_lockdown")
|
|
228
|
+
def release_lockdown(self) -> None:
|
|
229
|
+
self.logger.info("Received request to release robot lockdown")
|
|
260
230
|
|
|
261
|
-
|
|
262
|
-
self,
|
|
263
|
-
arm_pose_literal: str = Path(
|
|
264
|
-
...,
|
|
265
|
-
alias="arm_pose_literal",
|
|
266
|
-
title="Arm pose literal",
|
|
267
|
-
description="Arm pose as a literal",
|
|
268
|
-
),
|
|
269
|
-
) -> StartMissionResponse:
|
|
270
|
-
self.logger.info("Received request to start new move arm mission")
|
|
231
|
+
state: States = self.scheduling_utilities.get_state()
|
|
271
232
|
|
|
272
|
-
if
|
|
273
|
-
error_message
|
|
274
|
-
f"Received a request to move the arm but the robot "
|
|
275
|
-
f"{settings.ROBOT_NAME} does not support moving an arm"
|
|
276
|
-
)
|
|
233
|
+
if state != States.Lockdown:
|
|
234
|
+
error_message = f"Conflict - Release lockdown command received in invalid state - State: {state}"
|
|
277
235
|
self.logger.warning(error_message)
|
|
278
236
|
raise HTTPException(
|
|
279
|
-
status_code=HTTPStatus.
|
|
237
|
+
status_code=HTTPStatus.CONFLICT,
|
|
238
|
+
detail=error_message,
|
|
280
239
|
)
|
|
281
240
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
241
|
+
self.scheduling_utilities.release_robot_lockdown()
|
|
242
|
+
self.logger.info("Released lockdown successfully")
|
|
243
|
+
|
|
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")
|
|
247
|
+
|
|
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)
|
|
289
253
|
raise HTTPException(
|
|
290
|
-
status_code=HTTPStatus.
|
|
254
|
+
status_code=HTTPStatus.CONFLICT,
|
|
255
|
+
detail=message,
|
|
291
256
|
)
|
|
292
257
|
|
|
293
|
-
|
|
258
|
+
self.scheduling_utilities.set_maintenance_mode()
|
|
259
|
+
self.logger.info("Maintenance mode has been set")
|
|
294
260
|
|
|
295
|
-
|
|
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")
|
|
296
264
|
|
|
297
|
-
|
|
298
|
-
mission: Mission = Mission(tasks=[Task(steps=[step])])
|
|
265
|
+
state: States = self.scheduling_utilities.get_state()
|
|
299
266
|
|
|
300
|
-
|
|
301
|
-
f"
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return self._api_response(mission)
|
|
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
|
+
)
|
|
308
274
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
robot_package=settings.ROBOT_PACKAGE,
|
|
312
|
-
isar_id=settings.ISAR_ID,
|
|
313
|
-
robot_name=settings.ROBOT_NAME,
|
|
314
|
-
robot_capabilities=robot_settings.CAPABILITIES,
|
|
315
|
-
robot_map_name=settings.DEFAULT_MAP,
|
|
316
|
-
plant_short_name=settings.PLANT_SHORT_NAME,
|
|
317
|
-
)
|
|
275
|
+
self.scheduling_utilities.release_maintenance_mode()
|
|
276
|
+
self.logger.info("Maintenance mode successfully released")
|
|
318
277
|
|
|
319
278
|
def _api_response(self, mission: Mission) -> StartMissionResponse:
|
|
320
279
|
return StartMissionResponse(
|
|
@@ -322,9 +281,12 @@ class SchedulingController:
|
|
|
322
281
|
tasks=[self._task_api_response(task) for task in mission.tasks],
|
|
323
282
|
)
|
|
324
283
|
|
|
325
|
-
def _task_api_response(self, task:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
289
|
|
|
330
|
-
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,33 +1,35 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
+
MIIGGzCCBAOgAwIBAgIUCVuS8tL7R2bdjRJznkk1NN0oUa8wDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwgZwxCzAJBgNVBAYTAk5PMQ8wDQYDVQQIDAZCZXJnZW4xDzANBgNVBAcMBkJl
|
|
4
|
+
cmdlbjEUMBIGA1UECgwLRXFfUm9ib3RpY3MxETAPBgNVBAsMCFJvYm90aWNzMRww
|
|
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==
|
|
33
35
|
-----END CERTIFICATE-----
|
|
@@ -65,7 +65,7 @@ class Keyvault:
|
|
|
65
65
|
raise KeyvaultError # type: ignore
|
|
66
66
|
|
|
67
67
|
def get_secret_client(self) -> SecretClient:
|
|
68
|
-
if self.client
|
|
68
|
+
if self.client is None:
|
|
69
69
|
try:
|
|
70
70
|
credential: Union[ClientSecretCredential, DefaultAzureCredential]
|
|
71
71
|
if self.client_id and self.client_secret and self.tenant_id:
|
isar/config/log.py
CHANGED
|
@@ -1,66 +1,71 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import logging.config
|
|
3
|
+
import os
|
|
3
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
|
-
with as_file(source) as f:
|
|
19
|
-
log_config = yaml.safe_load(open(f))
|
|
13
|
+
|
|
14
|
+
def setup_loggers() -> None:
|
|
15
|
+
log_config = load_log_config()
|
|
20
16
|
|
|
21
17
|
logging.config.dictConfig(log_config)
|
|
22
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
|
+
|
|
23
25
|
handlers = []
|
|
24
26
|
if settings.LOG_HANDLER_LOCAL_ENABLED:
|
|
25
|
-
handlers.append(configure_console_handler(log_config=log_config))
|
|
26
|
-
if settings.LOG_HANDLER_APPLICATION_INSIGHTS_ENABLED:
|
|
27
27
|
handlers.append(
|
|
28
|
-
|
|
28
|
+
configure_console_handler(log_config=log_config, settings=settings)
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
for log_handler in handlers:
|
|
32
|
-
for
|
|
33
|
-
logging.getLogger(
|
|
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
44
|
|
|
38
|
-
def
|
|
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
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def configure_console_handler(log_config: dict, settings: Settings) -> logging.Handler:
|
|
39
53
|
handler = logging.StreamHandler()
|
|
40
54
|
handler.setLevel(log_config["root"]["level"])
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
)
|
|
46
62
|
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
connection_string = keyvault.get_secret(
|
|
55
|
-
"application-insights-connection-string"
|
|
56
|
-
).value
|
|
57
|
-
except KeyvaultError:
|
|
58
|
-
message: str = (
|
|
59
|
-
f"CRITICAL ERROR: Missing connection string for Application Insights in key vault '{keyvault.name}'."
|
|
63
|
+
else:
|
|
64
|
+
handler.setFormatter(
|
|
65
|
+
ColourizedFormatter(
|
|
66
|
+
log_config["formatters"]["colourized"]["format"],
|
|
67
|
+
style="{",
|
|
68
|
+
use_colors=True,
|
|
69
|
+
)
|
|
60
70
|
)
|
|
61
|
-
print(f"\n{message} \n")
|
|
62
|
-
raise ConfigurationError(message)
|
|
63
|
-
|
|
64
|
-
handler = AzureLogHandler(connection_string=connection_string)
|
|
65
|
-
handler.setLevel(log_config["root"]["level"])
|
|
66
71
|
return handler
|