> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Image library

> Golden images every VM is cloned from — importing one, the two decisions made at import that cannot be undone, and why a clone takes three seconds, a minute, or never finishes.

Every VM on this platform starts as a copy of a **golden image**: a PVC holding a prepared operating system, cloned into a new disk when a VM is created. The library is where those goldens are imported, and the choices made there decide what the VMs cloned from them can do — including things that cannot be changed afterwards.

The library is per supervisor. Each golden is a DataVolume in the `image-library` namespace of the supervisor it was imported on, and the **Image Library** entry in a supervisor's sidebar shows that supervisor's goldens only. `GET /api/images?supervisor=<name>` lists one supervisor. Without `supervisor` it lists every supervisor that Celum can reach.

## Four fields, and two of them are one-way doors

| Field | Values | Decides |
| - | - | - |
| `library` | `vm` · `kubernetes` | which picker ever offers it — a VM image or a cluster-node image |
| `imageType` | `disk` · `iso` | a bootable clone, or an installer that boots against a blank disk |
| `accessMode` | `ReadWriteMany` · `ReadWriteOnce` | **whether VMs from it can live-migrate, and how fast they clone** |
| `storageClass` | any class on the supervisor | **which VMs can use it at all** — empty means the cluster default |

> [!WARNING]
>
> **A VM can only clone from a golden that lives on the class the VM is being built on.** Not "more slowly" — at all. CDI clones by snapshot within one class, and a snapshot cannot be restored into another storage pool, so the claim sits `Pending` while the real error lands on a temporary PVC in the image namespace that nobody is watching.
>
> Left empty, an image goes to the cluster default, which then quietly decides which classes will ever be able to serve it. On a supervisor with one class that is the right behaviour and there is nothing to think about. On one with two — local disks and an array, say — import the golden onto **each** class you intend to build VMs on. The Create VM picker only offers images from the class you selected, and says how many exist elsewhere.
>
> The escape hatch, if you would rather keep one copy: set `cloneStrategy: copy` on the target class's StorageProfile and CDI copies across classes instead of snapshotting. It works — measured, 3.5 GiB in 36 s — but it is a host-assisted byte copy, so a 60 GiB golden is minutes rather than seconds, on every VM created.

