3 # Copyright (c) 2018 Orange and others.
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 """Ease deploying a single VM reachable via ssh
12 It offers a simple way to create all tenant network resources + a VM for
13 advanced testcases (e.g. deploying an orchestrator).
22 from xtesting.core import testcase
24 from functest.core import tenantnetwork
25 from functest.utils import config
26 from functest.utils import env
27 from functest.utils import functest_utils
30 class VmReady1(tenantnetwork.TenantNetwork1):
31 """Prepare a single VM (scenario1)
33 It inherits from TenantNetwork1 which creates all network resources and
34 prepares a future VM attached to that network.
36 It ensures that all testcases inheriting from SingleVm1 could work
37 without specific configurations (or at least read the same config data).
39 # pylint: disable=too-many-instance-attributes
41 __logger = logging.getLogger(__name__)
42 filename = '/home/opnfv/functest/images/cirros-0.4.0-x86_64-disk.img'
43 image_format = 'qcow2'
45 filename_alt = filename
46 image_alt_format = image_format
47 extra_alt_properties = extra_properties
48 visibility = 'private'
52 flavor_extra_specs = {}
56 flavor_alt_extra_specs = flavor_extra_specs
57 create_server_timeout = 180
59 def __init__(self, **kwargs):
60 if "case_name" not in kwargs:
61 kwargs["case_name"] = 'vmready1'
62 super(VmReady1, self).__init__(**kwargs)
66 def publish_image(self, name=None):
69 It allows publishing multiple images for the child testcases. It forces
70 the same configuration for all subtestcases.
74 Raises: expection on error
77 extra_properties = self.extra_properties.copy()
78 if env.get('IMAGE_PROPERTIES'):
79 extra_properties.update(
80 functest_utils.convert_ini_to_dict(
81 env.get('IMAGE_PROPERTIES')))
82 extra_properties.update(
83 getattr(config.CONF, '{}_extra_properties'.format(
85 image = self.cloud.create_image(
86 name if name else '{}-img_{}'.format(self.case_name, self.guid),
88 config.CONF, '{}_image'.format(self.case_name),
90 meta=extra_properties,
92 config.CONF, '{}_image_format'.format(self.case_name),
95 config.CONF, '{}_visibility'.format(self.case_name),
98 self.__logger.debug("image: %s", image)
101 def publish_image_alt(self, name=None):
102 """Publish alternative image
104 It allows publishing multiple images for the child testcases. It forces
105 the same configuration for all subtestcases.
109 Raises: expection on error
112 extra_alt_properties = self.extra_alt_properties.copy()
113 if env.get('IMAGE_PROPERTIES'):
114 extra_alt_properties.update(
115 functest_utils.convert_ini_to_dict(
116 env.get('IMAGE_PROPERTIES')))
117 extra_alt_properties.update(
118 getattr(config.CONF, '{}_extra_alt_properties'.format(
119 self.case_name), {}))
120 image = self.cloud.create_image(
121 name if name else '{}-img_alt_{}'.format(
122 self.case_name, self.guid),
124 config.CONF, '{}_image_alt'.format(self.case_name),
126 meta=extra_alt_properties,
128 config.CONF, '{}_image_alt_format'.format(self.case_name),
131 config.CONF, '{}_visibility'.format(self.case_name),
134 self.__logger.debug("image: %s", image)
137 def create_flavor(self, name=None):
140 It allows creating multiple flavors for the child testcases. It forces
141 the same configuration for all subtestcases.
145 Raises: expection on error
147 assert self.orig_cloud
148 flavor = self.orig_cloud.create_flavor(
149 name if name else '{}-flavor_{}'.format(self.case_name, self.guid),
150 getattr(config.CONF, '{}_flavor_ram'.format(self.case_name),
152 getattr(config.CONF, '{}_flavor_vcpus'.format(self.case_name),
154 getattr(config.CONF, '{}_flavor_disk'.format(self.case_name),
156 self.__logger.debug("flavor: %s", flavor)
157 flavor_extra_specs = self.flavor_extra_specs.copy()
158 if env.get('FLAVOR_EXTRA_SPECS'):
159 flavor_extra_specs.update(
160 functest_utils.convert_ini_to_dict(
161 env.get('FLAVOR_EXTRA_SPECS')))
162 flavor_extra_specs.update(
164 '{}_flavor_extra_specs'.format(self.case_name), {}))
165 self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs)
168 def create_flavor_alt(self, name=None):
171 It allows creating multiple alt flavors for the child testcases. It
172 forces the same configuration for all subtestcases.
176 Raises: expection on error
178 assert self.orig_cloud
179 flavor = self.orig_cloud.create_flavor(
180 name if name else '{}-flavor_alt_{}'.format(
181 self.case_name, self.guid),
182 getattr(config.CONF, '{}_flavor_alt_ram'.format(self.case_name),
183 self.flavor_alt_ram),
184 getattr(config.CONF, '{}_flavor_alt_vcpus'.format(self.case_name),
185 self.flavor_alt_vcpus),
186 getattr(config.CONF, '{}_flavor_alt_disk'.format(self.case_name),
187 self.flavor_alt_disk))
188 self.__logger.debug("flavor: %s", flavor)
189 flavor_alt_extra_specs = self.flavor_alt_extra_specs.copy()
190 if env.get('FLAVOR_EXTRA_SPECS'):
191 flavor_alt_extra_specs.update(
192 functest_utils.convert_ini_to_dict(
193 env.get('FLAVOR_EXTRA_SPECS')))
194 flavor_alt_extra_specs.update(
196 '{}_flavor_alt_extra_specs'.format(self.case_name), {}))
197 self.orig_cloud.set_flavor_specs(
198 flavor.id, flavor_alt_extra_specs)
201 def boot_vm(self, name=None, **kwargs):
202 """Boot the virtual machine
204 It allows booting multiple machines for the child testcases. It forces
205 the same configuration for all subtestcases.
209 Raises: expection on error
212 vm1 = self.cloud.create_server(
213 name if name else '{}-vm_{}'.format(self.case_name, self.guid),
214 image=self.image.id, flavor=self.flavor.id,
216 network=self.network.id if self.network else env.get(
218 timeout=self.create_server_timeout, wait=True, **kwargs)
219 self.__logger.debug("vm: %s", vm1)
222 def check_regex_in_console(self, name, regex=' login: ', loop=6):
223 """Wait for specific message in console
225 Returns: True or False on errors
228 for iloop in range(loop):
229 console = self.cloud.get_server_console(name)
230 self.__logger.debug("console: \n%s", console)
231 if re.search(regex, console):
233 "regex found: '%s' in console\n%s", regex, console)
236 "try %s: cannot find regex '%s' in console\n%s",
237 iloop + 1, regex, console)
239 self.__logger.error("cannot find regex '%s' in console", regex)
242 def clean_orphan_security_groups(self):
243 """Clean all security groups which are not owned by an existing tenant
245 It lists all orphan security groups in use as debug to avoid
246 misunderstanding the testcase results (it could happen if cloud admin
247 removes accounts without cleaning the virtual machines)
249 sec_groups = self.orig_cloud.list_security_groups()
250 for sec_group in sec_groups:
251 if not sec_group.tenant_id:
253 if not self.orig_cloud.get_project(sec_group.tenant_id):
254 self.__logger.debug("Cleaning security group %s", sec_group.id)
256 self.orig_cloud.delete_security_group(sec_group.id)
257 except Exception: # pylint: disable=broad-except
259 "Orphan security group %s in use", sec_group.id)
261 def count_hypervisors(self):
262 """Count hypervisors."""
263 if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false':
264 return len(self.orig_cloud.list_hypervisors())
265 return self.count_active_hypervisors()
267 def count_active_hypervisors(self):
268 """Count all hypervisors which are up."""
270 for hypervisor in self.orig_cloud.list_hypervisors():
271 if hypervisor['state'] == 'up':
274 self.__logger.warning(
275 "%s is down", hypervisor['hypervisor_hostname'])
278 def run(self, **kwargs):
281 Here are the main actions:
287 - TestCase.EX_RUN_ERROR on error
289 status = testcase.TestCase.EX_RUN_ERROR
292 assert super(VmReady1, self).run(
293 **kwargs) == testcase.TestCase.EX_OK
294 self.image = self.publish_image()
295 self.flavor = self.create_flavor()
297 status = testcase.TestCase.EX_OK
298 except Exception: # pylint: disable=broad-except
299 self.__logger.exception('Cannot run %s', self.case_name)
302 self.stop_time = time.time()
307 assert self.orig_cloud
309 super(VmReady1, self).clean()
311 self.cloud.delete_image(self.image.id)
313 self.orig_cloud.delete_flavor(self.flavor.id)
314 if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
315 self.clean_orphan_security_groups()
316 except Exception: # pylint: disable=broad-except
317 self.__logger.exception("Cannot clean all resources")
320 class VmReady2(VmReady1):
321 """Deploy a single VM reachable via ssh (scenario2)
323 It creates new user/project before creating and configuring all tenant
324 network resources, flavors, images, etc. required by advanced testcases.
326 It ensures that all testcases inheriting from SingleVm2 could work
327 without specific configurations (or at least read the same config data).
330 __logger = logging.getLogger(__name__)
332 def __init__(self, **kwargs):
333 if "case_name" not in kwargs:
334 kwargs["case_name"] = 'vmready2'
335 super(VmReady2, self).__init__(**kwargs)
337 assert self.orig_cloud
338 self.project = tenantnetwork.NewProject(
339 self.orig_cloud, self.case_name, self.guid)
340 self.project.create()
341 self.cloud = self.project.cloud
342 except Exception: # pylint: disable=broad-except
343 self.__logger.exception("Cannot create user or project")
349 super(VmReady2, self).clean()
352 except Exception: # pylint: disable=broad-except
353 self.__logger.exception("Cannot clean all resources")
356 class SingleVm1(VmReady1):
357 """Deploy a single VM reachable via ssh (scenario1)
359 It inherits from TenantNetwork1 which creates all network resources and
360 completes it by booting a VM attached to that network.
362 It ensures that all testcases inheriting from SingleVm1 could work
363 without specific configurations (or at least read the same config data).
365 # pylint: disable=too-many-instance-attributes
367 __logger = logging.getLogger(__name__)
369 ssh_connect_timeout = 1
370 ssh_connect_loops = 6
371 create_floating_ip_timeout = 120
372 check_console_loop = 6
373 check_console_regex = ' login: '
375 def __init__(self, **kwargs):
376 if "case_name" not in kwargs:
377 kwargs["case_name"] = 'singlevm1'
378 super(SingleVm1, self).__init__(**kwargs)
384 (_, self.key_filename) = tempfile.mkstemp()
387 """Create the security group and the keypair
389 It can be overriden to set other rules according to the services
392 Raises: Exception on error
395 self.keypair = self.cloud.create_keypair(
396 '{}-kp_{}'.format(self.case_name, self.guid))
397 self.__logger.debug("keypair: %s", self.keypair)
398 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
399 with open(self.key_filename, 'w') as private_key_file:
400 private_key_file.write(self.keypair.private_key)
401 self.sec = self.cloud.create_security_group(
402 '{}-sg_{}'.format(self.case_name, self.guid),
403 'created by OPNFV Functest ({})'.format(self.case_name))
404 self.cloud.create_security_group_rule(
405 self.sec.id, port_range_min='22', port_range_max='22',
406 protocol='tcp', direction='ingress')
407 self.cloud.create_security_group_rule(
408 self.sec.id, protocol='icmp', direction='ingress')
410 def connect(self, vm1):
411 """Connect to a virtual machine via ssh
413 It first adds a floating ip to the virtual machine and then establishes
422 if env.get('NO_TENANT_NETWORK').lower() != 'true':
423 fip = self.cloud.create_floating_ip(
424 network=self.ext_net.id, server=vm1, wait=True,
425 timeout=self.create_floating_ip_timeout)
426 self.__logger.debug("floating_ip: %s", fip)
427 ssh = paramiko.SSHClient()
428 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
429 for loop in range(self.ssh_connect_loops):
431 p_console = self.cloud.get_server_console(vm1)
432 self.__logger.debug("vm console: \n%s", p_console)
434 fip.floating_ip_address if fip else vm1.public_v4,
437 '{}_image_user'.format(self.case_name), self.username),
438 key_filename=self.key_filename,
441 '{}_vm_ssh_connect_timeout'.format(self.case_name),
442 self.ssh_connect_timeout))
444 except Exception as exc: # pylint: disable=broad-except
446 "try %s: cannot connect to %s: %s", loop + 1,
447 fip.floating_ip_address if fip else vm1.public_v4, exc)
451 "cannot connect to %s", fip.floating_ip_address)
456 """Say hello world via ssh
458 It can be overriden to execute any command.
460 Returns: echo exit codes
462 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
463 self.__logger.debug("output:\n%s", stdout.read().decode("utf-8"))
464 self.__logger.debug("error:\n%s", stderr.read().decode("utf-8"))
465 return stdout.channel.recv_exit_status()
467 def run(self, **kwargs):
470 Here are the main actions:
473 - create the security group
474 - execute the right command over ssh
478 - TestCase.EX_RUN_ERROR on error
480 status = testcase.TestCase.EX_RUN_ERROR
483 assert super(SingleVm1, self).run(
484 **kwargs) == testcase.TestCase.EX_OK
487 self.sshvm = self.boot_vm(
488 key_name=self.keypair.id, security_groups=[self.sec.id])
489 if self.check_regex_in_console(
490 self.sshvm.name, regex=self.check_console_regex,
491 loop=self.check_console_loop):
492 (self.fip, self.ssh) = self.connect(self.sshvm)
493 if not self.execute():
495 status = testcase.TestCase.EX_OK
496 except Exception: # pylint: disable=broad-except
497 self.__logger.exception('Cannot run %s', self.case_name)
499 self.stop_time = time.time()
504 assert self.orig_cloud
507 self.cloud.delete_floating_ip(self.fip.id)
509 self.cloud.delete_server(self.sshvm, wait=True)
511 self.cloud.delete_security_group(self.sec.id)
513 self.cloud.delete_keypair(self.keypair.name)
514 super(SingleVm1, self).clean()
515 except Exception: # pylint: disable=broad-except
516 self.__logger.exception("Cannot clean all resources")
519 class SingleVm2(SingleVm1):
520 """Deploy a single VM reachable via ssh (scenario2)
522 It creates new user/project before creating and configuring all tenant
523 network resources and vms required by advanced testcases.
525 It ensures that all testcases inheriting from SingleVm2 could work
526 without specific configurations (or at least read the same config data).
529 __logger = logging.getLogger(__name__)
531 def __init__(self, **kwargs):
532 if "case_name" not in kwargs:
533 kwargs["case_name"] = 'singlevm2'
534 super(SingleVm2, self).__init__(**kwargs)
536 assert self.orig_cloud
537 self.project = tenantnetwork.NewProject(
538 self.orig_cloud, self.case_name, self.guid)
539 self.project.create()
540 self.cloud = self.project.cloud
541 except Exception: # pylint: disable=broad-except
542 self.__logger.exception("Cannot create user or project")
548 super(SingleVm2, self).clean()
551 except Exception: # pylint: disable=broad-except
552 self.__logger.exception("Cannot clean all resources")