Skip to content

Commit 29804e2

Browse files
check all configs for mqtt and address stuff; adding pioreactor-web.target status and restart to leadeR
1 parent ebb5cc7 commit 29804e2

File tree

6 files changed

+164
-39
lines changed

6 files changed

+164
-39
lines changed

core/pioreactor/web/api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2365,10 +2365,10 @@ def update_config(filename: str) -> ResponseReturnValue:
23652365
assert config.get("cluster.topology", "leader_address")
23662366
assert config["mqtt"]
23672367

2368-
if config.get("cluster.topology", "leader_address").startswith("http") or config.get(
2369-
"mqtt", "broker_address"
2370-
).startswith("http"):
2371-
raise ValueError("Don't start addresses with http:// or https://")
2368+
if config.get("cluster.topology", "leader_address", fallback="").startswith("http") or config.get(
2369+
"mqtt", "broker_address", fallback=""
2370+
).startswith("http"):
2371+
abort_with(400, "Don't start addresses with http:// or https://")
23722372

23732373
except configparser.DuplicateSectionError as e:
23742374
msg = f"Duplicate section [{e.section}] was found. Please fix and try again."

core/pioreactor/web/tasks.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,15 @@ def reboot(wait=0) -> bool:
848848
return result.returncode == 0
849849

850850

851+
@huey.task()
852+
def restart_pioreactor_web_target() -> bool:
853+
logger.debug("Restarting pioreactor-web.target")
854+
if whoami.is_testing_env():
855+
return True
856+
result = run(["sudo", "systemctl", "restart", "pioreactor-web.target"])
857+
return result.returncode == 0
858+
859+
851860
@huey.task()
852861
def pios(*args: str, env: dict[str, str] | None = None) -> bool:
853862
env = filter_to_allowed_env(env or {})

core/pioreactor/web/unit_api.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import json
33
import os
44
import zipfile
5+
from functools import wraps
56
from io import BytesIO
67
from pathlib import Path
8+
from subprocess import run
79
from tempfile import NamedTemporaryFile
810
from time import sleep
911
from typing import Any
@@ -142,6 +144,16 @@ def _build_calibration_protocol_payloads() -> list[dict[str, Any]]:
142144
return sorted(protocols, key=lambda item: (item["target_device"], item["title"]))
143145

144146

147+
def require_leader(view_func):
148+
@wraps(view_func)
149+
def _wrapped(*args, **kwargs):
150+
if HOSTNAME != get_leader_hostname():
151+
abort_with(403, "This endpoint is only available on the leader.")
152+
return view_func(*args, **kwargs)
153+
154+
return _wrapped
155+
156+
145157
### SYSTEM
146158

147159

@@ -201,6 +213,49 @@ def shutdown() -> DelayedResponseReturnValue:
201213
return create_task_response(task)
202214

203215

216+
@unit_api_bp.route("/system/web_server/status", methods=["GET"])
217+
@require_leader
218+
def get_web_server_status() -> ResponseReturnValue:
219+
if whoami.is_testing_env():
220+
status_text = "active"
221+
return attach_cache_control(
222+
jsonify(
223+
{
224+
"service": "pioreactor-web.target",
225+
"state": "ready",
226+
"raw_status": status_text,
227+
}
228+
),
229+
max_age=0,
230+
)
231+
232+
result = run(
233+
["systemctl", "is-active", "pioreactor-web.target"],
234+
capture_output=True,
235+
text=True,
236+
)
237+
status_text = (result.stdout or result.stderr).strip()
238+
is_active = result.returncode == 0 and status_text == "active"
239+
state = "ready" if is_active else "disconnected"
240+
return attach_cache_control(
241+
jsonify(
242+
{
243+
"service": "pioreactor-web.target",
244+
"state": state,
245+
"raw_status": status_text,
246+
}
247+
),
248+
max_age=3,
249+
)
250+
251+
252+
@unit_api_bp.route("/system/web_server/restart", methods=["POST", "PATCH"])
253+
@require_leader
254+
def restart_web_server() -> DelayedResponseReturnValue:
255+
task = tasks.restart_pioreactor_web_target()
256+
return create_task_response(task)
257+
258+
204259
@unit_api_bp.route("/system/remove_file", methods=["POST", "PATCH"])
205260
def remove_file() -> DelayedResponseReturnValue:
206261
task_name = "remove_file"

