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