59071112e52bfddb925e0559b81558e44416740d
[functest.git] / functest / core / singlevm.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2018 Orange and others.
4 #
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
9
10 """Ease deploying a single VM reachable via ssh
11
12 It offers a simple way to create all tenant network resources + a VM for
13 advanced testcases (e.g. deploying an orchestrator).
14 """
15
16 import logging
17 import re
18 import tempfile
19 import time
20
21 import paramiko
22 from xtesting.core import testcase
23
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
28
29
30 class VmReady1(tenantnetwork.TenantNetwork1):
31     """Prepare a single VM (scenario1)
32
33     It inherits from TenantNetwork1 which creates all network resources and
34     prepares a future VM attached to that network.
35
36     It ensures that all testcases inheriting from SingleVm1 could work
37     without specific configurations (or at least read the same config data).
38     """
39     # pylint: disable=too-many-instance-attributes
40
41     __logger = logging.getLogger(__name__)
42     filename = '/home/opnfv/functest/images/cirros-0.5.1-x86_64-disk.img'
43     image_format = 'qcow2'
44     extra_properties = {}
45     filename_alt = filename
46     image_alt_format = image_format
47     extra_alt_properties = extra_properties
48     visibility = 'private'
49     flavor_ram = 512
50     flavor_vcpus = 1
51     flavor_disk = 1
52     flavor_extra_specs = {}
53     flavor_alt_ram = 1024
54     flavor_alt_vcpus = 1
55     flavor_alt_disk = 1
56     flavor_alt_extra_specs = flavor_extra_specs
57     create_server_timeout = 180
58
59     def __init__(self, **kwargs):
60         if "case_name" not in kwargs:
61             kwargs["case_name"] = 'vmready1'
62         super(VmReady1, self).__init__(**kwargs)
63         self.image = None
64         self.flavor = None
65
66     def publish_image(self, name=None):
67         """Publish image
68
69         It allows publishing multiple images for the child testcases. It forces
70         the same configuration for all subtestcases.
71
72         Returns: image
73
74         Raises: expection on error
75         """
76         assert self.cloud
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(
84                 self.case_name), {}))
85         image = self.cloud.create_image(
86             name if name else '{}-img_{}'.format(self.case_name, self.guid),
87             filename=getattr(
88                 config.CONF, '{}_image'.format(self.case_name),
89                 self.filename),
90             meta=extra_properties,
91             disk_format=getattr(
92                 config.CONF, '{}_image_format'.format(self.case_name),
93                 self.image_format),
94             visibility=getattr(
95                 config.CONF, '{}_visibility'.format(self.case_name),
96                 self.visibility),
97             wait=True)
98         self.__logger.debug("image: %s", image)
99         return image
100
101     def publish_image_alt(self, name=None):
102         """Publish alternative image
103
104         It allows publishing multiple images for the child testcases. It forces
105         the same configuration for all subtestcases.
106
107         Returns: image
108
109         Raises: expection on error
110         """
111         assert self.cloud
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),
123             filename=getattr(
124                 config.CONF, '{}_image_alt'.format(self.case_name),
125                 self.filename_alt),
126             meta=extra_alt_properties,
127             disk_format=getattr(
128                 config.CONF, '{}_image_alt_format'.format(self.case_name),
129                 self.image_format),
130             visibility=getattr(
131                 config.CONF, '{}_visibility'.format(self.case_name),
132                 self.visibility),
133             wait=True)
134         self.__logger.debug("image: %s", image)
135         return image
136
137     def create_flavor(self, name=None):
138         """Create flavor
139
140         It allows creating multiple flavors for the child testcases. It forces
141         the same configuration for all subtestcases.
142
143         Returns: flavor
144
145         Raises: expection on error
146         """
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),
151                     self.flavor_ram),
152             getattr(config.CONF, '{}_flavor_vcpus'.format(self.case_name),
153                     self.flavor_vcpus),
154             getattr(config.CONF, '{}_flavor_disk'.format(self.case_name),
155                     self.flavor_disk))
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(
163             getattr(config.CONF,
164                     '{}_flavor_extra_specs'.format(self.case_name), {}))
165         self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs)
166         return flavor
167
168     def create_flavor_alt(self, name=None):
169         """Create flavor
170
171         It allows creating multiple alt flavors for the child testcases. It
172         forces the same configuration for all subtestcases.
173
174         Returns: flavor
175
176         Raises: expection on error
177         """
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(
195             getattr(config.CONF,
196                     '{}_flavor_alt_extra_specs'.format(self.case_name), {}))
197         self.orig_cloud.set_flavor_specs(
198             flavor.id, flavor_alt_extra_specs)
199         return flavor
200
201     def boot_vm(self, name=None, **kwargs):
202         """Boot the virtual machine
203
204         It allows booting multiple machines for the child testcases. It forces
205         the same configuration for all subtestcases.
206
207         Returns: vm
208
209         Raises: expection on error
210         """
211         assert self.cloud
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,
216             network=self.network.id if self.network else env.get(
217                 "EXTERNAL_NETWORK"),
218             timeout=self.create_server_timeout, wait=True, **kwargs)
219         self.__logger.debug("vm: %s", vm1)
220         return vm1
221
222     def check_regex_in_console(self, name, regex=' login: ', loop=6):
223         """Wait for specific message in console
224
225         Returns: True or False on errors
226         """
227         assert self.cloud
228         for iloop in range(loop):
229             console = self.cloud.get_server_console(name)
230             self.__logger.debug("console: \n%s", console)
231             if re.search(regex, console):
232                 self.__logger.debug(
233                     "regex found: '%s' in console\n%s", regex, console)
234                 return True
235             self.__logger.debug(
236                 "try %s: cannot find regex '%s' in console\n%s",
237                 iloop + 1, regex, console)
238             time.sleep(10)
239         self.__logger.error("cannot find regex '%s' in console", regex)
240         return False
241
242     def clean_orphan_security_groups(self):
243         """Clean all security groups which are not owned by an existing tenant
244
245         It lists all orphan security groups in use as debug to avoid
246         misunderstanding the testcase results (it could happen if cloud admin
247         removes accounts without cleaning the virtual machines)
248         """
249         sec_groups = self.orig_cloud.list_security_groups()
250         for sec_group in sec_groups:
251             if not sec_group.tenant_id:
252                 continue
253             if not self.orig_cloud.get_project(sec_group.tenant_id):
254                 self.__logger.debug("Cleaning security group %s", sec_group.id)
255                 try:
256                     self.orig_cloud.delete_security_group(sec_group.id)
257                 except Exception:  # pylint: disable=broad-except
258                     self.__logger.debug(
259                         "Orphan security group %s in use", sec_group.id)
260
261     def count_hypervisors(self):
262         """Count hypervisors."""
263         if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false':
264             return len(self.orig_cloud.list_hypervisors())
265         return self.count_active_hypervisors()
266
267     def count_active_hypervisors(self):
268         """Count all hypervisors which are up."""
269         compute_cnt = 0
270         for hypervisor in self.orig_cloud.list_hypervisors():
271             if hypervisor['state'] == 'up':
272                 compute_cnt += 1
273             else:
274                 self.__logger.warning(
275                     "%s is down", hypervisor['hypervisor_hostname'])
276         return compute_cnt
277
278     def run(self, **kwargs):
279         """Boot the new VM
280
281         Here are the main actions:
282         - publish the image
283         - create the flavor
284
285         Returns:
286         - TestCase.EX_OK
287         - TestCase.EX_RUN_ERROR on error
288         """
289         status = testcase.TestCase.EX_RUN_ERROR
290         try:
291             assert self.cloud
292             assert super(VmReady1, self).run(
293                 **kwargs) == testcase.TestCase.EX_OK
294             self.image = self.publish_image()
295             self.flavor = self.create_flavor()
296             self.result = 100
297             status = testcase.TestCase.EX_OK
298         except Exception:  # pylint: disable=broad-except
299             self.__logger.exception('Cannot run %s', self.case_name)
300             self.result = 0
301         finally:
302             self.stop_time = time.time()
303         return status
304
305     def clean(self):
306         try:
307             assert self.orig_cloud
308             assert self.cloud
309             super(VmReady1, self).clean()
310             if self.image:
311                 self.cloud.delete_image(self.image.id)
312             if self.flavor:
313                 self.orig_cloud.delete_flavor(self.flavor.id)
314             if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
315                 self.clean_orphan_security_groups()
316         except Exception:  # pylint: disable=broad-except
317             self.__logger.exception("Cannot clean all resources")
318
319
320 class VmReady2(VmReady1):
321     """Deploy a single VM reachable via ssh (scenario2)
322
323     It creates new user/project before creating and configuring all tenant
324     network resources, flavors, images, etc. required by advanced testcases.
325
326     It ensures that all testcases inheriting from SingleVm2 could work
327     without specific configurations (or at least read the same config data).
328     """
329
330     __logger = logging.getLogger(__name__)
331
332     def __init__(self, **kwargs):
333         if "case_name" not in kwargs:
334             kwargs["case_name"] = 'vmready2'
335         super(VmReady2, self).__init__(**kwargs)
336         try:
337             assert self.orig_cloud
338             self.project = tenantnetwork.NewProject(
339                 self.orig_cloud, self.case_name, self.guid)
340             self.project.create()
341             self.cloud = self.project.cloud
342         except Exception:  # pylint: disable=broad-except
343             self.__logger.exception("Cannot create user or project")
344             self.cloud = None
345             self.project = None
346
347     def clean(self):
348         try:
349             super(VmReady2, self).clean()
350             assert self.project
351             self.project.clean()
352         except Exception:  # pylint: disable=broad-except
353             self.__logger.exception("Cannot clean all resources")
354
355
356 class SingleVm1(VmReady1):
357     """Deploy a single VM reachable via ssh (scenario1)
358
359     It inherits from TenantNetwork1 which creates all network resources and
360     completes it by booting a VM attached to that network.
361
362     It ensures that all testcases inheriting from SingleVm1 could work
363     without specific configurations (or at least read the same config data).
364     """
365     # pylint: disable=too-many-instance-attributes
366
367     __logger = logging.getLogger(__name__)
368     username = 'cirros'
369     ssh_connect_timeout = 1
370     ssh_connect_loops = 6
371     create_floating_ip_timeout = 120
372     check_console_loop = 6
373     check_console_regex = ' login: '
374
375     def __init__(self, **kwargs):
376         if "case_name" not in kwargs:
377             kwargs["case_name"] = 'singlevm1'
378         super(SingleVm1, self).__init__(**kwargs)
379         self.sshvm = None
380         self.sec = None
381         self.fip = None
382         self.keypair = None
383         self.ssh = None
384         (_, self.key_filename) = tempfile.mkstemp()
385
386     def prepare(self):
387         """Create the security group and the keypair
388
389         It can be overriden to set other rules according to the services
390         running in the VM
391
392         Raises: Exception on error
393         """
394         assert self.cloud
395         self.keypair = self.cloud.create_keypair(
396             '{}-kp_{}'.format(self.case_name, self.guid))
397         self.__logger.debug("keypair: %s", self.keypair)
398         self.__logger.debug("private_key:\n%s", self.keypair.private_key)
399         with open(self.key_filename, 'w') as private_key_file:
400             private_key_file.write(self.keypair.private_key)
401         self.sec = self.cloud.create_security_group(
402             '{}-sg_{}'.format(self.case_name, self.guid),
403             'created by OPNFV Functest ({})'.format(self.case_name))
404         self.cloud.create_security_group_rule(
405             self.sec.id, port_range_min='22', port_range_max='22',
406             protocol='tcp', direction='ingress')
407         self.cloud.create_security_group_rule(
408             self.sec.id, protocol='icmp', direction='ingress')
409
410     def connect(self, vm1):
411         """Connect to a virtual machine via ssh
412
413         It first adds a floating ip to the virtual machine and then establishes
414         the ssh connection.
415
416         Returns:
417         - (fip, ssh)
418         - None on error
419         """
420         assert vm1
421         fip = None
422         if env.get('NO_TENANT_NETWORK').lower() != 'true':
423             fip = self.cloud.create_floating_ip(
424                 network=self.ext_net.id, server=vm1, wait=True,
425                 timeout=self.create_floating_ip_timeout)
426             self.__logger.debug("floating_ip: %s", fip)
427         ssh = paramiko.SSHClient()
428         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
429         for loop in range(self.ssh_connect_loops):
430             try:
431                 p_console = self.cloud.get_server_console(vm1)
432                 self.__logger.debug("vm console: \n%s", p_console)
433                 ssh.connect(
434                     fip.floating_ip_address if fip else vm1.public_v4,
435                     username=getattr(
436                         config.CONF,
437                         '{}_image_user'.format(self.case_name), self.username),
438                     key_filename=self.key_filename,
439                     timeout=getattr(
440                         config.CONF,
441                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
442                         self.ssh_connect_timeout))
443                 break
444             except Exception as exc:  # pylint: disable=broad-except
445                 self.__logger.debug(
446                     "try %s: cannot connect to %s: %s", loop + 1,
447                     fip.floating_ip_address if fip else vm1.public_v4, exc)
448                 time.sleep(9)
449         else:
450             self.__logger.error(
451                 "cannot connect to %s", fip.floating_ip_address)
452             return None
453         return (fip, ssh)
454
455     def execute(self):
456         """Say hello world via ssh
457
458         It can be overriden to execute any command.
459
460         Returns: echo exit codes
461         """
462         (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
463         self.__logger.debug("output:\n%s", stdout.read().decode("utf-8"))
464         self.__logger.debug("error:\n%s", stderr.read().decode("utf-8"))
465         return stdout.channel.recv_exit_status()
466
467     def run(self, **kwargs):
468         """Boot the new VM
469
470         Here are the main actions:
471         - add a new ssh key
472         - boot the VM
473         - create the security group
474         - execute the right command over ssh
475
476         Returns:
477         - TestCase.EX_OK
478         - TestCase.EX_RUN_ERROR on error
479         """
480         status = testcase.TestCase.EX_RUN_ERROR
481         try:
482             assert self.cloud
483             assert super(SingleVm1, self).run(
484                 **kwargs) == testcase.TestCase.EX_OK
485             self.result = 0
486             self.prepare()
487             self.sshvm = self.boot_vm(
488                 key_name=self.keypair.id, security_groups=[self.sec.id])
489             if self.check_regex_in_console(
490                     self.sshvm.name, regex=self.check_console_regex,
491                     loop=self.check_console_loop):
492                 (self.fip, self.ssh) = self.connect(self.sshvm)
493                 if not self.execute():
494                     self.result = 100
495                     status = testcase.TestCase.EX_OK
496         except Exception:  # pylint: disable=broad-except
497             self.__logger.exception('Cannot run %s', self.case_name)
498         finally:
499             self.stop_time = time.time()
500         return status
501
502     def clean(self):
503         try:
504             assert self.orig_cloud
505             assert self.cloud
506             if self.fip:
507                 self.cloud.delete_floating_ip(self.fip.id)
508             if self.sshvm:
509                 self.cloud.delete_server(self.sshvm, wait=True)
510             if self.sec:
511                 self.cloud.delete_security_group(self.sec.id)
512             if self.keypair:
513                 self.cloud.delete_keypair(self.keypair.name)
514             super(SingleVm1, self).clean()
515         except Exception:  # pylint: disable=broad-except
516             self.__logger.exception("Cannot clean all resources")
517
518
519 class SingleVm2(SingleVm1):
520     """Deploy a single VM reachable via ssh (scenario2)
521
522     It creates new user/project before creating and configuring all tenant
523     network resources and vms required by advanced testcases.
524
525     It ensures that all testcases inheriting from SingleVm2 could work
526     without specific configurations (or at least read the same config data).
527     """
528
529     __logger = logging.getLogger(__name__)
530
531     def __init__(self, **kwargs):
532         if "case_name" not in kwargs:
533             kwargs["case_name"] = 'singlevm2'
534         super(SingleVm2, self).__init__(**kwargs)
535         try:
536             assert self.orig_cloud
537             self.project = tenantnetwork.NewProject(
538                 self.orig_cloud, self.case_name, self.guid)
539             self.project.create()
540             self.cloud = self.project.cloud
541         except Exception:  # pylint: disable=broad-except
542             self.__logger.exception("Cannot create user or project")
543             self.cloud = None
544             self.project = None
545
546     def clean(self):
547         try:
548             super(SingleVm2, self).clean()
549             assert self.project
550             self.project.clean()
551         except Exception:  # pylint: disable=broad-except
552             self.__logger.exception("Cannot clean all resources")