Compare commits
No commits in common. "main" and "v0.1.18" have entirely different histories.
85
Cargo.lock
generated
85
Cargo.lock
generated
|
|
@ -512,22 +512,6 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation-sys"
|
|
||||||
version = "0.8.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
|
|
@ -1419,16 +1403,6 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "io-kit-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"mach2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_ci"
|
name = "is_ci"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -1556,15 +1530,6 @@ version = "0.4.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mach2"
|
|
||||||
version = "0.4.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
|
|
@ -1640,17 +1605,6 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
|
checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nix"
|
|
||||||
version = "0.26.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
|
|
@ -2179,7 +2133,7 @@ dependencies = [
|
||||||
"if-addrs",
|
"if-addrs",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"nix 0.30.1",
|
"nix",
|
||||||
"num",
|
"num",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
|
@ -2386,24 +2340,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serialport"
|
|
||||||
version = "4.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"cfg-if",
|
|
||||||
"core-foundation",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"io-kit-sys",
|
|
||||||
"mach2",
|
|
||||||
"nix 0.26.4",
|
|
||||||
"scopeguard",
|
|
||||||
"unescaper",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
|
|
@ -2663,7 +2599,6 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
"rs-matter",
|
"rs-matter",
|
||||||
"rs-matter-stack",
|
"rs-matter-stack",
|
||||||
"serialport",
|
|
||||||
"static_cell",
|
"static_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2765,15 +2700,6 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unescaper"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -2967,15 +2893,6 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ log = { version = "0.4", optional = true }
|
||||||
rand = { version = "0.8", features = ["std", "std_rng"] }
|
rand = { version = "0.8", features = ["std", "std_rng"] }
|
||||||
rs-matter = { version = "0.1", default-features = false }
|
rs-matter = { version = "0.1", default-features = false }
|
||||||
rs-matter-stack = { git = "https://git.fishmt.net/nakajima/rs-matter-stack", branch = "master", default-features = false, optional = true }
|
rs-matter-stack = { git = "https://git.fishmt.net/nakajima/rs-matter-stack", branch = "master", default-features = false, optional = true }
|
||||||
serialport = { version = "4.9.0", default-features = false }
|
|
||||||
static_cell = { version = "2.1", optional = true }
|
static_cell = { version = "2.1", optional = true }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --
|
||||||
--device-id device-001
|
--device-id device-001
|
||||||
```
|
```
|
||||||
|
|
||||||
The tool waits for either a shell prompt or `login:` before sending the install script. If the device is already at a shell prompt, omit login flags. If it is at `login:`, provide credentials:
|
If the serial console is already at a shell prompt, omit login flags. If it is at `login:`, provide credentials:
|
||||||
|
|
||||||
```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 -- \
|
||||||
|
|
@ -71,7 +71,7 @@ cargo run --no-default-features --manifest-path ~/apps/toes-matter/Cargo.toml --
|
||||||
--device-id device-001
|
--device-id device-001
|
||||||
```
|
```
|
||||||
|
|
||||||
For passwordless root/login, use `--login root` and omit `--password`. The tool waits for `login:`, optional `Password:`, then a shell prompt marker (`# ` or `$ ` by default) before sending the install script. If it sees `login:` and no login was provided, it exits with an instruction to set `--login` / `SERIAL_LOGIN`. If your prompt is unusual, add `--prompt 'my-prompt>'`.
|
For passwordless root/login, use `--login root` and omit `--password`. The tool waits for `login:`, optional `Password:`, then a shell prompt marker (`# ` or `$ ` by default) before sending the install script. If your prompt is unusual, add `--prompt 'my-prompt>'`.
|
||||||
|
|
||||||
The wrapper script supports serial via environment too:
|
The wrapper script supports serial via environment too:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ set -euo pipefail
|
||||||
# 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]
|
||||||
#
|
#
|
||||||
# The tool waits for either a shell prompt or login:. If it sees login:, set
|
|
||||||
# SERIAL_LOGIN and, if required by the image, SERIAL_PASSWORD.
|
|
||||||
#
|
|
||||||
# Environment:
|
# Environment:
|
||||||
# CREDS_DIR Local output dir. Default: toes-matter/manufacturing/<device-id>/creds
|
# CREDS_DIR Local output dir. Default: toes-matter/manufacturing/<device-id>/creds
|
||||||
# REMOTE_CREDS_DIR Remote creds dir. Default: /var/lib/toes-matter
|
# REMOTE_CREDS_DIR Remote creds dir. Default: /var/lib/toes-matter
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
@ -188,7 +188,7 @@ fn print_usage() {
|
||||||
--service NAME Service to restart [default: {DEFAULT_SERVICE}]\n\
|
--service NAME Service to restart [default: {DEFAULT_SERVICE}]\n\
|
||||||
--no-restart Do not restart the service after provisioning\n\
|
--no-restart Do not restart the service after provisioning\n\
|
||||||
--device-id ID Device id (if not provided positionally)\n\
|
--device-id ID Device id (if not provided positionally)\n\
|
||||||
--serial PORT Install credentials through a serial shell [env: SERIAL_PORT]\n\
|
--serial PORT Write install script to a serial shell [env: SERIAL_PORT]\n\
|
||||||
--baud BAUD Configure serial baud [default: 115200, env: SERIAL_BAUD]\n\
|
--baud BAUD Configure serial baud [default: 115200, env: SERIAL_BAUD]\n\
|
||||||
--serial-delay-ms MS Delay between serial lines [default: 15, env: SERIAL_DELAY_MS]\n\
|
--serial-delay-ms MS Delay between serial lines [default: 15, env: SERIAL_DELAY_MS]\n\
|
||||||
--login USER Log in on serial before provisioning [env: SERIAL_LOGIN]\n\
|
--login USER Log in on serial before provisioning [env: SERIAL_LOGIN]\n\
|
||||||
|
|
@ -351,17 +351,19 @@ fn render_qr(qr_code: &str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
||||||
let commands = build_remote_install_commands(opts)?;
|
configure_serial(port, opts.baud)?;
|
||||||
|
|
||||||
|
let script = build_remote_install_script(opts)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!(
|
println!(
|
||||||
"==> Writing install commands to serial port {port} at {} baud",
|
"==> Writing install script to serial port {port} at {} baud",
|
||||||
opts.baud
|
opts.baud
|
||||||
);
|
);
|
||||||
if opts.serial_login.is_some() {
|
if opts.serial_login.is_some() {
|
||||||
println!(" Will wait for the serial console and log in if needed.");
|
println!(" Will log in on the serial console first.");
|
||||||
} else {
|
} else {
|
||||||
println!(" Will wait for a shell prompt. If the device is at login:, set SERIAL_LOGIN.");
|
println!(" This assumes the serial console is already at a shell prompt.");
|
||||||
}
|
}
|
||||||
if cfg!(target_os = "macos") && port.starts_with("/dev/tty.") {
|
if cfg!(target_os = "macos") && port.starts_with("/dev/tty.") {
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -369,43 +371,81 @@ fn write_to_serial(port: &str, opts: &Options) -> Result<()> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut serial = open_serial(port, opts.baud)?;
|
let mut serial = std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(port)?;
|
||||||
// Many serial consoles expect CRLF for Enter.
|
// Many serial consoles expect CRLF for Enter.
|
||||||
serial.write_all(b"\r\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
||||||
ensure_serial_shell(serial.as_mut(), opts)?;
|
if opts.serial_login.is_some() {
|
||||||
|
login_to_serial(&mut serial, opts)?;
|
||||||
println!("==> Sending serial install commands...");
|
|
||||||
for command in commands {
|
|
||||||
send_serial_command(serial.as_mut(), opts, &command)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for line in script.lines() {
|
||||||
|
serial.write_all(line.as_bytes())?;
|
||||||
|
serial.write_all(b"\r\n")?;
|
||||||
|
serial.flush()?;
|
||||||
|
std::thread::sleep(opts.serial_delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
println!("==> Serial provisioning complete");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_serial(port: &str, baud: u32) -> Result<Box<dyn serialport::SerialPort>> {
|
fn configure_serial(port: &str, baud: u32) -> Result<()> {
|
||||||
let mut serial = serialport::new(port, baud)
|
let port_flag = if cfg!(any(
|
||||||
.data_bits(serialport::DataBits::Eight)
|
target_os = "macos",
|
||||||
.parity(serialport::Parity::None)
|
target_os = "freebsd",
|
||||||
.stop_bits(serialport::StopBits::One)
|
target_os = "openbsd",
|
||||||
.flow_control(serialport::FlowControl::None)
|
target_os = "netbsd"
|
||||||
.timeout(Duration::from_millis(100))
|
)) {
|
||||||
.open()
|
"-f"
|
||||||
.map_err(|err| format!("failed to open serial port {port}: {err}"))?;
|
} else {
|
||||||
|
"-F"
|
||||||
|
};
|
||||||
|
|
||||||
// Some USB serial consoles do not present input until DTR is asserted.
|
let status = Command::new("stty")
|
||||||
let _ = serial.write_data_terminal_ready(true);
|
.arg(port_flag)
|
||||||
let _ = serial.write_request_to_send(true);
|
.arg(port)
|
||||||
let _ = serial.clear(serialport::ClearBuffer::All);
|
.arg(baud.to_string())
|
||||||
|
.arg("raw")
|
||||||
|
.arg("-echo")
|
||||||
|
.arg("-ixon")
|
||||||
|
.arg("-ixoff")
|
||||||
|
.arg("min")
|
||||||
|
.arg("0")
|
||||||
|
.arg("time")
|
||||||
|
.arg("5")
|
||||||
|
.status()?;
|
||||||
|
|
||||||
Ok(serial)
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("stty failed for {port} with status {status}").into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_serial_shell(serial: &mut dyn serialport::SerialPort, opts: &Options) -> Result<()> {
|
fn login_to_serial(serial: &mut std::fs::File, opts: &Options) -> Result<()> {
|
||||||
|
let login = opts
|
||||||
|
.serial_login
|
||||||
|
.as_deref()
|
||||||
|
.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"\r\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
serial.flush()?;
|
serial.flush()?;
|
||||||
|
|
@ -420,15 +460,10 @@ fn ensure_serial_shell(serial: &mut dyn serialport::SerialPort, opts: &Options)
|
||||||
let initial = wait_for_any(serial, &initial_needles, opts.serial_login_timeout)?;
|
let initial = wait_for_any(serial, &initial_needles, opts.serial_login_timeout)?;
|
||||||
|
|
||||||
if initial >= 2 {
|
if initial >= 2 {
|
||||||
println!("==> Serial console is at a shell prompt");
|
println!("==> Serial console already appears to be at a shell prompt");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let login = opts
|
|
||||||
.serial_login
|
|
||||||
.as_deref()
|
|
||||||
.ok_or("serial login prompt detected; pass --login USER or set SERIAL_LOGIN=USER")?;
|
|
||||||
|
|
||||||
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"\r\n")?;
|
serial.write_all(b"\r\n")?;
|
||||||
|
|
@ -467,29 +502,8 @@ fn ensure_serial_shell(serial: &mut dyn serialport::SerialPort, opts: &Options)
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_serial_command(
|
|
||||||
serial: &mut dyn serialport::SerialPort,
|
|
||||||
opts: &Options,
|
|
||||||
command: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
serial.write_all(command.as_bytes())?;
|
|
||||||
serial.write_all(b"\r\n")?;
|
|
||||||
serial.flush()?;
|
|
||||||
std::thread::sleep(opts.serial_delay);
|
|
||||||
|
|
||||||
let prompt_needles = opts
|
|
||||||
.serial_prompts
|
|
||||||
.iter()
|
|
||||||
.map(|prompt| prompt.as_bytes().to_vec())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
wait_for_any(serial, &prompt_needles, Duration::from_secs(60))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wait_for_any(
|
fn wait_for_any(
|
||||||
serial: &mut dyn serialport::SerialPort,
|
serial: &mut std::fs::File,
|
||||||
needles: &[Vec<u8>],
|
needles: &[Vec<u8>],
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
|
|
@ -516,11 +530,7 @@ fn wait_for_any(
|
||||||
return Ok(index);
|
return Ok(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err)
|
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
|
||||||
if matches!(
|
|
||||||
err.kind(),
|
|
||||||
std::io::ErrorKind::Interrupted | std::io::ErrorKind::TimedOut
|
|
||||||
) => {}
|
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -538,35 +548,42 @@ fn find_needle(haystack: &[u8], needles: &[Vec<u8>]) -> Option<usize> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_remote_install_commands(opts: &Options) -> Result<Vec<String>> {
|
fn build_remote_install_script(opts: &Options) -> Result<String> {
|
||||||
let mut commands = Vec::new();
|
let mut script = String::new();
|
||||||
|
|
||||||
// Keep commands single-line so each one can be sent and acknowledged at an
|
// Note: this script is sent line-by-line over a serial console.
|
||||||
// interactive shell prompt. Multi-line here-docs are fragile over serial
|
// Keep it POSIX-ish (sh-compatible) and avoid anything that would block
|
||||||
// consoles because a failed parse turns payload data into shell commands.
|
// waiting for interactive input.
|
||||||
commands.push("set -e".into());
|
|
||||||
commands.push("umask 077".into());
|
script.push_str("set -e\n");
|
||||||
commands.push(format!(
|
script.push_str(&format!(
|
||||||
"REMOTE_CREDS_DIR={}",
|
"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),
|
||||||
commands.push(format!(
|
|
||||||
"REMOTE_STATE_DIR={}",
|
|
||||||
shell_quote(&opts.remote_state_dir)
|
|
||||||
));
|
));
|
||||||
|
|
||||||
// Determine how to escalate.
|
// Determine how to escalate.
|
||||||
// - If already root, no sudo needed.
|
// - If already root, no sudo needed.
|
||||||
// - If sudo works without a password, use it.
|
// - If sudo works without a password, use it.
|
||||||
// - Otherwise, fail early with a clear message.
|
// - Otherwise, fail early with a clear message (so we don't hang at a password prompt).
|
||||||
commands.push(format!(
|
script.push_str(
|
||||||
"if [ \"$(id -u)\" -eq 0 ]; then SUDO=\"\"; elif command -v sudo >/dev/null 2>&1; then if sudo -n true >/dev/null 2>&1; then SUDO=\"sudo\"; else echo {} >&2; exit 1; fi; else echo {} >&2; exit 1; fi",
|
"if [ \"$(id -u)\" -eq 0 ]; then\n\
|
||||||
shell_quote("ERROR: sudo requires a password. Either provision using a root shell, or configure passwordless sudo for this user."),
|
SUDO=\"\"\n\
|
||||||
shell_quote("ERROR: sudo not found and not running as root."),
|
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.
|
// Create directories up-front.
|
||||||
commands.push("$SUDO mkdir -p \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"".into());
|
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
|
// 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
|
// service StateDirectory parent (e.g. /var/lib/toes-matter), and deleting it can race
|
||||||
// with the running service.
|
// with the running service.
|
||||||
|
|
@ -580,39 +597,36 @@ fn build_remote_install_commands(opts: &Options) -> Result<Vec<String>> {
|
||||||
] {
|
] {
|
||||||
let path = opts.creds_dir.join(file);
|
let path = opts.creds_dir.join(file);
|
||||||
let data = std::fs::read(&path)?;
|
let data = std::fs::read(&path)?;
|
||||||
|
let marker = format!(
|
||||||
|
"TOES_MATTER_{}",
|
||||||
|
file.replace(['.', '-'], "_").to_uppercase()
|
||||||
|
);
|
||||||
|
|
||||||
commands.push(format!(
|
script.push_str(&format!(
|
||||||
"tmp_b64=$(mktemp \"${{TMPDIR:-/tmp}}/toes-matter-{file}.b64.XXXXXX\")"
|
"base64 -d <<'{marker}' | $SUDO tee \"$REMOTE_CREDS_DIR/{file}\" >/dev/null\n"
|
||||||
));
|
));
|
||||||
commands.push(format!(
|
script.push_str(&base64_wrapped(&data));
|
||||||
"tmp_out=$(mktemp \"${{TMPDIR:-/tmp}}/toes-matter-{file}.decoded.XXXXXX\")"
|
script.push_str(&format!("\n{marker}\n"));
|
||||||
));
|
|
||||||
|
|
||||||
for chunk in base64_wrapped(&data).lines() {
|
|
||||||
commands.push(format!("printf %s {} >> \"$tmp_b64\"", shell_quote(chunk)));
|
|
||||||
}
|
|
||||||
|
|
||||||
commands.push("base64 -d < \"$tmp_b64\" > \"$tmp_out\"".into());
|
|
||||||
commands.push(format!(
|
|
||||||
"$SUDO tee \"$REMOTE_CREDS_DIR/{file}\" < \"$tmp_out\" >/dev/null"
|
|
||||||
));
|
|
||||||
commands.push("rm -f \"$tmp_b64\" \"$tmp_out\"".into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave creds/state readable only by root by default.
|
// Leave creds/state readable only by root by default.
|
||||||
commands.push("$SUDO chmod -R go-rwx \"$REMOTE_CREDS_DIR\" \"$REMOTE_STATE_DIR\"".into());
|
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() {
|
||||||
commands.push(format!(
|
script.push_str(&format!(
|
||||||
"if command -v systemctl >/dev/null 2>&1 && (systemctl list-unit-files {service} >/dev/null 2>&1 || systemctl status {service} >/dev/null 2>&1); then $SUDO systemctl restart {service}; else echo {message}; fi",
|
"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\
|
||||||
|
else\n\
|
||||||
|
echo 'Service {service_display} not found; skipping restart'\n\
|
||||||
|
fi\n",
|
||||||
service = shell_quote(service),
|
service = shell_quote(service),
|
||||||
message = shell_quote(&format!("Service {service} not found; skipping restart")),
|
service_display = service,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.push("echo 'toes-matter serial provisioning complete'".into());
|
script.push_str("echo 'toes-matter serial provisioning complete'\n");
|
||||||
|
|
||||||
Ok(commands)
|
Ok(script)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_wrapped(data: &[u8]) -> String {
|
fn base64_wrapped(data: &[u8]) -> String {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user