diff --git a/Cargo.lock b/Cargo.lock
index 1c0ca80d658..3a89e3ee31d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2950,6 +2950,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
[[package]]
name = "httparse"
version = "1.8.0"
@@ -3089,6 +3095,25 @@ dependencies = [
"tokio-rustls",
]
+[[package]]
+name = "hyper-staticfile"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "318ca89e4827e7fe4ddd2824f52337239796ae8ecc761a663324407dc3d8d7e7"
+dependencies = [
+ "futures-util",
+ "http",
+ "http-range",
+ "httpdate",
+ "hyper",
+ "mime_guess",
+ "percent-encoding",
+ "rand 0.8.5",
+ "tokio",
+ "url",
+ "winapi",
+]
+
[[package]]
name = "hyper-tls"
version = "0.5.0"
@@ -4792,6 +4817,7 @@ dependencies = [
"flate2",
"futures",
"http",
+ "hyper-staticfile",
"illumos-utils",
"internal-dns 0.1.0",
"ipnetwork",
diff --git a/Cargo.toml b/Cargo.toml
index f4a50b97343..18d48a76ccc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -183,6 +183,7 @@ http = "0.2.9"
httptest = "0.15.4"
hyper-rustls = "0.24.0"
hyper = "0.14"
+hyper-staticfile = "0.9.5"
humantime = "2.1.0"
illumos-utils = { path = "illumos-utils" }
indexmap = "1.9.3"
diff --git a/illumos-utils/src/running_zone.rs b/illumos-utils/src/running_zone.rs
index 1d2fd898306..ac465076636 100644
--- a/illumos-utils/src/running_zone.rs
+++ b/illumos-utils/src/running_zone.rs
@@ -13,6 +13,7 @@ use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
use camino::{Utf8Path, Utf8PathBuf};
use ipnetwork::IpNetwork;
use omicron_common::backoff;
+use slog::error;
use slog::info;
use slog::o;
use slog::warn;
@@ -24,6 +25,16 @@ use crate::zone::MockZones as Zones;
#[cfg(not(any(test, feature = "testing")))]
use crate::zone::Zones;
+/// Errors returned from methods for fetching SMF services and log files
+#[derive(thiserror::Error, Debug)]
+pub enum ServiceError {
+ #[error("I/O error")]
+ Io(#[from] std::io::Error),
+
+ #[error("Failed to run a command")]
+ RunCommand(#[from] RunCommandError),
+}
+
/// Errors returned from [`RunningZone::run_cmd`].
#[derive(thiserror::Error, Debug)]
#[error("Error running command in zone '{zone}': {err}")]
@@ -762,6 +773,128 @@ impl RunningZone {
pub fn links(&self) -> &Vec {
&self.inner.links
}
+
+ /// Return the running processes associated with all the SMF services this
+ /// zone is intended to run.
+ pub fn service_processes(
+ &self,
+ ) -> Result, ServiceError> {
+ let service_names = self.service_names()?;
+ let mut services = Vec::with_capacity(service_names.len());
+ for service_name in service_names.into_iter() {
+ let output = self.run_cmd(["ptree", "-s", &service_name])?;
+
+ // All Oxide SMF services currently run a single binary, though it
+ // may be run in a contract via `ctrun`. We don't care about that
+ // binary, but any others we _do_ want to collect data from.
+ for line in output.lines() {
+ if line.contains("ctrun") {
+ continue;
+ }
+ let line = line.trim();
+ let mut parts = line.split_ascii_whitespace();
+
+ // The first two parts should be the PID and the process binary
+ // path, respectively.
+ let Some(pid_s) = parts.next() else {
+ error!(
+ self.inner.log,
+ "failed to get service PID from ptree output";
+ "service" => &service_name,
+ );
+ continue;
+ };
+ let Ok(pid) = pid_s.parse() else {
+ error!(
+ self.inner.log,
+ "failed to parse service PID from ptree output";
+ "service" => &service_name,
+ "pid" => pid_s,
+ );
+ continue;
+ };
+ let Some(path) = parts.next() else {
+ error!(
+ self.inner.log,
+ "failed to get service binary from ptree output";
+ "service" => &service_name,
+ );
+ continue;
+ };
+ let binary = Utf8PathBuf::from(path);
+
+ // Fetch any log files for this SMF service.
+ let Some((log_file, rotated_log_files)) = self.service_log_files(&service_name)? else {
+ error!(
+ self.inner.log,
+ "failed to find log files for existing service";
+ "service_name" => &service_name,
+ );
+ continue;
+ };
+
+ services.push(ServiceProcess {
+ service_name: service_name.clone(),
+ binary,
+ pid,
+ log_file,
+ rotated_log_files,
+ });
+ }
+ }
+ Ok(services)
+ }
+
+ /// Return the names of the Oxide SMF services this zone is intended to run.
+ pub fn service_names(&self) -> Result, ServiceError> {
+ const NEEDLES: [&str; 2] = ["/oxide", "/system/illumos"];
+ let output = self.run_cmd(&["svcs", "-H", "-o", "fmri"])?;
+ Ok(output
+ .lines()
+ .filter(|line| NEEDLES.iter().any(|needle| line.contains(needle)))
+ .map(|line| line.trim().to_string())
+ .collect())
+ }
+
+ /// Return any SMF log files associated with the named service.
+ ///
+ /// Given a named service, this returns a tuple of the latest or current log
+ /// file, and an array of any rotated log files. If the service does not
+ /// exist, or there are no log files, `None` is returned.
+ pub fn service_log_files(
+ &self,
+ name: &str,
+ ) -> Result