# 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. """ Tests DRS on a cluster """ import logging import time from marvin.cloudstackTestCase import cloudstackTestCase from marvin.lib.base import (Cluster, Configurations, Host, Network, NetworkOffering, ServiceOffering, VirtualMachine, Zone) from marvin.lib.common import (get_domain, get_zone, get_template) from marvin.lib.utils import wait_until from marvin import jsonHelper from nose.plugins.attrib import attr class TestClusterDRS(cloudstackTestCase): @classmethod def setUpClass(cls): cls.testClient = super(TestClusterDRS, cls).getClsTestClient() cls.apiclient = cls.testClient.getApiClient() cls.services = cls.testClient.getParsedTestDataConfig() zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) cls.zone = Zone(zone.__dict__) cls.template = get_template(cls.apiclient, cls.zone.id) cls._cleanup = [] cls.logger = logging.getLogger("TestClusterDRS") cls.stream_handler = logging.StreamHandler() cls.logger.setLevel(logging.DEBUG) cls.logger.addHandler(cls.stream_handler) cls.skipTests = False clusters = Cluster.list(cls.apiclient, zoneid=cls.zone.id, allocationstate='Enabled') if not clusters or not isinstance(clusters, list) or len(clusters) < 1: cls.logger.debug("This test requires at least 1 (Up and Enabled) cluster in the zone") cls.skipTests = True return for cluster in clusters: cls.hosts = Host.list(cls.apiclient, zoneid=cls.zone.id, clusterid=cluster.id, state='Up', resourcestate='Enabled') if not cls.hosts or not isinstance(cls.hosts, list) or len(cls.hosts) < 2: cls.logger.debug("This test requires at least two (Up and Enabled) hosts in the zone") cls.skipTests = True return else: cls.cluster = Cluster(jsonHelper.jsonDump.dump(cluster)) break cls.domain = get_domain(cls.apiclient) # 1. Create large service offering cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offerings"]["large"]) cls._cleanup.append(cls.service_offering) # 2. Create a network cls.services["network"]["name"] = "Test Network" cls.network_offering = NetworkOffering.create( cls.apiclient, cls.services["l2-network_offering"] ) cls._cleanup.append(cls.network_offering) NetworkOffering.update( cls.network_offering, cls.apiclient, id=cls.network_offering.id, state="enabled" ) cls.network = Network.create( cls.apiclient, cls.services["l2-network"], networkofferingid=cls.network_offering.id, zoneid=cls.zone.id, accountid="admin", domainid=cls.domain.id, ) cls._cleanup.append(cls.network) @classmethod def tearDownClass(cls): super(TestClusterDRS, cls).tearDownClass() def setUp(self): if self.skipTests: self.skipTest("This test requires at least two (Up and Enabled) hosts in the zone") self.apiclient = self.testClient.getApiClient() self.cleanup = [] def tearDown(self): super(TestClusterDRS, self).tearDown() @classmethod def get_vm_host_id(cls, vm_id): list_vms = VirtualMachine.list(cls.apiclient, id=vm_id) vm = list_vms[0] return vm.hostid def wait_for_vm_start(self, vm): """ Wait until vm is Running """ def check_vm_state(): vms = VirtualMachine.list( self.apiclient, id=vm.id, listall=True ) if isinstance(vms, list): if vms[0].state == 'Running': return True, vms[0].state return False, vms[0].state res = wait_until(10, 30, check_vm_state) if not res: raise Exception("Failed to wait for VM %s (%s) to be Running" % (vm.name, vm.id)) return res def wait_for_plan_completion(self, plan): """ Wait until plan is completed """ def check_plan_status(): plans = self.cluster.listDrsPlans(self.apiclient, id=plan.id) if isinstance(plans, list): if plans[0].status == 'COMPLETED': return True, plans[0].status return False, plans[0].status res = wait_until(10, 30, check_plan_status) if not res: raise Exception("Failed to wait for completion of plan %s" % (plan.id)) return res def get_migrations(self): """ Wait until migrations are generated. Sometimes it takes a little bit of time for stats to get updated. We generate migrations until we get at least one migration """ def generate_migrations(): drs_plan = self.cluster.generateDrsPlan(self.apiclient, migrations=4) if len(drs_plan["migrations"]) > 0: return True, drs_plan["migrations"] return False, drs_plan["migrations"] res, migrations = wait_until(10, 30, generate_migrations) if not res: raise Exception("Failed to generate drs migrations") return migrations @attr(tags=["advanced"], required_hardware="false") def test_01_condensed_drs_algorithm(self): """ Verify DRS algorithm - condensed""" # 1. Deploy vm-1 on host 1 # 2. Deploy vm-2 on host 2 # 3. Execute DRS to move all VMs on the same host self.logger.debug("=== Running test_01_condensed_drs_algorithm ===") # 1. Deploy vm-1 on host 1 self.services["virtual_machine"]["name"] = "virtual-machine-1" self.services["virtual_machine"]["displayname"] = "virtual-machine-1" self.virtual_machine_1 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], serviceofferingid=self.service_offering.id, templateid=self.template.id, zoneid=self.zone.id, networkids=self.network.id, hostid=self.hosts[0].id) self.cleanup.append(self.virtual_machine_1) vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) # 2. Deploy vm-2 on host 2 self.services["virtual_machine"]["name"] = "virtual-machine-2" self.services["virtual_machine"]["displayname"] = "virtual-machine-2" self.virtual_machine_2 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], serviceofferingid=self.service_offering.id, templateid=self.template.id, zoneid=self.zone.id, networkids=self.network.id, hostid=self.hosts[1].id) vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) self.cleanup.append(self.virtual_machine_2) self.assertNotEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on different hosts") self.wait_for_vm_start(self.virtual_machine_1) self.wait_for_vm_start(self.virtual_machine_2) # 3. Generate & execute DRS to move all VMs on the same host Configurations.update(self.apiclient, "drs.algorithm", "condensed", clusterid=self.cluster.id) Configurations.update(self.apiclient, "drs.imbalance", "1.0", clusterid=self.cluster.id) migrations = self.get_migrations() vm_to_dest_host_map = { migration["virtualmachineid"]: migration["destinationhostid"] for migration in migrations } self.assertEqual(len(vm_to_dest_host_map), 1, msg="DRS plan should have 1 migrations") executed_plan = self.cluster.executeDrsPlan(self.apiclient, vm_to_dest_host_map) self.wait_for_plan_completion(executed_plan) vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) self.assertEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on the same host") @attr(tags=["advanced"], required_hardware="false") def test_02_balanced_drs_algorithm(self): """ Verify DRS algorithm - balanced""" # 1. Deploy vm-1 on host 1 # 2. Deploy vm-2 on host 2 # 3. Execute DRS to move all VMs on different hosts self.logger.debug("=== Running test_02_balanced_drs_algorithm ===") # 1. Deploy vm-1 on host 1 self.services["virtual_machine"]["name"] = "virtual-machine-1" self.services["virtual_machine"]["displayname"] = "virtual-machine-1" self.virtual_machine_1 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], serviceofferingid=self.service_offering.id, templateid=self.template.id, zoneid=self.zone.id, networkids=self.network.id, hostid=self.hosts[0].id) self.cleanup.append(self.virtual_machine_1) vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) # 2. Deploy vm-2 on host 1 self.services["virtual_machine"]["name"] = "virtual-machine-2" self.services["virtual_machine"]["displayname"] = "virtual-machine-2" self.virtual_machine_2 = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], serviceofferingid=self.service_offering.id, templateid=self.template.id, zoneid=self.zone.id, networkids=self.network.id, hostid=self.hosts[0].id) vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) self.cleanup.append(self.virtual_machine_2) self.assertEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on same hosts") self.wait_for_vm_start(self.virtual_machine_1) self.wait_for_vm_start(self.virtual_machine_2) # 3. Execute DRS to move all VMs on different hosts Configurations.update(self.apiclient, "drs.algorithm", "balanced", clusterid=self.cluster.id) Configurations.update(self.apiclient, "drs.imbalance", "1.0", clusterid=self.cluster.id) migrations = self.get_migrations() vm_to_dest_host_map = { migration["virtualmachineid"]: migration["destinationhostid"] for migration in migrations } self.assertEqual(len(vm_to_dest_host_map), 1, msg="DRS plan should have 1 migrations") executed_plan = self.cluster.executeDrsPlan(self.apiclient, vm_to_dest_host_map) self.wait_for_plan_completion(executed_plan) vm_1_host_id = self.get_vm_host_id(self.virtual_machine_1.id) vm_2_host_id = self.get_vm_host_id(self.virtual_machine_2.id) self.assertNotEqual(vm_1_host_id, vm_2_host_id, msg="Both VMs should be on different hosts")