diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java index fa76db9cf30..b94fc0cf2bf 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/DeployVMCmdByAdmin.java @@ -16,6 +16,10 @@ // under the License. package org.apache.cloudstack.api.command.admin.vm; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.api.response.PodResponse; import org.apache.log4j.Logger; import org.apache.cloudstack.api.APICommand; @@ -39,6 +43,19 @@ import com.cloud.vm.VirtualMachine; public class DeployVMCmdByAdmin extends DeployVMCmd { public static final Logger s_logger = Logger.getLogger(DeployVMCmdByAdmin.class.getName()); + @Parameter(name = ApiConstants.POD_ID, type = CommandType.UUID, entityType = PodResponse.class, description = "destination Pod ID to deploy the VM to - parameter available for root admin only", since = "4.13") + private Long podId; + + @Parameter(name = ApiConstants.CLUSTER_ID, type = CommandType.UUID, entityType = ClusterResponse.class, description = "destination Cluster ID to deploy the VM to - parameter available for root admin only", since = "4.13") + private Long clusterId; + + public Long getPodId() { + return podId; + } + + public Long getClusterId() { + return clusterId; + } @Override public void execute(){ diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java index 3d14b8998f6..06acc32eb6e 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/DeployVMCmd.java @@ -24,8 +24,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import org.apache.log4j.Logger; - import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.affinity.AffinityGroupResponse; import org.apache.cloudstack.api.ACL; @@ -49,6 +47,7 @@ import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.context.CallContext; import org.apache.commons.collections.MapUtils; +import org.apache.log4j.Logger; import com.cloud.event.EventTypes; import com.cloud.exception.ConcurrentOperationException; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java index b87c7de0187..5b3db8565d4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/vm/StartVMCmd.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.api.command.user.vm; +import org.apache.cloudstack.api.response.ClusterResponse; +import org.apache.cloudstack.api.response.PodResponse; import org.apache.log4j.Logger; import org.apache.cloudstack.acl.RoleType; @@ -60,6 +62,18 @@ public class StartVMCmd extends BaseAsyncCmd { required = true, description = "The ID of the virtual machine") private Long id; + @Parameter(name = ApiConstants.POD_ID, + type = CommandType.UUID, + entityType = PodResponse.class, + description = "destination Pod ID to deploy the VM to - parameter available for root admin only") + private Long podId; + + @Parameter(name = ApiConstants.CLUSTER_ID, + type = CommandType.UUID, + entityType = ClusterResponse.class, + description = "destination Cluster ID to deploy the VM to - parameter available for root admin only") + private Long clusterId; + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, @@ -82,6 +96,14 @@ public class StartVMCmd extends BaseAsyncCmd { return hostId; } + public Long getPodId() { + return podId; + } + + public Long getClusterId() { + return clusterId; + } + // /////////////////////////////////////////////////// // ///////////// API Implementation/////////////////// // /////////////////////////////////////////////////// diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java b/server/src/main/java/com/cloud/vm/UserVmManager.java index 6ffc28e4403..fc2f558797e 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManager.java +++ b/server/src/main/java/com/cloud/vm/UserVmManager.java @@ -99,6 +99,9 @@ public interface UserVmManager extends UserVmService { Pair> startVirtualMachine(long vmId, Long hostId, Map additionalParams, String deploymentPlannerToUse) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; + Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map additionalParams, String deploymentPlannerToUse) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException; + boolean upgradeVirtualMachine(Long id, Long serviceOfferingId, Map customParameters) throws ResourceUnavailableException, ConcurrentOperationException, ManagementServerException, VirtualMachineMigrationException; diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index e719d6f4f9f..a1a552db8d5 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -49,6 +49,7 @@ 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.DeployVMCmdByAdmin; 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; @@ -136,6 +137,7 @@ import com.cloud.dc.DataCenter.NetworkType; import com.cloud.dc.DataCenterVO; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.HostPodVO; +import com.cloud.dc.Pod; import com.cloud.dc.Vlan; import com.cloud.dc.Vlan.VlanType; import com.cloud.dc.VlanVO; @@ -2755,7 +2757,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override @ActionEvent(eventType = EventTypes.EVENT_VM_START, eventDescription = "starting Vm", async = true) public UserVm startVirtualMachine(StartVMCmd cmd) throws ExecutionException, ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { - return startVirtualMachine(cmd.getId(), cmd.getHostId(), null, cmd.getDeploymentPlanner()).first(); + return startVirtualMachine(cmd.getId(), cmd.getPodId(), cmd.getClusterId(), cmd.getHostId(), null, cmd.getDeploymentPlanner()).first(); } @Override @@ -4144,20 +4146,27 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "starting Vm", async = true) public UserVm startVirtualMachine(DeployVMCmd cmd) throws ResourceUnavailableException, InsufficientCapacityException, ConcurrentOperationException { - return startVirtualMachine(cmd, null, cmd.getDeploymentPlanner()); + long vmId = cmd.getEntityId(); + Long podId = null; + Long clusterId = null; + Long hostId = cmd.getHostId(); + Map diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap(); + if (cmd instanceof DeployVMCmdByAdmin) { + DeployVMCmdByAdmin adminCmd = (DeployVMCmdByAdmin)cmd; + podId = adminCmd.getPodId(); + clusterId = adminCmd.getClusterId(); + } + return startVirtualMachine(vmId, podId, clusterId, hostId, diskOfferingMap, null, cmd.getDeploymentPlanner()); } - private UserVm startVirtualMachine(DeployVMCmd cmd, Map additonalParams, String deploymentPlannerToUse) + private UserVm startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map diskOfferingMap, Map additonalParams, String deploymentPlannerToUse) throws ResourceUnavailableException, InsufficientCapacityException, ConcurrentOperationException { - - long vmId = cmd.getEntityId(); - Long hostId = cmd.getHostId(); UserVmVO vm = _vmDao.findById(vmId); - Pair> vmParamPair = null; + try { - vmParamPair = startVirtualMachine(vmId, hostId, additonalParams, deploymentPlannerToUse); + vmParamPair = startVirtualMachine(vmId, podId, clusterId, hostId, additonalParams, deploymentPlannerToUse); vm = vmParamPair.first(); // At this point VM should be in "Running" state @@ -4169,7 +4178,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir } try { - if (!cmd.getDataDiskTemplateToDiskOfferingMap().isEmpty()) { + if (!diskOfferingMap.isEmpty()) { List vols = _volsDao.findByInstance(tmpVm.getId()); for (VolumeVO vol : vols) { if (vol.getVolumeType() == Volume.Type.DATADISK) { @@ -4488,8 +4497,14 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir @Override public Pair> startVirtualMachine(long vmId, Long hostId, Map additionalParams, String deploymentPlannerToUse) throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { + return startVirtualMachine(vmId, null, null, hostId, additionalParams, deploymentPlannerToUse); + } + + @Override + public Pair> startVirtualMachine(long vmId, Long podId, Long clusterId, Long hostId, Map additionalParams, String deploymentPlannerToUse) + throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException { // Input validation - Account callerAccount = CallContext.current().getCallingAccount(); + final Account callerAccount = CallContext.current().getCallingAccount(); UserVO callerUser = _userDao.findById(CallContext.current().getCallingUserId()); // if account is removed, return error @@ -4514,19 +4529,6 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir throw new PermissionDeniedException("The owner of " + vm + " is disabled: " + vm.getAccountId()); } - Host destinationHost = null; - if (hostId != null) { - Account account = CallContext.current().getCallingAccount(); - if (!_accountService.isRootAdmin(account.getId())) { - throw new PermissionDeniedException( - "Parameter hostid can only be specified by a Root Admin, permission denied"); - } - destinationHost = _hostDao.findById(hostId); - if (destinationHost == null) { - throw new InvalidParameterValueException("Unable to find the host to deploy the VM, host id=" + hostId); - } - } - // check if vm is security group enabled if (_securityGroupMgr.isVmSecurityGroupEnabled(vmId) && _securityGroupMgr.getSecurityGroupsForVm(vmId).isEmpty() && !_securityGroupMgr.isVmMappedToDefaultSecurityGroup(vmId) && _networkModel.canAddDefaultSecurityGroup()) { @@ -4542,7 +4544,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir _securityGroupMgr.addInstanceToGroups(vmId, groupList); } } - + // Choose deployment planner + // Host takes 1st preference, Cluster takes 2nd preference and Pod takes 3rd + // Default behaviour is invoked when host, cluster or pod are not specified + boolean isRootAdmin = _accountService.isRootAdmin(callerAccount.getId()); + Pod destinationPod = getDestinationPod(podId, isRootAdmin); + Cluster destinationCluster = getDestinationCluster(clusterId, isRootAdmin); + Host destinationHost = getDestinationHost(hostId, isRootAdmin); DataCenterDeployment plan = null; boolean deployOnGivenHost = false; if (destinationHost != null) { @@ -4551,6 +4559,18 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir if (!AllowDeployVmIfGivenHostFails.value()) { deployOnGivenHost = true; } + } else if (destinationCluster != null) { + s_logger.debug("Destination Cluster to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationCluster.getPodId(), destinationCluster.getId(), null, null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } + } else if (destinationPod != null) { + s_logger.debug("Destination Pod to deploy the VM is specified, specifying a deployment plan to deploy the VM"); + plan = new DataCenterDeployment(vm.getDataCenterId(), destinationPod.getId(), null, null, null, null); + if (!AllowDeployVmIfGivenHostFails.value()) { + deployOnGivenHost = true; + } } // Set parameters @@ -4617,6 +4637,51 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir return vmParamPair; } + private Pod getDestinationPod(Long podId, boolean isRootAdmin) { + Pod destinationPod = null; + if (podId != null) { + if (!isRootAdmin) { + throw new PermissionDeniedException( + "Parameter " + ApiConstants.POD_ID + " can only be specified by a Root Admin, permission denied"); + } + destinationPod = _podDao.findById(podId); + if (destinationPod == null) { + throw new InvalidParameterValueException("Unable to find the pod to deploy the VM, pod id=" + podId); + } + } + return destinationPod; + } + + private Cluster getDestinationCluster(Long clusterId, boolean isRootAdmin) { + Cluster destinationCluster = null; + if (clusterId != null) { + if (!isRootAdmin) { + throw new PermissionDeniedException( + "Parameter " + ApiConstants.CLUSTER_ID + " can only be specified by a Root Admin, permission denied"); + } + destinationCluster = _clusterDao.findById(clusterId); + if (destinationCluster == null) { + throw new InvalidParameterValueException("Unable to find the cluster to deploy the VM, cluster id=" + clusterId); + } + } + return destinationCluster; + } + + private Host getDestinationHost(Long hostId, boolean isRootAdmin) { + Host destinationHost = null; + if (hostId != null) { + if (!isRootAdmin) { + throw new PermissionDeniedException( + "Parameter " + ApiConstants.HOST_ID + " can only be specified by a Root Admin, permission denied"); + } + destinationHost = _hostDao.findById(hostId); + if (destinationHost == null) { + throw new InvalidParameterValueException("Unable to find the host to deploy the VM, host id=" + hostId); + } + } + return destinationHost; + } + @Override public UserVm destroyVm(long vmId, boolean expunge) throws ResourceUnavailableException, ConcurrentOperationException { // Account caller = CallContext.current().getCallingAccount(); diff --git a/test/integration/smoke/test_vm_deployment_planner.py b/test/integration/smoke/test_vm_deployment_planner.py new file mode 100644 index 00000000000..f5cb092bc15 --- /dev/null +++ b/test/integration/smoke/test_vm_deployment_planner.py @@ -0,0 +1,261 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" BVT tests for CM Deployment Planner +""" +# Import Local Modules +from marvin.cloudstackAPI import (deployVirtualMachine, destroyVirtualMachine) +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + ServiceOffering, + Host, Pod, Cluster) +from marvin.lib.common import (get_domain, + get_zone, + get_template) +from marvin.lib.utils import cleanup_resources +from nose.plugins.attrib import attr + +class TestVMDeploymentPlanner(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + + testClient = super(TestVMDeploymentPlanner, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + cls.services = testClient.getParsedTestDataConfig() + + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests()) + cls.hypervisor = testClient.getHypervisorInfo() + cls.services['mode'] = cls.zone.networktype + + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + + # Create an account, network, VM and IP addresses + cls.account = Account.create( + cls.apiclient, + cls.services["account"], + domainid=cls.domain.id + ) + cls.service_offering = ServiceOffering.create( + cls.apiclient, + cls.services["service_offerings"]["tiny"] + ) + + cls.cleanup = [ + cls.account, + cls.service_offering + ] + + @classmethod + def tearDownClass(cls): + try: + cls.apiclient = super( + TestVMDeploymentPlanner, + cls + ).getClsTestClient().getApiClient() + # Clean up, terminate the created templates + cleanup_resources(cls.apiclient, cls.cleanup) + + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + + def deploy_vm(self, destination_id): + cmd = deployVirtualMachine.deployVirtualMachineCmd() + template = get_template( + self.apiclient, + self.zone.id, + hypervisor=self.hypervisor + ) + cmd.zoneid = self.zone.id + cmd.templateid = template.id + cmd.serviceofferingid = self.service_offering.id + cmd.hostid = destination_id + return self.apiclient.deployVirtualMachine(cmd) + + def destroy_vm(self, vm_id): + cmd = destroyVirtualMachine.destroyVirtualMachineCmd() + cmd.expunge = True + cmd.id = vm_id + return self.apiclient.destroyVirtualMachine(cmd) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="false") + def test_01_deploy_vm_on_specific_host(self): + hosts = Host.list( + self.apiclient, + zoneid=self.zone.id, + type='Routing' + ) + target_id = hosts[0].id + + vm = self.deploy_vm(target_id) + + self.assertEqual( + target_id, + vm.hostid, + "VM instance was not deployed on target host ID") + + self.destroy_vm(vm.id) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="false") + def test_02_deploy_vm_on_specific_cluster(self): + + # Select deployment cluster + clusters = Cluster.list( + self.apiclient, + ) + target_cluster = clusters[0] + target_id = target_cluster.id + cluster_hypervisor = target_cluster.hypervisortype + + template = get_template( + self.apiclient, + hypervisor=cluster_hypervisor + ) + + # deploy vm on cluster + cmd = deployVirtualMachine.deployVirtualMachineCmd() + cmd.zoneid = target_cluster.zoneid + cmd.serviceofferingid = self.service_offering.id + cmd.templateid = template.id + cmd.clusterid = target_id + vm = self.apiclient.deployVirtualMachine(cmd) + + vm_host = Host.list(self.apiclient, + id=vm.hostid + ) + + self.assertEqual( + target_id, + vm_host[0].clusterid, + "VM was not deployed on the provided cluster" + ) + self.destroy_vm(vm.id) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="false") + def test_03_deploy_vm_on_specific_pod(self): + pods = Pod.list( + self.apiclient, + ) + target_pod = pods[0] + + # Get host by Pod ID + host = Host.list( + self.apiclient, + podid=target_pod.id) + + # deploy vm on pod + cmd = deployVirtualMachine.deployVirtualMachineCmd() + cmd.zoneid = target_pod.zoneid + cmd.serviceofferingid = self.service_offering.id + + template = get_template( + self.apiclient, + hypervisor=host[0].hypervisortype + ) + + cmd.templateid = template.id + cmd.podid = target_pod.id + vm = self.apiclient.deployVirtualMachine(cmd) + + vm_host = Host.list(self.apiclient, + id=vm.hostid + ) + + self.assertEqual( + target_pod.id, + vm_host[0].podid, + "VM was not deployed on the target pod" + ) + self.destroy_vm(vm.id) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="false") + def test_04_deploy_vm_on_host_override_pod_and_cluster(self): + + # Optional parameters pod, cluster and host + pod = Pod.list(self.apiclient, zoneid=self.zone.id)[0] + clusters = Cluster.list(self.apiclient, zoneid=self.zone.id, podid=pod.id) + + self.assertEqual( + isinstance(clusters, list), + True, + "Check list response returns a valid list" + ) + + host = Host.list(self.apiclient, zoneid=self.zone.id, clusterid=clusters[0].id, type='Routing')[0] + + cmd = deployVirtualMachine.deployVirtualMachineCmd() + + # Required parameters + cmd.zoneid = self.zone.id + cmd.serviceofferingid = self.service_offering.id + template = get_template(self.apiclient, zone_id=self.zone.id, hypervisor=host.hypervisor) + cmd.templateid = template.id + + # Add optional deployment params + cmd.podid = pod.id + cmd.clusterid = clusters[1].id if len(clusters) > 1 else clusters[0].id + cmd.hostid = host.id + + vm = self.apiclient.deployVirtualMachine(cmd) + + self.assertEqual( + vm.hostid, + host.id, + "VM was not deployed on the target host ID" + ) + + self.destroy_vm(vm.id) + + @attr(tags=["advanced", "advancedns", "ssh", "smoke"], required_hardware="false") + def test_05_deploy_vm_on_cluster_override_pod(self): + + # Optional parameters pod, cluster and host + pod = Pod.list(self.apiclient, zoneid=self.zone.id)[0] + clusters = Cluster.list(self.apiclient, zoneid=self.zone.id, podid=pod.id) + + self.assertEqual( + isinstance(clusters, list), + True, + "Check list response returns a valid list" + ) + + cmd = deployVirtualMachine.deployVirtualMachineCmd() + + # Required parameters + cmd.zoneid = self.zone.id + cmd.serviceofferingid = self.service_offering.id + template = get_template(self.apiclient, zone_id=self.zone.id, hypervisor=clusters[0].hypervisortype) + cmd.templateid = template.id + + # Add optional deployment params + cmd.podid = pod.id + cmd.clusterid = clusters[0].id + + vm = self.apiclient.deployVirtualMachine(cmd) + + vm_host = Host.list(self.apiclient, + id=vm.hostid + ) + + self.assertEqual( + vm_host[0].clusterid, + clusters[0].id, + "VM was not deployed on the target cluster" + ) + + self.destroy_vm(vm.id) diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index a393ef7597b..6f74b97f6dd 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -5545,6 +5545,15 @@ textarea { background: #d6d6d6; } +.multi-wizard .content .section .larger-area { + height: 134px !important; +} + +.multi-wizard .content .section .lower-area { + height: 35px !important ; + margin: 7px auto auto !important ; +} + .multi-wizard .content .section .select-area .desc { float: right; width: 155px; @@ -5591,6 +5600,14 @@ textarea { -o-text-shadow: 0 2px 2px #efefef; } +.multi-wizard .content .section.select-deployment .select-area select { + margin: 9px 0 0 14px; +} + +.multi-wizard .content .section.select-deployment .select-area label.desc { + padding: 0; +} + .multi-wizard .content .section .select-area label.error { margin: 2px 0 0 14px; font-size: 10px; @@ -5625,6 +5642,14 @@ textarea { height: 206px; } +.multi-wizard .content .section.select-template { + height: 206px; +} + +.multi-wizard .content .section.smaller-height { + height: 126px !important; +} + .multi-wizard .content.tab-view { margin: 31px 0 0; padding: 0 8px; diff --git a/ui/index.html b/ui/index.html index 23fac1b7dd9..c855e67f4f9 100644 --- a/ui/index.html +++ b/ui/index.html @@ -94,17 +94,28 @@
-
-

