diff --git a/packages/early-boot-config/Cargo.toml b/packages/early-boot-config/Cargo.toml new file mode 100644 index 00000000000..39f8fb89356 --- /dev/null +++ b/packages/early-boot-config/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "early-boot-config" +version = "0.1.0" +edition = "2021" +publish = false +build = "../build.rs" + +[lib] +path = "../packages.rs" + +[package.metadata.build-package] +source-groups = [ + "early-boot-config/early-boot-config", + "early-boot-config/early-boot-config-provider", + + "early-boot-config/user-data-providers/ec2-identity-doc", + "early-boot-config/user-data-providers/ec2-imds", + "early-boot-config/user-data-providers/local-defaults", + "early-boot-config/user-data-providers/local-file", + "early-boot-config/user-data-providers/local-overrides", + "early-boot-config/user-data-providers/vmware-cd-rom", + "early-boot-config/user-data-providers/vmware-guestinfo", +] + +# RPM BuildRequires +[build-dependencies] +glibc = { path = "../glibc" } + +# RPM Requires +[dependencies] diff --git a/packages/os/early-boot-config.service b/packages/early-boot-config/early-boot-config.service similarity index 100% rename from packages/os/early-boot-config.service rename to packages/early-boot-config/early-boot-config.service diff --git a/packages/early-boot-config/early-boot-config.spec b/packages/early-boot-config/early-boot-config.spec new file mode 100644 index 00000000000..c0b44d16365 --- /dev/null +++ b/packages/early-boot-config/early-boot-config.spec @@ -0,0 +1,155 @@ +%global _cross_first_party 1 +%undefine _debugsource_packages + +Name: %{_cross_os}early-boot-config +Version: 0.0 +Release: 0%{?dist} +Summary: early-boot-config +License: Apache-2.0 OR MIT +URL: https://github.com/bottlerocket-os/bottlerocket + +Source100: early-boot-config.service + +BuildRequires: %{_cross_os}glibc-devel + +%description +%{summary}. + +%package -n %{_cross_os}early-boot-config-local +Summary: local-provider + +%description -n %{_cross_os}early-boot-config-local +%{summary}. + +%package -n %{_cross_os}early-boot-config-aws +Summary: early-boot-config package for AWS +Requires: %{name} +Requires: %{_cross_os}early-boot-config-local + +%description -n %{_cross_os}early-boot-config-aws +%{summary}. + +%ifarch x86_64 +%package -n %{_cross_os}early-boot-config-vmware +Summary: early-boot-config package for vmware +Requires: %{name} +Requires: %{_cross_os}early-boot-config-local + +%description -n %{_cross_os}early-boot-config-vmware +%{summary}. +%endif + +%package -n %{_cross_os}early-boot-config-metal +Summary: early-boot-config package for metal +Requires: %{name} +Requires: %{_cross_os}early-boot-config-local + +%description -n %{_cross_os}early-boot-config-metal +%{summary}. + +%prep +%setup -T -c +%cargo_prep + +%build +%cargo_build --manifest-path %{_builddir}/sources/Cargo.toml \ + -p early-boot-config \ + -p ec2-identity-doc-user-data-provider \ + -p ec2-imds-user-data-provider \ + -p local-defaults-user-data-provider \ + -p local-file-user-data-provider \ + -p local-overrides-user-data-provider \ +%ifarch x86_64 + -p vmware-cd-rom-user-data-provider \ + -p vmware-guestinfo-user-data-provider \ +%endif + %{nil} + +%global cargo_outdir %{getenv:HOME}/.cache/%{__cargo_target}/release +%global early_boot_config_bindir %{_cross_libexecdir}/early-boot-config/bin +%global early_boot_config_provider_dir %{_cross_libexecdir}/early-boot-config/data-providers.d + +%install +install -d %{buildroot}%{_cross_bindir} +install -p -m 0755 %{cargo_outdir}/early-boot-config %{buildroot}%{_cross_bindir} + +install -d %{buildroot}%{_cross_unitdir} +install -p -m 0644 %{S:100} %{buildroot}%{_cross_unitdir} + +install -d %{buildroot}%{early_boot_config_bindir} +install -p -m 0755 \ + %{cargo_outdir}/ec2-identity-doc-user-data-provider \ + %{cargo_outdir}/ec2-imds-user-data-provider \ + %{cargo_outdir}/local-defaults-user-data-provider \ + %{cargo_outdir}/local-file-user-data-provider \ + %{cargo_outdir}/local-overrides-user-data-provider \ + %{buildroot}%{early_boot_config_bindir} + +%ifarch x86_64 +install -p -m 0755 \ + %{cargo_outdir}/vmware-cd-rom-user-data-provider \ + %{cargo_outdir}/vmware-guestinfo-user-data-provider \ + %{buildroot}%{early_boot_config_bindir} +%endif + +install -d %{buildroot}%{early_boot_config_provider_dir} + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/ec2-identity-doc-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/30-ec2-identity-doc + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/ec2-imds-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/40-ec2-imds + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/local-defaults-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/10-local-defaults + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/local-file-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/20-local-user-data + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/local-overrides-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/99-local-overrides + +%ifarch x86_64 +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/vmware-cd-rom-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/30-vmware-cd-rom + +ln -rs \ + %{buildroot}%{early_boot_config_bindir}/vmware-guestinfo-user-data-provider \ + %{buildroot}%{early_boot_config_provider_dir}/40-vmware-guestinfo +%endif + +%files +%{_cross_bindir}/early-boot-config +%{_cross_unitdir}/early-boot-config.service +%dir %{early_boot_config_provider_dir} + +%files -n %{_cross_os}early-boot-config-local +%{early_boot_config_bindir}/local-defaults-user-data-provider +%{early_boot_config_bindir}/local-file-user-data-provider +%{early_boot_config_bindir}/local-overrides-user-data-provider +%{early_boot_config_provider_dir}/10-local-defaults +%{early_boot_config_provider_dir}/20-local-user-data +%{early_boot_config_provider_dir}/99-local-overrides + +%files -n %{_cross_os}early-boot-config-aws +%{early_boot_config_bindir}/ec2-identity-doc-user-data-provider +%{early_boot_config_bindir}/ec2-imds-user-data-provider +%{early_boot_config_provider_dir}/30-ec2-identity-doc +%{early_boot_config_provider_dir}/40-ec2-imds + +%ifarch x86_64 +%files -n %{_cross_os}early-boot-config-vmware +%{early_boot_config_bindir}/vmware-cd-rom-user-data-provider +%{early_boot_config_bindir}/vmware-guestinfo-user-data-provider +%{early_boot_config_provider_dir}/30-vmware-cd-rom +%{early_boot_config_provider_dir}/40-vmware-guestinfo +%endif + +# There are no metal-specific providers, just dependencies like the local file providers. +%files -n %{_cross_os}early-boot-config-metal diff --git a/packages/os/os.spec b/packages/os/os.spec index d4af73474b1..47d9c1640ed 100644 --- a/packages/os/os.spec +++ b/packages/os/os.spec @@ -40,7 +40,6 @@ Source19: host-containers-toml # 1xx sources: systemd units Source100: apiserver.service -Source101: early-boot-config.service Source102: sundog.service Source103: storewolf.service Source105: settings-applier.service @@ -87,7 +86,6 @@ Requires: %{_cross_os}bootstrap-containers Requires: %{_cross_os}bork Requires: %{_cross_os}corndog Requires: %{_cross_os}certdog -Requires: %{_cross_os}early-boot-config Requires: %{_cross_os}ghostdog Requires: %{_cross_os}host-containers Requires: %{_cross_os}logdog @@ -136,11 +134,6 @@ Summary: Bottlerocket API client %description -n %{_cross_os}apiclient %{summary}. -%package -n %{_cross_os}early-boot-config -Summary: Bottlerocket userdata configuration system -%description -n %{_cross_os}early-boot-config -%{summary}. - %package -n %{_cross_os}netdog Summary: Bottlerocket network configuration helper %if %{with systemd_networkd} @@ -336,7 +329,6 @@ static_pid="$!" echo "** Output from non-static builds:" %cargo_build --manifest-path %{_builddir}/sources/Cargo.toml \ -p apiserver \ - -p early-boot-config \ -p netdog \ -p sundog \ -p schnauzer \ @@ -377,7 +369,7 @@ fi install -d %{buildroot}%{_cross_bindir} for p in \ apiserver \ - early-boot-config netdog sundog schnauzer schnauzer-v2 bork \ + netdog sundog schnauzer schnauzer-v2 bork \ corndog thar-be-settings thar-be-updates host-containers \ storewolf settings-committer \ migrator prairiedog certdog \ @@ -470,10 +462,10 @@ install -p -m 0644 %{S:5} %{S:6} %{S:7} %{S:8} %{S:14} %{S:15} %{S:16} %{S:17} % install -d %{buildroot}%{_cross_unitdir} install -p -m 0644 \ - %{S:100} %{S:101} %{S:102} %{S:103} %{S:105} \ - %{S:106} %{S:107} %{S:110} %{S:111} %{S:112} \ - %{S:113} %{S:114} %{S:118} %{S:119} %{S:122} \ - %{S:123} \ + %{S:100} %{S:102} %{S:103} %{S:105} \ + %{S:106} %{S:107} %{S:110} %{S:111} \ + %{S:112} %{S:113} %{S:114} %{S:118} \ + %{S:119} %{S:122} %{S:123} \ %{buildroot}%{_cross_unitdir} %if %{with systemd_networkd} @@ -538,10 +530,6 @@ install -p -m 0644 %{S:400} %{S:401} %{S:402} %{buildroot}%{_cross_licensedir} %files -n %{_cross_os}apiclient %{_cross_bindir}/apiclient -%files -n %{_cross_os}early-boot-config -%{_cross_bindir}/early-boot-config -%{_cross_unitdir}/early-boot-config.service - %files -n %{_cross_os}netdog %{_cross_bindir}/netdog %{_cross_tmpfilesdir}/netdog.conf diff --git a/sources/Cargo.lock b/sources/Cargo.lock index 8b7268bdb06..c0f1ae6da62 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -1692,17 +1692,12 @@ dependencies = [ "apiclient", "async-trait", "base64", - "bottlerocket-variant", "constants", - "flate2", + "early-boot-config-provider", + "env_logger", "generate-readme", - "hex-literal", "http 0.2.12", - "imdsclient", - "lazy_static", "log", - "retry-read", - "serde", "serde-xml-rs", "serde_json", "serde_plain", @@ -1710,7 +1705,52 @@ dependencies = [ "snafu 0.8.2", "tokio", "toml", - "vmw_backdoor", + "walkdir", +] + +[[package]] +name = "early-boot-config-provider" +version = "0.1.0" +dependencies = [ + "async-trait", + "env_logger", + "flate2", + "generate-readme", + "hex-literal", + "lazy_static", + "log", + "retry-read", + "serde", + "serde_json", + "snafu 0.8.2", + "toml", +] + +[[package]] +name = "ec2-identity-doc-user-data-provider" +version = "0.1.0" +dependencies = [ + "async-trait", + "early-boot-config-provider", + "generate-readme", + "imdsclient", + "log", + "serde_json", + "snafu 0.8.2", + "tokio", +] + +[[package]] +name = "ec2-imds-user-data-provider" +version = "0.1.0" +dependencies = [ + "async-trait", + "early-boot-config-provider", + "generate-readme", + "imdsclient", + "log", + "snafu 0.8.2", + "tokio", ] [[package]] @@ -2451,6 +2491,30 @@ dependencies = [ "local-waker", ] +[[package]] +name = "local-defaults-user-data-provider" +version = "0.1.0" +dependencies = [ + "early-boot-config-provider", + "generate-readme", +] + +[[package]] +name = "local-file-user-data-provider" +version = "0.1.0" +dependencies = [ + "early-boot-config-provider", + "generate-readme", +] + +[[package]] +name = "local-overrides-user-data-provider" +version = "0.1.0" +dependencies = [ + "early-boot-config-provider", + "generate-readme", +] + [[package]] name = "local-waker" version = "0.1.4" @@ -4736,6 +4800,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vmware-cd-rom-user-data-provider" +version = "0.1.0" +dependencies = [ + "base64", + "early-boot-config-provider", + "generate-readme", + "log", + "serde", + "serde-xml-rs", + "snafu 0.8.2", +] + +[[package]] +name = "vmware-guestinfo-user-data-provider" +version = "0.1.0" +dependencies = [ + "base64", + "early-boot-config-provider", + "generate-readme", + "log", + "serde", + "serde_plain", + "snafu 0.8.2", + "vmw_backdoor", +] + [[package]] name = "vsimd" version = "0.8.0" diff --git a/sources/Cargo.toml b/sources/Cargo.toml index 80a16d2398e..2488993e9aa 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -8,7 +8,6 @@ members = [ "api/certdog", "api/corndog", "api/datastore", - "api/early-boot-config", "api/netdog", "api/sundog", "api/schnauzer", @@ -91,6 +90,17 @@ members = [ "driverdog", + "early-boot-config/early-boot-config", + "early-boot-config/early-boot-config-provider", + + "early-boot-config/user-data-providers/ec2-identity-doc", + "early-boot-config/user-data-providers/ec2-imds", + "early-boot-config/user-data-providers/local-defaults", + "early-boot-config/user-data-providers/local-file", + "early-boot-config/user-data-providers/local-overrides", + "early-boot-config/user-data-providers/vmware-cd-rom", + "early-boot-config/user-data-providers/vmware-guestinfo", + "generate-readme", "ghostdog", diff --git a/sources/api/early-boot-config/README.md b/sources/api/early-boot-config/README.md deleted file mode 100644 index ff36da7ce26..00000000000 --- a/sources/api/early-boot-config/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# early-boot-config - -Current version: 0.1.0 - -## Introduction - -early-boot-config sends provider-specific platform data to the Bottlerocket API. - -For most providers this means configuration from user data and platform metadata, taken from -something like an instance metadata service. - -Currently, Amazon EC2 is supported through the IMDSv1 HTTP API. Data will be taken from files in -/etc/early-boot-config instead, if available, for testing purposes. - -## Colophon - -This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/api/early-boot-config/build.rs b/sources/api/early-boot-config/build.rs deleted file mode 100644 index 8f095b023e6..00000000000 --- a/sources/api/early-boot-config/build.rs +++ /dev/null @@ -1,19 +0,0 @@ -use bottlerocket_variant::{Variant, VARIANT_ENV}; - -fn main() { - let variant = match Variant::from_env() { - Ok(variant) => variant, - Err(e) => { - eprintln!( - "For local builds, you must set the '{}' environment variable so we know \ - which data provider to build. Valid values are the directories in \ - models/src/variants/, for example 'aws-ecs-1': {}", - VARIANT_ENV, e, - ); - std::process::exit(1); - } - }; - variant.emit_cfgs(); - - generate_readme::from_main().unwrap(); -} diff --git a/sources/api/early-boot-config/src/main.rs b/sources/api/early-boot-config/src/main.rs deleted file mode 100644 index ad95e5e64a2..00000000000 --- a/sources/api/early-boot-config/src/main.rs +++ /dev/null @@ -1,193 +0,0 @@ -/*! -# Introduction - -early-boot-config sends provider-specific platform data to the Bottlerocket API. - -For most providers this means configuration from user data and platform metadata, taken from -something like an instance metadata service. - -Currently, Amazon EC2 is supported through the IMDSv1 HTTP API. Data will be taken from files in -/etc/early-boot-config instead, if available, for testing purposes. -*/ - -#[macro_use] -extern crate log; - -use simplelog::{Config as LogConfig, LevelFilter, SimpleLogger}; -use snafu::{ensure, ResultExt}; -use std::fs; -use std::str::FromStr; -use std::{env, process}; - -mod compression; -mod provider; -mod settings; -use crate::provider::{Platform, PlatformDataProvider}; - -// TODO -// Tests! - -// We only want to run early-boot-config once, at first boot. Our systemd unit file has a -// ConditionPathExists that will prevent it from running again if this file exists. -// We create it after running successfully. -const MARKER_FILE: &str = "/var/lib/bottlerocket/early-boot-config.ran"; - -/// Store the args we receive on the command line -#[derive(Debug)] -struct Args { - log_level: LevelFilter, - socket_path: String, -} - -/// Print a usage message in the event a bad arg is passed -fn usage() -> ! { - let program_name = env::args().next().unwrap_or_else(|| "program".to_string()); - eprintln!( - r"Usage: {} - [ --socket-path PATH ] - [ --log-level trace|debug|info|warn|error ] - - Socket path defaults to {}", - program_name, - constants::API_SOCKET, - ); - process::exit(2); -} - -/// Prints a more specific message before exiting through usage(). -fn usage_msg>(msg: S) -> ! { - eprintln!("{}\n", msg.as_ref()); - usage(); -} - -/// Parse the args to the program and return an Args struct -fn parse_args(args: env::Args) -> Args { - let mut log_level = None; - let mut socket_path = None; - - let mut iter = args.skip(1); - while let Some(arg) = iter.next() { - match arg.as_ref() { - "--socket-path" => { - socket_path = Some( - iter.next() - .unwrap_or_else(|| usage_msg("Did not give argument to --socket-path")), - ) - } - - "--log-level" => { - let log_level_str = iter - .next() - .unwrap_or_else(|| usage_msg("Did not give argument to --log-level")); - log_level = Some(LevelFilter::from_str(&log_level_str).unwrap_or_else(|_| { - usage_msg(format!("Invalid log level '{}'", log_level_str)) - })); - } - - _ => usage(), - } - } - - Args { - log_level: log_level.unwrap_or(LevelFilter::Info), - socket_path: socket_path.unwrap_or_else(|| constants::API_SOCKET.to_string()), - } -} - -async fn run() -> Result<()> { - // Parse and store the args passed to the program - let args = parse_args(env::args()); - - // SimpleLogger will send errors to stderr and anything less to stdout. - SimpleLogger::init(args.log_level, LogConfig::default()).context(error::LoggerSnafu)?; - - info!("early-boot-config started"); - - info!("Retrieving platform-specific data"); - let uri = &format!( - "{}?tx={}", - constants::API_SETTINGS_URI, - constants::LAUNCH_TRANSACTION - ); - let method = "PATCH"; - for settings_json in Platform - .platform_data() - .await - .context(error::ProviderSnafu)? - { - // Don't send an empty request to the API - if settings_json.json.is_empty() { - warn!("{} was empty", settings_json.desc); - continue; - } - - info!("Sending {} to API", settings_json.desc); - trace!("Request body: {}", settings_json.json); - let (code, response_body) = - apiclient::raw_request(&args.socket_path, uri, method, Some(settings_json.json)) - .await - .context(error::APIRequestSnafu { method, uri })?; - ensure!( - code.is_success(), - error::ResponseSnafu { - method, - uri, - code, - response_body, - } - ); - } - - fs::write(MARKER_FILE, "").unwrap_or_else(|e| { - warn!( - "Failed to create marker file {}, may unexpectedly run again: {}", - MARKER_FILE, e - ) - }); - - Ok(()) -} - -// Returning a Result from main makes it print a Debug representation of the error, but with Snafu -// we have nice Display representations of the error, so we wrap "main" (run) and print any error. -// https://github.com/shepmaster/snafu/issues/110 -#[tokio::main] -async fn main() { - if let Err(e) = run().await { - eprintln!("{}", e); - process::exit(1); - } -} - -mod error { - use http::StatusCode; - use snafu::Snafu; - - #[derive(Debug, Snafu)] - #[snafu(visibility(pub(super)))] - pub(super) enum Error { - #[snafu(display("Error {}ing '{}': {}", method, uri, source))] - APIRequest { - method: String, - uri: String, - #[snafu(source(from(apiclient::Error, Box::new)))] - source: Box, - }, - - #[snafu(display("Provider error: {}", source))] - Provider { source: Box }, - - #[snafu(display("Error {} when {}ing '{}': {}", code, method, uri, response_body))] - Response { - method: String, - uri: String, - code: StatusCode, - response_body: String, - }, - - #[snafu(display("Logger setup error: {}", source))] - Logger { source: log::SetLoggerError }, - } -} - -type Result = std::result::Result; diff --git a/sources/api/early-boot-config/src/provider.rs b/sources/api/early-boot-config/src/provider.rs deleted file mode 100644 index 198e683982b..00000000000 --- a/sources/api/early-boot-config/src/provider.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! The provider module owns the `PlatformDataProvider` trait - -use crate::settings::SettingsJson; -use async_trait::async_trait; - -mod local_file; - -#[cfg(variant_platform = "aws")] -mod aws; -#[cfg(variant_platform = "aws")] -pub(crate) use aws::AwsDataProvider as Platform; - -#[cfg(variant_platform = "vmware")] -mod vmware; -#[cfg(variant_platform = "vmware")] -pub(crate) use vmware::VmwareDataProvider as Platform; - -#[cfg(variant_platform = "metal")] -mod metal; -#[cfg(variant_platform = "metal")] -pub(crate) use metal::MetalDataProvider as Platform; - -/// Support for new platforms can be added by implementing this trait. -#[async_trait] -pub(crate) trait PlatformDataProvider { - /// You should return a list of SettingsJson, representing the settings changes you want to - /// send to the API. - /// - /// This is a list so that handling multiple data sources within a platform can feel more - /// natural; you can also send all changes in one entry if you like. - async fn platform_data( - &self, - ) -> std::result::Result, Box>; -} diff --git a/sources/api/early-boot-config/src/provider/aws.rs b/sources/api/early-boot-config/src/provider/aws.rs deleted file mode 100644 index ccb2fd7ab41..00000000000 --- a/sources/api/early-boot-config/src/provider/aws.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! The aws module implements the `PlatformDataProvider` trait for gathering userdata on AWS. - -use super::{PlatformDataProvider, SettingsJson}; -use crate::compression::expand_slice_maybe; -use async_trait::async_trait; -use imdsclient::ImdsClient; -use serde_json::json; -use snafu::{OptionExt, ResultExt}; -use std::fs; -use std::path::Path; - -use crate::provider::local_file; - -/// Unit struct for AWS so we can implement the PlatformDataProvider trait. -pub(crate) struct AwsDataProvider; - -impl AwsDataProvider { - const IDENTITY_DOCUMENT_FILE: &'static str = "/etc/early-boot-config/identity-document"; - const FALLBACK_REGION: &'static str = "us-east-1"; - - /// Fetches user data, which is expected to be in TOML form and contain a `[settings]` section, - /// returning a SettingsJson representing the inside of that section. - async fn user_data(client: &mut ImdsClient) -> Result> { - let user_data_raw = match client - .fetch_userdata() - .await - .context(error::ImdsRequestSnafu)? - { - Some(user_data_raw) => user_data_raw, - None => return Ok(None), - }; - let user_data_str = expand_slice_maybe(&user_data_raw) - .context(error::DecompressionSnafu { what: "user data" })?; - trace!("Received user data: {}", user_data_str); - - // Return early to prevent parsing an empty string - if user_data_str.trim().is_empty() { - return Ok(None); - } - - let json = SettingsJson::from_toml_str(&user_data_str, "user data").context( - error::SettingsToJSONSnafu { - from: "instance user data", - }, - )?; - Ok(Some(json)) - } - - /// Fetches the instance identity, returning a SettingsJson representing the values from the - /// document which we'd like to send to the API - currently just region. - async fn identity_document(client: &mut ImdsClient) -> Result> { - let desc = "instance identity document"; - let file = Self::IDENTITY_DOCUMENT_FILE; - - let region = if Path::new(file).exists() { - info!("{} found at {}, using it", desc, file); - let data = - fs::read_to_string(file).context(error::InputFileReadSnafu { path: file })?; - let iid: serde_json::Value = - serde_json::from_str(&data).context(error::DeserializeJsonSnafu)?; - iid.get("region") - .context(error::IdentityDocMissingDataSnafu { missing: "region" })? - .as_str() - .context(error::WrongTypeSnafu { - field_name: "region", - expected_type: "string", - })? - .to_owned() - } else { - client - .fetch_region() - .await - .context(error::ImdsRequestSnafu)? - .unwrap_or_else(|| Self::FALLBACK_REGION.to_owned()) - }; - trace!( - "Retrieved region from instance identity document: {}", - region - ); - - let val = json!({ "aws": {"region": region} }); - - let json = SettingsJson::from_val(&val, desc).context(error::SettingsToJSONSnafu { - from: "instance identity document", - })?; - Ok(Some(json)) - } -} - -#[async_trait] -impl PlatformDataProvider for AwsDataProvider { - /// Return settings changes from the instance identity document and user data. - async fn platform_data( - &self, - ) -> std::result::Result, Box> { - let mut output = Vec::new(); - - let mut client = ImdsClient::new(); - - // First read from any site-local defaults. For AWS, these would come from the second EBS - // volume, which may be a custom snapshot with settings and cached container images that - // is used across all variants. Placing it first gives user data from IMDS a chance to - // override any settings that don't make sense for this variant. - match local_file::user_data_defaults()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site defaults file: {}", - local_file::USER_DATA_DEFAULTS_FILE - ), - } - - // Next, read from any user-data specific to this install. For AWS, these would come from - // the first EBS volume containing the OS root filesystem and the private settings - // fileystem. It's less convenient to store settings here since the corresponding snapshot - // changes with every new version, but still possible. - match local_file::user_data()? { - Some(s) => output.push(s), - None => info!( - "No user data found via local file: {}", - local_file::USER_DATA_FILE - ), - } - - // Instance identity doc next, so the user has a chance to override - match Self::identity_document(&mut client).await? { - Some(s) => output.push(s), - None => warn!("No instance identity document found."), - } - - // Optional user-specified configuration / overrides - match Self::user_data(&mut client).await? { - Some(s) => output.push(s), - None => warn!("No user data found."), - } - - // Finally, apply any site-local overrides. For AWS, these again come from the second EBS - // volume. This file is placed last so that it takes precedence over any other source of - // configuration. It's useful for mandatory settings that must always be present. - match local_file::user_data_overrides()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site overrides file: {}", - local_file::USER_DATA_OVERRIDES_FILE - ), - } - - Ok(output) - } -} - -mod error { - use snafu::Snafu; - use std::io; - use std::path::PathBuf; - - #[derive(Debug, Snafu)] - #[snafu(visibility(pub(super)))] - pub(crate) enum Error { - #[snafu(display("Failed to decompress {}: {}", what, source))] - Decompression { what: String, source: io::Error }, - - #[snafu(display("Error deserializing from JSON: {}", source))] - DeserializeJson { source: serde_json::error::Error }, - - #[snafu(display("Instance identity document missing {}", missing))] - IdentityDocMissingData { missing: String }, - - #[snafu(display("IMDS client failed: {}", source))] - ImdsClient { source: imdsclient::Error }, - - #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] - InputFileRead { path: PathBuf, source: io::Error }, - - #[snafu(display("IMDS request failed: {}", source))] - ImdsRequest { source: imdsclient::Error }, - - #[snafu(display("Unable to serialize settings from {}: {}", from, source))] - SettingsToJSON { - from: String, - source: crate::settings::Error, - }, - - #[snafu(display( - "Wrong type while deserializing, expected '{}' to be type '{}'", - field_name, - expected_type - ))] - WrongType { - field_name: &'static str, - expected_type: &'static str, - }, - } -} - -type Result = std::result::Result; diff --git a/sources/api/early-boot-config/src/provider/local_file.rs b/sources/api/early-boot-config/src/provider/local_file.rs deleted file mode 100644 index 3518bcaf9a5..00000000000 --- a/sources/api/early-boot-config/src/provider/local_file.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! The local_file module provides a method for gathering userdata from local file - -use super::SettingsJson; -use crate::compression::expand_file_maybe; -use snafu::ResultExt; -use std::path::Path; - -pub(crate) const USER_DATA_FILE: &str = "/var/lib/bottlerocket/user-data.toml"; -pub(crate) const USER_DATA_DEFAULTS_FILE: &str = "/local/user-data-defaults.toml"; -pub(crate) const USER_DATA_OVERRIDES_FILE: &str = "/local/user-data-overrides.toml"; - -pub(crate) fn user_data() -> std::result::Result, Box> { - read_from_file(USER_DATA_FILE) -} - -pub(crate) fn user_data_defaults( -) -> std::result::Result, Box> { - read_from_file(USER_DATA_DEFAULTS_FILE) -} - -pub(crate) fn user_data_overrides( -) -> std::result::Result, Box> { - read_from_file(USER_DATA_OVERRIDES_FILE) -} - -fn read_from_file( - path: &str, -) -> std::result::Result, Box> { - if !Path::new(path).exists() { - return Ok(None); - } - info!("'{path}' exists, using it"); - - // Read the file, decompressing it if compressed. - let user_data_str = expand_file_maybe(path).context(error::InputFileReadSnafu { path })?; - - if user_data_str.is_empty() { - return Ok(None); - } - - let json = SettingsJson::from_toml_str(&user_data_str, "user data") - .context(error::SettingsToJSONSnafu { from: path })?; - - Ok(Some(json)) -} - -mod error { - use snafu::Snafu; - use std::io; - use std::path::PathBuf; - - #[derive(Debug, Snafu)] - #[snafu(visibility(pub(super)))] - pub(crate) enum Error { - #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] - InputFileRead { path: PathBuf, source: io::Error }, - - #[snafu(display("Unable to serialize settings from {}: {}", from, source))] - SettingsToJSON { - from: String, - source: crate::settings::Error, - }, - } -} diff --git a/sources/api/early-boot-config/src/provider/metal.rs b/sources/api/early-boot-config/src/provider/metal.rs deleted file mode 100644 index ab73a04e233..00000000000 --- a/sources/api/early-boot-config/src/provider/metal.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! The metal module implements the `PlatformDataProvider` trait for gathering userdata on bare -//! metal. - -use super::{PlatformDataProvider, SettingsJson}; -use async_trait::async_trait; - -use crate::provider::local_file; - -pub(crate) struct MetalDataProvider; - -#[async_trait] -impl PlatformDataProvider for MetalDataProvider { - async fn platform_data( - &self, - ) -> std::result::Result, Box> { - let mut output = Vec::new(); - - // First read from any site-local defaults. It's unlikely that this file will exist, since - // for bare metal provisioning these settings could just be written to the main user data - // file, but this is consistent with other platforms. - match local_file::user_data_defaults()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site defaults file: {}", - local_file::USER_DATA_DEFAULTS_FILE - ), - } - - // This is the main file where we expect settings, so warn if they're not found. - match local_file::user_data()? { - Some(s) => output.push(s), - None => warn!( - "No user data found via local file: {}", - local_file::USER_DATA_FILE - ), - } - - // Finally, apply any site-local overrides. - match local_file::user_data_overrides()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site overrides file: {}", - local_file::USER_DATA_OVERRIDES_FILE - ), - } - - Ok(output) - } -} diff --git a/sources/api/early-boot-config/src/provider/vmware.rs b/sources/api/early-boot-config/src/provider/vmware.rs deleted file mode 100644 index c3f1e758052..00000000000 --- a/sources/api/early-boot-config/src/provider/vmware.rs +++ /dev/null @@ -1,443 +0,0 @@ -//! The vmware module implements the `PlatformDataProvider` trait for gathering userdata on VMware -//! via mounted CDRom or the guestinfo interface - -use super::{PlatformDataProvider, SettingsJson}; -use crate::compression::{expand_file_maybe, expand_slice_maybe, OptionalCompressionReader}; -use async_trait::async_trait; -use base64::Engine; -use serde::Deserialize; -use snafu::{ensure, ResultExt}; -use std::ffi::OsStr; -use std::fs::File; -use std::io::BufReader; -use std::io::Cursor; -use std::io::Read; -use std::iter::FromIterator; -use std::path::Path; -use std::str; - -use crate::provider::local_file; - -pub(crate) struct VmwareDataProvider; - -impl VmwareDataProvider { - // This program expects that the CD-ROM is already mounted. Mounting happens elsewhere in a - // systemd unit file - const CD_ROM_MOUNT: &'static str = "/media/cdrom"; - // A mounted CD-ROM may contain an OVF file or a user-supplied file named `user-data` - const USER_DATA_FILENAMES: [&'static str; 5] = [ - "user-data", - "ovf-env.xml", - "OVF-ENV.XML", - "ovf_env.xml", - "OVF_ENV.XML", - ]; - - // The fields in which user data and its encoding are stored in guestinfo - const GUESTINFO_USERDATA: &'static str = "guestinfo.userdata"; - const GUESTINFO_USERDATA_ENCODING: &'static str = "guestinfo.userdata.encoding"; - - /// Read and decode user data from files via mounted CD-ROM - fn cdrom_user_data() -> Result> { - // Given the list of acceptable filenames, ensure only 1 exists and parse - // it for user data - info!("Attempting to retrieve user data from mounted CD-ROM"); - let mut user_data_files = Self::USER_DATA_FILENAMES - .iter() - .map(|filename| Path::new(Self::CD_ROM_MOUNT).join(filename)) - .filter(|file| file.exists()); - - let user_data_file = match user_data_files.next() { - Some(file) => file, - None => return Ok(None), - }; - - ensure!( - user_data_files.next().is_none(), - error::UserDataFileCountSnafu { - place: Self::CD_ROM_MOUNT - } - ); - - // XML files require extra processing, while a user-supplied file should already be in TOML - // format - info!("'{}' exists, using it", user_data_file.display()); - let user_data_str = match user_data_file.extension().and_then(OsStr::to_str) { - Some("xml") | Some("XML") => Self::ovf_user_data(&user_data_file)?, - // Since we only look for a specific list of file names, we should never find a file - // with an extension we don't understand. - Some(_) => unreachable!(), - None => { - // Read the file, decompressing it if compressed. - expand_file_maybe(&user_data_file).context(error::InputFileReadSnafu { - path: &user_data_file, - })? - } - }; - - if user_data_str.is_empty() { - return Ok(None); - } - - // User data could be 700MB compressed! Eek! :) - if user_data_str.len() <= 2048 { - trace!("Received user data: {}", user_data_str); - } else { - trace!( - "Received long user data, starts with: {}", - // (this isn't perfect because chars aren't grapheme clusters, but will error - // toward printing the whole input, which is fine) - String::from_iter(user_data_str.chars().take(2048)) - ); - } - - let json = SettingsJson::from_toml_str(&user_data_str, "user data from CD-ROM").context( - error::SettingsToJsonSnafu { - from: user_data_file.display().to_string(), - }, - )?; - - Ok(Some(json)) - } - - /// Read and base64 decode user data contained in an OVF file - // In VMware, user data is supplied to the host via an XML file. Within - // the XML file, there is a `PropertySection` that contains `Property` elements - // with attributes. User data is base64 encoded inside a `Property` element with - // the attribute "user-data". - // - fn ovf_user_data>(path: P) -> Result { - let path = path.as_ref(); - let file = File::open(path).context(error::InputFileReadSnafu { path })?; - let reader = OptionalCompressionReader::new(BufReader::new(file)); - - // Deserialize the OVF file, dropping everything we don't care about - let ovf: Environment = - serde_xml_rs::from_reader(reader).context(error::XmlDeserializeSnafu { path })?; - - // We have seen the keys in the `Property` section be "namespaced" like "oe:key" or - // "of:key". Since we aren't trying to validate the schema beyond the presence of the - // elements we care about, we can ignore the namespacing. An example of this type of - // namespacing can be found in the unit test sample data. `serde_xml_rs` effectively - // ignores these namespaces and returns "key" / "value": - // https://github.com/Rreverser/serde-xml-rs/issues/64#issuecomment=540448434 - let mut base64_str = String::new(); - let user_data_key = "user-data"; - for property in ovf.property_section.properties { - if property.key == user_data_key { - base64_str = property.value; - break; - } - } - - // Base64 decode the &str - let decoded_bytes = base64::engine::general_purpose::STANDARD - .decode(base64_str) - .context(error::Base64DecodeSnafu { - what: "OVF user data", - })?; - - // Decompress the data if it's compressed - let decoded = expand_slice_maybe(&decoded_bytes).context(error::DecompressionSnafu { - what: "OVF user data", - })?; - - Ok(decoded) - } - - /// Read and decode user data based on values retrieved from the guestinfo interface - fn guestinfo_user_data() -> Result> { - info!("Attempting to retrieve user data via guestinfo interface"); - - // It would be extremely odd to get here and not be on VMware, but check anyway - ensure!(vmw_backdoor::is_vmware_cpu(), error::NotVmwareSnafu); - - // `guestinfo.userdata.encoding` informs us how to handle the data in the - // `guestinfo.userdata` field - let maybe_encoding = Self::backdoor_get_bytes(Self::GUESTINFO_USERDATA_ENCODING)?; - let user_data_encoding: UserDataEncoding = match maybe_encoding { - Some(val) => { - let encoding_str = String::from_utf8(val).context(error::InvalidUtf8Snafu { - what: Self::GUESTINFO_USERDATA_ENCODING, - })?; - info!("Found user data encoding: {}", encoding_str); - - serde_plain::from_str(&encoding_str).context(error::UnknownEncodingSnafu { - encoding: encoding_str, - })? - } - - // The cloudinit VMware guestinfo data provider assumes any user data without an - // associated encoding means raw data is being passed. We will follow suit here. - None => { - warn!( - "'{}' unset, assuming raw user data", - Self::GUESTINFO_USERDATA_ENCODING - ); - UserDataEncoding::Raw - } - }; - - let user_data_bytes = match Self::backdoor_get_bytes(Self::GUESTINFO_USERDATA)? { - Some(val) => val, - None => return Ok(None), - }; - - let user_data_string = match user_data_encoding { - // gzip+base64 is gzip'ed user data that is base64 encoded - UserDataEncoding::Base64 | UserDataEncoding::GzipBase64 => { - info!("Decoding user data"); - let mut reader = Cursor::new(user_data_bytes); - let decoder = base64::read::DecoderReader::new( - &mut reader, - &base64::engine::general_purpose::STANDARD, - ); - - // Decompresses the data if it is gzip'ed - let mut output = String::new(); - let mut compression_reader = OptionalCompressionReader::new(decoder); - compression_reader.read_to_string(&mut output).context( - error::DecompressionSnafu { - what: "guestinfo user data", - }, - )?; - output - } - - UserDataEncoding::Raw => { - String::from_utf8(user_data_bytes).context(error::InvalidUtf8Snafu { - what: Self::GUESTINFO_USERDATA, - })? - } - }; - - let json = SettingsJson::from_toml_str(user_data_string, "user data from guestinfo") - .context(error::SettingsToJsonSnafu { from: "guestinfo" })?; - Ok(Some(json)) - } - - /// Request a key's value from guestinfo - fn backdoor_get_bytes(key: &str) -> Result>> { - // Probe and access the VMware backdoor. `kernel lockdown(7)` may block "privileged" - // mode because of its use of `iopl()`; the 5.15 kernels have it disabled regardless - // of lockdown mode. If this fails, fall back to "unprivileged" access without first - // requesting access to the relevant IO ports. KVM and VMware both have them special- - // cased in their emulation to not raise an exception to the guest OS and things - // should work out. - let mut backdoor = vmw_backdoor::probe_backdoor_privileged() - .or_else(|e| { - debug!( - "Unable to access guestinfo via privileged mode, using unprivileged: {}", - e - ); - vmw_backdoor::probe_backdoor() - }) - .context(error::BackdoorSnafu { - op: "probe and acquire access", - })?; - - let mut erpc = backdoor - .open_enhanced_chan() - .context(error::BackdoorSnafu { - op: "open eRPC channel", - })?; - - erpc.get_guestinfo(key.as_bytes()) - .context(error::GuestInfoSnafu { what: key }) - } -} - -#[async_trait] -impl PlatformDataProvider for VmwareDataProvider { - async fn platform_data( - &self, - ) -> std::result::Result, Box> { - let mut output = Vec::new(); - - // First read from any site-local defaults. It's unlikely that this file will exist, but - // this is consistent with other platforms. - match local_file::user_data_defaults()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site defaults file: {}", - local_file::USER_DATA_DEFAULTS_FILE - ), - } - - // Attempt to read from a local file next. This comes from the private settings filesystem - // rather than the data storage filesystem, and is also unlikely to exist. - match local_file::user_data()? { - Some(s) => output.push(s), - None => info!( - "No user data found via local file: {}", - local_file::USER_DATA_FILE - ), - } - - // Then look at the CD-ROM for user data. This isn't the preferred method of supplying user - // data, but might still be used. - match Self::cdrom_user_data()? { - Some(s) => output.push(s), - None => info!("No user data found via CD-ROM"), - } - - // Now, check guestinfo which is the preferred method. If it's populated, it will override - // any earlier settings found. - match Self::guestinfo_user_data()? { - Some(s) => output.push(s), - None => warn!("No user data found via guestinfo"), - } - - // Finally, apply any site-local overrides. It's unlikely to exist but again, this is - // consistent with other platforms. - match local_file::user_data_overrides()? { - Some(s) => output.push(s), - None => info!( - "No user data found via site overrides file: {}", - local_file::USER_DATA_OVERRIDES_FILE - ), - } - - Ok(output) - } -} - -// =^..^= =^..^= =^..^= =^..^= - -// Acceptable user data encodings -// When case-insensitive de/serialization is finalized, that's what we would want to use -// here instead of aliases: https://github.com/serde-rs/serde/pull/1902 -#[derive(Debug, Deserialize)] -enum UserDataEncoding { - #[serde(alias = "b64")] - #[serde(alias = "B64")] - #[serde(alias = "base64")] - Base64, - #[serde(alias = "gz+b64")] - #[serde(alias = "Gz+B64")] - #[serde(alias = "gzip+base64")] - #[serde(alias = "Gzip+Base64")] - GzipBase64, - Raw, -} - -// =^..^= =^..^= =^..^= =^..^= - -// Minimal expected structure for an OVF file with user data -#[derive(Debug, Deserialize)] -struct Environment { - #[serde(rename = "PropertySection", default)] - pub property_section: PropertySection, -} - -#[derive(Default, Debug, Deserialize)] -struct PropertySection { - #[serde(rename = "Property", default)] - pub properties: Vec, -} - -#[derive(Debug, Deserialize)] -struct Property { - pub key: String, - pub value: String, -} - -// =^..^= =^..^= =^..^= =^..^= - -mod error { - use snafu::Snafu; - use std::io; - use std::path::PathBuf; - - #[derive(Debug, Snafu)] - #[snafu(visibility(pub(super)))] - pub(crate) enum Error { - #[snafu(display("VMware backdoor: failed to '{}': '{}'", op, source))] - Backdoor { - op: String, - source: vmw_backdoor::VmwError, - }, - - #[snafu(display("Unable to decode base64 in {}: '{}'", what, source))] - Base64Decode { - what: String, - source: base64::DecodeError, - }, - - #[snafu(display("Failed to decompress {}: {}", what, source))] - Decompression { what: String, source: io::Error }, - - #[snafu(display("Failed to fetch key '{}' from guestinfo: {}", what, source))] - GuestInfo { - what: String, - source: vmw_backdoor::VmwError, - }, - - #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] - InputFileRead { path: PathBuf, source: io::Error }, - - #[snafu(display("'{}' contains invalid utf-8: {}", what, source))] - InvalidUtf8 { - what: String, - source: std::string::FromUtf8Error, - }, - - #[snafu(display( - "Unable to read user data from guestinfo, this is not a VMware virtual CPU" - ))] - NotVmware, - - #[snafu(display("Unable to serialize settings from {}: {}", from, source))] - SettingsToJson { - from: String, - source: crate::settings::Error, - }, - - #[snafu(display("Unknown user data encoding: '{}': {}", encoding, source))] - UnknownEncoding { - encoding: String, - source: serde_plain::Error, - }, - - #[snafu(display("Found multiple user data files in '{}', expected 1", place))] - UserDataFileCount { place: String }, - - #[snafu(display("Unable to deserialize XML from: '{}': {}", path.display(), source))] - XmlDeserialize { - path: PathBuf, - source: serde_xml_rs::Error, - }, - } -} - -type Result = std::result::Result; - -#[cfg(test)] -mod test { - use super::*; - use std::path::PathBuf; - - fn test_data() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data") - } - - #[test] - fn test_read_xml_user_data_namespaced_keys() { - let xml = test_data().join("namespaced_keys.xml"); - let expected_user_data = "settings.motd = \"hello\""; - - let actual_user_data = VmwareDataProvider::ovf_user_data(xml).unwrap(); - - assert_eq!(actual_user_data, expected_user_data) - } - - #[test] - fn test_read_xml_user_data() { - let xml = test_data().join("ovf-env.xml"); - let expected_user_data = "settings.motd = \"hello\""; - - let actual_user_data = VmwareDataProvider::ovf_user_data(xml).unwrap(); - - assert_eq!(actual_user_data, expected_user_data) - } -} diff --git a/sources/early-boot-config/early-boot-config-provider/Cargo.toml b/sources/early-boot-config/early-boot-config-provider/Cargo.toml new file mode 100644 index 00000000000..001c93dc4f6 --- /dev/null +++ b/sources/early-boot-config/early-boot-config-provider/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "early-boot-config-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning std::result::Result, Box>; +} + +/// This trait is the same as UserDataProvider, but it allows for async data sources, such as IMDS. +#[async_trait] +pub trait AsyncUserDataProvider { + async fn user_data( + &self, + ) -> std::result::Result, Box>; +} + +/// Provides the standard output format of a user data provider. Takes a result returned by a user +/// data provider, checks for errors, and returns the proper exit code. If the user data was +/// returned successfully, this will print its JSON to stdout. +pub fn print_userdata_output( + user_data: std::result::Result, Box>, +) -> ExitCode { + let (exit_code, output) = match user_data { + Ok(Some(user_data)) => match serde_json::to_string(&user_data) { + Ok(json) => (ExitCode::SUCCESS, json), + Err(e) => ( + ExitCode::FAILURE, + format!("Failed to serialize user data as JSON: {}", e), + ), + }, + Ok(None) => (ExitCode::SUCCESS, String::new()), + Err(e) => (ExitCode::FAILURE, format!("{}", e)), + }; + println!("{}", output); + exit_code +} + +/// Convenience function to set up logging for provider binaries. +/// +/// Since provider binaries return their output to early-boot-config on stdout, we want to make +/// sure all logging happens to stderr. For debugging purposes, the binaries' log level may be +/// configured via environment variable. +pub fn setup_provider_logging() { + // Filter at info level by default unless configured via environment variable + let log_level = Env::default().filter_or(LOG_LEVEL_ENV_VAR, "info"); + env_logger::Builder::from_env(log_level) + .format_module_path(false) + .target(Target::Stderr) + .write_style(WriteStyle::Never) + .init() +} + +/// Read user data from a given path, decompressing if necessary +pub fn user_data_from_file>( + path: P, +) -> std::result::Result, Box> { + let path = path.as_ref(); + + if !path.exists() { + info!("{} does not exist, not using it", path.display()); + return Ok(None); + } + info!("'{}' exists, using it", path.display()); + + // Read the file, decompressing it if compressed. + let user_data_str = expand_file_maybe(path).context(error::InputFileReadSnafu { path })?; + + if user_data_str.is_empty() { + warn!("{} exists but is empty", path.display()); + return Ok(None); + } + + trace!("Received user data: {}", user_data_str); + let desc = format!("user data from {}", path.display()); + let json = SettingsJson::from_toml_str(&user_data_str, desc) + .context(error::SettingsToJSONSnafu { from: path })?; + + Ok(Some(json)) +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub enum Error { + #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] + InputFileRead { path: PathBuf, source: io::Error }, + + #[snafu(display("Unable to serialize settings from {}: {}", from.display(), source))] + SettingsToJSON { + from: PathBuf, + source: crate::settings::Error, + }, + } +} diff --git a/sources/api/early-boot-config/src/settings.rs b/sources/early-boot-config/early-boot-config-provider/src/settings.rs similarity index 83% rename from sources/api/early-boot-config/src/settings.rs rename to sources/early-boot-config/early-boot-config-provider/src/settings.rs index 7a07f31ec64..5abd31d679f 100644 --- a/sources/api/early-boot-config/src/settings.rs +++ b/sources/early-boot-config/early-boot-config-provider/src/settings.rs @@ -1,14 +1,14 @@ //! The settings module owns the `SettingsJson` struct which contains the JSON settings data being //! sent to the API. -use serde::Serialize; +use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt}; /// SettingsJson represents a change that a provider would like to make in the API. -#[derive(Debug)] -pub(crate) struct SettingsJson { - pub(crate) json: String, - pub(crate) desc: String, +#[derive(Debug, Serialize, Deserialize)] +pub struct SettingsJson { + pub json: serde_json::Value, + pub desc: String, } impl SettingsJson { @@ -18,12 +18,12 @@ impl SettingsJson { /// The serializable object is typically something like a toml::Value or serde_json::Value, /// since they can be easily deserialized from text input in the platform, and manipulated as /// desired. - pub(crate) fn from_val(data: &impl Serialize, desc: S) -> Result + pub fn from_val(data: &impl Serialize, desc: S) -> Result where S: Into, { Ok(Self { - json: serde_json::to_string(&data).context(error::SettingsToJSONSnafu)?, + json: serde_json::to_value(data).context(error::SettingsToJSONSnafu)?, desc: desc.into(), }) } @@ -33,7 +33,7 @@ impl SettingsJson { /// /// This method takes care of the easy-to-miss task of removing the outer `settings` layer from /// the TOML data before it gets submitted to the API. - pub(crate) fn from_toml_str(data: S1, desc: S2) -> Result + pub fn from_toml_str(data: S1, desc: S2) -> Result where S1: AsRef, S2: Into, @@ -56,7 +56,7 @@ mod error { #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] - pub(crate) enum Error { + pub enum Error { #[snafu(display("Error serializing settings to JSON: {}", source))] SettingsToJSON { source: serde_json::error::Error }, @@ -71,5 +71,5 @@ mod error { } } -pub(crate) use error::Error; +pub use error::Error; type Result = std::result::Result; diff --git a/sources/api/early-boot-config/.gitignore b/sources/early-boot-config/early-boot-config/.gitignore similarity index 100% rename from sources/api/early-boot-config/.gitignore rename to sources/early-boot-config/early-boot-config/.gitignore diff --git a/sources/api/early-boot-config/Cargo.toml b/sources/early-boot-config/early-boot-config/Cargo.toml similarity index 51% rename from sources/api/early-boot-config/Cargo.toml rename to sources/early-boot-config/early-boot-config/Cargo.toml index cb66f78fb70..9474a841eb0 100644 --- a/sources/api/early-boot-config/Cargo.toml +++ b/sources/early-boot-config/early-boot-config/Cargo.toml @@ -10,32 +10,22 @@ build = "build.rs" exclude = ["README.md"] [dependencies] -apiclient = { path = "../apiclient", version = "0.1" } +apiclient = { path = "../../api/apiclient", version = "0.1" } async-trait = "0.1" base64 = "0.21" constants = { path = "../../constants", version = "0.1" } -flate2 = { version = "1", default-features = false, features = ["rust_backend"] } +early-boot-config-provider = { path = "../early-boot-config-provider", version = "0.1" } +env_logger = "0.10" http = "0.2" -imdsclient = { path = "../../imdsclient", version = "0.1" } log = "0.4" -retry-read = { path = "../../retry-read", version = "0.1" } -serde = { version = "1", features = ["derive"] } serde_json = "1" serde_plain = "1" serde-xml-rs = "0.6" simplelog = "0.12" snafu = "0.8" -tokio = { version = "~1.32", default-features = false, features = ["macros", "rt-multi-thread"] } # LTS +tokio = { version = "~1.32", default-features = false, features = ["process", "macros", "rt-multi-thread"] } # LTS toml = "0.8" - -[target.'cfg(target_arch = "x86_64")'.dependencies] -# vmw_backdoor includes x86_64 assembly, prevent it from building for ARM -vmw_backdoor = "0.2" +walkdir = "2.4" [build-dependencies] -bottlerocket-variant = { version = "0.1", path = "../../bottlerocket-variant" } generate-readme = { version = "0.1", path = "../../generate-readme" } - -[dev-dependencies] -hex-literal = "0.3" -lazy_static = "1" diff --git a/sources/early-boot-config/early-boot-config/README.md b/sources/early-boot-config/early-boot-config/README.md new file mode 100644 index 00000000000..f82ddc3392f --- /dev/null +++ b/sources/early-boot-config/early-boot-config/README.md @@ -0,0 +1,15 @@ +# early-boot-config + +Current version: 0.1.0 + +## Introduction + +early-boot-config sends user data to the Bottlerocket API. + +Variants include their required user data provider binaries via packages. early-boot-config discovers these binaries at runtime in `/usr/libexec/early-boot-config/data-providers.d` and runs them in order, sending any user data found to the API. + +User data provider binaries each implement the ability to obtain user data from a single source. Sources include local files, AWS Instance Metadata Service (IMDS), among others. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/api/early-boot-config/README.tpl b/sources/early-boot-config/early-boot-config/README.tpl similarity index 100% rename from sources/api/early-boot-config/README.tpl rename to sources/early-boot-config/early-boot-config/README.tpl diff --git a/sources/early-boot-config/early-boot-config/build.rs b/sources/early-boot-config/early-boot-config/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/early-boot-config/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/early-boot-config/src/main.rs b/sources/early-boot-config/early-boot-config/src/main.rs new file mode 100644 index 00000000000..42a3520b46d --- /dev/null +++ b/sources/early-boot-config/early-boot-config/src/main.rs @@ -0,0 +1,324 @@ +/*! +# Introduction + +early-boot-config sends user data to the Bottlerocket API. + +Variants include their required user data provider binaries via packages. early-boot-config discovers these binaries at runtime in `/usr/libexec/early-boot-config/data-providers.d` and runs them in order, sending any user data found to the API. + +User data provider binaries each implement the ability to obtain user data from a single source. Sources include local files, AWS Instance Metadata Service (IMDS), among others. +*/ + +#[macro_use] +extern crate log; + +use early_boot_config_provider::settings::SettingsJson; +use early_boot_config_provider::LOG_LEVEL_ENV_VAR; +use env_logger::{Target, WriteStyle}; +use log::LevelFilter; +use snafu::{ensure, ResultExt}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::str::{self, FromStr}; +use std::{env, io, process}; +use tokio::process::Command as AsyncCommand; +use walkdir::WalkDir; + +// We only want to run early-boot-config once, at first boot. Our systemd unit file has a +// ConditionPathExists that will prevent it from running again if this file exists. +// We create it after running successfully. +const MARKER_FILE: &str = "/var/lib/bottlerocket/early-boot-config.ran"; +/// The directory containing user data provider binaries +const PROVIDERS_DIR: &str = "/usr/libexec/early-boot-config/data-providers.d"; + +/// Store the args we receive on the command line +#[derive(Debug)] +struct Args { + log_level: LevelFilter, + socket_path: String, +} + +/// Print a usage message in the event a bad arg is passed +fn usage() -> ! { + let program_name = env::args().next().unwrap_or_else(|| "program".to_string()); + eprintln!( + r"Usage: {} + [ --socket-path PATH ] + [ --log-level trace|debug|info|warn|error ] + + Socket path defaults to {}", + program_name, + constants::API_SOCKET, + ); + process::exit(2); +} + +/// Prints a more specific message before exiting through usage(). +fn usage_msg>(msg: S) -> ! { + eprintln!("{}\n", msg.as_ref()); + usage(); +} + +/// Parse the args to the program and return an Args struct +fn parse_args(args: env::Args) -> Args { + let mut log_level = None; + let mut socket_path = None; + + let mut iter = args.skip(1); + while let Some(arg) = iter.next() { + match arg.as_ref() { + "--socket-path" => { + socket_path = Some( + iter.next() + .unwrap_or_else(|| usage_msg("Did not give argument to --socket-path")), + ) + } + + "--log-level" => { + let log_level_str = iter + .next() + .unwrap_or_else(|| usage_msg("Did not give argument to --log-level")); + log_level = Some(LevelFilter::from_str(&log_level_str).unwrap_or_else(|_| { + usage_msg(format!("Invalid log level '{}'", log_level_str)) + })); + } + + _ => usage(), + } + } + + Args { + log_level: log_level.unwrap_or(LevelFilter::Info), + socket_path: socket_path.unwrap_or_else(|| constants::API_SOCKET.to_string()), + } +} + +/// Gather user data providers to run in order +fn gather_providers() -> Result> { + Ok(WalkDir::new(PROVIDERS_DIR) + .max_depth(1) + .min_depth(1) + .sort_by_file_name() + .into_iter() + .collect::, _>>()? + .into_iter() + .filter(|f| f.path().is_symlink()) + .map(|f| f.into_path()) + .collect()) +} + +/// Run a user data provider binary +async fn run_provider

