Zone Management

Create, modify, and manage bhyve virtual machine zones through the API.

Table of contents

  1. TOC

Overview

The Zoneweaver Agent provides full zone lifecycle management:

  • Create zones with POST /zones - from scratch, from templates, or with existing storage
  • Modify zone configuration with PUT /zones/:zoneName - changes queue and apply on next boot
  • Start/Stop/Restart zones with existing lifecycle endpoints
  • Delete zones with DELETE /zones/:zoneName

All create and modify operations are asynchronous - they return a task ID immediately, and the actual work is processed in the background via the task queue. Track progress with GET /tasks/:taskId.


Creating Zones

Minimal Creation

Only name and brand are required:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "test-vm-01",
    "brand": "bhyve"
  }'

Response:

{
  "success": true,
  "task_id": "a1b2c3d4-...",
  "zone_name": "test-vm-01",
  "operation": "zone_create",
  "status": "pending",
  "message": "Zone creation task queued successfully"
}

This creates a bare bhyve zone with default settings and no disks - useful for PXE/netboot scenarios or when you plan to add resources via modification later.

Creation with Resources

Specify optional resources at creation time:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web-server-01",
    "brand": "bhyve",
    "ram": "2G",
    "vcpus": "2",
    "diskif": "virtio",
    "netif": "virtio",
    "vnc": "on",
    "boot_volume": {
      "create_new": true,
      "pool": "rpool",
      "dataset": "zones",
      "volume_name": "root",
      "size": "30G",
      "sparse": true
    },
    "nics": [
      {
        "physical": "vnic0",
        "global_nic": "igb0"
      }
    ],
    "cdroms": [
      { "path": "/iso/omnios-r151050.iso" }
    ],
    "start_after_create": true
  }'

Creation from Template

Clone an existing template for fast provisioning:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "from-template",
    "brand": "bhyve",
    "source": {
      "type": "template",
      "template_dataset": "rpool/templates/omnios-base",
      "clone_strategy": "clone"
    },
    "boot_volume": {
      "pool": "rpool",
      "dataset": "zones",
      "volume_name": "root",
      "size": "30G"
    }
  }'

Clone strategies:

  • clone - Thin ZFS clone (instant, shares blocks with template)
  • copy - Full ZFS send/recv (independent copy, slower)

Creation with Existing Storage

Attach an existing ZFS volume as the boot disk:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "migrated-vm",
    "brand": "bhyve",
    "ram": "4G",
    "vcpus": "4",
    "boot_volume": {
      "create_new": false,
      "existing_dataset": "rpool/vms/old-server/root"
    }
  }'

The API checks if existing zvols are already in use by another zone. Use "force": true to override this check.

Auto-Start After Creation

Set start_after_create: true to automatically boot the zone once creation completes:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "auto-start-vm",
    "brand": "bhyve",
    "ram": "2G",
    "start_after_create": true
  }'

The response includes both task IDs:

{
  "success": true,
  "task_id": "create-task-uuid",
  "start_task_id": "start-task-uuid",
  "zone_name": "auto-start-vm",
  "operation": "zone_create",
  "status": "pending",
  "message": "Zone creation task queued with auto-start"
}

Cloud-Init Provisioning

Configure cloud-init attributes for automated guest setup:

curl -X POST https://your-server:5001/zones \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "cloud-vm",
    "brand": "bhyve",
    "ram": "2G",
    "vcpus": "2",
    "boot_volume": {
      "create_new": true,
      "pool": "rpool",
      "dataset": "zones",
      "volume_name": "root",
      "size": "30G"
    },
    "cloud_init": {
      "enabled": "on",
      "dns_domain": "example.com",
      "password": "changeme",
      "resolvers": "8.8.8.8,8.8.4.4",
      "sshkey": "ssh-rsa AAAA..."
    }
  }'

Modifying Zones

Overview

Zone modifications are applied via zonecfg and take effect on next zone boot. The zone can continue running while changes are queued - this allows you to make multiple modifications before restarting.

Changing Resources

Update RAM, vCPUs, or other attributes:

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "ram": "4G",
    "vcpus": "4",
    "vnc": "on"
  }'

Response:

{
  "success": true,
  "task_id": "modify-task-uuid",
  "zone_name": "web-server-01",
  "operation": "zone_modify",
  "status": "pending",
  "message": "Modification queued. Changes will take effect on next zone boot.",
  "requires_restart": true
}

Adding and Removing NICs

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "add_nics": [
      { "physical": "vnic1", "global_nic": "igb0" }
    ],
    "remove_nics": ["vnic0"]
  }'

NICs with global_nic are created on-demand when the zone boots. Omit global_nic for pre-created VNICs.

Adding and Removing Disks

Add a new disk:

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "add_disks": [
      {
        "create_new": true,
        "pool": "rpool",
        "dataset": "zones",
        "volume_name": "data",
        "size": "100G"
      }
    ]
  }'

Attach an existing zvol:

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "add_disks": [
      {
        "create_new": false,
        "existing_dataset": "rpool/shared/backup-vol"
      }
    ]
  }'

Remove a disk:

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "remove_disks": ["disk0"]
  }'

Adding and Removing CD-ROMs

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "add_cdroms": [
      { "path": "/iso/install.iso" }
    ]
  }'

Updating Cloud-Init

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "cloud_init": {
      "enabled": "on",
      "dns_domain": "newdomain.com",
      "resolvers": "1.1.1.1,1.0.0.1"
    }
  }'

Setting Autoboot

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "autoboot": true
  }'

Tracking Progress

All creation and modification operations are asynchronous. Track progress via the task API:

curl https://your-server:5001/tasks/TASK_ID \
  -H "Authorization: Bearer hw_your_api_key"

Creation tasks report granular progress:

ProgressStage
5%Validating parameters
10%Preparing storage (ZFS volumes)
30%Importing template (if applicable)
40%Applying zone configuration
50%Configuring boot disk
60%Configuring additional disks
70%Configuring CD-ROMs
75%Configuring network interfaces
80%Configuring cloud-init
90%Installing zone
95%Creating database record
100%Complete

Error Handling

Duplicate Zone (409)

{
  "error": "Zone web-server-01 already exists in database"
}

Invalid Zone Name (400)

{
  "error": "Invalid zone name"
}

No Changes Specified (400)

{
  "error": "No modification fields specified"
}

Zvol In Use (during task execution)

If a zvol is already attached to another zone, the creation/modification task will fail unless force: true is set in the request body.

Rollback on Failure

If zone creation fails at any stage, the system automatically rolls back:

  1. Removes the zonecfg configuration (if applied)
  2. Destroys any ZFS datasets that were created during the task (does not touch existing datasets)

Available Properties

Zone Attributes

PropertyTypeDescriptionExample
namestringZone name (required)"web-server-01"
brandstringZone brand (required for creation)"bhyve"
ramstringMemory allocation"2G"
vcpusstringVirtual CPU count"2"
bootromstringBoot ROM firmware"BHYVE_RELEASE_CSM"
hostbridgestringHost bridge emulation"i440fx"
diskifstringDisk interface"virtio"
netifstringNetwork interface type"virtio"
os_typestringGuest OS type"generic"
vncstringVNC console"on"
acpistringACPI support"on"
xhcistringxHCI USB controller"on"
autobootbooleanAuto-boot on host startupfalse

Boot Volume Options

OptionDescription
create_new: trueCreate a new ZFS volume with specified pool/dataset/size
existing_datasetAttach an existing ZFS dataset (zvol)
From templateClone or copy from a template dataset
OmittedNo boot disk (diskless zone for PXE/netboot)

Provisioning Zones

Overview

After zone creation, you can run the provisioning pipeline to automatically configure networking, sync files, and execute provisioners (shell scripts, Ansible playbooks). This is the automated equivalent of manually logging into a zone and running setup commands.

For comprehensive provisioning documentation, see the Provisioning Pipeline Guide.

Provisioning Workflow

  1. Create zone (if not already created)
  2. Upload provisioning artifact — tar.gz with your scripts, playbooks, files
  3. Set provisioning configuration on the zone
  4. Kick off provisioning pipelinePOST /zones/{name}/provision
  5. Track progress — Monitor the task chain as each stage completes

Setting Provisioning Configuration

