This is an automated email from the ASF dual-hosted git repository.

rohit pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new f6ceeab3b3a server: Enforce strict host tag check (#9017)
f6ceeab3b3a is described below

commit f6ceeab3b3ab5e75163387705bb3e1480999a100
Author: Vishesh <[email protected]>
AuthorDate: Tue Jun 25 14:42:17 2024 +0530

    server: Enforce strict host tag check (#9017)
    
    Documentation PR: 
https://github.com/apache/cloudstack-documentation/pull/398
    
    Currently, an administrator can break host tag compatibility for a VM 
administrator by certain operations:
    * deploy/start VM on a specific host
    * migrate VM
    * restore VM
    * scale VM
    
    This PR allows the user to specify tags which must be checked during these 
operations.
    
    Global Settings
    1. `vm.strict.host.tags` - A comma-separated list of tags for strict host 
check (Default - empty)
    2. `vm.strict.resource.limit.host.tag.check` - Determines whether the 
resource limits tags are considered strict or not (Default - true)
    
    During the above operations, we now check and throw an error if host tags 
compatibility is being broken for tags specified in `vm.strict.host.tags`. If 
`vm.strict.resource.limit.host.tag.check` is set to `true`, tags set in 
`resource.limit.host.tags` are also checked during these operations.
---
 .github/workflows/ci.yml                           |   1 +
 .../src/main/java/com/cloud/host/HostVO.java       |  46 +-
 .../src/test/java/com/cloud/host/HostVOTest.java   |  53 +-
 .../deploy/DeploymentPlanningManagerImpl.java      |   5 +-
 .../src/main/java/com/cloud/vm/UserVmManager.java  |  33 ++
 .../main/java/com/cloud/vm/UserVmManagerImpl.java  |  57 ++-
 .../cloudstack/vm/UnmanagedVMsManagerImpl.java     |   2 +-
 .../java/com/cloud/vm/UserVmManagerImplTest.java   |  40 ++
 test/integration/smoke/test_vm_strict_host_tags.py | 552 +++++++++++++++++++++
 tools/marvin/marvin/config/test_data.py            |  19 +-
 tools/marvin/marvin/lib/base.py                    |   3 +-
 11 files changed, 766 insertions(+), 45 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4fe612cad5d..133e2c35b4e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -134,6 +134,7 @@ jobs:
                   smoke/test_usage
                   smoke/test_usage_events
                   smoke/test_vm_deployment_planner
+                  smoke/test_vm_strict_host_tags
                   smoke/test_vm_schedule
                   smoke/test_vm_life_cycle
                   smoke/test_vm_lifecycle_unmanage_import
diff --git a/engine/schema/src/main/java/com/cloud/host/HostVO.java 
b/engine/schema/src/main/java/com/cloud/host/HostVO.java
index 3e64d20d0e2..1a507da7957 100644
--- a/engine/schema/src/main/java/com/cloud/host/HostVO.java
+++ b/engine/schema/src/main/java/com/cloud/host/HostVO.java
@@ -16,13 +16,13 @@
 // under the License.
 package com.cloud.host;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 
 import javax.persistence.Column;
@@ -45,6 +45,7 @@ import javax.persistence.Transient;
 import org.apache.cloudstack.util.HypervisorTypeConverter;
 import org.apache.cloudstack.utils.jsinterpreter.TagAsRuleHelper;
 import 
org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 
@@ -768,27 +769,48 @@ public class HostVO implements Host {
         this.uuid = uuid;
     }
 
-    public boolean checkHostServiceOfferingAndTemplateTags(ServiceOffering 
serviceOffering, VirtualMachineTemplate template) {
-        if (serviceOffering == null || template == null) {
-            return false;
-        }
+    private Set<String> 
getHostServiceOfferingAndTemplateStrictTags(ServiceOffering serviceOffering, 
VirtualMachineTemplate template, Set<String> strictHostTags) {
         if (StringUtils.isEmpty(serviceOffering.getHostTag()) && 
StringUtils.isEmpty(template.getTemplateTag())) {
-            return true;
+            return new HashSet<>();
         }
-        if (getHostTags() == null) {
-            return false;
-        }
-        HashSet<String> hostTagsSet = new HashSet<>(getHostTags());
-        List<String> tags = new ArrayList<>();
+        List<String> hostTagsList = getHostTags();
+        HashSet<String> hostTagsSet = CollectionUtils.isNotEmpty(hostTagsList) 
? new HashSet<>(hostTagsList) : new HashSet<>();
+        HashSet<String> tags = new HashSet<>();
         if (StringUtils.isNotEmpty(serviceOffering.getHostTag())) {
             
tags.addAll(Arrays.asList(serviceOffering.getHostTag().split(",")));
         }
-        if (StringUtils.isNotEmpty(template.getTemplateTag()) && 
!tags.contains(template.getTemplateTag())) {
+        if (StringUtils.isNotEmpty(template.getTemplateTag())) {
             tags.add(template.getTemplateTag());
         }
+        tags.removeIf(tag -> !strictHostTags.contains(tag));
+        tags.removeAll(hostTagsSet);
+        return tags;
+    }
+
+    public boolean checkHostServiceOfferingAndTemplateTags(ServiceOffering 
serviceOffering, VirtualMachineTemplate template, Set<String> strictHostTags) {
+        if (serviceOffering == null || template == null) {
+            return false;
+        }
+        Set<String> tags = 
getHostServiceOfferingAndTemplateStrictTags(serviceOffering, template, 
strictHostTags);
+        if (tags.isEmpty()) {
+            return true;
+        }
+        List<String> hostTagsList = getHostTags();
+        HashSet<String> hostTagsSet = CollectionUtils.isNotEmpty(hostTagsList) 
? new HashSet<>(hostTagsList) : new HashSet<>();
         return hostTagsSet.containsAll(tags);
     }
 
+    public Set<String> 
getHostServiceOfferingAndTemplateMissingTags(ServiceOffering serviceOffering, 
VirtualMachineTemplate template, Set<String> strictHostTags) {
+        Set<String> tags = 
getHostServiceOfferingAndTemplateStrictTags(serviceOffering, template, 
strictHostTags);
+        if (tags.isEmpty()) {
+            return new HashSet<>();
+        }
+        List<String> hostTagsList = getHostTags();
+        HashSet<String> hostTagsSet = CollectionUtils.isNotEmpty(hostTagsList) 
? new HashSet<>(hostTagsList) : new HashSet<>();
+        tags.removeAll(hostTagsSet);
+        return tags;
+    }
+
     public boolean checkHostServiceOfferingTags(ServiceOffering 
serviceOffering) {
         if (serviceOffering == null) {
             return false;
diff --git a/engine/schema/src/test/java/com/cloud/host/HostVOTest.java 
b/engine/schema/src/test/java/com/cloud/host/HostVOTest.java
index cd9ac3cc172..3262c4cc291 100755
--- a/engine/schema/src/test/java/com/cloud/host/HostVOTest.java
+++ b/engine/schema/src/test/java/com/cloud/host/HostVOTest.java
@@ -20,14 +20,18 @@ import com.cloud.offering.ServiceOffering;
 import com.cloud.service.ServiceOfferingVO;
 import com.cloud.template.VirtualMachineTemplate;
 import com.cloud.vm.VirtualMachine;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import org.junit.Test;
-import org.junit.Before;
-import org.mockito.Mockito;
 
 public class HostVOTest {
     HostVO host;
@@ -37,7 +41,7 @@ public class HostVOTest {
     public void setUp() throws Exception {
         host = new HostVO();
         offering = new ServiceOfferingVO("TestSO", 0, 0, 0, 0, 0,
-                false, "TestSO", false,VirtualMachine.Type.User,false);
+                false, "TestSO", false, VirtualMachine.Type.User, false);
     }
 
     @Test
@@ -52,14 +56,14 @@ public class HostVOTest {
 
     @Test
     public void testRightTag() {
-        host.setHostTags(Arrays.asList("tag1","tag2"), false);
+        host.setHostTags(Arrays.asList("tag1", "tag2"), false);
         offering.setHostTag("tag2,tag1");
         assertTrue(host.checkHostServiceOfferingTags(offering));
     }
 
     @Test
     public void testWrongTag() {
-        host.setHostTags(Arrays.asList("tag1","tag2"), false);
+        host.setHostTags(Arrays.asList("tag1", "tag2"), false);
         offering.setHostTag("tag2,tag4");
         assertFalse(host.checkHostServiceOfferingTags(offering));
     }
@@ -87,40 +91,59 @@ public class HostVOTest {
 
     @Test
     public void testEitherNoSOOrTemplate() {
-        assertFalse(host.checkHostServiceOfferingAndTemplateTags(null, 
Mockito.mock(VirtualMachineTemplate.class)));
-        
assertFalse(host.checkHostServiceOfferingAndTemplateTags(Mockito.mock(ServiceOffering.class),
 null));
+        assertFalse(host.checkHostServiceOfferingAndTemplateTags(null, 
Mockito.mock(VirtualMachineTemplate.class), null));
+        
assertFalse(host.checkHostServiceOfferingAndTemplateTags(Mockito.mock(ServiceOffering.class),
 null, null));
     }
 
     @Test
     public void testNoTagOfferingTemplate() {
-        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
Mockito.mock(VirtualMachineTemplate.class)));
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Collections.emptySet()));
+        assertTrue(host.getHostServiceOfferingAndTemplateMissingTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Collections.emptySet()).isEmpty());
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Set.of("tag1", "tag2")));
+        assertTrue(host.getHostServiceOfferingAndTemplateMissingTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Set.of("tag1", "tag2")).isEmpty());
     }
 
     @Test
     public void testRightTagOfferingTemplate() {
         host.setHostTags(Arrays.asList("tag1", "tag2"), false);
         offering.setHostTag("tag2,tag1");
-        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
Mockito.mock(VirtualMachineTemplate.class)));
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Set.of("tag1")));
+        Set<String> actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, 
Mockito.mock(VirtualMachineTemplate.class), Set.of("tag1"));
+        assertTrue(actualMissingTags.isEmpty());
+
         host.setHostTags(Arrays.asList("tag1", "tag2", "tag3"), false);
         offering.setHostTag("tag2,tag1");
         VirtualMachineTemplate template = 
