/ src / bin / cmd / image.rs
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  }