NFVBENCH-40 Add pylint to tox
[nfvbench.git] / nfvbench / compute.py
1 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
2 #
3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
4 #    not use this file except in compliance with the License. You may obtain
5 #    a copy of the License at
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #    Unless required by applicable law or agreed to in writing, software
10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 #    License for the specific language governing permissions and limitations
13 #    under the License.
14 """Module for Openstack compute operations"""
15 import os
16 import time
17 import traceback
18
19 import keystoneauth1
20 from log import LOG
21 import novaclient
22
23 from glanceclient import exc as glance_exception
24
25 try:
26     from glanceclient.openstack.common.apiclient.exceptions import NotFound as GlanceImageNotFound
27 except ImportError:
28     from glanceclient.v1.apiclient.exceptions import NotFound as GlanceImageNotFound
29
30
31 class Compute(object):
32     def __init__(self, nova_client, glance_client, neutron_client, config):
33         self.novaclient = nova_client
34         self.glance_client = glance_client
35         self.neutronclient = neutron_client
36         self.config = config
37
38     def find_image(self, image_name):
39         try:
40             return next(self.glance_client.images.list(filters={'name': image_name}), None)
41         except (novaclient.exceptions.NotFound, keystoneauth1.exceptions.http.NotFound,
42                 GlanceImageNotFound):
43             pass
44         return None
45
46     def upload_image_via_url(self, final_image_name, image_file, retry_count=60):
47         '''
48         Directly uploads image to Nova via URL if image is not present
49         '''
50         retry = 0
51         try:
52             # check image is file/url based.
53             with open(image_file) as f_image:
54                 img = self.glance_client.images.create(name=str(final_image_name),
55                                                        disk_format="qcow2",
56                                                        container_format="bare",
57                                                        visibility="public")
58                 self.glance_client.images.upload(img.id, image_data=f_image)
59             # Check for the image in glance
60             while img.status in ['queued', 'saving'] and retry < retry_count:
61                 img = self.glance_client.images.get(img.id)
62                 retry += 1
63                 LOG.debug("Image not yet active, retrying %s of %s...", retry, retry_count)
64                 time.sleep(self.config.generic_poll_sec)
65             if img.status != 'active':
66                 LOG.error("Image uploaded but too long to get to active state")
67                 raise Exception("Image update active state timeout")
68         except glance_exception.HTTPForbidden:
69             LOG.error("Cannot upload image without admin access. Please make "
70                       "sure the image is uploaded and is either public or owned by you.")
71             return False
72         except IOError:
73             # catch the exception for file based errors.
74             LOG.error("Failed while uploading the image. Please make sure the "
75                       "image at the specified location %s is correct.", image_file)
76             return False
77         except keystoneauth1.exceptions.http.NotFound as exc:
78             LOG.error("Authentication error while uploading the image:" + str(exc))
79             return False
80         except Exception:
81             LOG.error(traceback.format_exc())
82             LOG.error("Failed to upload image %s.", image_file)
83             return False
84         return True
85
86     def delete_image(self, img_name):
87         try:
88             LOG.log("Deleting image %s...", img_name)
89             img = self.find_image(image_name=img_name)
90             self.glance_client.images.delete(img.id)
91         except Exception:
92             LOG.error("Failed to delete the image %s.", img_name)
93             return False
94
95         return True
96
97     # Remove keypair name from openstack if exists
98     def remove_public_key(self, name):
99         keypair_list = self.novaclient.keypairs.list()
100         for key in keypair_list:
101             if key.name == name:
102                 self.novaclient.keypairs.delete(name)
103                 LOG.info('Removed public key %s', name)
104                 break
105
106     # Test if keypair file is present if not create it
107     def create_keypair(self, name, private_key_pair_file):
108         self.remove_public_key(name)
109         keypair = self.novaclient.keypairs.create(name)
110         # Now write the keypair to the file if requested
111         if private_key_pair_file:
112             kpf = os.open(private_key_pair_file,
113                           os.O_WRONLY | os.O_CREAT, 0o600)
114             with os.fdopen(kpf, 'w') as kpf:
115                 kpf.write(keypair.private_key)
116         return keypair
117
118     # Add an existing public key to openstack
119     def add_public_key(self, name, public_key_file):
120         self.remove_public_key(name)
121         # extract the public key from the file
122         public_key = None
123         try:
124             with open(os.path.expanduser(public_key_file)) as pkf:
125                 public_key = pkf.read()
126         except IOError as exc:
127             LOG.error('Cannot open public key file %s: %s', public_key_file, exc)
128             return None
129         keypair = self.novaclient.keypairs.create(name, public_key)
130         return keypair
131
132     def init_key_pair(self, kp_name, ssh_access):
133         '''Initialize the key pair for all test VMs
134         if a key pair is specified in access, use that key pair else
135         create a temporary key pair
136         '''
137         if ssh_access.public_key_file:
138             return self.add_public_key(kp_name, ssh_access.public_key_file)
139         keypair = self.create_keypair(kp_name, None)
140         ssh_access.private_key = keypair.private_key
141         return keypair
142
143     def find_network(self, label):
144         net = self.novaclient.networks.find(label=label)
145         return net
146
147     # Create a server instance with name vmname
148     # and check that it gets into the ACTIVE state
149     def create_server(self, vmname, image, flavor, key_name,
150                       nic, sec_group, avail_zone=None, user_data=None,
151                       config_drive=None, files=None):
152
153         if sec_group:
154             security_groups = [sec_group['id']]
155         else:
156             security_groups = None
157
158         # Also attach the created security group for the test
159         instance = self.novaclient.servers.create(name=vmname,
160                                                   image=image,
161                                                   flavor=flavor,
162                                                   key_name=key_name,
163                                                   nics=nic,
164                                                   availability_zone=avail_zone,
165                                                   userdata=user_data,
166                                                   config_drive=config_drive,
167                                                   files=files,
168                                                   security_groups=security_groups)
169         return instance
170
171     def poll_server(self, instance):
172         return self.novaclient.servers.get(instance.id)
173
174     def get_server_list(self):
175         servers_list = self.novaclient.servers.list()
176         return servers_list
177
178     def find_floating_ips(self):
179         floating_ip = self.novaclient.floating_ips.list()
180         return floating_ip
181
182     def create_floating_ips(self, pool):
183         return self.novaclient.floating_ips.create(pool)
184
185     # Return the server network for a server
186     def find_server_network(self, vmname):
187         servers_list = self.get_server_list()
188         for server in servers_list:
189             if server.name == vmname and server.status == "ACTIVE":
190                 return server.networks
191         return None
192
193     # Returns True if server is present false if not.
194     # Retry for a few seconds since after VM creation sometimes
195     # it takes a while to show up
196     def find_server(self, vmname, retry_count):
197         for retry_attempt in range(retry_count):
198             servers_list = self.get_server_list()
199             for server in servers_list:
200                 if server.name == vmname and server.status == "ACTIVE":
201                     return True
202             # Sleep between retries
203             LOG.debug("[%s] VM not yet found, retrying %s of %s...",
204                       vmname, (retry_attempt + 1), retry_count)
205             time.sleep(self.config.generic_poll_sec)
206         LOG.error("[%s] VM not found, after %s attempts", vmname, retry_count)
207         return False
208
209     # Returns True if server is found and deleted/False if not,
210     # retry the delete if there is a delay
211     def delete_server_by_name(self, vmname):
212         servers_list = self.get_server_list()
213         for server in servers_list:
214             if server.name == vmname:
215                 LOG.info('Deleting server %s', server)
216                 self.novaclient.servers.delete(server)
217                 return True
218         return False
219
220     def delete_server(self, server):
221         self.novaclient.servers.delete(server)
222
223     def find_flavor(self, flavor_type):
224         try:
225             flavor = self.novaclient.flavors.find(name=flavor_type)
226             return flavor
227         except Exception:
228             return None
229
230     def create_flavor(self, name, ram, vcpus, disk, ephemeral=0, override=False):
231         if override:
232             self.delete_flavor(name)
233         return self.novaclient.flavors.create(name=name, ram=ram, vcpus=vcpus, disk=disk,
234                                               ephemeral=ephemeral)
235
236     def delete_flavor(self, flavor=None, name=None):
237         try:
238             if not flavor:
239                 flavor = self.find_flavor(name)
240             flavor.delete()
241             return True
242         except Exception:
243             return False
244
245     def normalize_az_host(self, az, host):
246         if not az:
247             az = self.config.availability_zone
248         return az + ':' + host
249
250     def auto_fill_az(self, host_list, host):
251         '''
252         no az provided, if there is a host list we can auto-fill the az
253         else we use the configured az if available
254         else we return an error
255         '''
256         if host_list:
257             for hyp in host_list:
258                 if hyp.host == host:
259                     return self.normalize_az_host(hyp.zone, host)
260             # no match on host
261             LOG.error('Passed host name does not exist: ' + host)
262             return None
263         if self.config.availability_zone:
264             return self.normalize_az_host(None, host)
265         LOG.error('--hypervisor passed without an az and no az configured')
266         return None
267
268     def sanitize_az_host(self, host_list, az_host):
269         '''
270         host_list: list of hosts as retrieved from openstack (can be empty)
271         az_host: either a host or a az:host string
272         if a host, will check host is in the list, find the corresponding az and
273                     return az:host
274         if az:host is passed will check the host is in the list and az matches
275         if host_list is empty, will return the configured az if there is no
276                     az passed
277         '''
278         if ':' in az_host:
279             # no host_list, return as is (no check)
280             if not host_list:
281                 return az_host
282             # if there is a host_list, extract and verify the az and host
283             az_host_list = az_host.split(':')
284             zone = az_host_list[0]
285             host = az_host_list[1]
286             for hyp in host_list:
287                 if hyp.host == host:
288                     if hyp.zone == zone:
289                         # matches
290                         return az_host
291                         # else continue - another zone with same host name?
292             # no match
293             LOG.error('No match for availability zone and host ' + az_host)
294             return None
295         else:
296             return self.auto_fill_az(host_list, az_host)
297
298     #
299     #   Return a list of 0, 1 or 2 az:host
300     #
301     #   The list is computed as follows:
302     #   The list of all hosts is retrieved first from openstack
303     #        if this fails, checks and az auto-fill are disabled
304     #
305     #   If the user provides a list of hypervisors (--hypervisor)
306     #       that list is checked and returned
307     #
308     #   If the user provides a configured az name (config.availability_zone)
309     #       up to the first 2 hosts from the list that match the az are returned
310     #
311     #   If the user did not configure an az name
312     #       up to the first 2 hosts from the list are returned
313     #   Possible return values:
314     #   [ az ]
315     #   [ az:hyp ]
316     #   [ az1:hyp1, az2:hyp2 ]
317     #   []  if an error occurred (error message printed to console)
318     #
319     def get_az_host_list(self):
320         avail_list = []
321         host_list = []
322
323         try:
324             host_list = self.novaclient.services.list()
325         except novaclient.exceptions.Forbidden:
326             LOG.warning('Operation Forbidden: could not retrieve list of hosts'
327                         ' (likely no permission)')
328
329         for host in host_list:
330             # this host must be a compute node
331             if host.binary != 'nova-compute' or host.state != 'up':
332                 continue
333             candidate = None
334             if self.config.availability_zone:
335                 if host.zone == self.config.availability_zone:
336                     candidate = self.normalize_az_host(None, host.host)
337             else:
338                 candidate = self.normalize_az_host(host.zone, host.host)
339             if candidate:
340                 avail_list.append(candidate)
341                 # pick first 2 matches at most
342                 if len(avail_list) == 2:
343                     break
344
345         # if empty we insert the configured az
346         if not avail_list:
347
348             if not self.config.availability_zone:
349                 LOG.error('Availability_zone must be configured')
350             elif host_list:
351                 LOG.error('No host matching the selection for availability zone: ' +
352                           self.config.availability_zone)
353                 avail_list = []
354             else:
355                 avail_list = [self.config.availability_zone]
356         return avail_list
357
358     def get_enabled_az_host_list(self, required_count=1):
359         """
360         Check which hypervisors are enabled and on which compute nodes they are running.
361         Pick required count of hosts.
362
363         :param required_count: count of compute-nodes to return
364         :return: list of enabled available compute nodes
365         """
366         host_list = []
367         hypervisor_list = []
368
369         try:
370             hypervisor_list = self.novaclient.hypervisors.list()
371             host_list = self.novaclient.services.list()
372         except novaclient.exceptions.Forbidden:
373             LOG.warning('Operation Forbidden: could not retrieve list of hypervisors'
374                         ' (likely no permission)')
375
376         hypervisor_list = [h for h in hypervisor_list if h.status == 'enabled' and h.state == 'up']
377         if self.config.availability_zone:
378             host_list = [h for h in host_list if h.zone == self.config.availability_zone]
379
380         if self.config.compute_nodes:
381             host_list = [h for h in host_list if h.host in self.config.compute_nodes]
382
383         hosts = [h.hypervisor_hostname for h in hypervisor_list]
384         host_list = [h for h in host_list if h.host in hosts]
385
386         avail_list = []
387         for host in host_list:
388             candidate = self.normalize_az_host(host.zone, host.host)
389             if candidate:
390                 avail_list.append(candidate)
391                 if len(avail_list) == required_count:
392                     return avail_list
393
394         return avail_list
395
396     def get_hypervisor(self, hyper_name):
397         # can raise novaclient.exceptions.NotFound
398         # first get the id from name
399         hyper = self.novaclient.hypervisors.search(hyper_name)[0]
400         # get full hypervisor object
401         return self.novaclient.hypervisors.get(hyper.id)
402
403     # Given 2 VMs test if they are running on same Host or not
404     def check_vm_placement(self, vm_instance1, vm_instance2):
405         try:
406             server_instance_1 = self.novaclient.servers.get(vm_instance1)
407             server_instance_2 = self.novaclient.servers.get(vm_instance2)
408             return bool(server_instance_1.hostId == server_instance_2.hostId)
409         except novaclient.exceptions:
410             LOG.warning("Exception in retrieving the hostId of servers")
411
412     # Create a new security group with appropriate rules
413     def security_group_create(self):
414         # check first the security group exists
415         sec_groups = self.neutronclient.list_security_groups()['security_groups']
416         group = [x for x in sec_groups if x['name'] == self.config.security_group_name]
417         if group:
418             return group[0]
419
420         body = {
421             'security_group': {
422                 'name': self.config.security_group_name,
423                 'description': 'PNS Security Group'
424             }
425         }
426         group = self.neutronclient.create_security_group(body)['security_group']
427         self.security_group_add_rules(group)
428
429         return group
430
431     # Delete a security group
432     def security_group_delete(self, group):
433         if group:
434             LOG.info("Deleting security group")
435             self.neutronclient.delete_security_group(group['id'])
436
437     # Add rules to the security group
438     def security_group_add_rules(self, group):
439         body = {
440             'security_group_rule': {
441                 'direction': 'ingress',
442                 'security_group_id': group['id'],
443                 'remote_group_id': None
444             }
445         }
446         if self.config.ipv6_mode:
447             body['security_group_rule']['ethertype'] = 'IPv6'
448             body['security_group_rule']['remote_ip_prefix'] = '::/0'
449         else:
450             body['security_group_rule']['ethertype'] = 'IPv4'
451             body['security_group_rule']['remote_ip_prefix'] = '0.0.0.0/0'
452
453         # Allow ping traffic
454         body['security_group_rule']['protocol'] = 'icmp'
455         body['security_group_rule']['port_range_min'] = None
456         body['security_group_rule']['port_range_max'] = None
457         self.neutronclient.create_security_group_rule(body)
458
459         # Allow SSH traffic
460         body['security_group_rule']['protocol'] = 'tcp'
461         body['security_group_rule']['port_range_min'] = 22
462         body['security_group_rule']['port_range_max'] = 22
463         self.neutronclient.create_security_group_rule(body)
464
465         # Allow TCP/UDP traffic for perf tools like iperf/nuttcp
466         # 5001: Data traffic (standard iperf data port)
467         # 5002: Control traffic (non standard)
468         # note that 5000/tcp is already picked by openstack keystone
469         body['security_group_rule']['protocol'] = 'tcp'
470         body['security_group_rule']['port_range_min'] = 5001
471         body['security_group_rule']['port_range_max'] = 5002
472         self.neutronclient.create_security_group_rule(body)
473         body['security_group_rule']['protocol'] = 'udp'
474         self.neutronclient.create_security_group_rule(body)