Simplify credentials provisioning to serial + match toes-image-builder paths
This commit is contained in:
parent
da0e01d437
commit
db793d49d9
18
README.md
18
README.md
|
|
@ -39,21 +39,16 @@ fn main() -> toes_matter::Result<()> {
|
||||||
|
|
||||||
## Post-flash headless provisioning
|
## Post-flash headless provisioning
|
||||||
|
|
||||||
After flashing a Linux device reachable over SSH, run the host-side credentials command from Linux or macOS:
|
After flashing a Linux device with a serial console, run the host-side credentials command from Linux or macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
||||||
user@device-host device-001
|
--serial /dev/cu.usbserial-0001 \
|
||||||
|
--device-id device-001
|
||||||
```
|
```
|
||||||
|
|
||||||
It generates `manufacturing/device-001/creds`, prints the manual code / QR payload, renders a terminal QR if `qrencode` is installed, copies the creds to `/var/lib/toes-matter`, creates `/var/lib/toes-matter/state`, and restarts `toes-matter.service` if present.
|
It generates `manufacturing/device-001/creds`, prints the manual code / QR payload, renders a terminal QR if `qrencode` is installed, copies the creds to `/var/lib/toes-matter`, creates `/var/lib/toes-matter/state`, and restarts `toes-matter.service` if present.
|
||||||
|
|
||||||
The older script is now just a wrapper around that command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
~/apps/toes-matter/scripts/provision-device.sh user@device-host device-001
|
|
||||||
```
|
|
||||||
|
|
||||||
Set these if your service uses different paths:
|
Set these if your service uses different paths:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -61,13 +56,6 @@ cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --
|
||||||
--remote-creds-dir /var/lib/my-app \
|
--remote-creds-dir /var/lib/my-app \
|
||||||
--remote-state-dir /var/lib/my-app/state \
|
--remote-state-dir /var/lib/my-app/state \
|
||||||
--service my-app.service \
|
--service my-app.service \
|
||||||
user@device-host device-001
|
|
||||||
```
|
|
||||||
|
|
||||||
Serial instead of network/SSH:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --bin toes-matter-credentials -- \
|
|
||||||
--serial /dev/cu.usbserial-0001 \
|
--serial /dev/cu.usbserial-0001 \
|
||||||
--device-id device-001
|
--device-id device-001
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ set -euo pipefail
|
||||||
# Compatibility wrapper for the Rust host provisioning command.
|
# Compatibility wrapper for the Rust host provisioning command.
|
||||||
# Works from Linux/macOS as long as cargo is available.
|
# Works from Linux/macOS as long as cargo is available.
|
||||||
#
|
#
|
||||||
# SSH usage:
|
|
||||||
# toes-matter/scripts/provision-device.sh user@device-host [device-id]
|
|
||||||
#
|
|
||||||
# Serial usage:
|
# Serial usage:
|
||||||
# SERIAL_PORT=/dev/cu.usbserial-0001 toes-matter/scripts/provision-device.sh [device-id]
|
# SERIAL_PORT=/dev/cu.usbserial-0001 toes-matter/scripts/provision-device.sh [device-id]
|
||||||
# SERIAL_PORT=/dev/cu.usbserial-0001 SERIAL_LOGIN=root SERIAL_PASSWORD=... toes-matter/scripts/provision-device.sh [device-id]
|
# SERIAL_PORT=/dev/cu.usbserial-0001 SERIAL_LOGIN=root SERIAL_PASSWORD=... toes-matter/scripts/provision-device.sh [device-id]
|
||||||
|
|
@ -17,7 +14,6 @@ set -euo pipefail
|
||||||
# REMOTE_STATE_DIR Remote Matter state dir. Default: /var/lib/toes-matter/state
|
# REMOTE_STATE_DIR Remote Matter state dir. Default: /var/lib/toes-matter/state
|
||||||
# SERVICE Optional service to restart. Default: toes-matter.service
|
# SERVICE Optional service to restart. Default: toes-matter.service
|
||||||
# Set SERVICE= to skip restart.
|
# Set SERVICE= to skip restart.
|
||||||
# SSH_OPTS Extra ssh/scp options, e.g. '-p 2222'
|
|
||||||
# SERIAL_PORT Serial device, e.g. /dev/cu.usbserial-0001 or /dev/tty.usbserial-0001
|
# SERIAL_PORT Serial device, e.g. /dev/cu.usbserial-0001 or /dev/tty.usbserial-0001
|
||||||
# SERIAL_LOGIN Optional serial login username
|
# SERIAL_LOGIN Optional serial login username
|
||||||
# SERIAL_PASSWORD Optional serial login password
|
# SERIAL_PASSWORD Optional serial login password
|
||||||
|
|
@ -25,26 +21,19 @@ set -euo pipefail
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
CRATE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
if [[ -n "${SERIAL_PORT:-}" ]]; then
|
if [[ -z "${SERIAL_PORT:-}" ]]; then
|
||||||
if [[ $# -gt 1 ]]; then
|
echo "Error: set SERIAL_PORT=/dev/cu.* (or /dev/tty.*)" >&2
|
||||||
echo "Usage with SERIAL_PORT: SERIAL_PORT=/dev/cu.* $0 [device-id]" >&2
|
exit 2
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEVICE_ID="${1:-$(date +%Y%m%d-%H%M%S)}"
|
|
||||||
TARGET_ARGS=(--serial "$SERIAL_PORT" --device-id "$DEVICE_ID")
|
|
||||||
else
|
|
||||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
|
||||||
echo "Usage: $0 user@device-host [device-id]" >&2
|
|
||||||
echo " or: SERIAL_PORT=/dev/cu.* $0 [device-id]" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET="$1"
|
|
||||||
DEVICE_ID="${2:-$(date +%Y%m%d-%H%M%S)}"
|
|
||||||
TARGET_ARGS=("$TARGET" "$DEVICE_ID")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $# -gt 1 ]]; then
|
||||||
|
echo "Usage: SERIAL_PORT=/dev/cu.* $0 [device-id]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEVICE_ID="${1:-$(date +%Y%m%d-%H%M%S)}"
|
||||||
|
TARGET_ARGS=(--serial "$SERIAL_PORT" --device-id "$DEVICE_ID")
|
||||||
|
|
||||||
CREDS_DIR="${CREDS_DIR:-$CRATE_DIR/manufacturing/$DEVICE_ID/creds}"
|
CREDS_DIR="${CREDS_DIR:-$CRATE_DIR/manufacturing/$DEVICE_ID/creds}"
|
||||||
REMOTE_CREDS_DIR="${REMOTE_CREDS_DIR:-/var/lib/toes-matter}"
|
REMOTE_CREDS_DIR="${REMOTE_CREDS_DIR:-/var/lib/toes-matter}"
|
||||||
REMOTE_STATE_DIR="${REMOTE_STATE_DIR:-/var/lib/toes-matter/state}"
|
REMOTE_STATE_DIR="${REMOTE_STATE_DIR:-/var/lib/toes-matter/state}"
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,6 @@ fn default_device_id() -> String {
|
||||||
format!("device-{secs}")
|
format!("device-{secs}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct GeneratedCredentials {
|
struct GeneratedCredentials {
|
||||||
manual_code: String,
|
manual_code: String,
|
||||||
qr_code: String,
|
qr_code: String,
|
||||||
|
|
@ -371,7 +370,8 @@ fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(port)?;
|
.open(port)?;
|
||||||
serial.write_all(b"\n")?;
|
// Many serial consoles expect CRLF for Enter.
|
||||||
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
||||||
if opts.serial_login.is_some() {
|
if opts.serial_login.is_some() {
|
||||||
|
|
@ -380,12 +380,24 @@ fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
||||||
|
|
||||||
for line in script.lines() {
|
for line in script.lines() {
|
||||||
serial.write_all(line.as_bytes())?;
|
serial.write_all(line.as_bytes())?;
|
||||||
serial.write_all(b"\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
std::thread::sleep(opts.serial_delay);
|
std::thread::sleep(opts.serial_delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("==> Serial install script sent");
|
println!("==> Serial install script sent; waiting for completion...");
|
||||||
|
|
||||||
|
// Wait for the install script to print its completion marker so failures
|
||||||
|
// (e.g. sudo password prompts) are visible to the operator.
|
||||||
|
let done = b"toes-matter serial provisioning complete".to_vec();
|
||||||
|
wait_for_any(
|
||||||
|
&mut serial,
|
||||||
|
&[done],
|
||||||
|
// The payload is small; give it some time on slower devices.
|
||||||
|
Duration::from_secs(60),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
println!("==> Serial provisioning complete");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +442,7 @@ fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
||||||
.ok_or("serial login requested without a username")?;
|
.ok_or("serial login requested without a username")?;
|
||||||
|
|
||||||
println!("==> Waiting for serial login prompt or shell prompt...");
|
println!("==> Waiting for serial login prompt or shell prompt...");
|
||||||
serial.write_all(b"\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
||||||
let mut initial_needles = vec![b"login:".to_vec(), b"Login:".to_vec()];
|
let mut initial_needles = vec![b"login:".to_vec(), b"Login:".to_vec()];
|
||||||
|
|
@ -449,7 +461,7 @@ fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
||||||
|
|
||||||
println!("==> Sending serial login username: {login}");
|
println!("==> Sending serial login username: {login}");
|
||||||
serial.write_all(login.as_bytes())?;
|
serial.write_all(login.as_bytes())?;
|
||||||
serial.write_all(b"\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
||||||
let mut after_user_needles = vec![b"Password:".to_vec(), b"password:".to_vec()];
|
let mut after_user_needles = vec![b"Password:".to_vec(), b"password:".to_vec()];
|
||||||
|
|
@ -469,7 +481,7 @@ fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
||||||
|
|
||||||
println!("==> Sending serial login password");
|
println!("==> Sending serial login password");
|
||||||
serial.write_all(password.as_bytes())?;
|
serial.write_all(password.as_bytes())?;
|
||||||
serial.write_all(b"\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
||||||
let prompt_needles = opts
|
let prompt_needles = opts
|
||||||
|
|
@ -534,14 +546,42 @@ fn find_needle(haystack: &[u8], needles: &[Vec<u8>]) -> Option<usize> {
|
||||||
fn build_remote_install_script(opts: &Options) -> Result<String> {
|
fn build_remote_install_script(opts: &Options) -> Result<String> {
|
||||||
let mut script = String::new();
|
let mut script = String::new();
|
||||||
|
|
||||||
|
// Note: this script is sent line-by-line over a serial console.
|
||||||
|
// Keep it POSIX-ish (sh-compatible) and avoid anything that would block
|
||||||
|
// waiting for interactive input.
|
||||||
|
|
||||||
script.push_str("set -e\n");
|
script.push_str("set -e\n");
|
||||||
script.push_str(&format!(
|
script.push_str(&format!(
|
||||||
"REMOTE_CREDS_DIR={}\nREMOTE_STATE_DIR={}\n",
|
"REMOTE_CREDS_DIR={}\nREMOTE_STATE_DIR={}\n",
|
||||||
shell_quote(&opts.remote_creds_dir),
|
shell_quote(&opts.remote_creds_dir),
|
||||||
shell_quote(&opts.remote_state_dir),
|
shell_quote(&opts.remote_state_dir),
|
||||||
));
|
));
|
||||||
script.push_str("sudo rm -rf \"$REMOTE_CREDS_DIR\"\n");
|
|
||||||
script.push_str("sudo mkdir -p \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
// Determine how to escalate.
|
||||||
|
// - If already root, no sudo needed.
|
||||||
|
// - If sudo works without a password, use it.
|
||||||
|
// - Otherwise, fail early with a clear message (so we don't hang at a password prompt).
|
||||||
|
script.push_str(
|
||||||
|
"if [ \"$(id -u)\" -eq 0 ]; then\n\
|
||||||
|
SUDO=\"\"\n\
|
||||||
|
elif command -v sudo >/dev/null 2>&1; then\n\
|
||||||
|
if sudo -n true >/dev/null 2>&1; then\n\
|
||||||
|
SUDO=\"sudo\"\n\
|
||||||
|
else\n\
|
||||||
|
echo 'ERROR: sudo requires a password. Either provision using a root shell, or configure passwordless sudo for this user.' >&2\n\
|
||||||
|
exit 1\n\
|
||||||
|
fi\n\
|
||||||
|
else\n\
|
||||||
|
echo 'ERROR: sudo not found and not running as root.' >&2\n\
|
||||||
|
exit 1\n\
|
||||||
|
fi\n\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create directories up-front.
|
||||||
|
script.push_str("$SUDO mkdir -p \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
||||||
|
// Don't `rm -rf` the whole creds dir; on our device image the creds dir is also the
|
||||||
|
// service StateDirectory parent (e.g. /var/lib/toes-matter), and deleting it can race
|
||||||
|
// with the running service.
|
||||||
|
|
||||||
for file in [
|
for file in [
|
||||||
"dac.der",
|
"dac.der",
|
||||||
|
|
@ -558,18 +598,19 @@ fn build_remote_install_script(opts: &Options) -> Result<String> {
|
||||||
);
|
);
|
||||||
|
|
||||||
script.push_str(&format!(
|
script.push_str(&format!(
|
||||||
"base64 -d <<'{marker}' | sudo tee \"$REMOTE_CREDS_DIR/{file}\" >/dev/null\n"
|
"base64 -d <<'{marker}' | $SUDO tee \"$REMOTE_CREDS_DIR/{file}\" >/dev/null\n"
|
||||||
));
|
));
|
||||||
script.push_str(&base64_wrapped(&data));
|
script.push_str(&base64_wrapped(&data));
|
||||||
script.push_str(&format!("\n{marker}\n"));
|
script.push_str(&format!("\n{marker}\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
script.push_str("sudo chmod -R go-rwx \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
// Leave creds/state readable only by root by default.
|
||||||
|
script.push_str("$SUDO chmod -R go-rwx \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"\n");
|
||||||
|
|
||||||
if let Some(service) = opts.service.as_deref() {
|
if let Some(service) = opts.service.as_deref() {
|
||||||
script.push_str(&format!(
|
script.push_str(&format!(
|
||||||
"if command -v systemctl >/dev/null 2>&1 && \\\n (systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1); then\n\
|
"if command -v systemctl >/dev/null 2>&1 && \\\n (systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1); then\n\
|
||||||
sudo systemctl restart {service}\n\
|
$SUDO systemctl restart {service}\n\
|
||||||
else\n\
|
else\n\
|
||||||
echo 'Service {service_display} not found; skipping restart'\n\
|
echo 'Service {service_display} not found; skipping restart'\n\
|
||||||
fi\n",
|
fi\n",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user