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 run(self, **kwargs):
262 Here are the main actions:
268 - TestCase.EX_RUN_ERROR on error
270 status = testcase.TestCase.EX_RUN_ERROR
273 assert super(VmReady1, self).run(
274 **kwargs) == testcase.TestCase.EX_OK
275 self.image = self.publish_image()
276 self.flavor = self.create_flavor()
278 status = testcase.TestCase.EX_OK
279 except Exception: # pylint: disable=broad-except
280 self.__logger.exception('Cannot run %s', self.case_name)
283 self.stop_time = time.time()
288 assert self.orig_cloud
290 super(VmReady1, self).clean()
292 self.cloud.delete_image(self.image.id)
294 self.orig_cloud.delete_flavor(self.flavor.id)
295 if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
296 self.clean_orphan_security_groups()
297 except Exception: # pylint: disable=broad-except
298 self.__logger.exception("Cannot clean all resources")
301 class VmReady2(VmReady1):
302 """Deploy a single VM reachable via ssh (scenario2)
304 It creates new user/project before creating and configuring all tenant
305 network resources, flavors, images, etc. required by advanced testcases.
307 It ensures that all testcases inheriting from SingleVm2 could work
308 without specific configurations (or at least read the same config data).
311 __logger = logging.getLogger(__name__)
313 def __init__(self, **kwargs):
314 if "case_name" not in kwargs:
315 kwargs["case_name"] = 'vmready2'
316 super(VmReady2, self).__init__(**kwargs)
318 assert self.orig_cloud
319 self.project = tenantnetwork.NewProject(
320 self.orig_cloud, self.case_name, self.guid)
321 self.project.create()
322 self.cloud = self.project.cloud
323 except Exception: # pylint: disable=broad-except
324 self.__logger.exception("Cannot create user or project")
330 super(VmReady2, self).clean()
333 except Exception: # pylint: disable=broad-except
334 self.__logger.exception("Cannot clean all resources")
337 class SingleVm1(VmReady1):
338 """Deploy a single VM reachable via ssh (scenario1)
340 It inherits from TenantNetwork1 which creates all network resources and
341 completes it by booting a VM attached to that network.
343 It ensures that all testcases inheriting from SingleVm1 could work
344 without specific configurations (or at least read the same config data).
346 # pylint: disable=too-many-instance-attributes
348 __logger = logging.getLogger(__name__)
350 ssh_connect_timeout = 1
351 ssh_connect_loops = 6
352 create_floating_ip_timeout = 120
354 def __init__(self, **kwargs):
355 if "case_name" not in kwargs:
356 kwargs["case_name"] = 'singlevm1'
357 super(SingleVm1, self).__init__(**kwargs)
363 (_, self.key_filename) = tempfile.mkstemp()
366 """Create the security group and the keypair
368 It can be overriden to set other rules according to the services
371 Raises: Exception on error
374 self.keypair = self.cloud.create_keypair(
375 '{}-kp_{}'.format(self.case_name, self.guid))
376 self.__logger.debug("keypair: %s", self.keypair)
377 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
378 with open(self.key_filename, 'w') as private_key_file:
379 private_key_file.write(self.keypair.private_key)
380 self.sec = self.cloud.create_security_group(
381 '{}-sg_{}'.format(self.case_name, self.guid),
382 'created by OPNFV Functest ({})'.format(self.case_name))
383 self.cloud.create_security_group_rule(
384 self.sec.id, port_range_min='22', port_range_max='22',
385 protocol='tcp', direction='ingress')
386 self.cloud.create_security_group_rule(
387 self.sec.id, protocol='icmp', direction='ingress')
389 def connect(self, vm1):
390 """Connect to a virtual machine via ssh
392 It first adds a floating ip to the virtual machine and then establishes
400 fip = self.cloud.create_floating_ip(
401 network=self.ext_net.id, server=vm1, wait=True,
402 timeout=self.create_floating_ip_timeout)
403 self.__logger.debug("floating_ip: %s", fip)
404 ssh = paramiko.SSHClient()
405 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
406 for loop in range(self.ssh_connect_loops):
408 p_console = self.cloud.get_server_console(vm1)
409 self.__logger.debug("vm console: \n%s", p_console)
411 fip.floating_ip_address,
414 '{}_image_user'.format(self.case_name), self.username),
415 key_filename=self.key_filename,
418 '{}_vm_ssh_connect_timeout'.format(self.case_name),
419 self.ssh_connect_timeout))
421 except Exception as exc: # pylint: disable=broad-except
423 "try %s: cannot connect to %s: %s", loop + 1,
424 fip.floating_ip_address, exc)
428 "cannot connect to %s", fip.floating_ip_address)
433 """Say hello world via ssh
435 It can be overriden to execute any command.
437 Returns: echo exit codes
439 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
440 self.__logger.debug("output:\n%s", stdout.read())
441 self.__logger.debug("error:\n%s", stderr.read())
442 return stdout.channel.recv_exit_status()
444 def run(self, **kwargs):
447 Here are the main actions:
450 - create the security group
451 - execute the right command over ssh
455 - TestCase.EX_RUN_ERROR on error
457 status = testcase.TestCase.EX_RUN_ERROR
460 assert super(SingleVm1, self).run(
461 **kwargs) == testcase.TestCase.EX_OK
464 self.sshvm = self.boot_vm(
465 key_name=self.keypair.id, security_groups=[self.sec.id])
466 (self.fip, self.ssh) = self.connect(self.sshvm)
467 if not self.execute():
469 status = testcase.TestCase.EX_OK
470 except Exception: # pylint: disable=broad-except
471 self.__logger.exception('Cannot run %s', self.case_name)
473 self.stop_time = time.time()
478 assert self.orig_cloud
481 self.cloud.delete_floating_ip(self.fip.id)
483 self.cloud.delete_server(self.sshvm, wait=True)
485 self.cloud.delete_security_group(self.sec.id)
487 self.cloud.delete_keypair(self.keypair.name)
488 super(SingleVm1, self).clean()
489 except Exception: # pylint: disable=broad-except
490 self.__logger.exception("Cannot clean all resources")
493 class SingleVm2(SingleVm1):
494 """Deploy a single VM reachable via ssh (scenario2)
496 It creates new user/project before creating and configuring all tenant
497 network resources and vms required by advanced testcases.
499 It ensures that all testcases inheriting from SingleVm2 could work
500 without specific configurations (or at least read the same config data).
503 __logger = logging.getLogger(__name__)
505 def __init__(self, **kwargs):
506 if "case_name" not in kwargs:
507 kwargs["case_name"] = 'singlevm2'
508 super(SingleVm2, self).__init__(**kwargs)
510 assert self.orig_cloud
511 self.project = tenantnetwork.NewProject(
512 self.orig_cloud, self.case_name, self.guid)
513 self.project.create()
514 self.cloud = self.project.cloud
515 except Exception: # pylint: disable=broad-except
516 self.__logger.exception("Cannot create user or project")
522 super(SingleVm2, self).clean()
525 except Exception: # pylint: disable=broad-except
526 self.__logger.exception("Cannot clean all resources")