Blacklick test_add_subport if OVN
[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.4.0-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, network=self.network.id,
216             timeout=self.create_server_timeout, wait=True, **kwargs)
217         self.__logger.debug("vm: %s", vm1)
218         return vm1
219
220     def check_regex_in_console(self, name, regex=' login: ', loop=1):
221         """Wait for specific message in console
222
223         Returns: True or False on errors
224         """
225         assert self.cloud
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(
231                     "regex found: '%s' in console\n%s", regex, console)
232                 return True
233             self.__logger.debug(
234                 "try %s: cannot find regex '%s' in console\n%s",
235                 iloop + 1, regex, console)
236             time.sleep(10)
237         self.__logger.error("cannot find regex '%s' in console", regex)
238         return False
239
240     def clean_orphan_security_groups(self):
241         """Clean all security groups which are not owned by an existing tenant
242
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)
246         """
247         sec_groups = self.orig_cloud.list_security_groups()
248         for sec_group in sec_groups:
249             if not sec_group.tenant_id:
250                 continue
251             if not self.orig_cloud.get_project(sec_group.tenant_id):
252                 self.__logger.debug("Cleaning security group %s", sec_group.id)
253                 try:
254                     self.orig_cloud.delete_security_group(sec_group.id)
255                 except Exception:  # pylint: disable=broad-except
256                     self.__logger.debug(
257                         "Orphan security group %s in use", sec_group.id)
258
259     def count_hypervisors(self):
260         """Count hypervisors."""
261         if env.get('SKIP_DOWN_HYPERVISORS').lower() == 'false':
262             return len(self.orig_cloud.list_hypervisors())
263         return self.count_active_hypervisors()
264
265     def count_active_hypervisors(self):
266         """Count all hypervisors which are up."""
267         compute_cnt = 0
268         for hypervisor in self.orig_cloud.list_hypervisors():
269             if hypervisor['state'] == 'up':
270                 compute_cnt += 1
271             else:
272                 self.__logger.warning(
273                     "%s is down", hypervisor['hypervisor_hostname'])
274         return compute_cnt
275
276     def run(self, **kwargs):
277         """Boot the new VM
278
279         Here are the main actions:
280         - publish the image
281         - create the flavor
282
283         Returns:
284         - TestCase.EX_OK
285         - TestCase.EX_RUN_ERROR on error
286         """
287         status = testcase.TestCase.EX_RUN_ERROR
288         try:
289             assert self.cloud
290             assert super(VmReady1, self).run(
291                 **kwargs) == testcase.TestCase.EX_OK
292             self.image = self.publish_image()
293             self.flavor = self.create_flavor()
294             self.result = 100
295             status = testcase.TestCase.EX_OK
296         except Exception:  # pylint: disable=broad-except
297             self.__logger.exception('Cannot run %s', self.case_name)
298             self.result = 0
299         finally:
300             self.stop_time = time.time()
301         return status
302
303     def clean(self):
304         try:
305             assert self.orig_cloud
306             assert self.cloud
307             super(VmReady1, self).clean()
308             if self.image:
309                 self.cloud.delete_image(self.image.id)
310             if self.flavor:
311                 self.orig_cloud.delete_flavor(self.flavor.id)
312             if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
313                 self.clean_orphan_security_groups()
314         except Exception:  # pylint: disable=broad-except
315             self.__logger.exception("Cannot clean all resources")
316
317
318 class VmReady2(VmReady1):
319     """Deploy a single VM reachable via ssh (scenario2)
320
321     It creates new user/project before creating and configuring all tenant
322     network resources, flavors, images, etc. required by advanced testcases.
323
324     It ensures that all testcases inheriting from SingleVm2 could work
325     without specific configurations (or at least read the same config data).
326     """
327
328     __logger = logging.getLogger(__name__)
329
330     def __init__(self, **kwargs):
331         if "case_name" not in kwargs:
332             kwargs["case_name"] = 'vmready2'
333         super(VmReady2, self).__init__(**kwargs)
334         try:
335             assert self.orig_cloud
336             self.project = tenantnetwork.NewProject(
337                 self.orig_cloud, self.case_name, self.guid)
338             self.project.create()
339             self.cloud = self.project.cloud
340         except Exception:  # pylint: disable=broad-except
341             self.__logger.exception("Cannot create user or project")
342             self.cloud = None
343             self.project = None
344
345     def clean(self):
346         try:
347             super(VmReady2, self).clean()
348             assert self.project
349             self.project.clean()
350         except Exception:  # pylint: disable=broad-except
351             self.__logger.exception("Cannot clean all resources")
352
353
354 class SingleVm1(VmReady1):
355     """Deploy a single VM reachable via ssh (scenario1)
356
357     It inherits from TenantNetwork1 which creates all network resources and
358     completes it by booting a VM attached to that network.
359
360     It ensures that all testcases inheriting from SingleVm1 could work
361     without specific configurations (or at least read the same config data).
362     """
363     # pylint: disable=too-many-instance-attributes
364
365     __logger = logging.getLogger(__name__)
366     username = 'cirros'
367     ssh_connect_timeout = 1
368     ssh_connect_loops = 6
369     create_floating_ip_timeout = 120
370
371     def __init__(self, **kwargs):
372         if "case_name" not in kwargs:
373             kwargs["case_name"] = 'singlevm1'
374         super(SingleVm1, self).__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             '{}-kp_{}'.format(self.case_name, 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(self.key_filename, 'w') as private_key_file:
396             private_key_file.write(self.keypair.private_key)
397         self.sec = self.cloud.create_security_group(
398             '{}-sg_{}'.format(self.case_name, self.guid),
399             'created by OPNFV Functest ({})'.format(self.case_name))
400         self.cloud.create_security_group_rule(
401             self.sec.id, port_range_min='22', port_range_max='22',
402             protocol='tcp', direction='ingress')
403         self.cloud.create_security_group_rule(
404             self.sec.id, protocol='icmp', direction='ingress')
405
406     def connect(self, vm1):
407         """Connect to a virtual machine via ssh
408
409         It first adds a floating ip to the virtual machine and then establishes
410         the ssh connection.
411
412         Returns:
413         - (fip, ssh)
414         - None on error
415         """
416         assert vm1
417         fip = self.cloud.create_floating_ip(
418             network=self.ext_net.id, server=vm1, wait=True,
419             timeout=self.create_floating_ip_timeout)
420         self.__logger.debug("floating_ip: %s", fip)
421         ssh = paramiko.SSHClient()
422         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
423         for loop in range(self.ssh_connect_loops):
424             try:
425                 p_console = self.cloud.get_server_console(vm1)
426                 self.__logger.debug("vm console: \n%s", p_console)
427                 ssh.connect(
428                     fip.floating_ip_address,
429                     username=getattr(
430                         config.CONF,
431                         '{}_image_user'.format(self.case_name), self.username),
432                     key_filename=self.key_filename,
433                     timeout=getattr(
434                         config.CONF,
435                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
436                         self.ssh_connect_timeout))
437                 break
438             except Exception as exc:  # pylint: disable=broad-except
439                 self.__logger.debug(
440                     "try %s: cannot connect to %s: %s", loop + 1,
441                     fip.floating_ip_address, exc)
442                 time.sleep(9)
443         else:
444             self.__logger.error(
445                 "cannot connect to %s", fip.floating_ip_address)
446             return None
447         return (fip, ssh)
448
449     def execute(self):
450         """Say hello world via ssh
451
452         It can be overriden to execute any command.
453
454         Returns: echo exit codes
455         """
456         (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
457         self.__logger.debug("output:\n%s", stdout.read())
458         self.__logger.debug("error:\n%s", stderr.read())
459         return stdout.channel.recv_exit_status()
460
461     def run(self, **kwargs):
462         """Boot the new VM
463
464         Here are the main actions:
465         - add a new ssh key
466         - boot the VM
467         - create the security group
468         - execute the right command over ssh
469
470         Returns:
471         - TestCase.EX_OK
472         - TestCase.EX_RUN_ERROR on error
473         """
474         status = testcase.TestCase.EX_RUN_ERROR
475         try:
476             assert self.cloud
477             assert super(SingleVm1, self).run(
478                 **kwargs) == testcase.TestCase.EX_OK
479             self.result = 0
480             self.prepare()
481             self.sshvm = self.boot_vm(
482                 key_name=self.keypair.id, security_groups=[self.sec.id])
483             (self.fip, self.ssh) = self.connect(self.sshvm)
484             if not self.execute():
485                 self.result = 100
486                 status = testcase.TestCase.EX_OK
487         except Exception:  # pylint: disable=broad-except
488             self.__logger.exception('Cannot run %s', self.case_name)
489         finally:
490             self.stop_time = time.time()
491         return status
492
493     def clean(self):
494         try:
495             assert self.orig_cloud
496             assert self.cloud
497             if self.fip:
498                 self.cloud.delete_floating_ip(self.fip.id)
499             if self.sshvm:
500                 self.cloud.delete_server(self.sshvm, wait=True)
501             if self.sec:
502                 self.cloud.delete_security_group(self.sec.id)
503             if self.keypair:
504                 self.cloud.delete_keypair(self.keypair.name)
505             super(SingleVm1, self).clean()
506         except Exception:  # pylint: disable=broad-except
507             self.__logger.exception("Cannot clean all resources")
508
509
510 class SingleVm2(SingleVm1):
511     """Deploy a single VM reachable via ssh (scenario2)
512
513     It creates new user/project before creating and configuring all tenant
514     network resources and vms required by advanced testcases.
515
516     It ensures that all testcases inheriting from SingleVm2 could work
517     without specific configurations (or at least read the same config data).
518     """
519
520     __logger = logging.getLogger(__name__)
521
522     def __init__(self, **kwargs):
523         if "case_name" not in kwargs:
524             kwargs["case_name"] = 'singlevm2'
525         super(SingleVm2, self).__init__(**kwargs)
526         try:
527             assert self.orig_cloud
528             self.project = tenantnetwork.NewProject(
529                 self.orig_cloud, self.case_name, self.guid)
530             self.project.create()
531             self.cloud = self.project.cloud
532         except Exception:  # pylint: disable=broad-except
533             self.__logger.exception("Cannot create user or project")
534             self.cloud = None
535             self.project = None
536
537     def clean(self):
538         try:
539             super(SingleVm2, self).clean()
540             assert self.project
541             self.project.clean()
542         except Exception:  # pylint: disable=broad-except
543             self.__logger.exception("Cannot clean all resources")