-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
User data support #911
User data support #911
Changes from all commits
cbe32b9
3427ae7
ef889e4
fa47eb9
bd8f3d5
159dde2
1bfbdef
a7561cd
f609797
38d6999
3a4334b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
use crate::db::{identity::Resource, model::Instance}; | ||
use fatfs::{FatType, FileSystem, FormatVolumeOptions, FsOptions}; | ||
use num_integer::Integer; | ||
use omicron_common::api::external::Error; | ||
use serde::Serialize; | ||
use std::io::{self, Cursor, Write}; | ||
use uuid::Uuid; | ||
|
||
pub const MAX_USER_DATA_BYTES: usize = 32 * 1024; // 32 KiB | ||
|
||
impl Instance { | ||
pub fn generate_cidata(&self) -> Result<Vec<u8>, Error> { | ||
// cloud-init meta-data is YAML, but YAML is a strict superset of JSON. | ||
let meta_data = serde_json::to_vec(&MetaData { | ||
instance_id: self.id(), | ||
local_hostname: &self.runtime().hostname, | ||
public_keys: &[], // TODO | ||
}) | ||
.map_err(|_| Error::internal_error("failed to serialize meta-data"))?; | ||
let cidata = | ||
build_vfat(&meta_data, &self.user_data).map_err(|err| { | ||
Error::internal_error(&format!( | ||
"failed to create cidata volume: {}", | ||
err | ||
)) | ||
})?; | ||
Ok(cidata) | ||
} | ||
} | ||
|
||
#[derive(Serialize)] | ||
#[serde(rename_all = "kebab-case")] | ||
struct MetaData<'a> { | ||
instance_id: Uuid, | ||
local_hostname: &'a str, | ||
public_keys: &'a [String], | ||
} | ||
|
||
fn build_vfat(meta_data: &[u8], user_data: &[u8]) -> io::Result<Vec<u8>> { | ||
let file_sectors = | ||
meta_data.len().div_ceil(&512) + user_data.len().div_ceil(&512); | ||
// vfat can hold more data than this, but we don't expect to ever need that for cloud-init | ||
// purposes. | ||
if file_sectors > 512 { | ||
return Err(io::Error::new(io::ErrorKind::Other, "too much vfat data")); | ||
} | ||
|
||
// https://github.com/oxidecomputer/omicron/pull/911#discussion_r851354213 | ||
// If we're storing < 170 KiB of clusters, the FAT overhead is 35 sectors; | ||
// 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); | ||
|
||
let mut disk = Cursor::new(vec![0; sectors * 512]); | ||
fatfs::format_volume( | ||
&mut disk, | ||
FormatVolumeOptions::new() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can explicitly specify that this needs to be FAT12 using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edit; Unless we think it's okay to impose an upper size bound, then setting the fat type would be totally reasonable here. |
||
.bytes_per_cluster(512) | ||
.fat_type(FatType::Fat12) | ||
.volume_label(*b"cidata "), | ||
)?; | ||
|
||
{ | ||
let fs = FileSystem::new(&mut disk, FsOptions::new())?; | ||
let root_dir = fs.root_dir(); | ||
for (file, data) in [("meta-data", meta_data), ("user-data", user_data)] | ||
{ | ||
if !data.is_empty() { | ||
let mut file = root_dir.create_file(file)?; | ||
file.write_all(data)?; | ||
} | ||
} | ||
} | ||
|
||
Ok(disk.into_inner()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
/// the fatfs crate has some unfortunate panics if you ask it to do | ||
/// incredibly stupid things, like format an empty disk or create a | ||
/// filesystem with an invalid cluster size. | ||
/// | ||
/// to ensure that our math for the filesystem size is correct, and also to | ||
/// ensure that we don't ask fatfs to do incredibly stupid things, this | ||
/// test checks that `build_vfat` works on a representative sample of weird | ||
/// file sizes. (32 KiB is our enforced limit for user_data, so push it a | ||
/// little further.) | ||
#[test] | ||
fn build_vfat_works_with_arbitrarily_sized_input() { | ||
let upper = crate::cidata::MAX_USER_DATA_BYTES + 4096; | ||
// somewhat arbitrarily-chosen prime numbers near 1 KiB and 256 bytes | ||
for md_size in (0..upper).step_by(1019) { | ||
for ud_size in (0..upper).step_by(269) { | ||
assert!(super::build_vfat( | ||
&vec![0x5a; md_size], | ||
&vec![0xa5; ud_size] | ||
) | ||
.is_ok()); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://noyaml.com/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙏