Skip to content

Commit af40432

Browse files
Ben Bensontdewey-rpi
authored andcommitted
rpi-sb-provision monitor
Added a TUI to monitor the status of each stage. Ability to click on the devices and view the logs afterwards. Added hooks in provisioner.sh to help the app Added dependency python3-textual
1 parent ae4fb7d commit af40432

File tree

3 files changed

+324
-0
lines changed

3 files changed

+324
-0
lines changed

app/layout.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Screen {
2+
layout: vertical;
3+
background: rgb(32, 0, 20);
4+
}
5+
6+
Processing {
7+
layout: horizontal;
8+
}
9+
Ended {
10+
layout: horizontal;
11+
}
12+
FileSelector {
13+
layout: horizontal;
14+
height: 3;
15+
}
16+
17+
LogScreen {
18+
background: rgb(32, 0, 20);
19+
}
20+
21+
.box {
22+
height: 1fr;
23+
}
24+
25+
.box2 {
26+
height: 100%;
27+
width: 1fr;
28+
/* background: rgb(102, 1, 63); */
29+
border: solid rgb(255, 0, 106);
30+
}
31+
.data_text {
32+
height: 100%;
33+
width: 1fr;
34+
color: rgb(255, 160, 200);
35+
}
36+
.fileselectorbutton {
37+
height: 3;
38+
width: 1fr;
39+
border: solid rgb(255, 0, 106);
40+
margin-left: 1;
41+
margin-right: 1;
42+
background: rgb(167, 0, 69);
43+
}

