mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
* Suqash commits to a single commit and rebase against master Update marvin tests to use white list * * Fix marvin test failure * Add new marvin negative tests cases * Remove hard-coded hypervisor types in marvin tests * Fix build error after rebase and add hugepagesless * Fix readability of python code * Fix failing test * Adding cleanup of vms for negative tests * Bug fixes - change config checks properly and block extraconfig in details * Trim to compare the keys * CR comments * Don't skip extraconfig without exception Co-authored-by: Boris Stoyanov - a.k.a Bobby <bss.stoyanov@gmail.com>
543 lines
22 KiB
Python
543 lines
22 KiB
Python
# 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 Virtual Machine additional configuration
|
|
"""
|
|
# Import System modules
|
|
import urllib
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from lxml import etree
|
|
from marvin.cloudstackAPI import (updateVirtualMachine,
|
|
deployVirtualMachine,
|
|
destroyVirtualMachine,
|
|
stopVirtualMachine,
|
|
startVirtualMachine,
|
|
updateConfiguration,
|
|
listVirtualMachines)
|
|
# Import Local Modules
|
|
from marvin.cloudstackTestCase import cloudstackTestCase
|
|
from marvin.lib.base import (Account,
|
|
ServiceOffering,
|
|
)
|
|
from marvin.lib.common import (get_domain,
|
|
get_zone,
|
|
get_template,
|
|
list_hosts)
|
|
from marvin.lib.utils import *
|
|
from nose.plugins.attrib import attr
|
|
|
|
class TestAddConfigtoDeployVM(cloudstackTestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
testClient = super(TestAddConfigtoDeployVM, 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.hostConfig = cls.config.__dict__["zones"][0].__dict__["pods"][0].__dict__["clusters"][0].__dict__["hosts"][
|
|
0].__dict__
|
|
|
|
# Set Zones and disk offerings
|
|
cls.services["small"]["zoneid"] = cls.zone.id
|
|
|
|
cls.services["iso1"]["zoneid"] = cls.zone.id
|
|
|
|
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
|
|
|
|
# Create an account, network, 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"]["small"]
|
|
)
|
|
|
|
cls.cleanup = [
|
|
cls.account,
|
|
cls.service_offering
|
|
]
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
try:
|
|
cls.apiclient = super(TestAddConfigtoDeployVM, 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 setUp(self):
|
|
self.apiclient = self.testClient.getApiClient()
|
|
self.hypervisor = self.testClient.getHypervisorInfo()
|
|
self.dbclient = self.testClient.getDbConnection()
|
|
|
|
"""
|
|
Set EnableAdditionalData to true
|
|
"""
|
|
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
|
|
updateConfigurationCmd.name = "enable.additional.vm.configuration"
|
|
updateConfigurationCmd.value = "true"
|
|
updateConfigurationCmd.scopename = "account"
|
|
updateConfigurationResponse = self.apiclient.updateConfiguration(updateConfigurationCmd)
|
|
self.debug("updated the parameter %s with value %s" % (
|
|
updateConfigurationResponse.name, updateConfigurationResponse.value))
|
|
|
|
# Ste Global Config value
|
|
def add_global_config(self, name, value):
|
|
self.apiclient = self.testClient.getApiClient()
|
|
self.hypervisor = self.testClient.getHypervisorInfo()
|
|
self.dbclient = self.testClient.getDbConnection()
|
|
|
|
cmd = updateConfiguration.updateConfigurationCmd()
|
|
cmd.name = name
|
|
cmd.value = value
|
|
return self.apiclient.updateConfiguration(cmd)
|
|
|
|
# Compare XML Element objects
|
|
def elements_equal(self, e1, e2):
|
|
if e1.tag != e2.tag:
|
|
return False
|
|
if e1.attrib != e2.attrib:
|
|
return False
|
|
if len(e1) != len(e2):
|
|
return False
|
|
return all(self.elements_equal(c1, c2) for c1, c2 in zip(e1, e2))
|
|
|
|
def destroy_vm(self, vm_id):
|
|
cmd = destroyVirtualMachine.destroyVirtualMachineCmd()
|
|
cmd.expunge = True
|
|
cmd.id = vm_id
|
|
return self.apiclient.destroyVirtualMachine(cmd)
|
|
|
|
def deploy_vm(self, hypervisor, extra_config=None):
|
|
cmd = deployVirtualMachine.deployVirtualMachineCmd()
|
|
if extra_config is not None:
|
|
cmd.extraconfig = extra_config
|
|
|
|
template = get_template(
|
|
self.apiclient,
|
|
self.zone.id,
|
|
hypervisor=hypervisor
|
|
)
|
|
cmd.zoneid = self.zone.id
|
|
cmd.templateid = template.id
|
|
cmd.serviceofferingid = self.service_offering.id
|
|
return self.apiclient.deployVirtualMachine(cmd)
|
|
|
|
def list_vm(self):
|
|
cmd = listVirtualMachines.listVirtualMachinesCmd()
|
|
cmd.hypervisor = self.hypervisor
|
|
return self.apiclient.listVirtualMachines(cmd)[0]
|
|
|
|
def update_vm(self, id, extra_config):
|
|
cmd = updateVirtualMachine.updateVirtualMachineCmd()
|
|
cmd.id = id
|
|
cmd.extraconfig = extra_config
|
|
return self.apiclient.updateVirtualMachine(cmd)
|
|
|
|
def stop_vm(self, id):
|
|
cmd = stopVirtualMachine.stopVirtualMachineCmd()
|
|
cmd.id = id
|
|
return self.apiclient.stopVirtualMachine(cmd)
|
|
|
|
def start_vm(self, id):
|
|
cmd = startVirtualMachine.startVirtualMachineCmd()
|
|
cmd.id = id
|
|
return self.apiclient.startVirtualMachine(cmd)
|
|
|
|
# Parse extraconfig for config with that returned by xe vm-param-get ...
|
|
def get_xen_param_values(self, config):
|
|
equal_sign_index = config.index("=")
|
|
cmd_option = config[:equal_sign_index]
|
|
cmd_value = config[equal_sign_index + 1:]
|
|
return cmd_option, cmd_value
|
|
|
|
# Format vm config such that it equals the one from vmx file
|
|
def prepare_vmware_config(self, config):
|
|
equal_sign_index = config.index("=")
|
|
cmd_option = config[:equal_sign_index]
|
|
cmd_value = config[equal_sign_index + 1:]
|
|
return cmd_option + ' = ' '"{}"'.format(cmd_value)
|
|
|
|
# Get vm uuid from xenserver host
|
|
def get_vm_uuid(self, instance_name, ssh_client):
|
|
cmd = 'xe vm-list name-label={} params=uuid '.format(instance_name)
|
|
result = ssh_client.execute(cmd)
|
|
uuid_str = result[0]
|
|
i = uuid_str.index(":")
|
|
return uuid_str[i + 1:].strip()
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_01_deploy_vm_with_extraconfig_throws_exception_kvm(self):
|
|
'''
|
|
Test that extra config is not added when element tag is not added on the allowed list global config on KVM hosts
|
|
'''
|
|
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'kvm':
|
|
raise self.skipTest("Skipping test case for non-kvm hypervisor")
|
|
|
|
'''
|
|
The following extraconfig is required for enabling hugepages on kvm
|
|
<memoryBacking>
|
|
<hugepages/>
|
|
</memoryBacking>
|
|
url encoded extra_config = '%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E'
|
|
'''
|
|
extraconfig = "%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E"
|
|
|
|
try:
|
|
# Clear KVM allow list to show that code throws exception when command is not included in the list
|
|
name = 'allow.additional.vm.configuration.list.kvm'
|
|
|
|
self.add_global_config(name, "")
|
|
self.assertRaises(Exception,
|
|
self.deploy_vm(hypervisor, extraconfig),
|
|
"Exception was not thrown, check kvm global configuration")
|
|
except Exception as e:
|
|
logging.debug(e)
|
|
finally:
|
|
self.destroy_vm(self.list_vm().id)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_02_deploy_vm_with_extraconfig_kvm(self):
|
|
'''
|
|
Test that extra config is added on KVM hosts
|
|
'''
|
|
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'kvm':
|
|
raise self.skipTest("Skipping test case for non-kvm hypervisor")
|
|
|
|
name = 'allow.additional.vm.configuration.list.kvm'
|
|
value = 'memoryBacking, hugepages, unsedConfigKey'
|
|
|
|
add_config_response = self.add_global_config(name, value)
|
|
|
|
if add_config_response.name:
|
|
try:
|
|
'''
|
|
The following extraconfig is required for enabling hugepages on kvm
|
|
<memoryBacking>
|
|
<hugepages/>
|
|
</memoryBacking>
|
|
url encoded extra_config = '%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E'
|
|
'''
|
|
extraconfig = "%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E"
|
|
|
|
response = self.deploy_vm(hypervisor, extraconfig)
|
|
|
|
host_id = response.hostid
|
|
host = list_hosts(
|
|
self.apiclient,
|
|
id=host_id,
|
|
hypervisor=hypervisor)
|
|
|
|
instance_name = response.instancename
|
|
host_ipaddress = host[0].ipaddress
|
|
|
|
ssh_client = SshClient(host_ipaddress, port=22,
|
|
user=self.hostConfig['username'],
|
|
passwd=self.hostConfig['password'])
|
|
virsh_cmd = 'virsh dumpxml %s' % instance_name
|
|
xml_res = ssh_client.execute(virsh_cmd)
|
|
xml_as_str = ''.join(xml_res)
|
|
|
|
extraconfig_decoded_xml = '<config>' + urllib.unquote(extraconfig) + '</config>'
|
|
|
|
# Root XML Elements
|
|
parser = etree.XMLParser(remove_blank_text=True)
|
|
domain_xml_root = ET.fromstring(xml_as_str, parser=parser)
|
|
decoded_xml_root = ET.fromstring(extraconfig_decoded_xml, parser=parser)
|
|
for child in decoded_xml_root:
|
|
find_element_in_domain_xml = domain_xml_root.find(child.tag)
|
|
|
|
# Fail if extra config is not found in domain xml
|
|
self.assertNotEquals(
|
|
0,
|
|
len(find_element_in_domain_xml),
|
|
'Element tag from extra config not added to VM'
|
|
)
|
|
|
|
# Compare found XML node with extra config node
|
|
is_a_match = self.elements_equal(child, find_element_in_domain_xml)
|
|
self.assertEquals(
|
|
True,
|
|
is_a_match,
|
|
'The element from tags from extra config do not match with those found in domain xml'
|
|
)
|
|
finally:
|
|
self.destroy_vm(response.id)
|
|
self.add_global_config(name, "")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_03_update_vm_with_extraconfig_kvm(self):
|
|
'''
|
|
Test that extra config is added on KVM hosts
|
|
'''
|
|
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'kvm':
|
|
raise self.skipTest("Skipping test case for non-kvm hypervisor")
|
|
|
|
name = 'allow.additional.vm.configuration.list.kvm'
|
|
value = 'memoryBacking, hugepages'
|
|
|
|
add_config_response = self.add_global_config(name, value)
|
|
|
|
if add_config_response.name:
|
|
try:
|
|
'''
|
|
The following extraconfig is required for enabling hugepages on kvm
|
|
<memoryBacking>
|
|
<hugepages/>
|
|
</memoryBacking>
|
|
url encoded extra_config = '%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E'
|
|
'''
|
|
extraconfig = "%3CmemoryBacking%3E%0D%0A++%3Chugepages%2F%3E%0D%0A%3C%2FmemoryBacking%3E"
|
|
|
|
response = self.deploy_vm(hypervisor)
|
|
vm_id = response.id
|
|
|
|
'''
|
|
For updateVirtualMachineCmd, the VM must be stopped and restarted for changes to take effect
|
|
'''
|
|
self.stop_vm(vm_id)
|
|
self.update_vm(vm_id, extraconfig)
|
|
start_resp = self.start_vm(vm_id)
|
|
|
|
host_id = start_resp.hostid
|
|
host = list_hosts(
|
|
self.apiclient,
|
|
id=host_id,
|
|
hypervisor=hypervisor)
|
|
|
|
instance_name = response.instancename
|
|
host_ipaddress = host[0].ipaddress
|
|
|
|
ssh_client = SshClient(host_ipaddress, port=22,
|
|
user=self.hostConfig['username'],
|
|
passwd=self.hostConfig['password'])
|
|
virsh_cmd = 'virsh dumpxml %s' % instance_name
|
|
xml_res = ssh_client.execute(virsh_cmd)
|
|
xml_as_str = ''.join(xml_res)
|
|
|
|
extraconfig_decoded_xml = '<config>' + urllib.unquote(extraconfig) + '</config>'
|
|
|
|
# Root XML Elements
|
|
parser = etree.XMLParser(remove_blank_text=True)
|
|
domain_xml_root = ET.fromstring(xml_as_str, parser=parser)
|
|
decoded_xml_root = ET.fromstring(extraconfig_decoded_xml, parser=parser)
|
|
for child in decoded_xml_root:
|
|
find_element_in_domain_xml = domain_xml_root.find(child.tag)
|
|
|
|
# Fail if extra config is not found in domain xml
|
|
self.assertNotEquals(
|
|
0,
|
|
len(find_element_in_domain_xml),
|
|
'Element tag from extra config not added to VM'
|
|
)
|
|
|
|
# Compare found XML node with extra config node
|
|
is_a_match = self.elements_equal(child, find_element_in_domain_xml)
|
|
self.assertEquals(
|
|
True,
|
|
is_a_match,
|
|
'The element from tags from extra config do not match with those found in domain xml'
|
|
)
|
|
finally:
|
|
self.destroy_vm(vm_id)
|
|
self.add_global_config(name, "")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_04_deploy_vm_with_extraconfig_throws_exception_vmware(self):
|
|
'''
|
|
Test that extra config is not added when configuration key is not added on the allowed list global config for VMWARE hosts
|
|
'''
|
|
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'vmware':
|
|
raise self.skipTest("Skipping test case for non-vmware hypervisor")
|
|
|
|
'''
|
|
The following extra configuration is used to set Hyper-V instance to run on ESXi host
|
|
hypervisor.cpuid.v0 = FALSE
|
|
'''
|
|
extraconfig = 'hypervisor.cpuid.v0%3DFALSE'
|
|
|
|
try:
|
|
# Clear VMWARE allow list to show that code throws exception when command is not included in the list
|
|
name = 'allow.additional.vm.configuration.list.vmware'
|
|
|
|
self.add_global_config(name, "")
|
|
self.assertRaises(Exception,
|
|
self.deploy_vm(hypervisor, extraconfig),
|
|
"Exception was not thrown, check VMWARE global configuration")
|
|
except Exception as e:
|
|
logging.debug(e)
|
|
finally:
|
|
self.destroy_vm(self.list_vm().id)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_05_deploy_vm_with_extraconfig_vmware(self):
|
|
'''
|
|
Test that extra config is added on VMware hosts
|
|
'''
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'vmware':
|
|
raise self.skipTest("Skipping test case for non-vmware hypervisor")
|
|
|
|
name = 'allow.additional.vm.configuration.list.vmware'
|
|
value = 'hypervisor.cpuid.v0'
|
|
|
|
add_config_response = self.add_global_config(name, value)
|
|
|
|
if add_config_response.name:
|
|
|
|
'''
|
|
The following extra configuration is used to set Hyper-V instance to run on ESXi host
|
|
hypervisor.cpuid.v0 = FALSE
|
|
'''
|
|
extraconfig = 'hypervisor.cpuid.v0%3DFALSE'
|
|
try:
|
|
response = self.deploy_vm(hypervisor, extraconfig)
|
|
host_id = response.hostid
|
|
host = list_hosts(
|
|
self.apiclient,
|
|
id=host_id)
|
|
|
|
instance_name = response.instancename
|
|
host_ipaddress = host[0].ipaddress
|
|
|
|
ssh_client = SshClient(host_ipaddress, port=22,
|
|
user=self.hostConfig['username'],
|
|
passwd=self.hostConfig['password'])
|
|
|
|
extraconfig_decoded = urllib.unquote(extraconfig)
|
|
config_arr = extraconfig_decoded.splitlines()
|
|
|
|
for config in config_arr:
|
|
vmx_config = self.prepare_vmware_config(config)
|
|
vmx_file_name = "\"$(esxcli vm process list | grep %s | tail -1 | awk '{print $3}')\"" % instance_name
|
|
# parse vm instance vmx file to see if extraconfig has been added
|
|
grep_config = "cat %s | grep -w '%s'" % (vmx_file_name, vmx_config)
|
|
result = ssh_client.execute(grep_config)
|
|
# Match exact configuration from vmx file, return empty result array if configuration is not found
|
|
self.assertNotEquals(
|
|
0,
|
|
len(result),
|
|
'Extra configuration not found in instance vmx file'
|
|
)
|
|
finally:
|
|
self.destroy_vm(response.id)
|
|
self.add_global_config(name, "")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_06_deploy_vm_with_extraconfig_throws_exception_xenserver(self):
|
|
'''
|
|
Test that extra config is not added when configuration key is not added on the allowed list global config for XenServer hosts
|
|
'''
|
|
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'xenserver':
|
|
raise self.skipTest("Skipping test case for non-xenserver hypervisor")
|
|
|
|
'''
|
|
Following commands are used to convert a VM from HVM to PV and set using vm-param-set
|
|
HVM-boot-policy=
|
|
PV-bootloader=pygrub
|
|
PV-args=hvc0
|
|
'''
|
|
|
|
extraconfig = 'HVM-boot-policy%3D%0APV-bootloader%3Dpygrub%0APV-args%3Dhvc0'
|
|
|
|
try:
|
|
# Clear VMWARE allow list to show that code throws exception when command is not included in the list
|
|
name = 'allow.additional.vm.configuration.list.xenserver'
|
|
|
|
self.add_global_config(name, "")
|
|
self.assertRaises(Exception,
|
|
self.deploy_vm(hypervisor, extraconfig),
|
|
"Exception was not thrown, check XenServer global configuration")
|
|
|
|
except Exception as e:
|
|
logging.debug(e)
|
|
finally:
|
|
self.destroy_vm(self.list_vm().id)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="true")
|
|
def test_07_deploy_vm_with_extraconfig_xenserver(self):
|
|
hypervisor = self.hypervisor.lower()
|
|
if hypervisor != 'xenserver':
|
|
raise self.skipTest("Skipping test case for non-xenserver hypervisor")
|
|
"""
|
|
Following commands are used to convert a VM from HVM to PV and set using vm-param-set
|
|
HVM-boot-policy=
|
|
PV-bootloader=pygrub
|
|
PV-args=hvc0
|
|
"""
|
|
|
|
name = 'allow.additional.vm.configuration.list.xenserver'
|
|
value = 'HVM-boot-policy, PV-bootloader, PV-args'
|
|
|
|
add_config_response = self.add_global_config(name, value)
|
|
|
|
if add_config_response.name:
|
|
extraconfig = 'HVM-boot-policy%3D%0APV-bootloader%3Dpygrub%0APV-args%3Dhvc0'
|
|
try:
|
|
response = self.deploy_vm(hypervisor, extraconfig)
|
|
host_id = response.hostid
|
|
host = list_hosts(
|
|
self.apiclient,
|
|
id=host_id)
|
|
|
|
host_ipaddress = host[0].ipaddress
|
|
|
|
ssh_client = SshClient(host_ipaddress, port=22,
|
|
user=self.hostConfig['username'],
|
|
passwd=self.hostConfig['password'])
|
|
|
|
extraconfig_decoded = urllib.unquote(extraconfig)
|
|
config_arr = extraconfig_decoded.splitlines()
|
|
|
|
# Get vm instance uuid
|
|
instance_uuid = self.get_vm_uuid(response.instancename, ssh_client)
|
|
for config in config_arr:
|
|
config_tuple = self.get_xen_param_values(config)
|
|
# Log on to XenServer host and check the vm-param-get
|
|
vm_config_check = 'xe vm-param-get param-name={} uuid={}'.format(config_tuple[0], instance_uuid)
|
|
result = ssh_client.execute(vm_config_check)
|
|
param_value = config_tuple[1].strip()
|
|
# Check if each configuration command has set the configuration as sent with extraconfig
|
|
self.assertEquals(
|
|
param_value,
|
|
result[0],
|
|
'Extra configuration not found in VM param list'
|
|
)
|
|
finally:
|
|
self.destroy_vm(response.id)
|
|
self.add_global_config(name, "")
|