+
+

-
-
- +
+ + +
+
+ + +
+
+ + +
+
+ +
-
+

diff --git a/ui/l10n/en.js b/ui/l10n/en.js index cd5f44de4d9..1e88ee4ded9 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1543,6 +1543,7 @@ var dictionary = { "label.select-view":"Select view", "label.select.a.template":"Select a template", "label.select.a.zone":"Select a zone", +"label.select.deployment.infrastructure":"Select deployment infrastructure", "label.select.instance":"Select instance", "label.select.host":"Select host", "label.select.instance.to.attach.volume.to":"Select instance to attach volume to", diff --git a/ui/scripts/instanceWizard.js b/ui/scripts/instanceWizard.js index d175f1f8dfd..d2ed9bccc03 100644 --- a/ui/scripts/instanceWizard.js +++ b/ui/scripts/instanceWizard.js @@ -22,6 +22,108 @@ var step6ContainerType = 'nothing-to-select'; //'nothing-to-select', 'select-network', 'select-security-group', 'select-advanced-sg'(advanced sg-enabled zone) cloudStack.instanceWizard = { + + fetchPodList: function (podcallback, parentId) { + var urlString = "listPods"; + if (parentId != -1) { + urlString += "&zoneid=" + parentId + } + $.ajax({ + url: createURL(urlString), + dataType: "json", + async: false, + success: function (json) { + var pods = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + var podsObjs = json.listpodsresponse.pod; + if (podsObjs !== undefined) { + $(podsObjs).each(function () { + pods.push({ + id: this.id, + description: this.name, + parentId: this.zoneid + }); + }); + } + podcallback(pods); + } + }); + }, + + fetchClusterList: function (clustercallback, parentId, zoneId) { + var urlString = "listClusters"; + // If Pod ID is not specified, filter clusters by Zone + if (parentId != -1) { + urlString += "&podid=" + parentId; + } else if (zoneId != -1) { + urlString += "&zoneid=" + zoneId; + } + + $.ajax({ + url: createURL(urlString), + dataType: "json", + async: false, + success: function (json) { + var clusters = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + var clusterObjs = json.listclustersresponse.cluster; + if (clusterObjs != undefined) { + $(clusterObjs).each(function () { + clusters.push({ + id: this.id, + description: this.name, + parentId: this.podid + }); + }); + } + clustercallback(clusters); + } + }); + }, + + fetchHostList: function (hostcallback, parentId, podId, zoneId) { + // If Cluster ID is not specified, filter hosts by Zone or Pod + var urlString = "listHosts&state=Up&type=Routing"; + + if (parentId != -1) { + urlString += "&clusterid=" + parentId; + } else if (podId != -1) { + urlString += "&podid=" + podId; + } else if (zoneId != -1) { + urlString += "&zoneid=" + zoneId + } + + $.ajax({ + url: createURL(urlString), + dataType: "json", + async: false, + success: function (json) { + var hosts = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + var hostObjs = json.listhostsresponse.host; + if (hostObjs != undefined) { + $(hostObjs).each(function () { + hosts.push({ + id: this.id, + description: this.name, + parentId: this.clusterid + }); + }); + } + hostcallback(hosts); + } + }); + }, + //min disk offering size when custom disk size is used minDiskOfferingSize: function() { return g_capabilities.customdiskofferingminsize; @@ -94,25 +196,114 @@ } //in all other cases (as well as from instance page) all zones are populated to dropdown else { + var postData = {}; + var zones = [{ + id: -1, + name: 'Default' + }]; $.ajax({ url: createURL("listZones&available=true"), dataType: "json", async: false, success: function(json) { zoneObjs = json.listzonesresponse.zone; - args.response.success({ - data: { - zones: zoneObjs - } + $(zoneObjs).each(function() { + zones.push({ + id: this.id, + name: this.name + }); }); } }); + + $.extend(postData, { + "zones": zones + }); + + if (isAdmin()) { + pods = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + $.ajax({ + url: createURL("listPods"), + dataType: "json", + async: false, + success: function(json) { + if (json.listpodsresponse.pod != undefined) { + podObjs = json.listpodsresponse.pod; + $(podObjs).each(function() { + pods.push({ + id: this.id, + description: this.name, + parentId: this.zoneid + }); + }); + } + } + }); + clusters = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + $.ajax({ + url: createURL("listClusters"), + dataType: "json", + async: false, + success: function(json) { + if (json.listclustersresponse.cluster != undefined) { + clusterObjs = json.listclustersresponse.cluster; + $(clusterObjs).each(function() { + clusters.push({ + id: this.id, + description: this.name, + parentId: this.podid + }); + }); + } + } + }); + hosts = [{ + id: -1, + description: 'Default', + parentId: -1 + }]; + $.ajax({ + url: createURL("listHosts&state=Up&type=Routing"), + dataType: "json", + async: false, + success: function(json) { + if (json.listhostsresponse.host != undefined) { + hostObjs = json.listhostsresponse.host; + $(hostObjs).each(function() { + hosts.push({ + id: this.id, + description: this.name, + parentId: this.clusterid + }); + }); + } + } + }); + $.extend(postData, { + "pods": pods, + "clusters": clusters, + "hosts": hosts + }); + + } + args.response.success({ + data: postData + }); } }, // Step 2: Select template function(args) { $(zoneObjs).each(function() { + args.currentData.zoneid = (args.currentData.zoneid == -1)? this.id : args.currentData.zoneid ; if (this.id == args.currentData.zoneid) { selectedZoneObj = this; return false; //break the $.each() loop @@ -539,7 +730,7 @@ var defaultNetworkArray = [], optionalNetworkArray = []; var networkData = { - zoneId: args.currentData.zoneid, + zoneId: selectedZoneObj.id, canusefordeploy: true }; @@ -662,7 +853,7 @@ dataType: "json", data: { forvpc: false, - zoneid: args.currentData.zoneid, + zoneid: selectedZoneObj.id, guestiptype: 'Isolated', supportedServices: 'SourceNat', specifyvlan: false, @@ -769,10 +960,29 @@ var deployVmData = {}; //step 1 : select zone + zoneId = (args.data.zoneid == -1)? selectedZoneObj.id : args.data.zoneid; $.extend(deployVmData, { - zoneid : args.data.zoneid + zoneid : zoneId }); + if (args.data.podid != -1) { + $.extend(deployVmData, { + podid : args.data.podid + }); + } + + if (args.data.clusterid != -1) { + $.extend(deployVmData, { + clusterid : args.data.clusterid + }); + } + + if (args.data.hostid != -1) { + $.extend(deployVmData, { + hostid : args.data.hostid + }); + } + //step 2: select template $.extend(deployVmData, { templateid : args.data.templateid diff --git a/ui/scripts/instances.js b/ui/scripts/instances.js index 514565985be..86d49c9a54c 100644 --- a/ui/scripts/instances.js +++ b/ui/scripts/instances.js @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. (function($, cloudStack) { - var vmMigrationHostObjs, ostypeObjs; + var vmMigrationHostObjs, ostypeObjs, zoneWideStorage; var vmStartAction = function(args) { var action = { @@ -919,49 +919,133 @@ title: 'label.action.start.instance', desc: 'message.action.start.instance', fields: { - hostId: { - label: 'label.host', - isHidden: function(args) { - if (isAdmin()) - return false; - else - return true; - }, - select: function(args) { - if (isAdmin()) { - $.ajax({ - url: createURL("listHosts&state=Up&type=Routing&zoneid=" + args.context.instances[0].zoneid), - dataType: "json", - async: true, - success: function(json) { - if (json.listhostsresponse.host != undefined) { - hostObjs = json.listhostsresponse.host; - var items = [{ - id: -1, - description: 'Default' - }]; - $(hostObjs).each(function() { - items.push({ - id: this.id, - description: this.name - }); + podId: { + label: 'label.pod', + isHidden: function(args) { + return !isAdmin(); + }, + select: function(args) { + if (isAdmin()) { + $.ajax({ + url: createURL("listPods&zoneid=" + args.context.instances[0].zoneid), + dataType: "json", + async: true, + success: function(json) { + if (json.listpodsresponse.pod != undefined) { + podObjs = json.listpodsresponse.pod; + var items = [{ + id: -1, + description: 'Default' + }]; + $(podObjs).each(function() { + items.push({ + id: this.id, + description: this.name }); - args.response.success({ - data: items - }); - } else { - cloudStack.dialog.notice({ - message: _l('No Hosts are avaialble') - }); - } + }); + args.response.success({ + data: items + }); + } else { + cloudStack.dialog.notice({ + message: _l('No Pods are available') + }); } - }); - } else { - args.response.success({ - data: null - }); - } + } + }); + } else { + args.response.success({ + data: null + }); } + } + }, + clusterId: { + label: 'label.cluster', + dependsOn: 'podId', + select: function(args) { + if (isAdmin()) { + var urlString = "listClusters&zoneid=" + args.context.instances[0].zoneid; + if (args.podId != -1) { + urlString += '&podid=' + args.podId; + } + $.ajax({ + url: createURL(urlString), + dataType: "json", + async: true, + success: function(json) { + if (json.listclustersresponse.cluster != undefined) { + clusterObjs = json.listclustersresponse.cluster; + var items = [{ + id: -1, + description: 'Default' + }]; + $(clusterObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + args.response.success({ + data: items + }); + } else { + cloudStack.dialog.notice({ + message: _l('No Clusters are avaialble') + }); + } + } + }); + + } else { + args.response.success({ + data: null + }); + } + } + }, + hostId: { + label: 'label.host', + dependsOn: 'clusterId', + select: function(args) { + var urlString = "listHosts&state=Up&type=Routing&zoneid=" + args.context.instances[0].zoneid; + if (args.clusterId != -1) { + urlString += "&clusterid=" + args.clusterId; + } + if (isAdmin()) { + $.ajax({ + url: createURL(urlString), + dataType: "json", + async: true, + success: function(json) { + if (json.listhostsresponse.host != undefined) { + hostObjs = json.listhostsresponse.host; + var items = [{ + id: -1, + description: 'Default' + }]; + $(hostObjs).each(function() { + items.push({ + id: this.id, + description: this.name + }); + }); + args.response.success({ + data: items + }); + } else { + cloudStack.dialog.notice({ + message: _l('No Hosts are avaialble') + }); + } + } + }); + } else { + args.response.success({ + data: null + }); + } + } } } }, @@ -969,6 +1053,16 @@ var data = { id: args.context.instances[0].id } + if (args.$form.find('.form-item[rel=podId]').css("display") != "none" && args.data.podId != -1) { + $.extend(data, { + podid: args.data.podId + }); + } + if (args.$form.find('.form-item[rel=clusterId]').css("display") != "none" && args.data.clusterId != -1) { + $.extend(data, { + clusterid: args.data.clusterId + }); + } if (args.$form.find('.form-item[rel=hostId]').css("display") != "none" && args.data.hostId != -1) { $.extend(data, { hostid: args.data.hostId diff --git a/ui/scripts/ui-custom/instanceWizard.js b/ui/scripts/ui-custom/instanceWizard.js index ef6d2246c47..5ce18702b68 100644 --- a/ui/scripts/ui-custom/instanceWizard.js +++ b/ui/scripts/ui-custom/instanceWizard.js @@ -277,6 +277,66 @@ }).click(); }; + if (isAdmin()) { + $step.find('.select-deployment .podid').parent().show(); + $step.find('.select-deployment .clusterid').parent().show(); + $step.find('.select-deployment .hostid').parent().show(); + + + var updateFieldOptions = function(fieldClass, wizardField) { + return function(data) { + var fieldSelect = $step.find('.select-deployment .' + fieldClass); + fieldSelect.find('option').remove().end(); + $(data).each(function() { + fieldSelect.append( + $('