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=6):
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
370 check_console_loop = 6
371 check_console_regex = ' login: '
373 def __init__(self, **kwargs):
374 if "case_name" not in kwargs:
375 kwargs["case_name"] = 'singlevm1'
376 super(SingleVm1, self).__init__(**kwargs)
382 (_, self.key_filename) = tempfile.mkstemp()
385 """Create the security group and the keypair
387 It can be overriden to set other rules according to the services
390 Raises: Exception on error
393 self.keypair = self.cloud.create_keypair(
394 '{}-kp_{}'.format(self.case_name, self.guid))
395 self.__logger.debug("keypair: %s", self.keypair)
396 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
397 with open(self.key_filename, 'w') as private_key_file:
398 private_key_file.write(self.keypair.private_key)
399 self.sec = self.cloud.create_security_group(
400 '{}-sg_{}'.format(self.case_name, self.guid),
401 'created by OPNFV Functest ({})'.format(self.case_name))
402 self.cloud.create_security_group_rule(
403 self.sec.id, port_range_min='22', port_range_max='22',
404 protocol='tcp', direction='ingress')
405 self.cloud.create_security_group_rule(
406 self.sec.id, protocol='icmp', direction='ingress')
408 def connect(self, vm1):
409 """Connect to a virtual machine via ssh
411 It first adds a floating ip to the virtual machine and then establishes
419 fip = self.cloud.create_floating_ip(
420 network=self.ext_net.id, server=vm1, wait=True,
421 timeout=self.create_floating_ip_timeout)
422 self.__logger.debug("floating_ip: %s", fip)
423 ssh = paramiko.SSHClient()
424 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
425 for loop in range(self.ssh_connect_loops):
427 p_console = self.cloud.get_server_console(vm1)
428 self.__logger.debug("vm console: \n%s", p_console)
430 fip.floating_ip_address,
433 '{}_image_user'.format(self.case_name), self.username),
434 key_filename=self.key_filename,
437 '{}_vm_ssh_connect_timeout'.format(self.case_name),
438 self.ssh_connect_timeout))
440 except Exception as exc: # pylint: disable=broad-except
442 "try %s: cannot connect to %s: %s", loop + 1,
443 fip.floating_ip_address, exc)
447 "cannot connect to %s", fip.floating_ip_address)
452 """Say hello world via ssh
454 It can be overriden to execute any command.
456 Returns: echo exit codes
458 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
459 self.__logger.debug("output:\n%s", stdout.read().decode("utf-8"))
460 self.__logger.debug("error:\n%s", stderr.read().decode("utf-8"))
461 return stdout.channel.recv_exit_status()
463 def run(self, **kwargs):
466 Here are the main actions:
469 - create the security group
470 - execute the right command over ssh
474 - TestCase.EX_RUN_ERROR on error
476 status = testcase.TestCase.EX_RUN_ERROR
479 assert super(SingleVm1, self).run(
480 **kwargs) == testcase.TestCase.EX_OK
483 self.sshvm = self.boot_vm(
484 key_name=self.keypair.id, security_groups=[self.sec.id])
485 if self.check_regex_in_console(
486 self.sshvm.name, regex=self.check_console_regex,
487 loop=self.check_console_loop):
488 (self.fip, self.ssh) = self.connect(self.sshvm)
489 if not self.execute():
491 status = testcase.TestCase.EX_OK
492 except Exception: # pylint: disable=broad-except
493 self.__logger.exception('Cannot run %s', self.case_name)
495 self.stop_time = time.time()
500 assert self.orig_cloud
503 self.cloud.delete_floating_ip(self.fip.id)
505 self.cloud.delete_server(self.sshvm, wait=True)
507 self.cloud.delete_security_group(self.sec.id)
509 self.cloud.delete_keypair(self.keypair.name)
510 super(SingleVm1, self).clean()
511 except Exception: # pylint: disable=broad-except
512 self.__logger.exception("Cannot clean all resources")
515 class SingleVm2(SingleVm1):
516 """Deploy a single VM reachable via ssh (scenario2)
518 It creates new user/project before creating and configuring all tenant
519 network resources and vms required by advanced testcases.
521 It ensures that all testcases inheriting from SingleVm2 could work
522 without specific configurations (or at least read the same config data).
525 __logger = logging.getLogger(__name__)
527 def __init__(self, **kwargs):
528 if "case_name" not in kwargs:
529 kwargs["case_name"] = 'singlevm2'
530 super(SingleVm2, self).__init__(**kwargs)
532 assert self.orig_cloud
533 self.project = tenantnetwork.NewProject(
534 self.orig_cloud, self.case_name, self.guid)
535 self.project.create()
536 self.cloud = self.project.cloud
537 except Exception: # pylint: disable=broad-except
538 self.__logger.exception("Cannot create user or project")
544 super(SingleVm2, self).clean()
547 except Exception: # pylint: disable=broad-except
548 self.__logger.exception("Cannot clean all resources")