Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  1.  Introduce `CLVM` and `CLVM_NG` as first-class `StoragePoolType` entries in CloudStack. CLVM supports RAW volumes on LVM and CLVM_NG support QCOW2.
  2. Design a lock-tracking layer that records and reconciles which KVM host holds the exclusive LVM activation for each volume.
  3. Implement a lock-transfer mechanism so CloudStack can safely move the exclusive lock between hosts without copying data.
  4. Introduce a three-tier migration strategy that avoids unnecessary data copying:
       - Lock Transfer - same pool, wrong host: transfer lock only.
       - Lightweight Migration - different CloudStack pool records sharing the same LVM Volume Group: transfer lock only.Same LV but different hosts - no data copy, just lock transfer
       - Full Migration - different Volume Groups: full data copy (existing behavior).
  5. Provide a secure-erase option for LVs at deletion time to prevent data leakage.
  6. Incremental snapshots for CLVM_NG leveraging bitmap - not supported in 4.23
    Note: Only full snapshots will be supported for clvm and clvm-ng


LVM Activation Modes

FlagMeaning
lvchange -aey <lv>Activate exclusively - one host only
lvchange -asy <lv>Activate shared : read-only on all cluster nodes
lvchange -an <lv>Deactivate

...

  • Source of truth: The actual LVM state is obtained via lvs, not the database.
  • Database as cache: volume_details.clvmLockHostId tracks which host has the lock for debugging purposes, but may be stale.
  • Sequential operations: Lock transfers execute sequentially via executeInSequence() to prevent races.
  • Idempotency: Lock transfer to the same host is a no-op

...

Table: volume_details
Key: clvmLockHostId (constant in {{VolumeInfoClvmPoolManager.java}})
Value: Host ID (Long)

...

          Purpose: Queries actual LVM lock state from KVM hosts

Code Block
1. Resolve hosts for pool:
   ├─ If pool.getClusterId() != null:
   │    hosts = hostDao.findByClusterId(clusterId, Type.Routing)
   └─ Else if zone-scoped:
        hosts = hostDao.findByDataCenterId(zoneId)

2. Filter to UP KVM routing hosts

3. For each host in hosts:
   ├─ Send QUERY_LOCK_STATE command
   ├─ If successful:
   │  ├─ Parse ClvmLockTransferAnswerqueryCurrentLockHolder(volumeId, volumeUuid, volumePath, pool, updateDatabase):
├─ lvPath = "/dev/{vgName}/{volumePath}"
│
├─ [Fast path] dbHostId = DB lookup (volume_details.clvmLockHostId)
│  └─ If dbHostId != null:
│     ├─ If dbHost is Up and KVM:
│     │  ├─ Send QUERY_LOCK_STATE to dbHostId
│     │  ├─ If active == TRUE:
│     │  │    return dbHostId                        // confirmed, early exit
│     │  └─ Else: log "fast path miss, falling back to fan-out"
│     └─ Else: log "fast path skip (host down/missing), falling back to fan-out"
│
├─ [Fan-out] Resolve hosts for pool:
│  ├─ If pool.getClusterId() != null:
│  │    hosts = hostDao.findByClusterId(clusterId, Type.Routing)
│  └─ Else if zone-scoped:
│       hosts = hostDao.findByDataCenterId(zoneId)
│
├─ Filter to UP KVM routing hosts, skip dbHostId (already checked above)
│
├─ For each host in hosts:
│  ├─ Send QUERY_LOCK_STATE command
│  └─ If active == TRUE: add to activeHostIds
│
└─ Evaluate results:
   ├─ If activeHostIds is empty:
   │  ├─ If updateDatabase and dbHostId != null:
   │  │    remove CLVM_LOCK_HOST_ID from volume_details
   │  └─ return null
   ├─ If activeHostIds.size > 1:
   │  ├─ Extract hostname from lv_hostlog warning "shared-mode LV (template?), skipping"├─└─ Resolve hostname → host IDreturn null
   └─ Else ├─ if updateDatabase(exactly one):
      ├─ If updateDatabase setClvmLockHostId(volumeId, hostId)
   │  └─ return hostId
   └─ If failed, try next host

4. If all hosts fail:
   ├─ Log warning
   └─ return nulland lockHostId != dbHostId:
      │    setClvmLockHostId(volumeId, lockHostId)   ← correct stale DB record
      └─ return lockHostId


Performance Considerations:

...

Purpose: Transfers exclusive lock from source to destination host

Code Block
1. actualLockHostId = queryCurrentLockHolder(..., queryActual=false)
   // Get real lock holder, not database hint

2. hostToDeactivate = actualLockHostId ?? sourceHostId
   // Use actual holder if known, fallback to provided source

32. if hostToDeactivate != null AND hostToDeactivate != destHostId:
   ├─ if host is UP:
   │    send DEACTIVATE command to hostToDeactivate
   └─ if host is DOWN:
        log warning "Host down, will force claim on destination"

43. send ACTIVATE_EXCLUSIVE command to destHostId
   // LVM force-claims if previous holder left cleanly

54. if activation successful:
   ├─ setClvmLockHostId(volumeId, destHostId)
   └─ return true

65. return false


setClvmLockHostId(volumeId, hostId)

...