Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/agent-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -1066,12 +1066,20 @@ Actions:

- `list`
- `status`
- `zfs`
- `mount`
- `unmount`
- `configure`

`list` returns parsed `/proc/mounts` and `/proc/partitions` data.

`zfs` is a read-only discovery action. It reports whether the ZFS kernel
module or command-line tools are present and, when available, returns `pools`
and `datasets` from `zpool list` and `zfs list`. Missing ZFS support returns
`available: false` with empty arrays rather than an error.

`status` also includes the same `zfs` discovery object.

`status` payload:

```json
Expand Down Expand Up @@ -1211,13 +1219,19 @@ Actions:

- `interfaces`
- `status`
- `dns`
- `routes`
- `get_config`
- `set_config`
- `reload`

`interfaces` returns interface names, MAC addresses, and operstate from
`/sys/class/net`.

`dns` returns the current `/etc/resolv.conf` contents in `resolv_conf` (or
`null` when unavailable). `routes` returns parsed JSON output from
`ip -json route show`. Both actions are read-only.

`status` payload:

```json
Expand Down
16 changes: 16 additions & 0 deletions src/agent/modules/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const INFO: ModuleInfo = ModuleInfo {
"plan",
"interfaces",
"status",
"dns",
"routes",
"get_config",
"set_config",
"reload",
Expand Down Expand Up @@ -67,6 +69,8 @@ actions!(Action [payload user] => {
let args = payload.ip_args();
crate::cmd!({ &INFO, "status", user } "ip" => &args ; JSON)
},
Dns => Ok(jsonf! { "resolv_conf": read_optional("/etc/resolv.conf") }),
Routes => crate::cmd!({ &INFO, "routes", user } "ip" ["-json", "route", "show"] JSON),
GetConfig: NetworkConfigRequest => {
Ok(jsonf! { payload.path, "contents": payload.read()? })
},
Expand Down Expand Up @@ -98,6 +102,12 @@ actions!(Action [payload user] => {
/// virtual or bond devices may not report an operstate), so a single unreadable
/// file does not fail the whole snapshot. Results are sorted by name for stable
/// output to the control plane.
fn read_optional(path: &str) -> Option<String> {
fs::read_to_string(path)
.ok()
.map(|contents| contents.trim().to_owned())
}

fn read_interfaces() -> Result<Vec<Value>> {
let mut interfaces = Vec::new();
for entry in fs::read_dir("/sys/class/net").context("failed to read /sys/class/net")? {
Expand All @@ -122,6 +132,12 @@ mod tests {
use super::*;
use crate::agent::module_support::SelinuxOptions;

#[test]
fn dns_read_returns_a_stable_shape() {
let response = Dns.handle(None).unwrap();
assert!(response.get("resolv_conf").is_some());
}

#[test]
fn dry_run_set_config_does_not_write() {
let dir = tempfile::tempdir().unwrap();
Expand Down
89 changes: 88 additions & 1 deletion src/agent/modules/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use crate::prelude::*;

use crate::agent::module_support::{SelinuxOptions, apply_selinux};
use std::process::Command;

/// Storage module entry point registered under feature `storage`.
#[derive(Clone, Copy, Debug)]
Expand All @@ -26,6 +27,7 @@ const INFO: ModuleInfo = ModuleInfo {
"plan",
"list",
"status",
"zfs",
"mount",
"unmount",
"configure",
Expand All @@ -39,6 +41,13 @@ impl Mod for StorageModule {
}

fn handle(&self, action: &str, payload: Value, user: Option<&str>) -> Result<Value> {
if matches!(action, "capabilities" | "plan") {
let mut response = INFO
.metadata_response(action, &payload)
.expect("metadata action matched");
response["zfs"] = discover_zfs();
return Ok(response);
}
Action::from_payload(action, payload)?.handle(user)
}
}
Expand All @@ -57,8 +66,9 @@ actions!(Action [payload user] => {
dry_run: bool,
} => {
let result = crate::cmd!({ &INFO, "status", user } "df" ["-h", payload.path.to_string_lossy().as_ref()] json)?;
Ok(jsonf! { result, payload.dry_run })
Ok(jsonf! { result, payload.dry_run, "zfs": discover_zfs() })
},
Zfs => Ok(discover_zfs()),
Mount {
source: String,
target: String,
Expand Down Expand Up @@ -108,6 +118,76 @@ actions!(Action [payload user] => {
},
});

/// Discover ZFS without making its absence an error. Distributions may expose
/// the tools in different locations, so invoking by PATH is preferable to
/// assuming `/usr/sbin` or `/sbin`. Both commands are read-only.
fn discover_zfs() -> Value {
let kernel_module = Path::new("/sys/module/zfs").exists()
|| fs::read_to_string("/proc/modules")
.is_ok_and(|modules| modules.lines().any(|line| line.starts_with("zfs ")));
let (tooling, pools) = run_zfs_list(
"zpool",
&[
"list",
"-H",
"-p",
"-o",
"name,size,alloc,free,frag,cap,dedupratio,health,altroot",
],
&[
"name",
"size",
"allocated",
"free",
"fragmentation",
"capacity",
"deduplication",
"health",
"altroot",
],
);
let (zfs_tooling, datasets) = run_zfs_list(
"zfs",
&[
"list",
"-H",
"-p",
"-o",
"name,used,available,refer,mountpoint",
],
&["name", "used", "available", "referenced", "mountpoint"],
);
jsonf! {
"available": kernel_module || tooling || zfs_tooling,
kernel_module,
"tools": tooling || zfs_tooling,
pools,
datasets,
}
}

fn run_zfs_list(program: &str, args: &[&str], fields: &[&str]) -> (bool, Vec<Value>) {
let output = match Command::new(program).args(args).output() {
Ok(output) if output.status.success() => output,
_ => return (false, Vec::new()),
};
let rows = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let values: Vec<_> = line.split('\t').collect();
(values.len() == fields.len()).then(|| {
fields
.iter()
.zip(values)
.map(|(field, value)| ((*field).to_owned(), Value::String(value.to_owned())))
.collect::<serde_json::Map<String, Value>>()
})
})
.map(Value::Object)
.collect();
(true, rows)
}

/// Default fstab location used when `configure` is invoked without an
/// explicit `fstab_path`.
fn default_fstab_path() -> PathBuf {
Expand Down Expand Up @@ -190,6 +270,13 @@ fn append_fstab_entry(path: &PathBuf, entry: &str) -> Result<()> {
mod tests {
use super::*;

#[test]
fn absent_zfs_is_reported_without_failing() {
let discovery = discover_zfs();
assert!(discovery["pools"].is_array());
assert!(discovery["datasets"].is_array());
}

#[test]
fn parses_mounts_file() {
let dir = tempfile::tempdir().unwrap();
Expand Down