Merge pull request #1767 from nvazquez/userVmAndTemplatesDetails

CLOUDSTACK-9457: Allow retrieval and modification of VM and template details via API and UIJIRA TICKET: https://issues.apache.org/jira/browse/CLOUDSTACK-9457

### Goal
This PR proposes list/add/update/delete user vm and vm template details via API and UI.

### VM UI Screenshots
Setting tab is added on Instances page. Actions allowed are: Add/Edit/Remove
![](https://issues.apache.org/jira/secure/attachment/12844858/VMDetails1.JPG "Screenshot 1 - VM Details")

Settings tab is only shown if instance is Stopped:
![](https://issues.apache.org/jira/secure/attachment/12844859/VMDetailsRunning.JPG "Screenshot 2 - VM Details Hidden Running VM")
![](https://issues.apache.org/jira/secure/attachment/12844860/VMDetailsStopped.JPG "Screenshot 3 - VM Details Stopped VM")

### Templates UI Screenshots
Setting tab is added on Templates page. Actions allowed are: Add/Edit/Remove:
![](https://issues.apache.org/jira/secure/attachment/12844857/TemplateDetails1.JPG "Screenshot 4 - Template Details")

* pr/1767:
  CLOUDSTACK-9457: Allow retrieval and modification of VM and template details via API and UI

Signed-off-by: Rajani Karuturi <rajani.karuturi@accelerite.com>
This commit is contained in:
Rajani Karuturi 2017-02-08 12:12:36 +05:30
commit 202b92f243
12 changed files with 445 additions and 21 deletions

View File

@ -649,6 +649,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";

View File

@ -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];
}
}
public boolean isCleanupDetails(){
return cleanupDetails == null ? false : cleanupDetails.booleanValue();
}
}

View File

@ -116,6 +116,11 @@ public class UpdateVMCmd extends BaseCustomIdCmd implements SecurityGroupAction
)
private List<String> 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///////////////////
/////////////////////////////////////////////////////

View File

@ -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;
}
}

7
engine/schema/src/com/cloud/vm/UserVmDetailVO.java Normal file → Executable file
View File

@ -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;
}
}

View File

@ -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<UserVmJo
}
// set resource details map
// only hypervisortoolsversion can be returned to the end user
UserVmDetailVO hypervisorToolsVersion = _userVmDetailsDao.findDetail(userVm.getId(), VmDetailConstants.HYPERVISOR_TOOLS_VERSION);
if (hypervisorToolsVersion != null) {
// Allow passing details to end user
List<UserVmDetailVO> vmDetails = _userVmDetailsDao.listDetails(userVm.getId());
if (vmDetails != null) {
Map<String, String> resourceDetails = new HashMap<String, String>();
resourceDetails.put(hypervisorToolsVersion.getName(), hypervisorToolsVersion.getValue());
for (UserVmDetailVO userVmDetailVO : vmDetails) {
resourceDetails.put(userVmDetailVO.getName(), userVmDetailVO.getValue());
}
userVmResponse.setDetails(resourceDetails);
}

View File

@ -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<TemplateAdapter> _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);
}

View File

@ -2309,6 +2309,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
Map<String,String> details = cmd.getDetails();
Account caller = CallContext.current().getCallingAccount();
List<Long> securityGroupIdList = getSecurityGroupIdList(cmd);
boolean cleanupDetails = cmd.isCleanupDetails();
// Input validation and permission checks
UserVmVO vmInstance = _vmDao.findById(id);
@ -2347,14 +2348,11 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
}
if (details != null && !details.isEmpty()) {
_vmDao.loadDetails(vmInstance);
for(Map.Entry<String,String> 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);
}

View File

@ -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 {

View File

@ -498,6 +498,10 @@
if (includingSecurityGroupService == false) {
hiddenTabs.push("securityGroups");
}
if (args.context.instances[0].state == 'Running') {
hiddenTabs.push("settings");
}
return hiddenTabs;
},
@ -2680,11 +2684,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;

121
ui/scripts/templates.js Normal file → Executable file
View File

@ -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
});
}
});
}
}
})
}
}
}
}
},

View File

@ -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 = $('<div>').listView({
context: context,
listView: listView
});
return $listView;
}
};
}(jQuery, cloudStack));