(log_level: &LevelFilter, provider: P) -> io::Result +where + P: AsRef, +{ + let provider = provider.as_ref(); + AsyncCommand::new(provider) + .env(LOG_LEVEL_ENV_VAR, log_level.as_str()) + .output() + .await +} + +/// Check that a user data provider succeeded and forward its logs +fn check_provider_status

(provider: P, output: &process::Output) -> Result<()> +where + P: AsRef, +{ + let provider = provider.as_ref(); + // Regardless of provider status, log its output + let provider_name = provider + .file_name() + .unwrap_or(provider.as_os_str()) + .to_string_lossy(); + let provider_logs = String::from_utf8_lossy(&output.stderr); + for line in provider_logs.lines() { + info!("Provider '{}': {}", provider_name, line); + } + + ensure!( + output.status.success(), + error::ProviderFailureSnafu { + provider: &provider, + message: String::from_utf8_lossy(&output.stdout), + } + ); + + Ok(()) +} + +/// Submit user data to the API +async fn submit_user_data(socket_path: S, user_data: serde_json::Value) -> Result<()> +where + S: AsRef, +{ + let socket_path = socket_path.as_ref(); + let uri = &format!( + "{}?tx={}", + constants::API_SETTINGS_URI, + constants::LAUNCH_TRANSACTION + ); + let method = "PATCH"; + trace!("Request body: {}", user_data); + + let (code, response_body) = + apiclient::raw_request(socket_path, uri, method, Some(user_data.to_string())) + .await + .context(error::APIRequestSnafu { method, uri })?; + + ensure!( + code.is_success(), + error::ResponseSnafu { + method, + uri, + code, + response_body, + } + ); + Ok(()) +} + +async fn run() -> Result<()> { + // Parse and store the args passed to the program + let args = parse_args(env::args()); + + env_logger::Builder::new() + .filter_level(args.log_level) + .format_module_path(false) + .target(Target::Stdout) + .write_style(WriteStyle::Never) + .init(); + + info!("early-boot-config started"); + + info!("Gathering user data providers"); + let mut threads = Vec::new(); + let providers = gather_providers()?; + for provider in providers { + threads.push(( + provider.clone(), + tokio::spawn(async move { run_provider(&args.log_level, &provider).await }), + )); + } + + for (provider, handle) in threads { + let result = + handle + .await + .context(error::ThreadSnafu)? + .context(error::CommandFailureSnafu { + provider: provider.clone(), + })?; + check_provider_status(&provider, &result)?; + + // User data providers output a serialized `SettingsJson` if they are successful in finding + // user data at their respective source. Output will be empty otherwise. + // + // Read into a string first to ensure UTF8 and strip any whitespace/newlines + let output_raw = str::from_utf8(&result.stdout) + .context(error::ProviderOutputSnafu { + provider: &provider, + })? + .trim() + .to_string(); + trace!("Provider '{}' output: {}", &provider.display(), &output_raw); + + if output_raw.is_empty() { + info!("No user data found via {}", &provider.display()); + continue; + } + + let output: SettingsJson = + serde_json::from_str(&output_raw).context(error::ProviderJsonSnafu { provider })?; + + info!("Found user data via {}, sending to API", output.desc); + submit_user_data(&args.socket_path, output.json).await?; + } + + fs::write(MARKER_FILE, "").unwrap_or_else(|e| { + warn!( + "Failed to create marker file {}, may unexpectedly run again: {}", + MARKER_FILE, e + ) + }); + + Ok(()) +} + +// Returning a Result from main makes it print a Debug representation of the error, but with Snafu +// we have nice Display representations of the error, so we wrap "main" (run) and print any error. +// https://github.com/shepmaster/snafu/issues/110 +#[tokio::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("{}", e); + process::exit(1); + } +} + +mod error { + use crate::PROVIDERS_DIR; + use http::StatusCode; + use snafu::Snafu; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(super) enum Error { + #[snafu(display("Error {}ing '{}': {}", method, uri, source))] + APIRequest { + method: String, + uri: String, + #[snafu(source(from(apiclient::Error, Box::new)))] + source: Box, + }, + + #[snafu(display("Failed to start provider '{}': {}", provider.display(), source))] + CommandFailure { + provider: PathBuf, + source: std::io::Error, + }, + + #[snafu(display("Provider error: {}", source))] + Provider { source: Box }, + + #[snafu(display("Provider '{}' failed: {}", provider.display(), message))] + ProviderFailure { provider: PathBuf, message: String }, + + #[snafu(display( + "Error deserializing provider output as JSON from {}: '{}'", + provider.display(), + source, + ))] + ProviderJson { + provider: PathBuf, + source: serde_json::Error, + }, + + #[snafu(display("Invalid (non-utf8) output from provider '{}': {}", provider.display(), source))] + ProviderOutput { + provider: PathBuf, + source: std::str::Utf8Error, + }, + + #[snafu(display("Error {} when {}ing '{}': {}", code, method, uri, response_body))] + Response { + method: String, + uri: String, + code: StatusCode, + response_body: String, + }, + + #[snafu(display("Thread execution error: {}", source))] + Thread { source: tokio::task::JoinError }, + + #[snafu(display("Logger setup error: {}", source))] + Logger { source: log::SetLoggerError }, + + #[snafu( + display("Unable to walk providers directory '{}': {}", PROVIDERS_DIR, source), + context(false) + )] + WalkDir { source: walkdir::Error }, + } +} + +type Result = std::result::Result; diff --git a/sources/early-boot-config/user-data-providers/README.md b/sources/early-boot-config/user-data-providers/README.md new file mode 100644 index 00000000000..a8cc7c174de --- /dev/null +++ b/sources/early-boot-config/user-data-providers/README.md @@ -0,0 +1,11 @@ +# user-data-providers + +## Introduction + +user-data-providers contains the user data provider binaries used by early-boot-config to set settings on boot. These binaries implement the interface defined in early-boot-config-provider. + +When installed, these binaries should be linked to in `/usr/libexec/early-boot-config/data-providers.d/`. The binaries will be executed by early-boot-config in order based on the two numbers at the start of the link name, e.g.: + +1. `10-local-defaults` +2. `20-local-file` +3. `99-local-overrides` diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/Cargo.toml b/sources/early-boot-config/user-data-providers/ec2-identity-doc/Cargo.toml new file mode 100644 index 00000000000..65caef016da --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ec2-identity-doc-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +async-trait = "0.1" +imdsclient = { path = "../../../imdsclient", version = "0.1" } +log = "0.4" +serde_json = "1" +snafu = "0.8" +tokio = { version = "~1.32", default-features = false, features = ["process", "macros", "rt-multi-thread"] } # LTS +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.md b/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.md new file mode 100644 index 00000000000..b8971d08f9c --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.md @@ -0,0 +1,13 @@ +# ec2-identity-doc-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to generate user data from data in the EC2 instance identity document. + +Currently used only to fetch the AWS region. Falls back to IMDS if the region is not found in the instance identity document. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.tpl b/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/build.rs b/sources/early-boot-config/user-data-providers/ec2-identity-doc/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/lib.rs b/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/lib.rs new file mode 100644 index 00000000000..73bde5901ab --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/lib.rs @@ -0,0 +1,111 @@ +/// EC2 Identity Document +#[macro_use] +extern crate log; + +use async_trait::async_trait; +use early_boot_config_provider::provider::AsyncUserDataProvider; +use early_boot_config_provider::settings::SettingsJson; +use imdsclient::ImdsClient; +use serde_json::json; +use snafu::{OptionExt, ResultExt}; +use std::{fs, path::Path}; + +const IDENTITY_DOCUMENT_FILE: &str = "/etc/early-boot-config/identity-document"; +const FALLBACK_REGION: &str = "us-east-1"; + +pub struct Ec2IdentityDoc; + +impl Ec2IdentityDoc { + async fn fetch_region() -> Result { + let region = if Path::new(IDENTITY_DOCUMENT_FILE).exists() { + info!("'{}' exists, using it", IDENTITY_DOCUMENT_FILE); + let data = + fs::read_to_string(IDENTITY_DOCUMENT_FILE).context(error::InputFileReadSnafu { + path: IDENTITY_DOCUMENT_FILE, + })?; + let iid: serde_json::Value = + serde_json::from_str(&data).context(error::DeserializeJsonSnafu)?; + + iid.get("region") + .context(error::IdentityDocMissingDataSnafu { missing: "region" })? + .as_str() + .context(error::WrongTypeSnafu { + field_name: "region", + expected_type: "string", + })? + .to_owned() + } else { + info!("Using IMDS for region"); + let mut client = ImdsClient::new(); + + client + .fetch_region() + .await + .context(error::ImdsRequestSnafu)? + .unwrap_or_else(|| FALLBACK_REGION.to_owned()) + }; + + Ok(region) + } +} + +#[async_trait] +impl AsyncUserDataProvider for Ec2IdentityDoc { + async fn user_data( + &self, + ) -> std::result::Result, Box> { + let region = Self::fetch_region().await?; + + trace!( + "Retrieved region from instance identity document: {}", + region + ); + let val = json!({ "aws": {"region": region} }); + let json = SettingsJson::from_val(&val, "EC2 instance identity document").context( + error::SettingsToJSONSnafu { + from: "instance identity document", + }, + )?; + + Ok(Some(json)) + } +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(crate) enum Error { + #[snafu(display("Error deserializing from JSON: {}", source))] + DeserializeJson { source: serde_json::error::Error }, + + #[snafu(display("Instance identity document missing {}", missing))] + IdentityDocMissingData { missing: String }, + + #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] + InputFileRead { path: PathBuf, source: io::Error }, + + #[snafu(display("IMDS request failed: {}", source))] + ImdsRequest { source: imdsclient::Error }, + + #[snafu(display("Unable to serialize settings from {}: {}", from, source))] + SettingsToJSON { + from: String, + source: early_boot_config_provider::settings::Error, + }, + + #[snafu(display( + "Wrong type while deserializing, expected '{}' to be type '{}'", + field_name, + expected_type + ))] + WrongType { + field_name: &'static str, + expected_type: &'static str, + }, + } +} +type Result = std::result::Result; diff --git a/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/main.rs b/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/main.rs new file mode 100644 index 00000000000..efe788c644c --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-identity-doc/src/main.rs @@ -0,0 +1,19 @@ +/*! +# Introduction + +User data provider binary used to generate user data from data in the EC2 instance identity document. + +Currently used only to fetch the AWS region. Falls back to IMDS if the region is not found in the instance identity document. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, AsyncUserDataProvider, +}; +use ec2_identity_doc_user_data_provider::Ec2IdentityDoc; +use std::process::ExitCode; + +#[tokio::main] +async fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(Ec2IdentityDoc.user_data().await) +} diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/Cargo.toml b/sources/early-boot-config/user-data-providers/ec2-imds/Cargo.toml new file mode 100644 index 00000000000..125a9ed9ef6 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ec2-imds-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +async-trait = "0.1" +imdsclient = { path = "../../../imdsclient", version = "0.1" } +log = "0.4" +snafu = "0.8" +tokio = { version = "~1.32", default-features = false, features = ["process", "macros", "rt-multi-thread"] } # LTS +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/README.md b/sources/early-boot-config/user-data-providers/ec2-imds/README.md new file mode 100644 index 00000000000..a63ce450899 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/README.md @@ -0,0 +1,11 @@ +# ec2-imds-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to fetch user data passed to an EC2 instance via the EC2 Instance Metadata Service (IMDS). + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/README.tpl b/sources/early-boot-config/user-data-providers/ec2-imds/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/build.rs b/sources/early-boot-config/user-data-providers/ec2-imds/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/src/lib.rs b/sources/early-boot-config/user-data-providers/ec2-imds/src/lib.rs new file mode 100644 index 00000000000..1b8214dd6c3 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/src/lib.rs @@ -0,0 +1,68 @@ +/// EC2 Instance Metadata Service +#[macro_use] +extern crate log; + +use async_trait::async_trait; +use early_boot_config_provider::compression::expand_slice_maybe; +use early_boot_config_provider::provider::AsyncUserDataProvider; +use early_boot_config_provider::settings::SettingsJson; +use imdsclient::ImdsClient; +use snafu::ResultExt; + +pub struct Ec2Imds; + +#[async_trait] +impl AsyncUserDataProvider for Ec2Imds { + async fn user_data( + &self, + ) -> std::result::Result, Box> { + let mut client = ImdsClient::new(); + + info!("Fetching user data from IMDS"); + let user_data_raw = match client + .fetch_userdata() + .await + .context(error::ImdsRequestSnafu)? + { + Some(user_data_raw) => user_data_raw, + None => return Ok(None), + }; + + let user_data_str = expand_slice_maybe(&user_data_raw) + .context(error::DecompressionSnafu { what: "user data" })?; + + if user_data_str.trim().is_empty() { + warn!("No user data found via IMDS"); + return Ok(None); + } + + trace!("Received user data: {}", user_data_str); + let json = SettingsJson::from_toml_str(&user_data_str, "EC2 IMDS").context( + error::SettingsToJSONSnafu { + from: "instance user data", + }, + )?; + Ok(Some(json)) + } +} + +mod error { + use snafu::Snafu; + use std::io; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(crate) enum Error { + #[snafu(display("Failed to decompress {}: {}", what, source))] + Decompression { what: String, source: io::Error }, + + #[snafu(display("IMDS request failed: {}", source))] + ImdsRequest { source: imdsclient::Error }, + + #[snafu(display("Unable to serialize settings from {}: {}", from, source))] + SettingsToJSON { + from: String, + source: early_boot_config_provider::settings::Error, + }, + } +} diff --git a/sources/early-boot-config/user-data-providers/ec2-imds/src/main.rs b/sources/early-boot-config/user-data-providers/ec2-imds/src/main.rs new file mode 100644 index 00000000000..71b5e01098b --- /dev/null +++ b/sources/early-boot-config/user-data-providers/ec2-imds/src/main.rs @@ -0,0 +1,17 @@ +/*! +# Introduction + +User data provider binary used to fetch user data passed to an EC2 instance via the EC2 Instance Metadata Service (IMDS). +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, AsyncUserDataProvider, +}; +use ec2_imds_user_data_provider::Ec2Imds; +use std::process::ExitCode; + +#[tokio::main] +async fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(Ec2Imds.user_data().await) +} diff --git a/sources/early-boot-config/user-data-providers/local-defaults/Cargo.toml b/sources/early-boot-config/user-data-providers/local-defaults/Cargo.toml new file mode 100644 index 00000000000..cc15c0cd806 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "local-defaults-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/local-defaults/README.md b/sources/early-boot-config/user-data-providers/local-defaults/README.md new file mode 100644 index 00000000000..8940be86628 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/README.md @@ -0,0 +1,11 @@ +# local-defaults-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to fetch the default user data provided in the file tree under `/local/user-data-defaults.toml`. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-defaults/README.tpl b/sources/early-boot-config/user-data-providers/local-defaults/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-defaults/build.rs b/sources/early-boot-config/user-data-providers/local-defaults/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/local-defaults/src/lib.rs b/sources/early-boot-config/user-data-providers/local-defaults/src/lib.rs new file mode 100644 index 00000000000..84dd80e7271 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/src/lib.rs @@ -0,0 +1,13 @@ +/// Site-local defaults +use early_boot_config_provider::provider::{user_data_from_file, UserDataProvider}; +use early_boot_config_provider::settings::SettingsJson; + +const LOCAL_DEFAULTS_FILE: &str = "/local/user-data-defaults.toml"; + +pub struct LocalDefaults; + +impl UserDataProvider for LocalDefaults { + fn user_data(&self) -> Result, Box> { + user_data_from_file(LOCAL_DEFAULTS_FILE) + } +} diff --git a/sources/early-boot-config/user-data-providers/local-defaults/src/main.rs b/sources/early-boot-config/user-data-providers/local-defaults/src/main.rs new file mode 100644 index 00000000000..f015007f3f8 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-defaults/src/main.rs @@ -0,0 +1,16 @@ +/*! +# Introduction + +User data provider binary used to fetch the default user data provided in the file tree under `/local/user-data-defaults.toml`. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, UserDataProvider, +}; +use local_defaults_user_data_provider::LocalDefaults; +use std::process::ExitCode; + +fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(LocalDefaults.user_data()) +} diff --git a/sources/early-boot-config/user-data-providers/local-file/Cargo.toml b/sources/early-boot-config/user-data-providers/local-file/Cargo.toml new file mode 100644 index 00000000000..a334e1a99f1 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "local-file-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/local-file/README.md b/sources/early-boot-config/user-data-providers/local-file/README.md new file mode 100644 index 00000000000..e47651b50aa --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/README.md @@ -0,0 +1,11 @@ +# local-file-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to fetch the user data provided in the file tree under `/var/lib/bottlerocket/user-data.toml`. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-file/README.tpl b/sources/early-boot-config/user-data-providers/local-file/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-file/build.rs b/sources/early-boot-config/user-data-providers/local-file/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/local-file/src/lib.rs b/sources/early-boot-config/user-data-providers/local-file/src/lib.rs new file mode 100644 index 00000000000..1e0854ee1d2 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/src/lib.rs @@ -0,0 +1,13 @@ +/// Local user data file +use early_boot_config_provider::provider::{user_data_from_file, UserDataProvider}; +use early_boot_config_provider::settings::SettingsJson; + +const LOCAL_USER_DATA: &str = "/var/lib/bottlerocket/user-data.toml"; + +pub struct LocalUserData; + +impl UserDataProvider for LocalUserData { + fn user_data(&self) -> Result, Box> { + user_data_from_file(LOCAL_USER_DATA) + } +} diff --git a/sources/early-boot-config/user-data-providers/local-file/src/main.rs b/sources/early-boot-config/user-data-providers/local-file/src/main.rs new file mode 100644 index 00000000000..6c8a3a22f79 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-file/src/main.rs @@ -0,0 +1,16 @@ +/*! +# Introduction + +User data provider binary used to fetch the user data provided in the file tree under `/var/lib/bottlerocket/user-data.toml`. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, UserDataProvider, +}; +use local_file_user_data_provider::LocalUserData; +use std::process::ExitCode; + +fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(LocalUserData.user_data()) +} diff --git a/sources/early-boot-config/user-data-providers/local-overrides/Cargo.toml b/sources/early-boot-config/user-data-providers/local-overrides/Cargo.toml new file mode 100644 index 00000000000..121950e5c5e --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "local-overrides-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/local-overrides/README.md b/sources/early-boot-config/user-data-providers/local-overrides/README.md new file mode 100644 index 00000000000..32c44361712 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/README.md @@ -0,0 +1,11 @@ +# local-overrides-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to fetch the user data overrides provided in the file tree under `/local/user-data-overrides.toml`. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-overrides/README.tpl b/sources/early-boot-config/user-data-providers/local-overrides/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/local-overrides/build.rs b/sources/early-boot-config/user-data-providers/local-overrides/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/local-overrides/src/lib.rs b/sources/early-boot-config/user-data-providers/local-overrides/src/lib.rs new file mode 100644 index 00000000000..06fa9869215 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/src/lib.rs @@ -0,0 +1,13 @@ +/// Site-local overrides +use early_boot_config_provider::provider::{user_data_from_file, UserDataProvider}; +use early_boot_config_provider::settings::SettingsJson; + +const LOCAL_OVERRIDES: &str = "/local/user-data-overrides.toml"; + +pub struct LocalOverrides; + +impl UserDataProvider for LocalOverrides { + fn user_data(&self) -> std::result::Result, Box> { + user_data_from_file(LOCAL_OVERRIDES) + } +} diff --git a/sources/early-boot-config/user-data-providers/local-overrides/src/main.rs b/sources/early-boot-config/user-data-providers/local-overrides/src/main.rs new file mode 100644 index 00000000000..4219ece7435 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/local-overrides/src/main.rs @@ -0,0 +1,16 @@ +/*! +# Introduction + +User data provider binary used to fetch the user data overrides provided in the file tree under `/local/user-data-overrides.toml`. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, UserDataProvider, +}; +use local_overrides_user_data_provider::LocalOverrides; +use std::process::ExitCode; + +fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(LocalOverrides.user_data()) +} diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/Cargo.toml b/sources/early-boot-config/user-data-providers/vmware-cd-rom/Cargo.toml new file mode 100644 index 00000000000..65b0014311b --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "vmware-cd-rom-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +base64 = "0.21" +log = "0.4" +serde = { version = "1", features = ["derive"] } +serde-xml-rs = "0.6" +snafu = "0.8" +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.md b/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.md new file mode 100644 index 00000000000..69b75bd9dde --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.md @@ -0,0 +1,11 @@ +# vmware-cd-rom-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary that fetches user data provided via CD-ROM to a VMware VM. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.tpl b/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/build.rs b/sources/early-boot-config/user-data-providers/vmware-cd-rom/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/lib.rs b/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/lib.rs new file mode 100644 index 00000000000..4bd06056a32 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/lib.rs @@ -0,0 +1,243 @@ +/// VMware CD-ROM +#[macro_use] +extern crate log; + +use base64::Engine; +use early_boot_config_provider::compression::{ + expand_file_maybe, expand_slice_maybe, OptionalCompressionReader, +}; +use early_boot_config_provider::provider::UserDataProvider; +use early_boot_config_provider::settings::SettingsJson; +use serde::Deserialize; +use snafu::{ensure, ResultExt}; +use std::ffi::OsStr; +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +// This program expects that the CD-ROM is already mounted. Mounting happens elsewhere in a +// systemd unit file +const CD_ROM_MOUNT: &str = "/media/cdrom"; +// A mounted CD-ROM may contain an OVF file or a user-supplied file named `user-data` +const USER_DATA_FILENAMES: [&str; 5] = [ + "user-data", + "ovf-env.xml", + "OVF-ENV.XML", + "ovf_env.xml", + "OVF_ENV.XML", +]; + +pub struct VmwareCdRom; + +impl VmwareCdRom { + // Using USER_DATA_FILENAMES, determine if any exist and if so, only 1 exists + fn user_data_path() -> Result> { + let mut user_data_files = USER_DATA_FILENAMES + .iter() + .map(|filename| Path::new(CD_ROM_MOUNT).join(filename)) + .filter(|file| file.exists()); + + let user_data_file = match user_data_files.next() { + Some(file) => file, + None => return Ok(None), + }; + + ensure!( + user_data_files.next().is_none(), + error::UserDataFileCountSnafu { + place: CD_ROM_MOUNT + } + ); + + Ok(Some(user_data_file)) + } + /// Read and base64 decode user data contained in an OVF file + // In VMware, user data is supplied to the host via an XML file. Within + // the XML file, there is a `PropertySection` that contains `Property` elements + // with attributes. User data is base64 encoded inside a `Property` element with + // the attribute "user-data". + // + fn ovf_user_data>(path: P) -> Result { + let path = path.as_ref(); + let file = File::open(path).context(error::InputFileReadSnafu { path })?; + let reader = OptionalCompressionReader::new(BufReader::new(file)); + + // Deserialize the OVF file, dropping everything we don't care about + let ovf: Environment = + serde_xml_rs::from_reader(reader).context(error::XmlDeserializeSnafu { path })?; + + // We have seen the keys in the `Property` section be "namespaced" like "oe:key" or + // "of:key". Since we aren't trying to validate the schema beyond the presence of the + // elements we care about, we can ignore the namespacing. An example of this type of + // namespacing can be found in the unit test sample data. `serde_xml_rs` effectively + // ignores these namespaces and returns "key" / "value": + // https://github.com/Rreverser/serde-xml-rs/issues/64#issuecomment=540448434 + let mut base64_str = String::new(); + let user_data_key = "user-data"; + for property in ovf.property_section.properties { + if property.key == user_data_key { + base64_str = property.value; + break; + } + } + + // Base64 decode the &str + let decoded_bytes = base64::engine::general_purpose::STANDARD + .decode(base64_str) + .context(error::Base64DecodeSnafu { + what: "OVF user data", + })?; + + // Decompress the data if it's compressed + let decoded = expand_slice_maybe(&decoded_bytes).context(error::DecompressionSnafu { + what: "OVF user data", + })?; + + Ok(decoded) + } +} + +impl UserDataProvider for VmwareCdRom { + fn user_data(&self) -> std::result::Result, Box> { + // Given the list of acceptable filenames, ensure only 1 exists and parse + // it for user data + info!("Attempting to retrieve user data from mounted CD-ROM"); + let user_data_file = match Self::user_data_path()? { + Some(path) => path, + None => return Ok(None), + }; + + // XML files require extra processing, while a user-supplied file should already be in TOML + // format + info!("'{}' exists, using it", user_data_file.display()); + let user_data_str = match user_data_file.extension().and_then(OsStr::to_str) { + Some("xml") | Some("XML") => Self::ovf_user_data(&user_data_file)?, + // Since we only look for a specific list of file names, we should never find a file + // with an extension we don't understand. + Some(_) => unreachable!(), + None => { + // Read the file, decompressing it if compressed. + expand_file_maybe(&user_data_file).context(error::InputFileReadSnafu { + path: &user_data_file, + })? + } + }; + + if user_data_str.is_empty() { + warn!("{} exists but is empty", user_data_file.display()); + return Ok(None); + } + + // User data could be 700MB compressed! Eek! :) + if user_data_str.len() <= 2048 { + trace!("Received user data: {}", user_data_str); + } else { + trace!( + "Received long user data, starts with: {}", + // (this isn't perfect because chars aren't grapheme clusters, but will error + // toward printing the whole input, which is fine) + String::from_iter(user_data_str.chars().take(2048)) + ); + } + + let json = SettingsJson::from_toml_str(&user_data_str, "CD-ROM").context( + error::SettingsToJsonSnafu { + from: user_data_file.display().to_string(), + }, + )?; + + Ok(Some(json)) + } +} + +// =^..^= =^..^= =^..^= =^..^= + +// Minimal expected structure for an OVF file with user data +#[derive(Debug, Deserialize)] +struct Environment { + #[serde(rename = "PropertySection", default)] + pub property_section: PropertySection, +} + +#[derive(Default, Debug, Deserialize)] +struct PropertySection { + #[serde(rename = "Property", default)] + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +struct Property { + pub key: String, + pub value: String, +} + +// =^..^= =^..^= =^..^= =^..^= + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(crate) enum Error { + #[snafu(display("Unable to decode base64 in {}: '{}'", what, source))] + Base64Decode { + what: String, + source: base64::DecodeError, + }, + + #[snafu(display("Failed to decompress {}: {}", what, source))] + Decompression { what: String, source: io::Error }, + + #[snafu(display("Unable to read input file '{}': {}", path.display(), source))] + InputFileRead { path: PathBuf, source: io::Error }, + + #[snafu(display("Unable to deserialize XML from: '{}': {}", path.display(), source))] + XmlDeserialize { + path: PathBuf, + source: serde_xml_rs::Error, + }, + + #[snafu(display("Unable to serialize settings from {}: {}", from, source))] + SettingsToJson { + from: String, + source: early_boot_config_provider::settings::Error, + }, + + #[snafu(display("Found multiple user data files in '{}', expected 1", place))] + UserDataFileCount { place: String }, + } +} + +type Result = std::result::Result; + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + fn test_data() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_data") + } + + #[test] + fn test_read_xml_user_data_namespaced_keys() { + let xml = test_data().join("namespaced_keys.xml"); + let expected_user_data = "settings.motd = \"hello\""; + + let actual_user_data = VmwareCdRom::ovf_user_data(xml).unwrap(); + + assert_eq!(actual_user_data, expected_user_data) + } + + #[test] + fn test_read_xml_user_data() { + let xml = test_data().join("ovf-env.xml"); + let expected_user_data = "settings.motd = \"hello\""; + + let actual_user_data = VmwareCdRom::ovf_user_data(xml).unwrap(); + + assert_eq!(actual_user_data, expected_user_data) + } +} diff --git a/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/main.rs b/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/main.rs new file mode 100644 index 00000000000..a91792ee755 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-cd-rom/src/main.rs @@ -0,0 +1,16 @@ +/*! +# Introduction + +User data provider binary that fetches user data provided via CD-ROM to a VMware VM. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, UserDataProvider, +}; +use std::process::ExitCode; +use vmware_cd_rom_user_data_provider::VmwareCdRom; + +fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(VmwareCdRom.user_data()) +} diff --git a/sources/api/early-boot-config/test_data/namespaced_keys.xml b/sources/early-boot-config/user-data-providers/vmware-cd-rom/test_data/namespaced_keys.xml similarity index 100% rename from sources/api/early-boot-config/test_data/namespaced_keys.xml rename to sources/early-boot-config/user-data-providers/vmware-cd-rom/test_data/namespaced_keys.xml diff --git a/sources/api/early-boot-config/test_data/ovf-env.xml b/sources/early-boot-config/user-data-providers/vmware-cd-rom/test_data/ovf-env.xml similarity index 100% rename from sources/api/early-boot-config/test_data/ovf-env.xml rename to sources/early-boot-config/user-data-providers/vmware-cd-rom/test_data/ovf-env.xml diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/Cargo.toml b/sources/early-boot-config/user-data-providers/vmware-guestinfo/Cargo.toml new file mode 100644 index 00000000000..8b88aafc0ca --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "vmware-guestinfo-user-data-provider" +version = "0.1.0" +authors = ["Zac Mrowicki ", "Sam Berning "] +edition = "2021" +license = "Apache-2.0 OR MIT" +publish = false +# Don't rebuild crate just because of changes to README. +exclude = ["README.md"] + +[dependencies] +base64 = "0.21" +log = "0.4" +serde = { version = "1", features = ["derive"] } +serde_plain = "1" +snafu = "0.8" +early-boot-config-provider = { path = "../../early-boot-config-provider", version = "0.1" } + +[target.'cfg(target_arch = "x86_64")'.dependencies] +# vmw_backdoor includes x86_64 assembly, prevent it from building for ARM +vmw_backdoor = "0.2" + +[build-dependencies] +generate-readme = { version = "0.1", path = "../../../generate-readme" } diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.md b/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.md new file mode 100644 index 00000000000..2f9fbc974b8 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.md @@ -0,0 +1,11 @@ +# vmware-guestinfo-user-data-provider + +Current version: 0.1.0 + +## Introduction + +User data provider binary used to fetch user data provided via VMware guestinfo. + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.tpl b/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.tpl new file mode 100644 index 00000000000..bf207d023ff --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/build.rs b/sources/early-boot-config/user-data-providers/vmware-guestinfo/build.rs new file mode 100644 index 00000000000..4764f5719ee --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_file("src/main.rs").unwrap(); +} diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/lib.rs b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/lib.rs new file mode 100644 index 00000000000..921eb1eaf73 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/lib.rs @@ -0,0 +1,14 @@ +/// VMware guestinfo +extern crate log; + +#[cfg(target_arch = "x86_64")] +mod x86_64; + +#[cfg(not(target_arch = "x86_64"))] +mod not_x86_64; + +#[cfg(target_arch = "x86_64")] +pub use x86_64::*; + +#[cfg(not(target_arch = "x86_64"))] +pub use not_x86_64::*; diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/main.rs b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/main.rs new file mode 100644 index 00000000000..5d9136061ac --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/main.rs @@ -0,0 +1,16 @@ +/*! +# Introduction + +User data provider binary used to fetch user data provided via VMware guestinfo. +*/ + +use early_boot_config_provider::provider::{ + print_userdata_output, setup_provider_logging, UserDataProvider, +}; +use std::process::ExitCode; +use vmware_guestinfo_user_data_provider::VmwareGuestinfo; + +fn main() -> ExitCode { + setup_provider_logging(); + print_userdata_output(VmwareGuestinfo.user_data()) +} diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/not_x86_64.rs b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/not_x86_64.rs new file mode 100644 index 00000000000..44b3978d65e --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/not_x86_64.rs @@ -0,0 +1,11 @@ +use early_boot_config_provider::provider::UserDataProvider; +use early_boot_config_provider::settings::SettingsJson; + +pub struct VmwareGuestinfo; + +impl UserDataProvider for VmwareGuestinfo { + #[allow(dead_code)] + fn user_data(&self) -> std::result::Result, Box> { + unimplemented!() + } +} diff --git a/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/x86_64.rs b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/x86_64.rs new file mode 100644 index 00000000000..ade8e79df94 --- /dev/null +++ b/sources/early-boot-config/user-data-providers/vmware-guestinfo/src/x86_64.rs @@ -0,0 +1,192 @@ +use early_boot_config_provider::provider::UserDataProvider; +use early_boot_config_provider::{compression::OptionalCompressionReader, settings::SettingsJson}; +use log::*; +use serde::Deserialize; +use snafu::{ensure, ResultExt}; +use std::io::{Cursor, Read}; + +// The fields in which user data and its encoding are stored in guestinfo +const GUESTINFO_USERDATA: &str = "guestinfo.userdata"; +const GUESTINFO_USERDATA_ENCODING: &str = "guestinfo.userdata.encoding"; + +pub struct VmwareGuestinfo; + +impl VmwareGuestinfo { + /// Fetch the user data's encoding from guestinfo. + // `guestinfo.userdata.encoding` informs us how to handle the data in the + // `guestinfo.userdata` field + fn fetch_encoding() -> Result { + let maybe_encoding = Self::backdoor_get_bytes(GUESTINFO_USERDATA_ENCODING)?; + let user_data_encoding: UserDataEncoding = match maybe_encoding { + Some(val) => { + let encoding_str = String::from_utf8(val).context(error::InvalidUtf8Snafu { + what: GUESTINFO_USERDATA_ENCODING, + })?; + info!("Found user data encoding: {}", encoding_str); + + serde_plain::from_str(&encoding_str).context(error::UnknownEncodingSnafu { + encoding: encoding_str, + })? + } + + // The cloudinit VMware guestinfo data provider assumes any user data without an + // associated encoding means raw data is being passed. We will follow suit here. + None => { + warn!( + "'{}' unset, assuming raw user data", + GUESTINFO_USERDATA_ENCODING + ); + UserDataEncoding::Raw + } + }; + + Ok(user_data_encoding) + } + + /// Request a key's value from guestinfo + fn backdoor_get_bytes(key: &str) -> Result>> { + // Probe and access the VMware backdoor. `kernel lockdown(7)` may block "privileged" + // mode because of its use of `iopl()`; the 5.15 kernels have it disabled regardless + // of lockdown mode. If this fails, fall back to "unprivileged" access without first + // requesting access to the relevant IO ports. KVM and VMware both have them special- + // cased in their emulation to not raise an exception to the guest OS and things + // should work out. + let mut backdoor = vmw_backdoor::probe_backdoor_privileged() + .or_else(|e| { + debug!( + "Unable to access guestinfo via privileged mode, using unprivileged: {}", + e + ); + vmw_backdoor::probe_backdoor() + }) + .context(error::BackdoorSnafu { + op: "probe and acquire access", + })?; + + let mut erpc = backdoor + .open_enhanced_chan() + .context(error::BackdoorSnafu { + op: "open eRPC channel", + })?; + + erpc.get_guestinfo(key.as_bytes()) + .context(error::GuestInfoSnafu { what: key }) + } +} + +impl UserDataProvider for VmwareGuestinfo { + fn user_data(&self) -> std::result::Result, Box> { + info!("Attempting to retrieve user data via guestinfo interface"); + + // It would be extremely odd to get here and not be on VMware, but check anyway + ensure!(vmw_backdoor::is_vmware_cpu(), error::NotVmwareSnafu); + + let user_data_encoding = Self::fetch_encoding()?; + let user_data_bytes = match Self::backdoor_get_bytes(GUESTINFO_USERDATA)? { + Some(val) => val, + None => return Ok(None), + }; + + let user_data_string = match user_data_encoding { + // gzip+base64 is gzip'ed user data that is base64 encoded + UserDataEncoding::Base64 | UserDataEncoding::GzipBase64 => { + info!("Decoding user data"); + let mut reader = Cursor::new(user_data_bytes); + let decoder = base64::read::DecoderReader::new( + &mut reader, + &base64::engine::general_purpose::STANDARD, + ); + + // Decompresses the data if it is gzip'ed + let mut output = String::new(); + let mut compression_reader = OptionalCompressionReader::new(decoder); + compression_reader.read_to_string(&mut output).context( + error::DecompressionSnafu { + what: "guestinfo user data", + }, + )?; + output + } + + UserDataEncoding::Raw => { + String::from_utf8(user_data_bytes).context(error::InvalidUtf8Snafu { + what: GUESTINFO_USERDATA, + })? + } + }; + + let json = SettingsJson::from_toml_str(user_data_string, "guestinfo") + .context(error::SettingsToJsonSnafu { from: "guestinfo" })?; + Ok(Some(json)) + } +} + +// =^..^= =^..^= =^..^= =^..^= + +// Acceptable user data encodings +// When case-insensitive de/serialization is finalized, that's what we would want to use +// here instead of aliases: https://github.com/serde-rs/serde/pull/1902 +#[derive(Debug, Deserialize)] +enum UserDataEncoding { + #[serde(alias = "b64")] + #[serde(alias = "B64")] + #[serde(alias = "base64")] + Base64, + #[serde(alias = "gz+b64")] + #[serde(alias = "Gz+B64")] + #[serde(alias = "gzip+base64")] + #[serde(alias = "Gzip+Base64")] + GzipBase64, + Raw, +} + +// =^..^= =^..^= =^..^= =^..^= + +mod error { + use snafu::Snafu; + use std::io; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(crate) enum Error { + #[snafu(display("VMware backdoor: failed to '{}': '{}'", op, source))] + Backdoor { + op: String, + source: vmw_backdoor::VmwError, + }, + + #[snafu(display("Failed to decompress {}: {}", what, source))] + Decompression { what: String, source: io::Error }, + + #[snafu(display("Failed to fetch key '{}' from guestinfo: {}", what, source))] + GuestInfo { + what: String, + source: vmw_backdoor::VmwError, + }, + + #[snafu(display("'{}' contains invalid utf-8: {}", what, source))] + InvalidUtf8 { + what: String, + source: std::string::FromUtf8Error, + }, + + #[snafu(display( + "Unable to read user data from guestinfo, this is not a VMware virtual CPU" + ))] + NotVmware, + + #[snafu(display("Unable to serialize settings from {}: {}", from, source))] + SettingsToJson { + from: String, + source: early_boot_config_provider::settings::Error, + }, + + #[snafu(display("Unknown user data encoding: '{}': {}", encoding, source))] + UnknownEncoding { + encoding: String, + source: serde_plain::Error, + }, + } +} + +type Result = std::result::Result; diff --git a/variants/Cargo.lock b/variants/Cargo.lock index a295fe578d9..5fd9b58d2d9 100644 --- a/variants/Cargo.lock +++ b/variants/Cargo.lock @@ -24,6 +24,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "iputils", "kernel-6_1", "login", @@ -38,6 +39,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "ecs-agent", "kernel-5_10", "release", @@ -50,6 +52,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "ecs-agent", "ecs-gpu-init", "kernel-5_10", @@ -65,6 +68,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "ecs-agent", "kernel-6_1", "release", @@ -77,6 +81,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "ecs-agent", "ecs-gpu-init", "kernel-6_1", @@ -99,6 +104,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_10", "kubernetes-1_23", "release", @@ -111,6 +117,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_10", "kmod-5_10-nvidia", "kubernetes-1_23", @@ -126,6 +133,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_24", "release", @@ -138,6 +146,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kmod-5_15-nvidia", "kubernetes-1_24", @@ -153,6 +162,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_25", "release", @@ -165,6 +175,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kmod-5_15-nvidia", "kubernetes-1_25", @@ -180,6 +191,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_26", "release", @@ -192,6 +204,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kmod-5_15-nvidia", "kubernetes-1_26", @@ -207,6 +220,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_27", "release", @@ -219,6 +233,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kmod-5_15-nvidia", "kubernetes-1_27", @@ -234,6 +249,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_28", "release", @@ -246,6 +262,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kmod-6_1-nvidia", "kubernetes-1_28", @@ -261,6 +278,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_29", "release", @@ -273,6 +291,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kmod-6_1-nvidia", "kubernetes-1_29", @@ -436,6 +455,13 @@ dependencies = [ "util-linux", ] +[[package]] +name = "early-boot-config" +version = "0.1.0" +dependencies = [ + "glibc", +] + [[package]] name = "ecr-credential-provider" version = "0.1.0" @@ -974,6 +1000,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "iputils", "kernel-6_1", "linux-firmware", @@ -989,6 +1016,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_25", "linux-firmware", @@ -1002,6 +1030,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_26", "linux-firmware", @@ -1015,6 +1044,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_27", "linux-firmware", @@ -1028,6 +1058,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_28", "release", @@ -1040,6 +1071,7 @@ dependencies = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_29", "release", @@ -1219,6 +1251,7 @@ dependencies = [ "docker-cli", "docker-engine", "docker-init", + "early-boot-config", "iputils", "kernel-6_1", "login", @@ -1233,6 +1266,7 @@ version = "0.1.0" dependencies = [ "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_25", "open-vm-tools", @@ -1245,6 +1279,7 @@ version = "0.1.0" dependencies = [ "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_26", "open-vm-tools", @@ -1257,6 +1292,7 @@ version = "0.1.0" dependencies = [ "cni", "cni-plugins", + "early-boot-config", "kernel-5_15", "kubernetes-1_27", "open-vm-tools", @@ -1269,6 +1305,7 @@ version = "0.1.0" dependencies = [ "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_28", "open-vm-tools", @@ -1281,6 +1318,7 @@ version = "0.1.0" dependencies = [ "cni", "cni-plugins", + "early-boot-config", "kernel-6_1", "kubernetes-1_29", "open-vm-tools", diff --git a/variants/aws-dev/Cargo.toml b/variants/aws-dev/Cargo.toml index bfa9456c43c..9f6f0172c2a 100644 --- a/variants/aws-dev/Cargo.toml +++ b/variants/aws-dev/Cargo.toml @@ -26,6 +26,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # docker @@ -45,6 +46,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # docker docker-cli = { path = "../../packages/docker-cli" } diff --git a/variants/aws-ecs-1-nvidia/Cargo.toml b/variants/aws-ecs-1-nvidia/Cargo.toml index 015f348d414..d58ad538e31 100644 --- a/variants/aws-ecs-1-nvidia/Cargo.toml +++ b/variants/aws-ecs-1-nvidia/Cargo.toml @@ -18,6 +18,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-5.10", # docker @@ -38,6 +39,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-5_10 = { path = "../../packages/kernel-5.10" } # docker docker-cli = { path = "../../packages/docker-cli" } diff --git a/variants/aws-ecs-1/Cargo.toml b/variants/aws-ecs-1/Cargo.toml index 3d53e68b097..24e3d3303c0 100644 --- a/variants/aws-ecs-1/Cargo.toml +++ b/variants/aws-ecs-1/Cargo.toml @@ -15,6 +15,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-5.10", # docker @@ -31,6 +32,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-5_10 = { path = "../../packages/kernel-5.10" } # docker docker-cli = { path = "../../packages/docker-cli" } diff --git a/variants/aws-ecs-2-nvidia/Cargo.toml b/variants/aws-ecs-2-nvidia/Cargo.toml index 2ace1284361..cba049b55b5 100644 --- a/variants/aws-ecs-2-nvidia/Cargo.toml +++ b/variants/aws-ecs-2-nvidia/Cargo.toml @@ -18,6 +18,7 @@ os-image-size-gib = 4 [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # docker @@ -46,6 +47,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # docker docker-cli = { path = "../../packages/docker-cli" } diff --git a/variants/aws-ecs-2/Cargo.toml b/variants/aws-ecs-2/Cargo.toml index d307bb3abc5..7feeeba9c92 100644 --- a/variants/aws-ecs-2/Cargo.toml +++ b/variants/aws-ecs-2/Cargo.toml @@ -17,6 +17,7 @@ systemd-networkd = true [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # docker @@ -40,6 +41,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # docker docker-cli = { path = "../../packages/docker-cli" } diff --git a/variants/aws-k8s-1.23-nvidia/Cargo.toml b/variants/aws-k8s-1.23-nvidia/Cargo.toml index 71dad8886d4..c4bcd82847c 100644 --- a/variants/aws-k8s-1.23-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.23-nvidia/Cargo.toml @@ -20,6 +20,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.10", "kubelet-1.23", "release", @@ -45,6 +46,7 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_10 = { path = "../../packages/kernel-5.10" } kubernetes-1_23 = { path = "../../packages/kubernetes-1.23" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } nvidia-container-toolkit = { path = "../../packages/nvidia-container-toolkit" } nvidia-k8s-device-plugin = { path = "../../packages/nvidia-k8s-device-plugin" } kmod-5_10-nvidia = { path = "../../packages/kmod-5.10-nvidia" } diff --git a/variants/aws-k8s-1.23/Cargo.toml b/variants/aws-k8s-1.23/Cargo.toml index 60cb5dbbf8e..dbb8a141076 100644 --- a/variants/aws-k8s-1.23/Cargo.toml +++ b/variants/aws-k8s-1.23/Cargo.toml @@ -17,6 +17,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.10", "kubelet-1.23", "release", @@ -39,3 +40,4 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_10 = { path = "../../packages/kernel-5.10" } kubernetes-1_23= { path = "../../packages/kubernetes-1.23" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/aws-k8s-1.24-nvidia/Cargo.toml b/variants/aws-k8s-1.24-nvidia/Cargo.toml index 6f3bc8a1233..433e21093bb 100644 --- a/variants/aws-k8s-1.24-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.24-nvidia/Cargo.toml @@ -20,6 +20,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.24", "release", @@ -45,6 +46,7 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_24 = { path = "../../packages/kubernetes-1.24" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } nvidia-container-toolkit = { path = "../../packages/nvidia-container-toolkit" } nvidia-k8s-device-plugin = { path = "../../packages/nvidia-k8s-device-plugin" } kmod-5_15-nvidia = { path = "../../packages/kmod-5.15-nvidia" } diff --git a/variants/aws-k8s-1.24/Cargo.toml b/variants/aws-k8s-1.24/Cargo.toml index 8d9298896a0..a4b0eed7ec1 100644 --- a/variants/aws-k8s-1.24/Cargo.toml +++ b/variants/aws-k8s-1.24/Cargo.toml @@ -17,6 +17,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.24", "release", @@ -39,3 +40,4 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_24= { path = "../../packages/kubernetes-1.24" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/aws-k8s-1.25-nvidia/Cargo.toml b/variants/aws-k8s-1.25-nvidia/Cargo.toml index 9e59896d1a8..46813dd3f8d 100644 --- a/variants/aws-k8s-1.25-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.25-nvidia/Cargo.toml @@ -20,6 +20,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.25", "release", @@ -45,6 +46,7 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_25 = { path = "../../packages/kubernetes-1.25" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } nvidia-container-toolkit = { path = "../../packages/nvidia-container-toolkit" } nvidia-k8s-device-plugin = { path = "../../packages/nvidia-k8s-device-plugin" } kmod-5_15-nvidia = { path = "../../packages/kmod-5.15-nvidia" } diff --git a/variants/aws-k8s-1.25/Cargo.toml b/variants/aws-k8s-1.25/Cargo.toml index 4eeb4a2614c..437baa928d7 100644 --- a/variants/aws-k8s-1.25/Cargo.toml +++ b/variants/aws-k8s-1.25/Cargo.toml @@ -17,6 +17,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.25", "release", @@ -39,3 +40,4 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_25 = { path = "../../packages/kubernetes-1.25" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/aws-k8s-1.26-nvidia/Cargo.toml b/variants/aws-k8s-1.26-nvidia/Cargo.toml index dadd6b69c54..eac05aa97be 100644 --- a/variants/aws-k8s-1.26-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.26-nvidia/Cargo.toml @@ -21,6 +21,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.26", "release", @@ -46,6 +47,7 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_26 = { path = "../../packages/kubernetes-1.26" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } nvidia-container-toolkit = { path = "../../packages/nvidia-container-toolkit" } nvidia-k8s-device-plugin = { path = "../../packages/nvidia-k8s-device-plugin" } kmod-5_15-nvidia = { path = "../../packages/kmod-5.15-nvidia" } diff --git a/variants/aws-k8s-1.26/Cargo.toml b/variants/aws-k8s-1.26/Cargo.toml index fc9ec230f3e..c7857aef34c 100644 --- a/variants/aws-k8s-1.26/Cargo.toml +++ b/variants/aws-k8s-1.26/Cargo.toml @@ -18,6 +18,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.26", "release", @@ -40,3 +41,4 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_26 = { path = "../../packages/kubernetes-1.26" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/aws-k8s-1.27-nvidia/Cargo.toml b/variants/aws-k8s-1.27-nvidia/Cargo.toml index f15ab848b62..6fcb991ec68 100644 --- a/variants/aws-k8s-1.27-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.27-nvidia/Cargo.toml @@ -21,6 +21,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.27", "release", @@ -46,6 +47,7 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_27 = { path = "../../packages/kubernetes-1.27" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } nvidia-container-toolkit = { path = "../../packages/nvidia-container-toolkit" } nvidia-k8s-device-plugin = { path = "../../packages/nvidia-k8s-device-plugin" } kmod-5_15-nvidia = { path = "../../packages/kmod-5.15-nvidia" } diff --git a/variants/aws-k8s-1.27/Cargo.toml b/variants/aws-k8s-1.27/Cargo.toml index eff3ed053f6..ec30f1dec1c 100644 --- a/variants/aws-k8s-1.27/Cargo.toml +++ b/variants/aws-k8s-1.27/Cargo.toml @@ -18,6 +18,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-aws", "kernel-5.15", "kubelet-1.27", "release", @@ -40,3 +41,4 @@ cni-plugins = { path = "../../packages/cni-plugins" } kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_27 = { path = "../../packages/kubernetes-1.27" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/aws-k8s-1.28-nvidia/Cargo.toml b/variants/aws-k8s-1.28-nvidia/Cargo.toml index 4a740cef9c2..7c5a38c16da 100644 --- a/variants/aws-k8s-1.28-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.28-nvidia/Cargo.toml @@ -22,6 +22,7 @@ systemd-networkd = true [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # k8s @@ -48,6 +49,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" } diff --git a/variants/aws-k8s-1.28/Cargo.toml b/variants/aws-k8s-1.28/Cargo.toml index 676cd9a428b..91e12ce1fa1 100644 --- a/variants/aws-k8s-1.28/Cargo.toml +++ b/variants/aws-k8s-1.28/Cargo.toml @@ -19,6 +19,7 @@ systemd-networkd = true [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # k8s @@ -41,6 +42,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" } diff --git a/variants/aws-k8s-1.29-nvidia/Cargo.toml b/variants/aws-k8s-1.29-nvidia/Cargo.toml index 7c68653464d..dbf31001316 100644 --- a/variants/aws-k8s-1.29-nvidia/Cargo.toml +++ b/variants/aws-k8s-1.29-nvidia/Cargo.toml @@ -22,6 +22,7 @@ systemd-networkd = true [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # k8s @@ -48,6 +49,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" } diff --git a/variants/aws-k8s-1.29/Cargo.toml b/variants/aws-k8s-1.29/Cargo.toml index 84dbe5be152..2d43eef51f4 100644 --- a/variants/aws-k8s-1.29/Cargo.toml +++ b/variants/aws-k8s-1.29/Cargo.toml @@ -19,6 +19,7 @@ systemd-networkd = true [package.metadata.build-variant] included-packages = [ # core + "early-boot-config-aws", "release", "kernel-6.1", # k8s @@ -41,6 +42,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" } diff --git a/variants/metal-dev/Cargo.toml b/variants/metal-dev/Cargo.toml index 553d40f28bd..03ba98bb834 100644 --- a/variants/metal-dev/Cargo.toml +++ b/variants/metal-dev/Cargo.toml @@ -25,6 +25,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-metal", "release", "kernel-6.1", "linux-firmware", @@ -45,6 +46,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } linux-firmware = { path = "../../packages/linux-firmware" } # docker diff --git a/variants/metal-k8s-1.25/Cargo.toml b/variants/metal-k8s-1.25/Cargo.toml index 30b640cab20..164fa0754a6 100644 --- a/variants/metal-k8s-1.25/Cargo.toml +++ b/variants/metal-k8s-1.25/Cargo.toml @@ -27,6 +27,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-metal", "kernel-5.15", "linux-firmware", "kubelet-1.25", @@ -44,3 +45,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } linux-firmware = { path = "../../packages/linux-firmware" } kubernetes-1_25 = { path = "../../packages/kubernetes-1.25" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/metal-k8s-1.26/Cargo.toml b/variants/metal-k8s-1.26/Cargo.toml index 7bb5402a953..608f015a877 100644 --- a/variants/metal-k8s-1.26/Cargo.toml +++ b/variants/metal-k8s-1.26/Cargo.toml @@ -28,6 +28,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-metal", "kernel-5.15", "linux-firmware", "kubelet-1.26", @@ -45,3 +46,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } linux-firmware = { path = "../../packages/linux-firmware" } kubernetes-1_26 = { path = "../../packages/kubernetes-1.26" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/metal-k8s-1.27/Cargo.toml b/variants/metal-k8s-1.27/Cargo.toml index aadc1fe333a..480d81a23a8 100644 --- a/variants/metal-k8s-1.27/Cargo.toml +++ b/variants/metal-k8s-1.27/Cargo.toml @@ -28,6 +28,7 @@ included-packages = [ "aws-iam-authenticator", "cni", "cni-plugins", + "early-boot-config-metal", "kernel-5.15", "linux-firmware", "kubelet-1.27", @@ -45,3 +46,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } linux-firmware = { path = "../../packages/linux-firmware" } kubernetes-1_27 = { path = "../../packages/kubernetes-1.27" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/metal-k8s-1.28/Cargo.toml b/variants/metal-k8s-1.28/Cargo.toml index d299e0252ee..585a66bdb83 100644 --- a/variants/metal-k8s-1.28/Cargo.toml +++ b/variants/metal-k8s-1.28/Cargo.toml @@ -29,6 +29,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-metal", "release", "kernel-6.1", # k8s @@ -44,6 +45,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s aws-iam-authenticator = { path = "../../packages/aws-iam-authenticator" } diff --git a/variants/metal-k8s-1.29/Cargo.toml b/variants/metal-k8s-1.29/Cargo.toml index 467e223fbad..ddac943fa11 100644 --- a/variants/metal-k8s-1.29/Cargo.toml +++ b/variants/metal-k8s-1.29/Cargo.toml @@ -36,6 +36,7 @@ included-packages = [ "cni", "cni-plugins", "kubelet-1.29", + "early-boot-config-metal", ] [lib] @@ -44,6 +45,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s aws-iam-authenticator = { path = "../../packages/aws-iam-authenticator" } diff --git a/variants/vmware-dev/Cargo.toml b/variants/vmware-dev/Cargo.toml index 2dec382d40f..4c82413b15e 100644 --- a/variants/vmware-dev/Cargo.toml +++ b/variants/vmware-dev/Cargo.toml @@ -30,6 +30,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-vmware", "release", "kernel-6.1", "open-vm-tools", @@ -50,6 +51,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } open-vm-tools = { path = "../../packages/open-vm-tools" } # docker diff --git a/variants/vmware-k8s-1.25/Cargo.toml b/variants/vmware-k8s-1.25/Cargo.toml index 37a148980a1..82784b69081 100644 --- a/variants/vmware-k8s-1.25/Cargo.toml +++ b/variants/vmware-k8s-1.25/Cargo.toml @@ -29,6 +29,7 @@ kernel-parameters = [ included-packages = [ "cni", "cni-plugins", + "early-boot-config-vmware", "kernel-5.15", "kubelet-1.25", "open-vm-tools", @@ -45,3 +46,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_25 = { path = "../../packages/kubernetes-1.25" } open-vm-tools = { path = "../../packages/open-vm-tools" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/vmware-k8s-1.26/Cargo.toml b/variants/vmware-k8s-1.26/Cargo.toml index 0ba194b0f33..74817c8f4f1 100644 --- a/variants/vmware-k8s-1.26/Cargo.toml +++ b/variants/vmware-k8s-1.26/Cargo.toml @@ -30,6 +30,7 @@ kernel-parameters = [ included-packages = [ "cni", "cni-plugins", + "early-boot-config-vmware", "kernel-5.15", "kubelet-1.26", "open-vm-tools", @@ -46,3 +47,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_26 = { path = "../../packages/kubernetes-1.26" } open-vm-tools = { path = "../../packages/open-vm-tools" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/vmware-k8s-1.27/Cargo.toml b/variants/vmware-k8s-1.27/Cargo.toml index 422ec9ddbdf..1a4a670fe66 100644 --- a/variants/vmware-k8s-1.27/Cargo.toml +++ b/variants/vmware-k8s-1.27/Cargo.toml @@ -30,6 +30,7 @@ kernel-parameters = [ included-packages = [ "cni", "cni-plugins", + "early-boot-config-vmware", "kernel-5.15", "kubelet-1.27", "open-vm-tools", @@ -46,3 +47,4 @@ kernel-5_15 = { path = "../../packages/kernel-5.15" } kubernetes-1_27 = { path = "../../packages/kubernetes-1.27" } open-vm-tools = { path = "../../packages/open-vm-tools" } release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } diff --git a/variants/vmware-k8s-1.28/Cargo.toml b/variants/vmware-k8s-1.28/Cargo.toml index b0bae19e4b3..3ad3cbce7af 100644 --- a/variants/vmware-k8s-1.28/Cargo.toml +++ b/variants/vmware-k8s-1.28/Cargo.toml @@ -32,6 +32,7 @@ kernel-parameters = [ ] included-packages = [ # core + "early-boot-config-vmware", "release", "kernel-6.1", # k8s @@ -48,6 +49,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" } diff --git a/variants/vmware-k8s-1.29/Cargo.toml b/variants/vmware-k8s-1.29/Cargo.toml index 71fa80abb92..470169e61d8 100644 --- a/variants/vmware-k8s-1.29/Cargo.toml +++ b/variants/vmware-k8s-1.29/Cargo.toml @@ -39,6 +39,7 @@ included-packages = [ "cni-plugins", "kubelet-1.29", # vmware + "early-boot-config-vmware", "open-vm-tools", ] @@ -48,6 +49,7 @@ path = "../variants.rs" [build-dependencies] # core release = { path = "../../packages/release" } +early-boot-config = { path = "../../packages/early-boot-config" } kernel-6_1 = { path = "../../packages/kernel-6.1" } # k8s cni = { path = "../../packages/cni" }