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,
215 auto_ip=False, network=self.network.id,
216 timeout=self.create_server_timeout, wait=True, **kwargs)
217 self.__logger.debug("vm: %s", vm1)
220 def check_regex_in_console(self, name, regex=' login: ', loop=1):
221 """Wait for specific message in console
223 Returns: True or False on errors
226 for iloop in range(loop):
227 console = self.cloud.get_server_console(name)
228 self.__logger.debug("console: \n%s", console)
229 if re.search(regex, console):
231 "regex found: '%s' in console\n%s", regex, console)
234 "try %s: cannot find regex '%s' in console\n%s",
235 iloop + 1, regex, console)
237 self.__logger.error("cannot find regex '%s' in console", regex)
240 def clean_orphan_security_groups(self):
241 """Clean all security groups which are not owned by an existing tenant
243 It lists all orphan security groups in use as debug to avoid
244 misunderstanding the testcase results (it could happen if cloud admin
245 removes accounts without cleaning the virtual machines)
247 sec_groups = self.orig_cloud.list_security_groups()
248 for sec_group in sec_groups:
249 if not sec_group.tenant_id:
251 if not self.orig_cloud.get_project(sec_group.tenant_id):
252 self.__logger.debug("Cleaning security group %s", sec_group.id)
254 self.orig_cloud.delete_security_group(sec_group.id)
255 except Exception: # pylint: disable=broad-except
257 "Orphan security group %s in use", sec_group.id)
259 def count_hypervisors(self):
260 """Count hypervisors."""
261 if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false':
262 return len(self.orig_cloud.list_hypervisors())
263 return self.count_active_hypervisors()
265 def count_active_hypervisors(self):
266 """Count all hypervisors which are up."""
268 for hypervisor in self.orig_cloud.list_hypervisors():
269 if hypervisor['state'] == 'up':
272 self.__logger.warning(
273 "%s is down", hypervisor['hypervisor_hostname'])
276 def run(self, **kwargs):
279 Here are the main actions:
285 - TestCase.EX_RUN_ERROR on error
287 status = testcase.TestCase.EX_RUN_ERROR
290 assert super(VmReady1, self).run(
291 **kwargs) == testcase.TestCase.EX_OK
292 self.image = self.publish_image()
293 self.flavor = self.create_flavor()
295 status = testcase.TestCase.EX_OK
296 except Exception: # pylint: disable=broad-except
297 self.__logger.exception('Cannot run %s', self.case_name)
300 self.stop_time = time.time()
305 assert self.orig_cloud
307 super(VmReady1, self).clean()
309 self.cloud.delete_image(self.image.id)
311 self.orig_cloud.delete_flavor(self.flavor.id)
312 if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
313 self.clean_orphan_security_groups()
314 except Exception: # pylint: disable=broad-except
315 self.__logger.exception("Cannot clean all resources")
318 class VmReady2(VmReady1):
319 """Deploy a single VM reachable via ssh (scenario2)
321 It creates new user/project before creating and configuring all tenant
322 network resources, flavors, images, etc. required by advanced testcases.
324 It ensures that all testcases inheriting from SingleVm2 could work
325 without specific configurations (or at least read the same config data).
328 __logger = logging.getLogger(__name__)
330 def __init__(self, **kwargs):
331 if "case_name" not in kwargs:
332 kwargs["case_name"] = 'vmready2'
333 super(VmReady2, self).__init__(**kwargs)
335 assert self.orig_cloud
336 self.project = tenantnetwork.NewProject(
337 self.orig_cloud, self.case_name, self.guid)
338 self.project.create()
339 self.cloud = self.project.cloud
340 except Exception: # pylint: disable=broad-except
341 self.__logger.exception("Cannot create user or project")
347 super(VmReady2, self).clean()
350 except Exception: # pylint: disable=broad-except
351 self.__logger.exception("Cannot clean all resources")
354 class SingleVm1(VmReady1):
355 """Deploy a single VM reachable via ssh (scenario1)
357 It inherits from TenantNetwork1 which creates all network resources and
358 completes it by booting a VM attached to that network.
360 It ensures that all testcases inheriting from SingleVm1 could work
361 without specific configurations (or at least read the same config data).
363 # pylint: disable=too-many-instance-attributes
365 __logger = logging.getLogger(__name__)
367 ssh_connect_timeout = 1
368 ssh_connect_loops = 6
369 create_floating_ip_timeout = 120
371 def __init__(self, **kwargs):
372 if "case_name" not in kwargs:
373 kwargs["case_name"] = 'singlevm1'
374 super(SingleVm1, self).__init__(**kwargs)
380 (_, self.key_filename) = tempfile.mkstemp()
383 """Create the security group and the keypair
385 It can be overriden to set other rules according to the services
388 Raises: Exception on error
391 self.keypair = self.cloud.create_keypair(
392 '{}-kp_{}'.format(self.case_name, self.guid))
393 self.__logger.debug("keypair: %s", self.keypair)
394 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
395 with open(self.key_filename, 'w') as private_key_file:
396 private_key_file.write(self.keypair.private_key)
397 self.sec = self.cloud.create_security_group(
398 '{}-sg_{}'.format(self.case_name, self.guid),
399 'created by OPNFV Functest ({})'.format(self.case_name))
400 self.cloud.create_security_group_rule(
401 self.sec.id, port_range_min='22', port_range_max='22',
402 protocol='tcp', direction='ingress')
403 self.cloud.create_security_group_rule(
404 self.sec.id, protocol='icmp', direction='ingress')
406 def connect(self, vm1):
407 """Connect to a virtual machine via ssh
409 It first adds a floating ip to the virtual machine and then establishes
417 fip = self.cloud.create_floating_ip(
418 network=self.ext_net.id, server=vm1, wait=True,
419 timeout=self.create_floating_ip_timeout)
420 self.__logger.debug("floating_ip: %s", fip)
421 ssh = paramiko.SSHClient()
422 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
423 for loop in range(self.ssh_connect_loops):
425 p_console = self.cloud.get_server_console(vm1)
426 self.__logger.debug("vm console: \n%s", p_console)
428 fip.floating_ip_address,
431 '{}_image_user'.format(self.case_name), self.username),
432 key_filename=self.key_filename,
435 '{}_vm_ssh_connect_timeout'.format(self.case_name),
436 self.ssh_connect_timeout))
438 except Exception as exc: # pylint: disable=broad-except
440 "try %s: cannot connect to %s: %s", loop + 1,
441 fip.floating_ip_address, exc)
445 "cannot connect to %s", fip.floating_ip_address)
450 """Say hello world via ssh
452 It can be overriden to execute any command.
454 Returns: echo exit codes
456 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
457 self.__logger.debug("output:\n%s", stdout.read())
458 self.__logger.debug("error:\n%s", stderr.read())
459 return stdout.channel.recv_exit_status()
461 def run(self, **kwargs):
464 Here are the main actions:
467 - create the security group
468 - execute the right command over ssh
472 - TestCase.EX_RUN_ERROR on error
474 status = testcase.TestCase.EX_RUN_ERROR
477 assert super(SingleVm1, self).run(
478 **kwargs) == testcase.TestCase.EX_OK
481 self.sshvm = self.boot_vm(
482 key_name=self.keypair.id, security_groups=[self.sec.id])
483 (self.fip, self.ssh) = self.connect(self.sshvm)
484 if not self.execute():
486 status = testcase.TestCase.EX_OK
487 except Exception: # pylint: disable=broad-except
488 self.__logger.exception('Cannot run %s', self.case_name)
490 self.stop_time = time.time()
495 assert self.orig_cloud
498 self.cloud.delete_floating_ip(self.fip.id)
500 self.cloud.delete_server(self.sshvm, wait=True)
502 self.cloud.delete_security_group(self.sec.id)
504 self.cloud.delete_keypair(self.keypair.name)
505 super(SingleVm1, self).clean()
506 except Exception: # pylint: disable=broad-except
507 self.__logger.exception("Cannot clean all resources")
510 class SingleVm2(SingleVm1):
511 """Deploy a single VM reachable via ssh (scenario2)
513 It creates new user/project before creating and configuring all tenant
514 network resources and vms required by advanced testcases.
516 It ensures that all testcases inheriting from SingleVm2 could work
517 without specific configurations (or at least read the same config data).
520 __logger = logging.getLogger(__name__)
522 def __init__(self, **kwargs):
523 if "case_name" not in kwargs:
524 kwargs["case_name"] = 'singlevm2'
525 super(SingleVm2, self).__init__(**kwargs)
527 assert self.orig_cloud
528 self.project = tenantnetwork.NewProject(
529 self.orig_cloud, self.case_name, self.guid)
530 self.project.create()
531 self.cloud = self.project.cloud
532 except Exception: # pylint: disable=broad-except
533 self.__logger.exception("Cannot create user or project")
539 super(SingleVm2, self).clean()
542 except Exception: # pylint: disable=broad-except
543 self.__logger.exception("Cannot clean all resources")