blob: 69df503fdc52eeed3a06957ce874f2474cd16e08 [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::{Partition, PartitionsConfig, Slot};
use anyhow::{Context, Result};
use assembly_manifest::Image;
use assembly_util::write_json_file;
use camino::Utf8PathBuf;
use serde_json::json;
use std::collections::BTreeMap;
use url::Url;
/// The type of the image used to correlate a partition with an image in the
/// assembly manifest.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
pub enum ImageType {
/// Zircon Boot Image.
ZBI,
/// Verified Boot Metadata.
VBMeta,
/// Fuchsia Volume Manager.
FVM,
/// Fuchsia Filesystem.
Fxfs,
}
/// A pair of an image path mapped to a specific partition.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
pub struct PartitionAndImage {
/// The partition on hardware.
pub partition: Partition,
/// The path to the image to place in the partition.
pub path: Utf8PathBuf,
}
/// A tool that can map sets of images into hardware partitions.
pub struct PartitionImageMapper {
partitions: PartitionsConfig,
images: BTreeMap<Slot, BTreeMap<ImageType, Utf8PathBuf>>,
}
impl PartitionImageMapper {
/// Construct a new mapper that targets the `partitions`.
pub fn new(partitions: PartitionsConfig) -> Self {
Self { partitions, images: BTreeMap::new() }
}
/// Map a set images that are intended for a specific slot to partitions.
pub fn map_images_to_slot(&mut self, images: &Vec<Image>, slot: Slot) {
let slot_entry = self.images.entry(slot).or_insert(BTreeMap::new());
for image in images.iter() {
match image {
Image::ZBI { path, .. } => {
slot_entry.insert(ImageType::ZBI, path.clone());
}
Image::VBMeta(path) => {
slot_entry.insert(ImageType::VBMeta, path.clone());
}
Image::FVMFastboot(path) => {
if let Slot::R = slot {
// Recovery should not include a separate FVM, because it is embedded into the
// ZBI as a ramdisk.
continue;
} else {
slot_entry.insert(ImageType::FVM, path.clone());
}
}
Image::FxfsSparse { path, .. } => {
if let Slot::R = slot {
// Recovery should not include a separate FVM, because it is embedded into the
// ZBI as a ramdisk.
continue;
} else {
slot_entry.insert(ImageType::Fxfs, path.clone());
}
}
_ => {}
}
}
}
/// Return the mappings of images to partitions.
pub fn map(&self) -> Vec<PartitionAndImage> {
self.map_internal(false)
}
/// Return the mappings of images to partitions.
/// Use the R slot images for all partitions.
pub fn map_recovery_on_all_slots(&self) -> Vec<PartitionAndImage> {
self.map_internal(true)
}
fn map_internal(&self, recovery_on_all_slots: bool) -> Vec<PartitionAndImage> {
let mut mapped_partitions = vec![];
// Assign the images to particular partitions.
for p in &self.partitions.partitions {
let (image_type, slot) = match &p {
Partition::ZBI { slot, .. } => (ImageType::ZBI, slot),
Partition::VBMeta { slot, .. } => (ImageType::VBMeta, slot),
// Arbitrarily, take the fvm from the slot A system.
Partition::FVM { .. } => (ImageType::FVM, &Slot::A),
// Arbitrarily, take Fxfs from the slot A system.
Partition::Fxfs { .. } => (ImageType::Fxfs, &Slot::A),
};
if let Some(slot) = match recovery_on_all_slots {
// If this is recovery mode, then fill every partition with images from the slot R
// system.
true => self.images.get(&Slot::R),
false => self.images.get(slot),
} {
if let Some(path) = slot.get(&image_type) {
mapped_partitions
.push(PartitionAndImage { partition: p.clone(), path: path.clone() });
}
}
}
mapped_partitions
}
/// Generate a size report that indicates the partition size and the size of the image mapped
/// to it.
pub fn generate_gerrit_size_report(
&self,
report_path: &Utf8PathBuf,
prefix: &String,
) -> Result<()> {
let mut report = BTreeMap::new();
let mappings = self.map();
for mapping in mappings {
let PartitionAndImage { partition, path } = mapping;
if let (Some(size), name) = (partition.size(), partition.name()) {
let metadata = std::fs::metadata(path).context("Getting image metadata")?;
let measured_size = metadata.len();
report.insert(format!("{}-{}", prefix, name), json!(measured_size));
report.insert(format!("{}-{}.budget", prefix, name), json!(size));
report.insert(format!("{}-{}.creepBudget", prefix, name), json!(200 * 1024));
let url = Url::parse_with_params(
"http://go/fuchsia-size-stats/single_component/",
&[("f", format!("component:in:{}-{}", prefix, name))],
)?;
report.insert(format!("{}-{}.owner", prefix, name), json!(url.as_str()));
}
}
write_json_file(&report_path, &report).context("Writing gerrit size report")
}
}
#[cfg(test)]
mod tests {
use super::*;
use assembly_manifest::{AssemblyManifest, BlobfsContents};
use assembly_util::read_config;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn test_map_fvm() {
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
Partition::FVM { name: "fvm".into(), size: None },
Partition::Fxfs { name: "fxfs".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let images_b = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/b/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/b/fuchsia.vbmeta".into()),
Image::FVM("path/to/b/fvm.blk".into()),
Image::FVMFastboot("path/to/b/fvm.fastboot.blk".into()),
],
};
let images_r = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/r/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/r/fuchsia.vbmeta".into()),
Image::FVM("path/to/r/fvm.blk".into()),
Image::FVMFastboot("path/to/r/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.map_images_to_slot(&images_b.images, Slot::B);
mapper.map_images_to_slot(&images_r.images, Slot::R);
let expected = vec![
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
path: "path/to/b/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
path: "path/to/b/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::FVM { name: "fvm".into(), size: None },
path: "path/to/a/fvm.fastboot.blk".into(),
},
];
assert_eq!(expected, mapper.map());
}
#[test]
fn test_map_recovery() {
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
Partition::FVM { name: "fvm".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let images_b = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/b/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/b/fuchsia.vbmeta".into()),
Image::FVM("path/to/b/fvm.blk".into()),
Image::FVMFastboot("path/to/b/fvm.fastboot.blk".into()),
],
};
let images_r = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/r/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/r/fuchsia.vbmeta".into()),
Image::FVM("path/to/r/fvm.blk".into()),
Image::FVMFastboot("path/to/r/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.map_images_to_slot(&images_b.images, Slot::B);
mapper.map_images_to_slot(&images_r.images, Slot::R);
let expected = vec![
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
];
assert_eq!(expected, mapper.map_recovery_on_all_slots());
}
#[test]
fn test_map_fxfs() {
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
Partition::FVM { name: "fvm".into(), size: None },
Partition::Fxfs { name: "fxfs".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FxfsSparse {
path: "path/to/a/fxfs.blk".into(),
contents: BlobfsContents::default(),
},
],
};
let images_b = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/b/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/b/fuchsia.vbmeta".into()),
Image::FxfsSparse {
path: "path/to/b/fxfs.blk".into(),
contents: BlobfsContents::default(),
},
],
};
let images_r = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/r/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/r/fuchsia.vbmeta".into()),
Image::FxfsSparse {
path: "path/to/r/fxfs.blk".into(),
contents: BlobfsContents::default(),
},
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.map_images_to_slot(&images_b.images, Slot::B);
mapper.map_images_to_slot(&images_r.images, Slot::R);
let expected = vec![
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
path: "path/to/b/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
path: "path/to/b/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::Fxfs { name: "fxfs".into(), size: None },
path: "path/to/a/fxfs.blk".into(),
},
];
assert_eq!(expected, mapper.map());
}
#[test]
fn test_no_slots() {
let partitions = PartitionsConfig { partitions: vec![], ..Default::default() };
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
assert!(mapper.map().is_empty());
}
#[test]
fn test_slot_a_only() {
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
Partition::FVM { name: "fvm".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let images_b = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/b/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/b/fuchsia.vbmeta".into()),
Image::FVM("path/to/b/fvm.blk".into()),
Image::FVMFastboot("path/to/b/fvm.fastboot.blk".into()),
],
};
let images_r = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/r/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/r/fuchsia.vbmeta".into()),
Image::FVM("path/to/r/fvm.blk".into()),
Image::FVMFastboot("path/to/r/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.map_images_to_slot(&images_b.images, Slot::B);
mapper.map_images_to_slot(&images_r.images, Slot::R);
let expected = vec![
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::FVM { name: "fvm".into(), size: None },
path: "path/to/a/fvm.fastboot.blk".into(),
},
];
assert_eq!(expected, mapper.map());
}
#[test]
fn test_missing_slot() {
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
Partition::ZBI { name: "zbi_b".into(), slot: Slot::B, size: None },
Partition::VBMeta { name: "vbmeta_b".into(), slot: Slot::B, size: None },
Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
Partition::FVM { name: "fvm".into(), size: None },
Partition::Fxfs { name: "fxfs".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/a/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/a/fuchsia.vbmeta".into()),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let images_r = AssemblyManifest {
images: vec![
Image::ZBI { path: "path/to/r/fuchsia.zbi".into(), signed: false },
Image::VBMeta("path/to/r/fuchsia.vbmeta".into()),
Image::FVM("path/to/r/fvm.blk".into()),
Image::FVMFastboot("path/to/r/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.map_images_to_slot(&images_r.images, Slot::R);
let expected = vec![
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: None },
path: "path/to/a/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::ZBI { name: "zbi_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.zbi".into(),
},
PartitionAndImage {
partition: Partition::VBMeta { name: "vbmeta_r".into(), slot: Slot::R, size: None },
path: "path/to/r/fuchsia.vbmeta".into(),
},
PartitionAndImage {
partition: Partition::FVM { name: "fvm".into(), size: None },
path: "path/to/a/fvm.fastboot.blk".into(),
},
];
assert_eq!(expected, mapper.map());
}
#[test]
fn test_size_report() {
let temp_dir = TempDir::new().unwrap();
let temp_dir_path = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()).unwrap();
let zbi_path = temp_dir_path.join("zbi");
let vbmeta_path = temp_dir_path.join("vbmeta");
let size_report_path = temp_dir_path.join("report.json");
std::fs::write(&zbi_path, "zbi").unwrap();
std::fs::write(&vbmeta_path, "vbmeta").unwrap();
let partitions = PartitionsConfig {
partitions: vec![
Partition::ZBI { name: "zbi_a".into(), slot: Slot::A, size: Some(100) },
Partition::VBMeta { name: "vbmeta_a".into(), slot: Slot::A, size: Some(200) },
Partition::FVM { name: "fvm".into(), size: None },
],
..Default::default()
};
let images_a = AssemblyManifest {
images: vec![
Image::ZBI { path: zbi_path, signed: false },
Image::VBMeta(vbmeta_path),
Image::FVM("path/to/a/fvm.blk".into()),
Image::FVMFastboot("path/to/a/fvm.fastboot.blk".into()),
],
};
let mut mapper = PartitionImageMapper::new(partitions);
mapper.map_images_to_slot(&images_a.images, Slot::A);
mapper.generate_gerrit_size_report(&size_report_path, &"prefix".to_string()).unwrap();
let result: serde_json::Value = read_config(&size_report_path).unwrap();
let expected = serde_json::json!({
"prefix-vbmeta_a": 6,
"prefix-vbmeta_a.budget": 200,
"prefix-vbmeta_a.creepBudget": 200 * 1024,
"prefix-vbmeta_a.owner": "http://go/fuchsia-size-stats/single_component/?f=component%3Ain%3Aprefix-vbmeta_a",
"prefix-zbi_a": 3,
"prefix-zbi_a.budget": 100,
"prefix-zbi_a.creepBudget": 200 * 1024,
"prefix-zbi_a.owner": "http://go/fuchsia-size-stats/single_component/?f=component%3Ain%3Aprefix-zbi_a",
});
assert_eq!(expected, result);
}
}