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.5.1-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().__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, f'{self.case_name}_extra_properties', {}))
84 image = self.cloud.create_image(
85 name if name else f'{self.case_name}-img_{self.guid}',
87 config.CONF, f'{self.case_name}_image',
89 meta=extra_properties,
91 config.CONF, f'{self.case_name}_image_format',
94 config.CONF, f'{self.case_name}_visibility',
97 self.__logger.debug("image: %s", image)
100 def publish_image_alt(self, name=None):
101 """Publish alternative image
103 It allows publishing multiple images for the child testcases. It forces
104 the same configuration for all subtestcases.
108 Raises: expection on error
111 extra_alt_properties = self.extra_alt_properties.copy()
112 if env.get('IMAGE_PROPERTIES'):
113 extra_alt_properties.update(
114 functest_utils.convert_ini_to_dict(
115 env.get('IMAGE_PROPERTIES')))
116 extra_alt_properties.update(
117 getattr(config.CONF, f'{self.case_name}_extra_alt_properties', {}))
118 image = self.cloud.create_image(
119 name if name else f'{self.case_name}-img_alt_{self.guid}',
121 config.CONF, f'{self.case_name}_image_alt',
123 meta=extra_alt_properties,
125 config.CONF, f'{self.case_name}_image_alt_format',
128 config.CONF, f'{self.case_name}_visibility',
131 self.__logger.debug("image: %s", image)
134 def create_flavor(self, name=None):
137 It allows creating multiple flavors for the child testcases. It forces
138 the same configuration for all subtestcases.
142 Raises: expection on error
144 assert self.orig_cloud
145 flavor = self.orig_cloud.create_flavor(
146 name if name else f'{self.case_name}-flavor_{self.guid}',
147 getattr(config.CONF, f'{self.case_name}_flavor_ram',
149 getattr(config.CONF, f'{self.case_name}_flavor_vcpus',
151 getattr(config.CONF, f'{self.case_name}_flavor_disk',
153 self.__logger.debug("flavor: %s", flavor)
154 flavor_extra_specs = self.flavor_extra_specs.copy()
155 if env.get('FLAVOR_EXTRA_SPECS'):
156 flavor_extra_specs.update(
157 functest_utils.convert_ini_to_dict(
158 env.get('FLAVOR_EXTRA_SPECS')))
159 flavor_extra_specs.update(
161 f'{self.case_name}_flavor_extra_specs', {}))
162 self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs)
165 def create_flavor_alt(self, name=None):
168 It allows creating multiple alt flavors for the child testcases. It
169 forces the same configuration for all subtestcases.
173 Raises: expection on error
175 assert self.orig_cloud
176 flavor = self.orig_cloud.create_flavor(
177 name if name else f'{self.case_name}-flavor_alt_{self.guid}',
178 getattr(config.CONF, f'{self.case_name}_flavor_alt_ram',
179 self.flavor_alt_ram),
180 getattr(config.CONF, f'{self.case_name}_flavor_alt_vcpus',
181 self.flavor_alt_vcpus),
182 getattr(config.CONF, f'{self.case_name}_flavor_alt_disk',
183 self.flavor_alt_disk))
184 self.__logger.debug("flavor: %s", flavor)
185 flavor_alt_extra_specs = self.flavor_alt_extra_specs.copy()
186 if env.get('FLAVOR_EXTRA_SPECS'):
187 flavor_alt_extra_specs.update(
188 functest_utils.convert_ini_to_dict(
189 env.get('FLAVOR_EXTRA_SPECS')))
190 flavor_alt_extra_specs.update(
192 f'{self.case_name}_flavor_alt_extra_specs', {}))
193 self.orig_cloud.set_flavor_specs(
194 flavor.id, flavor_alt_extra_specs)
197 def boot_vm(self, name=None, **kwargs):
198 """Boot the virtual machine
200 It allows booting multiple machines for the child testcases. It forces
201 the same configuration for all subtestcases.
205 Raises: expection on error
208 vm1 = self.cloud.create_server(
209 name if name else f'{self.case_name}-vm_{self.guid}',
210 image=self.image.id, flavor=self.flavor.id,
212 network=self.network.id if self.network else env.get(
214 timeout=self.create_server_timeout, wait=True, **kwargs)
215 self.__logger.debug("vm: %s", vm1)
218 def check_regex_in_console(self, name, regex=' login: ', loop=6):
219 """Wait for specific message in console
221 Returns: True or False on errors
224 for iloop in range(loop):
225 console = self.cloud.get_server_console(name)
226 self.__logger.debug("console: \n%s", console)
227 if re.search(regex, console):
229 "regex found: '%s' in console\n%s", regex, console)
232 "try %s: cannot find regex '%s' in console\n%s",
233 iloop + 1, regex, console)
235 self.__logger.error("cannot find regex '%s' in console", regex)
238 def clean_orphan_security_groups(self):
239 """Clean all security groups which are not owned by an existing tenant
241 It lists all orphan security groups in use as debug to avoid
242 misunderstanding the testcase results (it could happen if cloud admin
243 removes accounts without cleaning the virtual machines)
245 sec_groups = self.orig_cloud.list_security_groups()
246 for sec_group in sec_groups:
247 if not sec_group.tenant_id:
249 if not self.orig_cloud.get_project(sec_group.tenant_id):
250 self.__logger.debug("Cleaning security group %s", sec_group.id)
252 self.orig_cloud.delete_security_group(sec_group.id)
253 except Exception: # pylint: disable=broad-except
255 "Orphan security group %s in use", sec_group.id)
257 def count_hypervisors(self):
258 """Count hypervisors."""
259 if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false':
260 return len(self.orig_cloud.list_hypervisors())
261 return self.count_active_hypervisors()
263 def count_active_hypervisors(self):
264 """Count all hypervisors which are up."""
266 for hypervisor in self.orig_cloud.list_hypervisors():
267 if hypervisor['state'] == 'up':
270 self.__logger.warning(
271 "%s is down", hypervisor['hypervisor_hostname'])
274 def run(self, **kwargs):
277 Here are the main actions:
283 - TestCase.EX_RUN_ERROR on error
285 status = testcase.TestCase.EX_RUN_ERROR
289 **kwargs) == testcase.TestCase.EX_OK
290 self.image = self.publish_image()
291 self.flavor = self.create_flavor()
293 status = testcase.TestCase.EX_OK
294 except Exception: # pylint: disable=broad-except
295 self.__logger.exception('Cannot run %s', self.case_name)
298 self.stop_time = time.time()
303 assert self.orig_cloud
307 self.cloud.delete_image(self.image.id)
309 self.orig_cloud.delete_flavor(self.flavor.id)
310 if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
311 self.clean_orphan_security_groups()
312 except Exception: # pylint: disable=broad-except
313 self.__logger.exception("Cannot clean all resources")
316 class VmReady2(VmReady1):
317 """Deploy a single VM reachable via ssh (scenario2)
319 It creates new user/project before creating and configuring all tenant
320 network resources, flavors, images, etc. required by advanced testcases.
322 It ensures that all testcases inheriting from SingleVm2 could work
323 without specific configurations (or at least read the same config data).
326 __logger = logging.getLogger(__name__)
328 def __init__(self, **kwargs):
329 if "case_name" not in kwargs:
330 kwargs["case_name"] = 'vmready2'
331 super().__init__(**kwargs)
333 assert self.orig_cloud
334 self.project = tenantnetwork.NewProject(
335 self.orig_cloud, self.case_name, self.guid)
336 self.project.create()
337 self.cloud = self.project.cloud
338 except Exception: # pylint: disable=broad-except
339 self.__logger.exception("Cannot create user or project")
348 except Exception: # pylint: disable=broad-except
349 self.__logger.exception("Cannot clean all resources")
352 class SingleVm1(VmReady1):
353 """Deploy a single VM reachable via ssh (scenario1)
355 It inherits from TenantNetwork1 which creates all network resources and
356 completes it by booting a VM attached to that network.
358 It ensures that all testcases inheriting from SingleVm1 could work
359 without specific configurations (or at least read the same config data).
361 # pylint: disable=too-many-instance-attributes
363 __logger = logging.getLogger(__name__)
365 ssh_connect_timeout = 1
366 ssh_connect_loops = 6
367 create_floating_ip_timeout = 120
368 check_console_loop = 6
369 check_console_regex = ' login: '
371 def __init__(self, **kwargs):
372 if "case_name" not in kwargs:
373 kwargs["case_name"] = 'singlevm1'
374 super().__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 f'{self.case_name}-kp_{self.guid}')
393 self.__logger.debug("keypair: %s", self.keypair)
394 self.__logger.debug("private_key:\n%s", self.keypair.private_key)
396 self.key_filename, 'w', encoding='utf-8') as private_key_file:
397 private_key_file.write(self.keypair.private_key)
398 self.sec = self.cloud.create_security_group(
399 f'{self.case_name}-sg_{self.guid}',
400 f'created by OPNFV Functest ({self.case_name})')
401 self.cloud.create_security_group_rule(
402 self.sec.id, port_range_min='22', port_range_max='22',
403 protocol='tcp', direction='ingress')
404 self.cloud.create_security_group_rule(
405 self.sec.id, protocol='icmp', direction='ingress')
407 def connect(self, vm1):
408 """Connect to a virtual machine via ssh
410 It first adds a floating ip to the virtual machine and then establishes
419 if env.get('NO_TENANT_NETWORK').lower() != 'true':
420 fip = self.cloud.create_floating_ip(
421 network=self.ext_net.id, server=vm1, wait=True,
422 timeout=self.create_floating_ip_timeout)
423 self.__logger.debug("floating_ip: %s", fip)
424 ssh = paramiko.SSHClient()
425 ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
426 for loop in range(self.ssh_connect_loops):
428 p_console = self.cloud.get_server_console(vm1)
429 self.__logger.debug("vm console: \n%s", p_console)
431 fip.floating_ip_address if fip else vm1.public_v4,
434 f'{self.case_name}_image_user', self.username),
435 key_filename=self.key_filename,
438 f'{self.case_name}_vm_ssh_connect_timeout',
439 self.ssh_connect_timeout))
441 except Exception as exc: # pylint: disable=broad-except
443 "try %s: cannot connect to %s: %s", loop + 1,
444 fip.floating_ip_address if fip else vm1.public_v4, exc)
448 "cannot connect to %s", fip.floating_ip_address)
453 """Say hello world via ssh
455 It can be overriden to execute any command.
457 Returns: echo exit codes
459 (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
460 self.__logger.debug("output:\n%s", stdout.read().decode("utf-8"))
461 self.__logger.debug("error:\n%s", stderr.read().decode("utf-8"))
462 return stdout.channel.recv_exit_status()
464 def run(self, **kwargs):
467 Here are the main actions:
470 - create the security group
471 - execute the right command over ssh
475 - TestCase.EX_RUN_ERROR on error
477 status = testcase.TestCase.EX_RUN_ERROR
481 **kwargs) == testcase.TestCase.EX_OK
484 self.sshvm = self.boot_vm(
485 key_name=self.keypair.id, security_groups=[self.sec.id])
486 if self.check_regex_in_console(
487 self.sshvm.name, regex=self.check_console_regex,
488 loop=self.check_console_loop):
489 (self.fip, self.ssh) = self.connect(self.sshvm)
490 if not self.execute():
492 status = testcase.TestCase.EX_OK
493 except Exception: # pylint: disable=broad-except
494 self.__logger.exception('Cannot run %s', self.case_name)
496 self.stop_time = time.time()
501 assert self.orig_cloud
504 self.cloud.delete_floating_ip(self.fip.id)
506 self.cloud.delete_server(self.sshvm, wait=True)
508 self.cloud.delete_security_group(self.sec.id)
510 self.cloud.delete_keypair(self.keypair.name)
512 except Exception: # pylint: disable=broad-except
513 self.__logger.exception("Cannot clean all resources")
516 class SingleVm2(SingleVm1):
517 """Deploy a single VM reachable via ssh (scenario2)
519 It creates new user/project before creating and configuring all tenant
520 network resources and vms required by advanced testcases.
522 It ensures that all testcases inheriting from SingleVm2 could work
523 without specific configurations (or at least read the same config data).
526 __logger = logging.getLogger(__name__)
528 def __init__(self, **kwargs):
529 if "case_name" not in kwargs:
530 kwargs["case_name"] = 'singlevm2'
531 super().__init__(**kwargs)
533 assert self.orig_cloud
534 self.project = tenantnetwork.NewProject(
535 self.orig_cloud, self.case_name, self.guid)
536 self.project.create()
537 self.cloud = self.project.cloud
538 except Exception: # pylint: disable=broad-except
539 self.__logger.exception("Cannot create user or project")
548 except Exception: # pylint: disable=broad-except
549 self.__logger.exception("Cannot clean all resources")