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