From e8049af1534f1ab2cc8335034c2fd76c67f9fdec Mon Sep 17 00:00:00 2001 From: nvazquez Date: Tue, 27 Dec 2016 23:33:50 -0300 Subject: [PATCH] CLOUDSTACK-9457: Allow retrieval and modification of VM and template details via API and UI --- .../apache/cloudstack/api/ApiConstants.java | 1 + .../api/BaseUpdateTemplateOrIsoCmd.java | 12 +- .../api/command/user/vm/UpdateVMCmd.java | 9 + .../com/cloud/storage/VMTemplateDetailVO.java | 16 ++ .../src/com/cloud/vm/UserVmDetailVO.java | 7 + .../api/query/dao/UserVmJoinDaoImpl.java | 11 +- .../cloud/template/TemplateManagerImpl.java | 13 +- .../src/com/cloud/vm/UserVmManagerImpl.java | 14 +- .../template/TemplateManagerImplTest.java | 9 + ui/scripts/instances.js | 167 +++++++++++++++++- ui/scripts/templates.js | 121 ++++++++++++- ui/scripts/ui-custom/granularSettings.js | 86 ++++++++- 12 files changed, 445 insertions(+), 21 deletions(-) mode change 100644 => 100755 engine/schema/src/com/cloud/storage/VMTemplateDetailVO.java mode change 100644 => 100755 engine/schema/src/com/cloud/vm/UserVmDetailVO.java mode change 100644 => 100755 ui/scripts/templates.js diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 00e9d388c28..f7f2a37ff54 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -648,6 +648,7 @@ public class ApiConstants { public static final String OVM3_POOL = "ovm3pool"; public static final String OVM3_CLUSTER = "ovm3cluster"; public static final String OVM3_VIP = "ovm3vip"; + public static final String CLEAN_UP_DETAILS = "cleanupdetails"; public static final String ADMIN = "admin"; diff --git a/api/src/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java b/api/src/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java index 5dc2b06f4c7..36767345a4b 100644 --- a/api/src/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java +++ b/api/src/org/apache/cloudstack/api/BaseUpdateTemplateOrIsoCmd.java @@ -17,7 +17,6 @@ package org.apache.cloudstack.api; import org.apache.log4j.Logger; - import org.apache.cloudstack.api.command.user.iso.UpdateIsoCmd; import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.TemplateResponse; @@ -73,6 +72,11 @@ public abstract class BaseUpdateTemplateOrIsoCmd extends BaseCmd { @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "Details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].hypervisortoolsversion=xenserver61") protected Map details; + @Parameter(name = ApiConstants.CLEAN_UP_DETAILS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if details should be cleaned up or not (if set to true, details removed for this resource, details field ignored; if false or not set, no action)") + private Boolean cleanupDetails; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -129,4 +133,8 @@ public abstract class BaseUpdateTemplateOrIsoCmd extends BaseCmd { Collection paramsCollection = this.details.values(); return (Map) (paramsCollection.toArray())[0]; } -} \ No newline at end of file + + public boolean isCleanupDetails(){ + return cleanupDetails == null ? false : cleanupDetails.booleanValue(); + } +} diff --git a/api/src/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java b/api/src/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java index 4508f7e471a..eb03f086c69 100644 --- a/api/src/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java +++ b/api/src/org/apache/cloudstack/api/command/user/vm/UpdateVMCmd.java @@ -116,6 +116,11 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction ) private List securityGroupNameList; + @Parameter(name = ApiConstants.CLEAN_UP_DETAILS, + type = CommandType.BOOLEAN, + description = "optional boolean field, which indicates if details should be cleaned up or not (if set to true, details removed for this resource, details field ignored; if false or not set, no action)") + private Boolean cleanupDetails; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -173,6 +178,10 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction return securityGroupNameList; } + public boolean isCleanupDetails(){ + return cleanupDetails == null ? false : cleanupDetails.booleanValue(); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/engine/schema/src/com/cloud/storage/VMTemplateDetailVO.java b/engine/schema/src/com/cloud/storage/VMTemplateDetailVO.java old mode 100644 new mode 100755 index f988abab399..5010edfa762 --- a/engine/schema/src/com/cloud/storage/VMTemplateDetailVO.java +++ b/engine/schema/src/com/cloud/storage/VMTemplateDetailVO.java @@ -79,4 +79,20 @@ public class VMTemplateDetailVO implements ResourceDetail { public boolean isDisplay() { return display; } + + public void setId(long id) { + this.id = id; + } + + public void setResourceId(long resourceId) { + this.resourceId = resourceId; + } + + public void setName(String name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/engine/schema/src/com/cloud/vm/UserVmDetailVO.java b/engine/schema/src/com/cloud/vm/UserVmDetailVO.java old mode 100644 new mode 100755 index 2b169a38272..81bb6dd9d4f --- a/engine/schema/src/com/cloud/vm/UserVmDetailVO.java +++ b/engine/schema/src/com/cloud/vm/UserVmDetailVO.java @@ -80,4 +80,11 @@ public class UserVmDetailVO implements ResourceDetail { return display; } + public void setName(String name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } } diff --git a/server/src/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/UserVmJoinDaoImpl.java index 106cd25a5b0..75b42c21979 100644 --- a/server/src/com/cloud/api/query/dao/UserVmJoinDaoImpl.java +++ b/server/src/com/cloud/api/query/dao/UserVmJoinDaoImpl.java @@ -51,7 +51,6 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; import com.cloud.vm.UserVmDetailVO; import com.cloud.vm.VirtualMachine.State; -import com.cloud.vm.VmDetailConstants; import com.cloud.vm.VmStats; import com.cloud.vm.dao.NicSecondaryIpVO; import com.cloud.vm.dao.UserVmDetailsDao; @@ -292,11 +291,13 @@ public class UserVmJoinDaoImpl extends GenericDaoBaseWithTagInformation vmDetails = _userVmDetailsDao.listDetails(userVm.getId()); + if (vmDetails != null) { Map resourceDetails = new HashMap(); - resourceDetails.put(hypervisorToolsVersion.getName(), hypervisorToolsVersion.getValue()); + for (UserVmDetailVO userVmDetailVO : vmDetails) { + resourceDetails.put(userVmDetailVO.getName(), userVmDetailVO.getValue()); + } userVmResponse.setDetails(resourceDetails); } diff --git a/server/src/com/cloud/template/TemplateManagerImpl.java b/server/src/com/cloud/template/TemplateManagerImpl.java index 7130042bc5c..c20889eea28 100644 --- a/server/src/com/cloud/template/TemplateManagerImpl.java +++ b/server/src/com/cloud/template/TemplateManagerImpl.java @@ -161,6 +161,7 @@ import com.cloud.storage.dao.LaunchPermissionDao; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VMTemplateDetailsDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; @@ -269,6 +270,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, private ImageStoreDao _imgStoreDao; @Inject MessageBus _messageBus; + @Inject + private VMTemplateDetailsDao _tmpltDetailsDao; private boolean _disableExtraction = false; private List _adapters; @@ -1880,6 +1883,7 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, Integer sortKey = cmd.getSortKey(); Map details = cmd.getDetails(); Account account = CallContext.current().getCallingAccount(); + boolean cleanupDetails = cmd.isCleanupDetails(); // verify that template exists VMTemplateVO template = _tmpltDao.findById(id); @@ -1911,7 +1915,8 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, sortKey == null && isDynamicallyScalable == null && isRoutingTemplate == null && - details == null); + (! cleanupDetails && details == null) //update details in every case except this one + ); if (!updateNeeded) { return template; } @@ -1989,7 +1994,11 @@ public class TemplateManagerImpl extends ManagerBase implements TemplateManager, } } - if (details != null && !details.isEmpty()) { + if (cleanupDetails) { + template.setDetails(null); + _tmpltDetailsDao.removeDetails(id); + } + else if (details != null && !details.isEmpty()) { template.setDetails(details); _tmpltDao.saveDetails(template); } diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index 453800db31f..1db095692c8 100644 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -2307,6 +2307,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Map details = cmd.getDetails(); Account caller = CallContext.current().getCallingAccount(); List securityGroupIdList = getSecurityGroupIdList(cmd); + boolean cleanupDetails = cmd.isCleanupDetails(); // Input validation and permission checks UserVmVO vmInstance = _vmDao.findById(id); @@ -2345,14 +2346,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } - if (details != null && !details.isEmpty()) { - _vmDao.loadDetails(vmInstance); - - for(Map.Entry entry : details.entrySet()) { - if(entry instanceof Map.Entry) { - vmInstance.setDetail(entry.getKey(), entry.getValue()); - } - } + if (cleanupDetails){ + _vmDetailsDao.removeDetails(id); + } + else if (details != null && !details.isEmpty()) { + vmInstance.setDetails(details); _vmDao.saveDetails(vmInstance); } diff --git a/server/test/com/cloud/template/TemplateManagerImplTest.java b/server/test/com/cloud/template/TemplateManagerImplTest.java index 6e1693832a2..61a8a4acdb6 100644 --- a/server/test/com/cloud/template/TemplateManagerImplTest.java +++ b/server/test/com/cloud/template/TemplateManagerImplTest.java @@ -45,6 +45,7 @@ import com.cloud.storage.dao.LaunchPermissionDao; import com.cloud.storage.dao.SnapshotDao; import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; +import com.cloud.storage.dao.VMTemplateDetailsDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VMTemplateZoneDao; import com.cloud.storage.dao.VolumeDao; @@ -161,6 +162,9 @@ public class TemplateManagerImplTest { @Inject SnapshotDao snapshotDao; + @Inject + VMTemplateDetailsDao tmpltDetailsDao; + public class CustomThreadPoolExecutor extends ThreadPoolExecutor { AtomicInteger ai = new AtomicInteger(0); public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, @@ -623,6 +627,11 @@ public class TemplateManagerImplTest { return Mockito.mock(TemplateAdapter.class); } + @Bean + public VMTemplateDetailsDao vmTemplateDetailsDao() { + return Mockito.mock(VMTemplateDetailsDao.class); + } + public static class Library implements TypeFilter { @Override public boolean match(MetadataReader mdr, MetadataReaderFactory arg1) throws IOException { diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 19db2570296..ec10df88cf7 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -497,6 +497,10 @@ if (includingSecurityGroupService == false) { hiddenTabs.push("securityGroups"); } + + if (args.context.instances[0].state == 'Running') { + hiddenTabs.push("settings"); + } return hiddenTabs; }, @@ -2679,11 +2683,172 @@ } }); } - } + }, + + /** + * Settings tab + */ + settings: { + title: 'label.settings', + custom: cloudStack.uiCustom.granularDetails({ + dataProvider: function(args) { + $.ajax({ + url: createURL('listVirtualMachines&id=' + args.context.instances[0].id), + success: function(json) { + var details = json.listvirtualmachinesresponse.virtualmachine[0].details; + args.response.success({ + data: parseDetails(details) + }); + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + + }, + actions: { + edit: function(args) { + var data = { + name: args.data.jsonObj.name, + value: args.data.value + }; + var existingDetails; + $.ajax({ + url: createURL('listVirtualMachines&id=' + args.context.instances[0].id), + async:false, + success: function(json) { + var details = json.listvirtualmachinesresponse.virtualmachine[0].details; + console.log(details); + existingDetails = details; + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + console.log(existingDetails); + var newDetails = ''; + for (d in existingDetails) { + if (d != data.name) { + newDetails += 'details[0].' + d + '=' + existingDetails[d] + '&'; + } + } + newDetails += 'details[0].' + data.name + '=' + data.value; + + $.ajax({ + url: createURL('updateVirtualMachine&id=' + args.context.instances[0].id + '&' + newDetails), + async:false, + success: function(json) { + var items = json.updatevirtualmachineresponse.virtualmachine.details; + args.response.success({ + data: parseDetails(items) + }); + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + }, + remove: function(args) { + var existingDetails; + $.ajax({ + url: createURL('listVirtualMachines&id=' + args.context.instances[0].id), + async:false, + success: function(json) { + var details = json.listvirtualmachinesresponse.virtualmachine[0].details; + existingDetails = details; + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + + var detailToDelete = args.data.jsonObj.name; + var newDetails = '' + for (detail in existingDetails) { + if (detail != detailToDelete) { + newDetails += 'details[0].' + detail + '=' + existingDetails[detail] + '&'; + } + } + if (newDetails != '') { + newDetails = newDetails.substring(0, newDetails.length - 1); + } + else { + newDetails += 'cleanupdetails=true' + } + $.ajax({ + url: createURL('updateVirtualMachine&id=' + args.context.instances[0].id + '&' + newDetails), + async:false, + success: function(json) { + var items = json.updatevirtualmachineresponse.virtualmachine.details; + args.response.success({ + data: parseDetails(items) + }); + }, + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + }, + 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), + async:false, + success: function(json) { + var dets = json.listvirtualmachinesresponse.virtualmachine[0].details; + details = dets; + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + + var detailsFormat = ''; + for (key in details) { + detailsFormat += "details[0]." + key + "=" + details[key] + "&"; + } + // Add new detail to the existing ones + detailsFormat += "details[0]." + name + "=" + value; + $.ajax({ + url: createURL('updateVirtualMachine&id=' + args.context.instances[0].id + "&" + detailsFormat), + async: false, + success: function(json) { + var items = json.updatevirtualmachineresponse.virtualmachine.details; + args.response.success({ + data: parseDetails(items) + }); + }, + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + } + } + }) + } } } } }; + + var parseDetails = function(details) { + var listDetails = []; + for (detail in details){ + var det = {}; + det["name"] = detail; + det["value"] = details[detail]; + listDetails.push(det); + } + return listDetails; + } var vmActionfilter = cloudStack.actionFilter.vmActionFilter = function(args) { var jsonObj = args.context.item; diff --git a/ui/scripts/templates.js b/ui/scripts/templates.js old mode 100644 new mode 100755 index 96ef43aee1c..26f0fd1478a --- a/ui/scripts/templates.js +++ b/ui/scripts/templates.js @@ -1780,8 +1780,125 @@ } }} } - } - } + }, + /** + * Settings tab + */ + settings: { + title: 'label.settings', + custom: cloudStack.uiCustom.granularDetails({ + dataProvider: function(args) { + $.ajax({ + url: createURL('listTemplates'), + data: { + templatefilter: "self", + id: args.context.templates[0].id + }, + success: function(json) { + var details = json.listtemplatesresponse.template[0].details; + var listDetails = []; + for (detail in details){ + var det = {}; + det["name"] = detail; + det["value"] = details[detail]; + listDetails.push(det); + } + args.response.success({ + data: listDetails + }); + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + + }, + actions: { + edit: function(args) { + var data = { + name: args.data.jsonObj.name, + value: args.data.value + }; + var existingDetails = args.context.templates[0].details; + var newDetails = ''; + for (d in existingDetails) { + if (d != data.name) { + newDetails += 'details[0].' + d + '=' + existingDetails[d] + '&'; + } + } + newDetails += 'details[0].' + data.name + '=' + data.value; + + $.ajax({ + url: createURL('updateTemplate&id=' + args.context.templates[0].id + '&' + newDetails), + success: function(json) { + var template = json.updatetemplateresponse.template; + args.context.templates[0].details = template.details; + args.response.success({ + data: template.details + }); + }, + + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + }, + remove: function(args) { + var existingDetails = args.context.templates[0].details; + var detailToDelete = args.data.jsonObj.name; + var newDetails = '' + for (detail in existingDetails) { + if (detail != detailToDelete) { + newDetails += 'details[0].' + detail + '=' + existingDetails[detail] + '&'; + } + } + if (newDetails != '') { + newDetails = newDetails.substring(0, newDetails.length - 1); + } + else { + newDetails += 'cleanupdetails=true'; + } + $.ajax({ + url: createURL('updateTemplate&id=' + args.context.templates[0].id + '&' + newDetails), + success: function(json) { + var template = json.updatetemplateresponse.template; + args.context.templates[0].details = template.details; + args.response.success({ + data: template.details + }); + }, + error: function(json) { + args.response.error(parseXMLHttpResponse(json)); + } + }); + }, + add: function(args) { + var name = args.data.name; + var value = args.data.value; + var details = args.context.templates[0].details; + var detailsFormat = ''; + for (key in details) { + detailsFormat += "details[0]." + key + "=" + details[key] + "&"; + } + // Add new detail to the existing ones + detailsFormat += "details[0]." + name + "=" + value; + $.ajax({ + url: createURL('updateTemplate&id=' + args.context.templates[0].id + "&" + detailsFormat), + async: false, + success: function(json) { + var template = json.updatetemplateresponse.template; + args.context.templates[0].details = template.details; + args.response.success({ + data: template.details + }); + } + }); + } + } + }) + } + } } } }, diff --git a/ui/scripts/ui-custom/granularSettings.js b/ui/scripts/ui-custom/granularSettings.js index 5ab60b7af97..5312394127e 100644 --- a/ui/scripts/ui-custom/granularSettings.js +++ b/ui/scripts/ui-custom/granularSettings.js @@ -54,4 +54,88 @@ return $listView; } }; -}(jQuery, cloudStack)); + cloudStack.uiCustom.granularDetails = function(args) { + var dataProvider = args.dataProvider; + var actions = args.actions; + + return function(args) { + var context = args.context; + + var listView = { + id: 'details', + fields: { + name: { + label: 'label.name' + }, + value: { + label: 'label.value', + editable: true + } + }, + actions: { + edit: { + label: 'label.change.value', + action: actions.edit + }, + remove: { + label: 'Remove Setting', + messages: { + confirm: function(args) { + return 'Delete Setting'; + }, + notification: function(args) { + return 'Setting deleted'; + } + }, + action: actions.remove, + notification: { + poll: function(args) { + args.complete(); + } + } + }, + add : { + label: 'Add Setting', + messages: { + confirm: function(args) { + return 'Add Setting'; + }, + notification: function(args) { + return 'Setting added'; + } + }, + preFilter: function(args) { + return true; + }, + createForm: { + title: 'Add New Setting', + fields: { + name: { + label: 'label.name', + validation: { + required: true + } + }, + value: { + label: 'label.value', + validation: { + required: true + } + } + } + }, + action: actions.add + } + }, + dataProvider: dataProvider + }; + + var $listView = $('
').listView({ + context: context, + listView: listView + }); + + return $listView; + } + }; +}(jQuery, cloudStack)); \ No newline at end of file