1 # Copyright 2016 Cisco Systems, Inc. All rights reserved.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
15 '''Module for Openstack compute operations'''
16 from glanceclient import exc as glance_exception
26 from glanceclient.openstack.common.apiclient.exceptions import NotFound as GlanceImageNotFound
28 from glanceclient.v1.apiclient.exceptions import NotFound as GlanceImageNotFound
31 class Compute(object):
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
39 def find_image(self, image_name):
41 return next(self.glance_client.images.list(filters={'name': image_name}), None)
42 except (novaclient.exceptions.NotFound, keystoneauth1.exceptions.http.NotFound,
47 def upload_image_via_url(self, final_image_name, image_file, retry_count=60):
49 Directly uploads image to Nova via URL if image is not present
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),
59 container_format="bare",
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)
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.")
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)
80 except keystoneauth1.exceptions.http.NotFound as exc:
81 LOG.error("Authentication error while uploading the image:" + str(exc))
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)
90 def delete_image(self, img_name):
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)
96 LOG.error("Failed to delete the image %s.", img_name)
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:
106 self.novaclient.keypairs.delete(name)
107 LOG.info('Removed public key %s', name)
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)
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
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)
133 keypair = self.novaclient.keypairs.create(name, public_key)
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
141 if ssh_access.public_key_file:
142 return self.add_public_key(kp_name, ssh_access.public_key_file)
144 keypair = self.create_keypair(kp_name, None)
145 ssh_access.private_key = keypair.private_key
148 def find_network(self, label):
149 net = self.novaclient.networks.find(label=label)
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):
159 security_groups = [sec_group['id']]
161 security_groups = None
163 # Also attach the created security group for the test
164 instance = self.novaclient.servers.create(name=vmname,
169 availability_zone=avail_zone,
171 config_drive=config_drive,
173 security_groups=security_groups)
176 def poll_server(self, instance):
177 return self.novaclient.servers.get(instance.id)
179 def get_server_list(self):
180 servers_list = self.novaclient.servers.list()
183 def find_floating_ips(self):
184 floating_ip = self.novaclient.floating_ips.list()
187 def create_floating_ips(self, pool):
188 return self.novaclient.floating_ips.create(pool)
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
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":
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)
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)
225 def delete_server(self, server):
226 self.novaclient.servers.delete(server)
228 def find_flavor(self, flavor_type):
230 flavor = self.novaclient.flavors.find(name=flavor_type)
235 def create_flavor(self, name, ram, vcpus, disk, ephemeral=0, override=False):
237 self.delete_flavor(name)
238 return self.novaclient.flavors.create(name=name, ram=ram, vcpus=vcpus, disk=disk,
241 def delete_flavor(self, flavor=None, name=None):
244 flavor = self.find_flavor(name)
250 def normalize_az_host(self, az, host):
252 az = self.config.availability_zone
253 return az + ':' + host
255 def auto_fill_az(self, host_list, host):
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
262 for hyp in host_list:
264 return self.normalize_az_host(hyp.zone, host)
266 LOG.error('Passed host name does not exist: ' + host)
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')
273 def sanitize_az_host(self, host_list, az_host):
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
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
284 # no host_list, return as is (no check)
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:
296 # else continue - another zone with same host name?
298 LOG.error('No match for availability zone and host ' + az_host)
301 return self.auto_fill_az(host_list, az_host)
304 # Return a list of 0, 1 or 2 az:host
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
310 # If the user provides a list of hypervisors (--hypervisor)
311 # that list is checked and returned
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
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:
321 # [ az1:hyp1, az2:hyp2 ]
322 # [] if an error occurred (error message printed to console)
324 def get_az_host_list(self):
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)')
334 for host in host_list:
335 # this host must be a compute node
336 if host.binary != 'nova-compute' or host.state != 'up':
339 if self.config.availability_zone:
340 if host.zone == self.config.availability_zone:
341 candidate = self.normalize_az_host(None, host.host)
343 candidate = self.normalize_az_host(host.zone, host.host)
345 avail_list.append(candidate)
346 # pick first 2 matches at most
347 if len(avail_list) == 2:
350 # if empty we insert the configured az
353 if not self.config.availability_zone:
354 LOG.error('Availability_zone must be configured')
356 LOG.error('No host matching the selection for availability zone: ' +
357 self.config.availability_zone)
360 avail_list = [self.config.availability_zone]
363 def get_enabled_az_host_list(self, required_count=1):
365 Check which hypervisors are enabled and on which compute nodes they are running.
366 Pick required count of hosts.
368 :param required_count: count of compute-nodes to return
369 :return: list of enabled available compute nodes
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)')
381 hypervisor_list = filter(lambda h: h.status == 'enabled' and h.state == 'up',
383 if self.config.availability_zone:
384 host_list = filter(lambda h: h.zone == self.config.availability_zone, host_list)
386 if self.config.compute_nodes:
387 host_list = filter(lambda h: h.host in self.config.compute_nodes, host_list)
389 hosts = [h.hypervisor_hostname for h in hypervisor_list]
390 host_list = filter(lambda h: h.host in hosts, host_list)
393 for host in host_list:
394 candidate = self.normalize_az_host(host.zone, host.host)
396 avail_list.append(candidate)
397 if len(avail_list) == required_count:
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)
409 # Given 2 VMs test if they are running on same Host or not
410 def check_vm_placement(self, vm_instance1, vm_instance2):
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:
418 except novaclient.exceptions:
419 LOG.warning("Exception in retrieving the hostId of servers")
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]
431 'name': self.config.security_group_name,
432 'description': 'PNS Security Group'
435 group = self.neutronclient.create_security_group(body)['security_group']
436 self.security_group_add_rules(group)
440 # Delete a security group
441 def security_group_delete(self, group):
443 LOG.info("Deleting security group")
444 self.neutronclient.delete_security_group(group['id'])
446 # Add rules to the security group
447 def security_group_add_rules(self, group):
449 'security_group_rule': {
450 'direction': 'ingress',
451 'security_group_id': group['id'],
452 'remote_group_id': None
455 if self.config.ipv6_mode:
456 body['security_group_rule']['ethertype'] = 'IPv6'
457 body['security_group_rule']['remote_ip_prefix'] = '::/0'
459 body['security_group_rule']['ethertype'] = 'IPv4'
460 body['security_group_rule']['remote_ip_prefix'] = '0.0.0.0/0'
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)
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)
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)