public inbox for kdevops@lists.linux.dev
 help / color / mirror / Atom feed
From: Luis Chamberlain <mcgrof@kernel.org>
To: Chuck Lever <cel@kernel.org>, Daniel Gomez <da.gomez@kruces.com>,
	kdevops@lists.linux.dev
Cc: Luis Chamberlain <mcgrof@kernel.org>
Subject: [PATCH 5/5] rcloud: Add experimental private cloud infrastructure for bare metal
Date: Fri, 17 Oct 2025 19:32:17 -0700	[thread overview]
Message-ID: <20251018023218.2240269-6-mcgrof@kernel.org> (raw)
In-Reply-To: <20251018023218.2240269-1-mcgrof@kernel.org>

This introduces rcloud - an experimental Rust-based REST API server and
Terraform provider for managing virtual machines on bare metal servers
using libvirt and kdevops guestfs base images. NixOS support can be
added later.

Problem Statement:

For a long time one of the most difficult things to support on kdevops
is users with different distributions and the different libvirt setting
requirements. While one possibility is to build distribution packages
(rpms/debs) for a pre-defined kdevops setup, another alternative is to
abstract away guest management entirely and let users interact with a
private local cloud solution via REST API and Terraform.

Existing options evaluated:
 * OpenStack - heavy, complex, dated architecture
 * Ubicloud - Ruby-based

rcloud provides a lightweight, Rust-based alternative designed
specifically for kernel development and testing workflows.

Administrator Setup:

System administrators install rcloud once per bare metal server:

```bash
make defconfig-rcloud
make
make rcloud
```

This deploys:
- rcloud REST API server (systemd service on port 8765)
- Terraform provider for Infrastructure as Code
- Integration with kdevops guestfs base images
- Prometheus metrics endpoint for monitoring

The admin is responsible for creating base images that users will
consume:

```bash
make rcloud-base-images
```

User Testing Workflow:

Once rcloud is installed by an admin, users can provision VMs without
requiring root access or libvirt configuration knowledge:

```bash
make defconfig-rcloud-guest-test
make
make bringup
```

This enables:
- REST API access for VM lifecycle management (create, start, stop, destroy)
- Terraform-based VM provisioning
- Base image discovery and selection
- Health monitoring and status reporting

Implementation Details:

The rcloud implementation leverages:
- REST API for VM lifecycle management
- kdevops guestfs base images (no rebuilding required)
- libvirt for underlying VM management
- Rust for performance, safety, and reliability
- Prometheus metrics endpoint for observability
- Systemd integration for service management

All Rust code is formatted using Linux kernel rustfmt standards from
.rustfmt.toml (added in install-rust-deps commit) and verified with
cargo clippy for code quality.

Documentation:
- Architecture and design: workflows/rcloud/design.md
- Testing guide: workflows/rcloud/docs/testing.md
- Authentication (future): workflows/rcloud/docs/authentication.md

Generated-by: Claude AI
Signed-off-by: Luis Chamberlain <mcgrof@kernel.org>
---
 Makefile                                      |    5 +
 README.md                                     |    1 +
 defconfigs/rcloud                             |   53 +
 defconfigs/rcloud-guest-test                  |   58 +
 kconfigs/workflows/Kconfig                    |    2 +
 playbooks/install-rcloud-deps.yml             |    5 +
 playbooks/rcloud.yml                          |  107 +
 .../base_image/templates/virt-builder.j2      |    8 +-
 .../templates/rcloud/terraform.tfvars.j2      |   16 +
 .../tasks/install-deps/debian/main.yml        |   10 +
 .../tasks/install-deps/fedora/main.yml        |    9 +
 .../tasks/install-deps/main.yml               |   21 +
 .../tasks/install-deps/redhat/main.yml        |    9 +
 .../tasks/install-deps/suse/main.yml          |    9 +
 .../roles/install-rcloud-deps/tasks/main.yml  |   25 +
 scripts/generate_cloud_configs.py             |    2 +-
 scripts/install-rcloud-deps.Makefile          |   13 +
 scripts/terraform.Makefile                    |    8 +
 terraform-provider-rcloud/.gitignore          |   20 +
 terraform-provider-rcloud/README.md           |  101 +
 terraform-provider-rcloud/examples/main.tf    |   28 +
 terraform-provider-rcloud/go.mod              |   32 +
 terraform-provider-rcloud/go.sum              |   75 +
 .../internal/provider/client.go               |  207 +
 .../internal/provider/provider.go             |  129 +
 .../internal/provider/resource_vm.go          |  344 ++
 terraform-provider-rcloud/main.go             |   47 +
 terraform/Kconfig.providers                   |   12 +
 terraform/rcloud/Kconfig                      |   25 +
 terraform/rcloud/main.tf                      |    8 +
 terraform/rcloud/output.tf                    |   10 +
 terraform/rcloud/provider.tf                  |   14 +
 terraform/rcloud/shared.tf                    |   60 +
 terraform/rcloud/vars.tf                      |    9 +
 workflows/rcloud/.gitignore                   |    1 +
 workflows/rcloud/Cargo.lock                   | 3550 +++++++++++++++++
 workflows/rcloud/Cargo.toml                   |   69 +
 workflows/rcloud/DESIGN.md                    |  444 +++
 workflows/rcloud/Kconfig                      |   53 +
 workflows/rcloud/Makefile                     |  149 +
 workflows/rcloud/docs/AUTHENTICATION.md       |  166 +
 workflows/rcloud/docs/TESTING.md              |  429 ++
 workflows/rcloud/scripts/check-health.py      |  158 +
 workflows/rcloud/src/api/handlers/health.rs   |   51 +
 workflows/rcloud/src/api/handlers/images.rs   |   31 +
 workflows/rcloud/src/api/handlers/metrics.rs  |   15 +
 workflows/rcloud/src/api/handlers/mod.rs      |    4 +
 workflows/rcloud/src/api/handlers/vms.rs      |  172 +
 workflows/rcloud/src/api/mod.rs               |    3 +
 workflows/rcloud/src/api/models.rs            |   55 +
 workflows/rcloud/src/api/routes.rs            |   24 +
 workflows/rcloud/src/config/kdevops.rs        |  121 +
 workflows/rcloud/src/config/mod.rs            |  117 +
 workflows/rcloud/src/lib.rs                   |    4 +
 workflows/rcloud/src/main.rs                  |   64 +
 workflows/rcloud/src/metrics.rs               |  100 +
 workflows/rcloud/src/vm/disk.rs               |   66 +
 workflows/rcloud/src/vm/manager.rs            |  520 +++
 workflows/rcloud/src/vm/mod.rs                |    5 +
 workflows/rcloud/src/vm/xml.rs                |  169 +
 workflows/rcloud/tests/api_tests.rs           |   18 +
 61 files changed, 8037 insertions(+), 3 deletions(-)
 create mode 100644 defconfigs/rcloud
 create mode 100644 defconfigs/rcloud-guest-test
 create mode 100644 playbooks/install-rcloud-deps.yml
 create mode 100644 playbooks/rcloud.yml
 create mode 100644 playbooks/roles/gen_tfvars/templates/rcloud/terraform.tfvars.j2
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/install-deps/debian/main.yml
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/install-deps/fedora/main.yml
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/install-deps/main.yml
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/install-deps/redhat/main.yml
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/install-deps/suse/main.yml
 create mode 100644 playbooks/roles/install-rcloud-deps/tasks/main.yml
 create mode 100644 scripts/install-rcloud-deps.Makefile
 create mode 100644 terraform-provider-rcloud/.gitignore
 create mode 100644 terraform-provider-rcloud/README.md
 create mode 100644 terraform-provider-rcloud/examples/main.tf
 create mode 100644 terraform-provider-rcloud/go.mod
 create mode 100644 terraform-provider-rcloud/go.sum
 create mode 100644 terraform-provider-rcloud/internal/provider/client.go
 create mode 100644 terraform-provider-rcloud/internal/provider/provider.go
 create mode 100644 terraform-provider-rcloud/internal/provider/resource_vm.go
 create mode 100644 terraform-provider-rcloud/main.go
 create mode 100644 terraform/rcloud/Kconfig
 create mode 100644 terraform/rcloud/main.tf
 create mode 100644 terraform/rcloud/output.tf
 create mode 100644 terraform/rcloud/provider.tf
 create mode 100644 terraform/rcloud/shared.tf
 create mode 100644 terraform/rcloud/vars.tf
 create mode 100644 workflows/rcloud/.gitignore
 create mode 100644 workflows/rcloud/Cargo.lock
 create mode 100644 workflows/rcloud/Cargo.toml
 create mode 100644 workflows/rcloud/DESIGN.md
 create mode 100644 workflows/rcloud/Kconfig
 create mode 100644 workflows/rcloud/Makefile
 create mode 100644 workflows/rcloud/docs/AUTHENTICATION.md
 create mode 100644 workflows/rcloud/docs/TESTING.md
 create mode 100644 workflows/rcloud/scripts/check-health.py
 create mode 100644 workflows/rcloud/src/api/handlers/health.rs
 create mode 100644 workflows/rcloud/src/api/handlers/images.rs
 create mode 100644 workflows/rcloud/src/api/handlers/metrics.rs
 create mode 100644 workflows/rcloud/src/api/handlers/mod.rs
 create mode 100644 workflows/rcloud/src/api/handlers/vms.rs
 create mode 100644 workflows/rcloud/src/api/mod.rs
 create mode 100644 workflows/rcloud/src/api/models.rs
 create mode 100644 workflows/rcloud/src/api/routes.rs
 create mode 100644 workflows/rcloud/src/config/kdevops.rs
 create mode 100644 workflows/rcloud/src/config/mod.rs
 create mode 100644 workflows/rcloud/src/lib.rs
 create mode 100644 workflows/rcloud/src/main.rs
 create mode 100644 workflows/rcloud/src/metrics.rs
 create mode 100644 workflows/rcloud/src/vm/disk.rs
 create mode 100644 workflows/rcloud/src/vm/manager.rs
 create mode 100644 workflows/rcloud/src/vm/mod.rs
 create mode 100644 workflows/rcloud/src/vm/xml.rs
 create mode 100644 workflows/rcloud/tests/api_tests.rs

diff --git a/Makefile b/Makefile
index d4f81a14..bf72eee9 100644
--- a/Makefile
+++ b/Makefile
@@ -179,6 +179,7 @@ endif # WORKFLOW_KOTD_ENABLE
 DEFAULT_DEPS += $(DEFAULT_DEPS_REQS_EXTRA_VARS)
 
 include scripts/install-menuconfig-deps.Makefile
+include scripts/install-rcloud-deps.Makefile
 
 include Makefile.btrfs_progs
 
@@ -197,6 +198,10 @@ endif # CONFIG_HYPERVISOR_TUNING
 include Makefile.linux-mirror
 include Makefile.docker-mirror
 
+ifeq (y,$(CONFIG_RCLOUD))
+include workflows/rcloud/Makefile
+endif
+
 ifeq (y,$(CONFIG_KDEVOPS_DISTRO_REG_METHOD_TWOLINE))
 DEFAULT_DEPS += playbooks/secret.yml
 endif
diff --git a/README.md b/README.md
index a59bda76..52b07aed 100644
--- a/README.md
+++ b/README.md
@@ -377,6 +377,7 @@ want to just use the kernel that comes with your Linux distribution.
   * [kdevops CXL docs](docs/cxl.md)
   * [kdevops NFS docs](docs/nfs.md)
   * [kdevops selftests docs](docs/selftests.md)
+  * [kdevops rcloud docs](workflows/rcloud/docs/TESTING.md)
   * [kdevops reboot-limit docs](docs/reboot-limit.md)
   * [kdevops AI workflow docs](docs/ai/README.md)
   * [kdevops vLLM workflow docs](workflows/vllm/)
diff --git a/defconfigs/rcloud b/defconfigs/rcloud
new file mode 100644
index 00000000..751fa75c
--- /dev/null
+++ b/defconfigs/rcloud
@@ -0,0 +1,53 @@
+# rcloud server-only configuration
+#
+# PURPOSE: Production deployment of rcloud REST API server
+#
+# Use this when:
+#   - Deploying rcloud to a dedicated server
+#   - Base images already exist (created elsewhere or separately)
+#   - You only need the rcloud server component
+#
+# For testing/development, use 'defconfig-rcloud-guest-test' instead,
+# which includes both base image creation AND rcloud server setup.
+#
+# This defconfig:
+#   - Installs the rcloud REST API server
+#   - Enables the Terraform provider
+#   - Configures the systemd service
+#   - Does NOT create base images (assumes they exist)
+#   - Does NOT create test VMs (rcloud creates them on demand via API)
+#
+# After 'make rcloud', the API will be available at:
+#   http://localhost:8765/api/v1/health
+#
+# Test with:
+#   make rcloud-status                    # Check health
+#   curl http://localhost:8765/api/v1/vms # List VMs
+#   terraform apply                       # Use Terraform provider
+
+# Skip interactive bringup (rcloud server doesn't need test VMs)
+CONFIG_SKIP_BRINGUP=y
+
+# Disable test workflows (this is a cloud infrastructure server)
+CONFIG_WORKFLOWS=n
+
+# Enable guestfs for base image management
+CONFIG_GUESTFS=y
+
+# Use Debian nocloud variant for local VM testing
+# The nocloud variant doesn't have cloud-init and is designed for bare metal/local use
+# The generic variant requires cloud-init which virt-builder removes, breaking networking
+CONFIG_GUESTFS_DEBIAN_TRIXIE_NOCLOUD_AMD64=y
+
+# Don't copy host APT sources to guest base images
+# Base images should use standard Debian repositories for reliability
+# Custom repositories can be added during per-user VM customization
+# Note: Must disable GUESTFS_DEBIAN_COPY_HOST_SOURCES (not COPY_SOURCES directly)
+# because Kconfig 'select' cannot be overridden
+CONFIG_GUESTFS_DEBIAN_COPY_HOST_SOURCES=n
+
+# Enable rcloud REST API server
+CONFIG_RCLOUD=y
+CONFIG_RCLOUD_SERVER_BIND="127.0.0.1:8765"
+CONFIG_RCLOUD_WORKERS=4
+CONFIG_RCLOUD_ENABLE_TERRAFORM_PROVIDER=y
diff --git a/defconfigs/rcloud-guest-test b/defconfigs/rcloud-guest-test
new file mode 100644
index 00000000..01c8b0cc
--- /dev/null
+++ b/defconfigs/rcloud-guest-test
@@ -0,0 +1,58 @@
+# rcloud user/client configuration
+#
+# PURPOSE: Provision VMs through rcloud REST API using Terraform
+#
+# This defconfig is for USERS who want to provision VMs through an existing
+# rcloud server. The admin must have already set up the rcloud server using
+# defconfig-rcloud.
+#
+# Typical workflow:
+#   # Admin setup (one time):
+#   make defconfig-rcloud
+#   make
+#   make rcloud            # Sets up rcloud server and base images
+#
+#   # User workflow (on same system or remotely):
+#   make defconfig-rcloud-guest-test
+#   make
+#   make bringup           # Provisions VMs through rcloud API using Terraform
+#
+# This treats rcloud as a local cloud provider, just like AWS or Azure.
+# Users provision VMs through the standard Terraform workflow.
+
+# Basic system configuration
+CONFIG_KDEVOPS_HOSTS_PREFIX="rcloud-test"
+
+# Number of VMs to create
+CONFIG_KDEVOPS_NODES=1
+
+# VM resources (requested from rcloud)
+CONFIG_LIBVIRT_MACHINE_TYPE_Q35=y
+CONFIG_LIBVIRT_HOST_PASSTHROUGH=y
+CONFIG_LIBVIRT_MEMORY_MB=4096
+CONFIG_LIBVIRT_VCPUS=2
+
+# Disk configuration
+CONFIG_LIBVIRT_EXTRA_STORAGE_DRIVE_NVME=y
+CONFIG_LIBVIRT_NVME_DISK_SIZE_GIB=50
+
+# Disable test workflows (rcloud provides the infrastructure)
+CONFIG_WORKFLOWS=n
+
+# Use Terraform to provision VMs through rcloud API
+# This treats rcloud as a local cloud provider, similar to AWS/Azure
+CONFIG_TERRAFORM=y
+CONFIG_TERRAFORM_RCLOUD=y
+CONFIG_TERRAFORM_RCLOUD_API_URL="http://localhost:8765"
+# Use nocloud variant to match the base image created by defconfig-rcloud
+# The nocloud variant doesn't have cloud-init and works better for local testing
+CONFIG_TERRAFORM_RCLOUD_BASE_IMAGE="debian-13-nocloud-amd64-daily.raw"
+
+# IMPORTANT: SSH key configuration for rcloud service
+# The rcloud service cannot access files in ~/.ssh/ due to directory permissions.
+# You MUST configure the SSH key path to a location accessible by the rcloud service.
+# Recommended: Store keys in your kdevops directory, for example:
+#   CONFIG_TERRAFORM_SSH_CONFIG_PUBKEY_FILE="/path/to/kdevops/kdevops_terraform.pub"
+# The private key path will automatically derive from the public key path (removes .pub suffix).
+# After loading this defconfig, run 'make menuconfig' and update the SSH paths under:
+#   "Bring up methods" -> "Terraform ssh configuration" -> "SSH public key file"
diff --git a/kconfigs/workflows/Kconfig b/kconfigs/workflows/Kconfig
index 5797521f..1b583094 100644
--- a/kconfigs/workflows/Kconfig
+++ b/kconfigs/workflows/Kconfig
@@ -614,3 +614,5 @@ config KDEVOPS_WORKFLOW_NAME
 endif
 
 endif # WORKFLOWS
