Skip to content

Commit b24dc20

Browse files
anuragawyadvr
authored andcommitted
FIX4: Allow ready volumes to be attached to VMs that have never been started once (#71)
With this patch apache/cloudstack@b766bf7 we started tracking disks in attaching state so that other attach request can fail gracefully. However this missed the case where disks were in allocated state but attach was requested. For the use case where users want to attach disk in allocated state but not ready, we need to have allocated-attaching transition as well. We must take care of returning to the original state - allocated or ready - when attach request has completed. For the use case of unstarted vm's the disk must proceed as follows - "Allocated" -> Attaching -> Allocated. When VM is started, the disk is "created" and pool is assigned. For the use case of started VMs it's more trivial and disk proceeds as follows - Ready -> Attaching -> Ready. Test this by creating a VM with "startvm=false", create a disk and try attaching it in allocated state. It would give an exception on latest 4.11 but will be fixed on this patch.
1 parent 6a82134 commit b24dc20

4 files changed

Lines changed: 103 additions & 48 deletions

File tree

api/src/com/cloud/storage/Volume.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ enum State {
5252
UploadInProgress("Volume upload is in progress"),
5353
UploadError("Volume upload encountered some error"),
5454
UploadAbandoned("Volume upload is abandoned since the upload was never initiated within a specificed time"),
55-
Attaching("The volume is attaching to a VM");
55+
Attaching("The volume is attaching to a VM from Ready State. Attach requests when in allocated state do not transit to this state.");
5656

5757
String _description;
5858

@@ -120,8 +120,8 @@ public String getDescription() {
120120
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(UploadError, Event.DestroyRequested, Destroy, null));
121121
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(UploadAbandoned, Event.DestroyRequested, Destroy, null));
122122
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Ready, Event.AttachRequested, Attaching, null));
123-
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationSucceeded, Ready, null));
124123
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationFailed, Ready, null));
124+
s_fsm.addTransition(new StateMachine2.Transition<State, Event>(Attaching, Event.OperationSucceeded, Ready, null));
125125
}
126126
}
127127

server/src/com/cloud/network/element/ConfigDriveNetworkElement.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import javax.inject.Inject;
2525

26-
import com.cloud.storage.StoragePool;
2726
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
2827
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
2928
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
@@ -59,6 +58,7 @@
5958
import com.cloud.service.dao.ServiceOfferingDao;
6059
import com.cloud.storage.DataStoreRole;
6160
import com.cloud.storage.Storage;
61+
import com.cloud.storage.StoragePool;
6262
import com.cloud.storage.Volume;
6363
import com.cloud.storage.VolumeVO;
6464
import com.cloud.storage.dao.GuestOSCategoryDao;