app/main.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import ScrollableContainer, Container
3+
from textual.widgets import Header, Footer, DataTable, Static, Button
4+
from textual.reactive import reactive
5+
from textual.message import Message
6+
from textual.screen import Screen
7+
from textual.widget import Widget
8+
from textual import on
9+
from textual import events
10+
import systemctl_python
11+
12+
13+
class Devices_list(Static):
14+
dev_type_g = ""
15+
devices=reactive([])
16+
17+
def __init__(self, dev_type):
18+
self.dev_type = dev_type
19+
super().__init__()
20+
def update_devices(self) -> None:
21+
self.devices = systemctl_python.list_working_units("rpi-sb-" + self.dev_type + "*")
22+
def watch_devices(self, devices) -> None:
23+
"""Called when the devices variable changes"""
24+
text = ""
25+
for i in range(len(devices)):
26+
text += devices[i] + "\n"
27+
self.styles.height = len(devices)
28+
self.update(text)
29+
30+
def on_mount(self) -> None:
31+
self.set_interval(1/20, self.update_devices)
32+
33+
ROWS = [
34+
("Serial Number",),
35+
]
36+
37+
class CompletedDevicesList(Widget):
38+
dev_type_g = ""
39+
devices=reactive([])
40+
def compose(self) -> ComposeResult:
41+
yield DataTable()
42+
def __init__(self, dev_type):
43+
self.dev_type = dev_type
44+
super().__init__()
45+
def update_devices(self) -> None:
46+
self.devices = systemctl_python.list_completed_devices()
47+
def watch_devices(self, devices: list[str]) -> None:
48+
"""Called when the devices variable changes"""
49+
table = self.query_one(DataTable)
50+
table_devices = []
51+
for device in self.devices:
52+
table_devices.append((device, ))
53+
table.clear()
54+
table.add_rows(table_devices)
55+
56+
def on_mount(self) -> None:
57+
table = self.query_one(DataTable)
58+
table.add_columns(*ROWS[0])
59+
table.add_rows(ROWS[1:])
60+
self.set_interval(1/20, self.update_devices)
61+
62+
class Failed_devices_list(Static):
63+
dev_type_g = ""
64+
devices=reactive([])
65+
def compose(self) -> ComposeResult:
66+
yield DataTable()
67+
def __init__(self, dev_type):
68+
self.dev_type = dev_type
69+
super().__init__()
70+
def update_devices(self) -> None:
71+
self.devices = systemctl_python.list_failed_devices()
72+
def watch_devices(self, devices: list[str]) -> None:
73+
"""Called when the devices variable changes"""
74+
table = self.query_one(DataTable)
75+
table_devices = []# [("TEST",), ("TEST",)]
76+
for device in self.devices:
77+
table_devices.append((device, ))
78+
table.clear()
79+
table.add_rows(table_devices)
80+
81+
def on_mount(self) -> None:
82+
table = self.query_one(DataTable)
83+
table.add_columns(*ROWS[0])
84+
table.add_rows(ROWS[1:])
85+
self.set_interval(1/20, self.update_devices)
86+
87+
class Triage_Box(Static):
88+
def compose(self) -> ComposeResult:
89+
yield ScrollableContainer(Static("Triaging \n----------------"), Devices_list(dev_type="triage"))
90+
91+
class Keywrite_Box(Static):
92+
def compose(self) -> ComposeResult:
93+
yield ScrollableContainer(Static("Keywriting \n----------------"), Devices_list(dev_type="keywriter"))
94+
95+
class Provision_Box(Static):
96+
def compose(self) -> ComposeResult:
97+
yield ScrollableContainer(Static("Provisioning \n----------------"), Devices_list(dev_type="provision"))
98+
99+
100+
class Completed_Box(Static):
101+
def compose(self) -> ComposeResult:
102+
yield ScrollableContainer(Static("Completed \n----------------"), CompletedDevicesList(dev_type="provision"))
103+
104+
class Failed_Box(Static):
105+
def compose(self) -> ComposeResult:
106+
yield ScrollableContainer(Static("Failed \n----------------"), Failed_devices_list(dev_type="provision"))
107+
108+
class Processing(Static):
109+
def compose(self) -> ComposeResult:
110+
yield Triage_Box("1", classes="box2")
111+
yield Keywrite_Box("2", classes="box2")
112+
yield Provision_Box("3", classes="box2")
113+
114+
class Ended(Static):
115+
def compose(self) -> ComposeResult:
116+
yield Completed_Box("1", classes="box2")
117+
yield Failed_Box("2", classes="box2")
118+
119+
class FileSelector(Container):
120+
def __init__(self, filelist):
121+
self.filelist = filelist
122+
self.selected_file = None
123+
self.id_to_filename = {}
124+
super().__init__()
125+
def compose(self) -> ComposeResult:
126+
"""Create child widgets for the app."""
127+
# List files in the directory
128+
for file in self.filelist:
129+
self.id_to_filename.update([(file.replace(".", ""), file)])
130+
yield Button(file, id=file.replace(".", ""), classes="fileselectorbutton")
131+
def get_filename_from_id(self, id) -> str:
132+
return self.id_to_filename[id]
133+
134+
class MainScreen(Screen):
135+
def compose(self) -> ComposeResult:
136+
"""Create child widgets for the app."""
137+
yield Header()
138+
yield Footer()
139+
yield Processing("Processing", classes="box")
140+
yield Ended("Completed", classes="box")
141+
def action_goto_log(self) -> None:
142+
self.dismiss(self.query_one(Ended).get_device())
143+
144+
@on(DataTable.CellSelected)
145+
def on_cell_selected(self, event: DataTable.CellSelected) -> None:
146+
self.dismiss(event.value)
147+
148+
class LogScreen(Screen):
149+
def __init__(self, device_name):
150+
self.device_name = device_name
151+
super().__init__()
152+
153+
def compose(self) -> ComposeResult:
154+
"""Create child widgets for the app."""
155+
yield Header()
156+
yield Footer()
157+
yield Static("This is the log screen for device: " + self.device_name, id="header_string")
158+
yield FileSelector(filelist=systemctl_python.list_device_files(self.device_name))
159+
yield ScrollableContainer(Static(" ", id="file_contents"))
160+
161+
def on_button_pressed(self, event: Button.Pressed) -> None:
162+
static = self.query_one("#file_contents")
163+
fileselector = self.query_one("FileSelector")
164+
# Need to read the file into this container now!
165+
contents = systemctl_python.read_device_file(self.device_name, fileselector.get_filename_from_id(event.button.id))
166+
static.update(contents)
167+
168+
def on_screen_resume(self) -> None:
169+
static = self.query_one("#header_string")
170+
static.update(self.device_name)
171+
172+
173+
class App(App):
174+
"""A Textual app to manage stopwatches."""
175+
CSS_PATH = "layout.css"
176+
BINDINGS = [("m", "mainscreen", "Main Screen"), ("q", "quit", "Quit")]
177+
SCREENS = {"MainScreen": MainScreen(), "LogScreen": LogScreen("unknown-serial")}
178+
179+
def on_mount(self) -> None:
180+
self.title = "rpi-sb-provisioner"
181+
self.push_screen(LogScreen(device_name="INIT"))
182+
self.push_screen(MainScreen(), self.action_logscreen)
183+
184+
def action_mainscreen(self):
185+
self.pop_screen()
186+
self.push_screen(MainScreen(), self.action_logscreen)
187+
188+
def action_logscreen(self, device: str):
189+
self.push_screen(LogScreen(device))
190+
191+
if __name__ == "__main__":
192+
app = App()
193+
app.run()

