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