diff --git a/Cargo.lock b/Cargo.lock index 42d94ba71f8..c7034a2e7d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2387,6 +2387,16 @@ name = "config" version = "1.0.0" dependencies = [ "anyhow", + "clap 4.5.17", + "ic-types", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_with 1.14.0", + "tempfile", + "url", + "utils", ] [[package]] @@ -18785,6 +18795,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap 4.5.17", + "config", "partition_tools", "serde", "serde_json", @@ -18792,7 +18803,6 @@ dependencies = [ "tempfile", "tokio", "url", - "utils", ] [[package]] @@ -20902,11 +20912,6 @@ name = "utils" version = "1.0.0" dependencies = [ "anyhow", - "once_cell", - "serde", - "serde_json", - "serde_with 1.14.0", - "url", ] [[package]] diff --git a/ic-os/components/init/bootstrap-ic-node/guestos/bootstrap-ic-node.sh b/ic-os/components/init/bootstrap-ic-node/guestos/bootstrap-ic-node.sh index c84b7d5ed19..6b1f5f8b134 100755 --- a/ic-os/components/init/bootstrap-ic-node/guestos/bootstrap-ic-node.sh +++ b/ic-os/components/init/bootstrap-ic-node/guestos/bootstrap-ic-node.sh @@ -135,6 +135,10 @@ write_metric_attr "guestos_boot_action" \ "GuestOS boot action" \ "gauge" +# /boot/config/CONFIGURED serves as a tag to indicate that the one-time bootstrap configuration has been completed. +# If the `/boot/config/CONFIGURED` file is not present, the boot sequence will +# search for a virtual USB stick (the bootstrap config image) +# containing the injected configuration files, and create the file. if [ -f /boot/config/CONFIGURED ]; then echo "Bootstrap completed already" fi diff --git a/ic-os/docs/Configuration.adoc b/ic-os/docs/Configuration.adoc index 2f5824961f3..d505d0359b3 100644 --- a/ic-os/docs/Configuration.adoc +++ b/ic-os/docs/Configuration.adoc @@ -20,7 +20,7 @@ SetupOS validates, sanitizes, and copies all of its configuration files to the H deployment.json # Deployment-specific configurations nns_public_key.pem # NNS public key -Refer to link:../components/setupos-scripts/config.sh[config.sh] link:../components/setupos-scripts/setup-hostos-config.sh[setup-hostos-config.sh] +Refer to link:../../rs/ic_os/config/README.md[rs/ic_os/config] & link:../components/setupos-scripts/setup-hostos-config.sh[setup-hostos-config.sh] === HostOS -> GuestOS diff --git a/ic-os/guestos/docs/ConfigStore.adoc b/ic-os/guestos/docs/ConfigStore.adoc index 5044df14987..f0d0c8e382d 100644 --- a/ic-os/guestos/docs/ConfigStore.adoc +++ b/ic-os/guestos/docs/ConfigStore.adoc @@ -1,6 +1,6 @@ = GuestOS Config Store -This document describes the contents of the GuestOS *config* partition (*/dev/vda3* in the GuestOS disk image). The config partition stores information that must be preserved across system upgrades and needs to be available during early boot time. Consequently, this information cannot reside within the encrypted payload data partition. +This document calls out some of the contents of the GuestOS *config* partition (*/dev/vda3* in the GuestOS disk image). The config partition stores information that must be preserved across system upgrades and needs to be available during early boot time. Consequently, this information cannot reside within the encrypted payload data partition. Currently, all contents in the config partition are stored as plain-text without integrity protection. @@ -22,108 +22,10 @@ This file contains the key material used to derive the wrapping key for all bloc In the absence of a sealing key (which will be available in SEV-protected trusted execution environments), the `store.keyfile` is stored as plain-text. Once a sealing key becomes available, it should be used to wrap the contents of this file. -=== ssh/ - -This directory contains SSH host keys. These keys must be persisted across upgrades and are transferred to `/etc/ssh` during the boot process. - -=== node_exporter/ - -This directory contains the Node Exporter TLS keys. These keys must be persisted across upgrades and are transferred to `/etc/node_exporter` during the boot process. - -=== accounts_ssh_authorized_keys/ - -This directory contains individual files named `admin`, `backup`, `readonly` and `root`. The contents of these files serve as `authorized_keys` for their respective role account. This means that, for example, `accounts_ssh_authorized_keys/admin` is transferred to `~admin/.ssh/authorized_keys` on the target system. - -* *admin*: ... -* *backup / readonly*: These files can only be modified via an NNS proposal, and are in place for subnet recovery or issue debugging purposes. -* *root*: Used for development/debug builds only. - -This directory and any file in it is optional. By default, no authorized key is installed for any account, meaning no one has SSH access to the GuestOS. - -=== node_operator_private_key.pem - -This file contains the Node Operator private key, which is registered with the NNS and used to sign the IC join request. The private key can be generated using one of the following commands: - - dfx identity new --disable-encryption node_operator - cp ~/.config/dfx/identity/node_operator/identity.pem ./node_operator_private_key.pem - -Or - - quill generate --pem-file node_operator_private_key.pem - -=== network.conf - -Network configuration parameters. - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- *ipv6_address*: The IPv6 address of the node, used for the node to "identify" itself (via the registry). All public IC services are offered through this address, which will be assigned to the enp1s0 interface. It is used as the "private" management access to the node. If left blank, SLAAC is used on the interface. - -- *ipv6_gateway*: The default IPv6 gateway, only meaningful if ipv6_address is also provided. - -- *hostname*: The hostname, which can be any text in principle but is generally derived from the ID of the physical host (e.g., MAC address). - -Note: if this file is not given, the system will fall back to network auto configuration. - -=== nns.conf - -The IP address(es) of NNS node(s). - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- *nns_url*: The URL (HTTP) of the NNS node(s). If multiple URLs are provided, separate them with whitespace. If this key is not specified, http://127.0.0.1:8080 is assumed by default (which only works for nodes that do not require registration). - -This configuration is used when generating the replica configuration to fill in the nns_url placeholder. - -=== nns_public_key.pem - -This file must be a text file containing the public key of the NNS to be used. - == Development configuration files These configuration files should only be used for development and testing purposes. -=== backup.conf - -This file configures the usage of the backup spool directory. - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- *backup_retention_time_secs*: The maximum age of any file or directory kept in the backup spool. - -- *backup_purging_interval_secs*: The interval at which the backup spool directory will be scanned for files to delete. - -This configuration file should only be used for testnet deployments (to achieve shorter retention times) and must be missing for production deployments, where suitable production default values are assumed. - -=== filebeat.conf - -Configures filebeat to export logs out of the system. - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- elasticsearch_hosts: Space-separated lists of hosts to ship logs to. -- elasticsearch_tags: Space-separated list of tags to apply to exported log records. - -If left unspecified, filebeat will be left unconfigured and no logs are exported. - -=== socks_proxy.conf - -Configuration for socks proxy. - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- socks_proxy: URL of the socks proxy to use. E.g socks5://socksproxy.com:1080 - -=== bitcoin_addr.conf - -Configuration for bitcoin adapter. - -Must be a file of key/value pairs separated by "=" (one per line) with the following possible keys: - -- bitcoind_addr: Address of the bitcoind to be contacted by bitcoin adapter service. - -If left unspecified, the bitcoin adapter will not work properly due to lack of external system to contact. - == Injecting external state *Typical bootstrap process:* On first boot, the system will perform technical initialization (filesystems, etc.) and afterwards, initialize itself to act as a node in the IC. The node is initialized using key generation on the node itself (such that the private key never leaves the node) and through joining the IC (the node gets the rest of its state via joining the IC). "Registration" to the target IC is initiated by the node itself by sending a Node Operator-signed "join" request to its NNS. @@ -141,14 +43,3 @@ This behavior is suitable for the following use cases: - Externally controlled join of a node to a subnet: In this case, ic-prep is used to prepare key material to the node, while ic-admin is used to modify the target NNS such that it "accepts" the new node as part of the IC. -=== ic_crypto - -Externally generated cryptographic keys. - -Must be a directory with contents matching the internal representation of the ic_crypto directory. When given, this provides the private keys of the node. If not given, the node will generate its own private/public key pair. - -=== ic_registry_local_store - -Initial registry state. - -Must be a directory with contents matching the internal representation of the ic_registry_local_store. When given, this provides the initial state of the registry. If not given, the node will fetch (initial) registry state from the NNS. diff --git a/rs/ic_os/config/BUILD.bazel b/rs/ic_os/config/BUILD.bazel index e90ec656252..2438720360d 100644 --- a/rs/ic_os/config/BUILD.bazel +++ b/rs/ic_os/config/BUILD.bazel @@ -1,20 +1,54 @@ -load("@rules_rust//rust:defs.bzl", "rust_library") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test") package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. + "//rs/ic_os/utils", + "//rs/types/types", "@crate_index//:anyhow", + "@crate_index//:clap", + "@crate_index//:regex", + "@crate_index//:serde", + "@crate_index//:serde_json", + "@crate_index//:serde_with", + "@crate_index//:url", ] +DEV_DEPENDENCIES = [ + # Keep sorted. + "@crate_index//:once_cell", + "@crate_index//:tempfile", +] + +MACRO_DEPENDENCIES = [] + +ALIASES = {} + rust_library( - name = "config", + name = "config_lib", srcs = glob( ["src/**/*.rs"], + exclude = ["src/main.rs"], ), - aliases = {}, crate_name = "config", edition = "2021", - proc_macro_deps = [], deps = DEPENDENCIES, ) + +rust_binary( + name = "config", + srcs = ["src/main.rs"], + aliases = ALIASES, + crate_name = "config", + edition = "2021", + proc_macro_deps = MACRO_DEPENDENCIES, + deps = [":config_lib"] + DEPENDENCIES, +) + +rust_test( + name = "config_lib_test", + crate = ":config_lib", + # You may add other deps that are specific to the test configuration + deps = DEV_DEPENDENCIES, +) diff --git a/rs/ic_os/config/Cargo.toml b/rs/ic_os/config/Cargo.toml index fcc53eb1e5e..811e805c465 100644 --- a/rs/ic_os/config/Cargo.toml +++ b/rs/ic_os/config/Cargo.toml @@ -5,3 +5,23 @@ edition = "2021" [dependencies] anyhow = { workspace = true } +ic-types = { path = "../../types/types" } +clap = { workspace = true } +utils = { path = "../utils" } +url = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +serde_with = "1.6.2" +regex = { workspace = true } + +[dev-dependencies] +once_cell = "1.8" +tempfile = { workspace = true } + +[lib] +name = "config" +path = "src/lib.rs" + +[[bin]] +name = "config" +path = "src/main.rs" diff --git a/rs/ic_os/config/README.md b/rs/ic_os/config/README.md new file mode 100644 index 00000000000..53567a8c9f6 --- /dev/null +++ b/rs/ic_os/config/README.md @@ -0,0 +1,11 @@ +# IC-OS Config + +IC-OS Config is responsible for managing the configuration of IC-OS images. + +SetupOS transforms user-facing configuration files (like `config.ini`, `deployment.json`, etc.) into a SetupOSConfig struct. Then, in production, configuration is propagated from SetupOS → HostOS → GuestOS (→ replica) via the HostOSConfig and GuestOSConfig structures. + +All access to configuration and the config partition should go through the config structures. + +For testing, IC-OS Config is also used to create HostOS and GuestOS configuration directly. + +For details on the IC-OS configuration mechanism, refer to [ic-os/docs/Configuration.adoc](../../../ic-os/docs/Configuration.adoc) \ No newline at end of file diff --git a/rs/ic_os/config/src/config_ini.rs b/rs/ic_os/config/src/config_ini.rs new file mode 100644 index 00000000000..d90a77d95be --- /dev/null +++ b/rs/ic_os/config/src/config_ini.rs @@ -0,0 +1,334 @@ +use regex::Regex; +use std::collections::HashMap; +use std::fs::read_to_string; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::path::Path; + +use anyhow::bail; +use anyhow::{Context, Result}; + +pub type ConfigMap = HashMap; +pub struct ConfigIniSettings { + pub ipv6_prefix: Option, + pub ipv6_address: Option, + pub ipv6_prefix_length: u8, + pub ipv6_gateway: Ipv6Addr, + pub ipv4_address: Option, + pub ipv4_gateway: Option, + pub ipv4_prefix_length: Option, + pub domain: Option, + pub verbose: bool, +} + +// Prefix should have a max length of 19 ("1234:6789:1234:6789") +// It could have fewer characters though. Parsing as an ip address with trailing '::' should work. +fn is_valid_ipv6_prefix(ipv6_prefix: &str) -> bool { + ipv6_prefix.len() <= 19 && format!("{ipv6_prefix}::").parse::().is_ok() +} + +pub fn get_config_ini_settings(config_file_path: &Path) -> Result { + let config_map: ConfigMap = config_map_from_path(config_file_path)?; + + let ipv6_prefix = config_map + .get("ipv6_prefix") + .map(|prefix| { + if !is_valid_ipv6_prefix(prefix) { + bail!("Invalid ipv6 prefix: {}", prefix); + } + Ok(prefix.clone()) + }) + .transpose()?; + + // Per PFOPS - ipv6_prefix_length will always be 64 + let ipv6_prefix_length = 64_u8; + + // Optional ipv6_address - for testing. Takes precedence over ipv6_prefix. + let ipv6_address = config_map + .get("ipv6_address") + .map(|address| { + // ipv6_address might be formatted with the trailing suffix. Remove it. + address + .strip_suffix(&format!("/{}", ipv6_prefix_length)) + .unwrap_or(address) + .parse::() + .context(format!("Invalid IPv6 address: {}", address)) + }) + .transpose()?; + + if ipv6_address.is_none() && ipv6_prefix.is_none() { + bail!("Missing config parameter: need at least one of ipv6_prefix or ipv6_address"); + } + + let ipv6_gateway = config_map + .get("ipv6_gateway") + .context("Missing config parameter: ipv6_gateway")? + .parse::() + .context("Invalid IPv6 gateway address")?; + + let ipv4_address = config_map + .get("ipv4_address") + .map(|address| { + address + .parse::() + .context(format!("Invalid IPv4 address: {}", address)) + }) + .transpose()?; + + let ipv4_gateway = config_map + .get("ipv4_gateway") + .map(|address| { + address + .parse::() + .context(format!("Invalid IPv4 gateway: {}", address)) + }) + .transpose()?; + + let ipv4_prefix_length = config_map + .get("ipv4_prefix_length") + .map(|prefix| { + let prefix = prefix + .parse::() + .context(format!("Invalid IPv4 prefix length: {}", prefix))?; + if prefix > 32 { + bail!( + "IPv4 prefix length must be between 0 and 32, got {}", + prefix + ); + } + Ok(prefix) + }) + .transpose()?; + + let domain = config_map.get("domain").cloned(); + + let verbose = config_map + .get("verbose") + .is_some_and(|s| s.eq_ignore_ascii_case("true")); + + Ok(ConfigIniSettings { + ipv6_prefix, + ipv6_address, + ipv6_prefix_length, + ipv6_gateway, + ipv4_address, + ipv4_gateway, + ipv4_prefix_length, + domain, + verbose, + }) +} + +fn parse_config_line(line: &str) -> Option<(String, String)> { + // Skip blank lines and comments + if line.is_empty() || line.trim().starts_with('#') { + return None; + } + + let parts: Vec<&str> = line.splitn(2, '=').collect(); + if parts.len() == 2 { + Some((parts[0].trim().into(), parts[1].trim().into())) + } else { + eprintln!("Warning: skipping config line due to unrecognized format: \"{line}\""); + eprintln!("Expected format: \"=\""); + None + } +} + +pub fn config_map_from_path(config_file_path: &Path) -> Result { + let file_contents = read_to_string(config_file_path) + .with_context(|| format!("Error reading file: {}", config_file_path.display()))?; + + let normalized_file_contents = normalize_contents(&file_contents); + + Ok(normalized_file_contents + .lines() + .filter_map(parse_config_line) + .collect()) +} + +fn normalize_contents(contents: &str) -> String { + let mut normalized_contents = contents.replace("\r\n", "\n").replace("\r", "\n"); + + let comment_regex = Regex::new(r"#.*$").unwrap(); + normalized_contents = comment_regex + .replace_all(&normalized_contents, "") + .to_string(); + + normalized_contents = normalized_contents.replace("\"", "").replace("'", ""); + + normalized_contents = normalized_contents.to_lowercase(); + + let empty_line_regex = Regex::new(r"^\s*$\n?").unwrap(); + normalized_contents = empty_line_regex + .replace_all(&normalized_contents, "") + .to_string(); + + if !normalized_contents.ends_with('\n') { + normalized_contents.push('\n'); + } + + normalized_contents +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_is_valid_ipv6_prefix() { + // Valid prefixes + assert!(is_valid_ipv6_prefix("2a00:1111:1111:1111")); + assert!(is_valid_ipv6_prefix("2a00:111:11:11")); + assert!(is_valid_ipv6_prefix("2602:fb2b:100:10")); + + // Invalid prefixes + assert!(!is_valid_ipv6_prefix("2a00:1111:1111:1111:")); // Trailing colon + assert!(!is_valid_ipv6_prefix("2a00:1111:1111:1111:1111:1111")); // Too long + assert!(!is_valid_ipv6_prefix("abcd::1234:5678")); // Contains "::" + } + + #[test] + fn test_parse_config_line() { + assert_eq!( + parse_config_line("key=value"), + Some(("key".to_string(), "value".to_string())) + ); + assert_eq!( + parse_config_line(" key = value "), + Some(("key".to_string(), "value".to_string())) + ); + assert_eq!(parse_config_line(""), None); + assert_eq!(parse_config_line("# this is a comment"), None); + assert_eq!(parse_config_line("keywithoutvalue"), None); + assert_eq!( + parse_config_line("key=value=extra"), + Some(("key".to_string(), "value=extra".to_string())) + ); + } + + #[test] + fn test_config_map_from_path() -> Result<()> { + let mut temp_file = NamedTempFile::new()?; + let file_path = temp_file.path().to_path_buf(); + + writeln!(temp_file, "key1=value1")?; + writeln!(temp_file, "key2=value2")?; + writeln!(temp_file, "# This is a comment")?; + writeln!(temp_file, "key3=value3")?; + writeln!(temp_file)?; + + let config_map = config_map_from_path(&file_path)?; + + assert_eq!(config_map.get("key1"), Some(&"value1".to_string())); + assert_eq!(config_map.get("key2"), Some(&"value2".to_string())); + assert_eq!(config_map.get("key3"), Some(&"value3".to_string())); + assert_eq!(config_map.get("bad_key"), None); + + Ok(()) + } + + #[test] + fn test_config_map_from_path_crlf() -> Result<()> { + let mut temp_file = NamedTempFile::new()?; + let file_path = temp_file.path().to_path_buf(); + + writeln!(temp_file, "key4=value4\r\nkey5=value5\r\n")?; + + let config_map = config_map_from_path(&file_path)?; + + assert_eq!(config_map.get("key4"), Some(&"value4".to_string())); + assert_eq!(config_map.get("key5"), Some(&"value5".to_string())); + assert_eq!(config_map.get("bad_key"), None); + + Ok(()) + } + + #[test] + fn test_get_config_ini_settings() -> Result<()> { + // Test valid config.ini + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "\n\t\r")?; + writeln!(temp_file, "# COMMENT ")?; + writeln!(temp_file, "BAD INPUT ")?; + writeln!(temp_file, "\n\n\n\n")?; + writeln!(temp_file, "ipv6_prefix=2a00:fb01:400:200")?; + writeln!(temp_file, "ipv6_address=2a00:fb01:400:200::/64")?; + writeln!(temp_file, "ipv6_gateway=2a00:fb01:400:200::1")?; + writeln!(temp_file, "ipv4_address=212.71.124.178")?; + writeln!(temp_file, "ipv4_gateway=212.71.124.177")?; + writeln!(temp_file, "ipv4_prefix_length=28")?; + writeln!(temp_file, "domain=example.com")?; + writeln!(temp_file, "verbose=false")?; + + let temp_file_path = temp_file.path(); + + let config_ini_settings = get_config_ini_settings(temp_file_path)?; + + assert_eq!( + config_ini_settings.ipv6_prefix.unwrap(), + "2a00:fb01:400:200".to_string() + ); + assert_eq!( + config_ini_settings.ipv6_address.unwrap(), + "2a00:fb01:400:200::".parse::()? + ); + assert_eq!( + config_ini_settings.ipv6_gateway, + "2a00:fb01:400:200::1".parse::()? + ); + assert_eq!(config_ini_settings.ipv6_prefix_length, 64); + assert_eq!( + config_ini_settings.ipv4_address.unwrap(), + "212.71.124.178".parse::()? + ); + assert_eq!( + config_ini_settings.ipv4_gateway.unwrap(), + "212.71.124.177".parse::()? + ); + assert_eq!(config_ini_settings.ipv4_prefix_length.unwrap(), 28); + assert_eq!(config_ini_settings.domain, Some("example.com".to_string())); + assert!(!config_ini_settings.verbose); + + // Test ipv6_address without ipv6_prefix_length length + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "ipv6_address=2a00:fb01:400:200::")?; + let config_ini_settings = get_config_ini_settings(temp_file_path)?; + assert_eq!( + config_ini_settings.ipv6_address.unwrap(), + "2a00:fb01:400:200::".parse::()? + ); + assert_eq!(config_ini_settings.ipv6_prefix_length, 64); + + // Test missing ipv6 + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "ipv4_address=212.71.124.178")?; + writeln!(temp_file, "ipv4_gateway=212.71.124.177")?; + writeln!(temp_file, "ipv4_prefix_length=28")?; + + let temp_file_path = temp_file.path(); + let result = get_config_ini_settings(temp_file_path); + assert!(result.is_err()); + + // Test invalid IPv6 address + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "ipv6_prefix=invalid_ipv6_prefix")?; + writeln!(temp_file, "ipv6_gateway=2001:db8:85a3:0000::1")?; + writeln!(temp_file, "ipv4_address=192.168.1.1")?; + writeln!(temp_file, "ipv4_gateway=192.168.1.254")?; + writeln!(temp_file, "ipv4_prefix_length=24")?; + + let temp_file_path = temp_file.path(); + let result = get_config_ini_settings(temp_file_path); + assert!(result.is_err()); + + // Test missing prefix and address + let mut temp_file = NamedTempFile::new()?; + writeln!(temp_file, "ipv6_gateway=2001:db8:85a3:0000::1")?; + let result = get_config_ini_settings(temp_file_path); + assert!(result.is_err()); + + Ok(()) + } +} diff --git a/rs/ic_os/utils/src/deployment.rs b/rs/ic_os/config/src/deployment_json.rs similarity index 85% rename from rs/ic_os/utils/src/deployment.rs rename to rs/ic_os/config/src/deployment_json.rs index e8092aed20f..94c411f4068 100644 --- a/rs/ic_os/utils/src/deployment.rs +++ b/rs/ic_os/config/src/deployment_json.rs @@ -7,7 +7,7 @@ use serde_with::{serde_as, DisplayFromStr}; use url::Url; #[derive(PartialEq, Debug, Deserialize, Serialize)] -pub struct DeploymentJson { +pub struct DeploymentSettings { pub deployment: Deployment, pub logging: Logging, pub nns: Nns, @@ -42,7 +42,7 @@ pub struct Resources { pub cpu: Option, } -pub fn read_deployment_file(deployment_json: &Path) -> Result { +pub fn get_deployment_settings(deployment_json: &Path) -> Result { let file = File::open(deployment_json).context("failed to open deployment config file")?; serde_json::from_reader(&file).context("Invalid json content") } @@ -119,7 +119,7 @@ mod test { } }"#; - static DEPLOYMENT_STRUCT: Lazy = Lazy::new(|| { + static DEPLOYMENT_STRUCT: Lazy = Lazy::new(|| { let hosts = [ "elasticsearch-node-0.mercury.dfinity.systems:443", "elasticsearch-node-1.mercury.dfinity.systems:443", @@ -127,7 +127,7 @@ mod test { "elasticsearch-node-3.mercury.dfinity.systems:443", ] .join(" "); - DeploymentJson { + DeploymentSettings { deployment: Deployment { name: "mainnet".to_string(), mgmt_mac: None, @@ -159,7 +159,7 @@ mod test { } }"#; - static DEPLOYMENT_STRUCT_NO_MGMT_MAC: Lazy = Lazy::new(|| { + static DEPLOYMENT_STRUCT_NO_MGMT_MAC: Lazy = Lazy::new(|| { let hosts = [ "elasticsearch-node-0.mercury.dfinity.systems:443", "elasticsearch-node-1.mercury.dfinity.systems:443", @@ -167,7 +167,7 @@ mod test { "elasticsearch-node-3.mercury.dfinity.systems:443", ] .join(" "); - DeploymentJson { + DeploymentSettings { deployment: Deployment { name: "mainnet".to_string(), mgmt_mac: None, @@ -198,7 +198,7 @@ mod test { } }"#; - static DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC: Lazy = Lazy::new(|| { + static DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC: Lazy = Lazy::new(|| { let hosts = [ "elasticsearch-node-0.mercury.dfinity.systems:443", "elasticsearch-node-1.mercury.dfinity.systems:443", @@ -206,7 +206,7 @@ mod test { "elasticsearch-node-3.mercury.dfinity.systems:443", ] .join(" "); - DeploymentJson { + DeploymentSettings { deployment: Deployment { name: "mainnet".to_string(), mgmt_mac: None, @@ -238,7 +238,7 @@ mod test { } }"#; - static QEMU_CPU_DEPLOYMENT_STRUCT: Lazy = Lazy::new(|| { + static QEMU_CPU_DEPLOYMENT_STRUCT: Lazy = Lazy::new(|| { let hosts = [ "elasticsearch-node-0.mercury.dfinity.systems:443", "elasticsearch-node-1.mercury.dfinity.systems:443", @@ -246,7 +246,7 @@ mod test { "elasticsearch-node-3.mercury.dfinity.systems:443", ] .join(" "); - DeploymentJson { + DeploymentSettings { deployment: Deployment { name: "mainnet".to_string(), mgmt_mac: None, @@ -292,7 +292,7 @@ mod test { } }"#; - static MULTI_URL_STRUCT: Lazy = Lazy::new(|| { + static MULTI_URL_STRUCT: Lazy = Lazy::new(|| { let hosts = [ "elasticsearch-node-0.mercury.dfinity.systems:443", "elasticsearch-node-1.mercury.dfinity.systems:443", @@ -300,7 +300,7 @@ mod test { "elasticsearch-node-3.mercury.dfinity.systems:443", ] .join(" "); - DeploymentJson { + DeploymentSettings { deployment: Deployment { name: "mainnet".to_string(), mgmt_mac: None, @@ -322,27 +322,24 @@ mod test { #[test] fn deserialize_deployment() { - let parsed_deployment: DeploymentJson = { serde_json::from_str(DEPLOYMENT_STR).unwrap() }; + let parsed_deployment = { serde_json::from_str(DEPLOYMENT_STR).unwrap() }; assert_eq!(*DEPLOYMENT_STRUCT, parsed_deployment); - let parsed_deployment: DeploymentJson = - { serde_json::from_str(DEPLOYMENT_STR_NO_MGMT_MAC).unwrap() }; + let parsed_deployment = { serde_json::from_str(DEPLOYMENT_STR_NO_MGMT_MAC).unwrap() }; assert_eq!(*DEPLOYMENT_STRUCT_NO_MGMT_MAC, parsed_deployment); - let parsed_deployment: DeploymentJson = + let parsed_deployment = { serde_json::from_str(DEPLOYMENT_STR_NO_CPU_NO_MGMT_MAC).unwrap() }; assert_eq!(*DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC, parsed_deployment); - let parsed_cpu_deployment: DeploymentJson = - { serde_json::from_str(QEMU_CPU_DEPLOYMENT_STR).unwrap() }; + let parsed_cpu_deployment = { serde_json::from_str(QEMU_CPU_DEPLOYMENT_STR).unwrap() }; assert_eq!(*QEMU_CPU_DEPLOYMENT_STRUCT, parsed_cpu_deployment); - let parsed_multi_url_deployment: DeploymentJson = - { serde_json::from_str(MULTI_URL_STR).unwrap() }; + let parsed_multi_url_deployment = { serde_json::from_str(MULTI_URL_STR).unwrap() }; assert_eq!(*MULTI_URL_STRUCT, parsed_multi_url_deployment); @@ -350,7 +347,7 @@ mod test { // slash, so the above case parses with a slash for the sake of the // writeback test below. In practice, we have used addresses without // this slash, so here we verify that this parses to the same value. - let parsed_multi_url_sans_slash_deployment: DeploymentJson = + let parsed_multi_url_sans_slash_deployment = { serde_json::from_str(MULTI_URL_SANS_SLASH_STR).unwrap() }; assert_eq!(*MULTI_URL_STRUCT, parsed_multi_url_sans_slash_deployment); @@ -358,44 +355,39 @@ mod test { // Exercise DeserializeOwned using serde_json::from_value. // DeserializeOwned is used by serde_json::from_reader, which is the // main entrypoint of this code, in practice. - let parsed_deployment: DeploymentJson = - { serde_json::from_value(DEPLOYMENT_VALUE.clone()).unwrap() }; + let parsed_deployment = { serde_json::from_value(DEPLOYMENT_VALUE.clone()).unwrap() }; assert_eq!(*DEPLOYMENT_STRUCT, parsed_deployment); } #[test] fn serialize_deployment() { - let serialized_deployment = - serde_json::to_string_pretty::(&DEPLOYMENT_STRUCT).unwrap(); + let serialized_deployment = serde_json::to_string_pretty(&*DEPLOYMENT_STRUCT).unwrap(); // DEPLOYMENT_STRUCT serializes to DEPLOYMENT_STR_NO_MGMT_MAC because mgmt_mac field is skipped in serialization assert_eq!(DEPLOYMENT_STR_NO_MGMT_MAC, serialized_deployment); let serialized_deployment = - serde_json::to_string_pretty::(&DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC) - .unwrap(); + serde_json::to_string_pretty(&*DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC).unwrap(); assert_eq!(DEPLOYMENT_STR_NO_CPU_NO_MGMT_MAC, serialized_deployment); let serialized_deployment = - serde_json::to_string_pretty::(&DEPLOYMENT_STRUCT_NO_MGMT_MAC).unwrap(); + serde_json::to_string_pretty(&*DEPLOYMENT_STRUCT_NO_MGMT_MAC).unwrap(); assert_eq!(DEPLOYMENT_STR_NO_MGMT_MAC, serialized_deployment); let serialized_deployment = - serde_json::to_string_pretty::(&DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC) - .unwrap(); + serde_json::to_string_pretty(&*DEPLOYMENT_STRUCT_NO_CPU_NO_MGMT_MAC).unwrap(); assert_eq!(DEPLOYMENT_STR_NO_CPU_NO_MGMT_MAC, serialized_deployment); let serialized_deployment = - serde_json::to_string_pretty::(&QEMU_CPU_DEPLOYMENT_STRUCT).unwrap(); + serde_json::to_string_pretty(&*QEMU_CPU_DEPLOYMENT_STRUCT).unwrap(); assert_eq!(QEMU_CPU_DEPLOYMENT_STR, serialized_deployment); - let serialized_deployment = - serde_json::to_string_pretty::(&MULTI_URL_STRUCT).unwrap(); + let serialized_deployment = serde_json::to_string_pretty(&*MULTI_URL_STRUCT).unwrap(); assert_eq!(MULTI_URL_STR, serialized_deployment); } diff --git a/rs/ic_os/config/src/lib.rs b/rs/ic_os/config/src/lib.rs index ef505296d2c..c49f689cb53 100644 --- a/rs/ic_os/config/src/lib.rs +++ b/rs/ic_os/config/src/lib.rs @@ -1,38 +1,138 @@ -use std::collections::HashMap; -use std::fs::read_to_string; -use std::path::Path; +pub mod config_ini; +pub mod deployment_json; +pub mod types; use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs::{create_dir_all, File}; +use std::io::Write; +use std::path::Path; -pub type ConfigMap = HashMap; - -pub static DEFAULT_SETUPOS_CONFIG_FILE_PATH: &str = "/var/ic/config/config.ini"; +pub static DEFAULT_SETUPOS_CONFIG_OBJECT_PATH: &str = "/var/ic/config/config.json"; +pub static DEFAULT_SETUPOS_CONFIG_INI_FILE_PATH: &str = "/config/config.ini"; pub static DEFAULT_SETUPOS_DEPLOYMENT_JSON_PATH: &str = "/data/deployment.json"; +pub static DEFAULT_SETUPOS_NNS_PUBLIC_KEY_PATH: &str = "/data/nns_public_key.pem"; +pub static DEFAULT_SETUPOS_SSH_AUTHORIZED_KEYS_PATH: &str = "/config/ssh_authorized_keys"; +pub static DEFAULT_SETUPOS_NODE_OPERATOR_PRIVATE_KEY_PATH: &str = + "/config/node_operator_private_key.pem"; + +pub static DEFAULT_SETUPOS_HOSTOS_CONFIG_OBJECT_PATH: &str = "/var/ic/config/config-hostos.json"; -pub static DEFAULT_HOSTOS_CONFIG_FILE_PATH: &str = "/boot/config/config.ini"; +pub static DEFAULT_HOSTOS_CONFIG_INI_FILE_PATH: &str = "/boot/config/config.ini"; pub static DEFAULT_HOSTOS_DEPLOYMENT_JSON_PATH: &str = "/boot/config/deployment.json"; -fn parse_config_line(line: &str) -> Option<(String, String)> { - // Skip blank lines and comments - if line.is_empty() || line.trim().starts_with('#') { - return None; - } +pub fn serialize_and_write_config(path: &Path, config: &T) -> Result<()> { + let serialized_config = + serde_json::to_string_pretty(config).expect("Failed to serialize configuration"); - let parts: Vec<&str> = line.splitn(2, '=').collect(); - if parts.len() == 2 { - Some((parts[0].trim().into(), parts[1].trim().into())) - } else { - eprintln!("Warning: skipping config line due to unrecognized format: \"{line}\""); - eprintln!("Expected format: \"=\""); - None + if let Some(parent) = path.parent() { + create_dir_all(parent)?; } + + let mut file = File::create(path)?; + file.write_all(serialized_config.as_bytes())?; + Ok(()) +} + +pub fn deserialize_config Deserialize<'de>>(file_path: &str) -> Result { + let file = File::open(file_path).context(format!("Failed to open file: {}", file_path))?; + serde_json::from_reader(file).context(format!( + "Failed to deserialize JSON from file: {}", + file_path + )) } -pub fn config_map_from_path(config_file_path: &Path) -> Result { - let file_contents = read_to_string(config_file_path) - .with_context(|| format!("Error reading file: {}", config_file_path.display()))?; - Ok(file_contents - .lines() - .filter_map(parse_config_line) - .collect()) +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use types::{ + GuestOSConfig, GuestOSSettings, GuestosDevConfig, HostOSConfig, HostOSSettings, + ICOSSettings, Logging, NetworkSettings, SetupOSConfig, SetupOSSettings, + }; + + #[test] + fn test_serialize_and_deserialize() { + let network_settings = NetworkSettings { + ipv6_prefix: None, + ipv6_address: None, + ipv6_prefix_length: 64_u8, + ipv6_gateway: "2001:db8::1".parse().unwrap(), + ipv4_address: None, + ipv4_gateway: None, + ipv4_prefix_length: None, + domain: None, + mgmt_mac: None, + }; + let logging = Logging { + elasticsearch_hosts: [ + "elasticsearch-node-0.mercury.dfinity.systems:443", + "elasticsearch-node-1.mercury.dfinity.systems:443", + "elasticsearch-node-2.mercury.dfinity.systems:443", + "elasticsearch-node-3.mercury.dfinity.systems:443", + ] + .join(" "), + elasticsearch_tags: None, + }; + let icos_settings = ICOSSettings { + logging, + nns_public_key_path: PathBuf::from("/path/to/key"), + nns_urls: vec!["http://localhost".parse().unwrap()], + hostname: "mainnet".to_string(), + node_operator_private_key_path: None, + ssh_authorized_keys_path: None, + }; + let setupos_settings = SetupOSSettings; + let hostos_settings = HostOSSettings { + vm_memory: 490, + vm_cpu: "kvm".to_string(), + verbose: false, + }; + let guestos_settings = GuestOSSettings { + ic_crypto_path: None, + ic_state_path: None, + ic_registry_local_store_path: None, + guestos_dev: GuestosDevConfig::default(), + }; + + let setupos_config_struct = SetupOSConfig { + network_settings: network_settings.clone(), + icos_settings: icos_settings.clone(), + setupos_settings: setupos_settings.clone(), + hostos_settings: hostos_settings.clone(), + guestos_settings: guestos_settings.clone(), + }; + let hostos_config_struct = HostOSConfig { + network_settings: network_settings.clone(), + icos_settings: icos_settings.clone(), + hostos_settings: hostos_settings.clone(), + guestos_settings: guestos_settings.clone(), + }; + let guestos_config_struct = GuestOSConfig { + network_settings: network_settings.clone(), + icos_settings: icos_settings.clone(), + guestos_settings: guestos_settings.clone(), + }; + + fn serialize_and_deserialize(config: &T) + where + T: serde::Serialize + + serde::de::DeserializeOwned + + std::cmp::PartialEq + + std::fmt::Debug, + { + // Test serialization + let buffer = serde_json::to_vec_pretty(config).expect("Failed to serialize config"); + assert!(!buffer.is_empty()); + + // Test deserialization + let deserialized_config: T = + serde_json::from_slice(&buffer).expect("Failed to deserialize config"); + assert_eq!(*config, deserialized_config); + } + + serialize_and_deserialize(&setupos_config_struct); + serialize_and_deserialize(&hostos_config_struct); + serialize_and_deserialize(&guestos_config_struct); + } } diff --git a/rs/ic_os/config/src/main.rs b/rs/ic_os/config/src/main.rs new file mode 100644 index 00000000000..65e825ee95c --- /dev/null +++ b/rs/ic_os/config/src/main.rs @@ -0,0 +1,171 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use config::config_ini::{get_config_ini_settings, ConfigIniSettings}; +use config::deployment_json::get_deployment_settings; +use config::serialize_and_write_config; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use config::types::{ + GuestOSSettings, HostOSConfig, HostOSSettings, ICOSSettings, Logging, NetworkSettings, + SetupOSConfig, SetupOSSettings, +}; + +#[derive(Subcommand)] +pub enum Commands { + /// Creates SetupOSConfig object + CreateSetuposConfig { + #[arg(long, default_value = config::DEFAULT_SETUPOS_CONFIG_INI_FILE_PATH, value_name = "config.ini")] + config_ini_path: PathBuf, + + #[arg(long, default_value = config::DEFAULT_SETUPOS_DEPLOYMENT_JSON_PATH, value_name = "deployment.json")] + deployment_json_path: PathBuf, + + #[arg(long, default_value = config::DEFAULT_SETUPOS_NNS_PUBLIC_KEY_PATH, value_name = "nns_public_key.pem")] + nns_public_key_path: PathBuf, + + #[arg(long, default_value = config::DEFAULT_SETUPOS_SSH_AUTHORIZED_KEYS_PATH, value_name = "ssh_authorized_keys")] + ssh_authorized_keys_path: PathBuf, + + #[arg(long, default_value = config::DEFAULT_SETUPOS_NODE_OPERATOR_PRIVATE_KEY_PATH, value_name = "node_operator_private_key.pem")] + node_operator_private_key_path: PathBuf, + + #[arg(long, default_value = config::DEFAULT_SETUPOS_CONFIG_OBJECT_PATH, value_name = "config.json")] + setupos_config_json_path: PathBuf, + }, + /// Creates HostOSConfig object from existing SetupOS config.json file + GenerateHostosConfig { + #[arg(long, default_value = config::DEFAULT_SETUPOS_CONFIG_OBJECT_PATH, value_name = "config.json")] + setupos_config_json_path: PathBuf, + #[arg(long, default_value = config::DEFAULT_SETUPOS_HOSTOS_CONFIG_OBJECT_PATH, value_name = "config-hostos.json")] + hostos_config_json_path: PathBuf, + }, +} + +#[derive(Parser)] +#[command()] +struct ConfigArgs { + #[command(subcommand)] + command: Option, +} + +pub fn main() -> Result<()> { + let opts = ConfigArgs::parse(); + + match opts.command { + Some(Commands::CreateSetuposConfig { + config_ini_path, + deployment_json_path, + nns_public_key_path, + ssh_authorized_keys_path, + node_operator_private_key_path, + setupos_config_json_path, + }) => { + // get config.ini settings + let config_ini_settings = get_config_ini_settings(&config_ini_path)?; + let ConfigIniSettings { + ipv6_prefix, + ipv6_address, + ipv6_prefix_length, + ipv6_gateway, + ipv4_address, + ipv4_gateway, + ipv4_prefix_length, + domain, + verbose, + } = config_ini_settings; + + // get deployment.json variables + let deployment_json_settings = get_deployment_settings(&deployment_json_path)?; + + let network_settings = NetworkSettings { + ipv6_prefix, + ipv6_address, + ipv6_prefix_length, + ipv6_gateway, + ipv4_address, + ipv4_gateway, + ipv4_prefix_length, + domain, + mgmt_mac: deployment_json_settings.deployment.mgmt_mac, + }; + + let logging = Logging { + elasticsearch_hosts: deployment_json_settings.logging.hosts.to_string(), + elasticsearch_tags: None, + }; + + let icos_settings = ICOSSettings { + logging, + nns_public_key_path: nns_public_key_path.to_path_buf(), + nns_urls: deployment_json_settings.nns.url.clone(), + hostname: deployment_json_settings.deployment.name.to_string(), + node_operator_private_key_path: node_operator_private_key_path + .exists() + .then_some(node_operator_private_key_path), + ssh_authorized_keys_path: ssh_authorized_keys_path + .exists() + .then_some(ssh_authorized_keys_path), + }; + + let setupos_settings = SetupOSSettings; + + let hostos_settings = HostOSSettings { + vm_memory: deployment_json_settings.resources.memory, + vm_cpu: deployment_json_settings + .resources + .cpu + .clone() + .unwrap_or("kvm".to_string()), + verbose, + }; + + let guestos_settings = GuestOSSettings::default(); + + let setupos_config = SetupOSConfig { + network_settings, + icos_settings, + setupos_settings, + hostos_settings, + guestos_settings, + }; + + let setupos_config_json_path = Path::new(&setupos_config_json_path); + serialize_and_write_config(setupos_config_json_path, &setupos_config)?; + + println!( + "SetupOSConfig has been written to {}", + setupos_config_json_path.display() + ); + + Ok(()) + } + Some(Commands::GenerateHostosConfig { + setupos_config_json_path, + hostos_config_json_path, + }) => { + let setupos_config_json_path = Path::new(&setupos_config_json_path); + + let setupos_config: SetupOSConfig = + serde_json::from_reader(File::open(setupos_config_json_path)?)?; + + let hostos_config = HostOSConfig { + network_settings: setupos_config.network_settings, + icos_settings: setupos_config.icos_settings, + hostos_settings: setupos_config.hostos_settings, + guestos_settings: setupos_config.guestos_settings, + }; + + let hostos_config_json_path = Path::new(&hostos_config_json_path); + serialize_and_write_config(hostos_config_json_path, &hostos_config)?; + + println!( + "HostOSConfig has been written to {}", + hostos_config_json_path.display() + ); + + Ok(()) + } + None => Ok(()), + } +} diff --git a/rs/ic_os/config/src/types.rs b/rs/ic_os/config/src/types.rs new file mode 100644 index 00000000000..7562b55e12e --- /dev/null +++ b/rs/ic_os/config/src/types.rs @@ -0,0 +1,125 @@ +use ic_types::malicious_behaviour::MaliciousBehaviour; +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::path::PathBuf; +use url::Url; + +/// SetupOS configuration. User-facing configuration files +/// (e.g., `config.ini`, `deployment.json`) are transformed into `SetupOSConfig`. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct SetupOSConfig { + pub network_settings: NetworkSettings, + pub icos_settings: ICOSSettings, + pub setupos_settings: SetupOSSettings, + pub hostos_settings: HostOSSettings, + pub guestos_settings: GuestOSSettings, +} + +/// HostOS configuration. In production, this struct inherits settings from `SetupOSConfig`. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct HostOSConfig { + pub network_settings: NetworkSettings, + pub icos_settings: ICOSSettings, + pub hostos_settings: HostOSSettings, + pub guestos_settings: GuestOSSettings, +} + +/// GuestOS configuration. In production, this struct inherits settings from `HostOSConfig`. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct GuestOSConfig { + pub network_settings: NetworkSettings, + pub icos_settings: ICOSSettings, + pub guestos_settings: GuestOSSettings, +} + +/// Placeholder for SetupOS-specific settings. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct SetupOSSettings; + +/// HostOS-specific settings. +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct HostOSSettings { + pub vm_memory: u32, + pub vm_cpu: String, + pub verbose: bool, +} + +/// GuestOS-specific settings. +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] +pub struct GuestOSSettings { + /// Externally generated cryptographic keys. + /// Must be a directory with contents matching the internal representation of the ic_crypto directory. + /// When given, this provides the private keys of the node. + /// If not given, the node will generate its own private/public key pair. + pub ic_crypto_path: Option, + pub ic_state_path: Option, + /// Initial registry state. + /// Must be a directory with contents matching the internal representation of the ic_registry_local_store. + /// When given, this provides the initial state of the registry. + /// If not given, the node will fetch (initial) registry state from the NNS. + pub ic_registry_local_store_path: Option, + pub guestos_dev: GuestosDevConfig, +} + +/// GuestOS development configuration. These settings are strictly used for development images. +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] +pub struct GuestosDevConfig { + pub backup_spool: Option, + pub malicious_behavior: Option, + pub query_stats_epoch_length: Option, + pub bitcoind_addr: Option, + pub jaeger_addr: Option, + pub socks_proxy: Option, +} + +/// Configures the usage of the backup spool directory. +#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)] +pub struct BackupSpoolSettings { + /// The maximum age of any file or directory kept in the backup spool. + pub backup_retention_time_seconds: Option, + /// The interval at which the backup spool directory will be scanned for files to delete. + pub backup_purging_interval_seconds: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct NetworkSettings { + // Config.ini can specify ipv6_prefix and ipv6_gateway, or just an ipv6_address. + // ipv6_address takes precedence. Some tests provide only ipv6_address. + pub ipv6_prefix: Option, + pub ipv6_address: Option, + pub ipv6_prefix_length: u8, + pub ipv6_gateway: Ipv6Addr, + pub ipv4_address: Option, + pub ipv4_gateway: Option, + pub ipv4_prefix_length: Option, + pub domain: Option, + pub mgmt_mac: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct ICOSSettings { + pub logging: Logging, + /// This file must be a text file containing the public key of the NNS to be used. + pub nns_public_key_path: PathBuf, + /// The URL (HTTP) of the NNS node(s). + pub nns_urls: Vec, + pub hostname: String, + /// This file contains the Node Operator private key, + /// which is registered with the NNS and used to sign the IC join request. + pub node_operator_private_key_path: Option, + /// This directory contains individual files named `admin`, `backup`, `readonly`. + /// The contents of these files serve as `authorized_keys` for their respective role account. + /// This means that, for example, `accounts_ssh_authorized_keys/admin` + /// is transferred to `~admin/.ssh/authorized_keys` on the target system. + /// backup and readonly can only be modified via an NNS proposal + /// and are in place for subnet recovery or issue debugging purposes. + pub ssh_authorized_keys_path: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Logging { + /// Space-separated lists of hosts to ship logs to. + pub elasticsearch_hosts: String, + /// Space-separated list of tags to apply to exported log records. + pub elasticsearch_tags: Option, +} diff --git a/rs/ic_os/dev_test_tools/setupos-inject-configuration/BUILD.bazel b/rs/ic_os/dev_test_tools/setupos-inject-configuration/BUILD.bazel index 99976177f26..3aa287b6b04 100644 --- a/rs/ic_os/dev_test_tools/setupos-inject-configuration/BUILD.bazel +++ b/rs/ic_os/dev_test_tools/setupos-inject-configuration/BUILD.bazel @@ -5,7 +5,7 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. "//rs/ic_os/build_tools/partition_tools", - "//rs/ic_os/utils", + "//rs/ic_os/config:config_lib", "@crate_index//:anyhow", "@crate_index//:clap", "@crate_index//:serde", diff --git a/rs/ic_os/dev_test_tools/setupos-inject-configuration/Cargo.toml b/rs/ic_os/dev_test_tools/setupos-inject-configuration/Cargo.toml index 6662c930263..16bb8b431f4 100644 --- a/rs/ic_os/dev_test_tools/setupos-inject-configuration/Cargo.toml +++ b/rs/ic_os/dev_test_tools/setupos-inject-configuration/Cargo.toml @@ -13,4 +13,4 @@ serde_with = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } url = { workspace = true } -utils = { path = "../../utils"} +config = { path = "../../config"} diff --git a/rs/ic_os/dev_test_tools/setupos-inject-configuration/src/main.rs b/rs/ic_os/dev_test_tools/setupos-inject-configuration/src/main.rs index e85eaeaa7e8..206cfdb0bd6 100644 --- a/rs/ic_os/dev_test_tools/setupos-inject-configuration/src/main.rs +++ b/rs/ic_os/dev_test_tools/setupos-inject-configuration/src/main.rs @@ -12,8 +12,8 @@ use clap::{Args, Parser}; use tempfile::NamedTempFile; use url::Url; +use config::deployment_json::DeploymentSettings; use partition_tools::{ext::ExtPartition, fat::FatPartition, Partition}; -use utils::deployment::DeploymentJson; const SERVICE_NAME: &str = "setupos-inject-configuration"; @@ -227,7 +227,7 @@ async fn write_public_keys(path: &Path, ks: Vec) -> Result<(), Error> { async fn update_deployment(path: &Path, cfg: &DeploymentConfig) -> Result<(), Error> { let mut deployment_json = { let f = File::open(path).context("failed to open deployment config file")?; - let deployment_json: DeploymentJson = serde_json::from_reader(f)?; + let deployment_json: DeploymentSettings = serde_json::from_reader(f)?; deployment_json }; diff --git a/rs/ic_os/network/BUILD.bazel b/rs/ic_os/network/BUILD.bazel index 5929beb3512..64f1f412458 100644 --- a/rs/ic_os/network/BUILD.bazel +++ b/rs/ic_os/network/BUILD.bazel @@ -4,7 +4,7 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. - "//rs/ic_os/config", + "//rs/ic_os/config:config_lib", "//rs/ic_os/utils", "@crate_index//:anyhow", "@crate_index//:hex", diff --git a/rs/ic_os/network/src/info.rs b/rs/ic_os/network/src/info.rs index b69d2eb8b48..88109c8e71a 100644 --- a/rs/ic_os/network/src/info.rs +++ b/rs/ic_os/network/src/info.rs @@ -2,7 +2,7 @@ use std::net::Ipv6Addr; use anyhow::{bail, Context, Result}; -use config::ConfigMap; +use config::config_ini::ConfigMap; #[derive(Debug)] pub struct NetworkInfo { diff --git a/rs/ic_os/os_tools/guestos_tool/BUILD.bazel b/rs/ic_os/os_tools/guestos_tool/BUILD.bazel index 08f6b8b58fe..6ce380e64ff 100644 --- a/rs/ic_os/os_tools/guestos_tool/BUILD.bazel +++ b/rs/ic_os/os_tools/guestos_tool/BUILD.bazel @@ -4,7 +4,7 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. - "//rs/ic_os/config", + "//rs/ic_os/config:config_lib", "//rs/ic_os/network", "//rs/ic_os/utils", "@crate_index//:anyhow", diff --git a/rs/ic_os/os_tools/guestos_tool/src/generate_network_config.rs b/rs/ic_os/os_tools/guestos_tool/src/generate_network_config.rs index ad1da579da9..0f4eb2b2cb3 100644 --- a/rs/ic_os/os_tools/guestos_tool/src/generate_network_config.rs +++ b/rs/ic_os/os_tools/guestos_tool/src/generate_network_config.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use anyhow::{bail, Context, Result}; -use config::config_map_from_path; +use config::config_ini::config_map_from_path; use network::interfaces::{get_interface_name as get_valid_interface_name, get_interface_paths}; use utils::get_command_stdout; diff --git a/rs/ic_os/os_tools/hostos_tool/BUILD.bazel b/rs/ic_os/os_tools/hostos_tool/BUILD.bazel index 5d7a691c68d..ea5395c09e4 100644 --- a/rs/ic_os/os_tools/hostos_tool/BUILD.bazel +++ b/rs/ic_os/os_tools/hostos_tool/BUILD.bazel @@ -4,7 +4,7 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. - "//rs/ic_os/config", + "//rs/ic_os/config:config_lib", "//rs/ic_os/network", "//rs/ic_os/utils", "@crate_index//:anyhow", diff --git a/rs/ic_os/os_tools/hostos_tool/src/main.rs b/rs/ic_os/os_tools/hostos_tool/src/main.rs index 4e3f442ee74..c05a053ca42 100644 --- a/rs/ic_os/os_tools/hostos_tool/src/main.rs +++ b/rs/ic_os/os_tools/hostos_tool/src/main.rs @@ -3,16 +3,15 @@ use std::path::Path; use anyhow::{anyhow, Context, Result}; use clap::{Parser, Subcommand}; -use config::{ - config_map_from_path, DEFAULT_HOSTOS_CONFIG_FILE_PATH, DEFAULT_HOSTOS_DEPLOYMENT_JSON_PATH, -}; +use config::config_ini::config_map_from_path; +use config::deployment_json::get_deployment_settings; +use config::{DEFAULT_HOSTOS_CONFIG_INI_FILE_PATH, DEFAULT_HOSTOS_DEPLOYMENT_JSON_PATH}; use network::generate_network_config; use network::info::NetworkInfo; use network::ipv6::generate_ipv6_address; use network::mac_address::{generate_mac_address, FormattedMacAddress}; use network::node_type::NodeType; use network::systemd::DEFAULT_SYSTEMD_NETWORK_DIR; -use utils::deployment::read_deployment_file; use utils::to_cidr; #[derive(Subcommand)] @@ -35,7 +34,7 @@ pub enum Commands { #[derive(Parser)] struct HostOSArgs { - #[arg(short, long, default_value_t = DEFAULT_HOSTOS_CONFIG_FILE_PATH.to_string(), value_name = "FILE")] + #[arg(short, long, default_value_t = DEFAULT_HOSTOS_CONFIG_INI_FILE_PATH.to_string(), value_name = "FILE")] config: String, #[arg(short, long, default_value_t = DEFAULT_HOSTOS_DEPLOYMENT_JSON_PATH.to_string(), value_name = "FILE")] @@ -64,9 +63,9 @@ pub fn main() -> Result<()> { let network_info = NetworkInfo::from_config_map(&config_map)?; eprintln!("Network info config: {:?}", &network_info); - let deployment = read_deployment_file(Path::new(&opts.deployment_file)); + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)); - let deployment_name: Option<&str> = match &deployment { + let deployment_name: Option<&str> = match &deployment_settings { Ok(deployment) => Some(deployment.deployment.name.as_str()), Err(e) => { eprintln!("Error retrieving deployment file: {e}. Continuing without it"); @@ -74,7 +73,7 @@ pub fn main() -> Result<()> { } }; - let mgmt_mac: Option<&str> = match &deployment { + let mgmt_mac: Option<&str> = match &deployment_settings { Ok(deployment) => deployment.deployment.mgmt_mac.as_deref(), Err(_) => None, }; @@ -88,9 +87,9 @@ pub fn main() -> Result<()> { ) } Some(Commands::GenerateIpv6Address { node_type }) => { - let deployment = read_deployment_file(Path::new(&opts.deployment_file)) + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)) .context("Please specify a valid deployment file with '--deployment-file'")?; - eprintln!("Deployment config: {:?}", deployment); + eprintln!("Deployment config: {:?}", deployment_settings); let config_map = config_map_from_path(Path::new(&opts.config)) .context("Please specify a valid config file with '--config'")?; @@ -101,9 +100,9 @@ pub fn main() -> Result<()> { let node_type = node_type.parse::()?; let mac = generate_mac_address( - &deployment.deployment.name, + &deployment_settings.deployment.name, &node_type, - deployment.deployment.mgmt_mac.as_deref(), + deployment_settings.deployment.mgmt_mac.as_deref(), )?; let ipv6_prefix = network_info .ipv6_prefix @@ -120,15 +119,15 @@ pub fn main() -> Result<()> { let network_info = NetworkInfo::from_config_map(&config_map)?; eprintln!("Network info config: {:?}", &network_info); - let deployment = read_deployment_file(Path::new(&opts.deployment_file)) + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)) .context("Please specify a valid deployment file with '--deployment-file'")?; - eprintln!("Deployment config: {:?}", deployment); + eprintln!("Deployment config: {:?}", deployment_settings); let node_type = node_type.parse::()?; let mac = generate_mac_address( - &deployment.deployment.name, + &deployment_settings.deployment.name, &node_type, - deployment.deployment.mgmt_mac.as_deref(), + deployment_settings.deployment.mgmt_mac.as_deref(), )?; let mac = FormattedMacAddress::from(&mac); println!("{}", mac.get()); diff --git a/rs/ic_os/os_tools/setupos_tool/BUILD.bazel b/rs/ic_os/os_tools/setupos_tool/BUILD.bazel index e393c8a31aa..504a29a3f68 100644 --- a/rs/ic_os/os_tools/setupos_tool/BUILD.bazel +++ b/rs/ic_os/os_tools/setupos_tool/BUILD.bazel @@ -4,7 +4,7 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. - "//rs/ic_os/config", + "//rs/ic_os/config:config_lib", "//rs/ic_os/network", "//rs/ic_os/utils", "@crate_index//:anyhow", diff --git a/rs/ic_os/os_tools/setupos_tool/src/main.rs b/rs/ic_os/os_tools/setupos_tool/src/main.rs index 22ea56e50d6..0bcda31d0ba 100644 --- a/rs/ic_os/os_tools/setupos_tool/src/main.rs +++ b/rs/ic_os/os_tools/setupos_tool/src/main.rs @@ -3,16 +3,15 @@ use std::path::Path; use anyhow::{anyhow, Context, Result}; use clap::{Parser, Subcommand}; -use config::{ - config_map_from_path, DEFAULT_SETUPOS_CONFIG_FILE_PATH, DEFAULT_SETUPOS_DEPLOYMENT_JSON_PATH, -}; +use config::config_ini::config_map_from_path; +use config::deployment_json::get_deployment_settings; +use config::{DEFAULT_SETUPOS_CONFIG_INI_FILE_PATH, DEFAULT_SETUPOS_DEPLOYMENT_JSON_PATH}; use network::generate_network_config; use network::info::NetworkInfo; use network::ipv6::generate_ipv6_address; use network::mac_address::{generate_mac_address, FormattedMacAddress}; use network::node_type::NodeType; use network::systemd::DEFAULT_SYSTEMD_NETWORK_DIR; -use utils::deployment::read_deployment_file; use utils::to_cidr; #[derive(Subcommand)] @@ -35,7 +34,7 @@ pub enum Commands { #[derive(Parser)] struct SetupOSArgs { - #[arg(short, long, default_value_t = DEFAULT_SETUPOS_CONFIG_FILE_PATH.to_string(), value_name = "FILE")] + #[arg(short, long, default_value_t = DEFAULT_SETUPOS_CONFIG_INI_FILE_PATH.to_string(), value_name = "FILE")] config: String, #[arg(short, long, default_value_t = DEFAULT_SETUPOS_DEPLOYMENT_JSON_PATH.to_string(), value_name = "FILE")] @@ -63,8 +62,8 @@ pub fn main() -> Result<()> { let network_info = NetworkInfo::from_config_map(&config_map)?; eprintln!("Network info config: {:?}", &network_info); - let deployment = read_deployment_file(Path::new(&opts.deployment_file)); - let deployment_name: Option<&str> = match &deployment { + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)); + let deployment_name: Option<&str> = match &deployment_settings { Ok(deployment) => Some(deployment.deployment.name.as_str()), Err(e) => { eprintln!("Error retrieving deployment file: {e}. Continuing without it"); @@ -72,7 +71,7 @@ pub fn main() -> Result<()> { } }; - let mgmt_mac: Option<&str> = match &deployment { + let mgmt_mac: Option<&str> = match &deployment_settings { Ok(deployment) => deployment.deployment.mgmt_mac.as_deref(), Err(_) => None, }; @@ -86,9 +85,9 @@ pub fn main() -> Result<()> { ) } Some(Commands::GenerateIpv6Address { node_type }) => { - let deployment = read_deployment_file(Path::new(&opts.deployment_file)) + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)) .context("Please specify a valid deployment file with '--deployment-file'")?; - eprintln!("Deployment config: {:?}", deployment); + eprintln!("Deployment config: {:?}", deployment_settings); let config_map = config_map_from_path(Path::new(&opts.config)) .context("Please specify a valid config file with '--config'")?; @@ -100,9 +99,9 @@ pub fn main() -> Result<()> { let node_type = node_type.parse::()?; let mac = generate_mac_address( - &deployment.deployment.name, + &deployment_settings.deployment.name, &node_type, - deployment.deployment.mgmt_mac.as_deref(), + deployment_settings.deployment.mgmt_mac.as_deref(), )?; let ipv6_prefix = network_info .ipv6_prefix @@ -119,15 +118,15 @@ pub fn main() -> Result<()> { let network_info = NetworkInfo::from_config_map(&config_map)?; eprintln!("Network info config: {:?}", &network_info); - let deployment = read_deployment_file(Path::new(&opts.deployment_file)) + let deployment_settings = get_deployment_settings(Path::new(&opts.deployment_file)) .context("Please specify a valid deployment file with '--deployment-file'")?; - eprintln!("Deployment config: {:?}", deployment); + eprintln!("Deployment config: {:?}", deployment_settings); let node_type = node_type.parse::()?; let mac = generate_mac_address( - &deployment.deployment.name, + &deployment_settings.deployment.name, &node_type, - deployment.deployment.mgmt_mac.as_deref(), + deployment_settings.deployment.mgmt_mac.as_deref(), )?; let mac = FormattedMacAddress::from(&mac); println!("{}", mac.get()); diff --git a/rs/ic_os/release/BUILD.bazel b/rs/ic_os/release/BUILD.bazel index 7e3a9740a2c..d5a5eb40598 100644 --- a/rs/ic_os/release/BUILD.bazel +++ b/rs/ic_os/release/BUILD.bazel @@ -8,6 +8,7 @@ OBJECTS = { "hostos_tool": "//rs/ic_os/os_tools/hostos_tool:hostos_tool", "nft-exporter": "//rs/ic_os/nft_exporter:nft-exporter", "setupos_tool": "//rs/ic_os/os_tools/setupos_tool:setupos_tool", + "config": "//rs/ic_os/config:config", "vsock_guest": "//rs/ic_os/vsock/guest:vsock_guest", "vsock_host": "//rs/ic_os/vsock/host:vsock_host", "metrics-proxy": "@crate_index//:metrics-proxy__metrics-proxy", diff --git a/rs/ic_os/utils/BUILD.bazel b/rs/ic_os/utils/BUILD.bazel index 1fe7e1eb715..53b0a22cfb8 100644 --- a/rs/ic_os/utils/BUILD.bazel +++ b/rs/ic_os/utils/BUILD.bazel @@ -5,15 +5,6 @@ package(default_visibility = ["//rs:ic-os-pkg"]) DEPENDENCIES = [ # Keep sorted. "@crate_index//:anyhow", - "@crate_index//:serde", - "@crate_index//:serde_json", - "@crate_index//:serde_with", - "@crate_index//:url", -] - -DEV_DEPENDENCIES = [ - # Keep sorted. - "@crate_index//:once_cell", ] rust_library( @@ -32,5 +23,5 @@ rust_test( name = "test", size = "small", crate = ":utils", - deps = DEPENDENCIES + DEV_DEPENDENCIES, + deps = DEPENDENCIES, ) diff --git a/rs/ic_os/utils/Cargo.toml b/rs/ic_os/utils/Cargo.toml index f6032fcf829..a2b9ac44755 100644 --- a/rs/ic_os/utils/Cargo.toml +++ b/rs/ic_os/utils/Cargo.toml @@ -5,11 +5,3 @@ edition = "2021" [dependencies] anyhow = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_with = "1.6.2" -url = { workspace = true } - -[dev-dependencies] -once_cell = "1.8" - diff --git a/rs/ic_os/utils/src/lib.rs b/rs/ic_os/utils/src/lib.rs index 5e403788d84..f491bb260dc 100644 --- a/rs/ic_os/utils/src/lib.rs +++ b/rs/ic_os/utils/src/lib.rs @@ -4,8 +4,6 @@ use std::process::Command; use anyhow::{bail, Result}; -pub mod deployment; - /// Systemd requires ip addresses to be specified with the prefix length pub fn to_cidr(ipv6_address: Ipv6Addr, prefix_length: u8) -> String { format!("{}/{}", ipv6_address, prefix_length)