server/src/com/cloud/storage/VolumeApiServiceImpl.java

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,12 +1920,13 @@ private Volume orchestrateDetachVolumeFromVM(long vmId, long volumeId) {
19201920
// Mark the volume as detached
19211921
_volsDao.detachVolume(volume.getId());
19221922

1923-
// volume.getPoolId() should be null if the VM we are detaching the disk from has never been started before
1924-
DataStore dataStore = volume.getPoolId() != null ? dataStoreMgr.getDataStore(volume.getPoolId(), DataStoreRole.Primary) : null;
1925-
1926-
volService.revokeAccess(volFactory.getVolume(volume.getId()), host, dataStore);
1927-
1928-
handleTargetsForVMware(hostId, volumePool.getHostAddress(), volumePool.getPort(), volume.get_iScsiName());
1923+
// volumePool() should be null if the VM we are detaching the disk from has never been started before
1924+
// only revoke access on volumes that are actually on a datastore
1925+
if (volumePool != null) {
1926+
DataStore dataStore = dataStoreMgr.getDataStore(volume.getPoolId(), DataStoreRole.Primary);
1927+
volService.revokeAccess(volFactory.getVolume(volume.getId()), host, dataStore);
1928+
handleTargetsForVMware(hostId, volumePool.getHostAddress(), volumePool.getPort(), volume.get_iScsiName());
1929+
}
19291930

19301931
return _volsDao.findById(volumeId);
19311932
} else {
@@ -2634,21 +2635,16 @@ private boolean needMoveVolume(VolumeVO existingVolume, VolumeInfo newVolume) {
26342635
return !storeForExistingStoreScope.isSameScope(storeForNewStoreScope);
26352636
}
26362637

2637-
private synchronized void checkAndSetAttaching(Long volumeId, Long hostId) {
2638+
private synchronized void checkAndSetAttaching(Long volumeId) {
26382639
VolumeInfo volumeToAttach = volFactory.getVolume(volumeId);
26392640

26402641
if (volumeToAttach.isAttachedVM()) {
26412642
throw new CloudRuntimeException("volume: " + volumeToAttach.getName() + " is already attached to a VM: " + volumeToAttach.getAttachedVmName());
26422643
}
26432644
if (volumeToAttach.getState().equals(Volume.State.Ready)) {
26442645
volumeToAttach.stateTransit(Volume.Event.AttachRequested);
2645-
} else {
2646-
String error = null;
2647-
if (hostId == null) {
2648-
error = "Please try attach operation after starting VM once";
2649-
} else {
2650-
error = "Volume: " + volumeToAttach.getName() + " is in " + volumeToAttach.getState() + ". It should be in Ready state";
2651-
}
2646+
} else if (!volumeToAttach.getState().equals(Volume.State.Allocated)) {
2647+
final String error = "Volume: " + volumeToAttach.getName() + " is in " + volumeToAttach.getState() + ". It should be in Ready or Allocated state";
26522648
s_logger.error(error);
26532649
throw new CloudRuntimeException(error);
26542650
}
@@ -2684,7 +2680,7 @@ private VolumeVO sendAttachVolumeCommand(UserVmVO vm, VolumeVO volumeToAttach, L
26842680
// volumeToAttachStoragePool should be null if the VM we are attaching the disk to has never been started before
26852681
DataStore dataStore = volumeToAttachStoragePool != null ? dataStoreMgr.getDataStore(volumeToAttachStoragePool.getId(), DataStoreRole.Primary) : null;
26862682

2687-
checkAndSetAttaching(volumeToAttach.getId(), hostId);
2683+
checkAndSetAttaching(volumeToAttach.getId());
26882684

26892685
boolean attached = false;
26902686
try {
@@ -2773,9 +2769,10 @@ private VolumeVO sendAttachVolumeCommand(UserVmVO vm, VolumeVO volumeToAttach, L
27732769

27742770
volumeToAttach = _volsDao.findById(volumeToAttach.getId());
27752771

2776-
if (vm.getHypervisorType() == HypervisorType.KVM && volumeToAttachStoragePool.isManaged() && volumeToAttach.getPath() == null) {
2772+
if (volumeToAttach.getState().equals(Volume.State.Ready) &&
2773+
vm.getHypervisorType() == HypervisorType.KVM &&
2774+
volumeToAttachStoragePool.isManaged() && volumeToAttach.getPath() == null) {
27772775
volumeToAttach.setPath(volumeToAttach.get_iScsiName());
2778-
27792776
_volsDao.update(volumeToAttach.getId(), volumeToAttach);
27802777
}
27812778
}
@@ -2801,15 +2798,19 @@ private VolumeVO sendAttachVolumeCommand(UserVmVO vm, VolumeVO volumeToAttach, L
28012798
throw new CloudRuntimeException(errorMsg);
28022799
}
28032800
} finally {
2804-
Volume.Event ev = Volume.Event.OperationFailed;
2805-
VolumeInfo volInfo = volFactory.getVolume(volumeToAttach.getId());
2806-
if (attached) {
2807-
ev = Volume.Event.OperationSucceeded;
2808-
s_logger.debug("Volume: " + volInfo.getName() + " successfully attached to VM: " + volInfo.getAttachedVmName());
2809-
} else {
2810-
s_logger.debug("Volume: " + volInfo.getName() + " failed to attach to VM: " + volInfo.getAttachedVmName());
2801+
final VolumeInfo volInfo = volFactory.getVolume(volumeToAttach.getId());
2802+
// Transit events are only fired when volume was allocated at some point of time.
2803+
if (volInfo.getPoolId() != null || volInfo.getLastPoolId() != null) {
2804+
final Volume.Event ev;
2805+
if (attached) {
2806+
ev = Volume.Event.OperationSucceeded;
2807+
s_logger.debug("Volume: " + volInfo.getName() + " successfully attached to VM: " + volInfo.getAttachedVmName());
2808+
} else {
2809+
ev = Volume.Event.OperationFailed;
2810+
s_logger.debug("Volume: " + volInfo.getName() + " failed to attach to VM: " + volInfo.getAttachedVmName());
2811+
}
2812+
volInfo.stateTransit(ev);
28112813
}
2812-
volInfo.stateTransit(ev);
28132814
}
28142815
return _volsDao.findById(volumeToAttach.getId());
28152816
}

test/integration/smoke/test_volumes.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -398,40 +398,40 @@ def test_02_attach_volume(self):
398398
# 3. disk should be attached to instance successfully
399399

400400
self.debug(
401-
"Attaching volume (ID: %s) to VM (ID: %s)" % (
402-
self.volume.id,
403-
self.virtual_machine.id
404-
))
401+
"Attaching volume (ID: %s) to VM (ID: %s)" % (
402+
self.volume.id,
403+
self.virtual_machine.id
404+
))
405405
self.virtual_machine.attach_volume(self.apiClient, self.volume)
406406
self.attached = True
407407
list_volume_response = Volume.list(
408-
self.apiClient,
409-
id=self.volume.id
410-
)
408+
self.apiClient,
409+
id=self.volume.id
410+
)
411411
self.assertEqual(
412-
isinstance(list_volume_response, list),
413-
True,
414-
"Check list response returns a valid list"
415-
)
412+
isinstance(list_volume_response, list),
413+
True,
414+
"Check list response returns a valid list"
415+
)
416416
self.assertNotEqual(
417-
list_volume_response,
418-
None,
419-
"Check if volume exists in ListVolumes"
420-
)
417+
list_volume_response,
418+
None,
419+
"Check if volume exists in ListVolumes"
420+
)
421421
volume = list_volume_response[0]
422422
self.assertNotEqual(
423-
volume.virtualmachineid,
424-
None,
425-
"Check if volume state (attached) is reflected"
426-
)
423+
volume.virtualmachineid,
424+
None,
425+
"Check if volume state (attached) is reflected"
426+
)
427427
try:
428428
#Format the attached volume to a known fs
429429
format_volume_to_ext3(self.virtual_machine.get_ssh_client())
430430

