Rust Crates (Ecosystem)

Overview

Crates are Rust’s packages. Find them at crates.io.

Add to Cargo.toml:

[dependencies]
crate_name = "version"

handlebars - Templating

Same {{variable}} syntax as Antora UI templates.

Cargo.toml
[dependencies]
handlebars = "6"
serde_json = "1"
Basic usage
use handlebars::Handlebars;
use serde_json::json;

fn main() {
    let mut hbs = Handlebars::new();

    // Register template
    hbs.register_template_string("host", "Server: {{hostname}} at {{ip}}")
       .unwrap();

    // Render with data
    let data = json!({"hostname": "kvm-01", "ip": "10.50.1.100"});
    let output = hbs.render("host", &data).unwrap();

    println!("{}", output);
    // Output: Server: kvm-01 at 10.50.1.100
}
Template from file
use handlebars::Handlebars;
use serde_json::json;

fn main() {
    let mut hbs = Handlebars::new();

    // Load template file
    hbs.register_template_file("report", "templates/report.hbs")
       .unwrap();

    let data = json!({
        "title": "Network Report",
        "hosts": ["kvm-01", "kvm-02", "kvm-03"]
    });

    println!("{}", hbs.render("report", &data).unwrap());
}
Template syntax (same as Antora)
# {{title}}

