aboutsummaryrefslogtreecommitdiffstats
path: root/functest/core/singlevm.py
diff options
context:
space:
mode:
Diffstat (limited to 'functest/core/singlevm.py')
-rw-r--r--functest/core/singlevm.py549
1 files changed, 549 insertions, 0 deletions
diff --git a/functest/core/singlevm.py b/functest/core/singlevm.py
new file mode 100644
index 000000000..4bce516d3
--- /dev/null
+++ b/functest/core/singlevm.py
@@ -0,0 +1,549 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2018 Orange and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+
+"""Ease deploying a single VM reachable via ssh
+
+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
+
+import paramiko
+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):
+ """Prepare a single VM (scenario1)
+
+ It inherits from TenantNetwork1 which creates all network resources and
+ prepares a future VM attached to that network.
+
+ It ensures that all testcases inheriting from SingleVm1 could work
+ without specific configurations (or at least read the same config data).
+ """
+ # pylint: disable=too-many-instance-attributes
+
+ __logger = logging.getLogger(__name__)
+ filename = '/home/opnfv/functest/images/cirros-0.6.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().__init__(**kwargs)
+ self.image = None
+ self.flavor = None
+
+ 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
+ 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, f'{self.case_name}_extra_properties', {}))
+ image = self.cloud.create_image(
+ name if name else f'{self.case_name}-img_{self.guid}',
+ filename=getattr(
+ config.CONF, f'{self.case_name}_image',
+ self.filename),
+ meta=extra_properties,
+ disk_format=getattr(
+ config.CONF, f'{self.case_name}_image_format',
+ self.image_format),
+ visibility=getattr(
+ config.CONF, f'{self.case_name}_visibility',
+ 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, f'{self.case_name}_extra_alt_properties', {}))
+ image = self.cloud.create_image(
+ name if name else f'{self.case_name}-img_alt_{self.guid}',
+ filename=getattr(
+ config.CONF, f'{self.case_name}_image_alt',
+ self.filename_alt),
+ meta=extra_alt_properties,
+ disk_format=getattr(
+ config.CONF, f'{self.case_name}_image_alt_format',
+ self.image_format),
+ visibility=getattr(
+ config.CONF, f'{self.case_name}_visibility',
+ 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
+ flavor = self.orig_cloud.create_flavor(
+ name if name else f'{self.case_name}-flavor_{self.guid}',
+ getattr(config.CONF, f'{self.case_name}_flavor_ram',
+ self.flavor_ram),
+ getattr(config.CONF, f'{self.case_name}_flavor_vcpus',
+ self.flavor_vcpus),
+ getattr(config.CONF, f'{self.case_name}_flavor_disk',
+ self.flavor_disk))
+ 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,
+ f'{self.case_name}_flavor_extra_specs', {}))
+ 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 f'{self.case_name}-flavor_alt_{self.guid}',
+ getattr(config.CONF, f'{self.case_name}_flavor_alt_ram',
+ self.flavor_alt_ram),
+ getattr(config.CONF, f'{self.case_name}_flavor_alt_vcpus',
+ self.flavor_alt_vcpus),
+ getattr(config.CONF, f'{self.case_name}_flavor_alt_disk',
+ 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,
+ f'{self.case_name}_flavor_alt_extra_specs', {}))
+ self.orig_cloud.set_flavor_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 f'{self.case_name}-vm_{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
+
+ Here are the main actions:
+ - publish the image
+ - create the flavor
+
+ Returns:
+ - TestCase.EX_OK
+ - TestCase.EX_RUN_ERROR on error
+ """
+ status = testcase.TestCase.EX_RUN_ERROR
+ try:
+ assert self.cloud
+ 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
+
+ def clean(self):
+ try:
+ assert self.orig_cloud
+ assert self.cloud
+ 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
+ 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 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).
+ """
+
+ __logger = logging.getLogger(__name__)
+
+ def __init__(self, **kwargs):
+ if "case_name" not in kwargs:
+ kwargs["case_name"] = 'vmready2'
+ super().__init__(**kwargs)
+ try:
+ assert self.orig_cloud
+ self.project = tenantnetwork.NewProject(
+ self.orig_cloud, self.case_name, self.guid)
+ self.project.create()
+ self.cloud = self.project.cloud
+ except Exception: # pylint: disable=broad-except
+ self.__logger.exception("Cannot create user or project")
+ self.cloud = None
+ self.project = None
+
+ def clean(self):
+ try:
+ super().clean()
+ assert self.project
+ self.project.clean()
+ except Exception: # pylint: disable=broad-except
+ self.__logger.exception("Cannot clean all resources")
+
+
+class SingleVm1(VmReady1):
+ """Deploy a single VM reachable via ssh (scenario1)
+
+ It inherits from TenantNetwork1 which creates all network resources and
+ completes it by booting a VM attached to that network.
+
+ It ensures that all testcases inheriting from SingleVm1 could work
+ without specific configurations (or at least read the same config data).
+ """
+ # pylint: disable=too-many-instance-attributes
+
+ __logger = logging.getLogger(__name__)
+ username = 'cirros'
+ 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().__init__(**kwargs)
+ self.sshvm = None
+ self.sec = None
+ self.fip = None
+ self.keypair = None
+ self.ssh = None
+ (_, self.key_filename) = tempfile.mkstemp()
+
+ 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
+
+ Raises: Exception on error
+ """
+ assert self.cloud
+ self.keypair = self.cloud.create_keypair(
+ f'{self.case_name}-kp_{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', encoding='utf-8') as private_key_file:
+ private_key_file.write(self.keypair.private_key)
+ self.sec = self.cloud.create_security_group(
+ f'{self.case_name}-sg_{self.guid}',
+ f'created by OPNFV Functest ({self.case_name})')
+ self.cloud.create_security_group_rule(
+ self.sec.id, port_range_min='22', port_range_max='22',
+ protocol='tcp', direction='ingress')
+ self.cloud.create_security_group_rule(
+ self.sec.id, protocol='icmp', direction='ingress')
+
+ def connect(self, vm1):
+ """Connect to a virtual machine via ssh
+
+ 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:
+ 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,
+ f'{self.case_name}_image_user', self.username),
+ key_filename=self.key_filename,
+ timeout=getattr(
+ config.CONF,
+ f'{self.case_name}_vm_ssh_connect_timeout',
+ self.ssh_connect_timeout))
+ break
+ 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", fip.floating_ip_address)
+ return None
+ return (fip, ssh)
+
+ def execute(self):
+ """Say hello world via ssh
+
+ It can be overriden to execute any command.
+
+ Returns: echo exit codes
+ """
+ (_, 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):
+ """Boot the new VM
+
+ Here are the main actions:
+ - add a new ssh key
+ - boot the VM
+ - create the security group
+ - execute the right command over ssh
+
+ Returns:
+ - TestCase.EX_OK
+ - TestCase.EX_RUN_ERROR on error
+ """
+ status = testcase.TestCase.EX_RUN_ERROR
+ try:
+ assert self.cloud
+ assert super().run(
+ **kwargs) == testcase.TestCase.EX_OK
+ self.result = 0
+ 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:
+ self.stop_time = time.time()
+ return status
+
+ def clean(self):
+ try:
+ assert self.orig_cloud
+ assert self.cloud
+ 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
+ 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 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).
+ """
+
+ __logger = logging.getLogger(__name__)
+
+ def __init__(self, **kwargs):
+ if "case_name" not in kwargs:
+ kwargs["case_name"] = 'singlevm2'
+ super().__init__(**kwargs)
+ try:
+ assert self.orig_cloud
+ self.project = tenantnetwork.NewProject(
+ self.orig_cloud, self.case_name, self.guid)
+ self.project.create()
+ self.cloud = self.project.cloud
+ except Exception: # pylint: disable=broad-except
+ self.__logger.exception("Cannot create user or project")
+ self.cloud = None
+ self.project = None
+
+ def clean(self):
+ try:
+ super().clean()
+ assert self.project
+ self.project.clean()
+ except Exception: # pylint: disable=broad-except
+ self.__logger.exception("Cannot clean all resources")