diff --git a/api/src/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java b/api/src/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java index 435b7f18484..da5f68860bc 100644 --- a/api/src/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java +++ b/api/src/org/apache/cloudstack/api/command/admin/vm/AssignVMCmd.java @@ -29,9 +29,11 @@ import org.apache.cloudstack.api.ResponseObject.ResponseView; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.UserVmResponse; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; import com.cloud.uservm.UserVm; import com.cloud.vm.VirtualMachine; @@ -58,12 +60,15 @@ public class AssignVMCmd extends BaseCmd { description = "id of the VM to be moved") private Long virtualMachineId; - @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "account name of the new VM owner.") + @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "account name of the new VM owner.") private String accountName; - @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, required = true, description = "domain id of the new VM owner.") + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "domain id of the new VM owner.") private Long domainId; + @Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class, description = "an optional project for the new VM owner.") + private Long projectId; + //Network information @Parameter(name = ApiConstants.NETWORK_IDS, type = CommandType.LIST, @@ -98,6 +103,10 @@ public class AssignVMCmd extends BaseCmd { return domainId; } + public Long getProjectId() { + return projectId; + } + public List getNetworkIds() { return networkIds; } @@ -125,6 +134,9 @@ public class AssignVMCmd extends BaseCmd { UserVmResponse response = _responseGenerator.createUserVmResponse(ResponseView.Full, "virtualmachine", userVm).get(0); response.setResponseName(getCommandName()); setResponseObject(response); + } catch (InvalidParameterValueException e){ + e.printStackTrace(); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage()); } catch (Exception e) { e.printStackTrace(); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to move vm " + e.getMessage()); diff --git a/api/src/org/apache/cloudstack/api/response/ProjectResponse.java b/api/src/org/apache/cloudstack/api/response/ProjectResponse.java index ad29d2b18cb..0ae9e18612e 100644 --- a/api/src/org/apache/cloudstack/api/response/ProjectResponse.java +++ b/api/src/org/apache/cloudstack/api/response/ProjectResponse.java @@ -56,6 +56,10 @@ public class ProjectResponse extends BaseResponse implements ResourceLimitAndCou @Param(description = "the account name of the project's owner") private String ownerName; + @SerializedName("projectaccountname") + @Param(description="the project account name of the project") + private String projectAccountName; + @SerializedName(ApiConstants.STATE) @Param(description = "the state of the project") private String state; @@ -228,6 +232,10 @@ public class ProjectResponse extends BaseResponse implements ResourceLimitAndCou ownerName = owner; } + public void setProjectAccountName(String projectAccountName) { + this.projectAccountName = projectAccountName; + } + public void setState(String state) { this.state = state; } diff --git a/server/src/com/cloud/api/query/dao/ProjectJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/ProjectJoinDaoImpl.java index 65330ac29da..25598b35b27 100644 --- a/server/src/com/cloud/api/query/dao/ProjectJoinDaoImpl.java +++ b/server/src/com/cloud/api/query/dao/ProjectJoinDaoImpl.java @@ -93,6 +93,7 @@ public class ProjectJoinDaoImpl extends GenericDaoBase impl Account account = _accountDao.findByIdIncludingRemoved(proj.getProjectAccountId()); AccountJoinVO accountJn = ApiDBUtils.newAccountView(account); _accountJoinDao.setResourceLimits(accountJn, false, response); + response.setProjectAccountName(accountJn.getAccountName()); response.setObjectName("project"); return response; diff --git a/server/src/com/cloud/vm/UserVmManagerImpl.java b/server/src/com/cloud/vm/UserVmManagerImpl.java index de4eaf223f4..3f21a92a21e 100755 --- a/server/src/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/com/cloud/vm/UserVmManagerImpl.java @@ -5065,14 +5065,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (oldAccount == null) { throw new InvalidParameterValueException("Invalid account for VM " + vm.getAccountId() + " in domain."); } - // don't allow to move the vm from the project - if (oldAccount.getType() == Account.ACCOUNT_TYPE_PROJECT) { - InvalidParameterValueException ex = new InvalidParameterValueException("Specified Vm id belongs to the project and can't be moved"); - ex.addProxyObject(vm.getUuid(), "vmId"); - throw ex; - } - final Account newAccount = _accountService.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId()); - if (newAccount == null || newAccount.getType() == Account.ACCOUNT_TYPE_PROJECT) { + final Account newAccount = _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + if (newAccount == null) { throw new InvalidParameterValueException("Invalid accountid=" + cmd.getAccountName() + " in domain " + cmd.getDomainId()); } @@ -5263,17 +5257,155 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir s_logger.debug("AssignVM: Basic zone, adding security groups no " + securityGroupIdList.size() + " to " + vm.getInstanceName()); } else { - if (zone.isSecurityGroupEnabled()) { - throw new InvalidParameterValueException("Not yet implemented for SecurityGroupEnabled advanced networks."); - } else { - if (securityGroupIdList != null && !securityGroupIdList.isEmpty()) { - throw new InvalidParameterValueException("Can't move vm with security groups; security group feature is not enabled in this zone"); + if (zone.isSecurityGroupEnabled()) { // advanced zone with security groups + // cleanup the old security groups + _securityGroupMgr.removeInstanceFromGroups(cmd.getVmId()); + + Set applicableNetworks = new HashSet(); + String requestedIPv4ForDefaultNic = null; + String requestedIPv6ForDefaultNic = null; + // if networkIdList is null and the first network of vm is shared network, then keep it if possible + if (networkIdList == null || networkIdList.isEmpty()) { + NicVO defaultNicOld = _nicDao.findDefaultNicForVM(vm.getId()); + if (defaultNicOld != null) { + NetworkVO defaultNetworkOld = _networkDao.findById(defaultNicOld.getNetworkId()); + if (defaultNetworkOld != null && defaultNetworkOld.getGuestType() == Network.GuestType.Shared && defaultNetworkOld.getAclType() == ACLType.Domain) { + try { + _networkModel.checkNetworkPermissions(newAccount, defaultNetworkOld); + applicableNetworks.add(defaultNetworkOld); + requestedIPv4ForDefaultNic = defaultNicOld.getIPv4Address(); + requestedIPv6ForDefaultNic = defaultNicOld.getIPv6Address(); + s_logger.debug("AssignVM: use old shared network " + defaultNetworkOld.getName() + " with old ip " + requestedIPv4ForDefaultNic + " on default nic of vm:" + vm.getInstanceName()); + } catch (PermissionDeniedException e) { + s_logger.debug("AssignVM: the shared network on old default nic can not be applied to new account"); + } + } + } } // cleanup the network for the oldOwner _networkMgr.cleanupNics(vmOldProfile); _networkMgr.expungeNics(vmOldProfile); + if (networkIdList != null && !networkIdList.isEmpty()) { + // add any additional networks + for (Long networkId : networkIdList) { + NetworkVO network = _networkDao.findById(networkId); + if (network == null) { + InvalidParameterValueException ex = new InvalidParameterValueException( + "Unable to find specified network id"); + ex.addProxyObject(networkId.toString(), "networkId"); + throw ex; + } + + _networkModel.checkNetworkPermissions(newAccount, network); + + // don't allow to use system networks + NetworkOffering networkOffering = _entityMgr.findById(NetworkOffering.class, network.getNetworkOfferingId()); + if (networkOffering.isSystemOnly()) { + InvalidParameterValueException ex = new InvalidParameterValueException( + "Specified Network id is system only and can't be used for vm deployment"); + ex.addProxyObject(network.getUuid(), "networkId"); + throw ex; + } + applicableNetworks.add(network); + } + } + + // add the new nics + LinkedHashMap> networks = new LinkedHashMap>(); + int toggle = 0; + NetworkVO defaultNetwork = null; + for (NetworkVO appNet : applicableNetworks) { + NicProfile defaultNic = new NicProfile(); + if (toggle == 0) { + defaultNic.setDefaultNic(true); + defaultNic.setRequestedIPv4(requestedIPv4ForDefaultNic); + defaultNic.setRequestedIPv6(requestedIPv6ForDefaultNic); + defaultNetwork = appNet; + toggle++; + } + networks.put(appNet, new ArrayList(Arrays.asList(defaultNic))); + + } + + boolean isVmWare = (template.getHypervisorType() == HypervisorType.VMware); + if (securityGroupIdList != null && isVmWare) { + throw new InvalidParameterValueException("Security group feature is not supported for vmWare hypervisor"); + } else if (!isVmWare && (defaultNetwork == null || _networkModel.isSecurityGroupSupportedInNetwork(defaultNetwork)) && _networkModel.canAddDefaultSecurityGroup()) { + if (securityGroupIdList == null) { + securityGroupIdList = new ArrayList(); + } + SecurityGroup defaultGroup = _securityGroupMgr + .getDefaultSecurityGroup(newAccount.getId()); + if (defaultGroup != null) { + // check if security group id list already contains Default + // security group, and if not - add it + boolean defaultGroupPresent = false; + for (Long securityGroupId : securityGroupIdList) { + if (securityGroupId.longValue() == defaultGroup.getId()) { + defaultGroupPresent = true; + break; + } + } + + if (!defaultGroupPresent) { + securityGroupIdList.add(defaultGroup.getId()); + } + + } else { + // create default security group for the account + if (s_logger.isDebugEnabled()) { + s_logger.debug("Couldn't find default security group for the account " + + newAccount + " so creating a new one"); + } + defaultGroup = _securityGroupMgr.createSecurityGroup( + SecurityGroupManager.DEFAULT_GROUP_NAME, + SecurityGroupManager.DEFAULT_GROUP_DESCRIPTION, + newAccount.getDomainId(), newAccount.getId(), + newAccount.getAccountName()); + securityGroupIdList.add(defaultGroup.getId()); + } + } + + VirtualMachine vmi = _itMgr.findById(vm.getId()); + VirtualMachineProfileImpl vmProfile = new VirtualMachineProfileImpl(vmi); + + if (applicableNetworks.isEmpty()) { + throw new InvalidParameterValueException("No network is specified, please specify one when you move the vm. For now, please add a network to VM on NICs tab."); + } else { + _networkMgr.allocate(vmProfile, networks); + } + + _securityGroupMgr.addInstanceToGroups(vm.getId(), + securityGroupIdList); + s_logger.debug("AssignVM: Advanced zone, adding security groups no " + + securityGroupIdList.size() + " to " + + vm.getInstanceName()); + + } else { + if (securityGroupIdList != null && !securityGroupIdList.isEmpty()) { + throw new InvalidParameterValueException("Can't move vm with security groups; security group feature is not enabled in this zone"); + } Set applicableNetworks = new HashSet(); + // if networkIdList is null and the first network of vm is shared network, then keep it if possible + if (networkIdList == null || networkIdList.isEmpty()) { + NicVO defaultNicOld = _nicDao.findDefaultNicForVM(vm.getId()); + if (defaultNicOld != null) { + NetworkVO defaultNetworkOld = _networkDao.findById(defaultNicOld.getNetworkId()); + if (defaultNetworkOld != null && defaultNetworkOld.getGuestType() == Network.GuestType.Shared && defaultNetworkOld.getAclType() == ACLType.Domain) { + try { + _networkModel.checkNetworkPermissions(newAccount, defaultNetworkOld); + applicableNetworks.add(defaultNetworkOld); + } catch (PermissionDeniedException e) { + s_logger.debug("AssignVM: the shared network on old default nic can not be applied to new account"); + } + } + } + } + + // cleanup the network for the oldOwner + _networkMgr.cleanupNics(vmOldProfile); + _networkMgr.expungeNics(vmOldProfile); if (networkIdList != null && !networkIdList.isEmpty()) { // add any additional networks @@ -5296,7 +5428,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } applicableNetworks.add(network); } - } else { + } else if (applicableNetworks.isEmpty()) { NetworkVO defaultNetwork = null; List requiredOfferings = _networkOfferingDao.listByAvailability(Availability.Required, false); if (requiredOfferings.size() < 1) { @@ -5373,7 +5505,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir s_logger.debug("AssignVM: Advance virtual, adding networks no " + networks.size() + " to " + vm.getInstanceName()); } // END IF NON SEC GRP ENABLED } // END IF ADVANCED - s_logger.info("AssignVM: vm " + vm.getInstanceName() + " now belongs to account " + cmd.getAccountName()); + s_logger.info("AssignVM: vm " + vm.getInstanceName() + " now belongs to account " + newAccount.getAccountName()); return vm; } diff --git a/server/test/com/cloud/vm/UserVmManagerTest.java b/server/test/com/cloud/vm/UserVmManagerTest.java index 9df3f689714..294a93a168b 100644 --- a/server/test/com/cloud/vm/UserVmManagerTest.java +++ b/server/test/com/cloud/vm/UserVmManagerTest.java @@ -690,7 +690,7 @@ public class UserVmManagerTest { when(_accountService.getActiveAccountById(anyLong())).thenReturn(oldAccount); - when(_accountService.getActiveAccountByName(anyString(), anyLong())).thenReturn(newAccount); + when(_accountMgr.finalizeOwner(any(Account.class), anyString(), anyLong(), anyLong())).thenReturn(newAccount); doThrow(new PermissionDeniedException("Access check failed")).when(_accountMgr).checkAccess(any(Account.class), any(AccessType.class), any(Boolean.class), any(ControlledEntity.class)); diff --git a/ui/l10n/en.js b/ui/l10n/en.js index f6669ca823f..136ca15d6fa 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1909,6 +1909,7 @@ var dictionary = {"ICMP.code":"ICMP Code", "message.alert.state.detected":"Alert state detected", "message.allow.vpn.access":"Please enter a username and password of the user that you want to allow VPN access.", "message.apply.snapshot.policy":"You have successfully updated your current snapshot policy.", +"message.assign.instance.another":"Please specify the account type, domain, account name and network (optional) of the new account.
If the default nic of the vm is on a shared network, CloudStack will check if the network can be used by the new account if you do not specify one network.
If the default nic of the vm is on a isolated network, and the new account has more one isolated networks, you should specify one.", "message.attach.iso.confirm":"Please confirm that you want to attach the ISO to this virtual instance.", "message.attach.volume":"Please fill in the following data to attach a new volume. If you are attaching a disk volume to a Windows based virtual machine, you will need to reboot the instance to see the attached disk.", "message.basic.mode.desc":"Choose this network model if you do *not* want to enable any VLAN support. All virtual instances created under this network model will be assigned an IP directly from the network and security groups are used to provide security and segregation.", diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index ffbff416bda..012d11e8516 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -1910,7 +1910,50 @@ label: 'label.assign.instance.another', createForm: { title: 'label.assign.instance.another', + desc: 'message.assign.instance.another', + preFilter: function(args) { + var zone; + $.ajax({ + url: createURL('listZones'), + data: { + id: args.context.instances[0].zoneid + }, + async: false, + success: function(json) { + zone = json.listzonesresponse.zone[0]; + } + }); + if (zone.securitygroupsenabled == true) { + args.$form.find('.form-item[rel=securitygroup]').css('display', 'inline-block'); + } else { + args.$form.find('.form-item[rel=securitygroup]').hide(); + } + }, fields: { + accountType: { + label: 'Account Type', + select: function(args) { + var items = []; + items.push({id: 'account', description: 'Account'}); + items.push({id: 'project', description: 'Project'}); + args.response.success({data: items}); + + args.$select.change(function() { + var $form = $(this).closest('form'); + var $account = $form.find('.form-item[rel=account]'); + var $project = $form.find('.form-item[rel=project]'); + + var accountType = $(this).val(); + if (accountType == 'account') { // Account + $account.css('display', 'inline-block'); + $project.hide(); + } else if (accountType == 'project') { // Project + $project.css('display', 'inline-block'); + $account.hide(); + } + }); + } + }, domainid: { label: 'label.domain', validation: { @@ -1946,25 +1989,216 @@ }, account: { label: 'label.account', + dependsOn: 'domainid', validation: { required: true - } - } + }, + select: function(args) { + var dataObj = { + domainId: args.domainid, + state: 'Enabled', + listAll: true, + }; + $.ajax({ + url: createURL('listAccounts', { + ignoreProject: true + }), + data: dataObj, + success: function(json) { + accountObjs = json.listaccountsresponse.account; + var items = [{ + id: null, + description: '' + }]; + $(accountObjs).each(function() { + items.push({ + id: this.name, + description: this.name + }); + }) + + args.response.success({ + data: items + }); + } + }); + }, + }, + project: { + label: 'label.project', + dependsOn: 'domainid', + validation: { + required: true + }, + select: function(args) { + var dataObj = { + domainId: args.domainid, + state: 'Active', + listAll: true, + }; + $.ajax({ + url: createURL('listProjects', { + ignoreProject: true + }), + data: dataObj, + success: function(json) { + projectObjs = json.listprojectsresponse.project; + var items = [{ + id: null, + description: '' + }]; + $(projectObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }) + + args.response.success({ + data: items + }); + } + }); + }, + }, + network: { + label: 'label.network', + dependsOn: ['accountType', 'domainid', 'account', 'project'], + select: function(args) { + var dataObj = { + domainId: args.domainid, + listAll: true, + isrecursive: false + }; + if (args.data.accountType == 'account' && args.data.account != null && args.data.account != '') { + $.extend(dataObj, { + account: args.data.account + }); + } else if (args.data.accountType == 'project' && args.data.project != null && args.data.project != '') { + $.extend(dataObj, { + projectid: args.data.project + }); + } else { + args.response.success({ + data: null + }); + return; + } + $.ajax({ + url: createURL('listNetworks', { + ignoreProject: true + }), + data: dataObj, + success: function(json) { + var networkObjs = json.listnetworksresponse.network; + var items = [{ + id: null, + description: '' + }]; + $(networkObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }) + + args.response.success({ + data: items + }); + } + }); + }, + }, + securitygroup: { + label: 'label.security.group', + dependsOn: ['accountType', 'domainid', 'account', 'project'], + select: function(args) { + var dataObj = { + domainId: args.domainid, + listAll: true, + isrecursive: false + }; + if (args.data.accountType == 'account' && args.data.account != null && args.data.account != '') { + $.extend(dataObj, { + account: args.data.account + }); + } else if (args.data.accountType == 'project' && args.data.project != null && args.data.project != '') { + $.extend(dataObj, { + projectid: args.data.project + }); + } else { + args.response.success({ + data: null + }); + return; + } + $.ajax({ + url: createURL('listSecurityGroups', { + ignoreProject: true + }), + data: dataObj, + success: function(json) { + var sgObjs = json.listsecuritygroupsresponse.securitygroup; + var items = [{ + id: null, + description: '' + }]; + $(sgObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }) + + args.response.success({ + data: items + }); + } + }); + }, + }, } }, action: function(args) { - $.ajax({ - url: createURL('assignVirtualMachine'), - data: { - virtualmachineid: args.context.instances[0].id, - domainid: args.data.domainid, + var dataObj = { + virtualmachineid: args.context.instances[0].id, + domainid: args.data.domainid, + }; + var ignoreProject = false; + if (args.data.accountType == 'account') { + ignoreProject = true; + $.extend(dataObj, { account: args.data.account - }, + }); + } else if (args.data.accountType == 'project') { + $.extend(dataObj, { + projectid: args.data.project + }); + } + if (args.data.network != null && args.data.network != '') { + $.extend(dataObj, { + networkIds: args.data.network + }); + } + if (args.data.securitygroup != null && args.data.securitygroup != '') { + $.extend(dataObj, { + securitygroupIds: args.data.securitygroup + }); + } + + $.ajax({ + url: createURL('assignVirtualMachine', { + ignoreProject: ignoreProject + }), + data: dataObj, success: function(json) { var item = json.assignvirtualmachineresponse.virtualmachine; args.response.success({ data: item }); + }, + error: function(data) { + args.response.error(parseXMLHttpResponse(data)); } }); },