V3 to V4 Storage Migration
When upgrading from Fuzzball V3 to V4, the storage system is automatically migrated from the V3 three-tier model (Storage Driver, Storage Class, Storage Volume) to the V4 two-tier model (Storage Provisioner, Storage Volume). The upgrade creates V4 provisioners from your existing storage classes, links your volumes to them, and backfills volume ownership.
However, the automated migration handles database records only. Depending on how your V3 storage classes were configured, the actual directories on your storage backend may need adjustment so that V4 can locate them. This guide walks through the post-upgrade steps to get your storage fully functional.
Before you upgrade: Generate the migration report
Before upgrading to V4, generate a migration report on your V3 cluster. The report previews exactly what the automated migration will do: which storage classes will be converted to provisioners, what driver type each will use, and which volumes will be linked. This report is your reference for verifying the upgrade succeeded and for planning any post-upgrade path reconciliation.
Every cluster upgrade should generate this report before the V4 upgrade begins. The report is read-only and makes no changes to the cluster.
Prerequisites for the preflight report
fuzzballCLI configured and authenticated against the V3 clusterkubectlaccess to thefuzzball-systemnamespacejqinstalled- The
storage-migrate-preflightbundle from CIQ Support
Running the preflight tool
Obtain the storage-migrate-preflight.zip bundle from CIQ Support. Then:
# unzip storage-migrate-preflight.zip
# cd storage-migrate-preflight
# ./storage-migrate-preflight.sh
The tool discovers your cluster, connects to the operator pod, and runs a dry-run of the migration. Example output:
storage-migrate: previewing migration for cluster 491506d4-33fd-6849-5a22-e832418a3615
storage-migrate: found 2 storage class(es)
storage-migrate: processing class "persistent" (id=e8778dde-..., persistent=true)
org e700253c-...: 2 volume(s) in class "persistent"
-> will create provisioner "persistent" (driver=HOSTPATH)
-> volume "migration-test-vol-1" will be linked to provisioner "persistent"
-> volume "migration-test-vol-2" will be linked to provisioner "persistent"
storage-migrate: processing class "ephemeral" (id=7f33ca42-..., persistent=false)
no unmigrated volumes for class "ephemeral"
storage-migrate: completed successfully — 2 class(es) processed
What each line means:
- "will create provisioner" — a V4 provisioner will be created for this storage class in this organization, with the specified driver type
- "will be linked to provisioner" — this existing volume will be associated with the new provisioner
- "no unmigrated volumes" — the storage class exists but has no volumes that need migration
Save the report
Save the JSON report for reference during the post-upgrade steps:
# ./storage-migrate-preflight.sh --json | jq . > preflight-report.json
Review the report and confirm:
- The driver type mapping is correct for each storage class (HOSTPATH, NFS, or EFS)
- The expected volumes will be linked to the right provisioners
- No unexpected warnings about corrupted class definitions or orphaned volumes
If the preflight tool reports a driver type that doesn't match your storage backend (for example, HOSTPATH when you use NFS), contact CIQ Support before proceeding with the upgrade. The driver type can be corrected post-upgrade, but it is easier to address beforehand.
Once the report looks correct, proceed with the V4 upgrade. See Updating Fuzzball for the upgrade procedure.
Post-upgrade steps
The remainder of this guide covers steps to complete after the V4 upgrade has
finished. The operator runs storage-migrate automatically during reconciliation,
which converts your V3 storage classes to V4 provisioners as previewed in the
preflight report.
Prerequisites
Before starting the post-upgrade steps:
- Fuzzball V4 upgrade is complete and the operator has reconciled successfully
fuzzballCLI is installed and authenticated against the upgraded clusterkubectlaccess to thefuzzball-systemnamespace- Filesystem access to the storage backend (NFS mount point, host node, or AWS console)
- Pre-upgrade migration report has been generated and reviewed (see above)
If you upgraded without generating a preflight report, the automatic migration still ran during the upgrade. You can check the operator logs for equivalent output:
# kubectl logs -l app.kubernetes.io/name=fuzzball-operator -n fuzzball-system | grep storage-migrate
Proceed with Step 1 below using the log output in place of the preflight report.
Step 1: Verify the automated migration
After the V4 upgrade completes, confirm that provisioners were created from your V3 storage classes.
List all provisioners:
# fuzzball volume provisioner list
You should see one provisioner per V3 storage class per organization. For example, if
you had a persistent storage class and two organizations, you will see two
provisioners both named persistent, one for each organization.
Compare against the preflight-report.json you saved before the upgrade — the
provisioner count and names should match the dry-run output.
Inspect a specific provisioner:
# fuzzball volume provisioner info persistent
List volumes linked to a provisioner:
# fuzzball volume list --provisioner persistent
If provisioner list returns no results, the automatic migration may not have run.
Check the operator logs:
# kubectl logs -l app.kubernetes.io/name=fuzzball-operator -n fuzzball-system | grep storage-migrate
If you see errors, refer to the troubleshooting section at the end of this guide.
Step 2: Determine whether path reconciliation is needed
V3 storage classes used template-based directory naming that placed volumes inside
subdirectories. For example, a V3 persistent class on a typical on-premises
deployment stored volumes at:
/mnt/fuzzball-sharedfs/persistent/<group-UUID>/
V4 provisioners expect volumes as flat directories directly under the driver's base path:
/mnt/fuzzball-sharedfs/<volume-name>/
If your V3 storage classes used a basePath, subDir, or share parameter (most
default configurations do), then the directories on your storage backend are nested one
level deeper than V4 expects. You need to create symbolic links to bridge this gap.
If your V3 volumes are already at the top level of the storage backend with no nesting, skip to Step 4.
AWS EFS users: EFS requires additional reconciliation beyond symlinks. The V3 EFS
CSI driver created access points without the Name tag that V4 expects, and stored
data at nested paths (/persistent/<id>) rather than V4's flat layout (/<id>).
Both new access points and filesystem symlinks are needed. Contact CIQ Support
for the EFS-specific migration procedure, or refer to the internal runbook for the
complete step-by-step instructions.
How to check
SSH into a host that can access the storage backend (the NFS server, or any node that mounts the shared filesystem) and list the top-level directories:
# ls -la /mnt/fuzzball-sharedfs/
If you see directories like persistent/ or ephemeral/ that contain your actual
volume data as subdirectories, path reconciliation is needed.
Step 3: Create symlinks for path reconciliation
Never copy or move customer data. All path reconciliation uses symbolic links, which are instant, zero-copy, and fully reversible. Removing a symlink does not affect the original data.
On-premises NFS or hostpath
This is the most common scenario. V3 default classes create a persistent/ and/or
ephemeral/ subdirectory under the shared filesystem, with per-group or per-workflow
UUID directories inside.
On the machine that hosts or mounts your shared filesystem, run the discover-and-link
script for each V3 sub-path that contains volumes:
# ./discover-and-link.sh /mnt/fuzzball-sharedfs persistent
Scanning V3 volumes in: /mnt/fuzzball-sharedfs/persistent
LINK: /mnt/fuzzball-sharedfs/e700253c-... -> /mnt/fuzzball-sharedfs/persistent/e700253c-.../
LINK: /mnt/fuzzball-sharedfs/a1b2c3d4-... -> /mnt/fuzzball-sharedfs/persistent/a1b2c3d4-.../
Done. Created 2 symlink(s), skipped 0.
If you also have an ephemeral/ sub-path with data you want to preserve:
# ./discover-and-link.sh /mnt/fuzzball-sharedfs ephemeral
Ephemeral volumes are transient — they are created per-workflow and normally cleaned up
after the workflow completes. In most cases, you can safely skip the ephemeral/
directory. Only run the script for ephemeral/ if you have data there that was not
cleaned up and you need to preserve it.
CLICK TO EXPAND: discover-and-link.sh script
If you do not have the standalone script, you can create it from the listing below.
Save as discover-and-link.sh and run chmod +x discover-and-link.sh.
#!/bin/bash
# discover-and-link.sh — Creates symlinks to reconcile V3 storage paths for V4.
#
# Usage:
# ./discover-and-link.sh <storage-root> <v3-sub-path>
#
# Examples:
# ./discover-and-link.sh /mnt/fuzzball-sharedfs persistent
# ./discover-and-link.sh /srv/nfs/fuzzball persistent
set -euo pipefail
usage() {
echo "Usage: $0 <storage-root> <v3-sub-path>"
echo ""
echo "Arguments:"
echo " storage-root Absolute path to the provisioner base directory"
echo " (e.g., /mnt/fuzzball-sharedfs)"
echo " v3-sub-path V3 subdirectory containing volume directories"
echo " (e.g., persistent or ephemeral)"
exit 1
}
if [ $# -ne 2 ]; then
usage
fi
STORAGE_ROOT="$1"
V3_SUBPATH="$2"
V3_DIR="${STORAGE_ROOT}/${V3_SUBPATH}"
if [ ! -d "$STORAGE_ROOT" ]; then
echo "ERROR: Storage root does not exist: $STORAGE_ROOT" >&2
exit 1
fi
if [ ! -d "$V3_DIR" ]; then
echo "ERROR: V3 directory not found: $V3_DIR" >&2
echo "This may mean the storage class did not use a '$V3_SUBPATH' sub-path," >&2
echo "or the storage is mounted at a different location." >&2
exit 1
fi
created=0
skipped=0
echo "Scanning V3 volumes in: $V3_DIR"
for vol_dir in "$V3_DIR"/*/; do
[ -d "$vol_dir" ] || continue
vol_name=$(basename "$vol_dir")
link_path="${STORAGE_ROOT}/${vol_name}"
if [ -e "$link_path" ] || [ -L "$link_path" ]; then
echo " SKIP: $link_path already exists"
skipped=$((skipped + 1))
continue
fi
echo " LINK: $link_path -> $vol_dir"
ln -s "$vol_dir" "$link_path"
created=$((created + 1))
done
echo ""
echo "Done. Created $created symlink(s), skipped $skipped."
if [ "$created" -gt 0 ]; then
echo "Verify with: ls -la $STORAGE_ROOT"
fi
Verifying symlinks
After creating symlinks, confirm they point to the correct locations:
# ls -la /mnt/fuzzball-sharedfs/
You should see symlinks alongside the original V3 directories:
drwxr-xr-x 4 root root 4096 May 26 10:00 persistent/
drwxr-xr-x 2 root root 4096 May 26 10:00 ephemeral/
lrwxrwxrwx 1 root root 52 May 26 10:05 e700253c-... -> /mnt/fuzzball-sharedfs/persistent/e700253c-.../
lrwxrwxrwx 1 root root 52 May 26 10:05 a1b2c3d4-... -> /mnt/fuzzball-sharedfs/persistent/a1b2c3d4-.../
If your storage root is different
The default storage path is /mnt/fuzzball-sharedfs. If your cluster uses a different
path, adjust accordingly. You can check the provisioner's driver configuration to find
the correct base path:
# fuzzball volume provisioner info persistent -o yaml
Look for the path (hostpath driver) or target (NFS driver) field in the driver
configuration.
GCP and Azure deployments
GCP and Azure V3 configurations typically used a subDir parameter (e.g.,
subDir=persistent) under a shared NFS export. The same symlink approach applies —
mount the NFS export and run the script against the appropriate sub-path.
OCI deployments
OCI provisions separate NFS exports for /persistent and /ephemeral. Mount each
export independently and verify whether the volume directories are nested. If they are,
run the symlink script against each mount point.
Step 4: Scan for unlinked volumes
The automatic storage-migrate process links all V3 volumes that existed in the
database. However, if you have volume directories on the storage backend that were
not tracked in V3 (for example, directories created manually or by external tools),
you can use the scan command to discover and import them.
The scan command discovers volumes by listing directories at the provisioner's base path. It only discovers real directories, not symbolic links. If you created symlinks in Step 3, those symlinks make already-migrated volumes mountable but will not appear in scan results. This is expected — those volumes are already in the V4 database from the automatic migration.
If you need to discover volumes inside a V3 sub-path (like persistent/) that are
not already in the V4 database, edit the provisioner to point directly at the
sub-path instead of using symlinks:
# fuzzball volume provisioner edit <provisioner-name>
Change path: /mnt/fuzzball-sharedfs to path: /mnt/fuzzball-sharedfs/persistent,
then run the scan.
Preview what the scan will find:
# fuzzball volume provisioner scan persistent --dry-run
The scan now performs two-way reconciliation: it discovers new volumes and identifies
stale volumes (tracked in Fuzzball but not found on the backend). Symlinked directories
are followed during the scan, so volumes created via symlinks in Step 3 will be
discovered correctly. Always review the --dry-run output before running without
--dry-run.
If the discovered volumes look correct, run the scan without --dry-run to import new
volumes and remove stale records:
# fuzzball volume provisioner scan persistent
Imported volumes are marked as external — Fuzzball will not delete their backing data when the volume is removed. This is a safety feature for data that was not originally created by V4.
Verify the imported volumes:
# fuzzball volume list --provisioner persistent
Step 5: Verify with a test workflow
Submit a simple workflow that mounts one of your migrated volumes to confirm data is accessible:
version: v4
volumes:
test-vol:
use: persistent
name: <your-volume-name>
jobs:
verify:
image: ubuntu:22.04
mounts:
/data: test-vol
script: |
echo "Listing migrated volume contents:"
ls -la /data/
# fuzzball workflow start verify-migration.yaml
Monitor the workflow and confirm the volume contents are visible:
# fuzzball workflow events <workflow-id>
If the workflow runs successfully and your data is listed, the migration is complete.
Troubleshooting
Provisioner list is empty
The automatic storage-migrate did not run or encountered an error. Check the operator
logs:
# kubectl logs -l app.kubernetes.io/name=fuzzball-operator -n fuzzball-system | grep storage-migrate
If the migration failed, it can be re-run safely (it is idempotent). Contact CIQ support for assistance with manual re-runs.
Volume shows in the database but workflows cannot mount it
The V4 driver cannot find the volume directory at the expected path. Verify:
-
The provisioner's driver base path is correct:
# fuzzball volume provisioner info <provisioner-name> -o yaml -
The volume directory (or symlink) exists at
<base-path>/<volume-name>/:# ls -la /mnt/fuzzball-sharedfs/<volume-name> -
If the path is missing, create a symlink to the actual data location (see Step 3).
Scan discovers unexpected volume names
If you ran the scan before creating symlinks, it may have discovered the V3 directory
structure names (like persistent or ephemeral) as volume names. To fix this:
-
Remove the incorrectly imported volumes (this only removes the database record, not the data):
# fuzzball volume delete <provisioner> <wrong-name> --preserve-data -
Create the correct symlinks (Step 3).
-
Re-run the scan:
# fuzzball volume provisioner scan <provisioner>
Provisioner has the wrong driver type
The automatic migration auto-detects the driver type from the V3 class configuration. If it chose incorrectly (for example, hostpath instead of NFS), edit the provisioner to correct it:
# fuzzball volume provisioner edit <provisioner-name>
This opens the provisioner definition in your editor. Update the driver section with
the correct type and configuration, then save.
Symlinks are not followed by the storage driver
This is rare but can occur with highly restricted NFS configurations. As an alternative, you can edit the provisioner's driver configuration to point directly at the V3 sub-path:
# fuzzball volume provisioner edit <provisioner-name>
Change the path (hostpath) or target (NFS) to include the V3 sub-path. For example,
change path: /mnt/fuzzball-sharedfs to path: /mnt/fuzzball-sharedfs/persistent.