Use PUT /zones/{name} to set provisioning metadata:

curl -X PUT https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "provisioning": {
      "recipe_id": "uuid-of-debian-netplan-recipe",
      "mode": "ansible_local",
      "artifact_id": "uuid-of-uploaded-tar-gz",
      "credentials": {
        "username": "startcloud",
        "password": "STARTcloud24@!",
        "ssh_key_path": "/etc/zoneweaver-agent/ssh/provision_key"
      },
      "sync_folders": [
        {
          "source": "provisioning_dataset",
          "dest": "/vagrant",
          "exclude": []
        }
      ],
      "provisioners": [
        {
          "type": "ansible_local",
          "playbook": "/vagrant/ansible/playbook.yml",
          "collections": ["startcloud.startcloud_roles"],
          "extra_vars": {
            "hostname": "web01",
            "domain": "example.com"
          }
        }
      ],
      "variables": {
        "vnic_name": "eth0",
        "ip": "10.190.190.10",
        "gateway": "10.190.190.1",
        "dns": "8.8.8.8"
      }
    }
  }'

Key fields:

  • recipe_id — Zlogin automation recipe for network setup (see Provisioning Guide)
  • mode — Provisioner type: shell, ansible, ansible_local, or none
  • artifact_id — ID of uploaded provisioning artifact (tar.gz)
  • credentials — SSH credentials for zone access
  • provisioners — Array of provisioner definitions
  • variables — Custom variables for recipe and provisioners

Kicking Off Provisioning

Once configuration is set, start the pipeline:

curl -X POST https://your-server:5001/zones/web-server-01/provision \
  -H "Authorization: Bearer hw_your_api_key"

Response:

{
  "success": true,
  "task_id": "provision-task-uuid",
  "zone_name": "web-server-01",
  "operation": "zone_provision",
  "status": "pending",
  "message": "Provisioning pipeline started",
  "pipeline_tasks": {
    "extract": "extract-task-uuid",
    "boot": "boot-task-uuid",
    "setup": "setup-task-uuid",
    "wait_ssh": "wait-ssh-task-uuid",
    "sync": "sync-task-uuid",
    "provision": "provision-task-uuid"
  }
}

Provisioning Pipeline Stages

The pipeline executes these stages sequentially (each is a separate task with depends_on chaining):

StageTask OperationDescription
1. Extractzfs_create_dataset + artifact extractionExtract provisioning tar.gz to ZFS dataset
2. Bootzone_startStart the zone
3. Setupzone_setupRun zlogin recipe to configure network
4. Wait SSHzone_wait_sshPoll until SSH is responsive
5. Synczone_syncrsync files from host to zone
6. Provisionzone_provisionExecute provisioners (shell/ansible)

After completion, ZFS snapshots are created:

  • @pre-provision — Before running provisioners (rollback point)
  • @post-provision — After successful provisioning (known-good state)

Checking Provisioning Status

curl https://your-server:5001/zones/web-server-01/provision/status \
  -H "Authorization: Bearer hw_your_api_key"

Response:

{
  "success": true,
  "zone_name": "web-server-01",
  "status": "provisioning",
  "current_stage": "zone_sync",
  "progress": {
    "extract": "completed",
    "boot": "completed",
    "setup": "completed",
    "wait_ssh": "completed",
    "sync": "in_progress",
    "provision": "pending"
  },
  "last_provisioned_at": null
}

Canceling Provisioning

If provisioning is stuck or needs to be stopped:

curl -X POST https://your-server:5001/zones/web-server-01/provision/cancel \
  -H "Authorization: Bearer hw_your_api_key"

This cancels all pending/running tasks in the pipeline.

Rolling Back Provisioning

If provisioning failed or produced incorrect results, rollback to the pre-provision snapshot:

curl -X POST https://your-server:5001/zones/web-server-01/provision/rollback \
  -H "Authorization: Bearer hw_your_api_key"

This rolls back the provisioning ZFS dataset to @pre-provision, removing all provisioner changes.

Provisioning Use Cases

Scenario 1: Fresh Debian VM with Ansible

  1. Create zone from Debian template
  2. Upload Ansible playbook tar.gz
  3. Set provisioning config with debian-netplan recipe
  4. Kick off provisioning → Zone auto-configures network, runs Ansible playbook