Mockito.mock(VirtualMachineTemplate.class);
         Mockito.when(template.getTemplateTag()).thenReturn("tag3");
-        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
template));
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
template, Set.of("tag2", "tag3")));
+        actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, template, 
Set.of("tag2", "tag3"));
+        assertTrue(actualMissingTags.isEmpty());
         host.setHostTags(List.of("tag3"), false);
         offering.setHostTag(null);
-        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
template));
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
template, Set.of("tag3")));
+        actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, template, 
Set.of("tag3"));
+        assertTrue(actualMissingTags.isEmpty());
+
+        assertTrue(host.checkHostServiceOfferingAndTemplateTags(offering, 
template, Set.of("tag2", "tag1")));
+        actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, template, 
Set.of("tag2", "tag1"));
+        assertTrue(actualMissingTags.isEmpty());
     }
 
     @Test
     public void testWrongOfferingTag() {
-        host.setHostTags(Arrays.asList("tag1","tag2"), false);
+        host.setHostTags(Arrays.asList("tag1", "tag2"), false);
         offering.setHostTag("tag2,tag4");
         VirtualMachineTemplate template = 
Mockito.mock(VirtualMachineTemplate.class);
         Mockito.when(template.getTemplateTag()).thenReturn("tag1");
-        assertFalse(host.checkHostServiceOfferingAndTemplateTags(offering, 
template));
+        assertFalse(host.checkHostServiceOfferingAndTemplateTags(offering, 
template, Set.of("tag1", "tag2", "tag3", "tag4")));
+        Set<String> actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, template, 
Set.of("tag1", "tag2", "tag3", "tag4"));
+        assertEquals(Set.of("tag4"), actualMissingTags);
+
         offering.setHostTag("tag1,tag2");
         template = Mockito.mock(VirtualMachineTemplate.class);
         Mockito.when(template.getTemplateTag()).thenReturn("tag3");