> [!WARNING]
>
> **The access mode is chosen at import and cannot be changed on an existing image.** A VM built on a ReadWriteOnce image can never live-migrate and can never hold an [identity address](https://docs.celum.io/networking/vm-identity-pools.md) — the create call refuses the combination outright. Converting means re-importing the image; converting a VM that already exists means rebuilding it.
>
> Default to **ReadWriteMany** unless you have a reason not to. That is what the platform defaults to.

> [!NOTE]
>
> **The import form asks for the class; it did not always.** Every golden used to go to the cluster default, which is why an estate predating the field may have its whole library on one class. There is no move — re-import onto the class you need it to serve.
>
> The message to search for when this bites, from the driver rather than from the claim: `Not enough nodes fulfilling the auto-place criteria: has a deployed storage pool named [nvme]`.

## Why the access mode also decides clone speed

This is the part nobody expects, and it is a twenty-fold difference.

The access mode determines the PVC's **volume mode**, and CDI will only use a fast clone when the source and the target agree on it. With both in Block mode the storage driver clones natively; across a mismatch CDI falls back to a host-assisted byte copy through two pods.

| Golden | VM disk | Clone method | Measured, 10 GiB |
| - | - | - | - |
| Filesystem | Block | host-assisted copy | **62 s** |
| Block | Block | native CSI clone | **3 s** |

Same cluster, same storage, same chart versions. And the mismatch is not a misconfiguration you can tune away: both efficient strategies refuse cross-mode pairs outright, whatever the storage profile says.

> [!NOTE]
>
> **Block is used only where the StorageClass supports it.** A ReadWriteMany image on file-based storage — NFS, SMB, CephFS — stays in Filesystem mode, because a Block PVC there would never bind. So on file storage you get the migration capability without the clone speedup, which is the correct trade rather than a failure.

> [!NOTE]
>
> Importing straight into Block mode needs the container runtime to hand the block device to a non-root process. Talos ships that setting, which is why this works here at all; a supervisor without it would fail an import with a permission error on the device rather than anything mentioning images.

## Importing

> [tab] Dashboard
>
> **Image Library** → **Import OS image**. Pick the library and the image type as cards. Then set **Mode** to `url` and give a URL, or start from **Browse catalog** or **Browse S3 bucket**. Last, set the access mode and the storage class.

> [tab] API
>
> `POST /api/images` starts the import; `GET /api/images/{id}/status` follows it. `GET /api/images/catalog?provider=` lists what upstream providers publish, so a URL does not have to be found by hand.

> [tab] MCP
>
> `add_image` imports, `list_images` and `get_image_status` follow it, `get_image_catalog` browses upstream. Deleting an image has no tool.

An import is asynchronous and the image is unusable until its status reads `ready` — creating a VM against anything else is refused by name.

### Importing from an S3 bucket

You can keep goldens in your own object storage and import them from there instead of from the internet. Set up the bucket connections once, on **Registries → Image buckets**. See [Image buckets](https://docs.celum.io/gitops/registries.md#image-buckets).

> [tab] Dashboard
>
> **Image Library** → **Browse S3 bucket**. The browser lists the objects in the bucket that this supervisor imports from, with a name, version, type and size for each object. If more than one bucket is available, choose one in **Bucket**. **Filter objects…** narrows the list by key. **Import** on a row opens **Import Image** with **Mode** set to `s3` and that object already selected. You can also start from **Import OS image**, set **Mode** to `s3` and pick a **Bucket object** there.

> [tab] API
>
> `GET /api/images/s3-config?supervisor=<name>` lists the buckets the supervisor can use and names the default one. `GET /api/images/s3-objects?supervisor=<name>&connection=<bucket>&prefix=<prefix>` lists the objects you can import. `POST /api/images` with `sourceS3Key` imports one. Add `s3Connection` to choose a bucket. The two reads need `image:List` and the import needs `image:Create`, on `krn:celum:image:*`.

> [tab] MCP
>
> `get_image_s3_config` shows which bucket an import would use. `list_image_sources` lists the objects, and `add_image` with `sourceS3Key` imports one.

* **Which bucket.** If you choose no bucket, the import uses the supervisor's own default connection. If there is none, it uses a connection declared for the supervisor's site, then the supervisor's only connection. On a Celum appliance, the appliance's own image store comes last. A bucket that another supervisor lends through its site is shown as `— from <supervisor> (site <site>)`.
* **What is listed.** The browser lists disk and ISO files only: `.img`, `.qcow2`, `.raw`, `.iso`, `.vmdk`, `.vhd` and `.vhdx`, also when compressed with `.zst`, `.gz`, `.xz` or `.bz2`. It hides checksums and other files. It lists objects under every prefix.
* **Name and version.** The browser takes these from object metadata, if the uploader set it: `x-amz-meta-displayname`, `x-amz-meta-os`, `x-amz-meta-version` and `x-amz-meta-imagetype`. If not, it guesses them from the object key. Check them in the form before you import.
* **Library.** **Import** from the browser always selects the **Virtual machine** library. For a Talos node image, change it to **Kubernetes**.
* **The download.** The bucket credentials stay in Celum. Celum gives the supervisor's CDI importer a presigned URL that is valid for 12 hours, and the importer downloads the object directly from the bucket. Your supervisor's nodes must be able to reach the bucket endpoint, not only Celum. A re-import signs a new URL for the same object.

The size rule below applies to bucket objects too: set the size by hand for a compressed object.

### Two things about an import that produce no error at all

**A golden is the one volume that never gets a consumer.** Nothing mounts it; it exists to be cloned. Under a StorageClass with `volumeBindingMode: WaitForFirstConsumer` the PVC therefore waits for a pod nobody will ever create, the importer is never scheduled, and the DataVolume sits at `PendingPopulation` indefinitely. The only trace is a PVC event reading *"waiting for first consumer to be created before binding"* — which is not a failure message, because nothing failed. The import simply never started.

The platform stamps `cdi.kubevirt.io/storage.bind.immediate.requested` on every image import to prevent it. This was invisible for as long as the estate ran only Ceph, whose classes bind `Immediate`; the first supervisor on other storage hung its entire library.

**`Auto` cannot size a compressed source.** Leaving the size empty asks the platform to match the source, and against a `.img.gz` or similar it cannot: gzip's `ISIZE` trailer is modulo 2³², so it wraps at 4 GiB and reports a plausible wrong number for exactly the images large enough to matter. The fallback is `10Gi`, and a 60 GiB Windows golden then downloads in full before failing the conversion with *"virtual image size … is larger than the reported available storage"*. The form says so rather than guessing — **set the size by hand for any compressed source.**

### Three things you can do to a golden afterwards

* **Re-import** (`POST /api/images/{id}/reimport`, or **Retry import** on a failed row) deletes the golden's DataVolume and downloads the same source again, with the same access mode and storage class. It takes no options, so it cannot change the access mode. To change it, delete the image and import it again with the other mode.
* **Bake the guest agent** (`POST /api/images/{id}/bake-guest-agent`) installs `qemu-guest-agent` into the golden itself, so clones are agent-ready **without any network access at first boot**. Useful on an estate where a new VM cannot reach a package mirror. It is asynchronous and the image stays usable throughout — the rewrite happens at the end.
* **Upload to a bucket** (`POST /api/images/{id}/upload-to-bucket`) puts it in object storage, which is where ISOs and large images are kept.

## The default Talos image

**Set as default** in an image row's menu marks that golden as the default for its library. **Unset default** removes the mark. The **DEFAULT** column shows which images have it.

The mark matters in the **Kubernetes** library. The **Simple** setup of the [cluster form](https://docs.celum.io/clusters/create.md#setup-simple-or-advanced) does not ask for an image. On a KubeVirt-based ClusterClass it uses the ready Kubernetes-library image that is marked default. If no image is marked, it uses the ready image with the newest Talos release. The image it picks also sets the cluster's Talos version and Kubernetes version. In **Advanced** you choose the image yourself, and the picker labels the default image `default`.

In the **VM** library the mark is only a label. The VM form does not preselect it.

On a `talos-vsphere` ClusterClass the mark does not apply. There, Simple setup uses a vCenter template instead (see [vCenter templates](#vcenter-templates)).

> [!WARNING]
>
> **Nothing keeps the default unique.** When you set one image as the default, Celum does not unset the others. If two images are marked, Simple setup takes whichever comes first in the list. Unset the old default when you set a new one.

> [tab] Dashboard
>
> **Image Library** → the image's row menu → **Set as default** or **Unset default**.

> [tab] API
>
> `PUT /api/images/{id}` with `{"isDefault": true}`. The call writes only the fields you send: `displayName` sets the display name (an empty string clears it), and a body with neither field is refused with `400`. This needs `image:Update` on `krn:celum:image:<id>`. The id has the form `<supervisor>::<DataVolume name>`.

> [tab] MCP
>
> No tool sets or clears the default. `list_images` returns `isDefault` for each image.

## vCenter templates

Use the **vSphere** tab on supervisors that build Talos clusters on vSphere (`talos-vsphere` ClusterClasses). These clusters clone their nodes from a VM template in vCenter, not from a golden PVC. This tab lists the templates in vCenter and imports new ones. Celum stores nothing here. vCenter is the record, and a template shows in the list as soon as vCenter has it.

The tab needs the supervisor's Cluster API onboarding step done with a vSphere kind. Celum reads the vCenter address, thumbprint and credentials from what Cluster API uses, and the cluster agent on the supervisor connects to vCenter. If any of this is missing, the tab reads **vCenter inventory unavailable**.

### Import a Talos OVA into vCenter

> [tab] Dashboard
>
> **Image Library** → **vSphere** → **Import Talos OVA**. Pick a **Talos version** from the catalog. Keep or change the **Template name**, which is `talos-<version>` by default. Set the **Placement**: **Datacenter** and **Datastore** are required, and **Folder**, **Network** and **Resource pool** are optional. **Import** starts the upload and waits up to 8 minutes for the template to show in the list.

> [tab] API
>
> `POST /api/supervisors/{name}/vsphere/templates` with `url`, `name`, `datacenter` and `datastore`. Optional fields are `folder`, `network`, `resourcePool` and `diskProvisioning`, which is `thin` by default. The call returns `202` with `"status": "started"`, or `"status": "exists"` when a template with that name already exists. `GET /api/supervisors/{name}/vsphere/inventory` lists the templates. The import needs `capi:Install` and the list needs `capi:GetStatus`, both on `krn:celum:supervisor:<supervisor>:capi:*`.

> [tab] MCP
>
> `get_vsphere_inventory` lists the templates together with the rest of the inventory. No tool imports an OVA.

* The cluster agent on the supervisor downloads the OVA from its URL, uploads it to vCenter and marks the VM as a template. This runs in the background for up to 30 minutes.
* Before the upload starts, the agent logs in and checks the datacenter, datastore, resource pool and folder. A wrong placement or a bad credential therefore fails at once. If a template with the same name exists, the import does nothing. If a VM that is not a template has that name, the import is refused.
* The catalog offers the Talos `vmware-amd64.ova` from `factory.talos.dev`. It is built with the VMware guest-tools extension, which Cluster API needs to read a node's address. The supervisor must be able to reach `factory.talos.dev`. To import from a mirror, call the API with your own `url`.
* **Network** maps every network in the OVA to that port group.

> [!WARNING]
>
> **Celum does not report a failed upload.** The API returns as soon as the upload starts. A later failure is written only to the cluster agent's log on the supervisor. If the template is still missing after 8 minutes, the dialog tells you so. Look for `vsphere-import` in the cluster agent's log before you try again.

> [!TIP]
>
> Keep the default `talos-<version>` name. On a `talos-vsphere` class, the **Simple** setup uses the template whose name has the highest `x.y.z` version, and it reads the Talos version from that name.

## Verify

The image reads `ready`, with the access mode you asked for. Then create one VM from it and time the clone: a few seconds means the fast path, around a minute means the golden and the disk disagree on volume mode.

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| Creating a VM is refused because the image is not ready | the import is still running, or it failed. Follow the image's status |
| A VM cannot live-migrate and nothing explains why | its image is ReadWriteOnce. Decided at import; the VM must be rebuilt from an RWX image |
| The identity network is refused at VM create | same cause — an identity address exists to survive migration, so it needs a migratable image |
| Every clone takes about a minute | the golden is in Filesystem mode and the VM disk is Block. Re-import the golden as ReadWriteMany |
| A ReadWriteMany image still clones slowly | the StorageClass is file-based, so both sides stay Filesystem. Expected |
| An import fails with a permission error on a device | the container runtime is not configured to pass block devices to non-root. Not an image problem |
| A new VM sits with no OS or IP reported | the guest agent never installed. Bake it into the golden if first-boot networking is restricted |
| An import sits at `PendingPopulation` forever, no error | the class is `WaitForFirstConsumer` and the golden has no consumer. Expected on a release predating the immediate-bind annotation |
| A large compressed golden downloads fully, then fails on conversion | `Auto` sized it at `10Gi`. Set the size explicitly for any compressed source |
| A VM's disk stays `Pending` and the DataVolume reads `CloneInProgress` forever | the golden is on a different StorageClass than the VM. Import it onto that class too — the visible claim shows only "waiting for the external provisioner"; the driver's real answer is on a `tmp-pvc-…` in the image namespace |
| **Browse S3 bucket** says no bucket is connected | no connection serves this supervisor: it has none of its own and none for its site. Add one on **Registries → Image buckets** |
| A Talos template never shows on the **vSphere** tab | the upload failed after it started. Only the cluster agent's log on the supervisor gives the reason |
| **Simple** setup picked a Talos image you did not expect | a different Kubernetes-library image is marked default, or more than one is |

## Related

> [card] [Create a VM](https://docs.celum.io/vms/create.md)
>
> Where an image is consumed, and the rest of the create-time choices.

> [card] [Classes & profiles](https://docs.celum.io/storage/classes-and-profiles.md)
>
> Clone strategy and access-mode support per StorageClass.

> [card] [VM lifecycle](https://docs.celum.io/vms/lifecycle.md)
>
> Live migration — the capability the access mode grants or withholds.

> [card] [VM identity pools](https://docs.celum.io/networking/vm-identity-pools.md)
>
> Claim addresses, which need a migratable image.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.