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_active_hypervisors(self):
260 """Count all hypervisors which are up."""
262 for hypervisor in self.orig_cloud.list_hypervisors():
263 if hypervisor['state'] == 'up':
267 def run(self, **kwargs):
270 Here are the main actions:
276 - TestCase.EX_RUN_ERROR on error
278 status = testcase.TestCase.EX_RUN_ERROR
281 assert super(VmReady1, self).run(
282 **kwargs) == testcase.TestCase.EX_OK
283 self.image = self.publish_image()
284 self.flavor = self.create_flavor()
286 status = testcase.TestCase.EX_OK
287 except Exception: # pylint: disable=broad-except
288 self.__logger.exception('Cannot run %s', self.case_name)
291 self.stop_time = time.time()
296 assert self.orig_cloud
298 super(VmReady1, self).clean()
300 self.cloud.delete_image(self.image.id)
302 self.orig_cloud.delete_flavor(self.flavor.id)
303 if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
304 self.clean_orphan_security_groups()
305 except Exception: # pylint: disable=broad-except
306 self.__logger.exception("Cannot clean all resources")
309 class VmReady2(VmReady1):
310 """Deploy a single VM reachable via ssh (scenario2)
312 It creates new user/project before creating and configuring all tenant
313 network resources, flavors, images, etc. required by advanced testcases.
315 It ensures that all testcases inheriting from SingleVm2 could work
316 without specific configurations (or at least read the same config data).
319 __logger = logging.getLogger(__name__)
321 def __init__(self, **kwargs):
322 if "case_name" not in kwargs:
323 kwargs["case_name"] = 'vmready2'
324 super(VmReady2, self).__init__(**kwargs)
326 assert self.orig_cloud
327 self.project = tenantnetwork.NewProject(
328 self.orig_cloud, self.case_name, self.guid)
329 self.project.create()
330 self.cloud = self.project.cloud
331 except Exception: # pylint: disable=broad-except
332 self.__logger.exception("Cannot create user or project")
338 super(VmReady2, self).clean()
341 except Exception: # pylint: disable=broad-except
342 self.__logger.exception("Cannot clean all resources")
345 class SingleVm1(VmReady1):
346 """Deploy a single VM reachable via ssh (scenario1)
348 It inherits from TenantNetwork1 which creates all network resources and
349 completes it by booting a VM attached to that network.
351 It ensures that all testcases inheriting from SingleVm1 could work
352 without specific configurations (or at least read the same config data).
354 # pylint: disable=too-many-instance-attributes
356 __logger = logging.getLogger(__name__)
358 ssh_connect_timeout = 1
359 ssh_connect_loops = 6
360 create_floating_ip_timeout = 120
362 def __init__(self, **kwargs):
363 if "case_name" not in kwargs:
364 kwargs["case_name"] = 'singlevm1'
365 super(SingleVm1, self).__init__(**kwargs)
371 (_, self.key_filename) = tempfile.mkstemp()
374 """Create the security group and the keypair
376 It can be overriden to set other rules according to the services
379 Raises: Exception on error
382 self.keypair = self.cloud.create_keypair(
383 '{}-kp_{}'.format(self.case_name, self.guid))
384 self.__logger.debug("keypair: %s", self.keypair)
385 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
386 with open(self.key_filename, 'w') as private_key_file:
387 private_key_file.write(self.keypair.private_key)
388 self.sec = self.cloud.create_security_group(
389 '{}-sg_{}'.format(self.case_name, self.guid),
390 'created by OPNFV Functest ({})'.format(self.case_name))
391 self.cloud.create_security_group_rule(
392 self.sec.id, port_range_min='22', port_range_max='22',
393 protocol='tcp', direction='ingress')
394 self.cloud.create_security_group_rule(
395 self.sec.id, protocol='icmp', direction='ingress')
397 def connect(self, vm1):
398 """Connect to a virtual machine via ssh
400 It first adds a floating ip to the virtual machine and then establishes
408 fip = self.cloud.create_floating_ip(
409 network=self.ext_net.id, server=vm1, wait=True,
410 timeout=self.create_floating_ip_timeout)
411 self.__logger.debug("floating_ip: %s", fip)
412 ssh = paramiko.SSHClient()
413 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
414 for loop in range(self.ssh_connect_loops):
416 p_console = self.cloud.get_server_console(vm1)
417 self.__logger.debug("vm console: \n%s", p_console)
419 fip.floating_ip_address,
422 '{}_image_user'.format(self.case_name), self.username),
423 key_filename=self.key_filename,
426 '{}_vm_ssh_connect_timeout'.format(self.case_name),
427 self.ssh_connect_timeout))
429 except Exception as exc: # pylint: disable=broad-except
431 "try %s: cannot connect to %s: %s", loop + 1,
432 fip.floating_ip_address, exc)
436 "cannot connect to %s", fip.floating_ip_address)
441 """Say hello world via ssh
443 It can be overriden to execute any command.
445 Returns: echo exit codes
447 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
448 self.__logger.debug("output:\n%s", stdout.read())
449 self.__logger.debug("error:\n%s", stderr.read())
450 return stdout.channel.recv_exit_status()
452 def run(self, **kwargs):
455 Here are the main actions:
458 - create the security group
459 - execute the right command over ssh
463 - TestCase.EX_RUN_ERROR on error
465 status = testcase.TestCase.EX_RUN_ERROR
468 assert super(SingleVm1, self).run(
469 **kwargs) == testcase.TestCase.EX_OK
472 self.sshvm = self.boot_vm(
473 key_name=self.keypair.id, security_groups=[self.sec.id])
474 (self.fip, self.ssh) = self.connect(self.sshvm)
475 if not self.execute():
477 status = testcase.TestCase.EX_OK
478 except Exception: # pylint: disable=broad-except
479 self.__logger.exception('Cannot run %s', self.case_name)
481 self.stop_time = time.time()
486 assert self.orig_cloud
489 self.cloud.delete_floating_ip(self.fip.id)
491 self.cloud.delete_server(self.sshvm, wait=True)
493 self.cloud.delete_security_group(self.sec.id)
495 self.cloud.delete_keypair(self.keypair.name)
496 super(SingleVm1, self).clean()
497 except Exception: # pylint: disable=broad-except
498 self.__logger.exception("Cannot clean all resources")
501 class SingleVm2(SingleVm1):
502 """Deploy a single VM reachable via ssh (scenario2)
504 It creates new user/project before creating and configuring all tenant
505 network resources and vms required by advanced testcases.
507 It ensures that all testcases inheriting from SingleVm2 could work
508 without specific configurations (or at least read the same config data).
511 __logger = logging.getLogger(__name__)
513 def __init__(self, **kwargs):
514 if "case_name" not in kwargs:
515 kwargs["case_name"] = 'singlevm2'
516 super(SingleVm2, self).__init__(**kwargs)
518 assert self.orig_cloud
519 self.project = tenantnetwork.NewProject(
520 self.orig_cloud, self.case_name, self.guid)
521 self.project.create()
522 self.cloud = self.project.cloud
523 except Exception: # pylint: disable=broad-except
524 self.__logger.exception("Cannot create user or project")
530 super(SingleVm2, self).clean()
533 except Exception: # pylint: disable=broad-except
534 self.__logger.exception("Cannot clean all resources")