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