#virtual-machine #firecracker #micro-vm #devices #image-path #communicating #pku-cloud

bin+lib rustcracker

A crate for communicating with firecracker for the development of PKU-cloud

5 releases (2 stable)

1.1.0 Feb 22, 2024
1.0.1 Feb 17, 2024
0.2.1 Feb 3, 2024
0.1.0 Feb 1, 2024

#297 in Operating systems

Apache-2.0

265KB
5K SLoC

rustcracker

A crate for communicating with firecracker developed by Xue Haonan during development of PKU-cloud. Reference: firecracker-go-sdk

Thanks for supports from all members of LCPU (Linux Club of Peking University).

Example

Create 10 microVMs asynchronously. You may want to read this first.

//! Benchmark code of starting a Machine

use std::path::PathBuf;

use log::{error, info};
use run_script::ScriptOptions;
use rustcracker::{
    components::{
        command_builder::VMMCommandBuilder,
        machine::{Config, Machine, MachineError},
    },
    model::{
        balloon::Balloon,
        cpu_template::{CPUTemplate, CPUTemplateString},
        drive::Drive,
        logger::LogLevel,
        machine_configuration::MachineConfiguration,
        network_interface::NetworkInterface,
    },
    utils::{check_kvm, StdioTypes},
};

// directory that hold all the runtime structures.
const RUN_DIR: &'static str = "/tmp/rustcracker/run";

// directory that holds resources e.g. kernel image and file system image.
const RESOURCE_DIR: &'static str = "/tmp/rustcracker/res";

