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):
230 self.__logger.debug("regex found: ''%s' in console", regex)
234 "try %s: cannot find regex '%s' in console",
237 self.__logger.error("cannot find regex '%s' in console", regex)
240 def run(self, **kwargs):
243 Here are the main actions:
249 - TestCase.EX_RUN_ERROR on error
251 status = testcase.TestCase.EX_RUN_ERROR
254 assert super(VmReady1, self).run(
255 **kwargs) == testcase.TestCase.EX_OK
256 self.image = self.publish_image()
257 self.flavor = self.create_flavor()
259 status = testcase.TestCase.EX_OK
260 except Exception: # pylint: disable=broad-except
261 self.__logger.exception('Cannot run %s', self.case_name)
264 self.stop_time = time.time()
269 assert self.orig_cloud
271 super(VmReady1, self).clean()
273 self.cloud.delete_image(self.image.id)
275 self.orig_cloud.delete_flavor(self.flavor.id)
276 except Exception: # pylint: disable=broad-except
277 self.__logger.exception("Cannot clean all resources")
280 class VmReady2(VmReady1):
281 """Deploy a single VM reachable via ssh (scenario2)
283 It creates new user/project before creating and configuring all tenant
284 network resources, flavors, images, etc. required by advanced testcases.
286 It ensures that all testcases inheriting from SingleVm2 could work
287 without specific configurations (or at least read the same config data).
290 __logger = logging.getLogger(__name__)
292 def __init__(self, **kwargs):
293 if "case_name" not in kwargs:
294 kwargs["case_name"] = 'vmready2'
295 super(VmReady2, self).__init__(**kwargs)
297 assert self.orig_cloud
298 self.project = tenantnetwork.NewProject(
299 self.orig_cloud, self.case_name, self.guid)
300 self.project.create()
301 self.cloud = self.project.cloud
302 except Exception: # pylint: disable=broad-except
303 self.__logger.exception("Cannot create user or project")
309 super(VmReady2, self).clean()
312 except Exception: # pylint: disable=broad-except
313 self.__logger.exception("Cannot clean all resources")
316 class SingleVm1(VmReady1):
317 """Deploy a single VM reachable via ssh (scenario1)
319 It inherits from TenantNetwork1 which creates all network resources and
320 completes it by booting a VM attached to that network.
322 It ensures that all testcases inheriting from SingleVm1 could work
323 without specific configurations (or at least read the same config data).
325 # pylint: disable=too-many-instance-attributes
327 __logger = logging.getLogger(__name__)
329 ssh_connect_timeout = 1
330 ssh_connect_loops = 6
331 create_floating_ip_timeout = 120
333 def __init__(self, **kwargs):
334 if "case_name" not in kwargs:
335 kwargs["case_name"] = 'singlevm1'
336 super(SingleVm1, self).__init__(**kwargs)
342 (_, self.key_filename) = tempfile.mkstemp()
345 """Create the security group and the keypair
347 It can be overriden to set other rules according to the services
350 Raises: Exception on error
353 self.keypair = self.cloud.create_keypair(
354 '{}-kp_{}'.format(self.case_name, self.guid))
355 self.__logger.debug("keypair: %s", self.keypair)
356 self.__logger.debug("private_key: %s", self.keypair.private_key)
357 with open(self.key_filename, 'w') as private_key_file:
358 private_key_file.write(self.keypair.private_key)
359 self.sec = self.cloud.create_security_group(
360 '{}-sg_{}'.format(self.case_name, self.guid),
361 'created by OPNFV Functest ({})'.format(self.case_name))
362 self.cloud.create_security_group_rule(
363 self.sec.id, port_range_min='22', port_range_max='22',
364 protocol='tcp', direction='ingress')
365 self.cloud.create_security_group_rule(
366 self.sec.id, protocol='icmp', direction='ingress')
368 def connect(self, vm1):
369 """Connect to a virtual machine via ssh
371 It first adds a floating ip to the virtual machine and then establishes
379 fip = self.cloud.create_floating_ip(
380 network=self.ext_net.id, server=vm1, wait=True,
381 timeout=self.create_floating_ip_timeout)
382 self.__logger.debug("floating_ip: %s", fip)
383 ssh = paramiko.SSHClient()
384 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
385 for loop in range(self.ssh_connect_loops):
387 p_console = self.cloud.get_server_console(vm1)
388 self.__logger.debug("vm console: \n%s", p_console)
390 fip.floating_ip_address,
393 '{}_image_user'.format(self.case_name), self.username),
394 key_filename=self.key_filename,
397 '{}_vm_ssh_connect_timeout'.format(self.case_name),
398 self.ssh_connect_timeout))
400 except Exception as exc: # pylint: disable=broad-except
402 "try %s: cannot connect to %s: %s", loop + 1,
403 fip.floating_ip_address, exc)
407 "cannot connect to %s", fip.floating_ip_address)
412 """Say hello world via ssh
414 It can be overriden to execute any command.
416 Returns: echo exit codes
418 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
419 self.__logger.debug("output:\n%s", stdout.read())
420 self.__logger.debug("error:\n%s", stderr.read())
421 return stdout.channel.recv_exit_status()
423 def run(self, **kwargs):
426 Here are the main actions:
429 - create the security group
430 - execute the right command over ssh
434 - TestCase.EX_RUN_ERROR on error
436 status = testcase.TestCase.EX_RUN_ERROR
439 assert super(SingleVm1, self).run(
440 **kwargs) == testcase.TestCase.EX_OK
443 self.sshvm = self.boot_vm(
444 key_name=self.keypair.id, security_groups=[self.sec.id])
445 (self.fip, self.ssh) = self.connect(self.sshvm)
446 if not self.execute():
448 status = testcase.TestCase.EX_OK
449 except Exception: # pylint: disable=broad-except
450 self.__logger.exception('Cannot run %s', self.case_name)
452 self.stop_time = time.time()
457 assert self.orig_cloud
460 self.cloud.delete_floating_ip(self.fip.id)
462 self.cloud.delete_server(self.sshvm, wait=True)
464 self.cloud.delete_security_group(self.sec.id)
466 self.cloud.delete_keypair(self.keypair.name)
467 super(SingleVm1, self).clean()
468 except Exception: # pylint: disable=broad-except
469 self.__logger.exception("Cannot clean all resources")
472 class SingleVm2(SingleVm1):
473 """Deploy a single VM reachable via ssh (scenario2)
475 It creates new user/project before creating and configuring all tenant
476 network resources and vms required by advanced testcases.
478 It ensures that all testcases inheriting from SingleVm2 could work
479 without specific configurations (or at least read the same config data).
482 __logger = logging.getLogger(__name__)
484 def __init__(self, **kwargs):
485 if "case_name" not in kwargs:
486 kwargs["case_name"] = 'singlevm2'
487 super(SingleVm2, self).__init__(**kwargs)
489 assert self.orig_cloud
490 self.project = tenantnetwork.NewProject(
491 self.orig_cloud, self.case_name, self.guid)
492 self.project.create()
493 self.cloud = self.project.cloud
494 except Exception: # pylint: disable=broad-except
495 self.__logger.exception("Cannot create user or project")
501 super(SingleVm2, self).clean()
504 except Exception: # pylint: disable=broad-except
505 self.__logger.exception("Cannot clean all resources")