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