// directory that holds snapshots
const SNAPSHOT_DIR: &'static str = "/tmp/rustcracker/snapshot";
// a tokio coroutine that might be parallel with other coroutines
#[tokio::main]
async fn main() -> Result<(), MachineError> {
    // Initialize env logger
    let _ = env_logger::builder().is_test(false).try_init();

    // check that kvm is accessible
    check_kvm()?;

    // set up network
    let (code, _output, error) = run_script::run_script!(
        r#"
        TAP_DEV="tap$1"
        HOST_IFACE="eth$1"
        TAP_IP="172.16.0.1"
        MASK_SHORT="/30"

        # Setup network interface
        sudo ip link del "$TAP_DEV" 2> /dev/null || true
        sudo ip tuntap add dev "$TAP_DEV" mode tap
        sudo ip addr add "${TAP_IP}${MASK_SHORT}" dev "$TAP_DEV"
        sudo ip link set dev "$TAP_DEV" up

        # Enable ip forwarding
        sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

        # Set up microVM internet access
        sudo iptables -t nat -D POSTROUTING -o "$HOST_IFACE" -j MASQUERADE || true
        sudo iptables -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT \
            || true
        sudo iptables -D FORWARD -i "$TAP_DEV" -o "$HOST_IFACE" -j ACCEPT || true
        sudo iptables -t nat -A POSTROUTING -o "$HOST_IFACE" -j MASQUERADE
        sudo iptables -I FORWARD 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
        sudo iptables -I FORWARD 1 -i "$TAP_DEV" -o "$HOST_IFACE" -j ACCEPT
        "#,
        &vec![format!("0")],
        &ScriptOptions::new()
    ).unwrap();
    // if networking is configured successfully, then run the machine `name{id}``
    if !(code == 0 && error == "") {
        error!(target: "main", "Fail to set up network");
        return Err(MachineError::Execute(format!("Fail to set up network: {}",error)));
    }

    /* below are configurations that could be transmitted with json file (Serializable and Deserializable) */

    /* ############ configurations begin ############ */
    // the name of this microVM
    let vmid = "name1";

    // the directory that holds this microVM
    // /tmp/rustfire/run/name1
    let dir = PathBuf::from(RUN_DIR).join(vmid);
    std::fs::create_dir_all(&dir).map_err(|e| {
        MachineError::FileCreation(format!("fail to create {}: {}", dir.display(), e.to_string()))
    })?;

    // suppose that the logger is going to be created at "${RUN_DIR}/logger"
    // /tmp/rustfire/run/name1/log.fifo
    let log_fifo = dir.join("log.fifo");

    // metrics path
    // /tmp/rustcracker/run/name1/metrics.fifo
    let metrics_fifo = dir.join("metrics.fifo");

    // unix domain socket (communicate with firecracker) path
    // /tmp/rustcracker/run/name1/api.sock
    let socket_path = dir.join("api.sock");

    // kernel image path (prepare valid kernel image here)
    // /tmp/rustcracker/res/vmlinux
    let vmlinux_path = PathBuf::from(&RESOURCE_DIR).join("vmlinux");

    // root fs path (prepare valid root file system image here)
    // /tmp/rustcracker/res/rootfs
    let rootfs_path = PathBuf::from(&RESOURCE_DIR).join("rootfs");

    // firecracker binary
    // /tmp/rustcracker/res/firecracker
    let firecracker_path = PathBuf::from(&RESOURCE_DIR).join("firecracker");

    // path that holds snapshot
    let snapshot_dir = PathBuf::from(&SNAPSHOT_DIR).join(vmid);
    std::fs::create_dir_all(&snapshot_dir).map_err(|e| {
        MachineError::FileCreation(format!("fail to create {}: {}", snapshot_dir.display(), e.to_string()))
    })?;
    let snapshot_mem = snapshot_dir.join("mem");
    let snapshot_path = snapshot_dir.join("snapshot");

    let init_metadata = r#"{
        "name": "Xue Haonan",
        "email": "xuehaonan27@gmail.com"
    }"#;

    // write the configuration of the firecraker process
    let config = Config {
        // microVM's name
        vmid: Some(vmid.to_string()),
        // the path to unix domain socket that you want the firecracker to spawn
        socket_path: Some(socket_path.to_owned()),
        kernel_image_path: Some(vmlinux_path),
        log_fifo: Some(log_fifo.to_owned()),
        metrics_fifo: Some(metrics_fifo.to_owned()),
        log_level: Some(LogLevel::Debug),
        // the configuration of the microVM
        machine_cfg: Some(MachineConfiguration {
            // give microVM 1 virtual CPU
            vcpu_count: 1,
            // config correct CPU template here (as same as physical CPU template)
            cpu_template: Some(CPUTemplate(CPUTemplateString::None)),
            // give microVM 256 MiB memory
            mem_size_mib: 256,
            // disable hyperthreading
            ht_enabled: Some(false),
            track_dirty_pages: None,
        }),
        drives: Some(vec![Drive {
            // name that you like
            drive_id: "root".to_string(),
            // root fs is the ONLY root device that should be configured
            is_root_device: true,
            // if set true, then user cannot write to the rootfs
            is_read_only: true,
            path_on_host: rootfs_path,
            partuuid: None,
            cache_type: None,
            rate_limiter: None,
            io_engine: None,
            socket: None,
        }]),
        // kernel_args might be overrided
        // if any network interfaces configured, the `ip` field may be added or modified
        kernel_args: Some("".to_string()),
        network_interfaces: Some(vec![NetworkInterface {
            guest_mac: Some("06:00:AC:10:00:02".to_string()),
            host_dev_name: "tap0".into(),
            iface_id: "net1".into(),
            rx_rate_limiter: None,
            tx_rate_limiter: None,
        }]),
        net_ns: Some("my_netns".into()),
        balloon: Some(
            Balloon::new()
                .with_amount_mib(100)
                .with_stats_polling_interval_s(5)
                .set_deflate_on_oom(true),
        ),
        init_metadata: Some(init_metadata.to_string()),
        // configurations that could be set yourself and I don't want to set here
        forward_signals: None,
        log_path: None,
        metrics_path: None,
        initrd_path: None,

        // virtio devices
        vsock_devices: None,
        // when running in production environment, don't set this true to avoid validation
        disable_validation: false,
        enable_jailer: false,
        jailer_cfg: None,
        seccomp_level: None,
        mmds_address: None,
        stdin: Some(StdioTypes::Null),
        stdout: Some(StdioTypes::From {
            path: log_fifo.to_owned(),
        }),
        stderr: Some(StdioTypes::From {
            path: log_fifo.to_owned(),
        }),
        log_clear: Some(true),
        metrics_clear: Some(true),
        network_clear: Some(true),
        agent_init_timeout: None,
        agent_request_timeout: None,
    };
    /* ############ configurations end ############ */

    /* ############ Launching microVM ############ */
    // use sig_send to send a signal to firecracker process (yet implemented)
    // let (sig_send, sig_recv) = async_channel::bounded(64);

    #[allow(unused_variables)]
    let mut machine = Machine::new(config)?;
    // use exit_send to send a force stop instruction (MachineMessage::StopVMM) to the microVM

    // build your own microVM command
    let cmd = VMMCommandBuilder::new()
        .with_socket_path(&socket_path)
        .with_bin(&firecracker_path)
        .build();

    // set your own microVM command (optional)
    // if not, then the machine will start using default command
    // ${firecracker_path} --api-sock ${socket_path} --seccomp-level 0 --id ${config.vmid}
    // (seccomp level 0 means disable seccomp)
    machine.set_command(cmd.into());

    // start the microVM
    machine.start().await.map_err(|e| {
        // remove the socket, log and metrics in case we start fail
        let _ = std::fs::remove_file(&socket_path);
        let _ = std::fs::remove_file(&log_fifo);
        let _ = std::fs::remove_file(&metrics_fifo);
        e
    })?;

    /* ############ Checking microVM ############ */
    let metadata = machine.get_metadata().await?;
    info!(target: "Metadata", "{}", metadata);

    let instance_info = machine.describe_instance_info().await?;
    info!(target: "InstanceInfo", "{:#?}", instance_info);

    let balloon = machine.get_balloon_config().await?;
    info!(target: "Balloon", "{:#?}", balloon);

    let balloon_stats = machine.get_balloon_stats().await?;
    info!(target: "BalloonStats", "{:#?}", balloon_stats);

    /* ############ Modifying microVM ############ */
    let new_metadata = r#"{
        "name":"Mugen_Cyaegha",
        "email":"897657514@qq.com"
    }"#.to_string();
    machine.update_metadata(&new_metadata).await?;
    // machine.update_balloon(10).await?;
    // machine.update_balloon_stats(3).await?;
    machine.refresh_machine_configuration().await?;

    /* ############ Checking microVM ############ */
    let metadata = machine.get_metadata().await?;
    info!(target: "Re-Metadata", "{}", metadata);

    let instance_info = machine.describe_instance_info().await?;
    info!(target: "Re-InstanceInfo", "{:#?}", instance_info);

    let balloon = machine.get_balloon_config().await?;
    info!(target: "Re-Balloon", "{:#?}", balloon);

    let balloon_stats = machine.get_balloon_stats().await?;
    info!(target: "Re-BalloonStats", "{:#?}", balloon_stats);

    /* ############ Saving microVM ############ */
    // one should always pause the microVM before trying to create snapshot for it
    machine.pause().await?;
    info!(target: "Pause", "Paused");
    machine.create_snapshot(&snapshot_mem, &snapshot_path).await?;
    machine.resume().await?;
    info!(target: "Resume", "Resumed");

    /* ############ Exiting microVm ############ */
    // wait for the machine to exit.
    // Machine::wait will block until the firecracker process exit itself
    
    // machine.wait().await?;
    
    // explicitly call Machine::shutdown() and Machine::stop_vmm() to terminate the machine.
    tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
    machine.shutdown().await?;
    machine.stop_vmm().await?;

    Ok(())
}

Dependencies

~11–22MB
~290K SLoC