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