Merge steward orders 1 (r3): an order is one systemd job; refusal reads the named unit only
Soul cut-steward-orders-1.s5 holds at 77e2295. Not installed: steward-orders-2 installs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
64d63cb417
9 changed files with 4289 additions and 17 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -482,6 +482,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
ed25519-dalek = "2.2"
|
ed25519-dalek = "2.2"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
58
README.md
58
README.md
|
|
@ -58,6 +58,64 @@ idunn status --command <id>
|
||||||
the bindings, so `idunn up profile:full-gamecult` brings up everything bound
|
the bindings, so `idunn up profile:full-gamecult` brings up everything bound
|
||||||
into that profile.
|
into that profile.
|
||||||
|
|
||||||
|
### Steward orders
|
||||||
|
|
||||||
|
A second root unit from the same binary, `idunn-orders` (`idunn orders serve`),
|
||||||
|
takes typed orders from one steward Mind over `/run/idunn-orders/steward.sock`.
|
||||||
|
It has its own store, `orders.cc`, and its own binding, `steward.toml`; the
|
||||||
|
survival daemon takes no order. There is still no command string:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idunn order package-install <pkg>[=version]... --reason TEXT # apt-get update, install named packages, check them
|
||||||
|
idunn order unit <start|stop|restart|reload> <unit> --reason TEXT
|
||||||
|
idunn order deploy <service|profile:name> --reason TEXT # the same commit as `idunn up`
|
||||||
|
idunn order show <id> # status and output tails
|
||||||
|
idunn order list
|
||||||
|
```
|
||||||
|
|
||||||
|
A unit order submits exactly one job, for the unit it names: every verb runs
|
||||||
|
`systemctl <verb> --job-mode=ignore-requirements -- <unit>`, so systemd builds a
|
||||||
|
transaction of one job. It does not start what the unit requires or restart what
|
||||||
|
is part of it: order the dependency first, and order `postgresql@16-main.service`
|
||||||
|
rather than the `postgresql.service` wrapper. Targets are not orderable. The job is one; what the unit does afterwards is
|
||||||
|
not: systemd still runs full transactions of its own for `Restart=` after a
|
||||||
|
failure, for a `StopWhenUnneeded=` dependency left without a user, and for a
|
||||||
|
`Conflicts=` partner of a unit that fails, so an ordered stop of a unit
|
||||||
|
that something restarts does not last, and a start of a `Restart=always`
|
||||||
|
unit can report failure and be active seconds later. A unit
|
||||||
|
order may act on any unit except Idunn's own (`idunn-*`) and the minds' own
|
||||||
|
(`mind-self@*`, `mind-persona@*`), and the units that end or take the host
|
||||||
|
(`systemd-suspend`, `systemd-hibernate`, `systemd-hybrid-sleep`,
|
||||||
|
`systemd-suspend-then-hibernate`, `rescue`, `emergency`). That set is compiled
|
||||||
|
into the binary and read from no configuration. An order is refused, recorded
|
||||||
|
and posted when the ordered unit, or an alias it resolves from, is one of them;
|
||||||
|
when it has an emergency action (`SuccessAction=`, `FailureAction=`,
|
||||||
|
`StartLimitAction=`, `JobTimeoutAction=`) other than `none` or an `OnFailure=`
|
||||||
|
or `OnSuccess=` unit (not for a reload); or, for a stop or restart, when another
|
||||||
|
unit is `BoundBy=` it, because systemd stops that unit with it whatever the job
|
||||||
|
mode; for a start or restart, when it has an `Upholds=` unit, which systemd
|
||||||
|
starts in a full transaction whatever the job mode; or, for a stop, restart or
|
||||||
|
reload, when it has a `ConsistsOf=` member, a wrapper such as
|
||||||
|
`postgresql.service` whose verbs would exit 0 and touch nothing. A unit
|
||||||
|
`systemctl show` cannot resolve fails the order. Slices, scopes,
|
||||||
|
targets and globs are not units an order can name.
|
||||||
|
|
||||||
|
A package order installs the packages it names and nothing else: apt runs with
|
||||||
|
`--no-remove`, each name is passed as an exact-name pattern so `.` and `+`
|
||||||
|
match themselves, and a last step fails the order unless every named package
|
||||||
|
is installed.
|
||||||
|
|
||||||
|
An order runs once, after the steward's uid is checked and the instance's
|
||||||
|
brake check (`mind-control --instance <principal> check`) exits 0. The check
|
||||||
|
that the order's principal and uid are the bound steward's runs again at every
|
||||||
|
execution, so an order accepted before `steward.toml` changed is refused and
|
||||||
|
recorded, not run. The socket belongs to the steward user alone (mode 0600);
|
||||||
|
root may `show` and `list` orders but never issue one. An order that had
|
||||||
|
started when the service died is recorded as lost and never run again. No order releases a brake. Acceptance and result are posted to
|
||||||
|
`#operator` as a retried projection of `orders.cc`: a chat outage delays the
|
||||||
|
posts and not the orders. A refusal names the field and a code, never the value
|
||||||
|
it refused.
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
||||||
**Owner.** Deployment admission, and the continuity of installed bodies.
|
**Owner.** Deployment admission, and the continuity of installed bodies.
|
||||||
|
|
|
||||||
47
deploy/idunn-orders.service
Normal file
47
deploy/idunn-orders.service
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Idunn steward orders: typed package, unit and deploy orders from the steward
|
||||||
|
# Deliberately no After= or Requires= on any managed target, Synapse or
|
||||||
|
# idunn-yggdrasil: orders are how an operator repairs those, so none of them
|
||||||
|
# may be a condition of this unit starting. Posts are a retried projection and
|
||||||
|
# wait for Synapse on their own.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
# Root, from the same binary as idunn-yggdrasil, with its own store and socket.
|
||||||
|
# idunn-yggdrasil keeps its no-imperative-input invariant and its write set:
|
||||||
|
# no order code runs in `idunn serve`.
|
||||||
|
#
|
||||||
|
# The unit name matches idunn-*, which is in the always-refused set, so no
|
||||||
|
# order can stop or restart this service.
|
||||||
|
User=root
|
||||||
|
# No Group= and no UMask=: the service gives its socket to the steward user
|
||||||
|
# (mode 0600) itself and sets Idunn's own umask for the files it writes, so
|
||||||
|
# nothing here reaches control.cc. The steward user is the only peer the
|
||||||
|
# service answers: it checks SO_PEERCRED on every connection.
|
||||||
|
ExecStart=/usr/local/bin/idunn orders serve \
|
||||||
|
--store /var/lib/gamecult/idunn-orders \
|
||||||
|
--state-store /var/lib/gamecult/idunn/control.cc \
|
||||||
|
--steward-binding /etc/gamecult/idunn/steward.toml \
|
||||||
|
--socket /run/idunn-orders/steward.sock
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStopSec=20
|
||||||
|
|
||||||
|
RuntimeDirectory=idunn-orders
|
||||||
|
RuntimeDirectoryMode=0755
|
||||||
|
|
||||||
|
# The Synapse access token of @idunn, copied from a root-only file, and read
|
||||||
|
# through $CREDENTIALS_DIRECTORY; the service names it in steward.toml.
|
||||||
|
LoadCredential=idunn-matrix-token:/etc/gamecult/idunn/matrix-token
|
||||||
|
|
||||||
|
PrivateTmp=yes
|
||||||
|
ProtectHome=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
# Its own order store, and the control store a deploy order commits a command
|
||||||
|
# to. Transient units started with systemd-run run in the manager's namespace,
|
||||||
|
# so packages and units are not limited by this list.
|
||||||
|
ReadWritePaths=/var/lib/gamecult/idunn-orders /var/lib/gamecult/idunn
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
@ -2934,6 +2934,10 @@ enum Command {
|
||||||
recipe: PathBuf,
|
recipe: PathBuf,
|
||||||
binding: Option<PathBuf>,
|
binding: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
|
#[cfg(unix)]
|
||||||
|
OrdersServe(crate::steward_orders::ServeOptions),
|
||||||
|
#[cfg(unix)]
|
||||||
|
Order(crate::steward_orders::OrderCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: impl Iterator<Item = String>) -> Result<()> {
|
pub fn run(args: impl Iterator<Item = String>) -> Result<()> {
|
||||||
|
|
@ -2973,6 +2977,10 @@ pub fn run(args: impl Iterator<Item = String>) -> Result<()> {
|
||||||
requested_by,
|
requested_by,
|
||||||
} => expire(&state_store, &command_id, &requested_by),
|
} => expire(&state_store, &command_id, &requested_by),
|
||||||
Command::Validate { recipe, binding } => validate(&recipe, binding.as_deref()),
|
Command::Validate { recipe, binding } => validate(&recipe, binding.as_deref()),
|
||||||
|
#[cfg(unix)]
|
||||||
|
Command::OrdersServe(options) => crate::steward_orders::serve(options),
|
||||||
|
#[cfg(unix)]
|
||||||
|
Command::Order(order) => crate::steward_orders::run_order(order),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3025,6 +3033,10 @@ fn parse(args: impl Iterator<Item = String>) -> Result<Command> {
|
||||||
"cancel" => parse_cancel(args),
|
"cancel" => parse_cancel(args),
|
||||||
"expire" => parse_expire(args),
|
"expire" => parse_expire(args),
|
||||||
"validate" => parse_validate(args),
|
"validate" => parse_validate(args),
|
||||||
|
#[cfg(unix)]
|
||||||
|
"orders" => crate::steward_orders::parse_serve(args).map(Command::OrdersServe),
|
||||||
|
#[cfg(unix)]
|
||||||
|
"order" => crate::steward_orders::parse_order(args).map(Command::Order),
|
||||||
"--help" | "-h" | "help" => bail!(usage()),
|
"--help" | "-h" | "help" => bail!(usage()),
|
||||||
_ => bail!("unknown Idunn command {command:?}\n\n{}", usage()),
|
_ => bail!("unknown Idunn command {command:?}\n\n{}", usage()),
|
||||||
}
|
}
|
||||||
|
|
@ -4730,20 +4742,21 @@ fn resolve_selector(
|
||||||
Ok(targets)
|
Ok(targets)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit(
|
/// The one commit primitive for a deployment command: `idunn up` and a steward
|
||||||
|
/// deploy order both write theirs here. Returns the commit time.
|
||||||
|
pub(crate) fn commit_deployment_command(
|
||||||
store_path: &Path,
|
store_path: &Path,
|
||||||
|
command_id: &str,
|
||||||
selector: &str,
|
selector: &str,
|
||||||
requested_by: &str,
|
requested_by: &str,
|
||||||
wait: bool,
|
) -> Result<u64> {
|
||||||
timeout_seconds: u64,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Some(parent) = store_path.parent() {
|
if let Some(parent) = store_path.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
let now = now_millis()?;
|
let now = now_millis()?;
|
||||||
let command = DeploymentCommand {
|
let command = DeploymentCommand {
|
||||||
schema_version: DEPLOYMENT_COMMAND_SCHEMA.into(),
|
schema_version: DEPLOYMENT_COMMAND_SCHEMA.into(),
|
||||||
command_id: format!("up-{}", Uuid::new_v4()),
|
command_id: command_id.into(),
|
||||||
kind: CommandKind::Deploy,
|
kind: CommandKind::Deploy,
|
||||||
selector: selector.into(),
|
selector: selector.into(),
|
||||||
requested_by: requested_by.into(),
|
requested_by: requested_by.into(),
|
||||||
|
|
@ -4761,7 +4774,33 @@ fn submit(
|
||||||
)?,
|
)?,
|
||||||
"deployment command id collided"
|
"deployment command id collided"
|
||||||
);
|
);
|
||||||
println!("{}", command.command_id);
|
Ok(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A command's present state as `idunn status --command` derives it, for the
|
||||||
|
/// steward order poster: `queued` and `running` are not terminal.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) fn command_state(
|
||||||
|
state_store: &Path,
|
||||||
|
command_id: &str,
|
||||||
|
) -> Result<(&'static str, String)> {
|
||||||
|
let snapshot = ControlSnapshot::read(state_store)?;
|
||||||
|
let archived = read_history_transactions(state_store);
|
||||||
|
let mut transactions = snapshot.transaction_for_command(command_id);
|
||||||
|
transactions.extend(archived.iter().filter(|value| value.command_id == command_id));
|
||||||
|
Ok(derived_command_status(&transactions))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(
|
||||||
|
store_path: &Path,
|
||||||
|
selector: &str,
|
||||||
|
requested_by: &str,
|
||||||
|
wait: bool,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
) -> Result<()> {
|
||||||
|
let command_id = format!("up-{}", Uuid::new_v4());
|
||||||
|
let now = commit_deployment_command(store_path, &command_id, selector, requested_by)?;
|
||||||
|
println!("{command_id}");
|
||||||
if !wait {
|
if !wait {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
@ -4772,10 +4811,10 @@ fn submit(
|
||||||
snapshot
|
snapshot
|
||||||
.commands
|
.commands
|
||||||
.iter()
|
.iter()
|
||||||
.any(|stored| stored.value.command_id == command.command_id),
|
.any(|stored| stored.value.command_id == command_id),
|
||||||
"submitted deployment command disappeared"
|
"submitted deployment command disappeared"
|
||||||
);
|
);
|
||||||
let transactions = snapshot.transaction_for_command(&command.command_id);
|
let transactions = snapshot.transaction_for_command(&command_id);
|
||||||
if !transactions.is_empty() && transactions.iter().all(|value| value.is_terminal()) {
|
if !transactions.is_empty() && transactions.iter().all(|value| value.is_terminal()) {
|
||||||
if let Some(error) =
|
if let Some(error) =
|
||||||
transactions
|
transactions
|
||||||
|
|
@ -4800,7 +4839,7 @@ fn submit(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(
|
pub(crate) fn status(
|
||||||
out: &mut impl Write,
|
out: &mut impl Write,
|
||||||
store_path: &Path,
|
store_path: &Path,
|
||||||
incident_store: &Path,
|
incident_store: &Path,
|
||||||
|
|
@ -10592,7 +10631,13 @@ fn required<'a, T>(value: &'a Option<T>, label: &str) -> Result<&'a T> {
|
||||||
value.as_ref().with_context(|| format!("{label} is absent"))
|
value.as_ref().with_context(|| format!("{label} is absent"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn now_millis() -> Result<u64> {
|
/// The control store `idunn up` and `idunn serve` use unless told otherwise.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) fn default_state_store() -> PathBuf {
|
||||||
|
RuntimeOptions::default().state_store
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn now_millis() -> Result<u64> {
|
||||||
Ok(SystemTime::now()
|
Ok(SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.context("system clock predates Unix epoch")?
|
.context("system clock predates Unix epoch")?
|
||||||
|
|
@ -10612,12 +10657,12 @@ fn sibling_path(path: &Path, suffix: &str) -> PathBuf {
|
||||||
PathBuf::from(value)
|
PathBuf::from(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn string_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
pub(crate) fn string_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||||
args.next()
|
args.next()
|
||||||
.ok_or_else(|| anyhow!("{name} requires a value"))
|
.ok_or_else(|| anyhow!("{name} requires a value"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
|
pub(crate) fn path_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<PathBuf> {
|
||||||
Ok(PathBuf::from(string_value(args, name)?))
|
Ok(PathBuf::from(string_value(args, name)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10633,7 +10678,7 @@ fn u64_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<u64>
|
||||||
.with_context(|| format!("{name} requires a u64"))
|
.with_context(|| format!("{name} requires a u64"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_selector(value: &str) -> Result<()> {
|
pub(crate) fn require_selector(value: &str) -> Result<()> {
|
||||||
let value = value.strip_prefix("profile:").unwrap_or(value);
|
let value = value.strip_prefix("profile:").unwrap_or(value);
|
||||||
require_id(value, "deployment selector")
|
require_id(value, "deployment selector")
|
||||||
}
|
}
|
||||||
|
|
@ -10757,7 +10802,10 @@ fn usage() -> &'static str {
|
||||||
idunn status [--state-store PATH] [--incident-store PATH] [--command ID]\n\
|
idunn status [--state-store PATH] [--incident-store PATH] [--command ID]\n\
|
||||||
idunn cancel <command-id> [--state-store PATH]\n\
|
idunn cancel <command-id> [--state-store PATH]\n\
|
||||||
idunn expire <command-id> [--state-store PATH]\n\
|
idunn expire <command-id> [--state-store PATH]\n\
|
||||||
idunn validate --recipe PATH [--binding PATH]\n\n\
|
idunn validate --recipe PATH [--binding PATH]\n\
|
||||||
|
idunn orders serve [--store DIR] [--steward-binding PATH] [--socket PATH]\n\
|
||||||
|
idunn order <package-install PKG... | unit VERB UNIT | deploy SELECTOR> --reason TEXT\n\
|
||||||
|
idunn order show ID | idunn order list\n\n\
|
||||||
Recipes describe capability and process requirements. Idunn seals exact\n\
|
Recipes describe capability and process requirements. Idunn seals exact\n\
|
||||||
source and artifacts, admits one incarnation, and delegates execution to\n\
|
source and artifacts, admits one incarnation, and delegates execution to\n\
|
||||||
systemd and routing mechanics to the configured proxy."
|
systemd and routing mechanics to the configured proxy."
|
||||||
|
|
@ -11969,9 +12017,45 @@ mod tests {
|
||||||
vec!["serve", "--swarm-profile", "yggdrasil-local"],
|
vec!["serve", "--swarm-profile", "yggdrasil-local"],
|
||||||
vec!["serve", "--restart-command", "bad"],
|
vec!["serve", "--restart-command", "bad"],
|
||||||
vec!["validate-runtime-admission"],
|
vec!["validate-runtime-admission"],
|
||||||
|
vec!["order", "unit", "restart", "nginx.service", "--reason", "r", "--command", "sh -c bad"],
|
||||||
|
vec!["order", "unit", "restart", "nginx.service", "--reason", "r", "--shell"],
|
||||||
|
vec!["order", "run", "sh", "--reason", "r"],
|
||||||
|
vec!["order", "package-install", "git-lfs", "--reason", "r", "--deploy-command", "x"],
|
||||||
|
vec!["order", "unit", "restart", "nginx.service", "extra", "--reason", "r"],
|
||||||
|
vec!["order", "deploy", "vili", "--reason", "r", "--requested-by", "someone"],
|
||||||
|
vec!["orders", "serve", "--allow-units", "nginx.service"],
|
||||||
|
vec!["orders", "run"],
|
||||||
] {
|
] {
|
||||||
assert!(parse(arguments.into_iter().map(str::to_owned)).is_err());
|
assert!(parse(arguments.into_iter().map(str::to_owned)).is_err());
|
||||||
}
|
}
|
||||||
|
// The declarative order forms parse, and only as order commands.
|
||||||
|
#[cfg(unix)]
|
||||||
|
for arguments in [
|
||||||
|
vec!["order", "unit", "restart", "nginx.service", "--reason", "r"],
|
||||||
|
vec!["order", "package-install", "git-lfs", "--reason", "r"],
|
||||||
|
vec!["order", "deploy", "vili", "--reason", "r"],
|
||||||
|
vec!["order", "list"],
|
||||||
|
vec!["orders", "serve"],
|
||||||
|
] {
|
||||||
|
let parsed = parse(arguments.into_iter().map(str::to_owned)).unwrap();
|
||||||
|
assert!(matches!(parsed, Command::Order(_) | Command::OrdersServe(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deployment_commit_primitive_is_shared_and_never_overwrites() -> Result<()> {
|
||||||
|
let temp = TempDir::new()?;
|
||||||
|
let store = temp.path().join("control.cc");
|
||||||
|
commit_deployment_command(&store, "up-one", "vili", "steward:eureka order ord-1")?;
|
||||||
|
let snapshot = ControlSnapshot::read(&store)?;
|
||||||
|
assert_eq!(snapshot.commands.len(), 1);
|
||||||
|
let command = &snapshot.commands[0].value;
|
||||||
|
assert_eq!(command.command_id, "up-one");
|
||||||
|
assert_eq!(command.selector, "vili");
|
||||||
|
assert_eq!(command.requested_by, "steward:eureka order ord-1");
|
||||||
|
assert!(commit_deployment_command(&store, "up-one", "other", "x").is_err());
|
||||||
|
assert_eq!(ControlSnapshot::read(&store)?.commands.len(), 1);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1151,7 +1151,7 @@ mod tests {
|
||||||
let answerer = thread::spawn(move || -> Result<()> {
|
let answerer = thread::spawn(move || -> Result<()> {
|
||||||
for _ in 0..500 {
|
for _ in 0..500 {
|
||||||
client.poll_resends()?;
|
client.poll_resends()?;
|
||||||
while let Some(frame) = client.receive_once()? {
|
if let Some(frame) = client.receive_once()? {
|
||||||
let envelope = HostActuatorEnvelope::decode(&frame.payload)?;
|
let envelope = HostActuatorEnvelope::decode(&frame.payload)?;
|
||||||
let request = envelope.open_request("raven", &idunn_anchor)?;
|
let request = envelope.open_request("raven", &idunn_anchor)?;
|
||||||
assert_eq!(envelope.session_nonce, nonce);
|
assert_eq!(envelope.session_nonce, nonce);
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ pub mod launcher;
|
||||||
mod incident;
|
mod incident;
|
||||||
mod runner_evidence;
|
mod runner_evidence;
|
||||||
pub mod provisioning;
|
pub mod provisioning;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod steward_orders;
|
||||||
|
|
|
||||||
|
|
@ -366,7 +366,7 @@ pub(crate) fn show_status(
|
||||||
/// printable ASCII and tab pass; every other code point is a visible
|
/// printable ASCII and tab pass; every other code point is a visible
|
||||||
/// `\u{..}` escape and every invalid UTF-8 byte a `\x..` escape. A newline
|
/// `\u{..}` escape and every invalid UTF-8 byte a `\x..` escape. A newline
|
||||||
/// inside a line is escaped too: this function writes the only ones.
|
/// inside a line is escaped too: this function writes the only ones.
|
||||||
fn emit(out: &mut impl std::io::Write, lines: &[Vec<u8>]) -> std::io::Result<()> {
|
pub(crate) fn emit(out: &mut impl std::io::Write, lines: &[Vec<u8>]) -> std::io::Result<()> {
|
||||||
for line in lines {
|
for line in lines {
|
||||||
out.write_all(escape(line).as_bytes())?;
|
out.write_all(escape(line).as_bytes())?;
|
||||||
out.write_all(b"\n")?;
|
out.write_all(b"\n")?;
|
||||||
|
|
@ -374,7 +374,7 @@ fn emit(out: &mut impl std::io::Write, lines: &[Vec<u8>]) -> std::io::Result<()>
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape(bytes: &[u8]) -> String {
|
pub(crate) fn escape(bytes: &[u8]) -> String {
|
||||||
let mut shown = String::with_capacity(bytes.len());
|
let mut shown = String::with_capacity(bytes.len());
|
||||||
for chunk in bytes.utf8_chunks() {
|
for chunk in bytes.utf8_chunks() {
|
||||||
for character in chunk.valid().chars() {
|
for character in chunk.valid().chars() {
|
||||||
|
|
|
||||||
4079
src/steward_orders.rs
Normal file
4079
src/steward_orders.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue