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