Fix delete_keypair arg
[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             super(VmReady1, self).run(**kwargs)
172             self.image = self.publish_image()
173             self.flavor = self.create_flavor()
174             self.result = 100
175             status = testcase.TestCase.EX_OK
176         except Exception:  # pylint: disable=broad-except
177             self.__logger.exception('Cannot run %s', self.case_name)
178         finally:
179             self.stop_time = time.time()
180         return status
181
182     def clean(self):
183         try:
184             assert self.orig_cloud
185             assert self.cloud
186             super(VmReady1, self).clean()
187             self.cloud.delete_image(self.image.id)
188             self.orig_cloud.delete_flavor(self.flavor.id)
189         except Exception:  # pylint: disable=broad-except
190             self.__logger.exception("Cannot clean all ressources")
191
192
193 class VmReady2(VmReady1):
194     """Deploy a single VM reachable via ssh (scenario2)
195
196     It creates new user/project before creating and configuring all tenant
197     network ressources, flavors, images, etc. required by advanced testcases.
198
199     It ensures that all testcases inheriting from SingleVm2 could work
200     without specific configurations (or at least read the same config data).
201     """
202
203     __logger = logging.getLogger(__name__)
204
205     def __init__(self, **kwargs):
206         if "case_name" not in kwargs:
207             kwargs["case_name"] = 'vmready2'
208         super(VmReady2, self).__init__(**kwargs)
209         try:
210             assert self.orig_cloud
211             self.project = tenantnetwork.NewProject(
212                 self.orig_cloud, self.case_name, self.guid)
213             self.project.create()
214             self.cloud = self.project.cloud
215         except Exception:  # pylint: disable=broad-except
216             self.__logger.exception("Cannot create user or project")
217             self.cloud = None
218             self.project = None
219
220     def clean(self):
221         try:
222             super(VmReady2, self).clean()
223             assert self.project
224             self.project.clean()
225         except Exception:  # pylint: disable=broad-except
226             self.__logger.exception("Cannot clean all ressources")
227
228
229 class SingleVm1(VmReady1):
230     """Deploy a single VM reachable via ssh (scenario1)
231
232     It inherits from TenantNetwork1 which creates all network resources and
233     completes it by booting a VM attached to that network.
234
235     It ensures that all testcases inheriting from SingleVm1 could work
236     without specific configurations (or at least read the same config data).
237     """
238     # pylint: disable=too-many-instance-attributes
239
240     __logger = logging.getLogger(__name__)
241     username = 'cirros'
242     ssh_connect_timeout = 60
243     ssh_connect_loops = 6
244
245     def __init__(self, **kwargs):
246         if "case_name" not in kwargs:
247             kwargs["case_name"] = 'singlevm1'
248         super(SingleVm1, self).__init__(**kwargs)
249         self.sshvm = None
250         self.sec = None
251         self.fip = None
252         self.keypair = None
253         self.ssh = None
254         (_, self.key_filename) = tempfile.mkstemp()
255
256     def prepare(self):
257         """Create the security group and the keypair
258
259         It can be overriden to set other rules according to the services
260         running in the VM
261
262         Raises: Exception on error
263         """
264         assert self.cloud
265         self.keypair = self.cloud.create_keypair(
266             '{}-kp_{}'.format(self.case_name, self.guid))
267         self.__logger.debug("keypair: %s", self.keypair)
268         self.__logger.debug("private_key: %s", self.keypair.private_key)
269         with open(self.key_filename, 'w') as private_key_file:
270             private_key_file.write(self.keypair.private_key)
271         self.sec = self.cloud.create_security_group(
272             '{}-sg_{}'.format(self.case_name, self.guid),
273             'created by OPNFV Functest ({})'.format(self.case_name))
274         self.cloud.create_security_group_rule(
275             self.sec.id, port_range_min='22', port_range_max='22',
276             protocol='tcp', direction='ingress')
277         self.cloud.create_security_group_rule(
278             self.sec.id, protocol='icmp', direction='ingress')
279
280     def connect(self, vm1):
281         """Connect to a virtual machine via ssh
282
283         It first adds a floating ip to the virtual machine and then establishes
284         the ssh connection.
285
286         Returns:
287         - (fip, ssh)
288         - None on error
289         """
290         assert vm1
291         fip = self.cloud.create_floating_ip(
292             network=self.ext_net.id, server=vm1)
293         self.__logger.debug("floating_ip: %s", fip)
294         p_console = self.cloud.get_server_console(vm1)
295         self.__logger.debug("vm console: \n%s", p_console)
296         ssh = paramiko.SSHClient()
297         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
298         for loop in range(self.ssh_connect_loops):
299             try:
300                 ssh.connect(
301                     fip.floating_ip_address,
302                     username=getattr(
303                         config.CONF,
304                         '{}_image_user'.format(self.case_name), self.username),
305                     key_filename=self.key_filename,
306                     timeout=getattr(
307                         config.CONF,
308                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
309                         self.ssh_connect_timeout))
310                 break
311             except Exception:  # pylint: disable=broad-except
312                 self.__logger.debug(
313                     "try %s: cannot connect to %s", loop + 1,
314                     fip.floating_ip_address)
315                 time.sleep(10)
316         else:
317             self.__logger.error(
318                 "cannot connect to %s", fip.floating_ip_address)
319             return None
320         return (fip, ssh)
321
322     def execute(self):
323         """Say hello world via ssh
324
325         It can be overriden to execute any command.
326
327         Returns: echo exit codes
328         """
329         (_, stdout, _) = self.ssh.exec_command('echo Hello World')
330         self.__logger.debug("output:\n%s", stdout.read())
331         return stdout.channel.recv_exit_status()
332
333     def run(self, **kwargs):
334         """Boot the new VM
335
336         Here are the main actions:
337         - add a new ssh key
338         - boot the VM
339         - create the security group
340         - execute the right command over ssh
341
342         Returns:
343         - TestCase.EX_OK
344         - TestCase.EX_RUN_ERROR on error
345         """
346         status = testcase.TestCase.EX_RUN_ERROR
347         try:
348             assert self.cloud
349             super(SingleVm1, self).run(**kwargs)
350             self.result = 0
351             self.prepare()
352             self.sshvm = self.boot_vm(
353                 key_name=self.keypair.id, security_groups=[self.sec.id])
354             (self.fip, self.ssh) = self.connect(self.sshvm)
355             if not self.execute():
356                 self.result = 100
357                 status = testcase.TestCase.EX_OK
358         except Exception:  # pylint: disable=broad-except
359             self.__logger.exception('Cannot run %s', self.case_name)
360         finally:
361             self.stop_time = time.time()
362         return status
363
364     def clean(self):
365         try:
366             assert self.orig_cloud
367             assert self.cloud
368             self.cloud.delete_floating_ip(self.fip.id)
369             self.cloud.delete_server(self.sshvm, wait=True)
370             self.cloud.delete_security_group(self.sec.id)
371             self.cloud.delete_keypair(self.keypair.name)
372             super(SingleVm1, self).clean()
373         except Exception:  # pylint: disable=broad-except
374             self.__logger.exception("Cannot clean all ressources")
375
376
377 class SingleVm2(SingleVm1):
378     """Deploy a single VM reachable via ssh (scenario2)
379
380     It creates new user/project before creating and configuring all tenant
381     network ressources and vms required by advanced testcases.
382
383     It ensures that all testcases inheriting from SingleVm2 could work
384     without specific configurations (or at least read the same config data).
385     """
386
387     __logger = logging.getLogger(__name__)
388
389     def __init__(self, **kwargs):
390         if "case_name" not in kwargs:
391             kwargs["case_name"] = 'singlevm2'
392         super(SingleVm2, self).__init__(**kwargs)
393         try:
394             assert self.orig_cloud
395             self.project = tenantnetwork.NewProject(
396                 self.orig_cloud, self.case_name, self.guid)
397             self.project.create()
398             self.cloud = self.project.cloud
399         except Exception:  # pylint: disable=broad-except
400             self.__logger.exception("Cannot create user or project")
401             self.cloud = None
402             self.project = None
403
404     def clean(self):
405         try:
406             super(SingleVm2, self).clean()
407             assert self.project
408             self.project.clean()
409         except Exception:  # pylint: disable=broad-except
410             self.__logger.exception("Cannot clean all ressources")