image.rs
1 use std::path::{Path, PathBuf}; 2 3 use clap::Parser; 4 use tempfile::tempdir; 5 6 use ambient_ci::{ 7 action::UnsafeAction, 8 cloud_init::{CloudInitError, LocalDataStoreBuilder}, 9 image_store::{ImageStore, ImageStoreError, MetadataBuilder}, 10 plan::RunnablePlan, 11 qemu::{self, QemuError, QemuRunner}, 12 qemu_utils::{convert_image, QemuUtilError}, 13 run::{create_cloud_init_iso, create_executor_vdrive, create_source_vdrive}, 14 runlog::{RunLog, RunLogSource}, 15 util::{cat_text_file, mkdir, UtilError}, 16 vdrive::VirtualDriveError, 17 }; 18 19 use super::{AmbientError, Config, Leaf}; 20 21 const SECRET: &str = "xyzzy"; 22 23 /// Prepare image. 24 #[derive(Debug, Parser)] 25 pub struct PrepareImage { 26 /// The name of the image in the image store to use as the base image. 27 #[clap(long)] 28 base: String, 29 30 /// The name of the image in the new prepared image in the image store. 31 #[clap(long)] 32 new: String, 33 34 /// Location of the ambient-execute-plan binary. 35 #[clap(long)] 36 executor: PathBuf, 37 38 /// Enable network? 39 #[clap(long)] 40 network: bool, 41 42 /// Run log goes to this file. 43 #[clap(long)] 44 run_log: Option<PathBuf>, 45 46 /// Console log goes to this file. 47 #[clap(long)] 48 console_log: Option<PathBuf>, 49 50 /// Run shell command to modify image. 51 #[clap(long)] 52 shell: Option<String>, 53 } 54 55 impl Leaf for PrepareImage { 56 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 57 let mut image_store = 58 ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 59 if image_store.contains(&self.new) { 60 return Err(ImageError::ExistsAlready(self.new.clone()).into()); 61 } 62 if let Some(metadata) = image_store.get_metadata(&self.base) { 63 let filename = image_store.image_filename(metadata); 64 if !filename.exists() { 65 return Err(ImageError::NoSuchFile(filename.clone()).into()); 66 } 67 68 let filename = std::fs::canonicalize(&filename) 69 .map_err(|err| ImageError::Canonicalize(filename.clone(), err))?; 70 71 let tmp = tempdir().map_err(ImageError::TempDir)?; 72 let cow_image = tmp.path().join("new.qcow2"); 73 let src = tmp.path().join("src"); 74 mkdir(&src).map_err(ImageError::Util)?; 75 76 let mut plan = RunnablePlan::default(); 77 78 let shell = self.shell.clone().unwrap_or("echo hello, world".into()); 79 80 plan.push_unsafe_actions( 81 [ 82 UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)), 83 UnsafeAction::mkdir(Path::new(qemu::SOURCE_DIR)), 84 UnsafeAction::shell(&shell), 85 ] 86 .iter(), 87 ); 88 plan.set_executor_drive(qemu::EXECUTOR_DRIVE); 89 plan.set_source_drive(qemu::SOURCE_DRIVE); 90 plan.set_source_dir(qemu::SOURCE_DIR); 91 let executor_drive = create_executor_vdrive(&tmp, &plan, &self.executor) 92 .map_err(ImageError::CreateDrive)?; 93 let source_drive = create_source_vdrive(&tmp, &src).map_err(ImageError::CreateDrive)?; 94 95 let run_log = self 96 .run_log 97 .clone() 98 .unwrap_or_else(|| tmp.path().join("run.log")); 99 let console_log = self 100 .console_log 101 .clone() 102 .unwrap_or_else(|| tmp.path().join("console.log")); 103 104 let ds = create_cloud_init_iso(self.network).map_err(ImageError::CreateDrive)?; 105 106 let mut runlog = RunLog::default(); 107 let res = QemuRunner::default() 108 .config(config) 109 .base_image(&filename) 110 .cow_image(&cow_image) 111 .executor(&executor_drive) 112 .source(&source_drive) 113 .cloud_init(&ds) 114 .console_log(&console_log) 115 .raw_log(&run_log) 116 .network(self.network) 117 .run(RunLogSource::Plan, &mut runlog) 118 .map_err(ImageError::Qemu); 119 120 res?; 121 122 let full_image = tmp.path().join("full.qcow2"); 123 convert_image(&cow_image, &full_image).map_err(|err| { 124 ImageError::ConvertImage(cow_image.clone(), full_image.clone(), err) 125 })?; 126 127 let mut new_metadata = 128 MetadataBuilder::new(&self.new, &full_image).map_err(ImageError::ImageStore)?; 129 new_metadata.uefi(metadata.uefi()); 130 let new_metadata = new_metadata.build(); 131 132 image_store 133 .import(new_metadata) 134 .map_err(ImageError::ImageStore)?; 135 image_store.save().map_err(ImageError::ImageStore)?; 136 137 Ok(()) 138 } else { 139 Err(ImageError::NoSuchImage(self.base.clone()).into()) 140 } 141 } 142 } 143 144 /// Verify that a virtual machine image is acceptable for use with 145 /// ambient. This is done by booting a VM using the image, and 146 /// making sure the VM run the provided executable and then shuts down. 147 /// 148 /// If the verification doesn't finish within a reasonable time, the 149 /// image is not acceptable. 150 #[derive(Debug, Parser)] 151 pub struct VerifyImage { 152 /// The name of the image in the image store to test. 153 #[clap(long)] 154 name: String, 155 156 /// Location of the ambient-execute-plan binary. 157 #[clap(long)] 158 executor: PathBuf, 159 160 /// Enable network? 161 #[clap(long)] 162 network: bool, 163 164 /// Run log goes to this file. 165 #[clap(long)] 166 run_log: Option<PathBuf>, 167 168 /// Console log goes to this file. 169 #[clap(long)] 170 console_log: Option<PathBuf>, 171 } 172 173 impl Leaf for VerifyImage { 174 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 175 let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 176 if let Some(metadata) = image_store.get_metadata(&self.name) { 177 let filename = image_store.image_filename(metadata); 178 if !filename.exists() { 179 return Err(ImageError::NoSuchFile(filename.clone()).into()); 180 } 181 182 let filename = std::fs::canonicalize(&filename) 183 .map_err(|err| ImageError::Canonicalize(filename.clone(), err))?; 184 185 let tmp = tempdir().map_err(ImageError::TempDir)?; 186 let src = tmp.path().join("src"); 187 mkdir(&src).map_err(ImageError::Util)?; 188 189 let mut plan = RunnablePlan::default(); 190 plan.push_unsafe_actions( 191 [ 192 UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)), 193 UnsafeAction::mkdir(Path::new(qemu::SOURCE_DIR)), 194 UnsafeAction::shell(&format!("echo {SECRET}")), 195 ] 196 .iter(), 197 ); 198 plan.set_executor_drive(qemu::EXECUTOR_DRIVE); 199 plan.set_source_drive(qemu::SOURCE_DRIVE); 200 plan.set_source_dir(qemu::SOURCE_DIR); 201 let executor_drive = create_executor_vdrive(&tmp, &plan, &self.executor) 202 .map_err(ImageError::CreateDrive)?; 203 let source_drive = create_source_vdrive(&tmp, &src).map_err(ImageError::CreateDrive)?; 204 205 let run_log = self 206 .run_log 207 .clone() 208 .unwrap_or_else(|| tmp.path().join("run.log")); 209 let console_log = self 210 .console_log 211 .clone() 212 .unwrap_or_else(|| tmp.path().join("console.log")); 213 214 let ds = create_cloud_init_iso(self.network).map_err(ImageError::CreateDrive)?; 215 216 let mut runlog = RunLog::default(); 217 let res = QemuRunner::default() 218 .config(config) 219 .base_image(&filename) 220 .executor(&executor_drive) 221 .source(&source_drive) 222 .cloud_init(&ds) 223 .console_log(&console_log) 224 .raw_log(&run_log) 225 .network(self.network) 226 .run(RunLogSource::Plan, &mut runlog) 227 .map_err(ImageError::Qemu); 228 229 let log = cat_text_file(&run_log).map_err(ImageError::Util)?; 230 if !log.contains(SECRET) { 231 return Err(ImageError::NotAcceptable(filename.clone()).into()); 232 } 233 println!("image {} is acceptable to Ambient", filename.display()); 234 235 res?; 236 Ok(()) 237 } else { 238 Err(ImageError::NoSuchImage(self.name.clone()).into()) 239 } 240 } 241 } 242 243 /// Create a cloud-init local data store ISO file. 244 #[derive(Debug, Parser)] 245 pub struct CloudInit { 246 /// The name of the ISO file. 247 #[clap(long)] 248 filename: PathBuf, 249 250 /// Commands to run via cloud-init. 251 #[clap(long)] 252 runcmd: Vec<String>, 253 254 /// Enable networking? 255 #[clap(long)] 256 network: bool, 257 } 258 259 impl Leaf for CloudInit { 260 fn run(&self, _config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 261 let mut ds = LocalDataStoreBuilder::default() 262 .with_network(self.network) 263 .with_hostname("ambient") 264 .with_bootcmd("echo xyzzy2") 265 .with_bootcmd("echo more bootcmd"); 266 for runcmd in self.runcmd.iter() { 267 ds = ds.with_runcmd(runcmd); 268 } 269 let ds = ds.build().map_err(ImageError::CloudInit)?; 270 ds.iso(&self.filename).map_err(ImageError::CloudInit)?; 271 Ok(()) 272 } 273 } 274 275 /// List names of images in the image store. 276 #[derive(Debug, Parser)] 277 pub struct ListImages {} 278 279 impl Leaf for ListImages { 280 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 281 let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 282 for name in image_store.image_names().map_err(ImageError::ImageStore)? { 283 println!("{name}"); 284 } 285 Ok(()) 286 } 287 } 288 289 /// Import images in the image store. 290 #[derive(Debug, Parser)] 291 pub struct ImportImage { 292 /// Name of image in the store. 293 name: String, 294 295 /// File name of the image to import and copy into the store. 296 image: PathBuf, 297 298 /// Description of image. 299 #[clap(short, long)] 300 description: Option<String>, 301 302 /// URL for origin of image. 303 #[clap(short, long)] 304 url: Option<String>, 305 306 /// Should the image be booted with UEFI? 307 #[clap(long)] 308 uefi: bool, 309 } 310 311 impl Leaf for ImportImage { 312 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 313 let mut metadata = 314 MetadataBuilder::new(&self.name, &self.image).map_err(ImageError::ImageStore)?; 315 if let Some(d) = &self.description { 316 metadata.description(d); 317 } 318 if let Some(u) = &self.url { 319 metadata.url(u); 320 } 321 metadata.uefi(self.uefi || config.uefi()); 322 let metadata = metadata.build(); 323 324 let mut image_store = 325 ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 326 image_store 327 .import(metadata) 328 .map_err(ImageError::ImageStore)?; 329 image_store.save().map_err(ImageError::ImageStore)?; 330 Ok(()) 331 } 332 } 333 334 /// Remove images from the image store. 335 #[derive(Debug, Parser)] 336 pub struct RemoveImages { 337 /// Names of image to remove. 338 images: Vec<String>, 339 } 340 341 impl Leaf for RemoveImages { 342 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 343 let mut image_store = 344 ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 345 for image in self.images.iter() { 346 image_store.remove(image).map_err(ImageError::ImageStore)?; 347 } 348 image_store.save().map_err(ImageError::ImageStore)?; 349 Ok(()) 350 } 351 } 352 353 /// Show information about an image in the image store. 354 #[derive(Debug, Parser)] 355 pub struct ShowImage { 356 /// Name of image. 357 image: String, 358 } 359 360 impl Leaf for ShowImage { 361 fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> { 362 let image_store = ImageStore::new(config.image_store()).map_err(ImageError::ImageStore)?; 363 if let Some(image) = image_store.get_metadata(&self.image) { 364 println!("{}", image.to_json().map_err(ImageError::ImageStore)?); 365 Ok(()) 366 } else { 367 Err(ImageError::NoSuchImage(self.image.clone()))? 368 } 369 } 370 } 371 372 #[derive(Debug, thiserror::Error)] 373 pub enum ImageError { 374 #[error(transparent)] 375 Qemu(#[from] QemuError), 376 377 #[error("failed to create a temporary directory")] 378 TempDir(#[source] std::io::Error), 379 380 #[error(transparent)] 381 Util(#[from] UtilError), 382 383 #[error(transparent)] 384 VDrive(#[from] VirtualDriveError), 385 386 #[error(transparent)] 387 CloudInit(#[from] CloudInitError), 388 389 #[error("image {0} does not exist")] 390 NoSuchFile(PathBuf), 391 392 #[error("image {0} does not exist in the image store")] 393 NoSuchImage(String), 394 395 #[error("image {0} is NOT an acceptable image for Ambient")] 396 NotAcceptable(PathBuf), 397 398 #[error("image store already contains {0}")] 399 ExistsAlready(String), 400 401 #[error(transparent)] 402 CreateDrive(#[from] ambient_ci::run::RunError), 403 404 #[error(transparent)] 405 ImageStore(#[from] ImageStoreError), 406 407 #[error("failed to make filename canonical: {0}")] 408 Canonicalize(PathBuf, #[source] std::io::Error), 409 410 #[error("failed to convert image {0} to {1}")] 411 ConvertImage(PathBuf, PathBuf, #[source] QemuUtilError), 412 }