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