Skip to content

Commit

Permalink
Add cloud-init volume generation to standalone
Browse files Browse the repository at this point in the history
Because some of the maths (copied from Omicron) to generated the FAT
volume make use of usize::div_ceil(), the toolchain of
propolis-standalone was bumped to 1.73.  This necessitated some
clippy fixes unrelated to the changes to support cloud-init.
  • Loading branch information
pfmooney committed Oct 8, 2023
1 parent be9e31d commit 92719c2
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 5 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ dropshot = { git = "https://github.com/oxidecomputer/dropshot", branch = "main"
erased-serde = "0.3"
errno = "0.2.8"
expectorate = "1.0.5"
fatfs = "0.3.6"
futures = "0.3"
hex = "0.4.3"
http = "0.2.9"
Expand Down
2 changes: 2 additions & 0 deletions bin/propolis-standalone/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name = "propolis-standalone"
version = "0.1.0"
license = "MPL-2.0"
edition = "2021"
rust-version = "1.73"

[[bin]]
name = "propolis-standalone"
Expand All @@ -16,6 +17,7 @@ atty.workspace = true
bhyve_api.workspace = true
clap = { workspace = true, features = ["derive"] }
ctrlc.workspace = true
fatfs.workspace = true
futures.workspace = true
libc.workspace = true
toml.workspace = true
Expand Down
43 changes: 43 additions & 0 deletions bin/propolis-standalone/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,46 @@ Certain fields in `cpuid` data depend on aspects specific to the host (such as
vCPU count) or the vCPU they are associated with (such as APIC ID). Propolis
will "specialize" the data provided in the `cpuid` profile with logic appropriate
for the specific leafs involved.

## Configuring Cloud-Init

Propolis is able to assemble a disk image formatted in the
[NoCloud](https://cloudinit.readthedocs.io/en/latest/reference/datasources/nocloud.html)
fashion to be consumed by `cloud-init` inside the guest. An example of such configuration is as follows:
```toml
# ... other configuration bits

# Define a disk device to bear the cloud-init data
[dev.cloudinit]
driver = "pci-virtio-block"
pci-path = "0.16.0"
block_dev = "cloudinit_be"

# Define the backend to that disk as the cloudinit type
[block_dev.cloudinit_be]
type = "cloudinit"

# Data from this cloudinit section will be used to populate the above block_dev
[cloudinit]
user-data = '''
#cloud-config
users:
- default
- name: test
sudo: 'ALL=(ALL) NOPASSWD:ALL'
lock_passwd: false
hashed_passwd: '$6$rounds=4096$MBW/3OrwWLifnv30$QM.oCQ3pzV7X4EToX9IyZmplvaTgpZ6YJ50MhQrwlryj1soqBW5zvraVttYwfyWdxigHpZHTjY9kT.029UOEn1'
'''
# Instead of specifying string data like above, a path to a file can be used too:
# user-data-path = "path/to/file"

# Instance metadata is configured the same way:
# meta-data = "..."
# or
# meta-data-path = "path/to/file"

# Same with network configuration:
# network-config = "..."
# or
# network-config-path = "path/to/file"
```
103 changes: 103 additions & 0 deletions bin/propolis-standalone/src/cidata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use std::io::{Cursor, Write};
use std::sync::Arc;

use anyhow::{bail, Context};
use fatfs::{FileSystem, FormatVolumeOptions, FsOptions};

use propolis::block;
use propolis_standalone_config::Config;

const SECTOR_SZ: usize = 512;
const VOLUME_LABEL: [u8; 11] = *b"cidata ";

pub(crate) fn build_cidata_be(
config: &Config,
) -> anyhow::Result<Arc<block::InMemoryBackend>> {
let cidata = &config
.cloudinit
.as_ref()
.ok_or_else(|| anyhow::anyhow!("missing [cloudinit] config section"))?;

let fields = [
("user-data", &cidata.user_data, &cidata.user_data_path),
("meta-data", &cidata.meta_data, &cidata.meta_data_path),
("network-config", &cidata.network_config, &cidata.network_config_path),
];
let all_data = fields
.into_iter()
.map(|(name, str_data, path_data)| {
Ok((
name,
match (str_data, path_data) {
(None, None) => vec![],
(None, Some(path)) => std::fs::read(path).context(
format!("unable to read {name} from {path}"),
)?,
(Some(data), None) => data.clone().into(),
(Some(_), Some(_)) => {
bail!("cannot provide path and string for {name}");
}
},
))
})
.collect::<Result<Vec<_>, _>>()?;

let file_sectors: usize =
all_data.iter().map(|(_, data)| data.len().div_ceil(SECTOR_SZ)).sum();
// vfat can hold more data than this, but we don't expect to ever need that
// for cloud-init purposes.
if file_sectors > 512 {
bail!("too much vfat data: {file_sectors} > 512 sectors");
}

// Copying the match already done for this in Omicron:
//
// if we're storing < 341 KiB of clusters, the overhead is 37. With a limit
// of 512 sectors (error check above), we can assume an overhead of 37.
// Additionally, fatfs refuses to format a disk that is smaller than 42
// sectors.
let sectors = 42.max(file_sectors + 37);

// Some tools also require that the number of sectors is a multiple of the
// sectors-per-track. fatfs uses a default of 32 which won't evenly divide
// sectors as we compute above generally. To fix that we simply set it to
// match the number of sectors to make it trivially true.
let sectors_per_track = sectors.try_into().unwrap();

let mut disk = Cursor::new(vec![0; sectors * SECTOR_SZ]);
fatfs::format_volume(
&mut disk,
FormatVolumeOptions::new()
.bytes_per_cluster(512)
.sectors_per_track(sectors_per_track)
.fat_type(fatfs::FatType::Fat12)
.volume_label(VOLUME_LABEL),
)
.context("error formatting FAT volume")?;

let fs = FileSystem::new(&mut disk, FsOptions::new())?;
let root_dir = fs.root_dir();
for (name, data) in all_data.iter() {
if *name == "network-config" && data.is_empty() {
// Skip creating an empty network interfaces if nothing is provided.
// It is not required, unlike the other files
}
root_dir.create_file(name)?.write_all(data)?;
}
drop(root_dir);
drop(fs);

block::InMemoryBackend::create(
disk.into_inner(),
block::BackendOpts {
block_size: Some(SECTOR_SZ as u32),
read_only: Some(true),
..Default::default()
},
)
.context("could not create block backend")
}
6 changes: 6 additions & 0 deletions bin/propolis-standalone/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use propolis::cpuid;
use propolis::hw::pci::Bdf;
use propolis::inventory::ChildRegister;

use crate::cidata::build_cidata_be;
pub use propolis_standalone_config::{Config, SnapshotTag};
use propolis_standalone_config::{CpuVendor, CpuidEntry, Device};

Expand Down Expand Up @@ -88,6 +89,11 @@ pub fn block_backend(
let creg = ChildRegister::new(&be, None);
(be, creg)
}
"cloudinit" => {
let be = build_cidata_be(config).unwrap();
let creg = ChildRegister::new(&be, None);
(be, creg)
}
_ => {
panic!("unrecognized block dev type {}!", be.bdtype);
}
Expand Down
1 change: 1 addition & 0 deletions bin/propolis-standalone/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use propolis::vcpu::Vcpu;
use propolis::vmm::{Builder, Machine};
use propolis::*;

mod cidata;
mod config;
mod snapshot;

Expand Down
15 changes: 15 additions & 0 deletions crates/propolis-standalone-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ pub struct Config {

#[serde(default, rename = "cpuid")]
pub cpuid_profiles: BTreeMap<String, CpuidProfile>,

pub cloudinit: Option<CloudInit>,
}
impl Config {
pub fn cpuid_profile(&self) -> Option<&CpuidProfile> {
Expand Down Expand Up @@ -79,3 +81,16 @@ pub struct BlockDevice {
#[serde(flatten, default)]
pub options: BTreeMap<String, toml::Value>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct CloudInit {
pub user_data: Option<String>,
pub meta_data: Option<String>,
pub network_config: Option<String>,

// allow path-style contents as well
pub user_data_path: Option<String>,
pub meta_data_path: Option<String>,
pub network_config_path: Option<String>,
}
7 changes: 2 additions & 5 deletions lib/propolis/src/util/regmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ impl<ID> RegMap<ID> {
debug_assert!(copy_op.offset() == 0);
f(&reg.id, RWOp::Read(copy_op));
} else {
let mut scratch = Vec::new();
scratch.resize(reg_len, 0);
let mut scratch = vec![0; reg_len];
let mut sro = ReadOp::from_buf(0, &mut scratch);

f(&reg.id, RWOp::Read(&mut sro));
Expand All @@ -154,10 +153,8 @@ impl<ID> RegMap<ID> {
debug_assert!(copy_op.offset() == 0);
f(&reg.id, RWOp::Write(copy_op));
} else {
let mut scratch = Vec::new();
scratch.resize(reg_len, 0);
let mut scratch = vec![0; reg_len];

debug_assert!(scratch.len() == reg_len);
if !reg.flags.contains(Flags::NO_READ_MOD_WRITE) {
let mut sro = ReadOp::from_buf(0, &mut scratch);
f(&reg.id, RWOp::Read(&mut sro));
Expand Down

0 comments on commit 92719c2

Please sign in to comment.