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