DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.

DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
KVM clusters that use shared block storage (SAN, iSCSI, Fibre Channel) manage disks as LVM Logical Volumes inside a Volume Group visible across all nodes. LVM's cluster-aware variants - CLVM and its modern successor CLVM_NG using `lvmlockd`/`sanlock` - provide the locking infrastructure that prevents simultaneous writes from multiple hosts.
CloudStack today has no first-class understanding of these pool types. When a VM migrates or a volume is attached to a different VM, CloudStack performs a full volume data copy even when the data has not moved at all. Worse, after a migration CloudStack has no record of which host holds the current LVM exclusive lock, so the next VM start may fail because the lock is stale on the wrong host.
CLVM/CLVM_NG are Linux LVM construct and this design only targets KVM.
LVM Activation Modes
| Flag | Meaning |
|---|---|
| lvchange -aey <lv> | Activate exclusively - one host only |
| lvchange -asy <lv> | Activate shared : read-only on all cluster nodes |
| lvchange -an <lv> | Deactivate |
Architecture Overview:
┌─────────────────────────────────────────────────────────────┐
│ CloudStack Management Server │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌───▼─────────┐ ┌────▼──────┐ ┌───────▼────────┐
│ VolumeApi │ │ Volume │ │ Default │
│ ServiceImpl │ │Orchestrator│ │EndPointSelector│
│ │ │ │ │ │
│ • attach │ │ • VM start │ │ • Route ops │
│ • detach │ │ • migrate │ │ to lock host │
│ • migrate │ │ • prepare │ │ • Query lock │
└─────────────┘ └────────────┘ └────────────────┘
│ │ │
└────────────────┼────────────────┘
│
┌─────────▼─────────┐
│ VolumeServiceImpl │
│ │
│ • performLock │
│ Migration │
│ • findLockHost │
│ • isLightweight │
│ Needed │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ ClvmLockManager │
│ │
│ • transferVolume │
│ Lock │
│ • queryCurrent │
│ LockHolder │
│ • setClvmLock │
│ HostId │
└─────────┬─────────┘
│ (AgentManager)
│
┌───────────────┴───────────────┐
│ │
┌───────▼────────┐ ┌───────▼────────┐
│ KVM Agent 1 │ │ KVM Agent N │
│ (Host A) │ │ (Host B) │
│ │ │ │
│ LibvirtClvm │ │ LibvirtClvm │
│ LockTransfer │ │ LockTransfer │
│ Wrapper │ │ Wrapper │
│ │ │ │
│ lvchange -an │ │ lvchange -aey │
│ lvchange -aey │ │ lvs -o ... │
│ lvs │ │ │
└────────────────┘ └────────────────┘
│ │
└───────────────┬───────────────┘
│
┌───────────▼────────────┐
│ Shared Storage (SAN) │
│ LVM Volume Group │
│ /dev/vg-cluster01/... │
└────────────────────────┘
lvs, not the database.Lock Model and State Management
LVM Lock Types
Exclusive Lock: -aey
Shared Lock: -asy
Deactivated:-an
LV Attribute Interpretation
The lvs command returns attributes that reveal lock state:
$ lvs -o lv_name,lv_attr,lv_host --noheadings /dev/vg0/vm-123-disk-0 vm-123-disk-0 -wi-a-e--- host5.example.com
Position 4 (0-indexed): Active flag
Position 5 (0-indexed): Exclusivity flag
lv_host field: Hostname of the lock holder
Database State Tracking
Table: volume_details
Key: clvmLockHostId (constant in {{ClvmPoolManager.java}})
Value: Host ID (Long)
This is a cache/hint, not the source of truth. It can become stale when:
Reconciliation: ClvmLockManager.queryCurrentLockHolder() queries actual LVM state and optionally updates the database.
Core Components
public enum Operation {
DEACTIVATE("-an", "deactivate"),
ACTIVATE_EXCLUSIVE("-aey", "activate exclusively"),
ACTIVATE_SHARED("-asy", "activate in shared mode"),
QUERY_LOCK_STATE("query", "query lock state");
}
| Property | Value | Description |
|---|---|---|
| executeInSequence() | true | Prevents concurrent lock operations on same LV |
| setWait(30) | 30 secs | Agent timeout per operation |
| Fields | lvPath, operation, volumeUuid | command parameters |
| Field | Type | Description |
|---|---|---|
| currentLockHostname | String | Hostname holding the lock from lv_host field |
| isActive | boolean | Whether LV is active anywhere |
isExclusive | boolean | Weather the active lock is exclusive |
| lvAttributes | String | Raw lv_attr string for debugging |
getClvmLockHostId(volumeId, volumeUuid, volumePath, pool, queryActual)
Purpose: Returns the host ID holding the exclusive lock
if queryActual == true: return queryCurrentLockHolder(...) // Query LVM directly else: return database value from volume_details.clvmLockHostId
Parameters:
queryActual=true: Bypass database, query LVM state (expensive but accurate)
queryActual=false: Fast database lookup (may be stale)
queryCurrentLockHolder(volumeId, volumeUuid, volumePath, pool, updateDatabase)
Purpose: Queries actual LVM lock state from KVM hosts
queryCurrentLockHolder(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:
│ ├─ log warning "shared-mode LV (template?), skipping"
│ └─ return null
└─ Else (exactly one):
├─ If updateDatabase and lockHostId != dbHostId:
│ setClvmLockHostId(volumeId, lockHostId) ← correct stale DB record
└─ return lockHostId
Performance Considerations:
Network overhead
Worst case: Queries all hosts in cluster/zone
transferClvmVolumeLock(volumeUuid, volumeId, volumePath, pool, sourceHostId, destHostId)
Purpose: Transfers exclusive lock from source to destination host
1. hostToDeactivate = sourceHostId
// Use actual holder if known, fallback to provided source
2. 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"
3. send ACTIVATE_EXCLUSIVE command to destHostId
// LVM force-claims if previous holder left cleanly
4. if activation successful:
├─ setClvmLockHostId(volumeId, destHostId)
└─ return true
5. return false
setClvmLockHostId(volumeId, hostId)
Purpose: Updates the lock host ID in the database
existingDetail = volumeDetailsDao.findDetail(volumeId, CLVM_LOCK_HOST_ID)
if existingDetail != null:
existingDetail.setValue(String.valueOf(hostId))
volumeDetailsDao.update(existingDetail.getId(), existingDetail)
else:
volumeDetailsDao.addDetail(volumeId, CLVM_LOCK_HOST_ID,
String.valueOf(hostId), false)
It's the KVM agent side handler.
Execution Flow:
1. Receive ClvmLockTransferCommand from management server 2. if operation == QUERY_LOCK_STATE: return handleQueryLockState() 3. Map operation to lvchange flag: DEACTIVATE → "-an" ACTIVATE_EXCLUSIVE → "-aey" ACTIVATE_SHARED → "-asy" 4. Execute: /usr/sbin/lvchange <flag> <lvPath> timeout: 30 seconds 5. if command fails: return ClvmLockTransferAnswer(success=false, details=error) 6. return ClvmLockTransferAnswer(success=true)
Query Lock State Flow:
1. Execute: /usr/sbin/lvs -o lv_attr,lv_host --noheadings <lvPath>
2. Parse output:
" -wi-a-e--- host5.example.com"
3. Extract:
lvAttr = "-wi-a-e---"
hostname = "host5.example.com"
4. Derive state:
isActive = (lvAttr[4] == 'a')
isExclusive = (lvAttr[5] == 'e')
5. return ClvmLockTransferAnswer(
success=true,
currentLockHostname=hostname,
isActive=isActive,
isExclusive=isExclusive,
lvAttributes=lvAttr
)
Volume creation with destinationHostId as hint
select(VolumeInfo volume, String operation):
if volume.getDestinationHostId() != null:
if pool is CLVM type:
return getEndPointFromHostId(volume.getDestinationHostId())
// Ensures volume is created with exclusive lock on target host
Volume operations - existing volumes
select(DataObject object):
if object is VolumeInfo AND pool is CLVM type:
lockHostId = clvmLockManager.getClvmLockHostId(..., queryActual=true)
if lockHostId != null:
return getEndPointFromHostId(lockHostId)
// Route to host holding the lock
return default endpoint selection
Copy Operations - Primary to secondary
select(DataObject srcData, DataObject destData):
if srcData is VolumeInfo AND srcPool is CLVM type:
lockHostId = getClvmLockHostId(srcVolume)
if lockHostId != null:
return getEndPointFromHostId(lockHostId)
// Read from lock holder (volume is active there)
When creating or accessing CLVM volumes, CloudStack uses this priority:
VolumeOrchestrator.createVolume()
Determine target host for VM placement
(via DeploymentPlanner)
2. if pool is CLVM type:
volume.setDestinationHostId(targetHostId)
// Pre-set destination hint
3. Create volume via VolumeServiceImpl:
├─ DefaultEndPointSelector routes to targetHostId
└─ KVM agent executes:
lvcreate -L <size> -n <lv_name> <vg_name>
lvchange -aey /dev/<vg>/<lv>
4. setClvmLockHostId(volumeId, targetHostId)
Result: Volume created with exclusive lock already on the host where VM will start.
VolumeServiceImpl.deleteVolume()
Delete volume via storage driver: ├─ KVM agent executes: │ lvchange -an /dev/<vg>/<lv> │ lvremove /dev/<vg>/<lv> 2. clvmLockManager.clearClvmLockHostDetail(volume) // Remove database tracking 3. volumeDao.remove(volumeId)
Considers StoragePool level setting `clvm.secure.zero.fill` if when set to true, zero fills the lv before deletion for security reasons. Recommended to be set to true in production
VolumeApiServiceImpl.attachVolumeToVM()
if volume and VM are on different pools:
if both pools point to same VG:
// Lightweight migration
executeLightweightLockMigration(volume, vm)
else:
// Full migration
volumeMigrationService.migrateVolume(volume, destPool)
else:
// Same pool
if volume lock is on different host than VM:
// Lock transfer
volService.performLockMigration(volume, vm.getHostId())
// Actual libvirt attach
attachVolume(vm, volume)
VolumeApiServiceImpl.detachVolume()
1. Detach volume via libvirt API (volume remains activated exclusively on host) 2. volume_details.clvmLockHostId remains unchanged // Lock is retained on the host where VM ran 3. On next attach: ├─ queryCurrentLockHolder() will find actual state └─ performLockMigration() if needed
Note: Keeping the lock on detach avoids unnecessary lock churn if volume is quickly re-attached to the same VM.
VolumeOrchestrator.prepare()
1. Select destination host via DeploymentPlanner
2. transferClvmLocksForVmStart(volumes, destHostId, vm):
for each volume in VM's volumes:
if pool is not CLVM type:
continue
currentLockHost = clvmLockManager.getClvmLockHostId(..., queryActual=true)
if currentLockHost == null:
clvmLockManager.setClvmLockHostId(volume.getId(), destHostId)
else if currentLockHost != destHostId:
transferClvmVolumeLock(volume, currentLockHost, destHostId)
3. Start VM via libvirt
Note: All CLVM volumes are transferred to the VM's host *before* VM start, ensuring all disks are accessible when QEMU launches.
VirtualMachineManagerImpl.stop()
1. Stop VM via libvirt 2. CLVM volumes remain activated exclusively on the stopped VM's host // No lock release on stop 3. volume_details.clvmLockHostId remains unchanged
Note: Locks are retained to optimize quick restarts. Next start will transfer locks if VM is scheduled to a different host.
VirtualMachineManagerImpl.migrate()
1. Pre-migration checks and preparation: ├─ volumeMgr.prepareForMigration(profile, dest) ├─ Generate MigrateCommand with VM and disk information ├─ PreMigrationCommand already executed (Phase 0: Source → SHARED) └─ PrepareForMigrationCommand already executed (Phase 1: Dest → SHARED) 2. Initiate libvirt live migration: ├─ Command: domain.migrate(destConn, xmlDesc, migrateFlags) ├─ Libvirt uses shared storage ├─ Transfers memory state over network ├─ QEMU on destination opens block devices └─ BOTH hosts have volumes in SHARED mode (dual activation) 3. After successful migration: ├─ Deactivate CLVM volumes on source host: │ LibvirtComputingResource.modifyClvmVolumesStateForMigration( │ disks, resource, vmSpec, ClvmVolumeState.DEACTIVATE) │ → executes: lvchange -an /dev/<vg>/<lv> on source │ → Destination volumes remain ACTIVE in SHARED mode │ └─ Update CloudStack database tracking: updateClvmLockHostForVmVolumes(vm.getId(), destHost.getId()) → sets CLVM_LOCK_HOST_ID = destHost for all volumes 4. On migration failure: ├─ Revert CLVM volumes to EXCLUSIVE mode on source: │ LibvirtComputingResource.modifyClvmVolumesStateForMigration( │ disks, resource, vmSpec, ClvmVolumeState.EXCLUSIVE) │ → executes: lvchange -aey /dev/<vg>/<lv> on source │ └─ Destination volumes are deactivated/cleaned up via rollback
VirtualMachineManagerImpl.migrate()
1. VM is stopped, volumes have locks on old host 2. Start VM on new host: ├─ calls VolumeOrchestrator.prepare() └─ transferClvmLocksForVmStart() transfers all locks 3. VM starts on new host with all locks in place
cloudmonkey create storagepool \ zoneid=<zone-id> \ podid=<pod-id> \ clusterid=<cluster-id> \ name="clvm-pool-01" \ url="clvm:///<vg-name>" \ scope=cluster cloudmonkey create storagepool \ zoneid=<zone-id> \ podid=<pod-id> \ clusterid=<cluster-id> \ name="clvm-ng-pool-01" \ url="clvm_ng:///<vg-name>" \ scope=cluster
| Setting | Default | Scope | Description |
|---|---|---|---|
| clvm.secure.zero.fill | false | StoragePool | Zero-fill LVs before deletion to prevent data leakage |
When enabled, KVM agents execute dd if=/dev/zero of=<lv> before lvremove. This prevents the next VM allocated to that storage space from reading previous tenant data.
Performance impact: Increases deletion time proportional to volume size. Recommended for multi-tenant environments with strict security requirements.