From f085e59b21f579c85ba69bd45ef379e8fb80255b Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 7 Mar 2021 15:44:25 +0200 Subject: chore: delete some code that was commented out --- src/bin/vmadm.rs | 46 +++------------------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/src/bin/vmadm.rs b/src/bin/vmadm.rs index 8fe5540..9b16966 100644 --- a/src/bin/vmadm.rs +++ b/src/bin/vmadm.rs @@ -1,18 +1,11 @@ use anyhow::Context; use directories_next::ProjectDirs; -use vmadm::cmd; -use vmadm::config::Configuration; -use vmadm::spec::Specification; -//use bytesize::GIB; use log::debug; -//use std::fs; use std::path::{Path, PathBuf}; -//use std::thread; use structopt::StructOpt; -//use vmadm::cloudinit::CloudInitConfig; -//use vmadm::image::VirtualMachineImage; -//use vmadm::install::{virt_install, VirtInstallArgs}; -//use vmadm::spec::Specification; +use vmadm::cmd; +use vmadm::config::Configuration; +use vmadm::spec::Specification; const QUALIFIER: &str = ""; const ORG: &str = ""; @@ -122,36 +115,3 @@ fn config_filename(common: &CommonOptions) -> PathBuf { } } } - -// fn cloud_init(spec: &Path, dirname: &Path) -> anyhow::Result<()> { -// info!("generating cloud-init configuration"); - -// debug!("reading specification from {}", spec.display()); -// let spec = fs::read(spec)?; -// let spec: Specification = serde_yaml::from_slice(&spec)?; -// debug!("spec:\n{:#?}", spec); - -// info!("creating cloud-init config"); -// let init = CloudInitConfig::from(&spec)?; - -// debug!("creating directory {}", dirname.display()); -// std::fs::create_dir_all(dirname)?; -// init.create_dir(dirname)?; - -// Ok(()) -// } - -// fn cloud_init_iso(spec: &Path, iso: &Path) -> anyhow::Result<()> { -// info!("generating cloud-init ISO"); - -// debug!("reading specification from {}", spec.display()); -// let spec = fs::read(spec)?; -// let spec: Specification = serde_yaml::from_slice(&spec)?; -// debug!("spec:\n{:#?}", spec); - -// info!("creating cloud-init config"); -// let init = CloudInitConfig::from(&spec)?; -// init.create_iso(iso)?; - -// Ok(()) -// } -- cgit v1.2.1 From 54f0cba69a023ccf0b781dd76a2b370bf6400585 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 7 Mar 2021 16:00:23 +0200 Subject: doc: point at libvirt setup help --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 5525589..7c56cec 100644 --- a/README.md +++ b/README.md @@ -139,3 +139,27 @@ where `XXX` is the public key part of the CA key, as stored in `~/.ssh/ca/vmadm_ca.pub` in the example above. This tells your client that the CA key on the line should be accepted for all hosts (`*`). You can restrict it to only some hosts if you prefer. + +# Setup of host + +The host where vmadm is run needs to have libvirt running and you must +have access to the `qemu:///system` connection. +The Debian wiki has some useful documentation: + +* +* + +I set up my own libvirt hosts using an Ansible role: +. It works on +Debian. The short version: + +* install + - `libvirt` (Debian packages `libvirt-daemon-system`, + `libvirt-daemon`, `libvirt-daemon`) + - `virt-install` (Debian package `virtinst`) + - `qemu-img` (Debian package `qemu-uttils`) + - NSS lookups for VMs (Debian package `libnss-libvirt`) + - SSH client (Debian package `openssh-client`) +* make sure you are in the `libvirt` group +* edit `/etc/nsswitch.conf` to have `libvirt libvirt_guest` in the + `hosts` line -- cgit v1.2.1 From 1e8ba95def26de67f8fd618549d6b8f80a14ddd8 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sun, 7 Mar 2021 17:07:20 +0200 Subject: doc: add doc comments to crate --- src/cloudinit.rs | 27 ++++++++++++++++++++++++ src/cmd/cloud_init.rs | 10 +++++++++ src/cmd/delete.rs | 8 +++++++ src/cmd/list.rs | 8 +++++++ src/cmd/mod.rs | 5 +++++ src/cmd/new.rs | 10 +++++++++ src/config.rs | 23 ++++++++++++++++++++ src/image.rs | 10 +++++++++ src/install.rs | 23 ++++++++++++++++++++ src/lib.rs | Bin 111 -> 650 bytes src/spec.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/sshkeys.rs | 37 ++++++++++++++++++++++++++++++++ 12 files changed, 218 insertions(+) diff --git a/src/cloudinit.rs b/src/cloudinit.rs index ccf98eb..ec4efd3 100644 --- a/src/cloudinit.rs +++ b/src/cloudinit.rs @@ -1,3 +1,10 @@ +//! Prepare [cloud-init][] configuration. +//! +//! The configuration is provided to cloud-init via the "NoCloud" +//! route, using an ISO image file. +//! +//! [cloud-init]: https://cloud-init.io/ + use crate::spec::{Specification, SpecificationError}; use crate::sshkeys::{CaKey, KeyError, KeyKind, KeyPair}; use log::debug; @@ -92,26 +99,34 @@ log("vmadm cloud-init script ending") logfile.close() "#; +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum CloudInitError { + /// CA key is not specified. #[error("Host certificate requested, but no CA key specified")] NoCAKey, + /// Something went wrong creating ISO image with configuration. #[error("failed to create ISO image with genisoimage: {0}")] IsoFailed(String), + /// Error in the specification. #[error(transparent)] SpecificationError(#[from] SpecificationError), + /// Error in generating SSH host key or certificate. #[error(transparent)] KeyError(#[from] KeyError), + /// Something went wrong doing I/O. #[error(transparent)] IoError(#[from] std::io::Error), + /// Something went wrong parsing a string as UTF8. #[error(transparent)] StringError(#[from] std::string::FromUtf8Error), + /// Something went wrong parsing or serializing to YAML. #[error(transparent)] SerdeError(#[from] serde_yaml::Error), } @@ -252,6 +267,7 @@ impl Hostkeys { } } +/// Full cloud-init configuration. #[derive(Clone, Debug)] pub struct CloudInitConfig { metadata: Metadata, @@ -259,12 +275,14 @@ pub struct CloudInitConfig { } impl CloudInitConfig { + /// Create from a specification. pub fn from(spec: &Specification) -> Result { let metadata = Metadata::from(spec); let userdata = Userdata::from(spec)?; Ok(CloudInitConfig { metadata, userdata }) } + /// Debugging aid: dump cloud-init to stdout. pub fn dump(&self) { println!("==== meta-data:\n{}", self.metadata().unwrap()); println!("==== user-data:\n{}", self.userdata().unwrap()); @@ -278,6 +296,11 @@ impl CloudInitConfig { Ok(self.userdata.as_yaml()?) } + /// Put cloud-init configuration into a named directory. + /// + /// The files `meta-data` and `user-data` will be stored in the + /// directory. cloud-init reads them on first boot and makes + /// appropriate changes the host's configuration. pub fn create_dir(&self, path: &Path) -> Result<(), CloudInitError> { let metadata = path.join("meta-data"); debug!("writing metadata to {}", metadata.display()); @@ -300,6 +323,10 @@ impl CloudInitConfig { Ok(()) } + /// Create an ISO disk image file with the cloud-init configuration. + /// + /// The image will be attached to the VM when it starts. + /// cloud-init finds it via the volume ID (file system label). pub fn create_iso(&self, filename: &Path) -> Result<(), CloudInitError> { let dir = tempdir()?; self.create_dir(dir.path())?; diff --git a/src/cmd/cloud_init.rs b/src/cmd/cloud_init.rs index a9b3588..b266eb4 100644 --- a/src/cmd/cloud_init.rs +++ b/src/cmd/cloud_init.rs @@ -1,17 +1,27 @@ +//! The `cloud-init` sub-command. + use crate::cloudinit::{CloudInitConfig, CloudInitError}; use crate::spec::Specification; use log::{debug, info}; use std::path::Path; +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum CloudInitCommandError { + /// Error in the cloud-init configuration. #[error(transparent)] CloudInitError(#[from] CloudInitError), + /// Error doing I/O. #[error(transparent)] IoError(#[from] std::io::Error), } +/// The `cloud-init` sub-command. +/// +/// This sub-command generates the cloud-init configuration based on +/// specifications provided by the caller and writes them to files in +/// a directory named by the caller. pub fn cloud_init(specs: &[Specification], dirname: &Path) -> Result<(), CloudInitCommandError> { for spec in specs { let dirname = dirname.join(&spec.name); diff --git a/src/cmd/delete.rs b/src/cmd/delete.rs index 3f60e27..a9a0660 100644 --- a/src/cmd/delete.rs +++ b/src/cmd/delete.rs @@ -1,18 +1,26 @@ +//! The `delete` sub-command. + use crate::spec::Specification; use log::{debug, info}; use std::thread; use std::time::Duration; use virt::connect::Connect; +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum DeleteError { + /// Error creating virtual machine. #[error(transparent)] VirtError(#[from] virt::error::Error), + /// Error doing I/O. #[error(transparent)] IoError(#[from] std::io::Error), } +/// Delete VMs corresponding to specifications. +/// +/// Delete the VM corresponding to each specification provided by the caller. pub fn delete(specs: &[Specification]) -> Result<(), DeleteError> { for spec in specs { info!("deleting virtual machine {}", spec.name); diff --git a/src/cmd/list.rs b/src/cmd/list.rs index a67dfb7..c83be26 100644 --- a/src/cmd/list.rs +++ b/src/cmd/list.rs @@ -1,13 +1,21 @@ +//! The `list` sub-command. + use crate::config::Configuration; use virt::connect::Connect; +/// Errors returned from this module. #[derive(Debug, thiserror::Error)] pub enum ListError { + /// An error from libvirt. #[error(transparent)] VirtError(#[from] virt::error::Error), } +/// The `list` sub-command. +/// +/// Return all the virtual machines existing on the libvirt instance, +/// and their current state. pub fn list(_config: &Configuration) -> Result<(), ListError> { let conn = Connect::open("qemu:///system")?; let domains = conn.list_all_domains(0)?; diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index f1e029c..606e326 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,3 +1,8 @@ +//! Implementations of command line sub-commands. +//! +//! This module has sub-modules with functions for all the +//! sub-commands of the command line tool part of the crate. + pub mod new; pub use new::new; diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 0f7bc94..57f69ae 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -1,3 +1,5 @@ +//! The `new` sub-command. + use crate::cloudinit::{CloudInitConfig, CloudInitError}; use crate::image::{ImageError, VirtualMachineImage}; use crate::install::{virt_install, VirtInstallArgs, VirtInstallError}; @@ -9,18 +11,26 @@ use std::net::TcpStream; const SSH_PORT: i32 = 22; +/// Errors returned by this module. #[derive(Debug, thiserror::Error)] pub enum NewError { + /// Problem with cloud-init configuration. #[error(transparent)] CloudInitError(#[from] CloudInitError), + /// Problem creating VM image. #[error(transparent)] ImageError(#[from] ImageError), + /// Problem with libvirt. #[error(transparent)] VirtInstallError(#[from] VirtInstallError), } +/// The `new` sub-command. +/// +/// Create all the new virtual machines specified by the caller. Wait +/// until each VM's SSH port listens for connections. pub fn new(specs: &[Specification]) -> Result<(), NewError> { for spec in specs { info!("creating new VM {}", spec.name); diff --git a/src/config.rs b/src/config.rs index 9f99655..9894f45 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,31 +1,54 @@ +//! Tool configuration. + use log::debug; use serde::Deserialize; use std::default::Default; use std::fs; use std::path::{Path, PathBuf}; +/// Configuration from configuration file. #[derive(Default, Debug, Deserialize)] pub struct Configuration { + /// Base image, if provided. pub default_base_image: Option, + + /// Default size of new VM image, in GiB. pub default_image_gib: Option, + + /// Default memory to give to new VM, in MiB. pub default_memory_mib: Option, + + /// Default number of CPUs for a new VM. pub default_cpus: Option, + + /// Should host certificates be generated for new VMs? pub default_generate_host_certificate: Option, + + /// Directory where new VM images should be created, if given. pub image_directory: Option, + + /// List of path names of SSH public keys to put into the default + /// user's `authorized_keys` file. pub authorized_keys: Option>, + + /// Path name to SSH CA key for creating SSH host certificates. pub ca_key: Option, } +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum ConfigurationError { + /// I/O error. #[error(transparent)] IoError(#[from] std::io::Error), + /// YAML parsing error. #[error(transparent)] YamlError(#[from] serde_yaml::Error), } impl Configuration { + /// Read configuration from named file. pub fn from_file(filename: &Path) -> Result { if filename.exists() { debug!("reading configuration file {}", filename.display()); diff --git a/src/image.rs b/src/image.rs index db6102f..62f80f7 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,26 +1,34 @@ +//! Virtual machine image handling. + use std::fs::copy; use std::path::{Path, PathBuf}; use std::process::Command; use std::result::Result; +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum ImageError { + /// Error resizing image. #[error("qemu-img resize failed: {0}")] ResizeError(String), + /// I/O error. #[error(transparent)] IoError(#[from] std::io::Error), + /// Error parsing a string as UTF8. #[error(transparent)] StringError(#[from] std::string::FromUtf8Error), } +/// A virtual machine image. #[derive(Debug, Clone)] pub struct VirtualMachineImage { filename: PathBuf, } impl VirtualMachineImage { + /// Create new image from a base image. pub fn new_from_base(base_image: &Path, image: &Path) -> Result { copy(base_image, image)?; Ok(Self { @@ -28,10 +36,12 @@ impl VirtualMachineImage { }) } + /// Filename of the image. pub fn filename(&self) -> &Path { &self.filename } + /// Change size of image. pub fn resize(&self, new_size: u64) -> Result<(), ImageError> { let r = Command::new("qemu-img") .arg("resize") diff --git a/src/install.rs b/src/install.rs index 21b9f3f..7506e0d 100644 --- a/src/install.rs +++ b/src/install.rs @@ -1,24 +1,38 @@ +//! Create a new VM. +//! +//! This module runs the `virt-install` tool to create a new VM, using +//! an existing image file. It attaches the cloud-init ISO +//! configuration image to the VM. + use crate::cloudinit::{CloudInitConfig, CloudInitError}; use crate::image::VirtualMachineImage; use std::process::Command; use std::result::Result; use tempfile::tempdir; +/// Errors from this module #[derive(Debug, thiserror::Error)] pub enum VirtInstallError { + /// Failed to create VM. #[error("virt-install failed: {0}")] VirtInstallFailed(String), + /// I/O error. #[error(transparent)] IoError(#[from] std::io::Error), + /// Error parsing a string as UTF8. #[error(transparent)] StringError(#[from] std::string::FromUtf8Error), + /// Error from cloud-init configuration. #[error(transparent)] CloudInitError(#[from] CloudInitError), } +/// Arguments to virt-install. +/// +/// These are the arguments we can adjust, for running virt-install. #[derive(Debug)] pub struct VirtInstallArgs { name: String, @@ -29,6 +43,7 @@ pub struct VirtInstallArgs { } impl VirtInstallArgs { + /// Create new set of arguments for virt-install. pub fn new(name: &str, image: &VirtualMachineImage, init: &CloudInitConfig) -> Self { Self { name: name.to_string(), @@ -39,35 +54,43 @@ impl VirtInstallArgs { } } + /// Name for new VM. pub fn name(&self) -> &str { &self.name } + /// Memory for new VM, in MiB. pub fn memory(&self) -> u64 { self.memory } + /// Change memory to give to new VM, in MiB. pub fn set_memory(&mut self, memory: u64) { self.memory = memory } + /// Virtual CPUs for new VM. pub fn vcpus(&self) -> u64 { self.vcpus } + /// Change virtual CPUs for new VM. pub fn set_vcpus(&mut self, vcpus: u64) { self.vcpus = vcpus } + /// Image for new VM. pub fn image(&self) -> &VirtualMachineImage { &self.image } + /// cloud-init configuration for new VM. pub fn init(&self) -> &CloudInitConfig { &self.init } } +/// Create new VM with virt-install. pub fn virt_install(args: &VirtInstallArgs) -> Result<(), VirtInstallError> { let dir = tempdir()?; let iso = dir.path().join("cloudinit.iso"); diff --git a/src/lib.rs b/src/lib.rs index bddc1ad..e571e80 100644 Binary files a/src/lib.rs and b/src/lib.rs differ diff --git a/src/spec.rs b/src/spec.rs index 2c13af7..fcf6eab 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,3 +1,5 @@ +//! Virtual machine specification. + use crate::config::Configuration; use log::debug; @@ -115,62 +117,117 @@ where } } +/// Effective virtual machine specification. +/// +/// This is the specification as read from the input file, with the +/// defaults from the configuration file already applied. #[derive(Debug)] pub struct Specification { + /// Name of new virtual machine to create. pub name: String, + + /// SSH public keys to install in the default user's `authorized_keys` file. pub ssh_keys: Vec, + + /// RSA host key to install in new VM. pub rsa_host_key: Option, + + /// RSA host certificate. pub rsa_host_cert: Option, + + /// DSA host key to install in new VM. pub dsa_host_key: Option, + + /// DSA host certificate. pub dsa_host_cert: Option, + + /// ECDSA host key to install in new VM. pub ecdsa_host_key: Option, + + /// ECDSA host certificate. pub ecdsa_host_cert: Option, + + /// Ed25519 host key to install in new VM. pub ed25519_host_key: Option, + + /// Ed25519 host certificate. pub ed25519_host_cert: Option, + /// Path to base image. pub base: PathBuf, + + /// Path to new VM image, to be created. pub image: PathBuf, + + /// Size of new image, in GiB. pub image_size_gib: u64, + + /// Size of memory for new VM, in MiB. pub memory_mib: u64, + + /// CPUs new VM should have. pub cpus: u64, + + /// Should a new host key and certificate be created for new VM? pub generate_host_certificate: bool, + + /// Path to CA key for creating host certificate. pub ca_key: Option, } +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum SpecificationError { + /// No base image file specified. #[error("No base image or default base image specified for {0}")] NoBaseImage(String), + /// No image file specified. #[error("No image filename specified for {0} and no image_directory in configuration")] NoImage(String), + /// No image size specified. #[error("No image size specified for {0} and no default configured")] NoImageSize(String), + /// No memory size specified. #[error("No memory size specified for {0} and no default configured")] NoMemorySize(String), + /// No CPU count specified. #[error("No CPU count specified for {0} and no default configured")] NoCpuCount(String), + /// No SSH authorized keys specified. #[error("No SSH authorized keys specified for {0} and no default configured")] NoAuthorizedKeys(String), + /// Error reading SSH public key. #[error("Failed to read SSH public key file {0}")] SshKeyRead(PathBuf, #[source] std::io::Error), + /// I/O error. #[error(transparent)] IoError(#[from] std::io::Error), + /// Error parsing string as UTF8. #[error(transparent)] FromUtf8Error(#[from] std::string::FromUtf8Error), + /// Error parsing YAML. #[error(transparent)] YamlError(#[from] serde_yaml::Error), } impl Specification { + /// Read all specifications from a file. + /// + /// Apply values from the provided configuration so that the + /// returned specifications are *effective* and the caller doesn't + /// need to worry about the configuration anymore. + /// + /// Also, SSH public keys are read from the files named in the + /// input specification. pub fn from_file( config: &Configuration, filename: &Path, diff --git a/src/sshkeys.rs b/src/sshkeys.rs index 207f6fe..813a26d 100644 --- a/src/sshkeys.rs +++ b/src/sshkeys.rs @@ -1,3 +1,5 @@ +//! Generate SSH host keys and certificates. + use std::fs::{read, File, Permissions}; use std::io::Write; use std::os::unix::fs::PermissionsExt; @@ -5,31 +7,49 @@ use std::path::Path; use std::process::Command; use tempfile::tempdir; +/// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum KeyError { + /// Could not generate a new key pair. #[error("ssh-keygen failed to generate a key: {0}")] KeyGen(String), + /// Error creating a certificate. #[error("ssh-keygen failed to certify a key: {0}")] CertError(String), + /// I/O error. #[error(transparent)] IoError(#[from] std::io::Error), + /// Error parsing a string as UTF8. #[error(transparent)] Utf8Error(#[from] std::string::FromUtf8Error), } +/// Type of SSH key. pub enum KeyKind { + /// RSA key of desired length in bits. RSA(u32), + + /// DSA of fixed length. DSA, + + /// ECDSA key of 256 bits. ECDSA256, + + /// ECDSA key of 384 bits. ECDSA384, + + /// ECDSA key of 521 bits. ECDSA521, + + /// Ed25519 key of fixed length. Ed25519, } impl KeyKind { + /// Type of key as string for ssh-keygen -t option. pub fn as_str(&self) -> &str { match self { Self::RSA(_) => "rsa", @@ -41,6 +61,9 @@ impl KeyKind { } } + /// Number of bits needed for the key. + /// + /// This is only really meaningful for RSA keys. pub fn bits(&self) -> u32 { match self { Self::RSA(bits) => *bits, @@ -53,12 +76,14 @@ impl KeyKind { } } +/// A public/private key pair. pub struct KeyPair { public: String, private: String, } impl KeyPair { + /// Create pair from string representation. pub fn from_str(public: String, private: String) -> Self { Self { private: private, @@ -66,6 +91,7 @@ impl KeyPair { } } + /// Generate a new key pair of the desired kind. pub fn generate(kind: KeyKind) -> Result { let dirname = tempdir()?; let private_key = dirname.path().join("key"); @@ -93,9 +119,12 @@ impl KeyPair { )) } + /// Public key of the pair, as a string. pub fn public(&self) -> &str { &self.public } + + /// Private key of the pair, as a string. pub fn private(&self) -> &str { &self.private } @@ -106,22 +135,30 @@ fn read_string(filename: &Path) -> Result { Ok(String::from_utf8(bytes)?) } +/// A key for SSH certificate authority. +/// +/// This is used for creating host certificates. pub struct CaKey { private: String, } impl CaKey { + /// Create new CA key from a key pair. pub fn from(pair: KeyPair) -> Self { Self { private: pair.private().to_string(), } } + /// Read CA key from a file. pub fn from_file(filename: &Path) -> Result { let private = read_string(filename)?; Ok(Self { private }) } + /// Create a host certificate. + /// + /// Return as a string. pub fn certify_host(&self, host_key: &KeyPair, hostname: &str) -> Result { let dirname = tempdir()?; let ca_key = dirname.path().join("ca"); -- cgit v1.2.1