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