1use anyhow::Context;
7use fatfs::FormatVolumeOptions;
8use fatfs::FsOptions;
9use guid::Guid;
10use petri_artifacts_common::artifacts as common_artifacts;
11use petri_artifacts_common::tags::MachineArch;
12use petri_artifacts_common::tags::OsFlavor;
13use petri_artifacts_core::ArtifactResolver;
14use petri_artifacts_core::ResolvedArtifact;
15use std::io::Read;
16use std::io::Seek;
17use std::io::Write;
18use std::ops::Range;
19use std::path::Path;
20
21pub struct AgentImage {
23 os_flavor: OsFlavor,
24 pipette: Option<ResolvedArtifact>,
25 extras: Vec<(String, ResolvedArtifact)>,
26}
27
28impl AgentImage {
29 pub fn new(os_flavor: OsFlavor) -> Self {
31 Self {
32 os_flavor,
33 pipette: None,
34 extras: Vec::new(),
35 }
36 }
37
38 pub fn with_pipette(mut self, resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
40 self.pipette = match (self.os_flavor, arch) {
41 (OsFlavor::Windows, MachineArch::X86_64) => Some(
42 resolver
43 .require(common_artifacts::PIPETTE_WINDOWS_X64)
44 .erase(),
45 ),
46 (OsFlavor::Linux, MachineArch::X86_64) => Some(
47 resolver
48 .require(common_artifacts::PIPETTE_LINUX_X64)
49 .erase(),
50 ),
51 (OsFlavor::Windows, MachineArch::Aarch64) => Some(
52 resolver
53 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
54 .erase(),
55 ),
56 (OsFlavor::Linux, MachineArch::Aarch64) => Some(
57 resolver
58 .require(common_artifacts::PIPETTE_LINUX_AARCH64)
59 .erase(),
60 ),
61 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
62 todo!("No pipette binary yet for os");
63 }
64 };
65 self
66 }
67
68 pub fn contains_pipette(&self) -> bool {
70 self.pipette.is_some()
71 }
72
73 pub fn add_file(&mut self, name: &str, artifact: ResolvedArtifact) {
75 self.extras.push((name.to_string(), artifact));
76 }
77
78 pub fn build(&self) -> anyhow::Result<Option<tempfile::NamedTempFile>> {
81 let mut files = self
82 .extras
83 .iter()
84 .map(|(name, artifact)| (name.as_str(), PathOrBinary::Path(artifact.as_ref())))
85 .collect::<Vec<_>>();
86 let volume_label = match self.os_flavor {
87 OsFlavor::Windows => {
88 if let Some(pipette) = self.pipette.as_ref() {
91 files.push(("pipette.exe", PathOrBinary::Path(pipette.as_ref())));
92 }
93 b"pipette "
94 }
95 OsFlavor::Linux => {
96 if let Some(pipette) = self.pipette.as_ref() {
97 files.push(("pipette", PathOrBinary::Path(pipette.as_ref())));
98 }
99 files.extend([
102 (
103 "meta-data",
104 PathOrBinary::Binary(include_bytes!("../guest-bootstrap/meta-data")),
105 ),
106 (
107 "user-data",
108 if self.pipette.is_some() {
109 PathOrBinary::Binary(include_bytes!("../guest-bootstrap/user-data"))
110 } else {
111 PathOrBinary::Binary(include_bytes!(
112 "../guest-bootstrap/user-data-no-agent"
113 ))
114 },
115 ),
116 (
119 "network-config",
120 PathOrBinary::Binary(include_bytes!("../guest-bootstrap/network-config")),
121 ),
122 ]);
123 b"cidata " }
125 _ => b"cidata ",
127 };
128
129 if files.is_empty() {
130 Ok(None)
131 } else {
132 let mut image_file = tempfile::NamedTempFile::new()?;
133 image_file
134 .as_file()
135 .set_len(64 * 1024 * 1024)
136 .context("failed to set file size")?;
137 build_fat32_disk_image(&mut image_file, "CIDATA", volume_label, &files)?;
138 Ok(Some(image_file))
139 }
140 }
141}
142
143pub(crate) const SECTOR_SIZE: u64 = 512;
144
145pub(crate) enum PathOrBinary<'a> {
146 Path(&'a Path),
147 Binary(&'a [u8]),
148}
149
150pub(crate) fn build_fat32_disk_image(
151 file: &mut (impl Read + Write + Seek),
152 gpt_name: &str,
153 volume_label: &[u8; 11],
154 files: &[(&str, PathOrBinary<'_>)],
155) -> anyhow::Result<()> {
156 let partition_range =
157 build_gpt(file, gpt_name).context("failed to construct partition table")?;
158 build_fat32(
159 &mut fscommon::StreamSlice::new(file, partition_range.start, partition_range.end)?,
160 volume_label,
161 files,
162 )
163 .context("failed to format volume")?;
164 Ok(())
165}
166
167fn build_gpt(file: &mut (impl Read + Write + Seek), name: &str) -> anyhow::Result<Range<u64>> {
168 let mut gpt = gptman::GPT::new_from(file, SECTOR_SIZE, Guid::new_random().into())?;
169
170 gptman::GPT::write_protective_mbr_into(file, SECTOR_SIZE)?;
172
173 gpt[1] = gptman::GPTPartitionEntry {
175 partition_type_guid: guid::guid!("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7").into(),
177 unique_partition_guid: Guid::new_random().into(),
178 starting_lba: gpt.header.first_usable_lba,
179 ending_lba: gpt.header.last_usable_lba,
180 attribute_bits: 0,
181 partition_name: name.into(),
182 };
183 gpt.write_into(file)?;
184
185 let partition_start_byte = gpt[1].starting_lba * SECTOR_SIZE;
187 let partition_num_bytes = (gpt[1].ending_lba - gpt[1].starting_lba) * SECTOR_SIZE;
188 Ok(partition_start_byte..partition_start_byte + partition_num_bytes)
189}
190
191fn build_fat32(
192 file: &mut (impl Read + Write + Seek),
193 volume_label: &[u8; 11],
194 files: &[(&str, PathOrBinary<'_>)],
195) -> anyhow::Result<()> {
196 fatfs::format_volume(
197 &mut *file,
198 FormatVolumeOptions::new()
199 .volume_label(*volume_label)
200 .fat_type(fatfs::FatType::Fat32),
201 )
202 .context("failed to format volume")?;
203 let fs = fatfs::FileSystem::new(file, FsOptions::new()).context("failed to open fs")?;
204 for (path, src) in files {
205 let mut dest = fs
206 .root_dir()
207 .create_file(path)
208 .context("failed to create file")?;
209 match *src {
210 PathOrBinary::Path(src_path) => {
211 let mut src = fs_err::File::open(src_path)?;
212 std::io::copy(&mut src, &mut dest).context("failed to copy file")?;
213 }
214 PathOrBinary::Binary(src_data) => {
215 dest.write_all(src_data).context("failed to write file")?;
216 }
217 }
218 dest.flush().context("failed to flush file")?;
219 }
220 fs.unmount().context("failed to unmount fs")?;
221 Ok(())
222}