+
+source "workflows/rcloud/Kconfig"
diff --git a/playbooks/install-rcloud-deps.yml b/playbooks/install-rcloud-deps.yml
new file mode 100644
index 00000000..ea10a601
--- /dev/null
+++ b/playbooks/install-rcloud-deps.yml
@@ -0,0 +1,5 @@
+---
+- name: Install rcloud build dependencies
+  hosts: localhost
+  roles:
+    - role: install-rcloud-deps
diff --git a/playbooks/rcloud.yml b/playbooks/rcloud.yml
new file mode 100644
index 00000000..57249a57
--- /dev/null
+++ b/playbooks/rcloud.yml
@@ -0,0 +1,107 @@
+---
+- name: Install and configure rcloud REST API server
+  hosts: localhost
+  become: yes
+  become_method: sudo
+  tasks:
+    - name: Ensure rcloud binary was built
+      stat:
+        path: "{{ playbook_dir }}/../workflows/rcloud/target/release/rcloud"
+      register: rcloud_binary
+      failed_when: not rcloud_binary.stat.exists
+
+    - name: Install rcloud binary
+      copy:
+        src: "{{ playbook_dir }}/../workflows/rcloud/target/release/rcloud"
+        dest: /usr/local/bin/rcloud
+        mode: '0755'
+        owner: root
+        group: root
+
+    - name: Create rcloud system user
+      user:
+        name: rcloud
+        system: yes
+        shell: /usr/sbin/nologin
+        home: /var/lib/rcloud
+        create_home: yes
+
+    - name: Create rcloud configuration directory
+      file:
+        path: /etc/rcloud
+        state: directory
+        mode: '0755'
+        owner: root
+        group: root
+
+    - name: Create systemd service file
+      copy:
+        dest: /etc/systemd/system/rcloud.service
+        mode: '0644'
+        owner: root
+        group: root
+        content: |
+          [Unit]
+          Description=rcloud REST API server for VM management
+          Documentation=https://github.com/linux-kdevops/kdevops
+          After=network.target libvirtd.service
+          Requires=libvirtd.service
+
+          [Service]
+          Type=simple
+          User=rcloud
+          Group=rcloud
+          WorkingDirectory={{ topdir_path }}
+          Environment="KDEVOPS_ROOT={{ topdir_path }}"
+          Environment="RUST_LOG=info"
+          Environment="RCLOUD_STORAGE_POOL_PATH={{ kdevops_storage_pool_path | default(libvirt_storage_pool_path) }}"
+          Environment="RCLOUD_BASE_IMAGES_DIR={{ guestfs_base_image_dir }}"
+          Environment="RCLOUD_LIBVIRT_URI={{ libvirt_uri | default('qemu:///system') }}"
+          Environment="RCLOUD_NETWORK_BRIDGE={{ libvirt_bridge_name | default('default') }}"
+          ExecStart=/usr/local/bin/rcloud
+          Restart=on-failure
+          RestartSec=5s
+
+          # Security hardening
+          NoNewPrivileges=true
+          PrivateTmp=true
+          ProtectSystem=strict
+          ProtectHome=true
+          ReadWritePaths=/var/lib/rcloud {{ kdevops_storage_pool_path | default(libvirt_storage_pool_path) }}
+          ReadOnlyPaths={{ topdir_path }} {{ kdevops_storage_pool_path | default(libvirt_storage_pool_path) }}/guestfs
+
+          [Install]
+          WantedBy=multi-user.target
+
+    - name: Add rcloud user to libvirt-qemu group
+      user:
+        name: rcloud
+        groups: "{{ libvirt_qemu_group | default('libvirt-qemu') }}"
+        append: yes
+
+    - name: Reload systemd daemon
+      systemd:
+        daemon_reload: yes
+
+    - name: Enable rcloud service
+      systemd:
+        name: rcloud
+        enabled: yes
+
+    - name: Extract port from bind address
+      set_fact:
+        rcloud_port: "{{ (rcloud_server_bind | default('127.0.0.1:8765')).split(':')[1] }}"
+
+    - name: Display service status instructions
+      debug:
+        msg:
+          - "rcloud service installed successfully"
+          - "Start the service with: sudo systemctl start rcloud"
+          - "Check status with: sudo systemctl status rcloud"
+          - "View logs with: sudo journalctl -u rcloud -f"
+          - ""
+          - "API will be available at: http://{{ rcloud_server_bind | default('127.0.0.1:8765') }}"
+          - ""
+          - "Test with:"
+          - "  curl http://localhost:{{ rcloud_port }}/api/v1/health"
+          - "  curl http://localhost:{{ rcloud_port }}/api/v1/status"
diff --git a/playbooks/roles/base_image/templates/virt-builder.j2 b/playbooks/roles/base_image/templates/virt-builder.j2
index c56eefae..3fff5ca6 100644
--- a/playbooks/roles/base_image/templates/virt-builder.j2
+++ b/playbooks/roles/base_image/templates/virt-builder.j2
@@ -34,12 +34,16 @@ sm-unregister
 {% if distro_debian_based is defined and distro_debian_based %}
 {# Ugh, debian has to be told to bring up the network and regenerate ssh keys #}
 {# Hope we get that interface name right! #}
-install isc-dhcp-client,ifupdown
+install isc-dhcp-client,ifupdown,openssh-server
 mkdir /etc/network/interfaces.d/
 append-line /etc/network/interfaces.d/enp1s0:auto enp1s0
 append-line /etc/network/interfaces.d/enp1s0:allow-hotplug enp1s0
 append-line /etc/network/interfaces.d/enp1s0:iface enp1s0 inet dhcp
-firstboot-command systemctl disable systemd-networkd-wait-online.service
+run-command systemctl disable systemd-networkd.service
+run-command systemctl disable systemd-networkd-wait-online.service
+run-command systemctl mask systemd-networkd.service
+run-command systemctl enable networking.service
+run-command systemctl enable ifup@enp1s0.service
 firstboot-command systemctl stop ssh
 firstboot-command DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true dpkg-reconfigure -p low --force openssh-server
 firstboot-command systemctl start ssh
diff --git a/playbooks/roles/gen_tfvars/templates/rcloud/terraform.tfvars.j2 b/playbooks/roles/gen_tfvars/templates/rcloud/terraform.tfvars.j2
new file mode 100644
index 00000000..c738603a
--- /dev/null
+++ b/playbooks/roles/gen_tfvars/templates/rcloud/terraform.tfvars.j2
@@ -0,0 +1,16 @@
+# rcloud Terraform provider configuration
+rcloud_api_url = "{{ terraform_rcloud_api_url }}"
+rcloud_base_image = "{{ terraform_rcloud_base_image }}"
+
+# SSH configuration
+ssh_config_pubkey_file = "{{ kdevops_terraform_ssh_config_pubkey_file }}"
+ssh_config_privkey_file = "{{ kdevops_terraform_ssh_config_privkey_file }}"
+ssh_config_user = "{{ kdevops_terraform_ssh_config_user }}"
+ssh_config = "{{ sshconfig }}"
+ssh_config_port = {{ ansible_cfg_ssh_port }}
+# Use unique SSH config file per directory to avoid conflicts
+ssh_config_name = "{{ kdevops_ssh_config_prefix }}{{ topdir_path_sha256sum[:8] }}"
+
+ssh_config_update = {{ kdevops_terraform_ssh_config_update | lower }}
+ssh_config_use_strict_settings = {{ kdevops_terraform_ssh_config_update_strict | lower }}
+ssh_config_backup = {{ kdevops_terraform_ssh_config_update_backup | lower }}
diff --git a/playbooks/roles/install-rcloud-deps/tasks/install-deps/debian/main.yml b/playbooks/roles/install-rcloud-deps/tasks/install-deps/debian/main.yml
new file mode 100644
index 00000000..3852ad92
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/install-deps/debian/main.yml
@@ -0,0 +1,10 @@
+---
+- name: Install rcloud-specific build dependencies
+  become: true
+  become_method: sudo
+  ansible.builtin.apt:
+    name:
+      - libvirt-dev
+    state: present
+    update_cache: false
+  tags: ["rcloud", "deps"]
diff --git a/playbooks/roles/install-rcloud-deps/tasks/install-deps/fedora/main.yml b/playbooks/roles/install-rcloud-deps/tasks/install-deps/fedora/main.yml
new file mode 100644
index 00000000..98e38200
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/install-deps/fedora/main.yml
@@ -0,0 +1,9 @@
+---
+- name: Install rcloud-specific build dependencies
+  become: true
+  become_method: sudo
+  ansible.builtin.dnf:
+    name:
+      - libvirt-devel
+    state: present
+  tags: ["rcloud", "deps"]
diff --git a/playbooks/roles/install-rcloud-deps/tasks/install-deps/main.yml b/playbooks/roles/install-rcloud-deps/tasks/install-deps/main.yml
new file mode 100644
index 00000000..23cda58e
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/install-deps/main.yml
@@ -0,0 +1,21 @@
+---
+- name: Import optional distribution specific variables
+  ansible.builtin.include_vars: "{{ item }}"
+  ignore_errors: true
+  with_first_found:
+    - files:
+        - "{{ ansible_facts['os_family'] | lower }}.yml"
+      skip: true
+  tags: vars
+
+- name: Distribution specific setup
+  ansible.builtin.import_tasks: debian/main.yml
+  when: ansible_facts['os_family']|lower == 'debian'
+- ansible.builtin.import_tasks: suse/main.yml
+  when: ansible_facts['os_family']|lower == 'suse'
+- ansible.builtin.import_tasks: redhat/main.yml
+  when:
+    - ansible_facts['os_family']|lower == 'redhat'
+    - ansible_facts['distribution']|lower != "fedora"
+- ansible.builtin.import_tasks: fedora/main.yml
+  when: ansible_facts['distribution']|lower == "fedora"
diff --git a/playbooks/roles/install-rcloud-deps/tasks/install-deps/redhat/main.yml b/playbooks/roles/install-rcloud-deps/tasks/install-deps/redhat/main.yml
new file mode 100644
index 00000000..98e38200
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/install-deps/redhat/main.yml
@@ -0,0 +1,9 @@
+---
+- name: Install rcloud-specific build dependencies
+  become: true
+  become_method: sudo
+  ansible.builtin.dnf:
+    name:
+      - libvirt-devel
+    state: present
+  tags: ["rcloud", "deps"]
diff --git a/playbooks/roles/install-rcloud-deps/tasks/install-deps/suse/main.yml b/playbooks/roles/install-rcloud-deps/tasks/install-deps/suse/main.yml
new file mode 100644
index 00000000..1b7803cd
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/install-deps/suse/main.yml
@@ -0,0 +1,9 @@
+---
+- name: Install rcloud-specific build dependencies
+  become: true
+  become_method: sudo
+  community.general.zypper:
+    name:
+      - libvirt-devel
+    state: present
+  tags: ["rcloud", "deps"]
diff --git a/playbooks/roles/install-rcloud-deps/tasks/main.yml b/playbooks/roles/install-rcloud-deps/tasks/main.yml
new file mode 100644
index 00000000..ba49080f
--- /dev/null
+++ b/playbooks/roles/install-rcloud-deps/tasks/main.yml
@@ -0,0 +1,25 @@
+---
+- name: Import optional extra_args file
+  ansible.builtin.include_vars: "{{ item }}"
+  ignore_errors: true
+  with_first_found:
+    - files:
+        - "../extra_vars.yml"
+        - "../extra_vars.yaml"
+        - "../extra_vars.json"
+      skip: true
+  tags: vars
+
+# Include generic Rust dependencies role
+- name: Install Rust build dependencies
+  ansible.builtin.include_role:
+    name: install-rust-deps
+
+# Include generic Go dependencies role
+- name: Install Go build dependencies
+  ansible.builtin.include_role:
+    name: install-go-deps
+
+# Install rcloud-specific dependencies (libvirt development libraries)
+- name: Install rcloud-specific build dependencies
+  ansible.builtin.include_tasks: install-deps/main.yml
diff --git a/scripts/generate_cloud_configs.py b/scripts/generate_cloud_configs.py
index ceb2eafc..4f833c40 100755
--- a/scripts/generate_cloud_configs.py
+++ b/scripts/generate_cloud_configs.py
@@ -134,7 +134,7 @@ def generate_aws_kconfig() -> bool:
         if result.returncode == 0:
             # Write the output to the corresponding Kconfig file
             try:
-                with open(output_path, 'w') as f:
+                with open(output_path, "w") as f:
                     f.write(result.stdout)
             except IOError as e:
                 print(f"Error writing {kconfig_file}: {e}", file=sys.stderr)
diff --git a/scripts/install-rcloud-deps.Makefile b/scripts/install-rcloud-deps.Makefile
new file mode 100644
index 00000000..94489cdf
--- /dev/null
+++ b/scripts/install-rcloud-deps.Makefile
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: copyleft-next-0.3.1
+
+rcloud-deps:
+	$(Q)ansible-playbook $(ANSIBLE_VERBOSE) \
+		$(KDEVOPS_PLAYBOOKS_DIR)/install-rcloud-deps.yml \
+		-e 'kdevops_first_run=True'
+PHONY += rcloud-deps
+
+ifeq (y,$(CONFIG_RCLOUD))
+ifeq (y,$(CONFIG_KDEVOPS_FIRST_RUN))
+LOCALHOST_SETUP_WORK += rcloud-deps
+endif # CONFIG_KDEVOPS_FIRST_RUN
+endif # CONFIG_RCLOUD
diff --git a/scripts/terraform.Makefile b/scripts/terraform.Makefile
index ef11e0e3..6199ef33 100644
--- a/scripts/terraform.Makefile
+++ b/scripts/terraform.Makefile
@@ -24,6 +24,9 @@ endif
 ifeq (y,$(CONFIG_TERRAFORM_LAMBDALABS))
 export KDEVOPS_CLOUD_PROVIDER=lambdalabs
 endif
+ifeq (y,$(CONFIG_TERRAFORM_RCLOUD))
+export KDEVOPS_CLOUD_PROVIDER=rcloud
+endif
 
 KDEVOPS_NODES_TEMPLATE :=	$(KDEVOPS_NODES_ROLE_TEMPLATE_DIR)/terraform_nodes.tf.j2
 KDEVOPS_NODES :=		terraform/$(KDEVOPS_CLOUD_PROVIDER)/nodes.tf
@@ -66,6 +69,11 @@ TERRAFORM_EXTRA_VARS += terraform_private_net_prefix=$(subst ",,$(CONFIG_TERRAFO
 TERRAFORM_EXTRA_VARS += terraform_private_net_mask=$(subst ",,$(CONFIG_TERRAFORM_PRIVATE_NET_MASK))
 endif
 
+ifeq (y,$(CONFIG_TERRAFORM_RCLOUD))
+TERRAFORM_EXTRA_VARS += terraform_rcloud_api_url=$(subst ",,$(CONFIG_TERRAFORM_RCLOUD_API_URL))
+TERRAFORM_EXTRA_VARS += terraform_rcloud_base_image=$(subst ",,$(CONFIG_TERRAFORM_RCLOUD_BASE_IMAGE))
+endif
+
 SSH_CONFIG_USER:=$(subst ",,$(CONFIG_TERRAFORM_SSH_CONFIG_USER))
 # XXX: add support to auto-infer in devconfig role as we did with the bootlinux
 # role. Then we can re-use the same infer_uid_and_group=True variable and
diff --git a/terraform-provider-rcloud/.gitignore b/terraform-provider-rcloud/.gitignore
new file mode 100644
index 00000000..3943e6d8
--- /dev/null
+++ b/terraform-provider-rcloud/.gitignore
@@ -0,0 +1,20 @@
+# Binaries
+terraform-provider-rcloud
+*.exe
+*.dll
+*.so
+*.dylib
+
+# Test binary
+*.test
+
+# Output of the go coverage tool
+*.out
+
+# Terraform
+*.tfstate
+*.tfstate.*
+.terraform/
+.terraform.lock.hcl
+terraform.tfvars
+crash.log
diff --git a/terraform-provider-rcloud/README.md b/terraform-provider-rcloud/README.md
new file mode 100644
index 00000000..c9a73260
--- /dev/null
+++ b/terraform-provider-rcloud/README.md
@@ -0,0 +1,101 @@
+# Terraform Provider for rcloud
+
+Terraform provider for managing VMs through the rcloud REST API.
+
+## Requirements
+
+- [Terraform](https://www.terraform.io/downloads.html) >= 1.0
+- [Go](https://golang.org/doc/install) >= 1.21
+- Running rcloud API server
+
+## Building the Provider
+
+```bash
+cd terraform-provider-rcloud
+go mod download
+go build -o terraform-provider-rcloud
+```
+
+## Installing the Provider for Local Development
+
+### Linux/macOS
+
+```bash
+mkdir -p ~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64
+cp terraform-provider-rcloud ~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64/
+```
+
+## Using the Provider
+
+```hcl
+terraform {
+  required_providers {
+    rcloud = {
+      source = "kdevops/rcloud"
+      version = "~> 0.1.0"
+    }
+  }
+}
+
+provider "rcloud" {
+  endpoint = "http://localhost:8765"
+}
+
+resource "rcloud_vm" "example" {
+  name         = "my-vm"
+  vcpus        = 4
+  memory_gb    = 8
+  base_image   = "debian-13-generic-amd64.raw"
+  root_disk_gb = 100
+}
+
+output "vm_id" {
+  value = rcloud_vm.example.id
+}
+```
+
+## Configuration
+
+### Provider Configuration
+
+- `endpoint` - (Optional) rcloud API endpoint URL. Defaults to `http://localhost:8765`. Can also be set via `RCLOUD_ENDPOINT` environment variable.
+- `token` - (Optional) API authentication token. Can be set via `RCLOUD_TOKEN` environment variable.
+
+### Resource: rcloud_vm
+
+Creates and manages a virtual machine.
+
+#### Arguments
+
+- `name` - (Required) VM name. Changing this forces a new resource.
+- `vcpus` - (Required) Number of virtual CPUs. Changing this forces a new resource.
+- `memory_gb` - (Required) Memory in GB. Changing this forces a new resource.
+- `base_image` - (Required) Base image filename from guestfs base images directory. Changing this forces a new resource.
+- `root_disk_gb` - (Required) Root disk size in GB. Changing this forces a new resource.
+
+#### Attributes
+
+- `id` - VM UUID assigned by libvirt.
+- `state` - Current VM state (e.g., "running", "stopped").
+
+## Development
+
+### Building
+
+```bash
+go build -o terraform-provider-rcloud
+```
+
+### Testing
+
+```bash
+# Run unit tests
+go test ./...
+
+# Run acceptance tests (requires running rcloud server)
+TF_ACC=1 go test ./... -v
+```
+
+## License
+
+copyleft-next-0.3.1
diff --git a/terraform-provider-rcloud/examples/main.tf b/terraform-provider-rcloud/examples/main.tf
new file mode 100644
index 00000000..83c02103
--- /dev/null
+++ b/terraform-provider-rcloud/examples/main.tf
@@ -0,0 +1,28 @@
+terraform {
+  required_providers {
+    rcloud = {
+      source = "kdevops/rcloud"
+    }
+  }
+}
+
+provider "rcloud" {
+  endpoint = "http://localhost:8765"
+  # token = "your-api-token" # Optional for MVP
+}
+
+resource "rcloud_vm" "example" {
+  name         = "terraform-vm-01"
+  vcpus        = 4
+  memory_gb    = 8
+  base_image   = "debian-13-generic-amd64.raw"
+  root_disk_gb = 100
+}
+
+output "vm_id" {
+  value = rcloud_vm.example.id
+}
+
+output "vm_state" {
+  value = rcloud_vm.example.state
+}
diff --git a/terraform-provider-rcloud/go.mod b/terraform-provider-rcloud/go.mod
new file mode 100644
index 00000000..74aba1f6
--- /dev/null
+++ b/terraform-provider-rcloud/go.mod
@@ -0,0 +1,32 @@
+module github.com/linux-kdevops/terraform-provider-rcloud
+
+go 1.21
+
+require (
+	github.com/hashicorp/terraform-plugin-framework v1.4.2
+	github.com/hashicorp/terraform-plugin-log v0.9.0
+)
+
+require (
+	github.com/fatih/color v1.13.0 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/hashicorp/go-hclog v1.5.0 // indirect
+	github.com/hashicorp/go-plugin v1.5.2 // indirect
+	github.com/hashicorp/go-uuid v1.0.3 // indirect
+	github.com/hashicorp/terraform-plugin-go v0.19.1 // indirect
+	github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
+	github.com/hashicorp/terraform-svchost v0.1.1 // indirect
+	github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
+	github.com/mattn/go-colorable v0.1.12 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
+	github.com/oklog/run v1.0.0 // indirect
+	github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
+	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+	golang.org/x/net v0.17.0 // indirect
+	golang.org/x/sys v0.13.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
+	google.golang.org/grpc v1.59.0 // indirect
+	google.golang.org/protobuf v1.31.0 // indirect
+)
diff --git a/terraform-provider-rcloud/go.sum b/terraform-provider-rcloud/go.sum
new file mode 100644
index 00000000..524d485f
--- /dev/null
+++ b/terraform-provider-rcloud/go.sum
@@ -0,0 +1,75 @@
+github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
+github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
+github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-plugin v1.5.2 h1:aWv8eimFqWlsEiMrYZdPYl+FdHaBJSN4AWwGWfT1G2Y=
+github.com/hashicorp/go-plugin v1.5.2/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI=
+github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY=
+github.com/hashicorp/terraform-plugin-go v0.19.1 h1:lf/jTGTeELcz5IIbn/94mJdmnTjRYm6S6ct/JqCSr50=
+github.com/hashicorp/terraform-plugin-go v0.19.1/go.mod h1:5NMIS+DXkfacX6o5HCpswda5yjkSYfKzn1Nfl9l+qRs=
+github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
+github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
+github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
+github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
+github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
+github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
+github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
+github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
+github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
+github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
+github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/terraform-provider-rcloud/internal/provider/client.go b/terraform-provider-rcloud/internal/provider/client.go
new file mode 100644
index 00000000..91a0b15b
--- /dev/null
+++ b/terraform-provider-rcloud/internal/provider/client.go
@@ -0,0 +1,207 @@
+package provider
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"time"
+)
+
+// APIClient is the HTTP client for rcloud API
+type APIClient struct {
+	Endpoint          string
+	Token             string
+	SSHUser           string
+	SSHPublicKeyFile  string
+	HTTPClient        *http.Client
+}
+
+// VM represents a virtual machine
+type VM struct {
+	ID        string `json:"id"`
+	Name      string `json:"name"`
+	State     string `json:"state"`
+	VCPUs     int64  `json:"vcpus"`
+	MemoryMB  int64  `json:"memory_mb"`
+	IPAddress string `json:"ip_address,omitempty"`
+}
+
+// CreateVMRequest is the request to create a VM
+type CreateVMRequest struct {
+	Name          string  `json:"name"`
+	VCPUs         int64   `json:"vcpus"`
+	MemoryMB      int64   `json:"memory_mb"`
+	BaseImage     string  `json:"base_image"`
+	RootDiskGB    int64   `json:"root_disk_gb"`
+	SSHUser       *string `json:"ssh_user,omitempty"`
+	SSHPublicKey  *string `json:"ssh_public_key,omitempty"`
+}
+
+// CreateVMResponse is the response from creating a VM
+type CreateVMResponse struct {
+	ID    string `json:"id"`
+	Name  string `json:"name"`
+	State string `json:"state"`
+}
+
+// NewHTTPClient creates a new HTTP client with timeout
+func (c *APIClient) newHTTPClient() *http.Client {
+	if c.HTTPClient != nil {
+		return c.HTTPClient
+	}
+	return &http.Client{
+		Timeout: 30 * time.Second,
+	}
+}
+
+// CreateVM creates a new VM
+func (c *APIClient) CreateVM(req *CreateVMRequest) (*CreateVMResponse, error) {
+	body, err := json.Marshal(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request: %w", err)
+	}
+
+	httpReq, err := http.NewRequest("POST", c.Endpoint+"/api/v1/vms", bytes.NewBuffer(body))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	httpReq.Header.Set("Content-Type", "application/json")
+	if c.Token != "" {
+		httpReq.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+
+	client := c.newHTTPClient()
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return nil, fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusCreated {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	var createResp CreateVMResponse
+	if err := json.NewDecoder(resp.Body).Decode(&createResp); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return &createResp, nil
+}
+
+// GetVM retrieves a VM by ID
+func (c *APIClient) GetVM(id string) (*VM, error) {
+	httpReq, err := http.NewRequest("GET", c.Endpoint+"/api/v1/vms/"+id, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	if c.Token != "" {
+		httpReq.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+
+	client := c.newHTTPClient()
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return nil, fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == http.StatusNotFound {
+		return nil, fmt.Errorf("VM not found")
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	var vm VM
+	if err := json.NewDecoder(resp.Body).Decode(&vm); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return &vm, nil
+}
+
+// DeleteVM deletes a VM
+func (c *APIClient) DeleteVM(id string) error {
+	httpReq, err := http.NewRequest("DELETE", c.Endpoint+"/api/v1/vms/"+id, nil)
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	if c.Token != "" {
+		httpReq.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+
+	client := c.newHTTPClient()
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	return nil
+}
+
+// StartVM starts a VM
+func (c *APIClient) StartVM(id string) error {
+	httpReq, err := http.NewRequest("POST", c.Endpoint+"/api/v1/vms/"+id+"/start", nil)
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	if c.Token != "" {
+		httpReq.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+
+	client := c.newHTTPClient()
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	return nil
+}
+
+// StopVM stops a VM
+func (c *APIClient) StopVM(id string) error {
+	httpReq, err := http.NewRequest("POST", c.Endpoint+"/api/v1/vms/"+id+"/stop", nil)
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	if c.Token != "" {
+		httpReq.Header.Set("Authorization", "Bearer "+c.Token)
+	}
+
+	client := c.newHTTPClient()
+	resp, err := client.Do(httpReq)
+	if err != nil {
+		return fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		bodyBytes, _ := io.ReadAll(resp.Body)
+		return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(bodyBytes))
+	}
+
+	return nil
+}
diff --git a/terraform-provider-rcloud/internal/provider/provider.go b/terraform-provider-rcloud/internal/provider/provider.go
new file mode 100644
index 00000000..8a6d9ab2
--- /dev/null
+++ b/terraform-provider-rcloud/internal/provider/provider.go
@@ -0,0 +1,129 @@
+package provider
+
+import (
+	"context"
+	"os"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/provider"
+	"github.com/hashicorp/terraform-plugin-framework/provider/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Ensure RcloudProvider satisfies various provider interfaces.
+var _ provider.Provider = &RcloudProvider{}
+
+// RcloudProvider defines the provider implementation.
+type RcloudProvider struct {
+	// version is set to the provider version on release, "dev" when the
+	// provider is built and ran locally, and "test" when running acceptance
+	// testing.
+	version string
+}
+
+// RcloudProviderModel describes the provider data model.
+type RcloudProviderModel struct {
+	Endpoint          types.String `tfsdk:"endpoint"`
+	Token             types.String `tfsdk:"token"`
+	SSHUser           types.String `tfsdk:"ssh_user"`
+	SSHPublicKeyFile  types.String `tfsdk:"ssh_public_key_file"`
+}
+
+func (p *RcloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
+	resp.TypeName = "rcloud"
+	resp.Version = p.version
+}
+
+func (p *RcloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"endpoint": schema.StringAttribute{
+				MarkdownDescription: "rcloud API endpoint URL",
+				Optional:            true,
+			},
+			"token": schema.StringAttribute{
+				MarkdownDescription: "API token for authentication (optional for MVP)",
+				Optional:            true,
+				Sensitive:           true,
+			},
+			"ssh_user": schema.StringAttribute{
+				MarkdownDescription: "Default SSH username to create in VMs (can be overridden per VM)",
+				Optional:            true,
+			},
+			"ssh_public_key_file": schema.StringAttribute{
+				MarkdownDescription: "Path to SSH public key file to inject into VMs (can be overridden per VM)",
+				Optional:            true,
+			},
+		},
+	}
+}
+
+func (p *RcloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
+	var data RcloudProviderModel
+
+	resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Configuration values are now available.
+	// if data.Endpoint.IsNull() { /* ... */ }
+
+	// Default to environment variable or localhost
+	endpoint := os.Getenv("RCLOUD_ENDPOINT")
+	if !data.Endpoint.IsNull() {
+		endpoint = data.Endpoint.ValueString()
+	}
+	if endpoint == "" {
+		endpoint = "http://localhost:8765"
+	}
+
+	token := os.Getenv("RCLOUD_TOKEN")
+	if !data.Token.IsNull() {
+		token = data.Token.ValueString()
+	}
+
+	// SSH configuration
+	sshUser := ""
+	if !data.SSHUser.IsNull() {
+		sshUser = data.SSHUser.ValueString()
+	}
+
+	sshPublicKeyFile := ""
+	if !data.SSHPublicKeyFile.IsNull() {
+		sshPublicKeyFile = data.SSHPublicKeyFile.ValueString()
+	}
+
+	// Create API client
+	client := &APIClient{
+		Endpoint:         endpoint,
+		Token:            token,
+		SSHUser:          sshUser,
+		SSHPublicKeyFile: sshPublicKeyFile,
+	}
+
+	resp.DataSourceData = client
+	resp.ResourceData = client
+}
+
+func (p *RcloudProvider) Resources(ctx context.Context) []func() resource.Resource {
+	return []func() resource.Resource{
+		NewVMResource,
+	}
+}
+
+func (p *RcloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
+	return []func() datasource.DataSource{
+		// NewImageDataSource,
+	}
+}
+
+func New(version string) func() provider.Provider {
+	return func() provider.Provider {
+		return &RcloudProvider{
+			version: version,
+		}
+	}
+}
diff --git a/terraform-provider-rcloud/internal/provider/resource_vm.go b/terraform-provider-rcloud/internal/provider/resource_vm.go
new file mode 100644
index 00000000..2249a504
--- /dev/null
+++ b/terraform-provider-rcloud/internal/provider/resource_vm.go
@@ -0,0 +1,344 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &VMResource{}
+var _ resource.ResourceWithImportState = &VMResource{}
+
+func NewVMResource() resource.Resource {
+	return &VMResource{}
+}
+
+// VMResource defines the resource implementation.
+type VMResource struct {
+	client *APIClient
+}
+
+// VMResourceModel describes the resource data model.
+type VMResourceModel struct {
+	ID               types.String `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	VCPUs            types.Int64  `tfsdk:"vcpus"`
+	MemoryGB         types.Int64  `tfsdk:"memory_gb"`
+	BaseImage        types.String `tfsdk:"base_image"`
+	RootDiskGB       types.Int64  `tfsdk:"root_disk_gb"`
+	SSHUser          types.String `tfsdk:"ssh_user"`
+	SSHPublicKeyFile types.String `tfsdk:"ssh_public_key_file"`
+	State            types.String `tfsdk:"state"`
+	IPAddress        types.String `tfsdk:"ip_address"`
+}
+
+func (r *VMResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_vm"
+}
+
+func (r *VMResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		// This description is used by the documentation generator and the language server.
+		MarkdownDescription: "rcloud VM resource",
+
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Computed:            true,
+				MarkdownDescription: "VM identifier (UUID)",
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"name": schema.StringAttribute{
+				MarkdownDescription: "VM name",
+				Required:            true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"vcpus": schema.Int64Attribute{
+				MarkdownDescription: "Number of virtual CPUs",
+				Required:            true,
+				PlanModifiers: []planmodifier.Int64{
+					int64planmodifier.RequiresReplace(),
+				},
+			},
+			"memory_gb": schema.Int64Attribute{
+				MarkdownDescription: "Memory in GB",
+				Required:            true,
+				PlanModifiers: []planmodifier.Int64{
+					int64planmodifier.RequiresReplace(),
+				},
+			},
+			"base_image": schema.StringAttribute{
+				MarkdownDescription: "Base image filename from guestfs",
+				Required:            true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"root_disk_gb": schema.Int64Attribute{
+				MarkdownDescription: "Root disk size in GB",
+				Required:            true,
+				PlanModifiers: []planmodifier.Int64{
+					int64planmodifier.RequiresReplace(),
+				},
+			},
+			"ssh_user": schema.StringAttribute{
+				MarkdownDescription: "SSH username to create (defaults to provider configuration)",
+				Optional:            true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"ssh_public_key_file": schema.StringAttribute{
+				MarkdownDescription: "Path to SSH public key file (defaults to provider configuration)",
+				Optional:            true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"state": schema.StringAttribute{
+				Computed:            true,
+				MarkdownDescription: "VM state",
+			},
+			"ip_address": schema.StringAttribute{
+				Computed:            true,
+				MarkdownDescription: "VM IP address (available when VM is running with qemu-guest-agent)",
+			},
+		},
+	}
+}
+
+func (r *VMResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	// Prevent panic if the provider has not been configured.
+	if req.ProviderData == nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*APIClient)
+
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Resource Configure Type",
+			fmt.Sprintf("Expected *APIClient, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	r.client = client
+}
+
+func (r *VMResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var data VMResourceModel
+
+	// Read Terraform plan data into the model
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Determine SSH user: resource > provider > none
+	var sshUser *string
+	if !data.SSHUser.IsNull() {
+		val := data.SSHUser.ValueString()
+		sshUser = &val
+	} else if r.client.SSHUser != "" {
+		sshUser = &r.client.SSHUser
+	}
+
+	// Determine SSH public key file: resource > provider > none
+	sshPublicKeyFile := ""
+	if !data.SSHPublicKeyFile.IsNull() {
+		sshPublicKeyFile = data.SSHPublicKeyFile.ValueString()
+	} else if r.client.SSHPublicKeyFile != "" {
+		sshPublicKeyFile = r.client.SSHPublicKeyFile
+	}
+
+	// Read SSH public key content if file path is provided
+	var sshPublicKey *string
+	if sshPublicKeyFile != "" {
+		keyContent, err := os.ReadFile(sshPublicKeyFile)
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"SSH Key Read Error",
+				fmt.Sprintf("Unable to read SSH public key from %s: %s", sshPublicKeyFile, err),
+			)
+			return
+		}
+		keyStr := string(keyContent)
+		sshPublicKey = &keyStr
+		tflog.Debug(ctx, "Read SSH public key", map[string]any{"file": sshPublicKeyFile, "length": len(keyStr)})
+	}
+
+	// Create API request
+	createReq := &CreateVMRequest{
+		Name:         data.Name.ValueString(),
+		VCPUs:        data.VCPUs.ValueInt64(),
+		MemoryMB:     data.MemoryGB.ValueInt64() * 1024, // Convert GB to MB
+		BaseImage:    data.BaseImage.ValueString(),
+		RootDiskGB:   data.RootDiskGB.ValueInt64(),
+		SSHUser:      sshUser,
+		SSHPublicKey: sshPublicKey,
+	}
+
+	tflog.Info(ctx, "Creating VM", map[string]any{"name": createReq.Name, "ssh_user": sshUser != nil, "ssh_key": sshPublicKey != nil})
+
+	// Call API
+	createResp, err := r.client.CreateVM(createReq)
+	if err != nil {
+		resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create VM, got error: %s", err))
+		return
+	}
+
+	// Set the ID from create response
+	data.ID = types.StringValue(createResp.ID)
+
+	tflog.Trace(ctx, "Created VM", map[string]any{"id": createResp.ID})
+
+	// Wait for VM to boot and get IP address (with timeout)
+	// VMs need time to boot, start networking, and acquire DHCP lease
+	// We MUST wait for IP address before returning - bringup process depends on it
+	maxRetries := 300  // 5 minutes total (300 seconds)
+	retryDelay := 1 * time.Second
+
+	var vm *VM
+	var ipAcquired bool
+	for i := 0; i < maxRetries; i++ {
+		vm, err = r.client.GetVM(data.ID.ValueString())
+		if err != nil {
+			resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read VM after creation, got error: %s", err))
+			return
+		}
+
+		// If we have an IP address, we're done
+		if vm.IPAddress != "" {
+			tflog.Info(ctx, "VM acquired IP address", map[string]any{"ip": vm.IPAddress, "attempts": i + 1, "elapsed_seconds": i + 1})
+			ipAcquired = true
+			break
+		}
+
+		// Log progress every 30 seconds
+		if i > 0 && i % 30 == 0 {
+			tflog.Info(ctx, "Still waiting for VM IP address", map[string]any{"elapsed_seconds": i, "max_seconds": maxRetries})
+		}
+
+		// Wait before retrying
+		if i < maxRetries - 1 {
+			tflog.Debug(ctx, "Waiting for VM IP address", map[string]any{"attempt": i + 1, "max": maxRetries})
+			time.Sleep(retryDelay)
+		}
+	}
+
+	// CRITICAL: VM creation must not succeed without an IP address
+	// The bringup process needs the IP address to configure SSH access
+	if !ipAcquired {
+		resp.Diagnostics.AddError(
+			"VM Boot Timeout",
+			fmt.Sprintf("VM %s was created and started but did not acquire an IP address within %d seconds. "+
+				"This typically indicates a networking issue with the base image or libvirt network configuration. "+
+				"Check that the VM has networking configured and that the libvirt default network is active.",
+				data.Name.ValueString(), maxRetries),
+		)
+		// Attempt to clean up the VM since it's not usable without an IP
+		tflog.Warn(ctx, "Attempting to delete VM due to timeout", map[string]any{"id": data.ID.ValueString()})
+		if cleanupErr := r.client.DeleteVM(data.ID.ValueString()); cleanupErr != nil {
+			tflog.Error(ctx, "Failed to clean up VM after timeout", map[string]any{"error": cleanupErr.Error()})
+		}
+		return
+	}
+
+	// Populate all computed fields from the final API response
+	data.State = types.StringValue(vm.State)
+	data.IPAddress = types.StringValue(vm.IPAddress)
+
+	// Save data into Terraform state
+	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *VMResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var data VMResourceModel
+
+	// Read Terraform prior state data into the model
+	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Get VM from API
+	vm, err := r.client.GetVM(data.ID.ValueString())
+	if err != nil {
+		resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read VM, got error: %s", err))
+		return
+	}
+
+	// Update state
+	data.State = types.StringValue(vm.State)
+
+	// Update IP address if available
+	if vm.IPAddress != "" {
+		data.IPAddress = types.StringValue(vm.IPAddress)
+	} else {
+		data.IPAddress = types.StringNull()
+	}
+
+	// Save updated data into Terraform state
+	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *VMResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var data VMResourceModel
+
+	// Read Terraform plan data into the model
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Most VM attributes require replacement, so update is a no-op
+	// In the future, could support live updates like memory/vcpu hotplug
+
+	// Save updated data into Terraform state
+	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *VMResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var data VMResourceModel
+
+	// Read Terraform prior state data into the model
+	resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	tflog.Info(ctx, "Deleting VM", map[string]any{"id": data.ID.ValueString()})
+
+	// Delete VM via API
+	err := r.client.DeleteVM(data.ID.ValueString())
+	if err != nil {
+		resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete VM, got error: %s", err))
+		return
+	}
+
+	tflog.Trace(ctx, "Deleted VM", map[string]any{"id": data.ID.ValueString()})
+}
+
+func (r *VMResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
diff --git a/terraform-provider-rcloud/main.go b/terraform-provider-rcloud/main.go
new file mode 100644
index 00000000..57826bcd
--- /dev/null
+++ b/terraform-provider-rcloud/main.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"log"
+
+	"github.com/hashicorp/terraform-plugin-framework/providerserver"
+	"github.com/linux-kdevops/terraform-provider-rcloud/internal/provider"
+)
+
+// Run "go generate" to format example terraform files and generate the docs for the registry/website
+
+// If you do not have terraform installed, you can remove the formatting command, but its suggested to
+// ensure the documentation is formatted properly.
+//go:generate terraform fmt -recursive ./examples/
+
+// Run the docs generation tool, check its repository for more information on how it works and how docs
+// can be customized.
+//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
+
+var (
+	// these will be set by the goreleaser configuration
+	// to appropriate values for the compiled binary.
+	version string = "dev"
+
+	// goreleaser can pass other information to the main package, such as the specific commit
+	// https://goreleaser.com/cookbooks/using-main.version/
+)
+
+func main() {
+	var debug bool
+
+	flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve")
+	flag.Parse()
+
+	opts := providerserver.ServeOpts{
+		Address: "registry.terraform.io/kdevops/rcloud",
+		Debug:   debug,
+	}
+
+	err := providerserver.Serve(context.Background(), provider.New(version), opts)
+
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+}
diff --git a/terraform/Kconfig.providers b/terraform/Kconfig.providers
index 944abb99..652c8930 100644
--- a/terraform/Kconfig.providers
+++ b/terraform/Kconfig.providers
@@ -45,6 +45,17 @@ config TERRAFORM_LAMBDALABS
 	  solution. Lambda Labs provides GPU-accelerated instances optimized
 	  for machine learning and high-performance computing workloads.
 
+config TERRAFORM_RCLOUD
+	bool "rcloud - Local Private Cloud"
+	help
+	  Enabling this means you are going to use rcloud as your cloud
+	  solution. rcloud is a local private cloud that provides a REST API
+	  for VM management through libvirt, similar to public cloud providers.
+
+	  This requires a running rcloud server (see defconfig-rcloud).
+	  Users can then provision VMs through the rcloud API using the
+	  standard Terraform workflow, treating rcloud as a local cloud provider.
+
 endchoice
 
 source "terraform/gce/Kconfig"
@@ -53,3 +64,4 @@ source "terraform/azure/Kconfig"
 source "terraform/oci/Kconfig"
 source "terraform/openstack/Kconfig"
 source "terraform/lambdalabs/Kconfig"
+source "terraform/rcloud/Kconfig"
diff --git a/terraform/rcloud/Kconfig b/terraform/rcloud/Kconfig
new file mode 100644
index 00000000..3d3c8517
--- /dev/null
+++ b/terraform/rcloud/Kconfig
@@ -0,0 +1,25 @@
+if TERRAFORM_RCLOUD
+
+config TERRAFORM_RCLOUD_API_URL
+	string "rcloud API server URL"
+	default "http://localhost:8765"
+	help
+	  The URL of the rcloud REST API server. This should point to a running
+	  rcloud server instance that was set up using defconfig-rcloud.
+
+	  Default is http://localhost:8765 for local development and testing.
+
+	  For remote rcloud servers, use: http://server-hostname:8765
+
+config TERRAFORM_RCLOUD_BASE_IMAGE
+	string "Base image name to use for VMs"
+	default "debian-13-generic-amd64-daily"
+	help
+	  The name of the base image to use when creating VMs through rcloud.
+	  This must match a base image that exists in the rcloud server's
+	  base image directory (typically /xfs1/libvirt/guestfs/base_images/).
+
+	  The base images are created on the rcloud server using guestfs
+	  (see defconfig-rcloud).
+
+endif # TERRAFORM_RCLOUD
diff --git a/terraform/rcloud/main.tf b/terraform/rcloud/main.tf
new file mode 100644
index 00000000..8027c093
--- /dev/null
+++ b/terraform/rcloud/main.tf
@@ -0,0 +1,8 @@
+resource "rcloud_vm" "kdevops_vm" {
+  count        = local.kdevops_num_boxes
+  name         = element(var.kdevops_nodes, count.index)
+  vcpus        = 2
+  memory_gb    = 4
+  base_image   = var.rcloud_base_image
+  root_disk_gb = 50
+}
diff --git a/terraform/rcloud/output.tf b/terraform/rcloud/output.tf
new file mode 100644
index 00000000..d6f40f38
--- /dev/null
+++ b/terraform/rcloud/output.tf
@@ -0,0 +1,10 @@
+# All generic output goes here
+
+# Each provider's output.tf needs to define a controller_ip_map. This
+# map is used to build the Ansible controller's ssh configuration.
+# Each map entry contains the node's hostname and public/private IP
+# address.
+output "controller_ip_map" {
+  description = "The IP addresses assigned to each VM instance"
+  value       = zipmap(var.kdevops_nodes[*], rcloud_vm.kdevops_vm[*].ip_address)
+}
diff --git a/terraform/rcloud/provider.tf b/terraform/rcloud/provider.tf
new file mode 100644
index 00000000..fbcd1bed
--- /dev/null
+++ b/terraform/rcloud/provider.tf
@@ -0,0 +1,14 @@
+terraform {
+  required_providers {
+    rcloud = {
+      source  = "local/rcloud/rcloud"
+      version = "~> 1.0"
+    }
+  }
+}
+
+provider "rcloud" {
+  endpoint             = var.rcloud_api_url
+  ssh_user             = var.ssh_config_user
+  ssh_public_key_file  = var.ssh_config_pubkey_file
+}
diff --git a/terraform/rcloud/shared.tf b/terraform/rcloud/shared.tf
new file mode 100644
index 00000000..b663551b
--- /dev/null
+++ b/terraform/rcloud/shared.tf
@@ -0,0 +1,60 @@
+# Generic import of data.
+#
+# Terraform will process all *.tf files in alphabetical order, but the
+# order does not matter as terraform is declarative.
+
+variable "ssh_config" {
+  description = "Path to SSH config update script"
+  default     = "../scripts/update_ssh_config_lambdalabs.py"
+}
+
+variable "ssh_config_update" {
+  description = "Set this to true if you want terraform to update your ssh_config with the provisioned set of hosts"
+  type        = bool
+}
+
+variable "ssh_config_user" {
+  description = "If ssh_config_update is true, and this is set, it will be the user set for each host on your ssh config"
+  default     = "ubuntu"
+}
+
+variable "ssh_config_pubkey_file" {
+  description = "Path to the ssh public key file"
+  default     = "~/.ssh/kdevops_terraform.pub"
+}
+
+variable "ssh_config_privkey_file" {
+  description = "Path to the ssh private key file for authentication"
+  default     = "~/.ssh/kdevops_terraform"
+}
+
+variable "ssh_config_use_strict_settings" {
+  description = "Whether or not to use strict settings on ssh_config"
+  type        = bool
+}
+
+variable "ssh_config_backup" {
+  description = "Set this to true if you want to backup your ssh_config per update"
+  type        = bool
+}
+
+variable "ssh_config_kexalgorithms" {
+  description = "If set, this sets a custom ssh kexalgorithms"
+  default     = ""
+}
+
+variable "ssh_config_port" {
+  description = "SSH port to use for remote connections"
+  type        = number
+  default     = 22
+}
+
+variable "ssh_config_name" {
+  description = "SSH config file to update (allows per-directory configs)"
+  type        = string
+  default     = "~/.ssh/config"
+}
+
+locals {
+  kdevops_num_boxes = length(var.kdevops_nodes)
+}
diff --git a/terraform/rcloud/vars.tf b/terraform/rcloud/vars.tf
new file mode 100644
index 00000000..18ac2f02
--- /dev/null
+++ b/terraform/rcloud/vars.tf
@@ -0,0 +1,9 @@
+variable "rcloud_api_url" {
+  description = "rcloud REST API server URL"
+  type        = string
+}
+
+variable "rcloud_base_image" {
+  description = "Base image name to use for VMs (must exist in rcloud server's base images directory)"
+  type        = string
+}
diff --git a/workflows/rcloud/.gitignore b/workflows/rcloud/.gitignore
new file mode 100644
index 00000000..2f7896d1
--- /dev/null
+++ b/workflows/rcloud/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/workflows/rcloud/Cargo.lock b/workflows/rcloud/Cargo.lock
new file mode 100644
index 00000000..a4f641b3
--- /dev/null
+++ b/workflows/rcloud/Cargo.lock
@@ -0,0 +1,3550 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "actix-codec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "actix-http"
+version = "3.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "base64 0.22.1",
+ "bitflags",
+ "brotli",
+ "bytes",
+ "bytestring",
+ "derive_more",
+ "encoding_rs",
+ "flate2",
+ "foldhash",
+ "futures-core",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "language-tags",
+ "local-channel",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "sha1",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8"
+dependencies = [
+ "bytestring",
+ "cfg-if",
+ "http 0.2.12",
+ "regex",
+ "regex-lite",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "actix-rt"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
+dependencies = [
+ "actix-macros",
+ "futures-core",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "futures-util",
+ "mio",
+ "socket2 0.5.10",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "actix-service"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-utils"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
+dependencies = [
+ "local-waker",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-web"
+version = "4.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-utils",
+ "actix-web-codegen",
+ "bytes",
+ "bytestring",
+ "cfg-if",
+ "cookie",
+ "derive_more",
+ "encoding_rs",
+ "foldhash",
+ "futures-core",
+ "futures-util",
+ "impl-more",
+ "itoa",
+ "language-tags",
+ "log",
+ "mime",
+ "once_cell",
+ "pin-project-lite",
+ "regex",
+ "regex-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smallvec",
+ "socket2 0.5.10",
+ "time",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
+dependencies = [
+ "actix-router",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "backtrace"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "bytestring"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+[[package]]
+name = "chrono"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
+dependencies = [
+ "chrono",
+ "chrono-tz-build",
+ "phf",
+]
+
+[[package]]
+name = "chrono-tz-build"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
+dependencies = [
+ "parse-zoneinfo",
+ "phf",
+ "phf_codegen",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "config"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
+dependencies = [
+ "async-trait",
+ "convert_case",
+ "json5",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cookie"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
+]
+
+[[package]]
+name = "deunicode"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
+
+[[package]]
+name = "flate2"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.7+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.32.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
+
+[[package]]
+name = "globset"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
+dependencies = [
+ "bitflags",
+ "ignore",
+ "walkdir",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.3.1",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.3.1",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.3.1",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "hyper"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http 1.3.1",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http 1.3.1",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.0",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "impl-more"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+
+[[package]]
+name = "indexmap"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.0",
+]
+
+[[package]]
+name = "io-uring"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.176"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "local-channel"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "local-waker",
+]
+
+[[package]]
+name = "local-waker"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "mutually_exclusive_features"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577"
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
+dependencies = [
+ "regex",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pest"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a"
+dependencies = [
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prometheus-client"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca"
+dependencies = [
+ "dtoa",
+ "itoa",
+ "parking_lot",
+ "prometheus-client-derive-encode",
+]
+
+[[package]]
+name = "prometheus-client-derive-encode"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "rcloud"
+version = "0.1.0"
+dependencies = [
+ "actix-rt",
+ "actix-web",
+ "anyhow",
+ "chrono",
+ "clap",
+ "config",
+ "prometheus-client",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "tempfile",
+ "tera",
+ "thiserror",
+ "tokio",
+ "tokio-test",
+ "tracing",
+ "tracing-actix-web",
+ "tracing-subscriber",
+ "uuid",
+ "virt",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+
+[[package]]
+name = "reqwest"
+version = "0.12.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2 0.4.12",
+ "http 1.3.1",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+dependencies = [
+ "windows-sys 0.61.1",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.227"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.227"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.227"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "slug"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
+dependencies = [
+ "deunicode",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "tera"
+version = "1.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand 0.8.5",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.47.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "io-uring",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "socket2 0.6.0",
+ "tokio-macros",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-test"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
+dependencies = [
+ "async-stream",
+ "bytes",
+ "futures-core",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http 1.3.1",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-actix-web"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5360edd490ec8dee9fedfc6a9fd83ac2f01b3e1996e3261b9ad18a61971fe064"
+dependencies = [
+ "actix-web",
+ "mutually_exclusive_features",
+ "pin-project",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
+dependencies = [
+ "getrandom 0.3.3",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "virt"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5408c59dc1b3383e0da017a85a6799dd6e3d52790849ececadbf403513743fa6"
+dependencies = [
+ "libc",
+ "uuid",
+ "virt-sys",
+]
+
+[[package]]
+name = "virt-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7d9a603af8e27b33f1c8d721cb5caafcf77810c79e6ea02d2436906a14683fd"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasi"
+version = "0.14.7+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
+dependencies = [
+ "wasip2",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.0",
+ "windows-result 0.4.0",
+ "windows-strings 0.5.0",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.4",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f"
+dependencies = [
+ "windows-link 0.2.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b"
+dependencies = [
+ "windows-link 0.2.0",
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
+name = "winnow"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "yaml-rust2"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/workflows/rcloud/Cargo.toml b/workflows/rcloud/Cargo.toml
new file mode 100644
index 00000000..d02c6089
--- /dev/null
+++ b/workflows/rcloud/Cargo.toml
@@ -0,0 +1,69 @@
+[package]
+name = "rcloud"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.70"
+authors = ["kdevops contributors"]
+description = "Private cloud infrastructure for kdevops - Ubicloud alternative in Rust"
+license = "copyleft-next-0.3.1"
+repository = "https://github.com/linux-kdevops/kdevops"
+
+[[bin]]
+name = "rcloud"
+path = "src/main.rs"
+
+[dependencies]
+# Web framework
+actix-web = "4"
+actix-rt = "2"
+
+# Async runtime
+tokio = { version = "1", features = ["full"] }
+
+# Serialization
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde_yaml = "0.9"
+
+# libvirt bindings
+virt = "0.3"
+
+# Templating (for XML generation from guestfs templates)
+tera = "1"
+
+# Configuration management
+config = "0.14"
+
+# Logging and tracing
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
+tracing-actix-web = "0.7"
+
+# Metrics
+prometheus-client = "0.22"
+
+# Error handling
+anyhow = "1"
+thiserror = "1"
+
+# UUID generation
+uuid = { version = "1", features = ["v4", "serde"] }
+
+# Date/time
+chrono = { version = "0.4", features = ["serde"] }
+
+# CLI (for future CLI tool)
+clap = { version = "4", features = ["derive"], optional = true }
+
+[dev-dependencies]
+# Testing
+tokio-test = "0.4"
+reqwest = { version = "0.12", features = ["json"] }
+tempfile = "3"
+
+[features]
+default = []
+cli = ["clap"]
+
+[build-dependencies]
+# For build-time checks
diff --git a/workflows/rcloud/DESIGN.md b/workflows/rcloud/DESIGN.md
new file mode 100644
index 00000000..55c6bc0a
--- /dev/null
+++ b/workflows/rcloud/DESIGN.md
@@ -0,0 +1,444 @@
+# rcloud Design Document
+
+## Vision
+
+rcloud is a **private cloud infrastructure** built in Rust, designed to turn bare metal servers into a cloud platform comparable to Ubicloud/AWS/GCE. It provides a REST API and Terraform provider for declarative VM management.
+
+## Goals
+
+1. **Ubicloud Alternative**: Provide a self-hosted cloud platform for Linux kernel development and testing
+2. **Terraform Integration**: First-class Terraform provider for Infrastructure as Code
+3. **Multi-Host Scaling**: Start single-host, scale to multiple physical servers
+4. **Leverage kdevops**: Integrate with existing guestfs infrastructure and workflows
+5. **Developer-Friendly**: Simple API, clear documentation, easy to extend
+
+## Architecture
+
+### Single-Host MVP (Phase 1)
+
+```
+┌─────────────────────────────────────────────┐
+│ Clients                                      │
+│                                              │
+│  Terraform          curl/httpie       CLI   │
+│  (terraform apply)  (REST API)        (dev) │
+└──────────────┬──────────────┬───────────────┘
+               │              │
+               ▼              ▼
+┌──────────────────────────────────────────────┐
+│ rcloud API Server (Rust/actix-web)           │
+│                                               │
+│  ┌─────────────┐  ┌─────────────┐           │
+│  │ Terraform   │  │ REST API    │           │
+│  │ Endpoints   │  │ /api/v1/... │           │
+│  └─────────────┘  └─────────────┘           │
+│                                               │
+│  ┌─────────────────────────────────┐         │
+│  │ VM Manager                       │         │
+│  │ - Create/destroy VMs             │         │
+│  │ - Start/stop operations          │         │
+│  │ - State tracking                 │         │
+│  └─────────────────────────────────┘         │
+│                                               │
+│  ┌─────────────────────────────────┐         │
+│  │ guestfs Integration              │         │
+│  │ - Use kdevops base images        │         │
+│  │ - Reuse XML templates            │         │
+│  │ - Leverage storage pools         │         │
+│  └─────────────────────────────────┘         │
+└───────────────────┬───────────────────────────┘
+                    │
+                    ▼
+┌────────────────────────────────────────────────┐
+│ Host Infrastructure (kdevops-managed)          │
+│                                                 │
+│  ┌─────────────┐  ┌──────────────┐            │
+│  │ libvirt     │  │ guestfs      │            │
+│  │ (VMs)       │  │ (base images)│            │
+│  └─────────────┘  └──────────────┘            │
+│                                                 │
+│  Storage: /xfs1/libvirt/                       │
+│    - base_images/ (shared, read-only)          │
+│    - vms/ (per-VM storage)                     │
+└────────────────────────────────────────────────┘
+```
+
+## API Design
+
+### REST API Endpoints
+
+#### VM Management
+```
+POST   /api/v1/vms              Create VM
+GET    /api/v1/vms              List all VMs
+GET    /api/v1/vms/:id          Get VM details
+DELETE /api/v1/vms/:id          Destroy VM
+POST   /api/v1/vms/:id/start    Start VM
+POST   /api/v1/vms/:id/stop     Stop VM
+GET    /api/v1/vms/:id/console  Get console access info
+```
+
+#### Base Images
+```
+GET    /api/v1/images           List available base images
+GET    /api/v1/images/:id       Get image details
+```
+
+#### System Info
+```
+GET    /api/v1/status           System status & capacity
+GET    /api/v1/health           Health check
+```
+
+### VM Resource Schema
+
+```json
+{
+  "id": "vm-abc123",
+  "name": "web-server-01",
+  "state": "running",
+  "vcpus": 4,
+  "memory_mb": 8192,
+  "disks": [
+    {
+      "type": "root",
+      "size_gb": 100,
+      "driver": "virtio"
+    },
+    {
+      "type": "data",
+      "size_gb": 500,
+      "driver": "virtio"
+    }
+  ],
+  "network": {
+    "bridge": "virbr0",
+    "ip_address": "192.168.122.45",
+    "mac_address": "52:54:00:12:34:56"
+  },
+  "image": "debian13",
+  "created_at": "2025-10-10T12:00:00Z",
+  "updated_at": "2025-10-10T12:05:00Z"
+}
+```
+
+## Terraform Provider
+
+### Provider Configuration
+
+```hcl
+terraform {
+  required_providers {
+    rcloud = {
+      source  = "kdevops/rcloud"
+      version = "~> 0.1.0"
+    }
+  }
+}
+
+provider "rcloud" {
+  endpoint = "http://localhost:8765"
+  token    = var.rcloud_token  # Optional for MVP
+}
+```
+
+### Resource: rcloud_vm
+
+```hcl
+resource "rcloud_vm" "web_server" {
+  name      = "web-01"
+  vcpus     = 4
+  memory_gb = 8
+  image     = "debian13"
+
+  root_disk_gb = 100
+
+  extra_disks = [
+    {
+      size_gb = 500
+      type    = "virtio"
+    }
+  ]
+
+  network = {
+    bridge = "virbr0"
+  }
+}
+
+output "vm_ip" {
+  value = rcloud_vm.web_server.ip_address
+}
+```
+
+## Integration with kdevops
+
+### Use Existing Infrastructure
+
+1. **Base Images**: Read from `guestfs_base_image_dir`
+   - `/xfs1/libvirt/guestfs/base_images/debian-13-generic-amd64.raw`
+   - `/xfs1/libvirt/guestfs/base_images/ubuntu-24.04.raw`
+
+2. **Storage Pools**: Use `kdevops_storage_pool_path`
+   - `/xfs1/libvirt/kdevops/`
+
+3. **XML Templates**: Reuse `playbooks/roles/gen_nodes/templates/guestfs_q35.j2.xml`
+   - Parse and render with Rust templating (tera/handlebars)
+
+4. **Configuration**: Read from `extra_vars.yaml`
+   ```rust
+   let config = Config::from_kdevops("/path/to/kdevops")?;
+   let base_images = config.guestfs_base_image_dir;
+   let storage_pool = config.kdevops_storage_pool_path;
+   ```
+
+### Deployment Model
+
+```bash
+# Admin sets up kdevops infrastructure
+make defconfig-rcloud
+make bringup  # Sets up libvirt, guestfs, base images
+
+# Deploy rcloud service
+make rcloud-deploy  # Starts API server as systemd service
+
+# Users consume via Terraform
+terraform apply
+```
+
+## Implementation Plan
+
+### Phase 1: Core REST API (Week 1-2)
+
+**Milestone 1.1: Basic API Server**
+- [ ] Set up actix-web server
+- [ ] Health check endpoint (`/api/v1/health`)
+- [ ] Status endpoint (`/api/v1/status`)
+- [ ] Read kdevops config from `extra_vars.yaml`
+
+**Milestone 1.2: VM Lifecycle**
+- [ ] Create VM endpoint (`POST /api/v1/vms`)
+  - Generate libvirt XML from template
+  - Create disk images
+  - Define and start VM
+- [ ] List VMs endpoint (`GET /api/v1/vms`)
+- [ ] Get VM details endpoint (`GET /api/v1/vms/:id`)
+- [ ] Start/stop endpoints
+- [ ] Destroy VM endpoint
+
+**Milestone 1.3: guestfs Integration**
+- [ ] Read base images from guestfs
+- [ ] Use guestfs XML templates (Tera templating)
+- [ ] COW (copy-on-write) from base images
+- [ ] Proper storage pool paths
+
+### Phase 2: Terraform Provider (Week 3-4)
+
+**Milestone 2.1: Provider Skeleton**
+- [ ] Terraform plugin framework setup
+- [ ] Provider configuration schema
+- [ ] Connection to rcloud API
+
+**Milestone 2.2: VM Resource**
+- [ ] `rcloud_vm` resource CRUD
+- [ ] State management
+- [ ] Import existing VMs
+- [ ] Computed attributes (IP, state)
+
+**Milestone 2.3: Integration & Testing**
+- [ ] Example Terraform configs
+- [ ] Integration tests
+- [ ] Documentation
+
+### Phase 3: Production Readiness (Week 5-6)
+
+**Milestone 3.1: Robustness**
+- [ ] Error handling & recovery
+- [ ] Logging (tracing/slog)
+- [ ] Metrics (Prometheus)
+- [ ] Graceful shutdown
+
+**Milestone 3.2: Deployment**
+- [ ] Systemd service file
+- [ ] Ansible playbook for deployment
+- [ ] Configuration management
+- [ ] Monitoring setup
+
+**Milestone 3.3: Documentation**
+- [ ] API documentation (OpenAPI/Swagger)
+- [ ] Terraform provider docs
+- [ ] Deployment guide
+- [ ] Architecture diagrams
+
+## Technology Stack
+
+### Core Components
+
+- **Language**: Rust (stable)
+- **Web Framework**: actix-web 4.x
+- **Libvirt**: virt-rs crate
+- **Config**: serde-yaml, config-rs
+- **Templating**: tera (Jinja2-like for Rust)
+- **Database**: None for MVP (in-memory state, libvirt is source of truth)
+- **Logging**: tracing + tracing-subscriber
+- **Metrics**: prometheus-client
+
+### Terraform Provider
+
+- **Framework**: terraform-plugin-framework (Go)
+- **API Client**: reqwest (Rust) or generated OpenAPI client
+
+### Testing
+
+- **Unit Tests**: Rust built-in testing
+- **Integration Tests**: testcontainers-rs (for libvirt?)
+- **E2E Tests**: Terraform configs + real libvirt
+
+## File Structure
+
+```
+workflows/rcloud/
+├── DESIGN.md                    # This file
+├── README.md                    # User documentation
+├── Cargo.toml                   # Rust dependencies
+├── src/
+│   ├── main.rs                  # Entry point (API server)
+│   ├── api/
+│   │   ├── mod.rs              # API module
+│   │   ├── server.rs           # actix-web setup
+│   │   ├── routes.rs           # Route definitions
+│   │   ├── handlers/
+│   │   │   ├── vms.rs          # VM endpoints
+│   │   │   ├── images.rs       # Image endpoints
+│   │   │   └── health.rs       # Health/status endpoints
+│   │   └── models.rs           # API request/response types
+│   ├── vm/
+│   │   ├── mod.rs              # VM module
+│   │   ├── manager.rs          # VM lifecycle operations
+│   │   ├── libvirt.rs          # libvirt wrapper
+│   │   └── xml.rs              # XML generation (templates)
+│   ├── config/
+│   │   ├── mod.rs              # Config module
+│   │   ├── kdevops.rs          # Parse kdevops config
+│   │   └── rcloud.rs           # rcloud-specific config
+│   └── lib.rs                  # Library exports
+├── templates/
+│   └── vm-q35.xml.tera         # libvirt XML template (from guestfs)
+├── config/
+│   └── rcloud.yaml             # Default configuration
+└── tests/
+    ├── api_tests.rs            # API integration tests
+    └── vm_tests.rs             # VM operation tests
+
+terraform-provider-rcloud/       # Separate repo/directory
+├── main.go                      # Provider entry point
+├── provider/
+│   ├── provider.go             # Provider configuration
+│   ├── resource_vm.go          # rcloud_vm resource
+│   └── data_source_vm.go       # rcloud_vm data source
+└── examples/
+    └── main.tf                 # Example configuration
+```
+
+## Configuration
+
+### rcloud.yaml
+
+```yaml
+# Server configuration
+server:
+  bind_address: "0.0.0.0:8765"
+  workers: 4
+
+# kdevops integration
+kdevops:
+  config_path: "/path/to/kdevops"
+  extra_vars_file: "extra_vars.yaml"
+
+# libvirt
+libvirt:
+  uri: "qemu:///system"
+  storage_pool_path: "/xfs1/libvirt/kdevops"
+
+# VM defaults
+vm_defaults:
+  network_bridge: "virbr0"
+  disk_format: "raw"
+  disk_driver: "virtio"
+
+# Logging
+logging:
+  level: "info"
+  format: "json"  # or "text"
+```
+
+## Security Considerations (Future)
+
+For production multi-user deployment:
+
+1. **Authentication**: API tokens, OAuth2
+2. **Authorization**: Role-based access control (RBAC)
+3. **Network**: TLS/HTTPS for API
+4. **Resource Isolation**: User namespacing
+5. **Quotas**: Per-user resource limits
+
+## Monitoring & Observability
+
+1. **Metrics**: Prometheus-compatible `/metrics` endpoint
+   - VM count, CPU usage, memory usage
+   - API request rates, latencies
+   - Error rates
+
+2. **Logging**: Structured JSON logging
+   - Request/response logs
+   - VM operation logs
+   - Error traces
+
+3. **Tracing**: OpenTelemetry (future)
+
+## Open Questions
+
+1. **State Management**: Where to store VM metadata?
+   - Option A: In-memory (ephemeral, simple)
+   - Option B: sqlite (persistent, queryable)
+   - Option C: libvirt is source of truth (read from libvirt API)
+   - **Decision**: Start with Option C (libvirt as SSOT), add DB later if needed
+
+2. **Image Management**: How to handle base images?
+   - Option A: Read-only from guestfs location
+   - Option B: Import/copy to rcloud-managed location
+   - **Decision**: Option A for MVP
+
+3. **Networking**: Static IPs vs DHCP?
+   - MVP: Use libvirt's default network (DHCP)
+   - Future: Support static IPs, custom networks
+
+4. **Multi-tenancy**: User isolation?
+   - MVP: Single-user (no auth)
+   - Phase 2: API tokens
+   - Phase 3: Full multi-tenancy
+
+## Success Criteria
+
+### MVP (Phase 1 + 2 Complete)
+
+- [ ] REST API server running as systemd service
+- [ ] Can create/destroy VMs via API
+- [ ] Terraform provider can provision VMs
+- [ ] Uses kdevops guestfs base images
+- [ ] VMs boot and are accessible via SSH
+- [ ] Basic monitoring (Prometheus metrics)
+
+### Production Ready (Phase 3 Complete)
+
+- [ ] Comprehensive error handling
+- [ ] Full API documentation
+- [ ] Deployment automation (Ansible)
+- [ ] Integration tests passing
+- [ ] Performance tested (50+ concurrent VMs)
+
+## References
+
+- Ubicloud: https://www.ubicloud.com/
+- Terraform Provider Framework: https://developer.hashicorp.com/terraform/plugin/framework
+- libvirt Rust bindings: https://crates.io/crates/virt
+- actix-web: https://actix.rs/
diff --git a/workflows/rcloud/Kconfig b/workflows/rcloud/Kconfig
new file mode 100644
index 00000000..313259b7
--- /dev/null
+++ b/workflows/rcloud/Kconfig
@@ -0,0 +1,53 @@
+config RCLOUD
+	bool "Enable rcloud REST API server"
+	default n
+	help
+	  Enable rcloud - a private cloud infrastructure REST API server for
+	  managing VMs through libvirt. This provides a Ubicloud-like interface
+	  for declarative VM management using Terraform.
+
+	  rcloud leverages existing guestfs infrastructure (base images, storage
+	  pools) and provides a REST API for VM lifecycle management.
+
+	  When enabled, running 'make' will build the rcloud Rust binary, and
+	  'make rcloud' will install and configure the systemd service.
+
+	  This is typically enabled on a dedicated server to provide cloud-like
+	  VM provisioning to multiple users.
+
+if RCLOUD
+
+config RCLOUD_SERVER_BIND
+	string "rcloud API server bind address"
+	output yaml
+	default "127.0.0.1:8765"
+	help
+	  Address and port for the rcloud API server to bind to.
+
+	  Default is localhost only (127.0.0.1:8765) for security.
+	  Port 8765 is chosen to avoid conflicts with common services.
+	  Use 0.0.0.0:8765 to allow network access (not recommended without
+	  authentication - see docs/AUTHENTICATION.md).
+
+	  Users can access via SSH tunnel:
+	    ssh -L 8765:localhost:8765 server
+
+config RCLOUD_WORKERS
+	int "Number of worker threads"
+	output yaml
+	default 4
+	help
+	  Number of worker threads for the rcloud API server.
+	  Default is 4, suitable for most deployments.
+
+config RCLOUD_ENABLE_TERRAFORM_PROVIDER
+	bool "Build Terraform provider"
+	default y
+	help
+	  Build the Terraform provider for rcloud. This allows managing
+	  rcloud VMs using Infrastructure as Code with Terraform.
+
+	  The provider will be built in terraform-provider-rcloud/ and can
+	  be installed to ~/.terraform.d/plugins/ for local development.
+
+endif # RCLOUD
diff --git a/workflows/rcloud/Makefile b/workflows/rcloud/Makefile
new file mode 100644
index 00000000..14f99679
--- /dev/null
+++ b/workflows/rcloud/Makefile
@@ -0,0 +1,149 @@
+RCLOUD_WORKFLOW := workflows/rcloud
+
+# Check if rcloud is enabled
+ifeq (y,$(CONFIG_RCLOUD))
+
+# Extract port from bind address, removing quotes
+RCLOUD_PORT := $(shell echo $(CONFIG_RCLOUD_SERVER_BIND) | sed -e 's/.*://' -e 's/"//g')
+
+RCLOUD_DEPS :=
+
+# Add rcloud-build to default dependencies so 'make' builds rcloud
+DEFAULT_DEPS += rcloud-build
+
+# Check build prerequisites
+rcloud-check-deps:
+	@echo "Checking rcloud build dependencies..."
+	@command -v cargo >/dev/null 2>&1 || \
+		{ echo "ERROR: cargo not found. Install dependencies with 'make rcloud-deps' or install cargo package."; exit 1; }
+	@command -v rustc >/dev/null 2>&1 || \
+		{ echo "ERROR: rustc not found. Install dependencies with 'make rcloud-deps' or install rustc package."; exit 1; }
+	@command -v pkg-config >/dev/null 2>&1 || \
+		{ echo "ERROR: pkg-config not found. Install pkg-config package."; exit 1; }
+	@pkg-config --exists libvirt 2>/dev/null || \
+		{ echo "ERROR: libvirt development libraries not found. Install libvirt-dev (Debian) or libvirt-devel (RPM) package."; exit 1; }
+	@echo "All build dependencies satisfied"
+
+# Build rcloud Rust binary
+$(RCLOUD_WORKFLOW)/target/release/rcloud: rcloud-check-deps $(RCLOUD_DEPS) FORCE
+	@echo "Building rcloud REST API server..."
+	$(Q)cd $(RCLOUD_WORKFLOW) && cargo build --release
+	@echo "rcloud binary built at $(RCLOUD_WORKFLOW)/target/release/rcloud"
+
+# Build Terraform provider if enabled
+ifeq (y,$(CONFIG_RCLOUD_ENABLE_TERRAFORM_PROVIDER))
+# Check Go is available for Terraform provider build
+rcloud-check-go-deps:
+	@echo "Checking Go build dependencies..."
+	@command -v go >/dev/null 2>&1 || \
+		{ echo "ERROR: go not found. Install dependencies with 'make rcloud-deps' or install golang package."; exit 1; }
+	@echo "Go dependencies satisfied"
+
+terraform-provider-rcloud/terraform-provider-rcloud: rcloud-check-go-deps FORCE
+	@echo "Building Terraform provider for rcloud..."
+	$(Q)cd terraform-provider-rcloud && go build
+	@echo "Terraform provider built at terraform-provider-rcloud/terraform-provider-rcloud"
+
+RCLOUD_DEPS += terraform-provider-rcloud/terraform-provider-rcloud
+endif
+
+# Default target: build rcloud and dependencies
+rcloud-build: $(RCLOUD_WORKFLOW)/target/release/rcloud $(RCLOUD_DEPS)
+	@echo "rcloud build complete"
+
+# Create base images with proper customization (networking, SSH, users, etc.)
+rcloud-base-images:
+	@echo "Creating base images with virt-builder customization..."
+	$(Q)ansible-playbook $(ANSIBLE_VERBOSE) \
+		--limit 'localhost' \
+		--extra-vars=@./extra_vars.yaml \
+		--tags network,pool,base_image \
+		playbooks/guestfs.yml
+	@echo "Base images created and customized"
+
+# Install rcloud (copy binary, setup systemd service, install Terraform provider)
+rcloud: rcloud-build rcloud-base-images
+	@echo "Installing rcloud..."
+	$(Q)ansible-playbook $(ANSIBLE_VERBOSE) \
+		--extra-vars=@./extra_vars.yaml  \
+		-i inventory/hosts \
+		playbooks/rcloud.yml
+ifeq (y,$(CONFIG_RCLOUD_ENABLE_TERRAFORM_PROVIDER))
+	@echo "Installing Terraform provider to local plugin directory..."
+	$(Q)mkdir -p ~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64
+	$(Q)cp terraform-provider-rcloud/terraform-provider-rcloud \
+		~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64/
+	@echo "Terraform provider installed to ~/.terraform.d/"
+endif
+	@echo "rcloud installed and configured"
+	@echo ""
+	@echo "Starting rcloud service..."
+	$(Q)sudo systemctl start rcloud
+	@echo ""
+	@echo "Checking rcloud service status..."
+	$(Q)sudo systemctl status rcloud --no-pager || true
+	@echo ""
+	@echo "rcloud service started successfully"
+	@echo ""
+	$(Q)$(RCLOUD_WORKFLOW)/scripts/check-health.py http://localhost:$(RCLOUD_PORT)
+
+# Check rcloud service health and status
+rcloud-status:
+	@echo "Checking rcloud service health..."
+	@echo ""
+	$(Q)$(RCLOUD_WORKFLOW)/scripts/check-health.py http://localhost:$(RCLOUD_PORT)
+
+# Install Terraform provider to local development directory
+rcloud-terraform-install: terraform-provider-rcloud/terraform-provider-rcloud
+	@echo "Installing Terraform provider to local plugin directory..."
+	$(Q)mkdir -p ~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64
+	$(Q)cp terraform-provider-rcloud/terraform-provider-rcloud \
+		~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/0.1.0/linux_amd64/
+	@echo "Terraform provider installed"
+	@echo ""
+	@echo "You can now use it in Terraform configurations:"
+	@echo "  terraform {"
+	@echo "    required_providers {"
+	@echo "      rcloud = {"
+	@echo "        source = \"kdevops/rcloud\""
+	@echo "      }"
+	@echo "    }"
+	@echo "  }"
+
+# Clean build artifacts
+rcloud-clean:
+	@echo "Cleaning rcloud build artifacts..."
+	$(Q)cd $(RCLOUD_WORKFLOW) && cargo clean
+ifeq (y,$(CONFIG_RCLOUD_ENABLE_TERRAFORM_PROVIDER))
+	$(Q)cd terraform-provider-rcloud && rm -f terraform-provider-rcloud
+endif
+	@echo "rcloud clean complete"
+
+# Help target
+rcloud-help:
+	@echo "rcloud targets:"
+	@echo "  rcloud-build              - Build rcloud server and Terraform provider"
+	@echo "  rcloud-base-images        - Create base images with virt-builder customization"
+	@echo "  rcloud                    - Install and configure rcloud (runs rcloud-build and rcloud-base-images)"
+	@echo "  rcloud-status             - Check rcloud health and explain status"
+	@echo "  rcloud-terraform-install  - Install Terraform provider to ~/.terraform.d/"
+	@echo "  rcloud-clean              - Clean build artifacts"
+	@echo ""
+	@echo "Configuration:"
+	@echo "  RCLOUD_SERVER_BIND=$(CONFIG_RCLOUD_SERVER_BIND)"
+	@echo "  RCLOUD_WORKERS=$(CONFIG_RCLOUD_WORKERS)"
+	@echo ""
+	@echo "See workflows/rcloud/DESIGN.md for architecture details"
+
+HELP_TARGETS += rcloud-help
+PHONY += rcloud rcloud-build rcloud-base-images rcloud-status rcloud-clean rcloud-help rcloud-terraform-install
+PHONY += rcloud-check-deps rcloud-check-go-deps
+
+else
+# rcloud not enabled - provide no-op targets
+rcloud rcloud-build rcloud-base-images rcloud-status rcloud-clean rcloud-help rcloud-terraform-install:
+	$(Q)echo "rcloud is not enabled. Enable it with 'make menuconfig'"
+endif
+
+FORCE:
+.PHONY: $(PHONY) FORCE
diff --git a/workflows/rcloud/docs/AUTHENTICATION.md b/workflows/rcloud/docs/AUTHENTICATION.md
new file mode 100644
index 00000000..4d6e9e88
--- /dev/null
+++ b/workflows/rcloud/docs/AUTHENTICATION.md
@@ -0,0 +1,166 @@
+# rcloud Authentication (Future TODO)
+
+## Current State
+
+The MVP implementation has **no authentication** - any client with network access to the rcloud API server can create, modify, and destroy VMs. This is acceptable for:
+
+- Single-user private development environments
+- Environments where network access to the rcloud server is already restricted (firewall, VPN, SSH tunnel)
+- Trusted multi-user environments where users already have SSH access to the host
+
+## Future Authentication Options
+
+### Option 1: Simple API Token File (Recommended)
+
+File-based token authentication similar to SSH authorized_keys pattern.
+
+#### Design
+
+**Token Storage**: `~/.rcloud/tokens` or `/etc/rcloud/tokens`
+
+Format:
+```
+# username:token_hex:created_timestamp:description
+mcgrof:a1b2c3d4e5f6789...:1696896000:laptop
+jenkins:9f8e7d6c5b4a321...:1696896100:ci-system
+```
+
+**Client Authentication**:
+```bash
+# Store token in client config
+echo "a1b2c3d4e5f6789..." > ~/.rcloud/token
+
+# Or use environment variable
+export RCLOUD_TOKEN=a1b2c3d4e5f6789...
+
+# Use with curl
+curl -H "Authorization: Bearer a1b2c3d4e5f6789..." \
+  http://localhost:8765/api/v1/vms
+
+# Terraform provider already supports this
+provider "rcloud" {
+  endpoint = "http://localhost:8765"
+  token    = var.rcloud_token
+}
+```
+
+**Token Management CLI**:
+```bash
+# Generate new token
+rcloud token create --user mcgrof --description "my laptop"
+# Output: Token: a1b2c3d4e5f6789abcdef... (save this, it won't be shown again)
+
+# List active tokens
+rcloud token list
+# mcgrof    a1b2...  2024-10-10  laptop
+# jenkins   9f8e...  2024-10-10  ci-system
+
+# Revoke token
+rcloud token revoke a1b2c3d4e5f6789...
+```
+
+**Implementation**:
+- Simple actix-web middleware to check `Authorization: Bearer` header
+- Load tokens from file at startup (or watch for changes)
+- Log authentication attempts for audit trail
+- Token generation: `openssl rand -hex 32` or similar
+
+**Advantages**:
+- Very simple to implement and understand
+- No external dependencies (no database, no auth server)
+- Easy to audit (just a text file)
+- Works well with CI/CD (inject token as environment variable)
+- Already designed into Terraform provider
+- Familiar pattern for sysadmins (like SSH keys)
+
+**Trade-offs**:
+- Tokens are long-lived (manual revocation required)
+- No automatic expiration (could add timestamp checks)
+- Token transmitted in HTTP headers (use HTTPS in production)
+
+### Option 2: Mutual TLS (mTLS) with Client Certificates
+
+Certificate-based authentication similar to kubectl/Kubernetes.
+
+#### Design
+
+**Certificate Authority**:
+- rcloud acts as CA or uses existing CA
+- Generate client certificates signed by CA
+- Client presents certificate with every HTTPS request
+
+**Setup**:
+```bash
+# Server: Generate CA and server cert
+rcloud cert init-ca
+rcloud cert generate-server
+
+# Admin: Generate client certificate for user
+rcloud cert generate-client --user mcgrof --days 365
+
+# Client: Use certificate
+curl --cert ~/.rcloud/client.crt \
+     --key ~/.rcloud/client.key \
+     --cacert ~/.rcloud/ca.crt \
+     https://localhost:8443/api/v1/vms
+```
+
+**Implementation**:
+- Configure actix-web for HTTPS with client certificate validation
+- Extract username from certificate CN or Subject Alternative Name
+- Map certificate to permissions/roles
+
+**Advantages**:
+- Very secure (mutual authentication)
+- Standard TLS infrastructure
+- Automatic certificate expiration
+- No bearer tokens to leak
+- Works well with existing PKI infrastructure
+
+**Trade-offs**:
+- More complex setup (certificate management)
+- Requires HTTPS (TLS overhead)
+- Certificate distribution and renewal
+- May be overkill for simple use cases
+
+## Recommendation
+
+For the **first authentication implementation**, use **Option 1 (Simple API Token File)**:
+
+1. Simpler to implement and use
+2. Matches kdevops's file-based configuration philosophy
+3. Adequate security for private cloud environments
+4. Easy to evolve to more sophisticated schemes later
+5. Terraform provider already has token support built-in
+
+**Option 2 (mTLS)** is better suited for:
+- Multi-tenant production environments
+- Environments requiring strong cryptographic authentication
+- Integration with existing PKI infrastructure
+- Compliance requirements (audit trails, certificate rotation)
+
+## Implementation Priority
+
+**Phase 3 - Production Readiness** (from DESIGN.md):
+- [ ] Implement Option 1: Simple API token authentication
+  - [ ] Token file format and storage
+  - [ ] actix-web middleware for token validation
+  - [ ] Token management CLI commands
+  - [ ] Logging and audit trail
+  - [ ] Documentation and examples
+
+**Future Enhancement**:
+- [ ] Add token expiration timestamps
+- [ ] Support HTTPS with TLS
+- [ ] Consider mTLS for high-security deployments
+
+## MVP Deployment Model
+
+For now, rcloud security relies on **network-level access control**:
+
+1. **Localhost only**: Bind to `127.0.0.1:8765` (default)
+2. **SSH tunneling**: Users connect via `ssh -L 8080:localhost:8765 server`
+3. **Firewall rules**: Block port 8080 from external networks
+4. **VPN access**: Require VPN to access rcloud network
+
+This matches how many development tools work (Jupyter, development databases, etc.) and is adequate for single-server, trusted-user environments.
diff --git a/workflows/rcloud/docs/TESTING.md b/workflows/rcloud/docs/TESTING.md
new file mode 100644
index 00000000..0a4a648b
--- /dev/null
+++ b/workflows/rcloud/docs/TESTING.md
@@ -0,0 +1,429 @@
+# Testing rcloud
+
+## Quick Start Testing
+
+### 1. Setup rcloud Server
+
+On the server that will run rcloud:
+
+```bash
+# Configure and build
+make defconfig-rcloud
+make
+
+# Install and start service
+make rcloud
+sudo systemctl start rcloud
+sudo systemctl status rcloud
+
+# Verify API is running
+curl http://localhost:8765/api/v1/health
+curl http://localhost:8765/api/v1/status
+```
+
+### 2. Setup Test Environment with Base Images
+
+Before you can create VMs, you need guestfs base images:
+
+```bash
+# On a separate kdevops instance or the same system:
+make defconfig-rcloud-guest-test
+make
+
+# This will:
+# - Setup guestfs infrastructure
+# - Download and prepare Debian 13 base image
+# - Configure libvirt and storage pools
+```
+
+The base image will be created at the configured `guestfs_base_image_dir`
+(typically `/xfs1/libvirt/guestfs/base_images/`).
+
+### 3. Verify Base Images
+
+```bash
+curl http://localhost:8765/api/v1/images
+```
+
+Should return JSON with available base images:
+```json
+{
+  "images": [
+    {"name": "debian-13-generic-amd64.raw"}
+  ]
+}
+```
+
+### 4. Test REST API
+
+#### Create a VM
+
+```bash
+curl -X POST http://localhost:8765/api/v1/vms \
+  -H "Content-Type: application/json" \
+  -d '{
+    "name": "test-vm-01",
+    "vcpus": 2,
+    "memory_mb": 4096,
+    "base_image": "debian-13-generic-amd64.raw",
+    "root_disk_gb": 50
+  }'
+```
+
+Response:
+```json
+{
+  "id": "550e8400-e29b-41d4-a716-446655440000",
+  "name": "test-vm-01",
+  "state": "creating"
+}
+```
+
+#### List VMs
+
+```bash
+curl http://localhost:8765/api/v1/vms
+```
+
+#### Get VM Details
+
+```bash
+curl http://localhost:8765/api/v1/vms/550e8400-e29b-41d4-a716-446655440000
+```
+
+#### Stop VM
+
+```bash
+curl -X POST http://localhost:8765/api/v1/vms/550e8400-e29b-41d4-a716-446655440000/stop
+```
+
+#### Start VM
+
+```bash
+curl -X POST http://localhost:8765/api/v1/vms/550e8400-e29b-41d4-a716-446655440000/start
+```
+
+#### Destroy VM
+
+```bash
+curl -X DELETE http://localhost:8765/api/v1/vms/550e8400-e29b-41d4-a716-446655440000
+```
+
+### 5. Test Terraform Provider
+
+#### Install Provider
+
+```bash
+make rcloud-terraform-install
+```
+
+#### Create Test Configuration
+
+```bash
+mkdir -p ~/rcloud-test
+cd ~/rcloud-test
+
+cat > main.tf <<'EOF'
+terraform {
+  required_providers {
+    rcloud = {
+      source = "kdevops/rcloud"
+    }
+  }
+}
+
+provider "rcloud" {
+  endpoint = "http://localhost:8765"
+}
+
+resource "rcloud_vm" "test" {
+  name         = "terraform-test-01"
+  vcpus        = 2
+  memory_gb    = 4
+  base_image   = "debian-13-generic-amd64.raw"
+  root_disk_gb = 50
+}
+
+output "vm_id" {
+  value = rcloud_vm.test.id
+}
+
+output "vm_state" {
+  value = rcloud_vm.test.state
+}
+EOF
+```
+
+#### Run Terraform
+
+```bash
+terraform init
+terraform plan
+terraform apply
+
+# Check the VM was created
+curl http://localhost:8765/api/v1/vms
+
+# Destroy
+terraform destroy
+```
+
+## Testing Workflow
+
+### Option 1: Server and Client on Same Host
+
+```bash
+# Terminal 1: Setup rcloud server
+make defconfig-rcloud
+make
+make rcloud
+sudo systemctl start rcloud
+
+# Terminal 2: Setup guestfs base images
+make defconfig-rcloud-guest-test
+make
+
+# Terminal 3: Test API
+curl http://localhost:8765/api/v1/health
+curl http://localhost:8765/api/v1/images
+# ... create VMs via curl or Terraform
+```
+
+### Option 2: Separate Server and Client Machines
+
+**On Server:**
+```bash
+# Setup rcloud
+make defconfig-rcloud
+make
+make rcloud
+sudo systemctl start rcloud
+
+# If accessing from network, bind to all interfaces:
+# Edit /etc/systemd/system/rcloud.service
+# Add: Environment="RCLOUD_SERVER_BIND=0.0.0.0:8765"
+# Then:
+sudo systemctl daemon-reload
+sudo systemctl restart rcloud
+
+# Open firewall if needed:
+sudo firewall-cmd --add-port=8765/tcp --permanent
+sudo firewall-cmd --reload
+```
+
+**On Client:**
+```bash
+# Setup guestfs base images (if not already on server)
+make defconfig-rcloud-guest-test
+make
+
+# Test from client
+curl http://server-ip:8765/api/v1/health
+
+# Or use SSH tunnel for security
+ssh -L 8765:localhost:8765 user@server-ip
+curl http://localhost:8765/api/v1/health
+```
+
+### Option 3: Using SSH Tunnel (Recommended for Security)
+
+```bash
+# On client: Create SSH tunnel
+ssh -L 8765:localhost:8765 user@rcloud-server
+
+# In another terminal on client: Access API
+curl http://localhost:8765/api/v1/health
+
+# Use Terraform with tunnel
+cd ~/rcloud-test
+terraform apply
+```
+
+## Monitoring
+
+### Check rcloud Logs
+
+```bash
+sudo journalctl -u rcloud -f
+```
+
+### Check Prometheus Metrics
+
+```bash
+curl http://localhost:8765/metrics
+```
+
+Example metrics:
+```
+# HELP rcloud_http_requests_total Total number of HTTP requests
+# TYPE rcloud_http_requests_total counter
+rcloud_http_requests_total{method="POST",endpoint="/vms",status="201"} 5
+
+# HELP rcloud_vm_count Current number of VMs
+# TYPE rcloud_vm_count gauge
+rcloud_vm_count 2
+
+# HELP rcloud_vm_operations_total Total number of VM operations
+# TYPE rcloud_vm_operations_total counter
+rcloud_vm_operations_total{method="VM",endpoint="create",status="success"} 5
+```
+
+## Troubleshooting
+
+### rcloud Service Won't Start
+
+```bash
+# Check logs
+sudo journalctl -u rcloud -e
+
+# Common issues:
+# 1. libvirt not running
+sudo systemctl status libvirtd
+sudo systemctl start libvirtd
+
+# 2. rcloud user not in libvirt group
+sudo usermod -a -G libvirt rcloud
+
+# 3. KDEVOPS_ROOT not set correctly
+sudo systemctl edit rcloud
+# Add/fix: Environment="KDEVOPS_ROOT=/path/to/kdevops"
+```
+
+### Cannot Connect to API
+
+```bash
+# Check if service is listening
+sudo ss -tlnp | grep 8765
+
+# Check firewall
+sudo firewall-cmd --list-all
+
+# Check rcloud is running
+sudo systemctl status rcloud
+```
+
+### No Base Images Available
+
+```bash
+# Check guestfs base images directory
+ls -la $(grep guestfs_base_image_dir extra_vars.yaml | cut -d: -f2 | tr -d ' "')
+
+# Ensure guestfs setup ran
+make bringup  # This creates base images
+```
+
+### VM Creation Fails
+
+```bash
+# Check libvirt connection
+virsh -c qemu:///system list --all
+
+# Check storage pool
+virsh -c qemu:///system pool-list
+virsh -c qemu:///system pool-info default
+
+# Check base image exists and is readable
+ls -la /xfs1/libvirt/guestfs/base_images/
+
+# Check rcloud user can access libvirt
+sudo -u rcloud virsh -c qemu:///system list --all
+```
+
+### Terraform Apply Fails
+
+```bash
+# Check provider is installed
+ls -la ~/.terraform.d/plugins/registry.terraform.io/kdevops/rcloud/
+
+# Check API is accessible
+curl http://localhost:8765/api/v1/health
+
+# Enable Terraform logging
+export TF_LOG=DEBUG
+terraform apply
+```
+
+## Integration Testing
+
+### Automated Test Script
+
+```bash
+#!/bin/bash
+# test-rcloud.sh
+
+set -e
+
+API="http://localhost:8765"
+
+echo "1. Health check..."
+curl -f $API/api/v1/health
+
+echo "2. List images..."
+IMAGES=$(curl -s $API/api/v1/images | jq -r '.images[0].name')
+echo "Found image: $IMAGES"
+
+echo "3. Create VM..."
+VM_ID=$(curl -s -X POST $API/api/v1/vms \
+  -H "Content-Type: application/json" \
+  -d "{
+    \"name\": \"test-$(date +%s)\",
+    \"vcpus\": 2,
+    \"memory_mb\": 2048,
+    \"base_image\": \"$IMAGES\",
+    \"root_disk_gb\": 20
+  }" | jq -r '.id')
+echo "Created VM: $VM_ID"
+
+echo "4. Get VM details..."
+curl -s $API/api/v1/vms/$VM_ID | jq
+
+echo "5. List all VMs..."
+curl -s $API/api/v1/vms | jq
+
+echo "6. Stop VM..."
+curl -s -X POST $API/api/v1/vms/$VM_ID/stop | jq
+
+echo "7. Destroy VM..."
+curl -s -X DELETE $API/api/v1/vms/$VM_ID | jq
+
+echo "All tests passed!"
+```
+
+Run with:
+```bash
+chmod +x test-rcloud.sh
+./test-rcloud.sh
+```
+
+## Performance Testing
+
+### Create Multiple VMs
+
+```bash
+#!/bin/bash
+for i in {1..10}; do
+  curl -X POST http://localhost:8765/api/v1/vms \
+    -H "Content-Type: application/json" \
+    -d "{
+      \"name\": \"perf-test-$i\",
+      \"vcpus\": 2,
+      \"memory_mb\": 2048,
+      \"base_image\": \"debian-13-generic-amd64.raw\",
+      \"root_disk_gb\": 20
+    }" &
+done
+wait
+
+# Check they were all created
+curl http://localhost:8765/api/v1/vms | jq '.vms | length'
+```
+
+## Next Steps
+
+Once basic testing is complete:
+
+1. **Test with real workloads**: SSH into created VMs and run applications
+2. **Test Terraform workflows**: Use complex Terraform configs with multiple VMs
+3. **Test multi-user**: Have multiple users create VMs simultaneously
+4. **Monitor metrics**: Setup Prometheus to scrape the metrics endpoint
+5. **Test persistence**: Restart rcloud service and verify VMs are still manageable
diff --git a/workflows/rcloud/scripts/check-health.py b/workflows/rcloud/scripts/check-health.py
new file mode 100644
index 00000000..f5148984
--- /dev/null
+++ b/workflows/rcloud/scripts/check-health.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+"""
+rcloud Health Check Script
+
+This script queries the rcloud REST API health endpoint and provides
+a human-readable explanation of the service status.
+"""
+
+import json
+import sys
+import urllib.request
+import urllib.error
+
+
+def check_health(endpoint="http://localhost:8765"):
+    """
+    Check the health of the rcloud API server.
+
+    Args:
+        endpoint: The base URL of the rcloud API server
+
+    Returns:
+        Tuple of (success: bool, data: dict, message: str)
+    """
+    health_url = f"{endpoint}/api/v1/health"
+
+    try:
+        with urllib.request.urlopen(health_url, timeout=5) as response:
+            if response.status == 200:
+                data = json.loads(response.read().decode("utf-8"))
+                return True, data, None
+            else:
+                return False, None, f"HTTP {response.status}: {response.reason}"
+
+    except urllib.error.URLError as e:
+        return False, None, f"Connection failed: {e.reason}"
+    except urllib.error.HTTPError as e:
+        return False, None, f"HTTP error: {e.code} {e.reason}"
+    except json.JSONDecodeError as e:
+        return False, None, f"Invalid JSON response: {e}"
+    except Exception as e:
+        return False, None, f"Unexpected error: {e}"
+
+
+def explain_health_status(data):
+    """
+    Explain the health check response in human language.
+
+    Args:
+        data: The parsed JSON health check response
+
+    Returns:
+        List of explanation strings
+    """
+    explanations = []
+
+    # Explain overall status
+    status = data.get("status", "unknown")
+    if status == "healthy":
+        explanations.append(
+            "✅ SERVICE STATUS: The rcloud REST API server is running and healthy."
+        )
+        explanations.append(
+            "   This means the service is accepting connections and ready to handle requests."
+        )
+    elif status == "degraded":
+        explanations.append("⚠️  SERVICE STATUS: The service is running but degraded.")
+        explanations.append(
+            "   Some functionality may be limited or experiencing issues."
+        )
+    elif status == "unhealthy":
+        explanations.append("❌ SERVICE STATUS: The service is unhealthy.")
+        explanations.append("   Critical issues are preventing normal operation.")
+    else:
+        explanations.append(f"❓ SERVICE STATUS: Unknown status '{status}'")
+
+    # Explain version
+    version = data.get("version", "unknown")
+    explanations.append("")
+    explanations.append(f"📦 VERSION: rcloud {version}")
+    explanations.append(
+        "   This is the version of the rcloud server currently running."
+    )
+
+    # Explain what this means for operations
+    explanations.append("")
+    explanations.append("🔧 WHAT YOU CAN DO:")
+    if status == "healthy":
+        explanations.append("   • Create new virtual machines via the API")
+        explanations.append("   • List and manage existing VMs")
+        explanations.append("   • Use Terraform to provision infrastructure")
+        explanations.append("   • Query available base images")
+    else:
+        explanations.append("   • Check service logs: sudo journalctl -u rcloud -n 50")
+        explanations.append(
+            "   • Verify libvirt is running: sudo systemctl status libvirtd"
+        )
+        explanations.append("   • Check system resources: df -h, free -h")
+
+    return explanations
+
+
+def main():
+    """Main entry point for the health check script."""
+
+    # Allow custom endpoint as command line argument
+    endpoint = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:8765"
+
+    print("=" * 70)
+    print("rcloud Health Check")
+    print("=" * 70)
+    print(f"\nQuerying: {endpoint}/api/v1/health\n")
+
+    # Perform health check
+    success, data, error_msg = check_health(endpoint)
+
+    if not success:
+        print("❌ HEALTH CHECK FAILED")
+        print(f"\nError: {error_msg}\n")
+        print("Troubleshooting steps:")
+        print("  1. Verify the service is running:")
+        print("     sudo systemctl status rcloud")
+        print("  2. Check if the correct port is being used:")
+        print("     sudo ss -tlnp | grep rcloud")
+        print("  3. Review service logs:")
+        print("     sudo journalctl -u rcloud -n 50")
+        print("  4. Ensure you're using the correct endpoint URL")
+        print()
+        return 1
+
+    # Show raw JSON response
+    print("📄 RAW API RESPONSE:")
+    print("-" * 70)
+    print(json.dumps(data, indent=2))
+    print("-" * 70)
+    print()
+
+    # Show human-friendly explanation
+    print("📖 EXPLANATION:")
+    print("-" * 70)
+    for line in explain_health_status(data):
+        print(line)
+    print("-" * 70)
+    print()
+
+    # Show next steps
+    print("🔗 TRY THESE ENDPOINTS:")
+    print(f"  • List VMs:        curl {endpoint}/api/v1/vms")
+    print(f"  • List images:     curl {endpoint}/api/v1/images")
+    print(f"  • System status:   curl {endpoint}/api/v1/status")
+    print(f"  • Metrics:         curl {endpoint}/metrics")
+    print()
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/workflows/rcloud/src/api/handlers/health.rs b/workflows/rcloud/src/api/handlers/health.rs
new file mode 100644
index 00000000..f547c371
--- /dev/null
+++ b/workflows/rcloud/src/api/handlers/health.rs
@@ -0,0 +1,51 @@
+use actix_web::{web, HttpResponse, Result};
+use serde::{Deserialize, Serialize};
+use tracing::info;
+
+use crate::config::AppConfig;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct HealthResponse {
+    pub status: String,
+    pub version: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct SystemStatusResponse {
+    pub status: String,
+    pub version: String,
+    pub kdevops_root: String,
+    pub libvirt_uri: String,
+    pub storage_pool_path: String,
+    pub base_images_dir: String,
+    pub network_bridge: String,
+}
+
+/// Health check endpoint
+pub async fn health_check() -> Result<HttpResponse> {
+    info!("Health check requested");
+
+    let response = HealthResponse {
+        status: "healthy".to_string(),
+        version: env!("CARGO_PKG_VERSION").to_string(),
+    };
+
+    Ok(HttpResponse::Ok().json(response))
+}
+
+/// System status endpoint with configuration details
+pub async fn system_status(config: web::Data<AppConfig>) -> Result<HttpResponse> {
+    info!("System status requested");
+
+    let response = SystemStatusResponse {
+        status: "operational".to_string(),
+        version: env!("CARGO_PKG_VERSION").to_string(),
+        kdevops_root: config.kdevops_root.display().to_string(),
+        libvirt_uri: config.libvirt_uri.clone(),
+        storage_pool_path: config.storage_pool_path.display().to_string(),
+        base_images_dir: config.base_images_dir.display().to_string(),
+        network_bridge: config.network_bridge.clone(),
+    };
+
+    Ok(HttpResponse::Ok().json(response))
+}
diff --git a/workflows/rcloud/src/api/handlers/images.rs b/workflows/rcloud/src/api/handlers/images.rs
new file mode 100644
index 00000000..043809a3
--- /dev/null
+++ b/workflows/rcloud/src/api/handlers/images.rs
@@ -0,0 +1,31 @@
+use actix_web::{web, HttpResponse, Result};
+use tracing::{error, info};
+
+use crate::api::models::{ImageInfo, ListImagesResponse};
+use crate::config::AppConfig;
+use crate::vm::VmManager;
+
+/// List available base images
+pub async fn list_images(config: web::Data<AppConfig>) -> Result<HttpResponse> {
+    info!("Received request to list base images");
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.list_base_images() {
+        Ok(images) => {
+            let response = ListImagesResponse {
+                images: images
+                    .iter()
+                    .map(|name| ImageInfo { name: name.clone() })
+                    .collect(),
+            };
+            Ok(HttpResponse::Ok().json(response))
+        }
+        Err(e) => {
+            error!("Failed to list base images: {}", e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to list images: {}", e)
+            })))
+        }
+    }
+}
diff --git a/workflows/rcloud/src/api/handlers/metrics.rs b/workflows/rcloud/src/api/handlers/metrics.rs
new file mode 100644
index 00000000..0ea55ca6
--- /dev/null
+++ b/workflows/rcloud/src/api/handlers/metrics.rs
@@ -0,0 +1,15 @@
+use actix_web::{web, HttpResponse, Result};
+use tracing::info;
+
+use crate::metrics::Metrics;
+
+/// Prometheus metrics endpoint
+pub async fn metrics_handler(metrics: web::Data<Metrics>) -> Result<HttpResponse> {
+    info!("Metrics endpoint accessed");
+
+    let metrics_text = metrics.encode();
+
+    Ok(HttpResponse::Ok()
+        .content_type("text/plain; version=0.0.4")
+        .body(metrics_text))
+}
diff --git a/workflows/rcloud/src/api/handlers/mod.rs b/workflows/rcloud/src/api/handlers/mod.rs
new file mode 100644
index 00000000..d0240653
--- /dev/null
+++ b/workflows/rcloud/src/api/handlers/mod.rs
@@ -0,0 +1,4 @@
+pub mod health;
+pub mod images;
+pub mod metrics;
+pub mod vms;
diff --git a/workflows/rcloud/src/api/handlers/vms.rs b/workflows/rcloud/src/api/handlers/vms.rs
new file mode 100644
index 00000000..1277e1e4
--- /dev/null
+++ b/workflows/rcloud/src/api/handlers/vms.rs
@@ -0,0 +1,172 @@
+use actix_web::{web, HttpResponse, Result};
+use tracing::{error, info};
+
+use crate::api::models::{CreateVmRequest, CreateVmResponse, ListVmsResponse, VmResponse};
+use crate::config::AppConfig;
+use crate::vm::{VmManager, VmSpec};
+
+/// Create a new VM
+pub async fn create_vm(
+    config: web::Data<AppConfig>,
+    req: web::Json<CreateVmRequest>,
+) -> Result<HttpResponse> {
+    info!("Received request to create VM: {}", req.name);
+
+    let vm_spec = VmSpec {
+        name: req.name.clone(),
+        vcpus: req.vcpus,
+        memory_mb: req.memory_mb,
+        base_image: req.base_image.clone(),
+        root_disk_gb: req.root_disk_gb,
+        ssh_user: req.ssh_user.clone(),
+        ssh_public_key: req.ssh_public_key.clone(),
+    };
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.create_vm(&vm_spec) {
+        Ok(vm_id) => {
+            info!("Successfully created VM {} with ID {}", req.name, vm_id);
+            let response = CreateVmResponse {
+                id: vm_id,
+                name: req.name.clone(),
+                state: "creating".to_string(),
+            };
+            Ok(HttpResponse::Created().json(response))
+        }
+        Err(e) => {
+            error!("Failed to create VM {}: {}", req.name, e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to create VM: {}", e)
+            })))
+        }
+    }
+}
+
+/// List all VMs
+pub async fn list_vms(config: web::Data<AppConfig>) -> Result<HttpResponse> {
+    info!("Received request to list VMs");
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.list_vms() {
+        Ok(vms) => {
+            let response = ListVmsResponse {
+                vms: vms
+                    .iter()
+                    .map(|vm| VmResponse {
+                        id: vm.id.clone(),
+                        name: vm.name.clone(),
+                        state: vm.state.clone(),
+                        vcpus: vm.vcpus,
+                        memory_mb: vm.memory_mb,
+                        ip_address: vm.ip_address.clone(),
+                    })
+                    .collect(),
+            };
+            Ok(HttpResponse::Ok().json(response))
+        }
+        Err(e) => {
+            error!("Failed to list VMs: {}", e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to list VMs: {}", e)
+            })))
+        }
+    }
+}
+
+/// Get VM details
+pub async fn get_vm(
+    config: web::Data<AppConfig>,
+    vm_id: web::Path<String>,
+) -> Result<HttpResponse> {
+    info!("Received request to get VM: {}", vm_id);
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.get_vm(&vm_id) {
+        Ok(vm) => {
+            let response = VmResponse {
+                id: vm.id,
+                name: vm.name,
+                state: vm.state,
+                vcpus: vm.vcpus,
+                memory_mb: vm.memory_mb,
+                ip_address: vm.ip_address,
+            };
+            Ok(HttpResponse::Ok().json(response))
+        }
+        Err(e) => {
+            error!("Failed to get VM {}: {}", vm_id, e);
+            Ok(HttpResponse::NotFound().json(serde_json::json!({
+                "error": format!("VM not found: {}", e)
+            })))
+        }
+    }
+}
+
+/// Start a VM
+pub async fn start_vm(
+    config: web::Data<AppConfig>,
+    vm_id: web::Path<String>,
+) -> Result<HttpResponse> {
+    info!("Received request to start VM: {}", vm_id);
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.start_vm(&vm_id) {
+        Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({
+            "status": "started"
+        }))),
+        Err(e) => {
+            error!("Failed to start VM {}: {}", vm_id, e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to start VM: {}", e)
+            })))
+        }
+    }
+}
+
+/// Stop a VM
+pub async fn stop_vm(
+    config: web::Data<AppConfig>,
+    vm_id: web::Path<String>,
+) -> Result<HttpResponse> {
+    info!("Received request to stop VM: {}", vm_id);
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.stop_vm(&vm_id) {
+        Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({
+            "status": "stopped"
+        }))),
+        Err(e) => {
+            error!("Failed to stop VM {}: {}", vm_id, e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to stop VM: {}", e)
+            })))
+        }
+    }
+}
+
+/// Destroy a VM
+pub async fn destroy_vm(
+    config: web::Data<AppConfig>,
+    vm_id: web::Path<String>,
+) -> Result<HttpResponse> {
+    info!("Received request to destroy VM: {}", vm_id);
+
+    let manager = VmManager::new(config.as_ref().clone());
+
+    match manager.destroy_vm(&vm_id) {
+        Ok(_) => Ok(HttpResponse::Ok().json(serde_json::json!({
+            "status": "destroyed"
+        }))),
+        Err(e) => {
+            error!("Failed to destroy VM {}: {}", vm_id, e);
+            Ok(HttpResponse::InternalServerError().json(serde_json::json!({
+                "error": format!("Failed to destroy VM: {}", e)
+            })))
+        }
+    }
+}
diff --git a/workflows/rcloud/src/api/mod.rs b/workflows/rcloud/src/api/mod.rs
new file mode 100644
index 00000000..544ebd36
--- /dev/null
+++ b/workflows/rcloud/src/api/mod.rs
@@ -0,0 +1,3 @@
+pub mod handlers;
+pub mod models;
+pub mod routes;
diff --git a/workflows/rcloud/src/api/models.rs b/workflows/rcloud/src/api/models.rs
new file mode 100644
index 00000000..1624a096
--- /dev/null
+++ b/workflows/rcloud/src/api/models.rs
@@ -0,0 +1,55 @@
+use serde::{Deserialize, Serialize};
+
+/// Request to create a new VM
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateVmRequest {
+    pub name: String,
+    pub vcpus: u32,
+    pub memory_mb: u64,
+    pub base_image: String,
+    pub root_disk_gb: u64,
+    /// Optional SSH username to create (defaults to config if not provided)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ssh_user: Option<String>,
+    /// Optional SSH public key content to inject (defaults to config if not provided)
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ssh_public_key: Option<String>,
+}
+
+/// Response after creating a VM
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CreateVmResponse {
+    pub id: String,
+    pub name: String,
+    pub state: String,
+}
+
+/// VM details response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct VmResponse {
+    pub id: String,
+    pub name: String,
+    pub state: String,
+    pub vcpus: u32,
+    pub memory_mb: u64,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ip_address: Option<String>,
+}
+
+/// List of VMs response
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ListVmsResponse {
+    pub vms: Vec<VmResponse>,
+}
+
+/// Base image information
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ImageInfo {
+    pub name: String,
+}
+
+/// List of base images
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ListImagesResponse {
+    pub images: Vec<ImageInfo>,
+}
diff --git a/workflows/rcloud/src/api/routes.rs b/workflows/rcloud/src/api/routes.rs
new file mode 100644
index 00000000..84cc5239
--- /dev/null
+++ b/workflows/rcloud/src/api/routes.rs
@@ -0,0 +1,24 @@
+use actix_web::web;
+
+use super::handlers::{health, images, metrics, vms};
+
+/// Configure all API routes
+pub fn configure_routes(cfg: &mut web::ServiceConfig) {
+    cfg.service(
+        web::scope("/api/v1")
+            // Health endpoints
+            .route("/health", web::get().to(health::health_check))
+            .route("/status", web::get().to(health::system_status))
+            // VM endpoints
+            .route("/vms", web::post().to(vms::create_vm))
+            .route("/vms", web::get().to(vms::list_vms))
+            .route("/vms/{id}", web::get().to(vms::get_vm))
+            .route("/vms/{id}", web::delete().to(vms::destroy_vm))
+            .route("/vms/{id}/start", web::post().to(vms::start_vm))
+            .route("/vms/{id}/stop", web::post().to(vms::stop_vm))
+            // Image endpoints
+            .route("/images", web::get().to(images::list_images)),
+    )
+    // Metrics endpoint (outside /api/v1 scope, standard Prometheus path)
+    .route("/metrics", web::get().to(metrics::metrics_handler));
+}
diff --git a/workflows/rcloud/src/config/kdevops.rs b/workflows/rcloud/src/config/kdevops.rs
new file mode 100644
index 00000000..3949567e
--- /dev/null
+++ b/workflows/rcloud/src/config/kdevops.rs
@@ -0,0 +1,121 @@
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::fs;
+use std::path::Path;
+
+/// kdevops configuration parsed from extra_vars.yaml
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KdevopsConfig {
+    pub libvirt_uri: String,
+    pub storage_pool_path: String,
+    pub base_images_dir: String,
+    pub network_bridge: Option<String>,
+    pub rcloud_server_bind: Option<String>,
+    pub rcloud_workers: Option<usize>,
+    pub ssh_user: Option<String>,
+    pub ssh_pubkey_file: Option<String>,
+
+    #[serde(flatten)]
+    pub extra: HashMap<String, serde_yaml::Value>,
+}
+
+impl KdevopsConfig {
+    /// Load configuration from kdevops directory
+    pub fn load(kdevops_root: &Path) -> Result<Self> {
+        let extra_vars_path = kdevops_root.join("extra_vars.yaml");
+
+        let contents = fs::read_to_string(&extra_vars_path)
+            .with_context(|| format!("Failed to read {}", extra_vars_path.display()))?;
+
+        let yaml: serde_yaml::Value =
+            serde_yaml::from_str(&contents).context("Failed to parse extra_vars.yaml")?;
+
+        // Try environment variables first, then fall back to extra_vars.yaml
+        let storage_pool_path = std::env::var("RCLOUD_STORAGE_POOL_PATH")
+            .ok()
+            .or_else(|| Self::get_string(&yaml, "kdevops_storage_pool_path"))
+            .or_else(|| Self::get_string(&yaml, "libvirt_storage_pool_path"))
+            .context("Missing storage pool path. Set RCLOUD_STORAGE_POOL_PATH env var or kdevops_storage_pool_path in extra_vars.yaml")?;
+
+        let base_images_dir = std::env::var("RCLOUD_BASE_IMAGES_DIR")
+            .ok()
+            .or_else(|| Self::get_string(&yaml, "guestfs_base_image_dir"))
+            .context("Missing base images directory. Set RCLOUD_BASE_IMAGES_DIR env var or guestfs_base_image_dir in extra_vars.yaml")?;
+
+        // Extract configuration values
+        let config = Self {
+            libvirt_uri: std::env::var("RCLOUD_LIBVIRT_URI")
+                .ok()
+                .or_else(|| Self::get_string(&yaml, "libvirt_uri"))
+                .unwrap_or_else(|| "qemu:///system".to_string()),
+
+            storage_pool_path,
+            base_images_dir,
+
+            network_bridge: std::env::var("RCLOUD_NETWORK_BRIDGE")
+                .ok()
+                .or_else(|| Self::get_string(&yaml, "libvirt_bridge_name"))
+                .or_else(|| Some("virbr0".to_string())),
+
+            rcloud_server_bind: std::env::var("RCLOUD_SERVER_BIND")
+                .ok()
+                .or_else(|| Self::get_string(&yaml, "rcloud_server_bind")),
+
+            rcloud_workers: std::env::var("RCLOUD_WORKERS")
+                .ok()
+                .and_then(|s| s.parse().ok())
+                .or_else(|| Self::get_usize(&yaml, "rcloud_workers")),
+
+            ssh_user: Self::get_string(&yaml, "kdevops_terraform_ssh_config_user"),
+            ssh_pubkey_file: Self::get_string(&yaml, "kdevops_terraform_ssh_config_pubkey_file"),
+
+            extra: serde_yaml::from_value(yaml).context("Failed to parse extra configuration")?,
+        };
+
+        Ok(config)
+    }
+
+    /// Helper to extract string value from YAML
+    fn get_string(yaml: &serde_yaml::Value, key: &str) -> Option<String> {
+        yaml.get(key)
+            .and_then(|v| v.as_str())
+            .map(|s| s.to_string())
+    }
+
+    /// Helper to extract usize value from YAML
+    fn get_usize(yaml: &serde_yaml::Value, key: &str) -> Option<usize> {
+        yaml.get(key).and_then(|v| v.as_u64()).map(|n| n as usize)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::Write;
+    use tempfile::TempDir;
+
+    #[test]
+    fn test_load_kdevops_config() {
+        let temp_dir = TempDir::new().unwrap();
+        let extra_vars_path = temp_dir.path().join("extra_vars.yaml");
+
+        let yaml_content = r#"
+libvirt_uri: "qemu:///system"
+kdevops_storage_pool_path: "/xfs1/libvirt/kdevops"
+guestfs_base_image_dir: "/xfs1/libvirt/guestfs/base_images"
+libvirt_bridge_name: "virbr0"
+"#;
+
+        let mut file = fs::File::create(&extra_vars_path).unwrap();
+        file.write_all(yaml_content.as_bytes()).unwrap();
+        drop(file);
+
+        let config = KdevopsConfig::load(temp_dir.path()).unwrap();
+
+        assert_eq!(config.libvirt_uri, "qemu:///system");
+        assert_eq!(config.storage_pool_path, "/xfs1/libvirt/kdevops");
+        assert_eq!(config.base_images_dir, "/xfs1/libvirt/guestfs/base_images");
+        assert_eq!(config.network_bridge, Some("virbr0".to_string()));
+    }
+}
diff --git a/workflows/rcloud/src/config/mod.rs b/workflows/rcloud/src/config/mod.rs
new file mode 100644
index 00000000..1a7f6e61
--- /dev/null
+++ b/workflows/rcloud/src/config/mod.rs
@@ -0,0 +1,117 @@
+pub mod kdevops;
+
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+/// Main application configuration
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AppConfig {
+    /// Server configuration
+    pub server: ServerConfig,
+
+    /// kdevops root directory
+    pub kdevops_root: PathBuf,
+
+    /// Libvirt connection URI
+    pub libvirt_uri: String,
+
+    /// Storage pool path for VMs
+    pub storage_pool_path: PathBuf,
+
+    /// Base images directory (from guestfs)
+    pub base_images_dir: PathBuf,
+
+    /// Network bridge name
+    pub network_bridge: String,
+
+    /// SSH user for VM access
+    pub ssh_user: Option<String>,
+
+    /// SSH public key file for VM access
+    pub ssh_pubkey_file: Option<String>,
+
+    /// VM defaults
+    pub vm_defaults: VmDefaults,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ServerConfig {
+    pub bind_address: String,
+    pub workers: usize,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct VmDefaults {
+    pub disk_format: String,
+    pub disk_driver: String,
+}
+
+impl Default for ServerConfig {
+    fn default() -> Self {
+        Self {
+            bind_address: "127.0.0.1:8765".to_string(),
+            workers: 4,
+        }
+    }
+}
+
+impl Default for VmDefaults {
+    fn default() -> Self {
+        Self {
+            disk_format: "raw".to_string(),
+            disk_driver: "virtio".to_string(),
+        }
+    }
+}
+
+impl AppConfig {
+    /// Load configuration from environment and kdevops
+    pub fn load() -> Result<Self> {
+        // Determine kdevops root
+        let kdevops_root = std::env::var("KDEVOPS_ROOT")
+            .map(PathBuf::from)
+            .or_else(|_| std::env::current_dir())
+            .context("Failed to determine kdevops root directory")?;
+
+        // Load kdevops configuration
+        let kdevops_config = kdevops::KdevopsConfig::load(&kdevops_root)
+            .context("Failed to load kdevops configuration")?;
+
+        // Build server config from kdevops config or defaults
+        let server = ServerConfig {
+            bind_address: kdevops_config
+                .rcloud_server_bind
+                .clone()
+                .unwrap_or_else(|| ServerConfig::default().bind_address),
+            workers: kdevops_config
+                .rcloud_workers
+                .unwrap_or_else(|| ServerConfig::default().workers),
+        };
+
+        // Build application config from kdevops config
+        let config = Self {
+            server,
+            kdevops_root: kdevops_root.clone(),
+            libvirt_uri: kdevops_config.libvirt_uri.clone(),
+            storage_pool_path: PathBuf::from(&kdevops_config.storage_pool_path),
+            base_images_dir: PathBuf::from(&kdevops_config.base_images_dir),
+            network_bridge: kdevops_config
+                .network_bridge
+                .clone()
+                .unwrap_or_else(|| "default".to_string()),
+            ssh_user: kdevops_config.ssh_user.clone(),
+            ssh_pubkey_file: kdevops_config.ssh_pubkey_file.clone(),
+            vm_defaults: VmDefaults::default(),
+        };
+
+        Ok(config)
+    }
+
+    /// Get path to libvirt XML template
+    #[allow(dead_code)]
+    pub fn xml_template_path(&self) -> PathBuf {
+        self.kdevops_root
+            .join("playbooks/roles/gen_nodes/templates/guestfs_q35.j2.xml")
+    }
+}
diff --git a/workflows/rcloud/src/lib.rs b/workflows/rcloud/src/lib.rs
new file mode 100644
index 00000000..5678be12
--- /dev/null
+++ b/workflows/rcloud/src/lib.rs
@@ -0,0 +1,4 @@
+pub mod api;
+pub mod config;
+pub mod metrics;
+pub mod vm;
diff --git a/workflows/rcloud/src/main.rs b/workflows/rcloud/src/main.rs
new file mode 100644
index 00000000..79f1b553
--- /dev/null
+++ b/workflows/rcloud/src/main.rs
@@ -0,0 +1,64 @@
+use actix_web::{middleware, web, App, HttpServer};
+use tracing::{info, Level};
+use tracing_subscriber::FmtSubscriber;
+
+mod api;
+mod config;
+mod metrics;
+mod vm;
+
+use crate::api::routes::configure_routes;
+use crate::config::AppConfig;
+use crate::metrics::Metrics;
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+    // Initialize tracing
+    let subscriber = FmtSubscriber::builder()
+        .with_max_level(Level::INFO)
+        .with_target(false)
+        .with_thread_ids(true)
+        .with_file(true)
+        .with_line_number(true)
+        .json()
+        .finish();
+
+    tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber");
+
+    info!("Starting rcloud API server");
+
+    // Load configuration
+    let config = AppConfig::load().expect("Failed to load configuration");
+    info!("Loaded configuration from: {:?}", config.kdevops_root);
+    info!("Libvirt URI: {}", config.libvirt_uri);
+    info!("Storage pool: {}", config.storage_pool_path.display());
+
+    let bind_addr = config.server.bind_address.clone();
+    let workers = config.server.workers;
+
+    info!("Starting HTTP server on {}", bind_addr);
+
+    // Create shared application state
+    let app_state = web::Data::new(config);
+
+    // Create metrics
+    let metrics = web::Data::new(Metrics::new());
+    info!("Initialized Prometheus metrics");
+
+    // Start HTTP server
+    HttpServer::new(move || {
+        App::new()
+            .app_data(app_state.clone())
+            .app_data(metrics.clone())
+            // Middleware
+            .wrap(middleware::Logger::default())
+            .wrap(middleware::Compress::default())
+            .wrap(tracing_actix_web::TracingLogger::default())
+            // Configure routes
+            .configure(configure_routes)
+    })
+    .workers(workers)
+    .bind(&bind_addr)?
+    .run()
+    .await
+}
diff --git a/workflows/rcloud/src/metrics.rs b/workflows/rcloud/src/metrics.rs
new file mode 100644
index 00000000..7ddd1077
--- /dev/null
+++ b/workflows/rcloud/src/metrics.rs
@@ -0,0 +1,100 @@
+use prometheus_client::encoding::text::encode;
+use prometheus_client::encoding::EncodeLabelSet;
+use prometheus_client::metrics::counter::Counter;
+use prometheus_client::metrics::family::Family;
+use prometheus_client::metrics::gauge::Gauge;
+use prometheus_client::registry::Registry;
+use std::sync::{Arc, Mutex};
+
+/// Metrics labels for API endpoints
+#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
+pub struct ApiLabels {
+    pub method: String,
+    pub endpoint: String,
+    pub status: String,
+}
+
+/// Application metrics
+#[allow(dead_code)]
+pub struct Metrics {
+    registry: Arc<Mutex<Registry>>,
+    http_requests_total: Family<ApiLabels, Counter>,
+    vm_count: Gauge,
+    vm_operations_total: Family<ApiLabels, Counter>,
+}
+
+impl Metrics {
+    pub fn new() -> Self {
+        let mut registry = Registry::default();
+
+        // HTTP request counter
+        let http_requests_total = Family::<ApiLabels, Counter>::default();
+        registry.register(
+            "rcloud_http_requests_total",
+            "Total number of HTTP requests",
+            http_requests_total.clone(),
+        );
+
+        // VM count gauge
+        let vm_count = Gauge::default();
+        registry.register("rcloud_vm_count", "Current number of VMs", vm_count.clone());
+
+        // VM operations counter
+        let vm_operations_total = Family::<ApiLabels, Counter>::default();
+        registry.register(
+            "rcloud_vm_operations_total",
+            "Total number of VM operations",
+            vm_operations_total.clone(),
+        );
+
+        Self {
+            registry: Arc::new(Mutex::new(registry)),
+            http_requests_total,
+            vm_count,
+            vm_operations_total,
+        }
+    }
+
+    /// Record an HTTP request
+    #[allow(dead_code)]
+    pub fn record_request(&self, method: &str, endpoint: &str, status: &str) {
+        let labels = ApiLabels {
+            method: method.to_string(),
+            endpoint: endpoint.to_string(),
+            status: status.to_string(),
+        };
+        self.http_requests_total.get_or_create(&labels).inc();
+    }
+
+    /// Update VM count
+    #[allow(dead_code)]
+    pub fn set_vm_count(&self, count: i64) {
+        self.vm_count.set(count);
+    }
+
+    /// Record a VM operation
+    #[allow(dead_code)]
+    pub fn record_vm_operation(&self, operation: &str, status: &str) {
+        let labels = ApiLabels {
+            method: "VM".to_string(),
+            endpoint: operation.to_string(),
+            status: status.to_string(),
+        };
+        self.vm_operations_total.get_or_create(&labels).inc();
+    }
+
+    /// Encode metrics in Prometheus text format
+    pub fn encode(&self) -> String {
+        let mut buffer = String::new();
+        if let Ok(registry) = self.registry.lock() {
+            let _ = encode(&mut buffer, &registry);
+        }
+        buffer
+    }
+}
+
+impl Default for Metrics {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/workflows/rcloud/src/vm/disk.rs b/workflows/rcloud/src/vm/disk.rs
new file mode 100644
index 00000000..225de0a2
--- /dev/null
+++ b/workflows/rcloud/src/vm/disk.rs
@@ -0,0 +1,66 @@
+use anyhow::{Context, Result};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use tracing::info;
+
+/// Create a COW (copy-on-write) disk from a base image
+pub fn create_cow_disk(base_image: &Path, target_disk: &Path, size_gb: u64) -> Result<()> {
+    info!(
+        "Creating COW disk: {} (size: {}G, base: {})",
+        target_disk.display(),
+        size_gb,
+        base_image.display()
+    );
+
+    // Ensure parent directory exists
+    if let Some(parent) = target_disk.parent() {
+        std::fs::create_dir_all(parent)
+            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
+    }
+
+    // Use qemu-img to create COW disk
+    // qemu-img create -f qcow2 -b base_image.raw -F raw target.qcow2 size
+    let size_str = format!("{}G", size_gb);
+
+    let output = Command::new("qemu-img")
+        .arg("create")
+        .arg("-f")
+        .arg("qcow2")
+        .arg("-b")
+        .arg(base_image)
+        .arg("-F")
+        .arg("raw")
+        .arg(target_disk)
+        .arg(&size_str)
+        .output()
+        .context("Failed to execute qemu-img")?;
+
+    if !output.status.success() {
+        let stderr = String::from_utf8_lossy(&output.stderr);
+        anyhow::bail!("qemu-img failed: {}", stderr);
+    }
+
+    info!("Successfully created COW disk at {}", target_disk.display());
+    Ok(())
+}
+
+/// Delete a disk file
+pub fn delete_disk(disk_path: &Path) -> Result<()> {
+    if disk_path.exists() {
+        info!("Deleting disk: {}", disk_path.display());
+        std::fs::remove_file(disk_path)
+            .with_context(|| format!("Failed to delete disk {}", disk_path.display()))?;
+        info!("Successfully deleted disk");
+    }
+    Ok(())
+}
+
+/// Get the VM disk directory path
+pub fn get_vm_disk_dir(storage_pool: &Path, vm_name: &str) -> PathBuf {
+    storage_pool.join(vm_name)
+}
+
+/// Get the root disk path for a VM
+pub fn get_root_disk_path(storage_pool: &Path, vm_name: &str) -> PathBuf {
+    get_vm_disk_dir(storage_pool, vm_name).join("root.qcow2")
+}
diff --git a/workflows/rcloud/src/vm/manager.rs b/workflows/rcloud/src/vm/manager.rs
new file mode 100644
index 00000000..2c79caaa
--- /dev/null
+++ b/workflows/rcloud/src/vm/manager.rs
@@ -0,0 +1,520 @@
+use anyhow::{Context, Result};
+use tracing::{info, warn};
+use uuid::Uuid;
+use virt::connect::Connect;
+use virt::domain::Domain;
+
+use super::disk;
+use super::xml;
+use crate::config::AppConfig;
+
+/// VM specification for creation
+#[derive(Debug, Clone)]
+pub struct VmSpec {
+    pub name: String,
+    pub vcpus: u32,
+    pub memory_mb: u64,
+    pub base_image: String,
+    pub root_disk_gb: u64,
+    /// Optional SSH username (falls back to config if not provided)
+    pub ssh_user: Option<String>,
+    /// Optional SSH public key content (falls back to config if not provided)
+    pub ssh_public_key: Option<String>,
+}
+
+/// VM Manager handles VM lifecycle operations via libvirt
+pub struct VmManager {
+    config: AppConfig,
+}
+
+impl VmManager {
+    pub fn new(config: AppConfig) -> Self {
+        Self { config }
+    }
+
+    /// Connect to libvirt
+    fn connect(&self) -> Result<Connect> {
+        info!("Connecting to libvirt at {}", self.config.libvirt_uri);
+        Connect::open(Some(&self.config.libvirt_uri)).with_context(|| {
+            format!(
+                "Failed to connect to libvirt at {}",
+                self.config.libvirt_uri
+            )
+        })
+    }
+
+    /// Create a new VM
+    pub fn create_vm(&self, spec: &VmSpec) -> Result<String> {
+        let vm_id = Uuid::new_v4().to_string();
+        info!("Creating VM {} with ID {}", spec.name, vm_id);
+
+        // Validate base image exists
+        let base_image_path = self.config.base_images_dir.join(&spec.base_image);
+        if !base_image_path.exists() {
+            anyhow::bail!(
+                "Base image not found: {}. Available images in {}",
+                spec.base_image,
+                self.config.base_images_dir.display()
+            );
+        }
+
+        // Create VM disk directory and path
+        let disk_path = disk::get_root_disk_path(&self.config.storage_pool_path, &spec.name);
+        let vm_dir = disk::get_vm_disk_dir(&self.config.storage_pool_path, &spec.name);
+
+        info!("VM disk will be created at: {}", disk_path.display());
+        info!("Using base image: {}", base_image_path.display());
+
+        // Create COW disk from base image
+        if let Err(e) = disk::create_cow_disk(&base_image_path, &disk_path, spec.root_disk_gb) {
+            // Disk creation failed - no cleanup needed
+            return Err(e).context("Failed to create VM disk");
+        }
+
+        // Customize COW disk with target user and SSH keys
+        if let Err(e) = self.customize_vm_disk(
+            &disk_path,
+            spec.ssh_user.as_deref(),
+            spec.ssh_public_key.as_deref(),
+        ) {
+            // Customization failed - clean up disk
+            warn!("Failed to customize VM disk, cleaning up");
+            if let Err(cleanup_err) = disk::delete_disk(&disk_path) {
+                warn!("Failed to delete disk during cleanup: {}", cleanup_err);
+            }
+            if vm_dir.exists() {
+                if let Err(cleanup_err) = std::fs::remove_dir(&vm_dir) {
+                    warn!(
+                        "Failed to remove VM directory during cleanup: {}",
+                        cleanup_err
+                    );
+                }
+            }
+            return Err(e).context("Failed to customize VM disk");
+        }
+
+        // Determine if UEFI is required based on image name
+        // Debian nocloud images and other cloud images typically require UEFI
+        let requires_uefi = spec.base_image.contains("nocloud")
+            || spec.base_image.contains("uefi")
+            || spec.base_image.contains("cloud");
+
+        // Generate libvirt XML
+        let vm_xml = xml::generate_simple_vm_xml(
+            &spec.name,
+            spec.vcpus,
+            spec.memory_mb,
+            &disk_path,
+            &self.config.network_bridge,
+            requires_uefi,
+        );
+
+        // Define VM in libvirt
+        let conn = self.connect()?;
+        let domain = match Domain::define_xml(&conn, &vm_xml) {
+            Ok(domain) => domain,
+            Err(e) => {
+                // Libvirt define failed - clean up disk
+                warn!("Failed to define VM in libvirt, cleaning up disk");
+                if let Err(cleanup_err) = disk::delete_disk(&disk_path) {
+                    warn!("Failed to delete disk during cleanup: {}", cleanup_err);
+                }
+                if vm_dir.exists() {
+                    if let Err(cleanup_err) = std::fs::remove_dir(&vm_dir) {
+                        warn!(
+                            "Failed to remove VM directory during cleanup: {}",
+                            cleanup_err
+                        );
+                    }
+                }
+                return Err(e).context("Failed to define VM in libvirt");
+            }
+        };
+
+        // Start the VM
+        if let Err(e) = domain.create() {
+            // VM start failed - clean up libvirt domain and disk
+            warn!("Failed to start VM, cleaning up domain and disk");
+            // Use undefine_flags(4) to clean up NVRAM for UEFI VMs
+            if let Err(cleanup_err) = domain.undefine_flags(4) {
+                warn!("Failed to undefine domain during cleanup: {}", cleanup_err);
+            }
+            if let Err(cleanup_err) = disk::delete_disk(&disk_path) {
+                warn!("Failed to delete disk during cleanup: {}", cleanup_err);
+            }
+            if vm_dir.exists() {
+                if let Err(cleanup_err) = std::fs::remove_dir(&vm_dir) {
+                    warn!(
+                        "Failed to remove VM directory during cleanup: {}",
+                        cleanup_err
+                    );
+                }
+            }
+            return Err(e).context("Failed to start VM");
+        }
+
+        let uuid = domain.get_uuid_string().context("Failed to get VM UUID")?;
+
+        info!(
+            "Successfully created and started VM {} with UUID {}",
+            spec.name, uuid
+        );
+        Ok(uuid)
+    }
+
+    /// List all VMs
+    pub fn list_vms(&self) -> Result<Vec<VmInfo>> {
+        let conn = self.connect()?;
+
+        let domains = conn.list_all_domains(0).context("Failed to list domains")?;
+
+        let mut vms = Vec::new();
+        for domain in domains {
+            if let Ok(info) = self.get_vm_info_from_domain(&domain) {
+                vms.push(info);
+            }
+        }
+
+        Ok(vms)
+    }
+
+    /// Get VM information
+    pub fn get_vm(&self, id: &str) -> Result<VmInfo> {
+        let conn = self.connect()?;
+
+        // Try to find domain by UUID or name
+        let domain = Domain::lookup_by_uuid_string(&conn, id)
+            .or_else(|_| Domain::lookup_by_name(&conn, id))
+            .context("VM not found")?;
+
+        self.get_vm_info_from_domain(&domain)
+    }
+
+    /// Start a VM
+    pub fn start_vm(&self, id: &str) -> Result<()> {
+        info!("Starting VM {}", id);
+        let conn = self.connect()?;
+        let domain = Domain::lookup_by_uuid_string(&conn, id)
+            .or_else(|_| Domain::lookup_by_name(&conn, id))
+            .context("VM not found")?;
+
+        domain.create().context("Failed to start VM")?;
+        info!("VM {} started successfully", id);
+        Ok(())
+    }
+
+    /// Stop a VM
+    pub fn stop_vm(&self, id: &str) -> Result<()> {
+        info!("Stopping VM {}", id);
+        let conn = self.connect()?;
+        let domain = Domain::lookup_by_uuid_string(&conn, id)
+            .or_else(|_| Domain::lookup_by_name(&conn, id))
+            .context("VM not found")?;
+
+        domain.shutdown().context("Failed to stop VM")?;
+        info!("VM {} stopped successfully", id);
+        Ok(())
+    }
+
+    /// Destroy a VM
+    pub fn destroy_vm(&self, id: &str) -> Result<()> {
+        info!("Destroying VM {}", id);
+        let conn = self.connect()?;
+        let domain = Domain::lookup_by_uuid_string(&conn, id)
+            .or_else(|_| Domain::lookup_by_name(&conn, id))
+            .context("VM not found")?;
+
+        // Get VM name for disk cleanup
+        let vm_name = domain.get_name().context("Failed to get VM name")?;
+
+        // Stop if running
+        if domain.is_active().unwrap_or(false) {
+            domain.destroy().context("Failed to forcefully stop VM")?;
+        }
+
+        // Undefine domain with NVRAM cleanup for UEFI VMs
+        // VIR_DOMAIN_UNDEFINE_NVRAM = 4
+        // This flag removes NVRAM files for UEFI VMs and is safe for non-UEFI VMs
+        domain.undefine_flags(4).context("Failed to undefine VM")?;
+
+        // Delete disk files
+        let disk_path = disk::get_root_disk_path(&self.config.storage_pool_path, &vm_name);
+        if let Err(e) = disk::delete_disk(&disk_path) {
+            warn!("Failed to delete disk {}: {}", disk_path.display(), e);
+        }
+
+        // Try to remove VM directory
+        let vm_dir = disk::get_vm_disk_dir(&self.config.storage_pool_path, &vm_name);
+        if vm_dir.exists() {
+            if let Err(e) = std::fs::remove_dir(&vm_dir) {
+                warn!("Failed to remove VM directory {}: {}", vm_dir.display(), e);
+            }
+        }
+
+        info!("VM {} destroyed successfully", id);
+        Ok(())
+    }
+
+    /// Customize VM disk with target user and SSH configuration
+    fn customize_vm_disk(
+        &self,
+        disk_path: &std::path::Path,
+        ssh_user_override: Option<&str>,
+        ssh_public_key_content: Option<&str>,
+    ) -> Result<()> {
+        use std::fs::File;
+        use std::io::Write;
+        use std::process::Command;
+
+        // Determine SSH user: API parameter takes precedence, then fall back to config
+        let ssh_user = if let Some(user) = ssh_user_override {
+            user
+        } else if let Some(user) = &self.config.ssh_user {
+            user.as_str()
+        } else {
+            info!("No ssh_user provided via API or configured, skipping disk customization");
+            return Ok(());
+        };
+
+        // Handle SSH public key: either from API content or from config file path
+        let temp_key_file;
+        let ssh_key_path = if let Some(key_content) = ssh_public_key_content {
+            // SSH key content provided via API - write to temporary file
+            info!("Using SSH public key content from API request");
+            temp_key_file =
+                std::env::temp_dir().join(format!("rcloud_ssh_key_{}.pub", uuid::Uuid::new_v4()));
+
+            let mut file = File::create(&temp_key_file).with_context(|| {
+                format!(
+                    "Failed to create temporary SSH key file: {}",
+                    temp_key_file.display()
+                )
+            })?;
+
+            file.write_all(key_content.as_bytes()).with_context(|| {
+                format!(
+                    "Failed to write SSH key to temporary file: {}",
+                    temp_key_file.display()
+                )
+            })?;
+
+            file.write_all(b"\n")
+                .context("Failed to write newline to temporary SSH key file")?;
+
+            info!(
+                "Wrote SSH public key to temporary file: {}",
+                temp_key_file.display()
+            );
+            temp_key_file.as_path()
+        } else if let Some(file) = &self.config.ssh_pubkey_file {
+            // Fall back to configured file path
+            info!("Using SSH public key from configured file: {}", file);
+            std::path::Path::new(file)
+        } else {
+            info!("No ssh_public_key provided via API or configured, skipping disk customization");
+            return Ok(());
+        };
+
+        info!(
+            "Customizing VM disk to add user {} with SSH key from {}",
+            ssh_user,
+            ssh_key_path.display()
+        );
+
+        // Build virt-customize command to add user and SSH configuration
+        let mut cmd = Command::new("virt-customize");
+        cmd.arg("-a").arg(disk_path);
+
+        // Create user with home directory
+        cmd.arg("--run-command")
+            .arg(format!("useradd -m -s /bin/bash {}", ssh_user));
+
+        // Add user to sudoers with NOPASSWD
+        cmd.arg("--run-command").arg(format!(
+            "echo '{} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/{}",
+            ssh_user, ssh_user
+        ));
+
+        // Set proper permissions on sudoers file
+        cmd.arg("--run-command")
+            .arg(format!("chmod 0440 /etc/sudoers.d/{}", ssh_user));
+
+        // Create .ssh directory for user
+        cmd.arg("--run-command")
+            .arg(format!("mkdir -p /home/{}/.ssh", ssh_user));
+
+        // Set proper permissions on .ssh directory
+        cmd.arg("--run-command")
+            .arg(format!("chmod 0700 /home/{}/.ssh", ssh_user));
+
+        // Inject SSH public key
+        cmd.arg("--ssh-inject")
+            .arg(format!("{}:file:{}", ssh_user, ssh_key_path.display()));
+
+        // Set proper ownership on home directory
+        cmd.arg("--run-command").arg(format!(
+            "chown -R {}:{} /home/{}",
+            ssh_user, ssh_user, ssh_user
+        ));
+
+        info!("Running virt-customize to configure user and SSH access");
+        let output = cmd.output().context("Failed to execute virt-customize")?;
+
+        // Clean up temporary key file if we created one
+        if ssh_public_key_content.is_some() {
+            if let Err(e) = std::fs::remove_file(ssh_key_path) {
+                warn!(
+                    "Failed to remove temporary SSH key file {}: {}",
+                    ssh_key_path.display(),
+                    e
+                );
+            } else {
+                info!(
+                    "Cleaned up temporary SSH key file: {}",
+                    ssh_key_path.display()
+                );
+            }
+        }
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            warn!("virt-customize failed: exit_code={}", output.status);
+            warn!("virt-customize stdout: {}", stdout);
+            warn!("virt-customize stderr: {}", stderr);
+            anyhow::bail!(
+                "virt-customize failed: exit_code={}, stdout={}, stderr={}",
+                output.status,
+                stdout,
+                stderr
+            );
+        }
+
+        info!(
+            "Successfully customized VM disk with user {} and SSH configuration",
+            ssh_user
+        );
+        Ok(())
+    }
+
+    /// Extract VM info from libvirt Domain
+    fn get_vm_info_from_domain(&self, domain: &Domain) -> Result<VmInfo> {
+        let uuid = domain
+            .get_uuid_string()
+            .context("Failed to get domain UUID")?;
+        let name = domain.get_name().context("Failed to get domain name")?;
+
+        let is_active = domain.is_active().unwrap_or(false);
+        let state = if is_active { "running" } else { "stopped" };
+
+        let info = domain.get_info().context("Failed to get domain info")?;
+
+        // Query IP address from libvirt (only works if VM is running)
+        let ip_address = if is_active {
+            match self.get_domain_ip_address(domain) {
+                Ok(ip) => Some(ip),
+                Err(e) => {
+                    warn!("Failed to get IP address for domain {}: {}", name, e);
+                    None
+                }
+            }
+        } else {
+            None
+        };
+
+        Ok(VmInfo {
+            id: uuid,
+            name,
+            state: state.to_string(),
+            vcpus: info.nr_virt_cpu,
+            memory_mb: info.memory / 1024, // Convert from KiB to MiB
+            ip_address,
+        })
+    }
+
+    /// Get IP address from domain using virsh domifaddr command
+    fn get_domain_ip_address(&self, domain: &Domain) -> Result<String> {
+        use std::process::Command;
+
+        let vm_name = domain.get_name().context("Failed to get VM name")?;
+
+        // Use virsh domifaddr to get IP directly from domain
+        // This works without needing a defined libvirt network
+        // Note: virsh requires elevated permissions to access qemu:///system
+        let output = Command::new("sudo")
+            .arg("virsh")
+            .arg("domifaddr")
+            .arg(&vm_name)
+            .output()
+            .context("Failed to execute virsh domifaddr")?;
+
+        if !output.status.success() {
+            let stderr = String::from_utf8_lossy(&output.stderr);
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            anyhow::bail!(
+                "Failed to query domain interfaces: stdout={}, stderr={}",
+                stdout,
+                stderr
+            );
+        }
+
+        self.parse_domifaddr_output(&String::from_utf8_lossy(&output.stdout))
+    }
+
+    /// Parse virsh domifaddr output to extract IP address
+    fn parse_domifaddr_output(&self, output: &str) -> Result<String> {
+        // Format:
+        // Name       MAC address          Protocol     Address
+        // -------------------------------------------------------------------------------
+        // vnet0      52:54:00:xx:xx:xx    ipv4         192.168.122.100/24
+
+        for line in output.lines().skip(2) {
+            // Skip header lines
+            let fields: Vec<&str> = line.split_whitespace().collect();
+            if fields.len() >= 4 && fields[2] == "ipv4" {
+                let ip_with_prefix = fields[3];
+                // Remove /24 or other CIDR suffix
+                if let Some(ip) = ip_with_prefix.split('/').next() {
+                    return Ok(ip.to_string());
+                }
+            }
+        }
+
+        anyhow::bail!("No IP address found for domain")
+    }
+
+    /// List available base images
+    pub fn list_base_images(&self) -> Result<Vec<String>> {
+        let mut images = Vec::new();
+
+        let entries = std::fs::read_dir(&self.config.base_images_dir).with_context(|| {
+            format!(
+                "Failed to read base images directory: {}",
+                self.config.base_images_dir.display()
+            )
+        })?;
+
+        for entry in entries.flatten() {
+            let path = entry.path();
+            if path.is_file() {
+                if let Some(filename) = path.file_name() {
+                    images.push(filename.to_string_lossy().to_string());
+                }
+            }
+        }
+
+        images.sort();
+        Ok(images)
+    }
+}
+
+/// VM information for API responses
+#[derive(Debug, Clone)]
+pub struct VmInfo {
+    pub id: String,
+    pub name: String,
+    pub state: String,
+    pub vcpus: u32,
+    pub memory_mb: u64,
+    pub ip_address: Option<String>,
+}
diff --git a/workflows/rcloud/src/vm/mod.rs b/workflows/rcloud/src/vm/mod.rs
new file mode 100644
index 00000000..6b6462be
--- /dev/null
+++ b/workflows/rcloud/src/vm/mod.rs
@@ -0,0 +1,5 @@
+pub mod disk;
+pub mod manager;
+pub mod xml;
+
+pub use manager::{VmManager, VmSpec};
diff --git a/workflows/rcloud/src/vm/xml.rs b/workflows/rcloud/src/vm/xml.rs
new file mode 100644
index 00000000..9946b09e
--- /dev/null
+++ b/workflows/rcloud/src/vm/xml.rs
@@ -0,0 +1,169 @@
+use anyhow::{Context, Result};
+use serde::Serialize;
+use std::path::Path;
+use tera::{Context as TeraContext, Tera};
+use tracing::info;
+
+/// Template context for VM XML generation
+#[derive(Debug, Serialize)]
+pub struct VmXmlContext {
+    pub hostname: String,
+    pub libvirt_mem_mb: u64,
+    pub libvirt_vcpus_count: u32,
+    pub kdevops_storage_pool_path: String,
+    pub guestfs_path: String,
+    pub libvirt_session_public_network_dev: String,
+    pub qemu_bin_path: String,
+    pub guestfs_requires_uefi: bool,
+    pub libvirt_host_passthrough: bool,
+    pub libvirt_enable_gdb: bool,
+}
+
+impl VmXmlContext {
+    #[allow(dead_code)]
+    pub fn new(
+        vm_name: &str,
+        vcpus: u32,
+        memory_mb: u64,
+        storage_pool_path: &Path,
+        network_bridge: &str,
+    ) -> Self {
+        Self {
+            hostname: vm_name.to_string(),
+            libvirt_mem_mb: memory_mb,
+            libvirt_vcpus_count: vcpus,
+            kdevops_storage_pool_path: storage_pool_path.display().to_string(),
+            guestfs_path: storage_pool_path.display().to_string(),
+            libvirt_session_public_network_dev: network_bridge.to_string(),
+            qemu_bin_path: "/usr/bin/qemu-system-x86_64".to_string(),
+            guestfs_requires_uefi: false,
+            libvirt_host_passthrough: true,
+            libvirt_enable_gdb: false,
+        }
+    }
+}
+
+/// Render libvirt XML from guestfs template
+#[allow(dead_code)]
+pub fn render_vm_xml(template_path: &Path, context: &VmXmlContext) -> Result<String> {
+    info!(
+        "Rendering VM XML from template: {}",
+        template_path.display()
+    );
+
+    // Load template
+    let template_str = std::fs::read_to_string(template_path)
+        .with_context(|| format!("Failed to read template {}", template_path.display()))?;
+
+    // Create Tera instance
+    let mut tera = Tera::default();
+    tera.add_raw_template("vm.xml", &template_str)
+        .context("Failed to parse template")?;
+
+    // Create context
+    let mut tera_context = TeraContext::new();
+    tera_context.insert("hostname", &context.hostname);
+    tera_context.insert("libvirt_mem_mb", &context.libvirt_mem_mb);
+    tera_context.insert("libvirt_vcpus_count", &context.libvirt_vcpus_count);
+    tera_context.insert(
+        "kdevops_storage_pool_path",
+        &context.kdevops_storage_pool_path,
+    );
+    tera_context.insert("guestfs_path", &context.guestfs_path);
+    tera_context.insert(
+        "libvirt_session_public_network_dev",
+        &context.libvirt_session_public_network_dev,
+    );
+    tera_context.insert("qemu_bin_path", &context.qemu_bin_path);
+    tera_context.insert("guestfs_requires_uefi", &context.guestfs_requires_uefi);
+    tera_context.insert(
+        "libvirt_host_passthrough",
+        &context.libvirt_host_passthrough,
+    );
+    tera_context.insert("libvirt_enable_gdb", &context.libvirt_enable_gdb);
+
+    // Render template
+    let xml = tera
+        .render("vm.xml", &tera_context)
+        .context("Failed to render template")?;
+
+    info!("Successfully rendered VM XML ({} bytes)", xml.len());
+    Ok(xml)
+}
+
+/// Simplified XML generation for testing (without template)
+pub fn generate_simple_vm_xml(
+    vm_name: &str,
+    vcpus: u32,
+    memory_mb: u64,
+    disk_path: &Path,
+    network_name: &str,
+    requires_uefi: bool,
+) -> String {
+    // Determine firmware configuration based on UEFI requirement
+    let (os_section, firmware_feature) = if requires_uefi {
+        (
+            r#"  <os firmware='efi'>
+    <type arch='x86_64' machine='q35'>hvm</type>
+    <boot dev='hd'/>
+  </os>"#,
+            "    <smm state='on'/>",
+        )
+    } else {
+        (
+            r#"  <os>
+    <type arch='x86_64' machine='q35'>hvm</type>
+    <boot dev='hd'/>
+  </os>"#,
+            "",
+        )
+    };
+
+    format!(
+        r#"<domain type='kvm'>
+  <name>{}</name>
+  <memory unit='MiB'>{}</memory>
+  <vcpu placement='static'>{}</vcpu>
+{}
+  <features>
+    <acpi/>
+    <apic/>
+{}
+  </features>
+  <cpu mode='host-passthrough'/>
+  <clock offset='localtime'/>
+  <on_poweroff>destroy</on_poweroff>
+  <on_reboot>restart</on_reboot>
+  <on_crash>destroy</on_crash>
+  <devices>
+    <emulator>/usr/bin/qemu-system-x86_64</emulator>
+    <disk type='file' device='disk'>
+      <driver name='qemu' type='qcow2' cache='none' io='native'/>
+      <source file='{}'/>
+      <target dev='vda' bus='virtio'/>
+    </disk>
+    <interface type='network'>
+      <source network='{}'/>
+      <model type='virtio'/>
+    </interface>
+    <serial type='pty'>
+      <target type='isa-serial' port='0'/>
+    </serial>
+    <console type='pty'>
+      <target type='serial' port='0'/>
+    </console>
+    <memballoon model='virtio'/>
+    <rng model='virtio'>
+      <backend model='random'>/dev/urandom</backend>
+    </rng>
+  </devices>
+</domain>"#,
+        vm_name,
+        memory_mb,
+        vcpus,
+        os_section,
+        firmware_feature,
+        disk_path.display(),
+        network_name
+    )
+}
diff --git a/workflows/rcloud/tests/api_tests.rs b/workflows/rcloud/tests/api_tests.rs
new file mode 100644
index 00000000..f0b3acdb
--- /dev/null
+++ b/workflows/rcloud/tests/api_tests.rs
@@ -0,0 +1,18 @@
+use actix_web::{test, web, App};
+use rcloud::api::handlers::health;
+
+#[actix_rt::test]
+async fn test_health_check() {
+    let app =
+        test::init_service(App::new().route("/api/v1/health", web::get().to(health::health_check)))
+            .await;
+
+    let req = test::TestRequest::get().uri("/api/v1/health").to_request();
+
+    let resp = test::call_service(&app, req).await;
+    assert!(resp.status().is_success());
+
+    let body: serde_json::Value = test::read_body_json(resp).await;
+    assert_eq!(body["status"], "healthy");
+    assert!(body["version"].is_string());
+}
-- 
2.51.0


  parent reply	other threads:[~2025-10-18  2:32 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-10-18  2:32 [PATCH 0/5] rust + rcloud support Luis Chamberlain
2025-10-18  2:32 ` [PATCH 1/5] install-rust-deps: Add generic Rust toolchain role Luis Chamberlain
2025-10-18  2:32 ` [PATCH 2/5] CLAUDE.md: Add Rust code quality requirements Luis Chamberlain
2025-10-18  2:32 ` [PATCH 3/5] bootlinux: Integrate Rust toolchain dependencies Luis Chamberlain
2025-10-18  2:32 ` [PATCH 4/5] install-go-deps: Add generic Go toolchain role Luis Chamberlain
2025-10-18  2:32 ` Luis Chamberlain [this message]
2025-10-18 18:22 ` [PATCH 0/5] rust + rcloud support Chuck Lever
2025-10-21 16:58   ` Luis Chamberlain

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251018023218.2240269-6-mcgrof@kernel.org \
    --to=mcgrof@kernel.org \
    --cc=cel@kernel.org \
    --cc=da.gomez@kruces.com \
    --cc=kdevops@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox