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