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