431431
except Exception as e:
432432

433433
self.fail("SSH failed for VM: %s - %s" %
434-
(self.virtual_machine.ipaddress, e))
434+
(self.virtual_machine.ipaddress, e))
435435
return
436436

437437
@attr(tags = ["advanced", "advancedns", "smoke", "basic"], required_hardware="false")
@@ -857,6 +857,60 @@ def test_10_list_volumes(self):
857857
self.assertTrue(hasattr(root_volume, "podname"))
858858
self.assertEqual(root_volume.podname, list_pods.name)
859859

860+
@attr(tags = ["advanced", "advancedns", "smoke", "basic"], required_hardware="true")
861+
def test_11_attach_volume_with_unstarted_vm(self):
862+
"""Attach a created Volume to a unstarted VM
863+
"""
864+
# Validate the following
865+
# 1. Attach to a vm in startvm=false state works and vm can be started afterwards.
866+
# 2. shows list of volumes
867+
# 3. "Attach Disk" pop-up box will display with list of instances
868+
# 4. disk should be attached to instance successfully
869+
870+
test_vm = VirtualMachine.create(
871+
self.apiclient,
872+
self.services,
873+
accountid=self.account.name,
874+
domainid=self.account.domainid,
875+
serviceofferingid=self.service_offering.id,
876+
mode=self.services["mode"],
877+
startvm=False
878+
)
879+
880+
self.debug(
881+
"Attaching volume (ID: %s) to VM (ID: %s)" % (
882+
self.volume.id,
883+
test_vm.id
884+
))
885+
test_vm.attach_volume(self.apiClient, self.volume)
886+
test_vm.start(self.apiClient)
887+
888+
list_volume_response = Volume.list(
889+
self.apiClient,
890+
id=self.volume.id
891+
)
892+
self.assertEqual(
893+
isinstance(list_volume_response, list),
894+
True,
895+
"Check list response returns a valid list"
896+
)
897+
self.assertNotEqual(
898+
list_volume_response,
899+
None,
900+
"Check if volume exists in ListVolumes"
901+
)
902+
volume = list_volume_response[0]
903+
self.assertNotEqual(
904+
volume.virtualmachineid,
905+
None,
906+
"Check if volume state (attached) is reflected"
907+
)
908+
909+
test_vm.detach_volume(self.apiClient, self.volume)
910+
self.cleanup.append(test_vm)
911+
912+
return
913+
860914
def wait_for_attributes_and_return_root_vol(self):
861915
def checkVolumeResponse():
862916
list_volume_response = Volume.list(

0 commit comments

Comments
 (0)