app/systemctl_python.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import subprocess
2+
from os import listdir, path
3+
4+
def list_rpi_sb_units(service_name):
5+
output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True)
6+
triage=[]
7+
keywriter=[]
8+
provisioner=[]
9+
10+
lines = output.stdout.decode().split("\n")
11+
for line in lines:
12+
if "rpi-sb-" in line:
13+
name=line[line.find("rpi-sb-"):line.find(".service")]
14+
if "triage" in name:
15+
triage.append(name.replace("rpi-sb-triage@", ""))
16+
if "keywriter" in name:
17+
keywriter.append(name.replace("rpi-sb-keywriter@", ""))
18+
if "provisioner" in name:
19+
provisioner.append(name.replace("rpi-sb-provisioner@", ""))
20+
return [triage, keywriter, provisioner]
21+
22+
def list_working_units(service_name):
23+
output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True)
24+
units=[]
25+
lines = output.stdout.decode().split("\n")
26+
for line in lines:
27+
if "rpi-sb-" in line:
28+
if not("failed" in line):
29+
name=line[line.find("rpi-sb-"):line.find(".service")]
30+
units.append(name)
31+
return units
32+
33+
def list_failed_units(service_name):
34+
output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True)
35+
units=[]
36+
lines = output.stdout.decode().split("\n")
37+
for line in lines:
38+
if "rpi-sb-" in line:
39+
if "failed" in line:
40+
name=line[line.find("rpi-sb-"):line.find(".service")]
41+
units.append(name)
42+
return units
43+
44+
def list_seen_devices():
45+
if path.exists("/var/log/rpi-sb-provisioner/"):
46+
devices = listdir("/var/log/rpi-sb-provisioner")
47+
return devices
48+
else:
49+
return []
50+
51+
def list_completed_devices():
52+
all_devices = list_seen_devices()
53+
completed_devices = []
54+
for device in all_devices:
55+
if path.exists("/var/log/rpi-sb-provisioner/" + device + "/success"):
56+
f = open("/var/log/rpi-sb-provisioner/" + device + "/success", "r")
57+
status = f.read()
58+
if "1" in status:
59+
completed_devices.append(device)
60+
f.close()
61+
return completed_devices
62+
63+
def list_failed_devices():
64+
all_devices = list_seen_devices()
65+
failed_devices = []
66+
for device in all_devices:
67+
if path.exists("/var/log/rpi-sb-provisioner/" + device + "/finished"):
68+
if not(path.exists("/var/log/rpi-sb-provisioner/" + device + "/success")):
69+
f = open("/var/log/rpi-sb-provisioner/" + device + "/finished", "r")
70+
status = f.read()
71+
if "1" in status:
72+
failed_devices.append(device)
73+
f.close()
74+
return failed_devices
75+
76+
def list_device_files(device_name):
77+
if path.exists("/var/log/rpi-sb-provisioner/" + device_name):
78+
return listdir("/var/log/rpi-sb-provisioner/" + device_name)
79+
else:
80+
return []
81+
82+
def read_device_file(device_name, filename):
83+
contents = "Unable to read/open file!"
84+
if path.exists("/var/log/rpi-sb-provisioner/" + device_name + "/" + filename):
85+
f = open("/var/log/rpi-sb-provisioner/" + device_name + "/" + filename, "r")
86+
contents = f.read()
87+
f.close()
88+
return contents

0 commit comments

Comments
 (0)