diff --git a/api/src/main/java/com/cloud/storage/VolumeApiService.java b/api/src/main/java/com/cloud/storage/VolumeApiService.java index ec20c33e17eb..91b0bc0712fe 100644 --- a/api/src/main/java/com/cloud/storage/VolumeApiService.java +++ b/api/src/main/java/com/cloud/storage/VolumeApiService.java @@ -79,6 +79,8 @@ public interface VolumeApiService { Volume attachVolumeToVM(AttachVolumeCmd command); + Volume detachVolumeViaDestroyVM(long vmId, long volumeId); + Volume detachVolumeFromVM(DetachVolumeCmd cmd); Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index f03ddc7465c3..5a8cbe546a39 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -726,6 +726,8 @@ public class ApiConstants { public static final String EXITCODE = "exitcode"; public static final String TARGET_ID = "targetid"; + public static final String VOLUME_IDS = "volumeids"; + public enum HostDetails { all, capacity, events, stats, min; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java index 730c67767728..7b359b790477 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DestroyVMCmd.java @@ -31,6 +31,7 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.context.CallContext; import com.cloud.event.EventTypes; @@ -63,6 +64,14 @@ public class DestroyVMCmd extends BaseAsyncCmd { since = "4.2.1") private Boolean expunge; + @Parameter( name = ApiConstants.VOLUME_IDS, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = VolumeResponse.class, + description = "Comma separated list of UUIDs for volumes that will be deleted", + since = "4.12.0") + private List volumeIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -78,6 +87,10 @@ public boolean getExpunge() { return expunge; } + public List getVolumeIds() { + return volumeIds; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java index 3fcf7618ade3..04f8edaf9953 100644 --- a/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java @@ -1849,6 +1849,14 @@ private void validateRootVolumeDetachAttach(VolumeVO volume, UserVmVO vm) { } } + @ActionEvent(eventType = EventTypes.EVENT_VOLUME_DETACH, eventDescription = "detaching volume") + public Volume detachVolumeViaDestroyVM(long vmId, long volumeId) { + + Volume result = orchestrateDetachVolumeFromVM(vmId, volumeId); + + return result; + } + private Volume orchestrateDetachVolumeFromVM(long vmId, long volumeId) { Volume volume = _volsDao.findById(volumeId); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 86c87ed02b8d..ea09f2b8f319 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -16,86 +16,6 @@ // under the License. package com.cloud.vm; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import javax.inject.Inject; -import javax.naming.ConfigurationException; - -import org.apache.cloudstack.acl.ControlledEntity.ACLType; -import org.apache.cloudstack.acl.SecurityChecker.AccessType; -import org.apache.cloudstack.affinity.AffinityGroupService; -import org.apache.cloudstack.affinity.AffinityGroupVO; -import org.apache.cloudstack.affinity.dao.AffinityGroupDao; -import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.BaseCmd.HTTPMethod; -import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; -import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; -import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; -import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; -import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; -import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; -import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; -import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; -import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; -import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; -import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; -import org.apache.cloudstack.api.command.user.vm.StartVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; -import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; -import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; -import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; -import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; -import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; -import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.service.api.OrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; -import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; -import org.apache.cloudstack.framework.async.AsyncCallFuture; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.Configurable; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.managed.context.ManagedContextRunnable; -import org.apache.cloudstack.storage.command.DeleteCommand; -import org.apache.cloudstack.storage.command.DettachCommand; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.Command; @@ -301,6 +221,84 @@ import com.cloud.vm.snapshot.VMSnapshotManager; import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.dao.VMSnapshotDao; +import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.affinity.AffinityGroupService; +import org.apache.cloudstack.affinity.AffinityGroupVO; +import org.apache.cloudstack.affinity.dao.AffinityGroupDao; +import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd.HTTPMethod; +import org.apache.cloudstack.api.command.admin.vm.AssignVMCmd; +import org.apache.cloudstack.api.command.admin.vm.RecoverVMCmd; +import org.apache.cloudstack.api.command.user.vm.AddNicToVMCmd; +import org.apache.cloudstack.api.command.user.vm.DeployVMCmd; +import org.apache.cloudstack.api.command.user.vm.DestroyVMCmd; +import org.apache.cloudstack.api.command.user.vm.RebootVMCmd; +import org.apache.cloudstack.api.command.user.vm.RemoveNicFromVMCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMPasswordCmd; +import org.apache.cloudstack.api.command.user.vm.ResetVMSSHKeyCmd; +import org.apache.cloudstack.api.command.user.vm.RestoreVMCmd; +import org.apache.cloudstack.api.command.user.vm.ScaleVMCmd; +import org.apache.cloudstack.api.command.user.vm.SecurityGroupAction; +import org.apache.cloudstack.api.command.user.vm.StartVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateDefaultNicForVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVMCmd; +import org.apache.cloudstack.api.command.user.vm.UpdateVmNicIpCmd; +import org.apache.cloudstack.api.command.user.vm.UpgradeVMCmd; +import org.apache.cloudstack.api.command.user.vmgroup.CreateVMGroupCmd; +import org.apache.cloudstack.api.command.user.vmgroup.DeleteVMGroupCmd; +import org.apache.cloudstack.api.command.user.volume.ResizeVolumeCmd; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity; +import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.service.api.OrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService; +import org.apache.cloudstack.engine.subsystem.api.storage.VolumeService.VolumeApiResult; +import org.apache.cloudstack.framework.async.AsyncCallFuture; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.cloudstack.storage.command.DeleteCommand; +import org.apache.cloudstack.storage.command.DettachCommand; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import javax.naming.ConfigurationException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; public class UserVmManagerImpl extends ManagerBase implements UserVmManager, VirtualMachineGuru, UserVmService, Configurable { @@ -2751,10 +2749,15 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C // check if VM exists UserVmVO vm = _vmDao.findById(vmId); - if (vm == null) { + if (vm == null || vm.getRemoved() != null) { throw new InvalidParameterValueException("unable to find a virtual machine with id " + vmId); } + if (vm.getState() == State.Destroyed || vm.getState() == State.Expunging) { + s_logger.trace("Vm id=" + vmId + " is already destroyed"); + return vm; + } + // check if there are active volume snapshots tasks s_logger.debug("Checking if there are any ongoing snapshots on the ROOT volumes associated with VM with ID " + vmId); if (checkStatusOfVolumeSnapshots(vmId, Volume.Type.ROOT)) { @@ -2762,6 +2765,12 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } s_logger.debug("Found no ongoing snapshots on volume of type ROOT, for the vm with id " + vmId); + List volumes = getVolumesFromIds(cmd); + + checkForUnattachedVolumes(vmId, volumes); + validateVolumes(volumes); + detachVolumesFromVm(volumes); + UserVm destroyedVm = destroyVm(vmId, expunge); if (expunge) { if (!expunge(vm, ctx.getCallingUserId(), ctx.getCallingAccount())) { @@ -2769,9 +2778,26 @@ public UserVm destroyVm(DestroyVMCmd cmd) throws ResourceUnavailableException, C } } + deleteVolumesFromVm(volumes); + return destroyedVm; } + private List getVolumesFromIds(DestroyVMCmd cmd) { + List volumes = new ArrayList<>(); + if (cmd.getVolumeIds() != null) { + for (Long volId : cmd.getVolumeIds()) { + VolumeVO vol = _volsDao.findById(volId); + + if (vol == null) { + throw new InvalidParameterValueException("Unable to find volume with ID: " + volId); + } + volumes.add(vol); + } + } + return volumes; + } + @Override @DB public InstanceGroupVO createVmGroup(CreateVMGroupCmd cmd) { @@ -6446,4 +6472,52 @@ private boolean checkStatusOfVolumeSnapshots(long vmId, Volume.Type type) { } return false; } + + private void checkForUnattachedVolumes(long vmId, List volumes) { + + StringBuilder sb = new StringBuilder(); + + for (VolumeVO volume : volumes) { + if (volume.getInstanceId() == null || vmId != volume.getInstanceId()) { + sb.append(volume.toString() + "; "); + } + } + + if (!StringUtils.isEmpty(sb.toString())) { + throw new InvalidParameterValueException("The following supplied volumes are not attached to the VM: " + sb.toString()); + } + } + + private void validateVolumes(List volumes) { + + for (VolumeVO volume : volumes) { + if (!(volume.getVolumeType() == Volume.Type.ROOT || volume.getVolumeType() == Volume.Type.DATADISK)) { + throw new InvalidParameterValueException("Please specify volume of type " + Volume.Type.DATADISK.toString() + " or " + Volume.Type.ROOT.toString()); + } + } + } + + private void detachVolumesFromVm(List volumes) { + + for (VolumeVO volume : volumes) { + + Volume detachResult = _volumeService.detachVolumeViaDestroyVM(volume.getInstanceId(), volume.getId()); + + if (detachResult == null) { + s_logger.error("DestroyVM remove volume - failed to detach and delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); + } + } + } + + private void deleteVolumesFromVm(List volumes) { + + for (VolumeVO volume : volumes) { + + boolean deleteResult = _volumeService.deleteVolume(volume.getId(), CallContext.current().getCallingAccount()); + + if (!deleteResult) { + s_logger.error("DestroyVM remove volume - failed to delete volume " + volume.getInstanceId() + " from instance " + volume.getId()); + } + } + } } \ No newline at end of file diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index 5906a94869cc..10ef1534e435 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -32,7 +32,9 @@ Host, Iso, Router, - Configurations) + Configurations, + Volume, + DiskOffering) from marvin.lib.common import (get_domain, get_zone, get_template, @@ -786,6 +788,46 @@ def test_10_attachAndDetach_iso(self): ) return + @attr(tags = ["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false") + def test_11_destroy_vm_and_volumes(self): + """Test destroy Virtual Machine and it's volumes + """ + + # Validate the following + # 1. Deploys a VM and attaches disks to it + # 2. Destroys the VM with DataDisks option + + small_disk_offering = DiskOffering.list(self.apiclient, name='Small')[0] + + small_virtual_machine = VirtualMachine.create( + self.apiclient, + self.services["small"], + accountid=self.account.name, + domainid=self.account.domainid, + serviceofferingid=self.small_offering.id, + mode=self.services["mode"] + ) + vol1 = Volume.create( + self.apiclient, + self.services, + account=self.account.name, + diskofferingid=small_disk_offering.id, + domainid=self.account.domainid, + zoneid=self.zone.id + ) + + small_virtual_machine.attach_volume(self.apiclient, vol1) + + self.debug("Destroy VM - ID: %s" % small_virtual_machine.id) + small_virtual_machine.delete(self.apiclient, volumeIds=vol1.id) + + self.assertEqual(VirtualMachine.list(self.apiclient, id=small_virtual_machine.id), None, "List response contains records when it should not") + + self.assertEqual(Volume.list(self.apiclient, id=vol1.id), None, "List response contains records when it should not") + + return + + class TestSecuredVmMigration(cloudstackTestCase): @classmethod diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index ca85bb0bee30..148ea533a378 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -4180,6 +4180,15 @@ textarea { float: left; } +.ui-dialog div.form-container div.value label { + display: block; + width: 119px; + text-align: left; + font-size: 13px; + margin-top: 2px; + margin-left: -10px; +} + .ui-dialog div.form-container div.value input.hasDatepicker { color: #2F5D86; cursor: pointer; diff --git a/ui/l10n/en.js b/ui/l10n/en.js index e16031d9e8a3..8d1666357e77 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -639,6 +639,7 @@ var dictionary = { "label.delete.secondary.staging.store":"Delete Secondary Staging Store", "label.delete.sslcertificate":"Delete SSL Certificate", "label.delete.ucs.manager":"Delete UCS Manager", +"label.delete.volumes":"Volumes to be deleted", "label.delete.vpn.user":"Delete VPN user", "label.deleting.failed":"Deleting Failed", "label.deleting.processing":"Deleting....", @@ -1807,6 +1808,8 @@ var dictionary = { "label.volgroup":"Volume Group", "label.volume":"Volume", "label.volume.details":"Volume details", +"label.volume.empty":"No volumes attached to this VM", +"label.volume.ids":"Volume ID's", "label.volume.limits":"Volume Limits", "label.volume.migrated":"Volume migrated", "label.volume.name":"Volume Name", diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 0948db48d071..f5c4f5ec2744 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -112,6 +112,34 @@ label: 'label.expunge', isBoolean: true, isChecked: false + }, + volumes: { + label: 'label.delete.volumes', + isBoolean: true, + isChecked: false + }, + volumeids: { + label: 'label.volume.ids', + dependsOn: 'volumes', + isBoolean: true, + isHidden: true, + emptyMessage: 'label.volume.empty', + multiDataArray: true, + multiData: function(args) { + $.ajax({ + url: createURL("listVolumes&virtualMachineId=" + args.context.instances[0].id) + "&type=DATADISK", + dataType: "json", + async: true, + success: function(json) { + var volumes = json.listvolumesresponse.volume; + args.response.success({ + descriptionField: 'name', + valueField: 'id', + data: volumes + }); + } + }); + } } } }, @@ -126,6 +154,26 @@ expunge: true }); } + if (args.data.volumes == 'on') { + + var regex = RegExp('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'); + + var selectedVolumes = []; + + for (var key in args.data) { + var matches = key.match(regex); + + if (matches != null) { + selectedVolumes.push(key); + } + } + + $.extend(data, { + volumeids: $(selectedVolumes).map(function(index, volume) { + return volume; + }).toArray().join(',') + }); + } $.ajax({ url: createURL('destroyVirtualMachine'), data: data, @@ -685,7 +733,7 @@ if (includingSecurityGroupService == false) { hiddenTabs.push("securityGroups"); } - + if (args.context.instances[0].state == 'Running') { hiddenTabs.push("settings"); } @@ -2277,7 +2325,7 @@ $.extend(dataObj, { networkIds: args.data.network }); - } + } if (args.data.securitygroup != null && args.data.securitygroup != '') { $.extend(dataObj, { securitygroupIds: args.data.securitygroup @@ -3027,7 +3075,7 @@ }); } }, - + /** * Settings tab */ @@ -3078,7 +3126,7 @@ } } newDetails += 'details[0].' + data.name + '=' + data.value; - + $.ajax({ url: createURL('updateVirtualMachine&id=' + args.context.instances[0].id + '&' + newDetails), async:false, @@ -3108,7 +3156,7 @@ args.response.error(parseXMLHttpResponse(json)); } }); - + var detailToDelete = args.data.jsonObj.name; var newDetails = '' for (detail in existingDetails) { @@ -3139,7 +3187,7 @@ add: function(args) { var name = args.data.name; var value = args.data.value; - + var details; $.ajax({ url: createURL('listVirtualMachines&id=' + args.context.instances[0].id), @@ -3153,7 +3201,7 @@ args.response.error(parseXMLHttpResponse(json)); } }); - + var detailsFormat = ''; for (key in details) { detailsFormat += "details[0]." + key + "=" + details[key] + "&"; @@ -3181,7 +3229,7 @@ } } }; - + var parseDetails = function(details) { var listDetails = []; for (detail in details){ diff --git a/ui/scripts/ui/dialog.js b/ui/scripts/ui/dialog.js index 5b602c271fbd..e58efab74ba3 100644 --- a/ui/scripts/ui/dialog.js +++ b/ui/scripts/ui/dialog.js @@ -131,7 +131,7 @@ } }] }); - + return cloudStack.applyDefaultZindexAndOverlayOnJqueryDialogAndRemoveCloseButton($dialog); }; @@ -433,6 +433,68 @@ ); }); + } else if (field.multiDataArray) { + + $input = $('
'); + + multiArgs = { + context: args.context, + response: { + success: function(args) { + if (args.data == undefined || args.data.length == 0) { + + var label = field.emptyMessage != null ? field.emptyMessage : 'No data available'; + + $input + .addClass('value') + .appendTo($value) + .append( + $('