# # Copyright (c) 2019 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # import re import random from pytest import fixture, mark, skip, param import keywords.host_helper from utils.tis_log import LOG from consts.stx import FlavorSpec, ImageMetadata, NovaCLIOutput from keywords import nova_helper, vm_helper, system_helper, cinder_helper, \ host_helper, glance_helper MEMPAGE_HEADERS = ('app_total_4K', 'app_hp_avail_2M', 'app_hp_avail_1G') def skip_4k_for_ovs(mempage_size): if mempage_size in (None, 'any', 'small') and not system_helper.is_avs(): skip("4K VM is unsupported by OVS by default") @fixture(scope='module') def prepare_resource(add_admin_role_module): hypervisor = random.choice(host_helper.get_up_hypervisors()) flavor = nova_helper.create_flavor(name='flavor-1g', ram=1024, cleanup='module')[1] vol_id = cinder_helper.create_volume('vol-mem_page_size', cleanup='module')[1] return hypervisor, flavor, vol_id def _get_expt_indices(mempage_size): if mempage_size in ('small', None): expt_mempage_indices = (0,) elif str(mempage_size) == '2048': expt_mempage_indices = (1,) elif str(mempage_size) == '1048576': expt_mempage_indices = (2,) elif mempage_size == 'large': expt_mempage_indices = (1, 2) else: expt_mempage_indices = (0, 1, 2) return expt_mempage_indices def is_host_mem_sufficient(host, mempage_size=None, mem_gib=1): host_mems_per_proc = host_helper.get_host_memories(host, headers=MEMPAGE_HEADERS) mempage_size = 'small' if not mempage_size else mempage_size expt_mempage_indices = _get_expt_indices(mempage_size) for proc, mems_for_proc in host_mems_per_proc.items(): pages_4k, pages_2m, pages_1g = mems_for_proc mems_for_proc = (int(pages_4k * 4 / 1048576), int(pages_2m * 2 / 1024), int(pages_1g)) for index in expt_mempage_indices: avail_g_for_memsize = mems_for_proc[index] if avail_g_for_memsize >= mem_gib: LOG.info("{} has sufficient {} mempages to launch {}G " "vm".format(host, mempage_size, mem_gib)) return True, host_mems_per_proc LOG.info("{} does not have sufficient {} mempages to launch {}G " "vm".format(host, mempage_size, mem_gib)) return False, host_mems_per_proc def check_mempage_change(vm, host, prev_host_mems, mempage_size=None, mem_gib=1, numa_node=None): expt_mempage_indics = _get_expt_indices(mempage_size) if numa_node is None: numa_node = vm_helper.get_vm_numa_nodes_via_ps(vm_id=vm, host=host)[0] prev_host_mems = prev_host_mems[numa_node] current_host_mems = host_helper.get_host_memories( host, headers=MEMPAGE_HEADERS)[numa_node] if 0 in expt_mempage_indics: if current_host_mems[1:] == prev_host_mems[1:] and \ abs(prev_host_mems[0] - current_host_mems[ 0]) <= mem_gib * 512 * 1024 / 4: return for i in expt_mempage_indics: if i == 0: continue expt_pagecount = 1 if i == 2 else 1024 if prev_host_mems[i] - expt_pagecount == current_host_mems[i]: LOG.info("{} {} memory page reduced by {}GiB as " "expected".format(host, MEMPAGE_HEADERS[i], mem_gib)) return LOG.info("{} {} memory pages - Previous: {}, current: " "{}".format(host, MEMPAGE_HEADERS[i], prev_host_mems[i], current_host_mems[i])) assert 0, "{} available vm {} memory page count did not change as " \ "expected".format(host, mempage_size) @mark.parametrize('mem_page_size', [ param('2048', marks=mark.domain_sanity), param('large', marks=mark.p1), param('small', marks=mark.domain_sanity), param('1048576', marks=mark.p3), ]) def test_vm_mem_pool_default_config(prepare_resource, mem_page_size): """ Test memory used by vm is taken from the expected memory pool Args: prepare_resource (tuple): test fixture mem_page_size (str): mem page size setting in flavor Setup: - Create a flavor with 1G RAM (module) - Create a volume with default values (module) - Select a hypervisor to launch vm on Test Steps: - Set memory page size flavor spec to given value - Attempt to boot a vm with above flavor and a basic volume - Verify the system is taking memory from the expected memory pool: - If boot vm succeeded: - Calculate the available/used memory change on the vm host - Verify the memory is taken from memory pool specified via mem_page_size - If boot vm failed: - Verify system attempted to take memory from expected pool, but insufficient memory is available Teardown: - Delete created vm - Delete created volume and flavor (module) """ hypervisor, flavor_1g, volume_ = prepare_resource LOG.tc_step("Set memory page size extra spec in flavor") nova_helper.set_flavor(flavor_1g, **{FlavorSpec.CPU_POLICY: 'dedicated', FlavorSpec.MEM_PAGE_SIZE: mem_page_size}) LOG.tc_step("Check system host-memory-list before launch vm") is_sufficient, prev_host_mems = is_host_mem_sufficient( host=hypervisor, mempage_size=mem_page_size) LOG.tc_step("Boot a vm with mem page size spec - {}".format(mem_page_size)) code, vm_id, msg = vm_helper.boot_vm('mempool_' + mem_page_size, flavor_1g, source='volume', fail_ok=True, vm_host=hypervisor, source_id=volume_, cleanup='function') if not is_sufficient: LOG.tc_step("Check boot vm rejected due to insufficient memory from " "{} pool".format(mem_page_size)) assert 1 == code, "{} vm launched successfully when insufficient " \ "mempage configured on {}". \ format(mem_page_size, hypervisor) else: LOG.tc_step("Check vm launches successfully and {} available mempages " "change accordingly".format(hypervisor)) assert 0 == code, "VM failed to launch with '{}' " \ "mempages".format(mem_page_size) check_mempage_change(vm_id, host=hypervisor, prev_host_mems=prev_host_mems, mempage_size=mem_page_size) def get_hosts_to_configure(candidates): hosts_selected = [None, None] hosts_to_configure = [None, None] max_4k, expt_p1_4k, max_1g, expt_p1_1g = \ 1.5 * 1048576 / 4, 2.5 * 1048576 / 4, 1, 2 for host in candidates: host_mems = host_helper.get_host_memories(host, headers=MEMPAGE_HEADERS) if 1 not in host_mems: LOG.info("{} has only 1 processor".format(host)) continue proc0_mems, proc1_mems = host_mems[0], host_mems[1] p0_4k, p1_4k, p0_1g, p1_1g = \ proc0_mems[0], proc1_mems[0], proc0_mems[2], proc1_mems[2] if p0_4k <= max_4k and p0_1g <= max_1g: if not hosts_selected[1] and p1_4k >= expt_p1_4k and \ p1_1g <= max_1g: hosts_selected[1] = host elif not hosts_selected[0] and p1_4k <= max_4k and \ p1_1g >= expt_p1_1g: hosts_selected[0] = host if None not in hosts_selected: LOG.info("1G and 4k hosts already configured and selected: " "{}".format(hosts_selected)) break else: for i in range(len(hosts_selected)): if hosts_selected[i] is None: hosts_selected[i] = hosts_to_configure[i] = \ list(set(candidates) - set(hosts_selected))[0] LOG.info("Hosts selected: {}; To be configured: " "{}".format(hosts_selected, hosts_to_configure)) return hosts_selected, hosts_to_configure class TestConfigMempage: MEM_CONFIGS = [None, 'any', 'large', 'small', '2048', '1048576'] @fixture(scope='class') def add_1g_and_4k_pages(self, request, config_host_class, skip_for_one_proc, add_stxauto_zone, add_admin_role_module): storage_backing, candidate_hosts = \ keywords.host_helper.get_storage_backing_with_max_hosts() if len(candidate_hosts) < 2: skip("Less than two up hosts have same storage backing") LOG.fixture_step("Check mempage configs for hypervisors and select " "host to use or configure") hosts_selected, hosts_to_configure = get_hosts_to_configure( candidate_hosts) if set(hosts_to_configure) != {None}: def _modify(host): is_1g = True if hosts_selected.index(host) == 0 else False proc1_kwargs = {'gib_1g': 2, 'gib_4k_range': (None, 2)} if \ is_1g else {'gib_1g': 0, 'gib_4k_range': (2, None)} kwargs = {'gib_1g': 0, 'gib_4k_range': (None, 2)}, proc1_kwargs actual_mems = host_helper._get_actual_mems(host=host) LOG.fixture_step("Modify {} proc0 to have 0 of 1G pages and " "<2GiB of 4K pages".format(host)) host_helper.modify_host_memory(host, proc=0, actual_mems=actual_mems, **kwargs[0]) LOG.fixture_step("Modify {} proc1 to have >=2GiB of {} " "pages".format(host, '1G' if is_1g else '4k')) host_helper.modify_host_memory(host, proc=1, actual_mems=actual_mems, **kwargs[1]) for host_to_config in hosts_to_configure: if host_to_config: config_host_class(host=host_to_config, modify_func=_modify) LOG.fixture_step("Check mem pages for {} are modified " "and updated successfully". format(host_to_config)) host_helper.wait_for_memory_update(host=host_to_config) LOG.fixture_step("Check host memories for {} after mem config " "completed".format(hosts_selected)) _, hosts_unconfigured = get_hosts_to_configure(hosts_selected) assert not hosts_unconfigured[0], \ "Failed to configure {}. Expt: proc0:1g<2,4k<2gib;" \ "proc1:1g>=2,4k<2gib".format(hosts_unconfigured[0]) assert not hosts_unconfigured[1], \ "Failed to configure {}. Expt: proc0:1g<2,4k<2gib;" \ "proc1:1g<2,4k>=2gib".format(hosts_unconfigured[1]) LOG.fixture_step('(class) Add hosts to stxauto aggregate: ' '{}'.format(hosts_selected)) nova_helper.add_hosts_to_aggregate(aggregate='stxauto', hosts=hosts_selected) def remove_host_from_zone(): LOG.fixture_step('(class) Remove hosts from stxauto aggregate: ' '{}'.format(hosts_selected)) nova_helper.remove_hosts_from_aggregate(aggregate='stxauto', check_first=False) request.addfinalizer(remove_host_from_zone) return hosts_selected, storage_backing @fixture(scope='class') def flavor_2g(self, add_1g_and_4k_pages): hosts, storage_backing = add_1g_and_4k_pages LOG.fixture_step("Create a 2G memory flavor to be used by mempage " "testcases") flavor = nova_helper.create_flavor(name='flavor-2g', ram=2048, storage_backing=storage_backing, cleanup='class')[1] return flavor, hosts, storage_backing @fixture(scope='class') def image_mempage(self): LOG.fixture_step("(class) Create a glance image for mempage testcases") image_id = glance_helper.create_image(name='mempage', cleanup='class')[1] return image_id @fixture() def check_alarms(self, add_1g_and_4k_pages): hosts, storage_backing = add_1g_and_4k_pages host_helper.get_hypervisor_info(hosts=hosts) for host in hosts: host_helper.get_host_memories(host, wait_for_update=False) @fixture(params=MEM_CONFIGS) def flavor_mem_page_size(self, request, flavor_2g): flavor_id = flavor_2g[0] mem_page_size = request.param skip_4k_for_ovs(mem_page_size) if mem_page_size is None: nova_helper.unset_flavor(flavor_id, FlavorSpec.MEM_PAGE_SIZE) else: nova_helper.set_flavor(flavor_id, **{FlavorSpec.MEM_PAGE_SIZE: mem_page_size}) return mem_page_size @mark.parametrize('image_mem_page_size', MEM_CONFIGS) def test_boot_vm_mem_page_size(self, flavor_2g, flavor_mem_page_size, image_mempage, image_mem_page_size): """ Test boot vm with various memory page size setting in flavor and image. Args: flavor_2g (tuple): flavor id of a flavor with ram set to 2G, hosts configured and storage_backing flavor_mem_page_size (str): memory page size extra spec value to set in flavor image_mempage (str): image id for tis image image_mem_page_size (str): memory page metadata value to set in image Setup: - Create a flavor with 2G RAM (module) - Get image id of tis image (module) Test Steps: - Set/Unset flavor memory page size extra spec with given value ( unset if None is given) - Set/Unset image memory page size metadata with given value ( unset if None if given) - Attempt to boot a vm with above flavor and image - Verify boot result based on the mem page size values in the flavor and image Teardown: - Delete vm if booted - Delete created flavor (module) """ skip_4k_for_ovs(image_mem_page_size) flavor_id, hosts, storage_backing = flavor_2g if image_mem_page_size is None: glance_helper.unset_image(image_mempage, properties=ImageMetadata.MEM_PAGE_SIZE) expt_code = 0 else: glance_helper.set_image(image=image_mempage, properties={ImageMetadata.MEM_PAGE_SIZE: image_mem_page_size}) if flavor_mem_page_size is None: expt_code = 4 elif flavor_mem_page_size.lower() in ['any', 'large']: expt_code = 0 else: expt_code = 0 if flavor_mem_page_size.lower() == \ image_mem_page_size.lower() else 4 LOG.tc_step("Attempt to boot a vm with flavor_mem_page_size: {}, and " "image_mem_page_size: {}. And check return " "code is {}.".format(flavor_mem_page_size, image_mem_page_size, expt_code)) actual_code, vm_id, msg = vm_helper.boot_vm(name='mem_page_size', flavor=flavor_id, source='image', source_id=image_mempage, fail_ok=True, avail_zone='stxauto', cleanup='function') assert expt_code == actual_code, "Expect boot vm to return {}; " \ "Actual result: {} with msg: " \ "{}".format(expt_code, actual_code, msg) if expt_code != 0: assert re.search( NovaCLIOutput.VM_BOOT_REJECT_MEM_PAGE_SIZE_FORBIDDEN, msg) else: assert vm_helper.get_vm_host(vm_id) in hosts, \ "VM is not booted on hosts in stxauto zone" LOG.tc_step("Ensure VM is pingable from NatBox") vm_helper.wait_for_vm_pingable_from_natbox(vm_id) @mark.parametrize('mem_page_size', [ param('1048576', marks=mark.priorities('domain_sanity', 'nightly')), param('large'), param('small', marks=mark.nightly), ]) def test_schedule_vm_mempage_config(self, flavor_2g, mem_page_size): """ Test memory used by vm is taken from the expected memory pool and the vm was scheduled on the correct host/processor Args: flavor_2g (tuple): flavor id of a flavor with ram set to 2G, hosts, storage_backing mem_page_size (str): mem page size setting in flavor Setup: - Create host aggregate - Add two hypervisors to the host aggregate - Host-0 configuration: - Processor-0: - Insufficient 1g pages to boot vm that requires 2g - Insufficient 4k pages to boot vm that requires 2g - Processor-1: - Sufficient 1g pages to boot vm that requires 2g - Insufficient 4k pages to boot vm that requires 2g - Host-1 configuration: - Processor-0: - Insufficient 1g pages to boot vm that requires 2g - Insufficient 4k pages to boot vm that requires 2g - Processor-1: - Insufficient 1g pages to boot vm that requires 2g - Sufficient 4k pages to boot vm that requires 2g - Configure a compute to have 4 1G hugepages (module) - Create a flavor with 2G RAM (module) - Create a volume with default values (module) Test Steps: - Set memory page size flavor spec to given value - Boot a vm with above flavor and a basic volume - Calculate the available/used memory change on the vm host - Verify the memory is taken from 1G hugepage memory pool - Verify the vm was booted on a supporting host Teardown: - Delete created vm - Delete created volume and flavor (module) - Re-Configure the compute to have 0 hugepages (module) - Revert host mem pages back to original """ skip_4k_for_ovs(mem_page_size) flavor_id, hosts_configured, storage_backing = flavor_2g LOG.tc_step("Set memory page size extra spec in flavor") nova_helper.set_flavor(flavor_id, **{FlavorSpec.CPU_POLICY: 'dedicated', FlavorSpec.MEM_PAGE_SIZE: mem_page_size}) host_helper.wait_for_hypervisors_up(hosts_configured) prev_computes_mems = {} for host in hosts_configured: prev_computes_mems[host] = host_helper.get_host_memories( host=host, headers=MEMPAGE_HEADERS) LOG.tc_step( "Boot a vm with mem page size spec - {}".format(mem_page_size)) host_1g, host_4k = hosts_configured code, vm_id, msg = vm_helper.boot_vm('mempool_configured', flavor_id, fail_ok=True, avail_zone='stxauto', cleanup='function') assert 0 == code, "VM is not successfully booted." instance_name, vm_host = vm_helper.get_vm_values( vm_id, fields=[":instance_name", ":host"], strict=False) vm_node = vm_helper.get_vm_numa_nodes_via_ps( vm_id=vm_id, instance_name=instance_name, host=vm_host) if mem_page_size == '1048576': assert host_1g == vm_host, \ "VM is not created on the configured host " \ "{}".format(hosts_configured[0]) assert vm_node == [1], "VM (huge) did not boot on the correct " \ "processor" elif mem_page_size == 'small': assert host_4k == vm_host, "VM is not created on the configured " \ "host {}".format(hosts_configured[1]) assert vm_node == [1], "VM (small) did not boot on the correct " \ "processor" else: assert vm_host in hosts_configured LOG.tc_step("Calculate memory change on vm host - {}".format(vm_host)) check_mempage_change(vm_id, vm_host, prev_host_mems=prev_computes_mems[vm_host], mempage_size=mem_page_size, mem_gib=2, numa_node=vm_node[0]) LOG.tc_step("Ensure vm is pingable from NatBox") vm_helper.wait_for_vm_pingable_from_natbox(vm_id)