Count all active hypervisors
[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 resources + a VM for
13 advanced testcases (e.g. deploying an orchestrator).
14 """
15
16 import logging
17 import re
18 import tempfile
19 import time
20
21 import paramiko
22 from xtesting.core import testcase
23
24 from functest.core import tenantnetwork
25 from functest.utils import config
26 from functest.utils import env
27 from functest.utils import functest_utils
28
29
30 class VmReady1(tenantnetwork.TenantNetwork1):
31     """Prepare a single VM (scenario1)
32
33     It inherits from TenantNetwork1 which creates all network resources and
34     prepares a future VM attached to that network.
35
36     It ensures that all testcases inheriting from SingleVm1 could work
37     without specific configurations (or at least read the same config data).
38     """
39     # pylint: disable=too-many-instance-attributes
40
41     __logger = logging.getLogger(__name__)
42     filename = '/home/opnfv/functest/images/cirros-0.4.0-x86_64-disk.img'
43     image_format = 'qcow2'
44     extra_properties = {}
45     filename_alt = filename
46     image_alt_format = image_format
47     extra_alt_properties = extra_properties
48     visibility = 'private'
49     flavor_ram = 512
50     flavor_vcpus = 1
51     flavor_disk = 1
52     flavor_extra_specs = {}
53     flavor_alt_ram = 1024
54     flavor_alt_vcpus = 1
55     flavor_alt_disk = 1
56     flavor_alt_extra_specs = flavor_extra_specs
57     create_server_timeout = 180
58
59     def __init__(self, **kwargs):
60         if "case_name" not in kwargs:
61             kwargs["case_name"] = 'vmready1'
62         super(VmReady1, self).__init__(**kwargs)
63         self.image = None
64         self.flavor = None
65
66     def publish_image(self, name=None):
67         """Publish image
68
69         It allows publishing multiple images for the child testcases. It forces
70         the same configuration for all subtestcases.
71
72         Returns: image
73
74         Raises: expection on error
75         """
76         assert self.cloud
77         extra_properties = self.extra_properties.copy()
78         if env.get('IMAGE_PROPERTIES'):
79             extra_properties.update(
80                 functest_utils.convert_ini_to_dict(
81                     env.get('IMAGE_PROPERTIES')))
82         extra_properties.update(
83             getattr(config.CONF, '{}_extra_properties'.format(
84                 self.case_name), {}))
85         image = self.cloud.create_image(
86             name if name else '{}-img_{}'.format(self.case_name, self.guid),
87             filename=getattr(
88                 config.CONF, '{}_image'.format(self.case_name),
89                 self.filename),
90             meta=extra_properties,
91             disk_format=getattr(
92                 config.CONF, '{}_image_format'.format(self.case_name),
93                 self.image_format),
94             visibility=getattr(
95                 config.CONF, '{}_visibility'.format(self.case_name),
96                 self.visibility),
97             wait=True)
98         self.__logger.debug("image: %s", image)
99         return image
100
101     def publish_image_alt(self, name=None):
102         """Publish alternative image
103
104         It allows publishing multiple images for the child testcases. It forces
105         the same configuration for all subtestcases.
106
107         Returns: image
108
109         Raises: expection on error
110         """
111         assert self.cloud
112         extra_alt_properties = self.extra_alt_properties.copy()
113         if env.get('IMAGE_PROPERTIES'):
114             extra_alt_properties.update(
115                 functest_utils.convert_ini_to_dict(
116                     env.get('IMAGE_PROPERTIES')))
117         extra_alt_properties.update(
118             getattr(config.CONF, '{}_extra_alt_properties'.format(
119                 self.case_name), {}))
120         image = self.cloud.create_image(
121             name if name else '{}-img_alt_{}'.format(
122                 self.case_name, self.guid),
123             filename=getattr(
124                 config.CONF, '{}_image_alt'.format(self.case_name),
125                 self.filename_alt),
126             meta=extra_alt_properties,
127             disk_format=getattr(
128                 config.CONF, '{}_image_alt_format'.format(self.case_name),
129                 self.image_format),
130             visibility=getattr(
131                 config.CONF, '{}_visibility'.format(self.case_name),
132                 self.visibility),
133             wait=True)
134         self.__logger.debug("image: %s", image)
135         return image
136
137     def create_flavor(self, name=None):
138         """Create flavor
139
140         It allows creating multiple flavors for the child testcases. It forces
141         the same configuration for all subtestcases.
142
143         Returns: flavor
144
145         Raises: expection on error
146         """
147         assert self.orig_cloud
148         flavor = self.orig_cloud.create_flavor(
149             name if name else '{}-flavor_{}'.format(self.case_name, self.guid),
150             getattr(config.CONF, '{}_flavor_ram'.format(self.case_name),
151                     self.flavor_ram),
152             getattr(config.CONF, '{}_flavor_vcpus'.format(self.case_name),
153                     self.flavor_vcpus),
154             getattr(config.CONF, '{}_flavor_disk'.format(self.case_name),
155                     self.flavor_disk))
156         self.__logger.debug("flavor: %s", flavor)
157         flavor_extra_specs = self.flavor_extra_specs.copy()
158         if env.get('FLAVOR_EXTRA_SPECS'):
159             flavor_extra_specs.update(
160                 functest_utils.convert_ini_to_dict(
161                     env.get('FLAVOR_EXTRA_SPECS')))
162         flavor_extra_specs.update(
163             getattr(config.CONF,
164                     '{}_flavor_extra_specs'.format(self.case_name), {}))
165         self.orig_cloud.set_flavor_specs(flavor.id, flavor_extra_specs)
166         return flavor
167
168     def create_flavor_alt(self, name=None):
169         """Create flavor
170
171         It allows creating multiple alt flavors for the child testcases. It
172         forces the same configuration for all subtestcases.
173
174         Returns: flavor
175
176         Raises: expection on error
177         """
178         assert self.orig_cloud
179         flavor = self.orig_cloud.create_flavor(
180             name if name else '{}-flavor_alt_{}'.format(
181                 self.case_name, self.guid),
182             getattr(config.CONF, '{}_flavor_alt_ram'.format(self.case_name),
183                     self.flavor_alt_ram),
184             getattr(config.CONF, '{}_flavor_alt_vcpus'.format(self.case_name),
185                     self.flavor_alt_vcpus),
186             getattr(config.CONF, '{}_flavor_alt_disk'.format(self.case_name),
187                     self.flavor_alt_disk))
188         self.__logger.debug("flavor: %s", flavor)
189         flavor_alt_extra_specs = self.flavor_alt_extra_specs.copy()
190         if env.get('FLAVOR_EXTRA_SPECS'):
191             flavor_alt_extra_specs.update(
192                 functest_utils.convert_ini_to_dict(
193                     env.get('FLAVOR_EXTRA_SPECS')))
194         flavor_alt_extra_specs.update(
195             getattr(config.CONF,
196                     '{}_flavor_alt_extra_specs'.format(self.case_name), {}))
197         self.orig_cloud.set_flavor_specs(
198             flavor.id, flavor_alt_extra_specs)
199         return flavor
200
201     def boot_vm(self, name=None, **kwargs):
202         """Boot the virtual machine
203
204         It allows booting multiple machines for the child testcases. It forces
205         the same configuration for all subtestcases.
206
207         Returns: vm
208
209         Raises: expection on error
210         """
211         assert self.cloud
212         vm1 = self.cloud.create_server(
213             name if name else '{}-vm_{}'.format(self.case_name, self.guid),
214             image=self.image.id, flavor=self.flavor.id,
215             auto_ip=False, network=self.network.id,
216             timeout=self.create_server_timeout, wait=True, **kwargs)
217         self.__logger.debug("vm: %s", vm1)
218         return vm1
219
220     def check_regex_in_console(self, name, regex=' login: ', loop=1):
221         """Wait for specific message in console
222
223         Returns: True or False on errors
224         """
225         assert self.cloud
226         for iloop in range(loop):
227             console = self.cloud.get_server_console(name)
228             self.__logger.debug("console: \n%s", console)
229             if re.search(regex, console):
230                 self.__logger.debug(
231                     "regex found: '%s' in console\n%s", regex, console)
232                 return True
233             self.__logger.debug(
234                 "try %s: cannot find regex '%s' in console\n%s",
235                 iloop + 1, regex, console)
236             time.sleep(10)
237         self.__logger.error("cannot find regex '%s' in console", regex)
238         return False
239
240     def clean_orphan_security_groups(self):
241         """Clean all security groups which are not owned by an existing tenant
242
243         It lists all orphan security groups in use as debug to avoid
244         misunderstanding the testcase results (it could happen if cloud admin
245         removes accounts without cleaning the virtual machines)
246         """
247         sec_groups = self.orig_cloud.list_security_groups()
248         for sec_group in sec_groups:
249             if not sec_group.tenant_id:
250                 continue
251             if not self.orig_cloud.get_project(sec_group.tenant_id):
252                 self.__logger.debug("Cleaning security group %s", sec_group.id)
253                 try:
254                     self.orig_cloud.delete_security_group(sec_group.id)
255                 except Exception:  # pylint: disable=broad-except
256                     self.__logger.debug(
257                         "Orphan security group %s in use", sec_group.id)
258
259     def count_active_hypervisors(self):
260         """Count all hypervisors which are up."""
261         compute_cnt = 0
262         for hypervisor in self.orig_cloud.list_hypervisors():
263             if hypervisor['state'] == 'up':
264                 compute_cnt += 1
265         return compute_cnt
266
267     def run(self, **kwargs):
268         """Boot the new VM
269
270         Here are the main actions:
271         - publish the image
272         - create the flavor
273
274         Returns:
275         - TestCase.EX_OK
276         - TestCase.EX_RUN_ERROR on error
277         """
278         status = testcase.TestCase.EX_RUN_ERROR
279         try:
280             assert self.cloud
281             assert super(VmReady1, self).run(
282                 **kwargs) == testcase.TestCase.EX_OK
283             self.image = self.publish_image()
284             self.flavor = self.create_flavor()
285             self.result = 100
286             status = testcase.TestCase.EX_OK
287         except Exception:  # pylint: disable=broad-except
288             self.__logger.exception('Cannot run %s', self.case_name)
289             self.result = 0
290         finally:
291             self.stop_time = time.time()
292         return status
293
294     def clean(self):
295         try:
296             assert self.orig_cloud
297             assert self.cloud
298             super(VmReady1, self).clean()
299             if self.image:
300                 self.cloud.delete_image(self.image.id)
301             if self.flavor:
302                 self.orig_cloud.delete_flavor(self.flavor.id)
303             if env.get('CLEAN_ORPHAN_SECURITY_GROUPS').lower() == 'true':
304                 self.clean_orphan_security_groups()
305         except Exception:  # pylint: disable=broad-except
306             self.__logger.exception("Cannot clean all resources")
307
308
309 class VmReady2(VmReady1):
310     """Deploy a single VM reachable via ssh (scenario2)
311
312     It creates new user/project before creating and configuring all tenant
313     network resources, flavors, images, etc. required by advanced testcases.
314
315     It ensures that all testcases inheriting from SingleVm2 could work
316     without specific configurations (or at least read the same config data).
317     """
318
319     __logger = logging.getLogger(__name__)
320
321     def __init__(self, **kwargs):
322         if "case_name" not in kwargs:
323             kwargs["case_name"] = 'vmready2'
324         super(VmReady2, self).__init__(**kwargs)
325         try:
326             assert self.orig_cloud
327             self.project = tenantnetwork.NewProject(
328                 self.orig_cloud, self.case_name, self.guid)
329             self.project.create()
330             self.cloud = self.project.cloud
331         except Exception:  # pylint: disable=broad-except
332             self.__logger.exception("Cannot create user or project")
333             self.cloud = None
334             self.project = None
335
336     def clean(self):
337         try:
338             super(VmReady2, self).clean()
339             assert self.project
340             self.project.clean()
341         except Exception:  # pylint: disable=broad-except
342             self.__logger.exception("Cannot clean all resources")
343
344
345 class SingleVm1(VmReady1):
346     """Deploy a single VM reachable via ssh (scenario1)
347
348     It inherits from TenantNetwork1 which creates all network resources and
349     completes it by booting a VM attached to that network.
350
351     It ensures that all testcases inheriting from SingleVm1 could work
352     without specific configurations (or at least read the same config data).
353     """
354     # pylint: disable=too-many-instance-attributes
355
356     __logger = logging.getLogger(__name__)
357     username = 'cirros'
358     ssh_connect_timeout = 1
359     ssh_connect_loops = 6
360     create_floating_ip_timeout = 120
361
362     def __init__(self, **kwargs):
363         if "case_name" not in kwargs:
364             kwargs["case_name"] = 'singlevm1'
365         super(SingleVm1, self).__init__(**kwargs)
366         self.sshvm = None
367         self.sec = None
368         self.fip = None
369         self.keypair = None
370         self.ssh = None
371         (_, self.key_filename) = tempfile.mkstemp()
372
373     def prepare(self):
374         """Create the security group and the keypair
375
376         It can be overriden to set other rules according to the services
377         running in the VM
378
379         Raises: Exception on error
380         """
381         assert self.cloud
382         self.keypair = self.cloud.create_keypair(
383             '{}-kp_{}'.format(self.case_name, self.guid))
384         self.__logger.debug("keypair: %s", self.keypair)
385         self.__logger.debug("private_key:\n%s", self.keypair.private_key)
386         with open(self.key_filename, 'w') as private_key_file:
387             private_key_file.write(self.keypair.private_key)
388         self.sec = self.cloud.create_security_group(
389             '{}-sg_{}'.format(self.case_name, self.guid),
390             'created by OPNFV Functest ({})'.format(self.case_name))
391         self.cloud.create_security_group_rule(
392             self.sec.id, port_range_min='22', port_range_max='22',
393             protocol='tcp', direction='ingress')
394         self.cloud.create_security_group_rule(
395             self.sec.id, protocol='icmp', direction='ingress')
396
397     def connect(self, vm1):
398         """Connect to a virtual machine via ssh
399
400         It first adds a floating ip to the virtual machine and then establishes
401         the ssh connection.
402
403         Returns:
404         - (fip, ssh)
405         - None on error
406         """
407         assert vm1
408         fip = self.cloud.create_floating_ip(
409             network=self.ext_net.id, server=vm1, wait=True,
410             timeout=self.create_floating_ip_timeout)
411         self.__logger.debug("floating_ip: %s", fip)
412         ssh = paramiko.SSHClient()
413         ssh.set_missing_host_key_policy(paramiko.client.AutoAddPolicy())
414         for loop in range(self.ssh_connect_loops):
415             try:
416                 p_console = self.cloud.get_server_console(vm1)
417                 self.__logger.debug("vm console: \n%s", p_console)
418                 ssh.connect(
419                     fip.floating_ip_address,
420                     username=getattr(
421                         config.CONF,
422                         '{}_image_user'.format(self.case_name), self.username),
423                     key_filename=self.key_filename,
424                     timeout=getattr(
425                         config.CONF,
426                         '{}_vm_ssh_connect_timeout'.format(self.case_name),
427                         self.ssh_connect_timeout))
428                 break
429             except Exception as exc:  # pylint: disable=broad-except
430                 self.__logger.debug(
431                     "try %s: cannot connect to %s: %s", loop + 1,
432                     fip.floating_ip_address, exc)
433                 time.sleep(9)
434         else:
435             self.__logger.error(
436                 "cannot connect to %s", fip.floating_ip_address)
437             return None
438         return (fip, ssh)
439
440     def execute(self):
441         """Say hello world via ssh
442
443         It can be overriden to execute any command.
444
445         Returns: echo exit codes
446         """
447         (_, stdout, stderr) = self.ssh.exec_command('echo Hello World')
448         self.__logger.debug("output:\n%s", stdout.read())
449         self.__logger.debug("error:\n%s", stderr.read())
450         return stdout.channel.recv_exit_status()
451
452     def run(self, **kwargs):
453         """Boot the new VM
454
455         Here are the main actions:
456         - add a new ssh key
457         - boot the VM
458         - create the security group
459         - execute the right command over ssh
460
461         Returns:
462         - TestCase.EX_OK
463         - TestCase.EX_RUN_ERROR on error
464         """
465         status = testcase.TestCase.EX_RUN_ERROR
466         try:
467             assert self.cloud
468             assert super(SingleVm1, self).run(
469                 **kwargs) == testcase.TestCase.EX_OK
470             self.result = 0
471             self.prepare()
472             self.sshvm = self.boot_vm(
473                 key_name=self.keypair.id, security_groups=[self.sec.id])
474             (self.fip, self.ssh) = self.connect(self.sshvm)
475             if not self.execute():
476                 self.result = 100
477                 status = testcase.TestCase.EX_OK
478         except Exception:  # pylint: disable=broad-except
479             self.__logger.exception('Cannot run %s', self.case_name)
480         finally:
481             self.stop_time = time.time()
482         return status
483
484     def clean(self):
485         try:
486             assert self.orig_cloud
487             assert self.cloud
488             if self.fip:
489                 self.cloud.delete_floating_ip(self.fip.id)
490             if self.sshvm:
491                 self.cloud.delete_server(self.sshvm, wait=True)
492             if self.sec:
493                 self.cloud.delete_security_group(self.sec.id)
494             if self.keypair:
495                 self.cloud.delete_keypair(self.keypair.name)
496             super(SingleVm1, self).clean()
497         except Exception:  # pylint: disable=broad-except
498             self.__logger.exception("Cannot clean all resources")
499
500
501 class SingleVm2(SingleVm1):
502     """Deploy a single VM reachable via ssh (scenario2)
503
504     It creates new user/project before creating and configuring all tenant
505     network resources and vms required by advanced testcases.
506
507     It ensures that all testcases inheriting from SingleVm2 could work
508     without specific configurations (or at least read the same config data).
509     """
510
511     __logger = logging.getLogger(__name__)
512
513     def __init__(self, **kwargs):
514         if "case_name" not in kwargs:
515             kwargs["case_name"] = 'singlevm2'
516         super(SingleVm2, self).__init__(**kwargs)
517         try:
518             assert self.orig_cloud
519             self.project = tenantnetwork.NewProject(
520                 self.orig_cloud, self.case_name, self.guid)
521             self.project.create()
522             self.cloud = self.project.cloud
523         except Exception:  # pylint: disable=broad-except
524             self.__logger.exception("Cannot create user or project")
525             self.cloud = None
526             self.project = None
527
528     def clean(self):
529         try:
530             super(SingleVm2, self).clean()
531             assert self.project
532             self.project.clean()
533         except Exception:  # pylint: disable=broad-except
534             self.__logger.exception("Cannot clean all resources")