frontend/src/Leader.jsx

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,10 @@ function LeaderCard({leaderHostname}) {
592592

593593
function LeaderJobs(){
594594

595+
const webServerJobName = "web server"
595596
const [mqtt_to_db_streaming_state, set_mqtt_to_db_streaming_state] = React.useState("disconnected")
596597
const [monitor_state, set_monitor_state] = React.useState("disconnected")
598+
const [webServerState, setWebServerState] = React.useState("disconnected")
597599
const [otherLongRunningJobs, setOtherLongRunningJobs] = React.useState([])
598600
const [restartingJob, setRestartingJob] = React.useState(null)
599601

@@ -618,45 +620,93 @@ function LeaderJobs(){
618620
}
619621
}
620622

623+
async function restartWebServer() {
624+
setRestartingJob(webServerJobName);
625+
try {
626+
const response = await fetch("/unit_api/system/web_server/restart", {
627+
method: "POST",
628+
});
629+
if (!response.ok) {
630+
throw new Error(`Failed to restart web server: ${response.statusText}`);
631+
}
632+
} catch (error) {
633+
console.error("Error restarting web server:", error);
634+
} finally {
635+
setRestartingJob(null);
636+
}
637+
}
638+
621639
React.useEffect(() => {
622640
let ignore = false;
623-
fetch("/unit_api/long_running_jobs/running")
624-
.then((response) => {
625-
if (!response.ok) {
626-
throw new Error(`Failed to fetch long-running jobs: ${response.statusText}`);
627-
}
628-
return response.json();
629-
})
630-
.then((data) => {
631-
if (ignore) {
632-
return;
641+
const fetchJobs = async () => {
642+
try {
643+
const [jobsResult, webResult] = await Promise.allSettled([
644+
fetch("/unit_api/long_running_jobs/running"),
645+
fetch("/unit_api/system/web_server/status"),
646+
]);
647+
648+
if (jobsResult.status === "fulfilled") {
649+
const response = jobsResult.value;
650+
if (!response.ok) {
651+
throw new Error(`Failed to fetch long-running jobs: ${response.statusText}`);
652+
}
653+
const data = await response.json();
654+
655+
if (!ignore) {
656+
let mqttState = "disconnected";
657+
let monitorState = "disconnected";
658+
const remainingJobs = [];
659+
660+
data.forEach((job) => {
661+
switch (job.job_name) {
662+
case "mqtt_to_db_streaming":
663+
mqttState = "ready";
664+
break;
665+
case "monitor":
666+
monitorState = "ready";
667+
break;
668+
default:
669+
remainingJobs.push({ job_name: job.job_name, state: "ready" });
670+
break;
671+
}
672+
});
673+
674+
set_mqtt_to_db_streaming_state(mqttState);
675+
set_monitor_state(monitorState);
676+
setOtherLongRunningJobs(remainingJobs);
677+
}
678+
} else {
679+
console.error("Error fetching long-running jobs:", jobsResult.reason);
633680
}
634681

635-
let mqttState = "disconnected";
636-
let monitorState = "disconnected";
637-
const remainingJobs = [];
638-
639-
data.forEach((job) => {
640-
switch (job.job_name) {
641-
case "mqtt_to_db_streaming":
642-
mqttState = "ready";
643-
break;
644-
case "monitor":
645-
monitorState = "ready";
646-
break;
647-
default:
648-
remainingJobs.push({ job_name: job.job_name, state: "ready" });
649-
break;
682+
if (webResult.status === "fulfilled") {
683+
const response = webResult.value;
684+
if (response.ok) {
685+
const webData = await response.json();
686+
if (!ignore) {
687+
setWebServerState(webData?.state || "disconnected");
688+
}
689+
} else {
690+
console.error("Failed to fetch web server status:", response.statusText);
691+
if (!ignore) {
692+
setWebServerState("disconnected");
693+
}
650694
}
651-
});
652-
653-
set_mqtt_to_db_streaming_state(mqttState);
654-
set_monitor_state(monitorState);
655-
setOtherLongRunningJobs(remainingJobs);
656-
})
657-
.catch((error) => {
695+
} else {
696+
console.error("Error fetching web server status:", webResult.reason);
697+
if (!ignore) {
698+
setWebServerState("disconnected");
699+
}
700+
}
701+
} catch (error) {
658702
console.error("Error fetching long-running jobs:", error);
659-
});
703+
if (!ignore) {
704+
setWebServerState("disconnected");
705+
}
706+
}
707+
};
708+
709+
fetchJobs();
660710
return () => {
661711
ignore = true;
662712
};
@@ -678,6 +728,17 @@ function LeaderJobs(){
678728
</TableRow>
679729
</TableHead>
680730
<TableBody>
731+
<TableRow>
732+
<TableCell sx={{padding: "6px 0px"}}>{webServerJobName}</TableCell>
733+
<TableCell align="right"><StateTypography state={webServerState}/></TableCell>
734+
<TableCell align="right" sx={{padding: "6px 0px"}}>
735+
<RestartJobButton
736+
jobName={webServerJobName}
737+
onRestart={restartWebServer}
738+
isRestarting={restartingJob === webServerJobName}
739+
/>
740+
</TableCell>
741+
</TableRow>
681742
<TableRow>
682743
<TableCell sx={{padding: "6px 0px"}}>mqtt_to_db_streaming</TableCell>
683744
<TableCell align="right"><StateTypography state={mqtt_to_db_streaming_state}/></TableCell>

frontend/src/Pioreactor.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ function SettingsActionsDialog(props) {
825825
size="small"
826826
variant="outlined"
827827
label="XR"
828-
sx={{ ml: 0.5, height: 18, fontSize: "0.65rem", "& .MuiChip-label": { px: "5px" } }}
828+
sx={{ ml: 0.5, height: 18, fontSize: "0.65rem", "& .MuiChip-label": { px: "5px", pt: "1px" } }}
829829
/>
830830
) : null}
831831
</Typography>

frontend/src/Pioreactors.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2627,7 +2627,7 @@ function PioreactorCard({unit, isUnitActive, experiment, config, originalLabel,
26272627
size="small"
26282628
variant="outlined"
26292629
label="XR"
2630-
sx={{ ml: 0.5, height: 18, fontSize: "0.65rem", "& .MuiChip-label": { px: "5px" } }}
2630+
sx={{ ml: 0.5, height: 18, fontSize: "0.65rem", "& .MuiChip-label": { px: "5px", pt: "1px" } }}
26312631
/>
26322632
) : null}
26332633
</Typography>

0 commit comments

Comments
 (0)