Scenario 2: OmniOS Zone with Shell Scripts

  1. Create zone from OmniOS template
  2. Upload scripts tar.gz
  3. Set provisioning config with omnios-dladm recipe and shell provisioner
  4. Kick off provisioning → Zone configures network, runs setup scripts

Scenario 3: Cloud-Init Only (No Zlogin)

  1. Create zone with cloud-init attributes set
  2. Set provisioning config with cloud-init-wait recipe (no zlogin automation)
  3. Kick off provisioning → Pipeline waits for cloud-init, then syncs files and runs provisioners

Deleting Zones

Basic Delete

Delete a zone (configuration only):

curl -X DELETE https://your-server:5001/zones/web-server-01 \
  -H "Authorization: Bearer hw_your_api_key"

What happens:

  1. Zone halted (if running)
  2. Zone uninstalled (zoneadm uninstall -F)
  3. Zone configuration removed (zonecfg delete -F)
  4. Database record removed

ZFS datasets are NOT deleted — boot volumes, additional disks, and provisioning datasets remain on disk. This is the safe default to prevent accidental data loss.

Delete with ZFS Cleanup

To automatically destroy all zone-related ZFS datasets during deletion, use the cleanup_datasets query parameter:

curl -X DELETE "https://your-server:5001/zones/web-server-01?cleanup_datasets=true" \
  -H "Authorization: Bearer hw_your_api_key"

What happens:

  1. Zone halted (if running)
  2. Zone configuration read to identify all datasets
  3. Zone uninstalled
  4. Zone configuration removed
  5. ZFS datasets destroyed:
    • Boot volume (e.g., rpool/zones/web-server-01/root)
    • Zonepath (e.g., rpool/zones/web-server-01/path)
    • Provisioning dataset (e.g., rpool/zones/web-server-01/provisioning)
    • Additional disks within zone hierarchy (e.g., rpool/zones/web-server-01/data)
  6. Database record removed

Safety considerations:

  • ✅ Only destroys datasets within the zone’s own hierarchy (e.g., rpool/zones/web-server-01/*)
  • ✅ Does NOT destroy datasets attached via existing_dataset (external storage not owned by the zone)
  • ⚠️ Irreversible — All data in destroyed datasets is permanently lost
  • ⚠️ No ZFS snapshots preserved — If you want to keep snapshots, manually snapshot before deleting

When to Use Cleanup

Use cleanup_datasets=true when:

  • Zone is temporary (testing, CI/CD, ephemeral workloads)
  • You’re certain the data is no longer needed
  • You want a complete clean slate (no orphaned datasets)
  • Storage space is limited and you need to reclaim space immediately

Do NOT use cleanup when:

  • Zone data might be needed later (backups, forensics, rollback)
  • Datasets are shared with other systems (e.g., NFS-exported zvols)
  • You want to re-attach storage to a new zone later
  • You’re unsure about data retention requirements

Example: Clean Delete Workflow

Full cleanup of a test zone:

# 1. Halt the zone
curl -X POST https://your-server:5001/zones/test-zone/halt \
  -H "Authorization: Bearer hw_your_api_key"

# 2. Wait for halt to complete (check task status)

# 3. Delete with cleanup
curl -X DELETE "https://your-server:5001/zones/test-zone?cleanup_datasets=true" \
  -H "Authorization: Bearer hw_your_api_key"

# 4. Verify deletion
curl https://your-server:5001/zones/test-zone \
  -H "Authorization: Bearer hw_your_api_key"
# Should return 404 Not Found

Recovering from Failed Delete

If delete fails partway through:

  1. Check zone status:

    # On host
    pfexec zoneadm list -cv
    
  2. Manually complete cleanup:

    # On host
    pfexec zoneadm -z test-zone uninstall -F
    pfexec zonecfg -z test-zone delete -F
    pfexec zfs destroy -r rpool/zones/test-zone  # If cleanup_datasets was intended
    
  3. Remove database record:

    curl -X DELETE https://your-server:5001/zones/test-zone/force \
      -H "Authorization: Bearer hw_your_api_key"