Skip to content

Commit 64a6085

Browse files
some unit_api fixes
1 parent 0cec540 commit 64a6085

File tree

2 files changed

+42
-24
lines changed

2 files changed

+42
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
- new events in the dosing_automation_events table & export detailing when dosing starts and stops.
66
-
77

8+
#### Breaking changes
9+
10+
- Removed `/api/workers/<pioreactor_unit>/configuration`; use `/api/units/<pioreactor_unit>/configuration`.
11+
812
### 25.12.10
913

1014
#### Enhancements

core/pioreactor/web/unit_api.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -153,19 +153,31 @@ def shutdown() -> DelayedResponseReturnValue:
153153

154154
@unit_api_bp.route("/system/remove_file", methods=["POST", "PATCH"])
155155
def remove_file() -> DelayedResponseReturnValue:
156+
task_name = "remove_file"
156157
disallow_file = Path(os.environ["DOT_PIOREACTOR"]) / "DISALLOW_UI_FILE_SYSTEM"
157158
if os.path.isfile(disallow_file):
158159
publish_to_error_log(f"Delete blocked because {disallow_file} is present", task_name)
159160
abort(403, "DISALLOW_UI_FILE_SYSTEM is present")
160161

161-
162162
# use filepath in body
163-
body = request.get_json()
163+
body = current_app.get_json(request.data) or {}
164+
filepath = body.get("filepath")
165+
if not filepath:
166+
abort(400, "filepath field is required")
167+
assert filepath is not None
164168

165-
if not (body["filepath"].startswith("/home/pioreactor")):
169+
base_dir = Path(os.environ["DOT_PIOREACTOR"]).resolve()
170+
candidate_path = Path(filepath).expanduser()
171+
if not candidate_path.is_absolute():
172+
candidate_path = (base_dir / candidate_path).resolve()
173+
else:
174+
candidate_path = candidate_path.resolve()
175+
try:
176+
candidate_path.relative_to(base_dir)
177+
except ValueError:
166178
abort(403, "Access to this path is not allowed")
167179

168-
task = tasks.rm(body["filepath"])
180+
task = tasks.rm(str(candidate_path))
169181
return create_task_response(task)
170182

171183

@@ -183,23 +195,23 @@ def get_clock_time():
183195
@unit_api_bp.route("/system/utc_clock", methods=["PATCH", "POST"])
184196
def set_clock_time() -> DelayedResponseReturnValue: # type: ignore[return]
185197
if HOSTNAME == get_leader_hostname():
186-
if request.get_json(silent=True): # don't throw 415
187-
data = request.json
188-
new_time = data.get("utc_clock_time")
189-
if not new_time:
190-
abort(400, "utc_clock_time field is required")
198+
data = request.get_json(silent=True) # don't throw 415
199+
if not data:
200+
abort(400, "utc_clock_time field is required")
191201

192-
# validate the timestamp
193-
try:
194-
to_datetime(new_time)
195-
except ValueError:
196-
abort(400, "Invalid utc_clock_time format. Use ISO 8601.")
197-
198-
# Update the system clock (requires admin privileges)
199-
t = tasks.update_clock(new_time)
200-
return create_task_response(t)
201-
else:
202-
abort(404, "utc_clock_time field required")
202+
new_time = data.get("utc_clock_time")
203+
if not new_time:
204+
abort(400, "utc_clock_time field is required")
205+
206+
# validate the timestamp
207+
try:
208+
to_datetime(new_time)
209+
except ValueError:
210+
abort(400, "Invalid utc_clock_time format. Use ISO 8601.")
211+
212+
# Update the system clock (requires admin privileges)
213+
t = tasks.update_clock(new_time)
214+
return create_task_response(t)
203215
else:
204216
# sync using chrony
205217
t = tasks.sync_clock()
@@ -401,7 +413,7 @@ def get_settings_for_a_specific_job(job_name: str) -> ResponseReturnValue:
401413
if settings:
402414
return jsonify({"settings": {s["setting"]: s["value"] for s in settings}})
403415
else:
404-
return {"status": "error"}, 404
416+
abort(404, "No settings found for job.")
405417

406418

407419
@unit_api_bp.route("/jobs/settings/job_name/<job_name>/setting/<setting>", methods=["GET"])
@@ -421,7 +433,7 @@ def get_specific_setting_for_a_job(job_name: str, setting: str) -> ResponseRetur
421433
if setting_metadata:
422434
return jsonify({setting_metadata["setting"]: setting_metadata["value"]})
423435
else:
424-
return {"status": "error"}, 404
436+
abort(404, "Setting not found.")
425437

426438

427439
@unit_api_bp.route("/jobs/settings/job_name/<job_name>", methods=["PATCH"])
@@ -480,15 +492,15 @@ def get_installed_plugins() -> ResponseReturnValue:
480492
status, msg = False, "Timed out."
481493

482494
if not status:
483-
return jsonify([]), 404
495+
abort(404, msg)
484496
else:
485497
# sometimes an error from a plugin will be printed. We just want to last line, the json bit.
486498
_, _, plugins_as_json = msg.rpartition("\n")
487499
return attach_cache_control(
488500
Response(
489501
response=plugins_as_json,
490502
status=200,
491-
mimetype="text/json",
503+
mimetype="application/json",
492504
)
493505
)
494506

@@ -910,6 +922,7 @@ def get_calibrations_by_device(device: str) -> ResponseReturnValue:
910922
# first try to open it using our struct, but only to verify it.
911923
cal = to_builtins(yaml_decode(file.read_bytes(), type=AllCalibrations))
912924
cal["is_active"] = c.get(device) == cal["calibration_name"]
925+
cal["pioreactor_unit"] = HOSTNAME
913926
calibrations.append(cal)
914927
except Exception as e:
915928
publish_to_error_log(f"Error reading {file.stem}: {e}", "get_calibrations_by_device")
@@ -930,6 +943,7 @@ def get_calibration(device: str, cal_name: str) -> ResponseReturnValue:
930943
try:
931944
cal = to_builtins(yaml_decode(calibration_path.read_bytes(), type=AllCalibrations))
932945
cal["is_active"] = c.get(device) == cal["calibration_name"]
946+
cal["pioreactor_unit"] = HOSTNAME
933947
return attach_cache_control(jsonify(cal), max_age=10)
934948
except Exception as e:
935949
publish_to_error_log(f"Error reading {calibration_path.stem}: {e}", "get_calibration")

0 commit comments

Comments
 (0)