-        assertFalse(host.checkHostServiceOfferingAndTemplateTags(offering, 
template));
+        actualMissingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(offering, template, 
Set.of("tag1", "tag2", "tag3", "tag4"));
+        assertFalse(host.checkHostServiceOfferingAndTemplateTags(offering, 
template, Set.of("tag1", "tag2", "tag3", "tag4")));
+        assertEquals(Set.of("tag3"), actualMissingTags);
     }
 }
diff --git 
a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java 
b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java
index 784f6451c13..71977e79ece 100644
--- a/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java
+++ b/server/src/main/java/com/cloud/deploy/DeploymentPlanningManagerImpl.java
@@ -35,6 +35,7 @@ import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import com.cloud.vm.UserVmManager;
 import org.apache.cloudstack.affinity.AffinityGroupDomainMapVO;
 import com.cloud.storage.VMTemplateVO;
 import com.cloud.storage.dao.VMTemplateDao;
@@ -282,7 +283,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, 
Configurable {
         boolean storageMigrationNeededDuringClusterMigration = false;
         for (Volume volume : volumes) {
             StoragePoolVO pool = _storagePoolDao.findById(volume.getPoolId());
-            if (List.of(ScopeType.HOST, 
ScopeType.CLUSTER).contains(pool.getScope())) {
+            if (pool != null && List.of(ScopeType.HOST, 
ScopeType.CLUSTER).contains(pool.getScope())) {
                 storageMigrationNeededDuringClusterMigration = true;
                 break;
             }
@@ -777,7 +778,7 @@ StateListener<State, VirtualMachine.Event, VirtualMachine>, 
Configurable {
         VirtualMachineTemplate template = vmProfile.getTemplate();
         if (offering.getHostTag() != null || template.getTemplateTag() != 
null) {
             _hostDao.loadHostTags(host);
-            if (!host.checkHostServiceOfferingAndTemplateTags(offering, 
template)) {
+            if (!host.checkHostServiceOfferingAndTemplateTags(offering, 
template, UserVmManager.getStrictHostTags())) {
                 logger.debug("Service Offering host tag or template tag does 
not match the last host of this VM");
                 return false;
             }
diff --git a/server/src/main/java/com/cloud/vm/UserVmManager.java 
b/server/src/main/java/com/cloud/vm/UserVmManager.java
index 7cd1a98fd74..0dc7a7bb73f 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManager.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManager.java
@@ -17,9 +17,12 @@
 package com.cloud.vm;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
+import com.cloud.utils.StringUtils;
 import org.apache.cloudstack.api.BaseCmd.HTTPMethod;
 import org.apache.cloudstack.framework.config.ConfigKey;
 
@@ -38,6 +41,8 @@ import com.cloud.template.VirtualMachineTemplate;
 import com.cloud.uservm.UserVm;
 import com.cloud.utils.Pair;
 
+import static com.cloud.user.ResourceLimitService.ResourceLimitHostTags;
+
 /**
  *
  *
@@ -59,6 +64,22 @@ public interface UserVmManager extends UserVmService {
             "Destroys the VM's root volume when the VM is destroyed.",
             true, ConfigKey.Scope.Domain);
 
+    ConfigKey<String> StrictHostTags = new ConfigKey<>(
+            "Advanced",
+            String.class,
+            "vm.strict.host.tags",
+            "",
+            "A comma-separated list of tags which must match during operations 
like modifying the compute" +
+                    "offering for an instance, and starting or live migrating 
an instance to a specific host.",
+            true);
+    ConfigKey<Boolean> EnforceStrictResourceLimitHostTagCheck = new 
ConfigKey<Boolean>(
+            "Advanced",
+            Boolean.class,
+            "vm.strict.resource.limit.host.tag.check",
+            "true",
+            "If set to true, tags specified in `resource.limit.host.tags` are 
also included in vm.strict.host.tags.",
+            true);
+
     static final int MAX_USER_DATA_LENGTH_BYTES = 2048;
 
     public  static  final String CKS_NODE = "cksnode";
@@ -125,6 +146,18 @@ public interface UserVmManager extends UserVmService {
 
     void generateUsageEvent(VirtualMachine vm, boolean isDisplay, String 
eventType);
 
+    static Set<String> getStrictHostTags() {
+        String strictHostTags = StrictHostTags.value();
+        Set<String> strictHostTagsSet = new HashSet<>();
+        if (StringUtils.isNotEmpty(strictHostTags)) {
+            strictHostTagsSet.addAll(List.of(strictHostTags.split(",")));
+        }
+        if (EnforceStrictResourceLimitHostTagCheck.value() && 
StringUtils.isNotEmpty(ResourceLimitHostTags.value())) {
+            
strictHostTagsSet.addAll(List.of(ResourceLimitHostTags.value().split(",")));
+        }
+        return strictHostTagsSet;
+    }
+
     void persistDeviceBusInfo(UserVmVO paramUserVmVO, String paramString);
 
     boolean checkIfDynamicScalingCanBeEnabled(VirtualMachine vm, 
ServiceOffering offering, VirtualMachineTemplate template, Long zoneId);
diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java 
b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
index 6406d75e866..80286a3daec 100644
--- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
+++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java
@@ -1965,9 +1965,11 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
             HostVO instanceHost = _hostDao.findById(vmInstance.getHostId());
             _hostDao.loadHostTags(instanceHost);
 
-            if 
(!instanceHost.checkHostServiceOfferingAndTemplateTags(newServiceOfferingVO, 
template)) {
-                logger.error(String.format("Cannot upgrade VM [%s] as the new 
service offering [%s] does not have the required host tags %s.", vmInstance, 
newServiceOfferingVO,
-                        instanceHost.getHostTags()));
+            Set<String> strictHostTags = UserVmManager.getStrictHostTags();
+            if 
(!instanceHost.checkHostServiceOfferingAndTemplateTags(newServiceOfferingVO, 
template, strictHostTags)) {
+                logger.error("Cannot upgrade VM {} as the new service offering 
{} does not have the required host tags {}.",
+                        vmInstance, newServiceOfferingVO,
+                        
instanceHost.getHostServiceOfferingAndTemplateMissingTags(newServiceOfferingVO, 
template, strictHostTags));
                 return false;
             }
         }
@@ -2076,6 +2078,7 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
 
             // Check disable threshold for cluster is not crossed
             HostVO host = _hostDao.findById(vmInstance.getHostId());
+            _hostDao.loadDetails(host);
             if 
(_capacityMgr.checkIfClusterCrossesThreshold(host.getClusterId(), cpuDiff, 
memoryDiff)) {
                 throw new CloudRuntimeException(String.format("Unable to scale 
%s due to insufficient resources.", vmInstance.toString()));
             }
@@ -2088,12 +2091,14 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
                     
_resourceLimitMgr.updateVmResourceCountForServiceOfferingChange(caller.getAccountId(),
 vmInstance.isDisplay(),
                             (long) currentCpu, (long) newCpu, (long) 
currentMemory, (long) newMemory,
                             currentServiceOffering, newServiceOffering, 
template);
-                    // #1 Check existing host has capacity
+
+                    // #1 Check existing host has capacity & and the correct 
tags
                     if 
(!excludes.shouldAvoid(ApiDBUtils.findHostById(vmInstance.getHostId()))) {
                         existingHostHasCapacity = 
_capacityMgr.checkIfHostHasCpuCapability(vmInstance.getHostId(), newCpu, 
newSpeed)
                                 && 
_capacityMgr.checkIfHostHasCapacity(vmInstance.getHostId(), cpuDiff, 
ByteScaleUtils.mebibytesToBytes(memoryDiff), false,
                                         
_capacityMgr.getClusterOverProvisioningFactor(host.getClusterId(), 
Capacity.CAPACITY_TYPE_CPU),
-                                        
_capacityMgr.getClusterOverProvisioningFactor(host.getClusterId(), 
Capacity.CAPACITY_TYPE_MEMORY), false);
+                                        
_capacityMgr.getClusterOverProvisioningFactor(host.getClusterId(), 
Capacity.CAPACITY_TYPE_MEMORY), false)
+                                && checkEnforceStrictHostTagCheck(vmInstance, 
host);
                         excludes.addHost(vmInstance.getHostId());
                     }
 
@@ -5450,11 +5455,14 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
         boolean isRootAdmin = 
_accountService.isRootAdmin(callerAccount.getId());
         Pod destinationPod = getDestinationPod(podId, isRootAdmin);
         Cluster destinationCluster = getDestinationCluster(clusterId, 
isRootAdmin);
-        Host destinationHost = getDestinationHost(hostId, isRootAdmin, 
isExplicitHost);
+        HostVO destinationHost = getDestinationHost(hostId, isRootAdmin, 
isExplicitHost);
         DataCenterDeployment plan = null;
         boolean deployOnGivenHost = false;
         if (destinationHost != null) {
             logger.debug("Destination Host to deploy the VM is specified, 
specifying a deployment plan to deploy the VM");
+            _hostDao.loadHostTags(destinationHost);
+            validateStrictHostTagCheck(vm, destinationHost);
+
             final ServiceOfferingVO offering = 
serviceOfferingDao.findById(vm.getId(), vm.getServiceOfferingId());
             Pair<Boolean, Boolean> cpuCapabilityAndCapacity = 
_capacityMgr.checkIfHostHasCpuCapabilityAndCapacity(destinationHost, offering, 
false);
             if (!cpuCapabilityAndCapacity.first() || 
!cpuCapabilityAndCapacity.second()) {
@@ -5630,8 +5638,8 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
         return destinationCluster;
     }
 
-    private Host getDestinationHost(Long hostId, boolean isRootAdmin, boolean 
isExplicitHost) {
-        Host destinationHost = null;
+    private HostVO getDestinationHost(Long hostId, boolean isRootAdmin, 
boolean isExplicitHost) {
+        HostVO destinationHost = null;
         if (hostId != null) {
             if (isExplicitHost && !isRootAdmin) {
                 throw new PermissionDeniedException(
@@ -6673,6 +6681,31 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
         }
     }
 
+    protected boolean checkEnforceStrictHostTagCheck(VMInstanceVO vm, HostVO 
host) {
+        ServiceOffering serviceOffering = 
serviceOfferingDao.findByIdIncludingRemoved(vm.getServiceOfferingId());
+        VirtualMachineTemplate template = 
_templateDao.findByIdIncludingRemoved(vm.getTemplateId());
+        return checkEnforceStrictHostTagCheck(host, serviceOffering, template);
+    }
+
+    private boolean checkEnforceStrictHostTagCheck(HostVO host, 
ServiceOffering serviceOffering, VirtualMachineTemplate template) {
+        Set<String> strictHostTags = UserVmManager.getStrictHostTags();
+        return host.checkHostServiceOfferingAndTemplateTags(serviceOffering, 
template, strictHostTags);
+    }
+
+    protected void validateStrictHostTagCheck(VMInstanceVO vm, HostVO host) {
+        ServiceOffering serviceOffering = 
serviceOfferingDao.findByIdIncludingRemoved(vm.getServiceOfferingId());
+        VirtualMachineTemplate template = 
_templateDao.findByIdIncludingRemoved(vm.getTemplateId());
+
+        if (!checkEnforceStrictHostTagCheck(host, serviceOffering, template)) {
+            Set<String> missingTags = 
host.getHostServiceOfferingAndTemplateMissingTags(serviceOffering, template, 
UserVmManager.getStrictHostTags());
+            logger.error("Cannot deploy VM: {} to host : {} due to tag 
mismatch. host tags: {}, " +
+                            "strict host tags: {} serviceOffering tags: {}, 
template tags: {}, missing tags: {}",
+                    vm, host, host.getHostTags(), 
UserVmManager.getStrictHostTags(), serviceOffering.getHostTag(), 
template.getTemplateTag(), missingTags);
+            throw new InvalidParameterValueException(String.format("Cannot 
deploy VM, destination host: %s " +
+                    "is not compatible for the VM", host.getName()));
+        }
+    }
+
     private DeployDestination checkVmMigrationDestination(VMInstanceVO vm, 
Host srcHost, Host destinationHost) throws VirtualMachineMigrationException {
         if (destinationHost == null) {
             return null;
@@ -6698,6 +6731,10 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
             throw new CloudRuntimeException("Cannot migrate VM, VM is DPDK 
enabled VM but destination host is not DPDK enabled");
         }
 
+        HostVO destinationHostVO = _hostDao.findById(destinationHost.getId());
+        _hostDao.loadHostTags(destinationHostVO);
+        validateStrictHostTagCheck(vm, destinationHostVO);
+
         checkHostsDedication(vm, srcHost.getId(), destinationHost.getId());
 
         // call to core process
@@ -6707,7 +6744,6 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
         DeployDestination dest = new DeployDestination(dcVO, pod, cluster, 
destinationHost);
 
         // check max guest vm limit for the destinationHost
-        HostVO destinationHostVO = _hostDao.findById(destinationHost.getId());
         if (_capacityMgr.checkIfHostReachMaxGuestLimit(destinationHostVO)) {
             if (logger.isDebugEnabled()) {
                 logger.debug("Host name: " + destinationHost.getName() + ", 
hostId: " + destinationHost.getId()
@@ -8389,7 +8425,8 @@ public class UserVmManagerImpl extends ManagerBase 
implements UserVmManager, Vir
     public ConfigKey<?>[] getConfigKeys() {
         return new ConfigKey<?>[] {EnableDynamicallyScaleVm, 
AllowDiskOfferingChangeDuringScaleVm, AllowUserExpungeRecoverVm, 
VmIpFetchWaitInterval, VmIpFetchTrialMax,
                 VmIpFetchThreadPoolMax, VmIpFetchTaskWorkers, 
AllowDeployVmIfGivenHostFails, EnableAdditionalVmConfig, DisplayVMOVFProperties,
-                KvmAdditionalConfigAllowList, 
XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, 
DestroyRootVolumeOnVmDestruction};
+                KvmAdditionalConfigAllowList, 
XenServerAdditionalConfigAllowList, VmwareAdditionalConfigAllowList, 
DestroyRootVolumeOnVmDestruction,
+                EnforceStrictResourceLimitHostTagCheck, StrictHostTags};
     }
 
     @Override
diff --git 
a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java 
b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
index 73144ac2966..b1c653d3f92 100644
--- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
@@ -447,7 +447,7 @@ public class UnmanagedVMsManagerImpl implements 
UnmanagedVMsManager {
             return true;
         }
         hostDao.loadHostTags(host);
-        return host.checkHostServiceOfferingAndTemplateTags(serviceOffering, 
template);
+        return host.checkHostServiceOfferingAndTemplateTags(serviceOffering, 
template, UserVmManager.getStrictHostTags());
     }
 
     private boolean storagePoolSupportsDiskOffering(StoragePool pool, 
DiskOffering diskOffering) {
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java 
b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index d117c460034..39128f21c87 100644
--- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
+++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
@@ -1524,6 +1524,46 @@ public class UserVmManagerImplTest {
     }
 
     @Test
+    public void testValidateStrictHostTagCheckPass() {
+        ServiceOfferingVO serviceOffering = 
Mockito.mock(ServiceOfferingVO.class);
+        VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
+
+        VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
+        HostVO destinationHostVO = Mockito.mock(HostVO.class);
+
+        
Mockito.when(_serviceOfferingDao.findByIdIncludingRemoved(1L)).thenReturn(serviceOffering);
+        
Mockito.when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template);
+
+        Mockito.when(vm.getServiceOfferingId()).thenReturn(1L);
+        Mockito.when(vm.getTemplateId()).thenReturn(2L);
+
+        
Mockito.when(destinationHostVO.checkHostServiceOfferingAndTemplateTags(Mockito.any(ServiceOffering.class),
 Mockito.any(VirtualMachineTemplate.class), Mockito.anySet())).thenReturn(true);
+
+        userVmManagerImpl.validateStrictHostTagCheck(vm, destinationHostVO);
+
+        Mockito.verify(
+                destinationHostVO, Mockito.times(1)
+        
).checkHostServiceOfferingAndTemplateTags(Mockito.any(ServiceOffering.class), 
Mockito.any(VirtualMachineTemplate.class), Mockito.anySet());
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void testValidateStrictHostTagCheckFail() {
+        ServiceOfferingVO serviceOffering = 
Mockito.mock(ServiceOfferingVO.class);
+        VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
+
+        VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
+        HostVO destinationHostVO = Mockito.mock(HostVO.class);
+
+        
Mockito.when(_serviceOfferingDao.findByIdIncludingRemoved(1L)).thenReturn(serviceOffering);
+        
Mockito.when(templateDao.findByIdIncludingRemoved(2L)).thenReturn(template);
+
+        Mockito.when(vm.getServiceOfferingId()).thenReturn(1L);
+        Mockito.when(vm.getTemplateId()).thenReturn(2L);
+
+        
Mockito.when(destinationHostVO.checkHostServiceOfferingAndTemplateTags(Mockito.any(ServiceOffering.class),
 Mockito.any(VirtualMachineTemplate.class), 
Mockito.anySet())).thenReturn(false);
+        userVmManagerImpl.validateStrictHostTagCheck(vm, destinationHostVO);
+    }
+
     public void testGetRootVolumeSizeForVmRestore() {
         VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
         Mockito.when(template.getSize()).thenReturn(10L * GiB_TO_BYTES);
diff --git a/test/integration/smoke/test_vm_strict_host_tags.py 
b/test/integration/smoke/test_vm_strict_host_tags.py
new file mode 100644
index 00000000000..163869c8fae
--- /dev/null
+++ b/test/integration/smoke/test_vm_strict_host_tags.py
@@ -0,0 +1,552 @@
+# 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.
+
+# Import Local Modules
+from marvin.cloudstackAPI import (expungeVirtualMachine, updateConfiguration)
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.lib.base import (Account, ServiceOffering, Template, Host, 
VirtualMachine)
+from marvin.lib.common import (get_domain, get_zone)
+from nose.plugins.attrib import attr
+
+
+class TestVMDeploymentPlannerStrictTags(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+
+        testClient = super(TestVMDeploymentPlannerStrictTags, 
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_h1 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h1"])
+        cls.service_offering_h2 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h2"])
+
+        cls.template_t1 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t1")
+        cls.template_t1.download(cls.apiclient)
+
+        cls.template_t2 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t2")
+        cls.template_t2.download(cls.apiclient)
+
+        hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, type='Routing')
+        cls.host_h1 = hosts[0] if len(hosts) >= 1 else None
+
+        cls._cleanup = [cls.account, cls.service_offering_h1, 
cls.service_offering_h2, cls.template_t1, cls.template_t2]
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.host_h1:
+            Host.update(cls.apiclient, id=cls.host_h1.id, hosttags="")
+        cls.updateConfiguration("vm.strict.host.tags", "")
+        cls.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        cls.updateConfiguration("resource.limit.host.tags", "")
+        super(TestVMDeploymentPlannerStrictTags, cls).tearDownClass()
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        if self.host_h1:
+            Host.update(self.apiclient, id=self.host_h1.id, 
hosttags="h1,t1,v1")
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+        self.cleanup = []
+
+    def tearDown(self):
+        self.cleanup_vm_for_template(self.template_t1.id)
+        self.cleanup_vm_for_template(self.template_t2.id)
+        super(TestVMDeploymentPlannerStrictTags, self).tearDown()
+
+    def cleanup_vm_for_template(self, templateid):
+        vm_list = VirtualMachine.list(self.apiclient, listall=True, 
templateid=templateid)
+        if type(vm_list) is list:
+            for vm in vm_list:
+                self.expunge_vm(vm)
+
+    def expunge_vm(self, vm):
+        try:
+            cmd = expungeVirtualMachine.expungeVirtualMachineCmd()
+            cmd.id = vm.id
+            self.apiclient.expungeVirtualMachine(cmd)
+        except Exception as e:
+            self.debug("Failed to expunge VM: %s" % e)
+
+    @classmethod
+    def updateConfiguration(self, name, value):
+        cmd = updateConfiguration.updateConfigurationCmd()
+        cmd.name = name
+        cmd.value = value
+        self.apiclient.updateConfiguration(cmd)
+
+    def deploy_vm(self, destination_id, template_id, service_offering_id):
+        return VirtualMachine.create(self.apiclient, 
self.services["virtual_machine"], zoneid=self.zone.id,
+                                     templateid=template_id, 
serviceofferingid=service_offering_id,
+                                     hostid=destination_id)
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_01_deploy_vm_on_specific_host_without_strict_tags(self):
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_02_deploy_vm_on_any_host_without_strict_tags(self):
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+
+        vm = self.deploy_vm(None, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertIsNotNone(vm, "VM instance was not deployed")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_03_deploy_vm_on_specific_host_with_strict_tags_success(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"false")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_04_deploy_vm_on_any_host_with_strict_tags_success(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"false")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(None, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertIsNotNone(vm, "VM instance was not deployed")
+
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+
+        vm = self.deploy_vm(None, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_05_deploy_vm_on_specific_host_with_strict_tags_failure(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        try:
+            vm = self.deploy_vm(self.host_h1.id, self.template_t2.id, 
self.service_offering_h1.id)
+            self.cleanup.append(vm)
+            self.fail("VM should not be deployed")
+        except Exception as e:
+            self.assertTrue("Cannot deploy VM, destination host" in str(e))
+
+        try:
+            vm = self.deploy_vm(self.host_h1.id, self.template_t2.id, 
self.service_offering_h2.id)
+            self.cleanup.append(vm)
+            self.fail("VM should not be deployed")
+        except Exception as e:
+            self.assertTrue("Cannot deploy VM, destination host" in str(e))
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_06_deploy_vm_on_any_host_with_strict_tags_failure(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        try:
+            vm = self.deploy_vm(None, self.template_t2.id, 
self.service_offering_h1.id)
+            self.cleanup.append(vm)
+            self.fail("VM should not be deployed")
+        except Exception as e:
+            self.assertTrue("No suitable host found for vm " in str(e))
+
+
+class TestScaleVMStrictTags(cloudstackTestCase):
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestScaleVMStrictTags, 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_h1 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h1"])
+        cls.service_offering_h2 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h2"])
+
+        cls.template_t1 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t1")
+        cls.template_t1.download(cls.apiclient)
+
+        cls.template_t2 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t2")
+        cls.template_t2.download(cls.apiclient)
+
+        hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, type='Routing')
+        cls.host_h1 = hosts[0] if len(hosts) >= 1 else None
+
+        cls._cleanup = [cls.account, cls.service_offering_h1, 
cls.service_offering_h2, cls.template_t1, cls.template_t2]
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.host_h1:
+            Host.update(cls.apiclient, id=cls.host_h1.id, hosttags="")
+        cls.updateConfiguration("vm.strict.host.tags", "")
+        cls.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        cls.updateConfiguration("resource.limit.host.tags", "")
+        super(TestScaleVMStrictTags, cls).tearDownClass()
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        if self.host_h1:
+            Host.update(self.apiclient, id=self.host_h1.id, 
hosttags="h1,t1,v1,h2,t2,v2")
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+        self.cleanup = []
+
+    def tearDown(self):
+        self.cleanup_vm_for_template(self.template_t1.id)
+        self.cleanup_vm_for_template(self.template_t2.id)
+        super(TestScaleVMStrictTags, self).tearDown()
+
+    def cleanup_vm_for_template(self, templateid):
+        vm_list = VirtualMachine.list(self.apiclient, listall=True, 
templateid=templateid)
+        if type(vm_list) is list:
+            for vm in vm_list:
+                self.expunge_vm(vm)
+
+    def expunge_vm(self, vm):
+        try:
+            cmd = expungeVirtualMachine.expungeVirtualMachineCmd()
+            cmd.id = vm.id
+            self.apiclient.expungeVirtualMachine(cmd)
+        except Exception as e:
+            self.debug("Failed to expunge VM: %s" % e)
+
+    @classmethod
+    def updateConfiguration(self, name, value):
+        cmd = updateConfiguration.updateConfigurationCmd()
+        cmd.name = name
+        cmd.value = value
+        self.apiclient.updateConfiguration(cmd)
+
+    def deploy_vm(self, destination_id, template_id, service_offering_id):
+        return VirtualMachine.create(self.apiclient, 
self.services["virtual_machine"], zoneid=self.zone.id,
+                                     templateid=template_id, 
serviceofferingid=service_offering_id,
+                                     hostid=destination_id)
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_01_scale_vm_strict_tags_success(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+        vm.stop(self.apiclient)
+        vm.scale(self.apiclient, serviceOfferingId=self.service_offering_h2.id)
+        vm.start(self.apiclient)
+        scaled_vm = VirtualMachine.list(self.apiclient, id=vm.id, 
listall=True)[0]
+        self.assertEqual(scaled_vm.serviceofferingid, 
self.service_offering_h2.id, "VM was not scaled")
+        self.assertEqual(self.host_h1.id, scaled_vm.hostid, "VM was not 
scaled")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_02_scale_vm_strict_tags_failure(self):
+        if self.host_h1:
+            Host.update(self.apiclient, id=self.host_h1.id, 
hosttags="h1,t1,v1")
+
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+        try:
+            vm.stop(self.apiclient)
+            vm.scale(self.apiclient, 
serviceOfferingId=self.service_offering_h2.id)
+            vm.start(self.apiclient)
+            self.fail("VM should not be be able scale and start")
+        except Exception as e:
+            self.assertTrue("No suitable host found for vm " in str(e))
+
+
+class TestRestoreVMStrictTags(cloudstackTestCase):
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestRestoreVMStrictTags, 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_h1 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h1"])
+        cls.service_offering_h2 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h2"])
+
+        cls.template_t1 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t1")
+        cls.template_t1.download(cls.apiclient)
+
+        cls.template_t2 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t2")
+        cls.template_t2.download(cls.apiclient)
+
+        hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, type='Routing')
+        cls.host_h1 = hosts[0] if len(hosts) >= 1 else None
+
+        cls._cleanup = [cls.account, cls.service_offering_h1, 
cls.service_offering_h2, cls.template_t1, cls.template_t2]
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.host_h1:
+            Host.update(cls.apiclient, id=cls.host_h1.id, hosttags="")
+        cls.updateConfiguration("vm.strict.host.tags", "")
+        cls.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        cls.updateConfiguration("resource.limit.host.tags", "")
+        super(TestRestoreVMStrictTags, cls).tearDownClass()
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        if self.host_h1:
+            Host.update(self.apiclient, id=self.host_h1.id, 
hosttags="h1,t1,v1")
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+        self.cleanup = []
+
+    def tearDown(self):
+        self.cleanup_vm_for_template(self.template_t1.id)
+        self.cleanup_vm_for_template(self.template_t2.id)
+        super(TestRestoreVMStrictTags, self).tearDown()
+
+    def cleanup_vm_for_template(self, templateid):
+        vm_list = VirtualMachine.list(self.apiclient, listall=True, 
templateid=templateid)
+        if type(vm_list) is list:
+            for vm in vm_list:
+                self.expunge_vm(vm)
+
+    def expunge_vm(self, vm):
+        try:
+            cmd = expungeVirtualMachine.expungeVirtualMachineCmd()
+            cmd.id = vm.id
+            self.apiclient.expungeVirtualMachine(cmd)
+        except Exception as e:
+            self.debug("Failed to expunge VM: %s" % e)
+
+    @classmethod
+    def updateConfiguration(self, name, value):
+        cmd = updateConfiguration.updateConfigurationCmd()
+        cmd.name = name
+        cmd.value = value
+        self.apiclient.updateConfiguration(cmd)
+
+    def deploy_vm(self, destination_id, template_id, service_offering_id):
+        return VirtualMachine.create(self.apiclient, 
self.services["virtual_machine"], zoneid=self.zone.id,
+                                     templateid=template_id, 
serviceofferingid=service_offering_id,
+                                     hostid=destination_id)
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_01_restore_vm_strict_tags_success(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+
+        vm.restore(self.apiclient, templateid=self.template_t2.id, 
expunge=True)
+        restored_vm = VirtualMachine.list(self.apiclient, id=vm.id, 
listall=True)[0]
+        self.assertEqual(restored_vm.templateid, self.template_t2.id, "VM was 
not restored")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_02_restore_vm_strict_tags_failure(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+        try:
+            vm.restore(self.apiclient, templateid=self.template_t2.id, 
expunge=True)
+            self.fail("VM should not be restored")
+        except Exception as e:
+            self.assertTrue("No suitable host found for vm " in str(e))
+
+
+class TestMigrateVMStrictTags(cloudstackTestCase):
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestMigrateVMStrictTags, 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
+
+        hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, type='Routing')
+        cls.host_h1 = hosts[0] if len(hosts) >= 1 else None
+        cls.host_h2 = None
+        if len(hosts) >= 2:
+            for host in hosts[1:]:
+                if host.clusterid == cls.host_h1.clusterid:
+                    cls.host_h2 = host
+                    break
+
+        if not cls.host_h2:
+            cls.skipTest("There are not enough hosts to run this test")
+
+        # Create an account, network, VM and IP addresses
+        cls.account = Account.create(cls.apiclient, cls.services["account"], 
domainid=cls.domain.id)
+        cls.service_offering_h1 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h1"])
+        cls.service_offering_h2 = ServiceOffering.create(cls.apiclient, 
cls.services["service_offering_h2"])
+
+        cls.template_t1 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t1")
+        cls.template_t1.download(cls.apiclient)
+
+        cls.template_t2 = Template.register(cls.apiclient, 
cls.services["test_templates"][cls.hypervisor.lower()],
+                                            zoneid=cls.zone.id, 
hypervisor=cls.hypervisor.lower(), templatetag="t2")
+        cls.template_t2.download(cls.apiclient)
+
+        cls._cleanup = [cls.account, cls.service_offering_h1, 
cls.service_offering_h2, cls.template_t1, cls.template_t2]
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls.host_h1:
+            Host.update(cls.apiclient, id=cls.host_h1.id, hosttags="")
+        if cls.host_h2:
+            Host.update(cls.apiclient, id=cls.host_h2.id, hosttags="")
+        cls.updateConfiguration("vm.strict.host.tags", "")
+        cls.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        cls.updateConfiguration("resource.limit.host.tags", "")
+        super(TestMigrateVMStrictTags, cls).tearDownClass()
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        if self.host_h1:
+            Host.update(self.apiclient, id=self.host_h1.id, 
hosttags="h1,t1,v1")
+        self.updateConfiguration("vm.strict.host.tags", "")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "")
+        self.cleanup = []
+
+    def tearDown(self):
+        self.cleanup_vm_for_template(self.template_t1.id)
+        self.cleanup_vm_for_template(self.template_t2.id)
+        super(TestMigrateVMStrictTags, self).tearDown()
+
+    def cleanup_vm_for_template(self, templateid):
+        vm_list = VirtualMachine.list(self.apiclient, listall=True, 
templateid=templateid)
+        if type(vm_list) is list:
+            for vm in vm_list:
+                self.expunge_vm(vm)
+
+    def expunge_vm(self, vm):
+        try:
+            cmd = expungeVirtualMachine.expungeVirtualMachineCmd()
+            cmd.id = vm.id
+            self.apiclient.expungeVirtualMachine(cmd)
+        except Exception as e:
+            self.debug("Failed to expunge VM: %s" % e)
+
+    @classmethod
+    def updateConfiguration(self, name, value):
+        cmd = updateConfiguration.updateConfigurationCmd()
+        cmd.name = name
+        cmd.value = value
+        self.apiclient.updateConfiguration(cmd)
+
+    def deploy_vm(self, destination_id, template_id, service_offering_id):
+        return VirtualMachine.create(self.apiclient, 
self.services["virtual_machine"], zoneid=self.zone.id,
+                                     templateid=template_id, 
serviceofferingid=service_offering_id,
+                                     hostid=destination_id)
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_01_migrate_vm_strict_tags_success(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+        Host.update(self.apiclient, id=self.host_h2.id, hosttags="h1,t1,v1")
+        vm.migrate(self.apiclient, self.host_h2.id)
+        migrated_vm = VirtualMachine.list(self.apiclient, id=vm.id, 
listall=True)[0]
+        self.assertEqual(migrated_vm.hostid, self.host_h2.id, "VM was not 
migratd")
+
+    @attr(tags=["advanced", "advancedns", "ssh", "smoke"], 
required_hardware="false")
+    def test_02_migrate_vm_strict_tags_failure(self):
+        self.updateConfiguration("vm.strict.host.tags", "v1,v2")
+        self.updateConfiguration("vm.strict.resource.limit.host.tag.check", 
"true")
+        self.updateConfiguration("resource.limit.host.tags", "h1,h2,t1,t2")
+
+        vm = self.deploy_vm(self.host_h1.id, self.template_t1.id, 
self.service_offering_h1.id)
+        self.cleanup.append(vm)
+
+        self.assertEqual(self.host_h1.id, vm.hostid, "VM instance was not 
deployed on target host ID")
+        Host.update(self.apiclient, id=self.host_h2.id, hosttags="h2,t2,v2")
+        try:
+            vm.migrate(self.apiclient, self.host_h2.id)
+            VirtualMachine.list(self.apiclient, id=vm.id, listall=True)[0]
+            self.fail("VM should not be migrated")
+        except Exception as e:
+            self.assertTrue("Cannot deploy VM, destination host:" in str(e))
diff --git a/tools/marvin/marvin/config/test_data.py 
b/tools/marvin/marvin/config/test_data.py
index ef9bfd774f7..e96dba1c4d5 100644
--- a/tools/marvin/marvin/config/test_data.py
+++ b/tools/marvin/marvin/config/test_data.py
@@ -177,9 +177,9 @@ test_data = {
     "service_offering_h2": {
         "name": "Tagged h2 Small Instance",
         "displaytext": "Tagged h2 Small Instance",
-        "cpunumber": 1,
-        "cpuspeed": 100,
-        "memory": 256,
+        "cpunumber": 2,
+        "cpuspeed": 200,
+        "memory": 512,
         "hosttags": "h2"
     },
     "disk_offering": {
@@ -1034,7 +1034,18 @@ test_data = {
             "requireshvm": "True",
             "ispublic": "True",
             "deployasis": "True"
-        }
+        },
+        "simulator": {
+            "name": "tiny-simulator",
+            "displaytext": "tiny simulator",
+            "format": "vhd",
+            "hypervisor": "simulator",
+            "ostype": "Other Linux (64-bit)",
+            "url": 
"http://dl.openvm.eu/cloudstack/macchinina/x86_64/macchinina.vhd.bz2";,
+            "requireshvm": "True",
+            "ispublic": "True",
+            "isextractable": "True"
+        },
     },
     "test_ovf_templates": [
         {
diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py
index 6acf6a8ad63..9c69d33a4ca 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -782,6 +782,8 @@ class VirtualMachine:
         cmd.virtualmachineid = self.id
         if templateid:
             cmd.templateid = templateid
+        if expunge:
+            cmd.expunge = expunge
         if diskofferingid:
             cmd.diskofferingid = diskofferingid
         if rootdisksize:
@@ -794,7 +796,6 @@ class VirtualMachine:
                     'key': key,
                     'value': value
                 })
-
         return apiclient.restoreVirtualMachine(cmd)
 
     def get_ssh_client(

Reply via email to