d1806eb12a5458d87f6728e7d3ff3225b94277b9
[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
28
29 class VmReady1(tenantnetwork.TenantNetwork1):
30     """Prepare a single VM (scenario1)
31
32     It inherits from TenantNetwork1 which creates all network resources and
33     prepares a future VM attached to that network.
34
35     It ensures that all testcases inheriting from SingleVm1 could work
36     without specific configurations (or at least read the same config data).
37     """
38     # pylint: disable=too-many-instance-attributes
39
40     __logger = logging.getLogger(__name__)
41     filename = '/home/opnfv/functest/images/cirros-0.4.0-x86_64-disk.img'
42     image_format = 'qcow2'
43     extra_properties = {}
44     filename_alt = filename
45     image_alt_format = image_format
46     extra_alt_properties = extra_properties
47     visibility = 'private'
48     flavor_ram = 512
49     flavor_vcpus = 1
50     flavor_disk = 1
51     flavor_extra_specs = {}
52     flavor_alt_ram = 1024
53     flavor_alt_vcpus = 1
54     flavor_alt_disk = 1
55     flavor_alt_extra_specs = flavor_extra_specs
56     create_server_timeout = 180
57
58     def __init__(self, **kwargs):
59         if "case_name" not in kwargs:
60             kwargs["case_name"] = 'vmready1'
61         super(VmReady1, self).__init__(**kwargs)
62         self.image = None
63         self.flavor = None
64
65     def publish_image(self, name=None):
66         """Publish image
67
68         It allows publishing multiple images for the child testcases. It forces
69         the same configuration for all subtestcases.
70
71         Returns: image
72
73         Raises: expection on error
74         """
75         assert self.cloud
76         extra_properties = self.extra_properties.copy()
77         if env.get('IMAGE_PROPERTIES'):
78             extra_properties.update(
79                 dict((k.strip(), v.strip()) for k, v in (
80                     item.split(': ') for item in env.get(
81                         'IMAGE_PROPERTIES').split(','))))
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                 dict((k.strip(), v.strip()) for k, v in (
116                     item.split(': ') for item in env.get(
117                         'IMAGE_PROPERTIES').split(','))))
118         extra_alt_properties.update(
119             getattr(config.CONF, '{}_extra_alt_properties'.format(
120                 self.case_name), {}))
121         image = self.cloud.create_image(
122             name if name else '{}-img_alt_{}'.format(
123                 self.case_name, self.guid),
124             filename=getattr(
125                 config.CONF, '{}_image_alt'.format(self.case_name),
126                 self.filename_alt),
127             meta=extra_alt_properties,
128             disk_format=getattr(
129                 config.CONF, '{}_image_alt_format'.format(self.case_name),
130                 self.image_format),
131             visibility=getattr(
132                 config.CONF, '{}_visibility'.format(self.case_name),
133                 self.visibility),
134             wait=True)
135         self.__logger.debug("image: %s", image)
136         return image
137
138     def create_flavor(self, name=None):
139         """Create flavor
140
141         It allows creating multiple flavors for the child testcases. It forces
142         the same configuration for all subtestcases.
143
144         Returns: flavor
145
146         Raises: expection on error
147         """
148         assert self.orig_cloud
149         flavor = self.orig_cloud.create_flavor(
150             name if name else '{}-flavor_{}'.format(self.case_name, self.guid),
151             getattr(config.CONF, '{}_flavor_ram'.format(self.case_name),
152                     self.flavor_ram),
153             getattr(config.CONF, '{}_flavor_vcpus'.format(self.case_name),
154                     self.flavor_vcpus),
155             getattr(config.CONF, '{}_flavor_disk'.format(self.case_name),
156                     self.flavor_disk))
157         self.__logger.debug("flavor: %s", flavor)
158         flavor_extra_specs = self.flavor_extra_specs.copy()
159         flavor_extra_specs.update(
160             getattr(config.CONF,
161                     '{}_flavor_extra_specs'.format(self.case_name), {}))
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 '{}-flavor_alt_{}'.format(
178                 self.case_name, self.guid),
179             getattr(config.CONF, '{}_flavor_alt_ram'.format(self.case_name),
180                     self.flavor_alt_ram),
181             getattr(config.CONF, '{}_flavor_alt_vcpus'.format(self.case_name),
182                     self.flavor_alt_vcpus),
183             getattr(config.CONF, '{}_flavor_alt_disk'.format(self.case_name),
184                     self.flavor_alt_disk))
185         self.__logger.debug("flavor: %s", flavor)
186         flavor_alt_extra_specs = self.flavor_alt_extra_specs.copy()
187         flavor_alt_extra_specs.update(
188             getattr(config.CONF,
189                     '{}_flavor_alt_extra_specs'.format(self.case_name), {}))
190         self.orig_cloud.set_flavor_specs(
191             flavor.id, flavor_alt_extra_specs)
192         return flavor
193
194     def boot_vm(self, name=None, **kwargs):
195         """Boot the virtual machine
196
197         It allows booting multiple machines for the child testcases. It forces
198         the same configuration for all subtestcases.
199
200         Returns: vm
201
202         Raises: expection on error
203         """
204         assert self.cloud
205         vm1 = self.cloud.create_server(
206             name if name else '{}-vm_{}'.format(self.case_name, self.guid),
207             image=self.image.id, flavor=self.flavor.id,
208             auto_ip=False, network=self.network.id,
209             timeout=self.create_server_timeout, wait=True, **kwargs)
210         self.__logger.debug("vm: %s", vm1)
211         return vm1
212
213     def check_regex_in_console(self, name, regex=' login: ', loop=1):
214         """Wait for specific message in console
215
216         Returns: True or False on errors
217         """
218         assert self.cloud
219         for iloop in range(loop):
220             console = self.cloud.get_server_console(name)
221             self.__logger.debug("console: \n%s", console)
222             if re.search(regex, console):
223                 self.__logger.debug("regex found: ''%s' in console", regex)
224                 return True
225             else:
226                 self.__logger.debug(
227                     "try %s: cannot find regex '%s' in console",
228                     iloop + 1, regex)
229                 time.sleep(10)
230         self.__logger.error("cannot find regex '%s' in console", regex)
231         return False
232
233     def run(self, **kwargs):
234         """Boot the new VM
235
236         Here are the main actions:
237         - publish the image
238         - create the flavor
239
240         Returns:
241         - TestCase.EX_OK
242         - TestCase.EX_RUN_ERROR on error
243         """
244         status = testcase.TestCase.EX_RUN_ERROR
245         try:
246             assert self.cloud
247             assert super(VmReady1, self).run(
248                 **kwargs) == testcase.TestCase.EX_OK
249             self.image = self.publish_image()
250             self.flavor = self.create_flavor()
251             self.result = 100
252             status = testcase.TestCase.EX_OK
253         except Exception:  # pylint: disable=broad-except
254             self.__logger.exception('Cannot run %s', self.case_name)
255             self.result = 0
256         finally:
257             self.stop_time = time.time()
258         return status
259
260     def clean(self):
261         try:
262             assert self.orig_cloud
263             assert self.cloud
264             super(VmReady1, self).clean()
265             if self.image:
266                 self.cloud.delete_image(self.image.id)
267             if self.flavor:
268                 self.orig_cloud.delete_flavor(self.flavor.id)
269         except Exception:  # pylint: disable=broad-except
270             self.__logger.exception("Cannot clean all resources")
271
272
273 class VmReady2(VmReady1):
274     """Deploy a single VM reachable via ssh (scenario2)
275
276     It creates new user/project before creating and configuring all tenant
277     network resources, flavors, images, etc. required by advanced testcases.
278
279     It ensures that all testcases inheriting from SingleVm2 could work
280     without specific configurations (or at least read the same config data).
281     """
282
283     __logger = logging.getLogger(__name__)
284
285     def __init__(self, **kwargs):
286         if "case_name" not in kwargs:
287             kwargs["case_name"] = 'vmready2'
288         super(VmReady2, self).__init__(**kwargs)
289         try:
290             assert self.orig_cloud
291             self.project = tenantnetwork.NewProject(
292                 self.orig_cloud, self.case_name, self.guid)
293             self.project.create()
294             self.cloud = self.project.cloud
295         except Exception:  # pylint: disable=broad-except
296             self.__logger.exception("Cannot create user or project")
297             self.cloud = None
298             self.project = None
299
300     def clean(self):
301         try:
302             super(VmReady2, self).clean()
303             assert self.project
304             self.project.clean()
305         except Exception:  # pylint: disable=broad-except
306             self.__logger.exception("Cannot clean all resources")
307
308
309 class SingleVm1(VmReady1):
310     """Deploy a single VM reachable via ssh (scenario1)
311
312     It inherits from TenantNetwork1 which creates all network resources and
313     completes it by booting a VM attached to that network.
314
315     It ensures that all testcases inheriting from SingleVm1 could work
316     without specific configurations (or at least read the same config data).
317     """
318     # pylint: disable=too-many-instance-attributes
319
320     __logger = logging.getLogger(__name__)
321     username = 'cirros'
322     ssh_connect_timeout = 1
323     ssh_connect_loops = 6
324     create_floating_ip_timeout = 120
325
326     def __init__(self, **kwargs):
327         if "case_name" not in kwargs:
328             kwargs["case_name"] = 'singlevm1'
329         super(SingleVm1, self).__init__(**kwargs)
330         self.sshvm = None
331         self.sec = None
332         self.fip = None
333         self.keypair = None
334         self.ssh = None
335         (_, self.key_filename) = tempfile.mkstemp()
336
337     def prepare(self):
338         """Create the security group and the keypair
339
340         It can be overriden to set other rules according to the services
341         running in the VM
342
343         Raises: Exception on error
344         """
345         assert self.cloud
346         self.keypair = self.cloud.create_keypair(
347             '{}-kp_{}'.format(self.case_name, self.guid))
348         self.__logger.debug("keypair: %s", self.keypair)
349         self.__logger.debug("private_key: %s", self.keypair.private_key)
350         with open(self.key_filename, 'w') as private_key_file:
351             private_key_file.write(self.keypair.private_key)
352         self.sec = self.cloud.create_security_group(
353             '{}-sg_{}'.format(self.case_name, self.guid),
354             'created by OPNFV Functest ({})'.format(self.case_name))
355         self.cloud.create_security_group_rule(
356             self.sec.id, port_range_min='22', port_range_max='22',
357             protocol='tcp', direction='ingress')
358         self.cloud.create_security_group_rule(
359             self.sec.id, protocol='icmp', direction='ingress')
360
361     def connect(self, vm1):
362         """Connect to a virtual machine via ssh
363
364         It first adds a floating ip to the virtual machine and then establishes
365         the ssh connection.
366
367         Returns:
368         - (fip, ssh)
369         - None on error
370         """
371         assert vm1
372         fip = self.cloud.create_floating_ip(
373             network=self.ext_net.id, server=vm1, wait=True,
374             timeout=self.create_floating_ip_timeout)
375         self.__logger.debug("floating_ip: %s", fip)
376         ssh = paramiko.SSHClient()
377         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
378         for loop in range(self.ssh_connect_loops):
379             try:
380                 p_console = self.cloud.get_server_console(vm1)
381                 self.__logger.debug("vm console: \n%s", p_console)
382                 ssh.connect(
383                     fip.floating_ip_address,
384                     username=getattr(
385                         config.CONF,
386                         '{}_image_user'.format(self.case_name), self.username),
387                     key_filename=self.key_filename,
388                     timeout=getattr(
389                         config.CONF,
390                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
391                         self.ssh_connect_timeout))
392                 break
393             except Exception as exc:  # pylint: disable=broad-except
394                 self.__logger.debug(
395                     "try %s: cannot connect to %s: %s", loop + 1,
396                     fip.floating_ip_address, exc)
397                 time.sleep(9)
398         else:
399             self.__logger.error(
400                 "cannot connect to %s", fip.floating_ip_address)
401             return None
402         return (fip, ssh)
403
404     def execute(self):
405         """Say hello world via ssh
406
407         It can be overriden to execute any command.
408
409         Returns: echo exit codes
410         """
411         (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
412         self.__logger.debug("output:\n%s", stdout.read())
413         self.__logger.debug("error:\n%s", stderr.read())
414         return stdout.channel.recv_exit_status()
415
416     def run(self, **kwargs):
417         """Boot the new VM
418
419         Here are the main actions:
420         - add a new ssh key
421         - boot the VM
422         - create the security group
423         - execute the right command over ssh
424
425         Returns:
426         - TestCase.EX_OK
427         - TestCase.EX_RUN_ERROR on error
428         """
429         status = testcase.TestCase.EX_RUN_ERROR
430         try:
431             assert self.cloud
432             assert super(SingleVm1, self).run(
433                 **kwargs) == testcase.TestCase.EX_OK
434             self.result = 0
435             self.prepare()
436             self.sshvm = self.boot_vm(
437                 key_name=self.keypair.id, security_groups=[self.sec.id])
438             (self.fip, self.ssh) = self.connect(self.sshvm)
439             if not self.execute():
440                 self.result = 100
441                 status = testcase.TestCase.EX_OK
442         except Exception:  # pylint: disable=broad-except
443             self.__logger.exception('Cannot run %s', self.case_name)
444         finally:
445             self.stop_time = time.time()
446         return status
447
448     def clean(self):
449         try:
450             assert self.orig_cloud
451             assert self.cloud
452             if self.fip:
453                 self.cloud.delete_floating_ip(self.fip.id)
454             if self.sshvm:
455                 self.cloud.delete_server(self.sshvm, wait=True)
456             if self.sec:
457                 self.cloud.delete_security_group(self.sec.id)
458             if self.keypair:
459                 self.cloud.delete_keypair(self.keypair.name)
460             super(SingleVm1, self).clean()
461         except Exception:  # pylint: disable=broad-except
462             self.__logger.exception("Cannot clean all resources")
463
464
465 class SingleVm2(SingleVm1):
466     """Deploy a single VM reachable via ssh (scenario2)
467
468     It creates new user/project before creating and configuring all tenant
469     network resources and vms required by advanced testcases.
470
471     It ensures that all testcases inheriting from SingleVm2 could work
472     without specific configurations (or at least read the same config data).
473     """
474
475     __logger = logging.getLogger(__name__)
476
477     def __init__(self, **kwargs):
478         if "case_name" not in kwargs:
479             kwargs["case_name"] = 'singlevm2'
480         super(SingleVm2, self).__init__(**kwargs)
481         try:
482             assert self.orig_cloud
483             self.project = tenantnetwork.NewProject(
484                 self.orig_cloud, self.case_name, self.guid)
485             self.project.create()
486             self.cloud = self.project.cloud
487         except Exception:  # pylint: disable=broad-except
488             self.__logger.exception("Cannot create user or project")
489             self.cloud = None
490             self.project = None
491
492     def clean(self):
493         try:
494             super(SingleVm2, self).clean()
495             assert self.project
496             self.project.clean()
497         except Exception:  # pylint: disable=broad-except
498             self.__logger.exception("Cannot clean all resources")