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