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