X-Git-Url: https://gerrit.opnfv.org/gerrit/gitweb?a=blobdiff_plain;f=functest%2Fcore%2Fsinglevm.py;h=b2d855b30970923ca4db662a464beaff3cdbd76f;hb=61138bfec980625bec6c219b9e27685c281e5965;hp=331ada4841cddb42de199af8e83c3a965e3e5249;hpb=b2824f4dc2f32731667e2cf511635dc885d08be9;p=functest.git diff --git a/functest/core/singlevm.py b/functest/core/singlevm.py index 331ada484..b2d855b30 100644 --- a/functest/core/singlevm.py +++ b/functest/core/singlevm.py @@ -9,11 +9,12 @@ """Ease deploying a single VM reachable via ssh -It offers a simple way to create all tenant network ressources + a VM for +It offers a simple way to create all tenant network resources + a VM for advanced testcases (e.g. deploying an orchestrator). """ import logging +import re import tempfile import time @@ -22,10 +23,12 @@ from xtesting.core import testcase from functest.core import tenantnetwork from functest.utils import config +from functest.utils import env +from functest.utils import functest_utils class VmReady1(tenantnetwork.TenantNetwork1): - """Deploy a single VM reachable via ssh (scenario1) + """Prepare a single VM (scenario1) It inherits from TenantNetwork1 which creates all network resources and prepares a future VM attached to that network. @@ -36,44 +39,241 @@ class VmReady1(tenantnetwork.TenantNetwork1): # pylint: disable=too-many-instance-attributes __logger = logging.getLogger(__name__) - filename = '/home/opnfv/functest/images/cirros-0.4.0-x86_64-disk.img' - flavor_ram = 1024 + filename = '/home/opnfv/functest/images/cirros-0.5.1-x86_64-disk.img' + image_format = 'qcow2' + extra_properties = {} + filename_alt = filename + image_alt_format = image_format + extra_alt_properties = extra_properties + visibility = 'private' + flavor_ram = 512 flavor_vcpus = 1 flavor_disk = 1 + flavor_extra_specs = {} + flavor_alt_ram = 1024 + flavor_alt_vcpus = 1 + flavor_alt_disk = 1 + flavor_alt_extra_specs = flavor_extra_specs + create_server_timeout = 180 def __init__(self, **kwargs): if "case_name" not in kwargs: kwargs["case_name"] = 'vmready1' - super(VmReady1, self).__init__(**kwargs) - self.orig_cloud = self.cloud + super().__init__(**kwargs) self.image = None self.flavor = None - def _publish_image(self): + def publish_image(self, name=None): + """Publish image + + It allows publishing multiple images for the child testcases. It forces + the same configuration for all subtestcases. + + Returns: image + + Raises: expection on error + """ assert self.cloud - meta = getattr( - config.CONF, '{}_extra_properties'.format(self.case_name), None) - self.image = self.cloud.create_image( - '{}-img_{}'.format(self.case_name, self.guid), + extra_properties = self.extra_properties.copy() + if env.get('IMAGE_PROPERTIES'): + extra_properties.update( + functest_utils.convert_ini_to_dict( + env.get('IMAGE_PROPERTIES'))) + extra_properties.update( + getattr(config.CONF, '{}_extra_properties'.format( + self.case_name), {})) + image = self.cloud.create_image( + name if name else '{}-img_{}'.format(self.case_name, self.guid), filename=getattr( config.CONF, '{}_image'.format(self.case_name), self.filename), - meta=meta) - self.__logger.debug("image: %s", self.image) - - def _create_flavor(self): + meta=extra_properties, + disk_format=getattr( + config.CONF, '{}_image_format'.format(self.case_name), + self.image_format), + visibility=getattr( + config.CONF, '{}_visibility'.format(self.case_name), + self.visibility), + wait=True) + self.__logger.debug("image: %s", image) + return image + + def publish_image_alt(self, name=None): + """Publish alternative image + + It allows publishing multiple images for the child testcases. It forces + the same configuration for all subtestcases. + + Returns: image + + Raises: expection on error + """ + assert self.cloud + extra_alt_properties = self.extra_alt_properties.copy() + if env.get('IMAGE_PROPERTIES'): + extra_alt_properties.update( + functest_utils.convert_ini_to_dict( + env.get('IMAGE_PROPERTIES'))) + extra_alt_properties.update( + getattr(config.CONF, '{}_extra_alt_properties'.format( + self.case_name), {})) + image = self.cloud.create_image( + name if name else '{}-img_alt_{}'.format( + self.case_name, self.guid), + filename=getattr( + config.CONF, '{}_image_alt'.format(self.case_name), + self.filename_alt), + meta=extra_alt_properties, + disk_format=getattr( + config.CONF, '{}_image_alt_format'.format(self.case_name), + self.image_format), + visibility=getattr( + config.CONF, '{}_visibility'.format(self.case_name), + self.visibility), + wait=True) + self.__logger.debug("image: %s", image) + return image + + def create_flavor(self, name=None): + """Create flavor + + It allows creating multiple flavors for the child testcases. It forces + the same configuration for all subtestcases. + + Returns: flavor + + Raises: expection on error + """ assert self.orig_cloud - self.flavor = self.orig_cloud.create_flavor( - '{}-flavor_{}'.format(self.case_name, self.guid), + flavor = self.orig_cloud.create_flavor( + name if name else '{}-flavor_{}'.format(self.case_name, self.guid), getattr(config.CONF, '{}_flavor_ram'.format(self.case_name), self.flavor_ram), getattr(config.CONF, '{}_flavor_vcpus'.format(self.case_name), self.flavor_vcpus), getattr(config.CONF, '{}_flavor_disk'.format(self.case_name), self.flavor_disk)) - self.__logger.debug("flavor: %s", self.flavor) + self.__logger.debug("flavor: %s", flavor) + flavor_extra_specs = self.flavor_extra_specs.copy() + if env.get('FLAVOR_EXTRA_SPECS'): + flavor_extra_specs.update( + functest_utils.convert_ini_to_dict( + env.get('FLAVOR_EXTRA_SPECS'))) + flavor_extra_specs.update( + getattr(config.CONF, + '{}_flavor_extra_specs'.format(self.case_name), {})) + self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs) + return flavor + + def create_flavor_alt(self, name=None): + """Create flavor + + It allows creating multiple alt flavors for the child testcases. It + forces the same configuration for all subtestcases. + + Returns: flavor + + Raises: expection on error + """ + assert self.orig_cloud + flavor = self.orig_cloud.create_flavor( + name if name else '{}-flavor_alt_{}'.format( + self.case_name, self.guid), + getattr(config.CONF, '{}_flavor_alt_ram'.format(self.case_name), + self.flavor_alt_ram), + getattr(config.CONF, '{}_flavor_alt_vcpus'.format(self.case_name), + self.flavor_alt_vcpus), + getattr(config.CONF, '{}_flavor_alt_disk'.format(self.case_name), + self.flavor_alt_disk)) + self.__logger.debug("flavor: %s", flavor) + flavor_alt_extra_specs = self.flavor_alt_extra_specs.copy() + if env.get('FLAVOR_EXTRA_SPECS'): + flavor_alt_extra_specs.update( + functest_utils.convert_ini_to_dict( + env.get('FLAVOR_EXTRA_SPECS'))) + flavor_alt_extra_specs.update( + getattr(config.CONF, + '{}_flavor_alt_extra_specs'.format(self.case_name), {})) self.orig_cloud.set_flavor_specs( - self.flavor.id, getattr(config.CONF, 'flavor_extra_specs', {})) + flavor.id, flavor_alt_extra_specs) + return flavor + + def boot_vm(self, name=None, **kwargs): + """Boot the virtual machine + + It allows booting multiple machines for the child testcases. It forces + the same configuration for all subtestcases. + + Returns: vm + + Raises: expection on error + """ + assert self.cloud + vm1 = self.cloud.create_server( + name if name else '{}-vm_{}'.format(self.case_name, self.guid), + image=self.image.id, flavor=self.flavor.id, + auto_ip=False, + network=self.network.id if self.network else env.get( + "EXTERNAL_NETWORK"), + timeout=self.create_server_timeout, wait=True, **kwargs) + self.__logger.debug("vm: %s", vm1) + return vm1 + + def check_regex_in_console(self, name, regex=' login: ', loop=6): + """Wait for specific message in console + + Returns: True or False on errors + """ + assert self.cloud + for iloop in range(loop): + console = self.cloud.get_server_console(name) + self.__logger.debug("console: \n%s", console) + if re.search(regex, console): + self.__logger.debug( + "regex found: '%s' in console\n%s", regex, console) + return True + self.__logger.debug( + "try %s: cannot find regex '%s' in console\n%s", + iloop + 1, regex, console) + time.sleep(10) + self.__logger.error("cannot find regex '%s' in console", regex) + return False + + def clean_orphan_security_groups(self): + """Clean all security groups which are not owned by an existing tenant + + It lists all orphan security groups in use as debug to avoid + misunderstanding the testcase results (it could happen if cloud admin + removes accounts without cleaning the virtual machines) + """ + sec_groups = self.orig_cloud.list_security_groups() + for sec_group in sec_groups: + if not sec_group.tenant_id: + continue + if not self.orig_cloud.get_project(sec_group.tenant_id): + self.__logger.debug("Cleaning security group %s", sec_group.id) + try: + self.orig_cloud.delete_security_group(sec_group.id) + except Exception: # pylint: disable=broad-except + self.__logger.debug( + "Orphan security group %s in use", sec_group.id) + + def count_hypervisors(self): + """Count hypervisors.""" + if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false': + return len(self.orig_cloud.list_hypervisors()) + return self.count_active_hypervisors() + + def count_active_hypervisors(self): + """Count all hypervisors which are up.""" + compute_cnt = 0 + for hypervisor in self.orig_cloud.list_hypervisors(): + if hypervisor['state'] == 'up': + compute_cnt += 1 + else: + self.__logger.warning( + "%s is down", hypervisor['hypervisor_hostname']) + return compute_cnt def run(self, **kwargs): """Boot the new VM @@ -89,13 +289,15 @@ class VmReady1(tenantnetwork.TenantNetwork1): status = testcase.TestCase.EX_RUN_ERROR try: assert self.cloud - super(VmReady1, self).run(**kwargs) - self._publish_image() - self._create_flavor() + assert super().run( + **kwargs) == testcase.TestCase.EX_OK + self.image = self.publish_image() + self.flavor = self.create_flavor() self.result = 100 status = testcase.TestCase.EX_OK except Exception: # pylint: disable=broad-except self.__logger.exception('Cannot run %s', self.case_name) + self.result = 0 finally: self.stop_time = time.time() return status @@ -104,18 +306,22 @@ class VmReady1(tenantnetwork.TenantNetwork1): try: assert self.orig_cloud assert self.cloud - self.cloud.delete_image(self.image) - self.orig_cloud.delete_flavor(self.flavor.id) - self.cloud.delete_image(self.image) + super().clean() + if self.image: + self.cloud.delete_image(self.image.id) + if self.flavor: + self.orig_cloud.delete_flavor(self.flavor.id) + if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true': + self.clean_orphan_security_groups() except Exception: # pylint: disable=broad-except - pass + self.__logger.exception("Cannot clean all resources") class VmReady2(VmReady1): """Deploy a single VM reachable via ssh (scenario2) It creates new user/project before creating and configuring all tenant - network ressources, flavors, images, etc. required by advanced testcases. + network resources, flavors, images, etc. required by advanced testcases. It ensures that all testcases inheriting from SingleVm2 could work without specific configurations (or at least read the same config data). @@ -126,7 +332,7 @@ class VmReady2(VmReady1): def __init__(self, **kwargs): if "case_name" not in kwargs: kwargs["case_name"] = 'vmready2' - super(VmReady2, self).__init__(**kwargs) + super().__init__(**kwargs) try: assert self.orig_cloud self.project = tenantnetwork.NewProject( @@ -140,11 +346,11 @@ class VmReady2(VmReady1): def clean(self): try: - super(VmReady2, self).clean() + super().clean() assert self.project self.project.clean() except Exception: # pylint: disable=broad-except - self.__logger.exception("cannot clean all ressources") + self.__logger.exception("Cannot clean all resources") class SingleVm1(VmReady1): @@ -160,21 +366,25 @@ class SingleVm1(VmReady1): __logger = logging.getLogger(__name__) username = 'cirros' - ssh_connect_timeout = 60 + ssh_connect_timeout = 1 + ssh_connect_loops = 6 + create_floating_ip_timeout = 120 + check_console_loop = 6 + check_console_regex = ' login: ' def __init__(self, **kwargs): if "case_name" not in kwargs: kwargs["case_name"] = 'singlevm1' - super(SingleVm1, self).__init__(**kwargs) + super().__init__(**kwargs) self.sshvm = None self.sec = None self.fip = None self.keypair = None - self.ssh = paramiko.SSHClient() + self.ssh = None (_, self.key_filename) = tempfile.mkstemp() - def create_sg_rules(self): - """Create the security group + def prepare(self): + """Create the security group and the keypair It can be overriden to set other rules according to the services running in the VM @@ -182,6 +392,12 @@ class SingleVm1(VmReady1): Raises: Exception on error """ assert self.cloud + self.keypair = self.cloud.create_keypair( + '{}-kp_{}'.format(self.case_name, self.guid)) + self.__logger.debug("keypair: %s", self.keypair) + self.__logger.debug("private_key:\n%s", self.keypair.private_key) + with open(self.key_filename, 'w') as private_key_file: + private_key_file.write(self.keypair.private_key) self.sec = self.cloud.create_security_group( '{}-sg_{}'.format(self.case_name, self.guid), 'created by OPNFV Functest ({})'.format(self.case_name)) @@ -191,37 +407,31 @@ class SingleVm1(VmReady1): self.cloud.create_security_group_rule( self.sec.id, protocol='icmp', direction='ingress') - def _boot_vm(self): - assert self.cloud - self.keypair = self.cloud.create_keypair( - '{}-kp_{}'.format(self.case_name, self.guid)) - self.__logger.debug("keypair: %s", self.keypair) - self.__logger.debug("private_key: %s", self.keypair.private_key) - with open(self.key_filename, 'w') as private_key_file: - private_key_file.write(self.keypair.private_key) + def connect(self, vm1): + """Connect to a virtual machine via ssh - self.sshvm = self.cloud.create_server( - '{}-vm_{}'.format(self.case_name, self.guid), - image=self.image.id, flavor=self.flavor.id, - key_name=self.keypair.id, - auto_ip=False, wait=True, - network=self.network.id, - security_groups=[self.sec.id]) - self.__logger.debug("vm: %s", self.sshvm) - self.fip = self.cloud.create_floating_ip( - network=self.ext_net.id, server=self.sshvm) - self.__logger.debug("floating_ip: %s", self.fip) - self.sshvm = self.cloud.wait_for_server(self.sshvm, auto_ip=False) - - def _connect(self): - assert self.cloud - p_console = self.cloud.get_server_console(self.sshvm.id) - self.__logger.debug("vm console: \n%s", p_console) - self.ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy()) - for loop in range(6): + It first adds a floating ip to the virtual machine and then establishes + the ssh connection. + + Returns: + - (fip, ssh) + - None on error + """ + assert vm1 + fip = None + if env.get('NO_TENANT_NETWORK').lower() != 'true': + fip = self.cloud.create_floating_ip( + network=self.ext_net.id, server=vm1, wait=True, + timeout=self.create_floating_ip_timeout) + self.__logger.debug("floating_ip: %s", fip) + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy()) + for loop in range(self.ssh_connect_loops): try: - self.ssh.connect( - self.sshvm.public_v4, + p_console = self.cloud.get_server_console(vm1) + self.__logger.debug("vm console: \n%s", p_console) + ssh.connect( + fip.floating_ip_address if fip else vm1.public_v4, username=getattr( config.CONF, '{}_image_user'.format(self.case_name), self.username), @@ -231,15 +441,16 @@ class SingleVm1(VmReady1): '{}_vm_ssh_connect_timeout'.format(self.case_name), self.ssh_connect_timeout)) break - except Exception: # pylint: disable=broad-except - self.__logger.info( - "try %s: cannot connect to %s", loop + 1, - self.sshvm.public_v4) - time.sleep(10) + except Exception as exc: # pylint: disable=broad-except + self.__logger.debug( + "try %s: cannot connect to %s: %s", loop + 1, + fip.floating_ip_address if fip else vm1.public_v4, exc) + time.sleep(9) else: - self.__logger.error("cannot connect to %s", self.sshvm.public_v4) - return False - return True + self.__logger.error( + "cannot connect to %s", fip.floating_ip_address) + return None + return (fip, ssh) def execute(self): """Say hello world via ssh @@ -248,8 +459,9 @@ class SingleVm1(VmReady1): Returns: echo exit codes """ - (_, stdout, _) = self.ssh.exec_command('echo Hello World') - self.__logger.info("output:\n%s", stdout.read()) + (_, stdout, stderr) = self.ssh.exec_command('echo Hello World') + self.__logger.debug("output:\n%s", stdout.read().decode("utf-8")) + self.__logger.debug("error:\n%s", stderr.read().decode("utf-8")) return stdout.channel.recv_exit_status() def run(self, **kwargs): @@ -268,14 +480,19 @@ class SingleVm1(VmReady1): status = testcase.TestCase.EX_RUN_ERROR try: assert self.cloud - super(SingleVm1, self).run(**kwargs) + assert super().run( + **kwargs) == testcase.TestCase.EX_OK self.result = 0 - self.create_sg_rules() - self._boot_vm() - assert self._connect() - if not self.execute(): - self.result = 100 - status = testcase.TestCase.EX_OK + self.prepare() + self.sshvm = self.boot_vm( + key_name=self.keypair.id, security_groups=[self.sec.id]) + if self.check_regex_in_console( + self.sshvm.name, regex=self.check_console_regex, + loop=self.check_console_loop): + (self.fip, self.ssh) = self.connect(self.sshvm) + if not self.execute(): + self.result = 100 + status = testcase.TestCase.EX_OK except Exception: # pylint: disable=broad-except self.__logger.exception('Cannot run %s', self.case_name) finally: @@ -286,22 +503,24 @@ class SingleVm1(VmReady1): try: assert self.orig_cloud assert self.cloud - self.cloud.delete_server(self.sshvm, wait=True) - self.cloud.delete_security_group(self.sec.id) - self.cloud.delete_image(self.image) - self.orig_cloud.delete_flavor(self.flavor.id) - self.cloud.delete_keypair(self.keypair.id) - self.cloud.delete_floating_ip(self.fip.id) - self.cloud.delete_image(self.image) + if self.fip: + self.cloud.delete_floating_ip(self.fip.id) + if self.sshvm: + self.cloud.delete_server(self.sshvm, wait=True) + if self.sec: + self.cloud.delete_security_group(self.sec.id) + if self.keypair: + self.cloud.delete_keypair(self.keypair.name) + super().clean() except Exception: # pylint: disable=broad-except - pass + self.__logger.exception("Cannot clean all resources") class SingleVm2(SingleVm1): """Deploy a single VM reachable via ssh (scenario2) It creates new user/project before creating and configuring all tenant - network ressources and vms required by advanced testcases. + network resources and vms required by advanced testcases. It ensures that all testcases inheriting from SingleVm2 could work without specific configurations (or at least read the same config data). @@ -312,7 +531,7 @@ class SingleVm2(SingleVm1): def __init__(self, **kwargs): if "case_name" not in kwargs: kwargs["case_name"] = 'singlevm2' - super(SingleVm2, self).__init__(**kwargs) + super().__init__(**kwargs) try: assert self.orig_cloud self.project = tenantnetwork.NewProject( @@ -326,8 +545,8 @@ class SingleVm2(SingleVm1): def clean(self): try: - super(SingleVm2, self).clean() + super().clean() assert self.project self.project.clean() except Exception: # pylint: disable=broad-except - self.__logger.exception("cannot clean all ressources") + self.__logger.exception("Cannot clean all resources")