diff --git a/.gitignore b/.gitignore index 70f3b0a..a4eeab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.deb # macOS Finder intermediates -*.DS_Store \ No newline at end of file +*.DS_Store + +*/__pycache__ diff --git a/README.adoc b/README.adoc index e733bd4..d36b9e6 100644 --- a/README.adoc +++ b/README.adoc @@ -53,19 +53,41 @@ Now, install the `rpi-sb-provisioner` package from the releases area: ---- $ sudo dpkg -i rpi-sb-provisioner_foo.deb $ sudo apt --fix-broken install +$ sudo reboot now ---- -Copy the example configuration file into the expected location: +Next, you will have to configure `rpi-sb-provisioner` by using the TUI. In a terminal, run: ---- -$ cp /etc/default/rpi-sb-provisioner /etc/rpi-sb-provisioner/config +$ configure.sh ---- -Edit this configuration file with your editor of choice. For example: +WARNING: This will not work if you have not reboot after installing the package! ----- -$ sudo nano /etc/rpi-sb-provisioner/config ----- +Running this command will open up a full screen text UI. The TUI supports mouse input or keyboard navigation! +Each of the boxes contains a name, text entry and help button. The steps for editing each parameter are as follows: + +[pdfwidth=90%] +.A parameter entry area +image::docs/images/rpi-config-textfield.png[] + + +*1 -* Click or use `tab` to click the help button to view the information about the parameter + +*2 -* Navigate to the text field and enter the value you wish + +*3 -* To stage this value for writing, you must click `return` on your keyboard. If the value is successfully verified, then the field will change color to green and a tick should appear. If validation fails, a warning popup should appear with some help text. A cross will also appear next to the parameter name. + +[pdfwidth=90%] +.A successfully verified parameter +image::docs/images/rpi-config-successfully-verified.png[] + + +*4 -* Repeat the above steps to complete your required parameters (some are optional). + +*5 -* Write to the configuration file by pressing the `Write verified params to config file` button at the bottom of the screen + +Once you have followed all those steps, `rpi-sb-provisioner` should be correctly configured and ready to run. == Configuration fields @@ -176,9 +198,10 @@ No further intervention is required in the success case. WARNING: `rpi-sb-provisioner` will not, by default, block JTAG access. If you wish to make use of this facility, you _must_ specify this in the Raspberry Pi Bootloader configuration pointed to by `RPI_DEVICE_BOOTLOADER_CONFIG_FILE` -=== Monitoring via the app +=== Monitoring via the monitoring application -`rpi-sb-provisioner` also contains a monitoring TUI. This can be used to observe how far through the process each device is. It also allows for easy introspection of the log files and lists all completed and failed devices. +`rpi-sb-provisioner` also contains a monitoring application. This can be used to observe the progress of a device as it is being provisioned. It also allows for easy introspection of the log files and lists all completed and failed devices. +The monitoring application supports both mouse or keyboard input. Navigation between boxes can be acheived by using the `tab` key or by clicking on the desired area. To run, type into a terminal window: @@ -186,8 +209,10 @@ To run, type into a terminal window: $ monitor.sh ---- -The TUI will start up with 3 sections, triaging, keywriting and provisoning. If a device is connected, you will be able to watch it progress through each of the sections. -The TUI also has two areas at the bottom, one for successfully completed provisions and one for failed provisions. Clicking on the device name will open up a log window, along with options to view the log files for each step of the provisioning service. +The TUI will intialise with 2 rows, the top one showing the progress of a device throughout the process, with each of the columns being for devices in the following stages: triaging, keywriting and provisoning. +When a device is connected, you will be able to watch it progress through each of the sections. +The second row of the TUI also has two boxes at the bottom, the left being successfully completed provisions and the right for failed provisions. +Clicking on the device name will open up a second window, with buttons to view the log files for each step of the provisioning service. To return to the main monitoring screen, just press the key `m`. To quit the app use the key combination `CTRL-C` or `q`. diff --git a/app/layout.css b/app/layout.css deleted file mode 100644 index 02e0cb3..0000000 --- a/app/layout.css +++ /dev/null @@ -1,43 +0,0 @@ -Screen { - layout: vertical; - background: rgb(32, 0, 20); -} - -Processing { - layout: horizontal; -} -Ended { - layout: horizontal; -} -FileSelector { - layout: horizontal; - height: 3; -} - -LogScreen { - background: rgb(32, 0, 20); -} - -.box { - height: 1fr; -} - -.box2 { - height: 100%; - width: 1fr; - /* background: rgb(102, 1, 63); */ - border: solid rgb(255, 0, 106); -} -.data_text { - height: 100%; - width: 1fr; - color: rgb(255, 160, 200); -} -.fileselectorbutton { - height: 3; - width: 1fr; - border: solid rgb(255, 0, 106); - margin-left: 1; - margin-right: 1; - background: rgb(167, 0, 69); -} \ No newline at end of file diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 09f04f6..0000000 --- a/app/main.py +++ /dev/null @@ -1,193 +0,0 @@ -from textual.app import App, ComposeResult -from textual.containers import ScrollableContainer, Container -from textual.widgets import Header, Footer, DataTable, Static, Button -from textual.reactive import reactive -from textual.message import Message -from textual.screen import Screen -from textual.widget import Widget -from textual import on -from textual import events -import systemctl_python - - -class Devices_list(Static): - dev_type_g = "" - devices=reactive([]) - - def __init__(self, dev_type): - self.dev_type = dev_type - super().__init__() - def update_devices(self) -> None: - self.devices = systemctl_python.list_working_units("rpi-sb-" + self.dev_type + "*") - def watch_devices(self, devices) -> None: - """Called when the devices variable changes""" - text = "" - for i in range(len(devices)): - text += devices[i] + "\n" - self.styles.height = len(devices) - self.update(text) - - def on_mount(self) -> None: - self.set_interval(1/20, self.update_devices) - -ROWS = [ - ("Serial Number",), -] - -class CompletedDevicesList(Widget): - dev_type_g = "" - devices=reactive([]) - def compose(self) -> ComposeResult: - yield DataTable() - def __init__(self, dev_type): - self.dev_type = dev_type - super().__init__() - def update_devices(self) -> None: - self.devices = systemctl_python.list_completed_devices() - def watch_devices(self, devices: list[str]) -> None: - """Called when the devices variable changes""" - table = self.query_one(DataTable) - table_devices = [] - for device in self.devices: - table_devices.append((device, )) - table.clear() - table.add_rows(table_devices) - - def on_mount(self) -> None: - table = self.query_one(DataTable) - table.add_columns(*ROWS[0]) - table.add_rows(ROWS[1:]) - self.set_interval(1/20, self.update_devices) - -class Failed_devices_list(Static): - dev_type_g = "" - devices=reactive([]) - def compose(self) -> ComposeResult: - yield DataTable() - def __init__(self, dev_type): - self.dev_type = dev_type - super().__init__() - def update_devices(self) -> None: - self.devices = systemctl_python.list_failed_devices() - def watch_devices(self, devices: list[str]) -> None: - """Called when the devices variable changes""" - table = self.query_one(DataTable) - table_devices = []# [("TEST",), ("TEST",)] - for device in self.devices: - table_devices.append((device, )) - table.clear() - table.add_rows(table_devices) - - def on_mount(self) -> None: - table = self.query_one(DataTable) - table.add_columns(*ROWS[0]) - table.add_rows(ROWS[1:]) - self.set_interval(1/20, self.update_devices) - -class Triage_Box(Static): - def compose(self) -> ComposeResult: - yield ScrollableContainer(Static("Triaging \n----------------"), Devices_list(dev_type="triage")) - -class Keywrite_Box(Static): - def compose(self) -> ComposeResult: - yield ScrollableContainer(Static("Keywriting \n----------------"), Devices_list(dev_type="keywriter")) - -class Provision_Box(Static): - def compose(self) -> ComposeResult: - yield ScrollableContainer(Static("Provisioning \n----------------"), Devices_list(dev_type="provision")) - - -class Completed_Box(Static): - def compose(self) -> ComposeResult: - yield ScrollableContainer(Static("Completed \n----------------"), CompletedDevicesList(dev_type="provision")) - -class Failed_Box(Static): - def compose(self) -> ComposeResult: - yield ScrollableContainer(Static("Failed \n----------------"), Failed_devices_list(dev_type="provision")) - -class Processing(Static): - def compose(self) -> ComposeResult: - yield Triage_Box("1", classes="box2") - yield Keywrite_Box("2", classes="box2") - yield Provision_Box("3", classes="box2") - -class Ended(Static): - def compose(self) -> ComposeResult: - yield Completed_Box("1", classes="box2") - yield Failed_Box("2", classes="box2") - -class FileSelector(Container): - def __init__(self, filelist): - self.filelist = filelist - self.selected_file = None - self.id_to_filename = {} - super().__init__() - def compose(self) -> ComposeResult: - """Create child widgets for the app.""" - # List files in the directory - for file in self.filelist: - self.id_to_filename.update([(file.replace(".", ""), file)]) - yield Button(file, id=file.replace(".", ""), classes="fileselectorbutton") - def get_filename_from_id(self, id) -> str: - return self.id_to_filename[id] - -class MainScreen(Screen): - def compose(self) -> ComposeResult: - """Create child widgets for the app.""" - yield Header() - yield Footer() - yield Processing("Processing", classes="box") - yield Ended("Completed", classes="box") - def action_goto_log(self) -> None: - self.dismiss(self.query_one(Ended).get_device()) - - @on(DataTable.CellSelected) - def on_cell_selected(self, event: DataTable.CellSelected) -> None: - self.dismiss(event.value) - -class LogScreen(Screen): - def __init__(self, device_name): - self.device_name = device_name - super().__init__() - - def compose(self) -> ComposeResult: - """Create child widgets for the app.""" - yield Header() - yield Footer() - yield Static("This is the log screen for device: " + self.device_name, id="header_string") - yield FileSelector(filelist=systemctl_python.list_device_files(self.device_name)) - yield ScrollableContainer(Static(" ", id="file_contents")) - - def on_button_pressed(self, event: Button.Pressed) -> None: - static = self.query_one("#file_contents") - fileselector = self.query_one("FileSelector") - # Need to read the file into this container now! - contents = systemctl_python.read_device_file(self.device_name, fileselector.get_filename_from_id(event.button.id)) - static.update(contents) - - def on_screen_resume(self) -> None: - static = self.query_one("#header_string") - static.update(self.device_name) - - -class App(App): - """A Textual app to manage stopwatches.""" - CSS_PATH = "layout.css" - BINDINGS = [("m", "mainscreen", "Main Screen"), ("q", "quit", "Quit")] - SCREENS = {"MainScreen": MainScreen(), "LogScreen": LogScreen("unknown-serial")} - - def on_mount(self) -> None: - self.title = "rpi-sb-provisioner" - self.push_screen(LogScreen(device_name="INIT")) - self.push_screen(MainScreen(), self.action_logscreen) - - def action_mainscreen(self): - self.pop_screen() - self.push_screen(MainScreen(), self.action_logscreen) - - def action_logscreen(self, device: str): - self.push_screen(LogScreen(device)) - -if __name__ == "__main__": - app = App() - app.run() \ No newline at end of file diff --git a/app/systemctl_python.py b/app/systemctl_python.py deleted file mode 100644 index fd543c8..0000000 --- a/app/systemctl_python.py +++ /dev/null @@ -1,88 +0,0 @@ -import subprocess -from os import listdir, path - -def list_rpi_sb_units(service_name): - output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True) - triage=[] - keywriter=[] - provisioner=[] - - lines = output.stdout.decode().split("\n") - for line in lines: - if "rpi-sb-" in line: - name=line[line.find("rpi-sb-"):line.find(".service")] - if "triage" in name: - triage.append(name.replace("rpi-sb-triage@", "")) - if "keywriter" in name: - keywriter.append(name.replace("rpi-sb-keywriter@", "")) - if "provisioner" in name: - provisioner.append(name.replace("rpi-sb-provisioner@", "")) - return [triage, keywriter, provisioner] - -def list_working_units(service_name): - output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True) - units=[] - lines = output.stdout.decode().split("\n") - for line in lines: - if "rpi-sb-" in line: - if not("failed" in line): - name=line[line.find("rpi-sb-"):line.find(".service")] - units.append(name) - return units - -def list_failed_units(service_name): - output = subprocess.run(["systemctl", "list-units", service_name, "-l", "--all", "--no-pager"], capture_output=True) - units=[] - lines = output.stdout.decode().split("\n") - for line in lines: - if "rpi-sb-" in line: - if "failed" in line: - name=line[line.find("rpi-sb-"):line.find(".service")] - units.append(name) - return units - -def list_seen_devices(): - if path.exists("/var/log/rpi-sb-provisioner/"): - devices = listdir("/var/log/rpi-sb-provisioner") - return devices - else: - return [] - -def list_completed_devices(): - all_devices = list_seen_devices() - completed_devices = [] - for device in all_devices: - if path.exists("/var/log/rpi-sb-provisioner/" + device + "/success"): - f = open("/var/log/rpi-sb-provisioner/" + device + "/success", "r") - status = f.read() - if "1" in status: - completed_devices.append(device) - f.close() - return completed_devices - -def list_failed_devices(): - all_devices = list_seen_devices() - failed_devices = [] - for device in all_devices: - if path.exists("/var/log/rpi-sb-provisioner/" + device + "/finished"): - if not(path.exists("/var/log/rpi-sb-provisioner/" + device + "/success")): - f = open("/var/log/rpi-sb-provisioner/" + device + "/finished", "r") - status = f.read() - if "1" in status: - failed_devices.append(device) - f.close() - return failed_devices - -def list_device_files(device_name): - if path.exists("/var/log/rpi-sb-provisioner/" + device_name): - return listdir("/var/log/rpi-sb-provisioner/" + device_name) - else: - return [] - -def read_device_file(device_name, filename): - contents = "Unable to read/open file!" - if path.exists("/var/log/rpi-sb-provisioner/" + device_name + "/" + filename): - f = open("/var/log/rpi-sb-provisioner/" + device_name + "/" + filename, "r") - contents = f.read() - f.close() - return contents \ No newline at end of file diff --git a/config/config.py b/config/config.py new file mode 100755 index 0000000..450b10b --- /dev/null +++ b/config/config.py @@ -0,0 +1,208 @@ +from textual.app import App, ComposeResult +from textual.containers import ScrollableContainer, Container +from textual.widgets import Header, Footer, DataTable, Static, Button, Input +from textual.reactive import reactive +from textual.message import Message +from textual.screen import Screen, ModalScreen +from textual.widget import Widget +from textual.validation import ValidationResult +from textual import on +from textual import events +import validator, os + +class ParamWidget(Widget): + def __init__(self, paramname, paramvalue, currentval): + self.paramname = paramname + self.paramvalue = paramvalue + self.currentval = currentval + super().__init__() + def compose(self) -> ComposeResult: + yield Static(self.paramname, classes="paramname", id="nameentry_" + self.paramname) + yield Input(placeholder=self.paramvalue, classes="paramentry", value=self.currentval, id="param_entry_"+self.paramname) #, validate_on="blur", validators=[validate(self.paramname)]) + yield Button("Help!", classes="paramhelp", id=self.paramname + "_helpbutton") + +class MainScreen(Screen): + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + for param in defaultparams: + yield ParamWidget(paramname=param, paramvalue=defaultparams[param], currentval=initialparams[param]) + yield Container(Button("Write verified params to config file", id="write_button", classes="write_button"), classes="bottom_bar") + +class HelpScreen(Screen): + def __init__(self, paramname, defaultvalue, currentvalue, optional, helptext): + self.paramname = paramname + self.defaultvalue = defaultvalue + self.currentvalue = currentvalue + self.optional = optional + self.helptext = helptext + if self.defaultvalue == "": + self.defaultvalue = "None" + super().__init__() + def compose(self) -> ComposeResult: + yield Container(Static(self.paramname + "\n"), Static(self.optional + "\n"), Static(self.helptext + "\n"), Static("Default Value: " + self.defaultvalue + "\n"), Button("OK", id="close_help_screen"), id="dialog") + + +class ValidatedScreen(Screen): + def __init__(self, paramname, errmsg, defaultvalue, currentvalue, optional, helptext): + self.paramname = paramname + self.defaultvalue = defaultvalue + self.currentvalue = currentvalue + self.optional = optional + self.helptext = helptext + self.errmsg = errmsg + if self.defaultvalue == "": + self.defaultvalue = "None" + super().__init__() + def compose(self) -> ComposeResult: + yield Container(Static("ERROR VALIDATING: " + self.paramname + " - NOT WRITING!" + "\n"), + Static("Error is : " + self.errmsg + "\n"), + Static("This value is " + self.optional + "\n"), + Static(self.helptext + "\n"), + Static("Default Value: " + self.defaultvalue + "\n"), + Button("OK", id="close_help_screen"), + id="dialog") + + +class OpeningScreen(Screen): + def __init__(self, differed_params, mandatory_not_set): + self.differed_params = "" + self.mandatory_not_set = "" + if len(differed_params) != 0: + for param in differed_params: + self.differed_params += param + " " + if len(mandatory_not_set) != 0: + for param in mandatory_not_set: + self.mandatory_not_set += param + " " + super().__init__() + def compose(self) -> ComposeResult: + if self.differed_params == "": + warning_text_1 = "" + else: + warning_text_1 = "WARNING - The parameters: " + self.differed_params + "vary from that suggested in the defaults file!\n" + + if self.mandatory_not_set == "": + warning_text_2 = "" + else: + warning_text_2 = "WARNING - The Mandatory values: " + self.mandatory_not_set + "have also not been set and will need to be set for provisioner to run!\n" + + yield Container(Static(warning_text_1), + Static(warning_text_2), + Button("OK", id="close_help_screen2"), + id="dialog2") + + + +class App(App): + CSS_PATH = "config_app.css" + BINDINGS = [("q", "quit", "Quit")] + SCREENS = {"MainScreen": MainScreen()} + + def on_mount(self) -> None: + self.title = "rpi-sb-provisioner config editor" + self.push_screen(MainScreen()) + if (len(different_from_defaults) > 0) or (len(mandatory_not_set) > 0): + self.push_screen(OpeningScreen(different_from_defaults, mandatory_not_set)) + + def action_mainscreen(self): + self.pop_screen() + self.push_screen(MainScreen()) + + @on(Button.Pressed) + def on_button_pressed(self, event: Button.Pressed) -> None: + if "helpbutton" in event.button.id: + paramname = event.button.id.replace("_helpbutton", "") + self.push_screen(HelpScreen(paramname, defaultparams[paramname], "idk", required[paramname], helper[paramname])) + if "close_help_screen" in event.button.id: + self.pop_screen() + if "write_button" in event.button.id: + f = open("/etc/rpi-sb-provisioner/config", "w+") + for param in params_to_save: + if param != "": + f.write(param + "=" + params_to_save[param] + "\n") + f.close() + quit() + + @on(Input.Submitted) + def on_input_submitted(self, event: Input.Submitted) -> None: + if "param_entry" in event.input.id: + paramname = event.input.id.replace("param_entry_", "") + validate = getattr(validator, "validate_" + paramname) + success, errmsg = validate(event.input.value) + if not(success): + inputbox = self.query_one("#param_entry_" + paramname) + inputbox.classes = "paramentry" + nametext = self.query_one("#nameentry_" + paramname) + nametext.update("╳ - " + paramname) + self.push_screen(ValidatedScreen(paramname, errmsg, defaultparams[paramname], "idk", required[paramname], helper[paramname])) + if success: + inputbox = self.query_one("#param_entry_" + paramname) + inputbox.classes = "success_entry" + params_to_save[paramname] = event.input.value + nametext = self.query_one("#nameentry_" + paramname) + nametext.update("✓ - " + paramname) + + + + + +### initially need to open the default config files +defaultparams = {} +initialparams = {} +params_to_save = {} +f = open("/etc/default/rpi-sb-provisioner", "r") +contents_by_line = f.read().split("\n") +for line in contents_by_line: + if len(line.split("=")) > 1: + defaultparams.update([(line.split("=")[0], line.split("=")[1])]) + initialparams.update([(line.split("=")[0], line.split("=")[1])]) + params_to_save.update([(line.split("=")[0], line.split("=")[1])]) + else: + defaultparams.update([(line.split("=")[0], "")]) + params_to_save.update([(line.split("=")[0], "")]) + initialparams.update([(line.split("=")[0], "")]) + +if os.path.exists("/etc/rpi-sb-provisioner/config"): + f = open("/etc/rpi-sb-provisioner/config", "r") + contents_by_line = f.read().split("\n") + for line in contents_by_line: + if len(line.split("=")) > 1: + initialparams.update([(line.split("=")[0], line.split("=")[1])]) + params_to_save[line.split("=")[0]] = line.split("=")[1] + else: + initialparams.update([(line.split("=")[0], "")]) + try: + initialparams.pop("") + defaultparams.pop("") + except: + pass + +### Find the differences! +different_from_defaults = [] +for param in defaultparams: + if defaultparams[param] != "": + if param in initialparams: + if initialparams[param] != defaultparams[param]: + different_from_defaults.append(param) + else: + different_from_defaults.append(param) + +### Load helper descriptor! +helper = {} +required = {} +mandatory_not_set = [] +f = open("config_app.helper") +contents_by_param = f.read().split("\n") +for line in contents_by_param: + if len(line.split("|")) > 1: + helper.update([(line.split("|")[0], line.split("|")[2])]) + required.update([(line.split("|")[0], line.split("|")[1])]) + if "Mandatory" in required[line.split("|")[0]]: + if params_to_save[line.split("|")[0]] == "": + mandatory_not_set.append(line.split("|")[0]) + else: + print("Error - unable to correctly parse helper line: " + line) + +if __name__ == "__main__": + app = App() + app.run() \ No newline at end of file diff --git a/config/config.sh b/config/config.sh new file mode 100755 index 0000000..50fe2be --- /dev/null +++ b/config/config.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd /usr/share/rpi-sb-provisioner/config/ +python3 config.py \ No newline at end of file diff --git a/config/config_app.css b/config/config_app.css new file mode 100755 index 0000000..8bc4423 --- /dev/null +++ b/config/config_app.css @@ -0,0 +1,87 @@ +Screen { + layout: vertical; + color: white; + background: rgb(32, 0, 20); +} + +ParamWidget { + layout: horizontal; + height: 5; + border: solid rgb(255, 0, 106); +} + +OpeningScreen { + align: center middle; + background: rgba(0, 0, 0, 0.60); +} + +.bottom_bar { + layout: horizontal; + height: 5; + align: center middle; +} +.validate_button { + margin-left: 3; + margin-right: 3; +} + +.paramname { + margin-top: 1; + margin-right: 3; + height: 3; + text-align: right; + width: 0.6fr; + /* background: rgb(102, 1, 63); */ +} +.paramentry { + height: 3; + width: 1fr; + color: white; + background: rgb(138, 68, 100); +} +.success_entry { + height: 3; + width: 1fr; + color: white; + background: green; +} +.parambutton { + height: 3; + width: 8; + /* background: rgb(102, 1, 63); */ +} + +HelpScreen { + align: center middle; + background: rgba(0, 0, 0, 0.60); +} +ValidatedScreen { + align: center middle; + background: rgba(0, 0, 0, 0.60); +} + +#dialog { + padding: 0 1; + width: 50%; + height: 50%; + border: thick rgb(138, 68, 100) 80%; + background: rgb(138, 68, 100); +} +#dialog2 { + padding: 0 1; + width: 50%; + height: 50%; + border: thick rgb(138, 68, 100) 80%; + background: rgb(138, 68, 100); +} +.close_help_screen2 { + height: 3; + width: 8; + align: center middle; + /* background: rgb(102, 1, 63); */ +} +.write_button{ + margin-left: 3; + margin-right: 3; + background: rgb(0, 139, 139); +} \ No newline at end of file diff --git a/config/config_app.helper b/config/config_app.helper new file mode 100755 index 0000000..de88d17 --- /dev/null +++ b/config/config_app.helper @@ -0,0 +1,10 @@ +CUSTOMER_KEY_FILE_PEM|Mandatory|The fully qualified path to your signing key, encoded in PEM format. This file is expected to contain an RSA 2048-bit Private Key. +GOLD_MASTER_OS_FILE|Mandatory|This should be your 'gold master' OS image. No customisation should be present in this image that you would not expect to be deployed to your entire fleet. rpi-sb-provisioner assumes this image has been created using pi-gen, and using a non-pi-gen image may produce undefined behaviour +RPI_DEVICE_STORAGE_TYPE|Mandatory|Specify the kind of storage your target will use. Supported values are sd, emmc, nvme. +RPI_DEVICE_FAMILY|Mandatory|Specify the family of Raspberry Pi device you are provisioning. Supported values are 4. For example, A Raspberry Pi Compute Module 4 would be family 4 +RPI_DEVICE_BOOTLOADER_CONFIG_FILE|Mandatory, with a default|Warning: rpi-sb-provisioner will ignore the Raspberry Pi Bootloader configuration built by pi-gen, and use the one provided in this variable. Specify the Raspberry Pi Bootloader configuration you want your provisioned devices to use. A default is provided. Further information on the format of this configuration file can be found in the Raspberry Pi Documentation, at https://www.raspberrypi.com/documentation/computers/config_txt.html +RPI_DEVICE_LOCK_JTAG|Optional|Raspberry Pi devices have a mechanism to restrict JTAG access to the device. Note that using this function will prevent Raspberry Pi engineers from being able to assist in debugging your device, should you request assitance. Set to any value to enable the JTAG restrictions. +RPI_DEVICE_EEPROM_WP_SET|Optional|Raspberry Pi devices that use an EEPROM as part of their boot flow can configure that EEPROM to enable write protection - preventing modification. Set to any value to enable EEPROM write protection. +DEVICE_SERIAL_STORE|Optional, with a default|Specify a location for the seen-devices storage directory. This directory will contain a zero-length file named with the serial number of each device seen, with the created files being used inside the state machine of rpi-sb-provisioner +RPI_SB_WORKDIR|Optional|Warning - If you do not set this variable, your modified OS intermediates will not be stored, and will be unavailable for inspection. Set to a location to cache OS assets between provisioning sessions. Recommended for use in production. For example: /srv/rpi-sb-provisioner/ +DEMO_MODE_ONLY|Optional|Set to 1 to allow the service to run without actually writing keys or OS images. You may, for example, use DEMO_MODE_ONLY in combination with RPI_SB_WORKDIR to inspect the modifications rpi-sb-provisioner would make to your OS ahead of deployment. Warning - Setting DEMO_MODE_ONLY will cause your seen-devices storage location to change to a subdirectory of the one specified by RPI_DEVICE_SERIAL_STORE, demo/ \ No newline at end of file diff --git a/config/validator.py b/config/validator.py new file mode 100644 index 0000000..3d995b3 --- /dev/null +++ b/config/validator.py @@ -0,0 +1,71 @@ +## Format of return will be [Happy: bool, error: str] +from os import path +import subprocess + +def validate_CUSTOMER_KEY_FILE_PEM(text) -> tuple[bool, str]: + if path.exists(text): + pass + else: + return (False, "Could not find file " + text) + output = subprocess.run(["openssl", "rsa", "-in", text, "-check", "-noout"], capture_output=True) + if "RSA key ok" in output.stdout.decode(): + pass + else: + return (False, "openssl error: " + output.stdout.decode() + output.stderr.decode()) + # "openssl rsa -in " + text + " -check -noout" + return (True, "") + + +def validate_GOLD_MASTER_OS_FILE(text) -> tuple[bool, str]: + if path.exists(text): + pass + else: + return (False, "Could not find file " + text) + + return (True, "") + +def validate_RPI_DEVICE_STORAGE_TYPE(text) -> tuple[bool, str]: + if text in "sd nvme emmc": + return (True, "") + else: + return (False, "type `" + text + "` was not any of sd, nvme or emmc") + +def validate_RPI_DEVICE_FAMILY(text) -> tuple[bool, str]: + if text in "45": + return (True, "") + else: + return (False, "type `" + text + "` was not any of 4 or 5") + +def validate_RPI_DEVICE_BOOTLOADER_CONFIG_FILE(text) -> tuple[bool, str]: + if path.exists(text): + pass + else: + return (False, "Could not find file " + text) + + return (True, "") + +def validate_RPI_DEVICE_LOCK_JTAG(text) -> tuple[bool, str]: + return (True, "") + +def validate_RPI_DEVICE_EEPROM_WP_SET(text) -> tuple[bool, str]: + return (True, "") + +def validate_DEVICE_SERIAL_STORE(text) -> tuple[bool, str]: + if text[0] == "/": + pass + else: + return (False, "Please specify absolute path, beginning with /") + + return (True, "") + + +def validate_DEMO_MODE_ONLY(text) -> tuple[bool, str]: + return (True, "") + +def validate_RPI_SB_WORKDIR(text) -> tuple[bool, str]: + if len(text) > 0: + if text[0] == "/": + pass + else: + return (False, "Please specify absolute path, beginning with /") + return (True, "") diff --git a/docs/images/rpi-config-successfully-verified.png b/docs/images/rpi-config-successfully-verified.png new file mode 100644 index 0000000..4383102 Binary files /dev/null and b/docs/images/rpi-config-successfully-verified.png differ diff --git a/docs/images/rpi-config-textfield.png b/docs/images/rpi-config-textfield.png new file mode 100644 index 0000000..87a618a Binary files /dev/null and b/docs/images/rpi-config-textfield.png differ diff --git a/nfpm.yaml b/nfpm.yaml index acf9219..8f05f3c 100755 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -251,6 +251,21 @@ contents: - src: monitor/monitor.css dst: /usr/share/rpi-sb-provisioner/monitor/monitor.css + - src: config/config.sh + dst: /usr/local/bin/config.sh + + - src: config/config.py + dst: /usr/share/rpi-sb-provisioner/config/config.py + + - src: config/validator.py + dst: /usr/share/rpi-sb-provisioner/config/validator.py + + - src: config/config_app.css + dst: /usr/share/rpi-sb-provisioner/config/config_app.css + + - src: config/config_app.helper + dst: /usr/share/rpi-sb-provisioner/config/config_app.helper + # This will add all files in some/directory or in subdirectories at the # same level under the directory /etc. This means the tree structure in # some/directory will not be replicated. @@ -370,11 +385,11 @@ contents: umask: 0o002 # Scripts to run at specific stages. (overridable) -# scripts: -# preinstall: ./scripts/preinstall.sh -# postinstall: ./scripts/postinstall.sh -# preremove: ./scripts/preremove.sh -# postremove: ./scripts/postremove.sh +scripts: + # preinstall: ./scripts/preinstall.sh + postinstall: ./scripts/postinstall.sh + # preremove: ./scripts/preremove.sh + # postremove: ./scripts/postremove.sh # All fields above marked as `overridable` can be overridden for a given # package format in this section. diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh new file mode 100755 index 0000000..046889c --- /dev/null +++ b/scripts/postinstall.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +if [ ! $(getent group rpi-sb-provisioner) ]; then + groupadd rpi-sb-provisioner +else + echo "Group rpi-sb-provisioner already exists" +fi + +if id -nGz "pi" | grep -qzxF "rpi-sb-provisioner" +then + echo User \`pi\' already belongs to group \`rpi-sb-provisioner\' +else + usermod --append --groups rpi-sb-provisioner pi +fi + +if id -nGz "root" | grep -qzxF "rpi-sb-provisioner" +then + echo User \`root\' already belongs to group \`rpi-sb-provisioner\' +else + usermod --append --groups rpi-sb-provisioner root +fi + +if ! [ -f /etc/rpi-sb-provisioner/config ]; then + touch /etc/rpi-sb-provisioner/config +else + echo "Config file already exists" +fi + +chown :rpi-sb-provisioner /etc/rpi-sb-provisioner/config +chmod g+w /etc/rpi-sb-provisioner/config \ No newline at end of file