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