diff --git a/Cargo.lock b/Cargo.lock
index 8b72b1e179..43d64e1433 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10648,6 +10648,16 @@ dependencies = [
"serde",
]
+[[package]]
+name = "uzers"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63"
+dependencies = [
+ "libc",
+ "log",
+]
+
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -11458,7 +11468,7 @@ dependencies = [
]
[[package]]
-name = "zone-network-setup"
+name = "zone-setup"
version = "0.1.0"
dependencies = [
"anyhow",
@@ -11469,6 +11479,7 @@ dependencies = [
"omicron-workspace-hack",
"slog",
"tokio",
+ "uzers",
"zone 0.3.0",
]
diff --git a/Cargo.toml b/Cargo.toml
index a22d0a0827..ad79092fae 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -79,7 +79,7 @@ members = [
"wicket",
"wicketd",
"workspace-hack",
- "zone-network-setup",
+ "zone-setup",
]
default-members = [
@@ -158,7 +158,7 @@ default-members = [
"wicket-dbg",
"wicket",
"wicketd",
- "zone-network-setup",
+ "zone-setup",
]
resolver = "2"
@@ -442,6 +442,7 @@ update-common = { path = "update-common" }
update-engine = { path = "update-engine" }
usdt = "0.5.0"
uuid = { version = "1.8.0", features = ["serde", "v4"] }
+uzers = "0.11"
walkdir = "2.5"
whoami = "1.5"
wicket = { path = "wicket" }
diff --git a/illumos-utils/src/lib.rs b/illumos-utils/src/lib.rs
index 550170b0f2..d041c866b0 100644
--- a/illumos-utils/src/lib.rs
+++ b/illumos-utils/src/lib.rs
@@ -24,6 +24,7 @@ pub mod route;
pub mod running_zone;
pub mod scf;
pub mod svc;
+pub mod svcadm;
pub mod vmm_reservoir;
pub mod zfs;
pub mod zone;
diff --git a/illumos-utils/src/svcadm.rs b/illumos-utils/src/svcadm.rs
new file mode 100644
index 0000000000..0d472187df
--- /dev/null
+++ b/illumos-utils/src/svcadm.rs
@@ -0,0 +1,21 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! Utilities for manipulating SMF services.
+
+use crate::zone::SVCADM;
+use crate::{execute, ExecutionError, PFEXEC};
+
+/// Wraps commands for interacting with svcadm.
+pub struct Svcadm {}
+
+#[cfg_attr(any(test, feature = "testing"), mockall::automock)]
+impl Svcadm {
+ pub fn refresh_logadm_upgrade() -> Result<(), ExecutionError> {
+ let mut cmd = std::process::Command::new(PFEXEC);
+ let cmd = cmd.args(&[SVCADM, "refresh", "logadm-upgrade"]);
+ execute(cmd)?;
+ Ok(())
+ }
+}
diff --git a/package-manifest.toml b/package-manifest.toml
index 2819010335..c21f9c0f73 100644
--- a/package-manifest.toml
+++ b/package-manifest.toml
@@ -100,7 +100,7 @@ only_for_targets.image = "standard"
source.type = "composite"
source.packages = [
"omicron-nexus.tar.gz",
- "zone-network-setup.tar.gz",
+ "zone-setup.tar.gz",
"zone-network-install.tar.gz",
"opte-interface-setup.tar.gz",
]
@@ -130,11 +130,7 @@ output.intermediate_only = true
service_name = "oximeter"
only_for_targets.image = "standard"
source.type = "composite"
-source.packages = [
- "oximeter-collector.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
-]
+source.packages = [ "oximeter-collector.tar.gz", "zone-setup.tar.gz", "zone-network-install.tar.gz" ]
output.type = "zone"
[package.oximeter-collector]
@@ -157,8 +153,8 @@ source.type = "composite"
source.packages = [
"clickhouse_svc.tar.gz",
"internal-dns-cli.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
+ "zone-setup.tar.gz",
+ "zone-network-install.tar.gz"
]
output.type = "zone"
@@ -183,8 +179,8 @@ source.type = "composite"
source.packages = [
"clickhouse_keeper_svc.tar.gz",
"internal-dns-cli.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
+ "zone-setup.tar.gz",
+ "zone-network-install.tar.gz"
]
output.type = "zone"
@@ -209,8 +205,8 @@ source.type = "composite"
source.packages = [
"cockroachdb-service.tar.gz",
"internal-dns-cli.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
+ "zone-setup.tar.gz",
+ "zone-network-install.tar.gz"
]
output.type = "zone"
@@ -245,8 +241,8 @@ source.type = "composite"
source.packages = [
"dns-server.tar.gz",
"internal-dns-customizations.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
+ "zone-setup.tar.gz",
+ "zone-network-install.tar.gz"
]
output.type = "zone"
@@ -257,7 +253,7 @@ source.type = "composite"
source.packages = [
"dns-server.tar.gz",
"external-dns-customizations.tar.gz",
- "zone-network-setup.tar.gz",
+ "zone-setup.tar.gz",
"zone-network-install.tar.gz",
"opte-interface-setup.tar.gz",
]
@@ -298,10 +294,11 @@ service_name = "ntp"
only_for_targets.image = "standard"
source.type = "composite"
source.packages = [
+ "chrony-setup.tar.gz",
"ntp-svc.tar.gz",
"opte-interface-setup.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
+ "zone-setup.tar.gz",
+ "zone-network-install.tar.gz"
]
output.type = "zone"
@@ -311,8 +308,17 @@ only_for_targets.image = "standard"
source.type = "local"
source.paths = [
{ from = "smf/ntp/manifest", to = "/var/svc/manifest/site/ntp" },
- { from = "smf/ntp/method", to = "/var/svc/method" },
- { from = "smf/ntp/etc", to = "/etc" },
+]
+output.intermediate_only = true
+output.type = "zone"
+
+[package.chrony-setup]
+service_name = "chrony-setup"
+only_for_targets.image = "standard"
+source.type = "local"
+source.paths = [
+ { from = "smf/chrony-setup/manifest.xml", to = "/var/svc/manifest/site/chrony-setup/manifest.xml" },
+ { from = "smf/chrony-setup/etc", to = "/etc" },
]
output.intermediate_only = true
output.type = "zone"
@@ -457,11 +463,7 @@ output.intermediate_only = true
service_name = "crucible"
only_for_targets.image = "standard"
source.type = "composite"
-source.packages = [
- "crucible.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
-]
+source.packages = [ "crucible.tar.gz", "zone-setup.tar.gz", "zone-network-install.tar.gz" ]
output.type = "zone"
@@ -469,11 +471,7 @@ output.type = "zone"
service_name = "crucible_pantry"
only_for_targets.image = "standard"
source.type = "composite"
-source.packages = [
- "crucible-pantry.tar.gz",
- "zone-network-setup.tar.gz",
- "zone-network-install.tar.gz",
-]
+source.packages = [ "crucible-pantry.tar.gz", "zone-setup.tar.gz", "zone-network-install.tar.gz" ]
output.type = "zone"
# Packages not built within Omicron, but which must be imported.
@@ -746,11 +744,11 @@ source.paths = [
output.type = "zone"
output.intermediate_only = true
-[package.zone-network-setup]
-service_name = "zone-network-cli"
+[package.zone-setup]
+service_name = "zone-setup-cli"
only_for_targets.image = "standard"
source.type = "local"
-source.rust.binary_names = ["zone-networking"]
+source.rust.binary_names = ["zone-setup"]
source.rust.release = true
output.type = "zone"
output.intermediate_only = true
diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs
index 3584e8f139..8ba1504f8d 100644
--- a/sled-agent/src/services.rs
+++ b/sled-agent/src/services.rs
@@ -2003,7 +2003,7 @@ impl ServiceManager {
Self::dns_install(info, Some(dns_servers.to_vec()), domain)
.await?;
- let mut ntp_config = PropertyGroupBuilder::new("config")
+ let mut chrony_config = PropertyGroupBuilder::new("config")
.add_property("allow", "astring", &rack_net)
.add_property(
"boundary",
@@ -2012,7 +2012,7 @@ impl ServiceManager {
);
for s in ntp_servers {
- ntp_config = ntp_config.add_property(
+ chrony_config = chrony_config.add_property(
"server",
"astring",
&s.to_string(),
@@ -2027,13 +2027,17 @@ impl ServiceManager {
}
let ntp_service = ServiceBuilder::new("oxide/ntp")
- .add_instance(
+ .add_instance(ServiceInstanceBuilder::new("default"));
+
+ let chrony_setup_service =
+ ServiceBuilder::new("oxide/chrony-setup").add_instance(
ServiceInstanceBuilder::new("default")
- .add_property_group(ntp_config),
+ .add_property_group(chrony_config),
);
let mut profile = ProfileBuilder::new("omicron")
.add_service(nw_setup_service)
+ .add_service(chrony_setup_service)
.add_service(disabled_ssh_service)
.add_service(dns_install_service)
.add_service(dns_client_service)
diff --git a/smf/ntp/etc/logadm.d/chrony.logadm.conf b/smf/chrony-setup/etc/logadm.d/chrony.logadm.conf
similarity index 100%
rename from smf/ntp/etc/logadm.d/chrony.logadm.conf
rename to smf/chrony-setup/etc/logadm.d/chrony.logadm.conf
diff --git a/smf/chrony-setup/manifest.xml b/smf/chrony-setup/manifest.xml
new file mode 100644
index 0000000000..f31f13a2ea
--- /dev/null
+++ b/smf/chrony-setup/manifest.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Oxide Chrony Setup
+
+
+ Configures chronyd for the NTP zone
+
+
+
+
+
diff --git a/smf/ntp/etc/inet/chrony.conf.boundary b/smf/ntp/etc/inet/chrony.conf.boundary
deleted file mode 100644
index d13bc9c815..0000000000
--- a/smf/ntp/etc/inet/chrony.conf.boundary
+++ /dev/null
@@ -1,32 +0,0 @@
-#
-# Configuration file for a boundary NTP server - one which communicates with
-# NTP servers outside the rack.
-#
-
-pool @SERVER@ iburst maxdelay 0.1 maxsources 16
-
-driftfile /var/lib/chrony/drift
-ntsdumpdir /var/lib/chrony
-dumpdir /var/lib/chrony
-pidfile /var/run/chrony/chronyd.pid
-logdir /var/log/chrony
-
-log measurements statistics tracking
-
-allow fe80::/10
-allow @ALLOW@
-
-# Enable local reference mode, which keeps us operating as an NTP server that
-# appears synchronised even if there are currently no active upstreams. When
-# in this mode, we report as stratum 10 to clients.
-local stratum 10
-
-# makestep
-# We allow chrony to step the system clock during the first three time updates
-# if we are more than 0.1 seconds out.
-makestep 0.1 3
-
-# When a leap second occurs we slew the clock over approximately 37 seconds.
-leapsecmode slew
-maxslewrate 2708.333
-
diff --git a/smf/ntp/etc/inet/chrony.conf.internal b/smf/ntp/etc/inet/chrony.conf.internal
deleted file mode 100644
index 9e9ff3ddea..0000000000
--- a/smf/ntp/etc/inet/chrony.conf.internal
+++ /dev/null
@@ -1,31 +0,0 @@
-#
-# Configuration file for an internal NTP server - one which communicates with
-# boundary NTP servers within the rack.
-#
-
-server @SERVER@ iburst minpoll 0 maxpoll 4
-
-driftfile /var/lib/chrony/drift
-ntsdumpdir /var/lib/chrony
-dumpdir /var/lib/chrony
-pidfile /var/run/chrony/chronyd.pid
-logdir /var/log/chrony
-
-log measurements statistics tracking
-
-# makestep
-# We allow chrony to step the system clock if we are more than a day out,
-# regardless of how many clock updates have occurred since boot.
-# The boundary NTP servers are configured with local reference mode, which
-# means that if they start up without external connectivity, they will appear
-# as authoritative servers even if they are advertising January 1987
-# (which is the default system clock on a gimlet after boot).
-# This configuration allows a one-off adjustment once RSS begins and the
-# boundary servers are synchronised, after which the clock will advance
-# monotonically forwards.
-makestep 86400 -1
-
-# When a leap second occurs we slew the clock over approximately 37 seconds.
-leapsecmode slew
-maxslewrate 2708.333
-
diff --git a/smf/ntp/manifest/manifest.xml b/smf/ntp/manifest/manifest.xml
index 7783bbe76c..df427a16a5 100644
--- a/smf/ntp/manifest/manifest.xml
+++ b/smf/ntp/manifest/manifest.xml
@@ -39,6 +39,11 @@
+
+
+
+
@@ -57,7 +62,9 @@
The service also always starts the binary with ASLR enabled,
regardless of whether it was linked with -zaslr
-->
-
-
-
-
-
-
-
-
-
-
-
diff --git a/smf/ntp/method/svc-site-ntp b/smf/ntp/method/svc-site-ntp
deleted file mode 100755
index 8b1e3c7d52..0000000000
--- a/smf/ntp/method/svc-site-ntp
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/ksh
-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-# Copyright 2023 Oxide Computer Company
-
-. /lib/svc/share/smf_include.sh
-
-function die {
- typeset rc=$1; shift
- echo "$@" >&2
- exit $rc
-}
-
-function fatal { die $SMF_EXIT_ERR_FATAL "$@"; }
-function config { die $SMF_EXIT_ERR_CONFIG "$@"; }
-
-[[ -n "$SMF_FMRI" ]] || fatal "SMF framework variables are not initialized."
-
-typeset -r action=${1:?action parameter not specified}
-typeset -r contract=$2 # For the refresh and stop methods
-
-for var in file boundary server allow; do
- nameref _var=$var
-
- typeset _var=`svcprop -p config/$var $SMF_FMRI`
- (($? == 0)) || config "Could not retrieve config/$var property"
- [[ $_var == '""' ]] && _var=
-
- # config/allow is optional - not used for non-boundary configurations.
- if [[ $var != allow ]]; then
- [[ -n "$_var" ]] || config "config/$var is empty."
- fi
-done
-
-typeset template=/etc/inet/chrony.conf.internal
-[[ "$boundary" = true ]] && template=/etc/inet/chrony.conf.boundary
-
-cat <<-EOM
- NTP Service Configuration
- -------------------------
- Servers: $server
- Allow: $allow
- Boundary: $boundary
- Template: $template
- Config: $file
-EOM
-
-function generate_config_file {
- typeset oldsum=
- [[ -r "$file" ]] && oldsum=$(digest -a sha256 $file)
-
- serverline=$(grep '@SERVER@' $template | head -1)
- [[ -n "$serverline" ]] || fatal "No @SERVER@ line found in $template"
-
- {
- sed < $template "
- /@SERVER@/d
- s^@ALLOW@^$allow^g
- " || fatal "Could not generate configuration file"
- for s in $server; do
- echo "$serverline" | sed "s^@SERVER@^$s^"
- done
- echo
- } > $file
-
- typeset newsum=$(digest -a sha256 $file)
-
- # This function's exit status indicates if the configuration file has
- # changed.
- [[ -z "$oldsum" || "$oldsum" != "$newsum" ]]
-}
-
-function start_daemon {
- echo "* Starting daemon"
- /usr/sbin/chronyd -d -f "$file" &
-}
-
-function stop_daemon {
- echo "* Stopping daemon"
- smf_kill_contract $contract TERM 1 30
- rc=$?
- ((rc == 1)) && fatal "Invalid contract in $action"
- # It's possible that the contract did not empty with SIGTERM; move on
- # to KILL.
- ((rc == 2)) && smf_kill_contract $contract KILL 1
-}
-
-# The NTP zone delivers a logadm fragment into /etc/logadm.d/ that needs to
-# be added to the system's /etc/logadm.conf. Unfortunately, the service which
-# does this - system/logadm-upgrade - only processes files with mode 444 and
-# root:sys ownership so we need to adjust things here (until omicron package
-# supports including ownership and permissions in the generated tar files).
-function update_logadm {
- typeset cf=/etc/logadm.conf
- typeset ff=/etc/logadm.d/chrony.logadm.conf
-
- egrep -s "chrony_logs" $cf && return
-
- echo "* Updating logadm"
- chown root:sys $ff
- chmod 444 $ff
- touch $ff
- svcadm refresh logadm-upgrade
-}
-
-case $action in
- start)
- generate_config_file
- update_logadm
- start_daemon
- ;;
- refresh)
- generate_config_file && stop_daemon
- # SMF will restart the service since the contract is now empty.
- ;;
- stop)
- stop_daemon
- ;;
- *)
- fatal "Unknown action '$action'"
- ;;
-esac
-
-exit $SMF_EXIT_OK
-
diff --git a/smf/opte-interface-setup/manifest.xml b/smf/opte-interface-setup/manifest.xml
index 68641f56fc..dc1301d846 100644
--- a/smf/opte-interface-setup/manifest.xml
+++ b/smf/opte-interface-setup/manifest.xml
@@ -18,7 +18,7 @@
diff --git a/smf/zone-network-setup/manifest.xml b/smf/zone-network-setup/manifest.xml
index a20ff949a4..8955f5adc0 100644
--- a/smf/zone-network-setup/manifest.xml
+++ b/smf/zone-network-setup/manifest.xml
@@ -18,7 +18,7 @@
diff --git a/zone-network-setup/src/bin/zone-networking.rs b/zone-network-setup/src/bin/zone-networking.rs
deleted file mode 100644
index e36afd6b03..0000000000
--- a/zone-network-setup/src/bin/zone-networking.rs
+++ /dev/null
@@ -1,202 +0,0 @@
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this
-// file, You can obtain one at https://mozilla.org/MPL/2.0/.
-
-//! CLI to set up zone networking
-
-use anyhow::anyhow;
-use clap::{arg, command, ArgMatches, Command};
-use illumos_utils::ipadm::Ipadm;
-use illumos_utils::route::{Gateway, Route};
-use omicron_common::cmd::fatal;
-use omicron_common::cmd::CmdError;
-use slog::{info, Logger};
-use std::fs;
-use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
-
-pub const HOSTS_FILE: &str = "/etc/inet/hosts";
-
-fn parse_ip(s: &str) -> anyhow::Result {
- if s == "unknown" {
- return Err(anyhow!("ERROR: Missing input value"));
- };
- s.parse().map_err(|_| anyhow!("ERROR: Invalid IP address"))
-}
-
-fn parse_ipv4(s: &str) -> anyhow::Result {
- if s == "unknown" {
- return Err(anyhow!("ERROR: Missing input value"));
- };
- s.parse().map_err(|_| anyhow!("ERROR: Invalid IPv4 address"))
-}
-
-fn parse_ipv6(s: &str) -> anyhow::Result {
- if s == "unknown" {
- return Err(anyhow!("ERROR: Missing input value"));
- };
- s.parse().map_err(|_| anyhow!("ERROR: Invalid IPv6 address"))
-}
-
-fn parse_datalink(s: &str) -> anyhow::Result {
- if s == "unknown" {
- return Err(anyhow!("ERROR: Missing data link"));
- };
- s.parse().map_err(|_| anyhow!("ERROR: Invalid data link"))
-}
-
-fn parse_opte_iface(s: &str) -> anyhow::Result {
- if s == "unknown" {
- return Err(anyhow!("ERROR: Missing OPTE interface"));
- };
- s.parse().map_err(|_| anyhow!("ERROR: Invalid OPTE interface"))
-}
-
-#[tokio::main]
-async fn main() {
- if let Err(message) = do_run().await {
- fatal(message);
- }
-}
-
-async fn do_run() -> Result<(), CmdError> {
- let log = dropshot::ConfigLogging::File {
- path: "/dev/stderr".into(),
- level: dropshot::ConfigLoggingLevel::Info,
- if_exists: dropshot::ConfigLoggingIfExists::Append,
- }
- .to_logger("zone-networking")
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- let matches = command!()
- .subcommand(
- Command::new("set-up")
- .about(
- "Sets up common networking configuration across all zones",
- )
- .arg(
- arg!(
- -d --datalink "datalink"
- )
- .required(true)
- .value_parser(parse_datalink),
- )
- .arg(
- arg!(
- -g --gateway "gateway"
- )
- .required(true)
- .value_parser(parse_ipv6),
- )
- .arg(
- arg!(
- -s --static_addr "static_addr"
- )
- .required(true)
- .value_parser(parse_ipv6),
- ),
- )
- .subcommand(
- Command::new("opte-interface-set-up")
- .about("Sets up OPTE interface")
- .arg(
- arg!(
- -i --opte_interface "opte_interface"
- )
- .required(true)
- .value_parser(parse_opte_iface),
- )
- .arg(
- arg!(
- -g --opte_gateway "opte_gateway"
- )
- .required(true)
- .value_parser(parse_ipv4),
- )
- .arg(
- arg!(
- -p --opte_ip "opte_ip"
- )
- .required(true)
- .value_parser(parse_ip),
- ),
- )
- .get_matches();
-
- if let Some(matches) = matches.subcommand_matches("set-up") {
- set_up(matches, log.clone()).await?;
- }
-
- if let Some(matches) = matches.subcommand_matches("opte-interface-set-up") {
- opte_interface_set_up(matches, log.clone()).await?;
- }
-
- Ok(())
-}
-
-async fn set_up(matches: &ArgMatches, log: Logger) -> Result<(), CmdError> {
- let datalink: &String = matches.get_one("datalink").unwrap();
- let static_addr: &Ipv6Addr = matches.get_one("static_addr").unwrap();
- let gateway: Ipv6Addr = *matches.get_one("gateway").unwrap();
- let zonename = zone::current().await.map_err(|err| {
- CmdError::Failure(anyhow!(
- "Could not determine local zone name: {}",
- err
- ))
- })?;
-
- // TODO: remove when https://github.com/oxidecomputer/stlouis/issues/435 is
- // addressed
- info!(&log, "Ensuring a temporary IP interface is created"; "data link" => ?datalink);
- Ipadm::set_temp_interface_for_datalink(&datalink)
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Setting MTU to 9000 for IPv6 and IPv4"; "data link" => ?datalink);
- Ipadm::set_interface_mtu(&datalink)
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Ensuring static and auto-configured addresses are set on the IP interface"; "data link" => ?datalink, "static address" => ?static_addr);
- Ipadm::create_static_and_autoconfigured_addrs(&datalink, static_addr)
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Ensuring there is a default route"; "gateway" => ?gateway);
- Route::ensure_default_route_with_gateway(Gateway::Ipv6(gateway))
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Populating hosts file for zone"; "zonename" => ?zonename);
- fs::write(
- HOSTS_FILE,
- format!(
- r#"
-::1 localhost loghost
-127.0.0.1 localhost loghost
-{static_addr} {zonename}.local {zonename}
-"#
- ),
- )
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- Ok(())
-}
-
-async fn opte_interface_set_up(
- matches: &ArgMatches,
- log: Logger,
-) -> Result<(), CmdError> {
- let interface: &String = matches.get_one("opte_interface").unwrap();
- let gateway: Ipv4Addr = *matches.get_one("opte_gateway").unwrap();
- let opte_ip: &IpAddr = matches.get_one("opte_ip").unwrap();
-
- info!(&log, "Creating gateway on the OPTE IP interface if it doesn't already exist"; "OPTE interface" => ?interface);
- Ipadm::create_opte_gateway(interface)
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Ensuring there is a gateway route"; "OPTE gateway" => ?gateway, "OPTE interface" => ?interface, "OPTE IP" => ?opte_ip);
- Route::ensure_opte_route(&gateway, interface, &opte_ip)
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- info!(&log, "Ensuring there is a default route"; "gateway" => ?gateway);
- Route::ensure_default_route_with_gateway(Gateway::Ipv4(gateway))
- .map_err(|err| CmdError::Failure(anyhow!(err)))?;
-
- Ok(())
-}
diff --git a/zone-network-setup/Cargo.toml b/zone-setup/Cargo.toml
similarity index 88%
rename from zone-network-setup/Cargo.toml
rename to zone-setup/Cargo.toml
index 28854b82f7..6ee89e5302 100644
--- a/zone-network-setup/Cargo.toml
+++ b/zone-setup/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "zone-network-setup"
+name = "zone-setup"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
@@ -14,3 +14,4 @@ dropshot.workspace = true
tokio.workspace = true
omicron-workspace-hack.workspace = true
zone.workspace = true
+uzers.workspace = true
\ No newline at end of file
diff --git a/zone-setup/src/bin/zone-setup.rs b/zone-setup/src/bin/zone-setup.rs
new file mode 100644
index 0000000000..333f479721
--- /dev/null
+++ b/zone-setup/src/bin/zone-setup.rs
@@ -0,0 +1,484 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! CLI to set up zone configuration
+
+use anyhow::anyhow;
+use clap::{arg, command, value_parser, Arg, ArgMatches, Command};
+use illumos_utils::ipadm::Ipadm;
+use illumos_utils::route::{Gateway, Route};
+use illumos_utils::svcadm::Svcadm;
+use omicron_common::cmd::fatal;
+use omicron_common::cmd::CmdError;
+use slog::{info, Logger};
+use std::fs::{metadata, read_to_string, set_permissions, write, OpenOptions};
+use std::io::Write;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::os::unix::fs::chown;
+use std::path::Path;
+use uzers::{get_group_by_name, get_user_by_name};
+
+pub const HOSTS_FILE: &str = "/etc/inet/hosts";
+pub const CHRONY_CONFIG_FILE: &str = "/etc/inet/chrony.conf";
+pub const LOGADM_CONFIG_FILE: &str = "/etc/logadm.d/chrony.logadm.conf";
+pub const ROOT: &str = "root";
+pub const SYS: &str = "sys";
+
+pub const COMMON_NW_CMD: &str = "common-networking";
+pub const OPTE_INTERFACE_CMD: &str = "opte-interface";
+pub const CHRONY_SETUP_CMD: &str = "chrony-setup";
+
+fn parse_ip(s: &str) -> anyhow::Result {
+ if s == "unknown" {
+ return Err(anyhow!("ERROR: Missing input value"));
+ };
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid IP address"))
+}
+
+fn parse_ipv4(s: &str) -> anyhow::Result {
+ if s == "unknown" {
+ return Err(anyhow!("ERROR: Missing input value"));
+ };
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid IPv4 address"))
+}
+
+fn parse_ipv6(s: &str) -> anyhow::Result {
+ if s == "unknown" {
+ return Err(anyhow!("ERROR: Missing input value"));
+ };
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid IPv6 address"))
+}
+
+fn parse_datalink(s: &str) -> anyhow::Result {
+ if s == "unknown" {
+ return Err(anyhow!("ERROR: Missing data link"));
+ };
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid data link"))
+}
+
+fn parse_opte_iface(s: &str) -> anyhow::Result {
+ if s == "unknown" {
+ return Err(anyhow!("ERROR: Missing OPTE interface"));
+ };
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid OPTE interface"))
+}
+
+fn parse_chrony_conf(s: &str) -> anyhow::Result {
+ if s == "" {
+ return Err(anyhow!("ERROR: Missing chrony configuration file"));
+ };
+
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid chrony configuration file"))
+}
+
+fn parse_boundary(s: &str) -> anyhow::Result {
+ s.parse().map_err(|_| anyhow!("ERROR: Invalid boundary input"))
+}
+
+#[tokio::main]
+async fn main() {
+ if let Err(message) = do_run().await {
+ fatal(message);
+ }
+}
+
+async fn do_run() -> Result<(), CmdError> {
+ let log = dropshot::ConfigLogging::File {
+ path: "/dev/stderr".into(),
+ level: dropshot::ConfigLoggingLevel::Info,
+ if_exists: dropshot::ConfigLoggingIfExists::Append,
+ }
+ .to_logger("zone-setup")
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ let matches = command!()
+ .subcommand(
+ Command::new(COMMON_NW_CMD)
+ .about(
+ "Sets up common networking configuration across all zones",
+ )
+ .arg(
+ arg!(
+ -d --datalink "datalink"
+ )
+ .required(true)
+ .value_parser(parse_datalink),
+ )
+ .arg(
+ arg!(
+ -g --gateway "gateway"
+ )
+ .required(true)
+ .value_parser(parse_ipv6),
+ )
+ .arg(
+ arg!(
+ -s --static_addr "static_addr"
+ )
+ .required(true)
+ .value_parser(parse_ipv6),
+ ),
+ )
+ .subcommand(
+ Command::new(OPTE_INTERFACE_CMD)
+ .about("Sets up OPTE interface")
+ .arg(
+ arg!(
+ -i --opte_interface "opte_interface"
+ )
+ .required(true)
+ .value_parser(parse_opte_iface),
+ )
+ .arg(
+ arg!(
+ -g --opte_gateway "opte_gateway"
+ )
+ .required(true)
+ .value_parser(parse_ipv4),
+ )
+ .arg(
+ arg!(
+ -p --opte_ip "opte_ip"
+ )
+ .required(true)
+ .value_parser(parse_ip),
+ ),
+ )
+ .subcommand(
+ Command::new(CHRONY_SETUP_CMD)
+ .about("Sets up Chrony configuration for NTP zone")
+ .arg(
+ arg!(-f --file "Chrony configuration file")
+ .default_value(CHRONY_CONFIG_FILE)
+ .value_parser(parse_chrony_conf)
+ )
+ .arg(
+ arg!(-b --boundary "Whether this is a boundary or internal NTP zone")
+ .required(true)
+ .value_parser(parse_boundary),
+ )
+ .arg(
+ Arg::new("servers")
+ .short('s')
+ .long("servers")
+ .num_args(1..)
+ .value_delimiter(' ')
+ .value_parser(value_parser!(String))
+ .help("List of NTP servers separated by a space")
+ .required(true)
+ )
+ .arg(
+ arg!(-a --allow "Allowed IPv6 range")
+ .num_args(0..=1)
+ ),
+ )
+ .get_matches();
+
+ if let Some(matches) = matches.subcommand_matches(COMMON_NW_CMD) {
+ common_nw_set_up(matches, log.clone()).await?;
+ }
+
+ if let Some(matches) = matches.subcommand_matches(OPTE_INTERFACE_CMD) {
+ opte_interface_set_up(matches, log.clone()).await?;
+ }
+
+ if let Some(matches) = matches.subcommand_matches(CHRONY_SETUP_CMD) {
+ chrony_setup(matches, log.clone()).await?;
+ }
+
+ Ok(())
+}
+
+async fn chrony_setup(
+ matches: &ArgMatches,
+ log: Logger,
+) -> Result<(), CmdError> {
+ let servers =
+ matches.get_many::("servers").unwrap().collect::>();
+ let allow: Option<&String> = matches.get_one("allow");
+
+ let file: &String = matches.get_one("file").unwrap();
+ let is_boundary: &bool = matches.get_one("boundary").unwrap();
+ println!(
+ "servers: {:?}\nfile: {}\nallow: {:?}\nboundary: {:?}",
+ servers, file, allow, is_boundary
+ );
+
+ generate_chrony_config(&log, is_boundary, allow, file, servers)?;
+
+ // The NTP zone delivers a logadm fragment into /etc/logadm.d/ that needs to
+ // be added to the system's /etc/logadm.conf. Unfortunately, the service which
+ // does this - system/logadm-upgrade - only processes files with mode 444 and
+ // root:sys ownership so we need to adjust things here (until omicron package
+ // supports including ownership and permissions in the generated tar files).
+ info!(&log, "Setting mode 444 and root:sys ownership to logadm fragment file"; "logadm config" => ?LOGADM_CONFIG_FILE);
+ set_permissions_for_logadm_config()?;
+
+ info!(&log, "Updating logadm"; "logadm config" => ?LOGADM_CONFIG_FILE);
+ Svcadm::refresh_logadm_upgrade()
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ Ok(())
+}
+
+fn set_permissions_for_logadm_config() -> Result<(), CmdError> {
+ let mut perms = metadata(LOGADM_CONFIG_FILE)
+ .map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not retrieve chrony logadm configuration file {} metadata: {}",
+ LOGADM_CONFIG_FILE,
+ err
+ ))
+ })?
+ .permissions();
+ perms.set_readonly(true);
+ set_permissions(LOGADM_CONFIG_FILE, perms).map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not set 444 permissions on chrony logadm configuration file {}: {}",
+ LOGADM_CONFIG_FILE,
+ err
+ ))
+ })?;
+
+ let root_uid = match get_user_by_name(ROOT) {
+ Some(user) => user.uid(),
+ None => {
+ return Err(CmdError::Failure(anyhow!(format!(
+ "Could not retrieve ID from user: {}",
+ ROOT
+ ))))
+ }
+ };
+
+ let sys_gid = match get_group_by_name(SYS) {
+ Some(group) => group.gid(),
+ None => {
+ return Err(CmdError::Failure(anyhow!(format!(
+ "Could not retrieve ID from group: {}",
+ SYS
+ ))))
+ }
+ };
+
+ chown(LOGADM_CONFIG_FILE, Some(root_uid), Some(sys_gid)).map_err(
+ |err| {
+ CmdError::Failure(anyhow!(
+ "Could not set ownership of logadm configuration file {}: {}",
+ LOGADM_CONFIG_FILE,
+ err
+ ))
+ },
+ )?;
+
+ Ok(())
+}
+
+fn generate_chrony_config(
+ log: &Logger,
+ is_boundary: &bool,
+ allow: Option<&String>,
+ file: &String,
+ servers: Vec<&String>,
+) -> Result<(), CmdError> {
+ let internal_ntp_tpl = String::from(
+ "#
+# Configuration file for an internal NTP server - one which communicates with
+# boundary NTP servers within the rack.
+#
+
+driftfile /var/lib/chrony/drift
+ntsdumpdir /var/lib/chrony
+dumpdir /var/lib/chrony
+pidfile /var/run/chrony/chronyd.pid
+logdir /var/log/chrony
+
+log measurements statistics tracking
+
+# makestep
+# We allow chrony to step the system clock if we are more than a day out,
+# regardless of how many clock updates have occurred since boot.
+# The boundary NTP servers are configured with local reference mode, which
+# means that if they start up without external connectivity, they will appear
+# as authoritative servers even if they are advertising January 1987
+# (which is the default system clock on a gimlet after boot).
+# This configuration allows a one-off adjustment once RSS begins and the
+# boundary servers are synchronised, after which the clock will advance
+# monotonically forwards.
+makestep 86400 -1
+
+# When a leap second occurs we slew the clock over approximately 37 seconds.
+leapsecmode slew
+maxslewrate 2708.333
+
+",
+ );
+
+ let boundary_ntp_tpl = String::from(
+ "#
+# Configuration file for a boundary NTP server - one which communicates with
+# NTP servers outside the rack.
+#
+
+driftfile /var/lib/chrony/drift
+ntsdumpdir /var/lib/chrony
+dumpdir /var/lib/chrony
+pidfile /var/run/chrony/chronyd.pid
+logdir /var/log/chrony
+
+log measurements statistics tracking
+
+allow fe80::/10
+allow @ALLOW@
+
+# Enable local reference mode, which keeps us operating as an NTP server that
+# appears synchronised even if there are currently no active upstreams. When
+# in this mode, we report as stratum 10 to clients.
+local stratum 10
+
+# makestep
+# We allow chrony to step the system clock during the first three time updates
+# if we are more than 0.1 seconds out.
+makestep 0.1 3
+
+# When a leap second occurs we slew the clock over approximately 37 seconds.
+leapsecmode slew
+maxslewrate 2708.333
+
+",
+ );
+
+ let mut contents =
+ if *is_boundary { boundary_ntp_tpl } else { internal_ntp_tpl };
+
+ if let Some(allow) = allow {
+ contents = contents.replace("@ALLOW@", &allow.to_string());
+ }
+
+ let new_config = if *is_boundary {
+ for s in servers {
+ let str_line =
+ format!("pool {} iburst maxdelay 0.1 maxsources 16\n", s);
+ contents.push_str(&str_line)
+ }
+ contents
+ } else {
+ for s in servers {
+ let str_line = format!("server {} iburst minpoll 0 maxpoll 4\n", s);
+ contents.push_str(&str_line)
+ }
+ contents
+ };
+
+ // We read the contents from the old configuration file if it existed
+ // so that we can verify if it changed.
+ let old_file = if Path::exists(Path::new(file)) {
+ Some(read_to_string(file).map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not read old chrony configuration file {}: {}",
+ file,
+ err
+ ))
+ })?)
+ } else {
+ None
+ };
+
+ let mut config_file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(file)
+ .map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not create chrony configuration file {}: {}",
+ file,
+ err
+ ))
+ })?;
+ config_file.write(new_config.as_bytes()).map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not write to chrony configuration file {}: {}",
+ file,
+ err
+ ))
+ })?;
+
+ if old_file.clone().is_some_and(|f| f != new_config) {
+ info!(&log, "Chrony configuration file has changed";
+ "old configuration file" => ?old_file, "new configuration file" => ?new_config,);
+ }
+
+ Ok(())
+}
+
+async fn common_nw_set_up(
+ matches: &ArgMatches,
+ log: Logger,
+) -> Result<(), CmdError> {
+ let datalink: &String = matches.get_one("datalink").unwrap();
+ let static_addr: &Ipv6Addr = matches.get_one("static_addr").unwrap();
+ let gateway: Ipv6Addr = *matches.get_one("gateway").unwrap();
+ let zonename = zone::current().await.map_err(|err| {
+ CmdError::Failure(anyhow!(
+ "Could not determine local zone name: {}",
+ err
+ ))
+ })?;
+
+ // TODO: remove when https://github.com/oxidecomputer/stlouis/issues/435 is
+ // addressed
+ info!(&log, "Ensuring a temporary IP interface is created"; "data link" => ?datalink);
+ Ipadm::set_temp_interface_for_datalink(&datalink)
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Setting MTU to 9000 for IPv6 and IPv4"; "data link" => ?datalink);
+ Ipadm::set_interface_mtu(&datalink)
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Ensuring static and auto-configured addresses are set on the IP interface"; "data link" => ?datalink, "static address" => ?static_addr);
+ Ipadm::create_static_and_autoconfigured_addrs(&datalink, static_addr)
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Ensuring there is a default route"; "gateway" => ?gateway);
+ Route::ensure_default_route_with_gateway(Gateway::Ipv6(gateway))
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Populating hosts file for zone"; "zonename" => ?zonename);
+ write(
+ HOSTS_FILE,
+ format!(
+ r#"
+::1 localhost loghost
+127.0.0.1 localhost loghost
+{static_addr} {zonename}.local {zonename}
+"#
+ ),
+ )
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ Ok(())
+}
+
+async fn opte_interface_set_up(
+ matches: &ArgMatches,
+ log: Logger,
+) -> Result<(), CmdError> {
+ let interface: &String = matches.get_one("opte_interface").unwrap();
+ let gateway: Ipv4Addr = *matches.get_one("opte_gateway").unwrap();
+ let opte_ip: &IpAddr = matches.get_one("opte_ip").unwrap();
+
+ info!(&log, "Creating gateway on the OPTE IP interface if it doesn't already exist"; "OPTE interface" => ?interface);
+ Ipadm::create_opte_gateway(interface)
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Ensuring there is a gateway route"; "OPTE gateway" => ?gateway, "OPTE interface" => ?interface, "OPTE IP" => ?opte_ip);
+ Route::ensure_opte_route(&gateway, interface, &opte_ip)
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ info!(&log, "Ensuring there is a default route"; "gateway" => ?gateway);
+ Route::ensure_default_route_with_gateway(Gateway::Ipv4(gateway))
+ .map_err(|err| CmdError::Failure(anyhow!(err)))?;
+
+ Ok(())
+}