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
14 """Module for Openstack compute operations"""
23 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):
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
38 def find_image(self, image_name):
40 return next(self.glance_client.images.list(filters={'name': image_name}), None)
41 except (novaclient.exceptions.NotFound, keystoneauth1.exceptions.http.NotFound,
46 def upload_image_via_url(self, final_image_name, image_file, retry_count=60):
48 Directly uploads image to Nova via URL if image is not present
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),
56 container_format="bare",
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)
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.")
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)
77 except keystoneauth1.exceptions.http.NotFound as exc:
78 LOG.error("Authentication error while uploading the image:" + str(exc))
81 LOG.error(traceback.format_exc())
82 LOG.error("Failed to upload image %s.", image_file)
86 def delete_image(self, img_name):
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)
92 LOG.error("Failed to delete the image %s.", img_name)
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:
102 self.novaclient.keypairs.delete(name)
103 LOG.info('Removed public key %s', name)
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)
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
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)
129 keypair = self.novaclient.keypairs.create(name, public_key)
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
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
143 def find_network(self, label):
144 net = self.novaclient.networks.find(label=label)
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):
154 security_groups = [sec_group['id']]
156 security_groups = None
158 # Also attach the created security group for the test
159 instance = self.novaclient.servers.create(name=vmname,
164 availability_zone=avail_zone,
166 config_drive=config_drive,
168 security_groups=security_groups)
171 def poll_server(self, instance):
172 return self.novaclient.servers.get(instance.id)
174 def get_server_list(self):
175 servers_list = self.novaclient.servers.list()
178 def find_floating_ips(self):
179 floating_ip = self.novaclient.floating_ips.list()
182 def create_floating_ips(self, pool):
183 return self.novaclient.floating_ips.create(pool)
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
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":
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)
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)
220 def delete_server(self, server):
221 self.novaclient.servers.delete(server)
223 def find_flavor(self, flavor_type):
225 flavor = self.novaclient.flavors.find(name=flavor_type)
230 def create_flavor(self, name, ram, vcpus, disk, ephemeral=0, override=False):
232 self.delete_flavor(name)
233 return self.novaclient.flavors.create(name=name, ram=ram, vcpus=vcpus, disk=disk,
236 def delete_flavor(self, flavor=None, name=None):
239 flavor = self.find_flavor(name)
245 def normalize_az_host(self, az, host):
247 az = self.config.availability_zone
248 return az + ':' + host
250 def auto_fill_az(self, host_list, host):
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
257 for hyp in host_list:
259 return self.normalize_az_host(hyp.zone, host)
261 LOG.error('Passed host name does not exist: ' + host)
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')
268 def sanitize_az_host(self, host_list, az_host):
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
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
279 # no host_list, return as is (no check)
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:
291 # else continue - another zone with same host name?
293 LOG.error('No match for availability zone and host ' + az_host)
296 return self.auto_fill_az(host_list, az_host)
299 # Return a list of 0, 1 or 2 az:host
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
305 # If the user provides a list of hypervisors (--hypervisor)
306 # that list is checked and returned
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
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:
316 # [ az1:hyp1, az2:hyp2 ]
317 # [] if an error occurred (error message printed to console)
319 def get_az_host_list(self):
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)')
329 for host in host_list:
330 # this host must be a compute node
331 if host.binary != 'nova-compute' or host.state != 'up':
334 if self.config.availability_zone:
335 if host.zone == self.config.availability_zone:
336 candidate = self.normalize_az_host(None, host.host)
338 candidate = self.normalize_az_host(host.zone, host.host)
340 avail_list.append(candidate)
341 # pick first 2 matches at most
342 if len(avail_list) == 2:
345 # if empty we insert the configured az
348 if not self.config.availability_zone:
349 LOG.error('Availability_zone must be configured')
351 LOG.error('No host matching the selection for availability zone: ' +
352 self.config.availability_zone)
355 avail_list = [self.config.availability_zone]
358 def get_enabled_az_host_list(self, required_count=1):
360 Check which hypervisors are enabled and on which compute nodes they are running.
361 Pick required count of hosts.
363 :param required_count: count of compute-nodes to return
364 :return: list of enabled available compute nodes
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)')
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]
380 if self.config.compute_nodes:
381 host_list = [h for h in host_list if h.host in self.config.compute_nodes]
383 hosts = [h.hypervisor_hostname for h in hypervisor_list]
384 host_list = [h for h in host_list if h.host in hosts]
387 for host in host_list:
388 candidate = self.normalize_az_host(host.zone, host.host)
390 avail_list.append(candidate)
391 if len(avail_list) == required_count:
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)
403 # Given 2 VMs test if they are running on same Host or not
404 def check_vm_placement(self, vm_instance1, vm_instance2):
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")
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]
422 'name': self.config.security_group_name,
423 'description': 'PNS Security Group'
426 group = self.neutronclient.create_security_group(body)['security_group']
427 self.security_group_add_rules(group)
431 # Delete a security group
432 def security_group_delete(self, group):
434 LOG.info("Deleting security group")
435 self.neutronclient.delete_security_group(group['id'])
437 # Add rules to the security group
438 def security_group_add_rules(self, group):
440 'security_group_rule': {
441 'direction': 'ingress',
442 'security_group_id': group['id'],
443 'remote_group_id': None
446 if self.config.ipv6_mode:
447 body['security_group_rule']['ethertype'] = 'IPv6'
448 body['security_group_rule']['remote_ip_prefix'] = '::/0'
450 body['security_group_rule']['ethertype'] = 'IPv4'
451 body['security_group_rule']['remote_ip_prefix'] = '0.0.0.0/0'
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)
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)
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)