Update linters and fix all new issues
[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().__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, f'{self.case_name}_extra_properties', {}))
84         image = self.cloud.create_image(
85             name if name else f'{self.case_name}-img_{self.guid}',
86             filename=getattr(
87                 config.CONF, f'{self.case_name}_image',
88                 self.filename),
89             meta=extra_properties,
90             disk_format=getattr(
91                 config.CONF, f'{self.case_name}_image_format',
92                 self.image_format),
93             visibility=getattr(
94                 config.CONF, f'{self.case_name}_visibility',
95                 self.visibility),
96             wait=True)
97         self.__logger.debug("image: %s", image)
98         return image
99
100     def publish_image_alt(self, name=None):
101         """Publish alternative image
102
103         It allows publishing multiple images for the child testcases. It forces
104         the same configuration for all subtestcases.
105
106         Returns: image
107
108         Raises: expection on error
109         """
110         assert self.cloud
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}',
120             filename=getattr(
121                 config.CONF, f'{self.case_name}_image_alt',
122                 self.filename_alt),
123             meta=extra_alt_properties,
124             disk_format=getattr(
125                 config.CONF, f'{self.case_name}_image_alt_format',
126                 self.image_format),
127             visibility=getattr(
128                 config.CONF, f'{self.case_name}_visibility',
129                 self.visibility),
130             wait=True)
131         self.__logger.debug("image: %s", image)
132         return image
133
134     def create_flavor(self, name=None):
135         """Create flavor
136
137         It allows creating multiple flavors for the child testcases. It forces
138         the same configuration for all subtestcases.
139
140         Returns: flavor
141
142         Raises: expection on error
143         """
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',
148                     self.flavor_ram),
149             getattr(config.CONF, f'{self.case_name}_flavor_vcpus',
150                     self.flavor_vcpus),
151             getattr(config.CONF, f'{self.case_name}_flavor_disk',
152                     self.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(
160             getattr(config.CONF,
161                     f'{self.case_name}_flavor_extra_specs', {}))
162         self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs)
163         return flavor
164
165     def create_flavor_alt(self, name=None):
166         """Create flavor
167
168         It allows creating multiple alt flavors for the child testcases. It
169         forces the same configuration for all subtestcases.
170
171         Returns: flavor
172
173         Raises: expection on error
174         """
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(
191             getattr(config.CONF,
192                     f'{self.case_name}_flavor_alt_extra_specs', {}))
193         self.orig_cloud.set_flavor_specs(
194             flavor.id, flavor_alt_extra_specs)
195         return flavor
196
197     def boot_vm(self, name=None, **kwargs):
198         """Boot the virtual machine
199
200         It allows booting multiple machines for the child testcases. It forces
201         the same configuration for all subtestcases.
202
203         Returns: vm
204
205         Raises: expection on error
206         """
207         assert self.cloud
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,
211             auto_ip=False,
212             network=self.network.id if self.network else env.get(
213                 "EXTERNAL_NETWORK"),
214             timeout=self.create_server_timeout, wait=True, **kwargs)
215         self.__logger.debug("vm: %s", vm1)
216         return vm1
217
218     def check_regex_in_console(self, name, regex=' login: ', loop=6):
219         """Wait for specific message in console
220
221         Returns: True or False on errors
222         """
223         assert self.cloud
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):
228                 self.__logger.debug(
229                     "regex found: '%s' in console\n%s", regex, console)
230                 return True
231             self.__logger.debug(
232                 "try %s: cannot find regex '%s' in console\n%s",
233                 iloop + 1, regex, console)
234             time.sleep(10)
235         self.__logger.error("cannot find regex '%s' in console", regex)
236         return False
237
238     def clean_orphan_security_groups(self):
239         """Clean all security groups which are not owned by an existing tenant
240
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)
244         """
245         sec_groups = self.orig_cloud.list_security_groups()
246         for sec_group in sec_groups:
247             if not sec_group.tenant_id:
248                 continue
249             if not self.orig_cloud.get_project(sec_group.tenant_id):
250                 self.__logger.debug("Cleaning security group %s", sec_group.id)
251                 try:
252                     self.orig_cloud.delete_security_group(sec_group.id)
253                 except Exception:  # pylint: disable=broad-except
254                     self.__logger.debug(
255                         "Orphan security group %s in use", sec_group.id)
256
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()
262
263     def count_active_hypervisors(self):
264         """Count all hypervisors which are up."""
265         compute_cnt = 0
266         for hypervisor in self.orig_cloud.list_hypervisors():
267             if hypervisor['state'] == 'up':
268                 compute_cnt += 1
269             else:
270                 self.__logger.warning(
271                     "%s is down", hypervisor['hypervisor_hostname'])
272         return compute_cnt
273
274     def run(self, **kwargs):
275         """Boot the new VM
276
277         Here are the main actions:
278         - publish the image
279         - create the flavor
280
281         Returns:
282         - TestCase.EX_OK
283         - TestCase.EX_RUN_ERROR on error
284         """
285         status = testcase.TestCase.EX_RUN_ERROR
286         try:
287             assert self.cloud
288             assert super().run(
289                 **kwargs) == testcase.TestCase.EX_OK
290             self.image = self.publish_image()
291             self.flavor = self.create_flavor()
292             self.result = 100
293             status = testcase.TestCase.EX_OK
294         except Exception:  # pylint: disable=broad-except
295             self.__logger.exception('Cannot run %s', self.case_name)
296             self.result = 0
297         finally:
298             self.stop_time = time.time()
299         return status
300
301     def clean(self):
302         try:
303             assert self.orig_cloud
304             assert self.cloud
305             super().clean()
306             if self.image:
307                 self.cloud.delete_image(self.image.id)
308             if self.flavor:
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")
314
315
316 class VmReady2(VmReady1):
317     """Deploy a single VM reachable via ssh (scenario2)
318
319     It creates new user/project before creating and configuring all tenant
320     network resources, flavors, images, etc. required by advanced testcases.
321
322     It ensures that all testcases inheriting from SingleVm2 could work
323     without specific configurations (or at least read the same config data).
324     """
325
326     __logger = logging.getLogger(__name__)
327
328     def __init__(self, **kwargs):
329         if "case_name" not in kwargs:
330             kwargs["case_name"] = 'vmready2'
331         super().__init__(**kwargs)
332         try:
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")
340             self.cloud = None
341             self.project = None
342
343     def clean(self):
344         try:
345             super().clean()
346             assert self.project
347             self.project.clean()
348         except Exception:  # pylint: disable=broad-except
349             self.__logger.exception("Cannot clean all resources")
350
351
352 class SingleVm1(VmReady1):
353     """Deploy a single VM reachable via ssh (scenario1)
354
355     It inherits from TenantNetwork1 which creates all network resources and
356     completes it by booting a VM attached to that network.
357
358     It ensures that all testcases inheriting from SingleVm1 could work
359     without specific configurations (or at least read the same config data).
360     """
361     # pylint: disable=too-many-instance-attributes
362
363     __logger = logging.getLogger(__name__)
364     username = 'cirros'
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: '
370
371     def __init__(self, **kwargs):
372         if "case_name" not in kwargs:
373             kwargs["case_name"] = 'singlevm1'
374         super().__init__(**kwargs)
375         self.sshvm = None
376         self.sec = None
377         self.fip = None
378         self.keypair = None
379         self.ssh = None
380         (_, self.key_filename) = tempfile.mkstemp()
381
382     def prepare(self):
383         """Create the security group and the keypair
384
385         It can be overriden to set other rules according to the services
386         running in the VM
387
388         Raises: Exception on error
389         """
390         assert self.cloud
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)
395         with open(
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')
406
407     def connect(self, vm1):
408         """Connect to a virtual machine via ssh
409
410         It first adds a floating ip to the virtual machine and then establishes
411         the ssh connection.
412
413         Returns:
414         - (fip, ssh)
415         - None on error
416         """
417         assert vm1
418         fip = None
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):
427             try:
428                 p_console = self.cloud.get_server_console(vm1)
429                 self.__logger.debug("vm console: \n%s", p_console)
430                 ssh.connect(
431                     fip.floating_ip_address if fip else vm1.public_v4,
432                     username=getattr(
433                         config.CONF,
434                         f'{self.case_name}_image_user', self.username),
435                     key_filename=self.key_filename,
436                     timeout=getattr(
437                         config.CONF,
438                         f'{self.case_name}_vm_ssh_connect_timeout',
439                         self.ssh_connect_timeout))
440                 break
441             except Exception as exc:  # pylint: disable=broad-except
442                 self.__logger.debug(
443                     "try %s: cannot connect to %s: %s", loop + 1,
444                     fip.floating_ip_address if fip else vm1.public_v4, exc)
445                 time.sleep(9)
446         else:
447             self.__logger.error(
448                 "cannot connect to %s", fip.floating_ip_address)
449             return None
450         return (fip, ssh)
451
452     def execute(self):
453         """Say hello world via ssh
454
455         It can be overriden to execute any command.
456
457         Returns: echo exit codes
458         """
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()
463
464     def run(self, **kwargs):
465         """Boot the new VM
466
467         Here are the main actions:
468         - add a new ssh key
469         - boot the VM
470         - create the security group
471         - execute the right command over ssh
472
473         Returns:
474         - TestCase.EX_OK
475         - TestCase.EX_RUN_ERROR on error
476         """
477         status = testcase.TestCase.EX_RUN_ERROR
478         try:
479             assert self.cloud
480             assert super().run(
481                 **kwargs) == testcase.TestCase.EX_OK
482             self.result = 0
483             self.prepare()
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():
491                     self.result = 100
492                     status = testcase.TestCase.EX_OK
493         except Exception:  # pylint: disable=broad-except
494             self.__logger.exception('Cannot run %s', self.case_name)
495         finally:
496             self.stop_time = time.time()
497         return status
498
499     def clean(self):
500         try:
501             assert self.orig_cloud
502             assert self.cloud
503             if self.fip:
504                 self.cloud.delete_floating_ip(self.fip.id)
505             if self.sshvm:
506                 self.cloud.delete_server(self.sshvm, wait=True)
507             if self.sec:
508                 self.cloud.delete_security_group(self.sec.id)
509             if self.keypair:
510                 self.cloud.delete_keypair(self.keypair.name)
511             super().clean()
512         except Exception:  # pylint: disable=broad-except
513             self.__logger.exception("Cannot clean all resources")
514
515
516 class SingleVm2(SingleVm1):
517     """Deploy a single VM reachable via ssh (scenario2)
518
519     It creates new user/project before creating and configuring all tenant
520     network resources and vms required by advanced testcases.
521
522     It ensures that all testcases inheriting from SingleVm2 could work
523     without specific configurations (or at least read the same config data).
524     """
525
526     __logger = logging.getLogger(__name__)
527
528     def __init__(self, **kwargs):
529         if "case_name" not in kwargs:
530             kwargs["case_name"] = 'singlevm2'
531         super().__init__(**kwargs)
532         try:
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")
540             self.cloud = None
541             self.project = None
542
543     def clean(self):
544         try:
545             super().clean()
546             assert self.project
547             self.project.clean()
548         except Exception:  # pylint: disable=broad-except
549             self.__logger.exception("Cannot clean all resources")