2a8ae879aca751a4756e8b65287db25121b6b4b3
[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, _) = self.ssh.exec_command('echo Hello World')
385         self.__logger.debug("output:\n%s", stdout.read())
386         return stdout.channel.recv_exit_status()
387
388     def run(self, **kwargs):
389         """Boot the new VM
390
391         Here are the main actions:
392         - add a new ssh key
393         - boot the VM
394         - create the security group
395         - execute the right command over ssh
396
397         Returns:
398         - TestCase.EX_OK
399         - TestCase.EX_RUN_ERROR on error
400         """
401         status = testcase.TestCase.EX_RUN_ERROR
402         try:
403             assert self.cloud
404             assert super(SingleVm1, self).run(
405                 **kwargs) == testcase.TestCase.EX_OK
406             self.result = 0
407             self.prepare()
408             self.sshvm = self.boot_vm(
409                 key_name=self.keypair.id, security_groups=[self.sec.id])
410             (self.fip, self.ssh) = self.connect(self.sshvm)
411             if not self.execute():
412                 self.result = 100
413                 status = testcase.TestCase.EX_OK
414         except Exception:  # pylint: disable=broad-except
415             self.__logger.exception('Cannot run %s', self.case_name)
416         finally:
417             self.stop_time = time.time()
418         return status
419
420     def clean(self):
421         try:
422             assert self.orig_cloud
423             assert self.cloud
424             if self.fip:
425                 self.cloud.delete_floating_ip(self.fip.id)
426             if self.sshvm:
427                 self.cloud.delete_server(self.sshvm, wait=True)
428             if self.sec:
429                 self.cloud.delete_security_group(self.sec.id)
430             if self.keypair:
431                 self.cloud.delete_keypair(self.keypair.name)
432             super(SingleVm1, self).clean()
433         except Exception:  # pylint: disable=broad-except
434             self.__logger.exception("Cannot clean all ressources")
435
436
437 class SingleVm2(SingleVm1):
438     """Deploy a single VM reachable via ssh (scenario2)
439
440     It creates new user/project before creating and configuring all tenant
441     network ressources and vms required by advanced testcases.
442
443     It ensures that all testcases inheriting from SingleVm2 could work
444     without specific configurations (or at least read the same config data).
445     """
446
447     __logger = logging.getLogger(__name__)
448
449     def __init__(self, **kwargs):
450         if "case_name" not in kwargs:
451             kwargs["case_name"] = 'singlevm2'
452         super(SingleVm2, self).__init__(**kwargs)
453         try:
454             assert self.orig_cloud
455             self.project = tenantnetwork.NewProject(
456                 self.orig_cloud, self.case_name, self.guid)
457             self.project.create()
458             self.cloud = self.project.cloud
459         except Exception:  # pylint: disable=broad-except
460             self.__logger.exception("Cannot create user or project")
461             self.cloud = None
462             self.project = None
463
464     def clean(self):
465         try:
466             super(SingleVm2, self).clean()
467             assert self.project
468             self.project.clean()
469         except Exception:  # pylint: disable=broad-except
470             self.__logger.exception("Cannot clean all ressources")