Blacklist create_and_delete_stack if no floating ip
[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 run(self, **kwargs):
260         """Boot the new VM
261
262         Here are the main actions:
263         - publish the image
264         - create the flavor
265
266         Returns:
267         - TestCase.EX_OK
268         - TestCase.EX_RUN_ERROR on error
269         """
270         status = testcase.TestCase.EX_RUN_ERROR
271         try:
272             assert self.cloud
273             assert super(VmReady1, self).run(
274                 **kwargs) == testcase.TestCase.EX_OK
275             self.image = self.publish_image()
276             self.flavor = self.create_flavor()
277             self.result = 100
278             status = testcase.TestCase.EX_OK
279         except Exception:  # pylint: disable=broad-except
280             self.__logger.exception('Cannot run %s', self.case_name)
281             self.result = 0
282         finally:
283             self.stop_time = time.time()
284         return status
285
286     def clean(self):
287         try:
288             assert self.orig_cloud
289             assert self.cloud
290             super(VmReady1, self).clean()
291             if self.image:
292                 self.cloud.delete_image(self.image.id)
293             if self.flavor:
294                 self.orig_cloud.delete_flavor(self.flavor.id)
295             if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
296                 self.clean_orphan_security_groups()
297         except Exception:  # pylint: disable=broad-except
298             self.__logger.exception("Cannot clean all resources")
299
300
301 class VmReady2(VmReady1):
302     """Deploy a single VM reachable via ssh (scenario2)
303
304     It creates new user/project before creating and configuring all tenant
305     network resources, flavors, images, etc. required by advanced testcases.
306
307     It ensures that all testcases inheriting from SingleVm2 could work
308     without specific configurations (or at least read the same config data).
309     """
310
311     __logger = logging.getLogger(__name__)
312
313     def __init__(self, **kwargs):
314         if "case_name" not in kwargs:
315             kwargs["case_name"] = 'vmready2'
316         super(VmReady2, self).__init__(**kwargs)
317         try:
318             assert self.orig_cloud
319             self.project = tenantnetwork.NewProject(
320                 self.orig_cloud, self.case_name, self.guid)
321             self.project.create()
322             self.cloud = self.project.cloud
323         except Exception:  # pylint: disable=broad-except
324             self.__logger.exception("Cannot create user or project")
325             self.cloud = None
326             self.project = None
327
328     def clean(self):
329         try:
330             super(VmReady2, self).clean()
331             assert self.project
332             self.project.clean()
333         except Exception:  # pylint: disable=broad-except
334             self.__logger.exception("Cannot clean all resources")
335
336
337 class SingleVm1(VmReady1):
338     """Deploy a single VM reachable via ssh (scenario1)
339
340     It inherits from TenantNetwork1 which creates all network resources and
341     completes it by booting a VM attached to that network.
342
343     It ensures that all testcases inheriting from SingleVm1 could work
344     without specific configurations (or at least read the same config data).
345     """
346     # pylint: disable=too-many-instance-attributes
347
348     __logger = logging.getLogger(__name__)
349     username = 'cirros'
350     ssh_connect_timeout = 1
351     ssh_connect_loops = 6
352     create_floating_ip_timeout = 120
353
354     def __init__(self, **kwargs):
355         if "case_name" not in kwargs:
356             kwargs["case_name"] = 'singlevm1'
357         super(SingleVm1, self).__init__(**kwargs)
358         self.sshvm = None
359         self.sec = None
360         self.fip = None
361         self.keypair = None
362         self.ssh = None
363         (_, self.key_filename) = tempfile.mkstemp()
364
365     def prepare(self):
366         """Create the security group and the keypair
367
368         It can be overriden to set other rules according to the services
369         running in the VM
370
371         Raises: Exception on error
372         """
373         assert self.cloud
374         self.keypair = self.cloud.create_keypair(
375             '{}-kp_{}'.format(self.case_name, self.guid))
376         self.__logger.debug("keypair: %s", self.keypair)
377         self.__logger.debug("private_key:\n%s", self.keypair.private_key)
378         with open(self.key_filename, 'w') as private_key_file:
379             private_key_file.write(self.keypair.private_key)
380         self.sec = self.cloud.create_security_group(
381             '{}-sg_{}'.format(self.case_name, self.guid),
382             'created by OPNFV Functest ({})'.format(self.case_name))
383         self.cloud.create_security_group_rule(
384             self.sec.id, port_range_min='22', port_range_max='22',
385             protocol='tcp', direction='ingress')
386         self.cloud.create_security_group_rule(
387             self.sec.id, protocol='icmp', direction='ingress')
388
389     def connect(self, vm1):
390         """Connect to a virtual machine via ssh
391
392         It first adds a floating ip to the virtual machine and then establishes
393         the ssh connection.
394
395         Returns:
396         - (fip, ssh)
397         - None on error
398         """
399         assert vm1
400         fip = self.cloud.create_floating_ip(
401             network=self.ext_net.id, server=vm1, wait=True,
402             timeout=self.create_floating_ip_timeout)
403         self.__logger.debug("floating_ip: %s", fip)
404         ssh = paramiko.SSHClient()
405         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
406         for loop in range(self.ssh_connect_loops):
407             try:
408                 p_console = self.cloud.get_server_console(vm1)
409                 self.__logger.debug("vm console: \n%s", p_console)
410                 ssh.connect(
411                     fip.floating_ip_address,
412                     username=getattr(
413                         config.CONF,
414                         '{}_image_user'.format(self.case_name), self.username),
415                     key_filename=self.key_filename,
416                     timeout=getattr(
417                         config.CONF,
418                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
419                         self.ssh_connect_timeout))
420                 break
421             except Exception as exc:  # pylint: disable=broad-except
422                 self.__logger.debug(
423                     "try %s: cannot connect to %s: %s", loop + 1,
424                     fip.floating_ip_address, exc)
425                 time.sleep(9)
426         else:
427             self.__logger.error(
428                 "cannot connect to %s", fip.floating_ip_address)
429             return None
430         return (fip, ssh)
431
432     def execute(self):
433         """Say hello world via ssh
434
435         It can be overriden to execute any command.
436
437         Returns: echo exit codes
438         """
439         (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
440         self.__logger.debug("output:\n%s", stdout.read())
441         self.__logger.debug("error:\n%s", stderr.read())
442         return stdout.channel.recv_exit_status()
443
444     def run(self, **kwargs):
445         """Boot the new VM
446
447         Here are the main actions:
448         - add a new ssh key
449         - boot the VM
450         - create the security group
451         - execute the right command over ssh
452
453         Returns:
454         - TestCase.EX_OK
455         - TestCase.EX_RUN_ERROR on error
456         """
457         status = testcase.TestCase.EX_RUN_ERROR
458         try:
459             assert self.cloud
460             assert super(SingleVm1, self).run(
461                 **kwargs) == testcase.TestCase.EX_OK
462             self.result = 0
463             self.prepare()
464             self.sshvm = self.boot_vm(
465                 key_name=self.keypair.id, security_groups=[self.sec.id])
466             (self.fip, self.ssh) = self.connect(self.sshvm)
467             if not self.execute():
468                 self.result = 100
469                 status = testcase.TestCase.EX_OK
470         except Exception:  # pylint: disable=broad-except
471             self.__logger.exception('Cannot run %s', self.case_name)
472         finally:
473             self.stop_time = time.time()
474         return status
475
476     def clean(self):
477         try:
478             assert self.orig_cloud
479             assert self.cloud
480             if self.fip:
481                 self.cloud.delete_floating_ip(self.fip.id)
482             if self.sshvm:
483                 self.cloud.delete_server(self.sshvm, wait=True)
484             if self.sec:
485                 self.cloud.delete_security_group(self.sec.id)
486             if self.keypair:
487                 self.cloud.delete_keypair(self.keypair.name)
488             super(SingleVm1, self).clean()
489         except Exception:  # pylint: disable=broad-except
490             self.__logger.exception("Cannot clean all resources")
491
492
493 class SingleVm2(SingleVm1):
494     """Deploy a single VM reachable via ssh (scenario2)
495
496     It creates new user/project before creating and configuring all tenant
497     network resources and vms required by advanced testcases.
498
499     It ensures that all testcases inheriting from SingleVm2 could work
500     without specific configurations (or at least read the same config data).
501     """
502
503     __logger = logging.getLogger(__name__)
504
505     def __init__(self, **kwargs):
506         if "case_name" not in kwargs:
507             kwargs["case_name"] = 'singlevm2'
508         super(SingleVm2, self).__init__(**kwargs)
509         try:
510             assert self.orig_cloud
511             self.project = tenantnetwork.NewProject(
512                 self.orig_cloud, self.case_name, self.guid)
513             self.project.create()
514             self.cloud = self.project.cloud
515         except Exception:  # pylint: disable=broad-except
516             self.__logger.exception("Cannot create user or project")
517             self.cloud = None
518             self.project = None
519
520     def clean(self):
521         try:
522             super(SingleVm2, self).clean()
523             assert self.project
524             self.project.clean()
525         except Exception:  # pylint: disable=broad-except
526             self.__logger.exception("Cannot clean all resources")