{{#each hosts}}
- {{this}}
{{/each}}

{{#if active}}
Status: ONLINE
{{else}}
Status: OFFLINE
{{/if}}

serde - Serialization

The standard for JSON, YAML, TOML parsing.

Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Struct to/from JSON
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct NetworkDevice {
    hostname: String,
    ip: String,
    vlan: u16,
}

fn main() {
    // Struct to JSON
    let device = NetworkDevice {
        hostname: "sw-01".to_string(),
        ip: "10.50.1.10".to_string(),
        vlan: 10,
    };
    let json = serde_json::to_string_pretty(&device).unwrap();
    println!("{}", json);

    // JSON to Struct
    let json_str = r#"{"hostname":"kvm-01","ip":"10.50.1.100","vlan":50}"#;
    let parsed: NetworkDevice = serde_json::from_str(json_str).unwrap();
    println!("{:?}", parsed);
}

clap - CLI Argument Parsing

Build command-line tools like ripgrep, fd.

Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
Basic CLI
use clap::Parser;

#[derive(Parser)]
#[command(name = "netcheck")]
#[command(about = "Check network connectivity")]
struct Cli {
    /// Target IP or hostname
    target: String,

    /// Port to check
    #[arg(short, long, default_value_t = 443)]
    port: u16,

    /// Verbose output
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();

    if cli.verbose {
        println!("Checking {}:{}", cli.target, cli.port);
    }
    // ... do the check
}
Usage
netcheck 10.50.1.50 --port 22 --verbose
netcheck kvm-01 -p 443 -v
netcheck --help

reqwest - HTTP Client

Make HTTP requests (like curl).

Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
GET request
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let body = reqwest::blocking::get("https://httpbin.org/ip")?
        .text()?;
    println!("{}", body);
    Ok(())
}
POST with JSON
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Payload {
    hostname: String,
    action: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::blocking::Client::new();

    let payload = Payload {
        hostname: "kvm-01".to_string(),
        action: "reboot".to_string(),
    };

    let resp = client
        .post("https://api.example.com/hosts")
        .json(&payload)
        .send()?;

    println!("Status: {}", resp.status());
    Ok(())
}

tokio - Async Runtime

Required for async Rust (non-blocking I/O).

Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
Async main
#[tokio::main]
async fn main() {
    let result = do_something_async().await;
    println!("{}", result);
}

async fn do_something_async() -> String {
    // async operations here
    "done".to_string()
}

Infrastructure Documentation with cargo doc

Use Rust types to document your infrastructure. The docs become executable, testable reference.

D2 vs cargo doc:

  • D2 = Visual diagram: "kvm-01 connects to sw-01 on VLAN 50"

  • cargo doc = Type reference: "NetworkDevice has hostname, ip, vlan fields with these constraints"

Use BOTH: D2 for topology, cargo doc for API/type reference.

Infrastructure types as documentation (src/lib.rs)
//! # Domus Infrastructure Types
//!
//! Documented infrastructure for the home enterprise.
//!
//! ## Network Layout
//!
//! | VLAN | Purpose | Subnet |
//! |------|---------|--------|
//! | 10 | Data | 10.50.10.0/24 |
//! | 50 | Management | 10.50.1.0/24 |
//! | 100 | Guest | 10.50.100.0/24 |

use serde::{Deserialize, Serialize};

/// A hypervisor running KVM/libvirt.
///
/// # Infrastructure Role
///
/// Hypervisors host all virtual machines in the environment.
/// Each hypervisor connects to management VLAN 50.
///
/// # Examples
///
/// ```
/// use infra::Hypervisor;
///
/// let kvm01 = Hypervisor {
///     hostname: "kvm-01".to_string(),
///     ip: "10.50.1.100".to_string(),
///     cores: 32,
///     ram_gb: 128,
///     storage_tb: 4,
/// };
///
/// assert_eq!(kvm01.fqdn(), "kvm-01.inside.domusdigitalis.dev");
/// ```
#[derive(Debug, Serialize, Deserialize)]
pub struct Hypervisor {
    /// Short hostname (e.g., "kvm-01")
    pub hostname: String,
    /// Management IP on VLAN 50
    pub ip: String,
    /// Physical CPU cores
    pub cores: u8,
    /// RAM in gigabytes
    pub ram_gb: u16,
    /// Storage in terabytes
    pub storage_tb: u8,
}

impl Hypervisor {
    /// Returns the fully qualified domain name.
    pub fn fqdn(&self) -> String {
        format!("{}.inside.domusdigitalis.dev", self.hostname)
    }
}

/// Virtual machine running on a hypervisor.
///
/// # Lifecycle
///
/// VMs are provisioned via:
/// 1. `virt-install` for initial creation
/// 2. Ansible for configuration
/// 3. FreeIPA for identity
///
/// # Examples
///
/// ```
/// use infra::{VirtualMachine, VmState};
///
/// let ipa01 = VirtualMachine {
///     hostname: "ipa-01".to_string(),
///     ip: "10.50.1.51".to_string(),
///     vcpus: 4,
///     ram_gb: 8,
///     hypervisor: "kvm-01".to_string(),
///     state: VmState::Running,
/// };
///
/// assert!(ipa01.is_running());
/// ```
#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualMachine {
    pub hostname: String,
    pub ip: String,
    pub vcpus: u8,
    pub ram_gb: u16,
    pub hypervisor: String,
    pub state: VmState,
}

impl VirtualMachine {
    pub fn is_running(&self) -> bool {
        matches!(self.state, VmState::Running)
    }
}

/// VM power state.
#[derive(Debug, Serialize, Deserialize)]
pub enum VmState {
    /// VM is running
    Running,
    /// VM is stopped
    Stopped,
    /// VM is paused (suspended)
    Paused,
}

/// Network VLAN definition.
///
/// # Examples
///
/// ```
/// use infra::Vlan;
///
/// let mgmt = Vlan::new(50, "Management", "10.50.1.0/24");
/// assert_eq!(mgmt.gateway(), "10.50.1.1");
/// ```
#[derive(Debug)]
pub struct Vlan {
    pub id: u16,
    pub name: String,
    pub subnet: String,
}

impl Vlan {
    pub fn new(id: u16, name: &str, subnet: &str) -> Self {
        Self {
            id,
            name: name.to_string(),
            subnet: subnet.to_string(),
        }
    }

    /// Returns the gateway (first usable IP).
    pub fn gateway(&self) -> String {
        // Simplified: assumes .1 is gateway
        let parts: Vec<&str> = self.subnet.split('/').collect();
        let octets: Vec<&str> = parts[0].split('.').collect();
        format!("{}.{}.{}.1", octets[0], octets[1], octets[2])
    }
}

/// Cloud provider resources.
pub mod cloud {
    //! # Cloud Resources
    //!
    //! Types for documenting cloud infrastructure.

    /// Cloudflare Pages deployment.
    ///
    /// # Examples
    ///
    /// ```
    /// use infra::cloud::CloudflarePages;
    ///
    /// let docs = CloudflarePages {
    ///     project: "domus-docs".to_string(),
    ///     domain: "docs.domusdigitalis.dev".to_string(),
    ///     build_cmd: "npx antora antora-playbook.yml".to_string(),
    ///     output_dir: "build/site".to_string(),
    /// };
    /// ```
    #[derive(Debug)]
    pub struct CloudflarePages {
        pub project: String,
        pub domain: String,
        pub build_cmd: String,
        pub output_dir: String,
    }

    /// AWS EC2 instance type reference.
    ///
    /// # Size Reference
    ///
    /// | Type | vCPUs | RAM | Use Case |
    /// |------|-------|-----|----------|
    /// | t3.micro | 2 | 1GB | Testing |
    /// | t3.small | 2 | 2GB | Dev |
    /// | t3.medium | 2 | 4GB | Small prod |
    /// | m5.large | 2 | 8GB | Production |
    #[derive(Debug)]
    pub struct Ec2Instance {
        pub instance_id: String,
        pub instance_type: String,
        pub availability_zone: String,
        pub private_ip: String,
        pub state: String,
    }
}
Generate and browse
cargo doc --open

You get:

  • Searchable HTML docs for all your infrastructure types

  • Tables render in the docs (VLAN table, EC2 sizes)

  • Examples that are TESTED (cargo test --doc)

  • Cross-links between types ([`VmState]` links to enum)

When to use what:

| Need | Tool | |------|------| | Show network topology | D2 | | Show data flow | D2 | | Document types/APIs | cargo doc | | Executable examples | cargo doc | | Architecture overview | D2 | | Configuration reference | cargo doc |


Crates to Explore Later

| Crate | Purpose | When | |-------|---------|------| | anyhow | Simplified error handling | After Error Handling section | | thiserror | Custom error types | After Error Handling section | | regex | Regular expressions | When parsing text | | chrono | Date/time | When working with timestamps | | log + env_logger | Logging | For CLI tools | | ssh2 | SSH connections | For infrastructure tools | | rusqlite | SQLite database | For local storage |