1 ##############################################################################
2 # Copyright (c) 2016 Huawei Technologies Co.,Ltd and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
14 from keystoneauth1 import loading
15 from keystoneauth1 import session
19 from cinderclient import client as cinderclient
20 from novaclient import client as novaclient
21 from glanceclient import client as glanceclient
22 from neutronclient.neutron import client as neutronclient
25 log = logging.getLogger(__name__)
27 DEFAULT_HEAT_API_VERSION = '1'
28 DEFAULT_API_VERSION = '2'
31 # *********************************************
33 # *********************************************
34 def get_credentials():
35 """Returns a creds dictionary filled with parsed from env
37 Keystone API version used is 3; v2 was deprecated in 2014 (Icehouse). Along
38 with this deprecation, environment variable 'OS_TENANT_NAME' is replaced by
41 creds = {'username': os.environ.get('OS_USERNAME'),
42 'password': os.environ.get('OS_PASSWORD'),
43 'auth_url': os.environ.get('OS_AUTH_URL'),
44 'project_name': os.environ.get('OS_PROJECT_NAME')
47 if os.getenv('OS_USER_DOMAIN_NAME'):
48 creds['user_domain_name'] = os.getenv('OS_USER_DOMAIN_NAME')
49 if os.getenv('OS_PROJECT_DOMAIN_NAME'):
50 creds['project_domain_name'] = os.getenv('OS_PROJECT_DOMAIN_NAME')
55 def get_session_auth():
56 loader = loading.get_plugin_loader('password')
57 creds = get_credentials()
58 auth = loader.load_from_options(**creds)
63 auth = get_session_auth()
65 cacert = os.environ['OS_CACERT']
67 return session.Session(auth=auth)
69 insecure = os.getenv('OS_INSECURE', '').lower() == 'true'
70 cacert = False if insecure else cacert
71 return session.Session(auth=auth, verify=cacert)
74 def get_endpoint(service_type, endpoint_type='publicURL'):
75 auth = get_session_auth()
76 # for multi-region, we need to specify region
77 # when finding the endpoint
78 return get_session().get_endpoint(auth=auth,
79 service_type=service_type,
80 endpoint_type=endpoint_type,
81 region_name=os.environ.get(
85 # *********************************************
87 # *********************************************
88 def get_heat_api_version(): # pragma: no cover
90 api_version = os.environ['HEAT_API_VERSION']
92 return DEFAULT_HEAT_API_VERSION
94 log.info("HEAT_API_VERSION is set in env as '%s'", api_version)
98 def get_cinder_client_version(): # pragma: no cover
100 api_version = os.environ['OS_VOLUME_API_VERSION']
102 return DEFAULT_API_VERSION
104 log.info("OS_VOLUME_API_VERSION is set in env as '%s'", api_version)
108 def get_cinder_client(): # pragma: no cover
110 return cinderclient.Client(get_cinder_client_version(), session=sess)
113 def get_nova_client_version(): # pragma: no cover
115 api_version = os.environ['OS_COMPUTE_API_VERSION']
117 return DEFAULT_API_VERSION
119 log.info("OS_COMPUTE_API_VERSION is set in env as '%s'", api_version)
123 def get_nova_client(): # pragma: no cover
125 return novaclient.Client(get_nova_client_version(), session=sess)
128 def get_neutron_client_version(): # pragma: no cover
130 api_version = os.environ['OS_NETWORK_API_VERSION']
132 return DEFAULT_API_VERSION
134 log.info("OS_NETWORK_API_VERSION is set in env as '%s'", api_version)
138 def get_neutron_client(): # pragma: no cover
140 return neutronclient.Client(get_neutron_client_version(), session=sess)
143 def get_glance_client_version(): # pragma: no cover
145 api_version = os.environ['OS_IMAGE_API_VERSION']
147 return DEFAULT_API_VERSION
149 log.info("OS_IMAGE_API_VERSION is set in env as '%s'", api_version)
153 def get_glance_client(): # pragma: no cover
155 return glanceclient.Client(get_glance_client_version(), session=sess)
158 def get_shade_client():
159 return shade.openstack_cloud()
162 # *********************************************
164 # *********************************************
165 def create_keypair(name, key_path=None): # pragma: no cover
167 with open(key_path) as fpubkey:
168 keypair = get_nova_client().keypairs.create(
169 name=name, public_key=fpubkey.read())
171 except Exception: # pylint: disable=broad-except
172 log.exception("Error [create_keypair(nova_client)]")
175 def create_instance_and_wait_for_active(shade_client, name, image,
176 flavor, auto_ip=True, ips=None,
177 ip_pool=None, root_volume=None,
178 terminate_volume=False, wait=True,
179 timeout=180, reuse_ips=True,
180 network=None, boot_from_volume=False,
181 volume_size='20', boot_volume=None,
182 volumes=None, nat_destination=None,
184 """Create a virtual server instance.
186 :param name:(string) Name of the server.
187 :param image:(dict) Image dict, name or ID to boot with. Image is required
188 unless boot_volume is given.
189 :param flavor:(dict) Flavor dict, name or ID to boot onto.
190 :param auto_ip: Whether to take actions to find a routable IP for
192 :param ips: List of IPs to attach to the server.
193 :param ip_pool:(string) Name of the network or floating IP pool to get an
195 :param root_volume:(string) Name or ID of a volume to boot from.
196 (defaults to None - deprecated, use boot_volume)
197 :param boot_volume:(string) Name or ID of a volume to boot from.
198 :param terminate_volume:(bool) If booting from a volume, whether it should
199 be deleted when the server is destroyed.
200 :param volumes:(optional) A list of volumes to attach to the server.
201 :param wait:(optional) Wait for the address to appear as assigned to the server.
202 :param timeout: Seconds to wait, defaults to 60.
203 :param reuse_ips:(bool)Whether to attempt to reuse pre-existing
204 floating ips should a floating IP be needed.
205 :param network:(dict) Network dict or name or ID to attach the server to.
206 Mutually exclusive with the nics parameter. Can also be be
207 a list of network names or IDs or network dicts.
208 :param boot_from_volume:(bool) Whether to boot from volume. 'boot_volume'
209 implies True, but boot_from_volume=True with
210 no boot_volume is valid and will create a
211 volume from the image and use that.
212 :param volume_size: When booting an image from volume, how big should
213 the created volume be?
214 :param nat_destination: Which network should a created floating IP
215 be attached to, if it's not possible to infer from
216 the cloud's configuration.
217 :param meta:(optional) A dict of arbitrary key/value metadata to store for
218 this server. Both keys and values must be <=255 characters.
219 :param reservation_id: A UUID for the set of servers being requested.
220 :param min_count:(optional extension) The minimum number of servers to
222 :param max_count:(optional extension) The maximum number of servers to
224 :param security_groups: A list of security group names.
225 :param userdata: User data to pass to be exposed by the metadata server
226 this can be a file type object as well or a string.
227 :param key_name:(optional extension) Name of previously created keypair to
228 inject into the instance.
229 :param availability_zone: Name of the availability zone for instance
231 :param block_device_mapping:(optional) A dict of block device mappings for
233 :param block_device_mapping_v2:(optional) A dict of block device mappings
235 :param nics:(optional extension) An ordered list of nics to be added to
236 this server, with information about connected networks, fixed
238 :param scheduler_hints:(optional extension) Arbitrary key-value pairs
239 specified by the client to help boot an instance.
240 :param config_drive:(optional extension) Value for config drive either
241 boolean, or volume-id.
242 :param disk_config:(optional extension) Control how the disk is partitioned
243 when the server is created. Possible values are 'AUTO'
245 :param admin_pass:(optional extension) Add a user supplied admin password.
247 :returns: The created server.
250 return shade_client.create_server(
251 name, image, flavor, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool,
252 root_volume=root_volume, terminate_volume=terminate_volume,
253 wait=wait, timeout=timeout, reuse_ips=reuse_ips, network=network,
254 boot_from_volume=boot_from_volume, volume_size=volume_size,
255 boot_volume=boot_volume, volumes=volumes,
256 nat_destination=nat_destination, **kwargs)
257 except exc.OpenStackCloudException as o_exc:
258 log.error("Error [create_instance(shade_client)]. "
259 "Exception message, '%s'", o_exc.orig_message)
262 def attach_server_volume(server_id, volume_id,
263 device=None): # pragma: no cover
265 get_nova_client().volumes.create_server_volume(server_id,
267 except Exception: # pylint: disable=broad-except
268 log.exception("Error [attach_server_volume(nova_client, '%s', '%s')]",
269 server_id, volume_id)
275 def delete_instance(shade_client, name_or_id, wait=False, timeout=180,
276 delete_ips=False, delete_ip_retry=1):
277 """Delete a server instance.
279 :param name_or_id: name or ID of the server to delete
280 :param wait:(bool) If true, waits for server to be deleted.
281 :param timeout:(int) Seconds to wait for server deletion.
282 :param delete_ips:(bool) If true, deletes any floating IPs associated with
284 :param delete_ip_retry:(int) Number of times to retry deleting
285 any floating ips, should the first try be
287 :returns: True if delete succeeded, False otherwise.
290 return shade_client.delete_server(
291 name_or_id, wait=wait, timeout=timeout, delete_ips=delete_ips,
292 delete_ip_retry=delete_ip_retry)
293 except exc.OpenStackCloudException as o_exc:
294 log.error("Error [delete_instance(shade_client, '%s')]. "
295 "Exception message: %s", name_or_id,
300 def get_server_by_name(name): # pragma: no cover
302 return get_nova_client().servers.list(search_opts={'name': name})[0]
304 log.exception('Failed to get nova client')
308 def create_flavor(name, ram, vcpus, disk, **kwargs): # pragma: no cover
310 return get_nova_client().flavors.create(name, ram, vcpus,
312 except Exception: # pylint: disable=broad-except
313 log.exception("Error [create_flavor(nova_client, %s, %s, %s, %s, %s)]",
314 name, ram, disk, vcpus, kwargs['is_public'])
318 def get_flavor_id(nova_client, flavor_name): # pragma: no cover
319 flavors = nova_client.flavors.list(detailed=True)
322 if f.name == flavor_name:
328 def get_flavor_by_name(name): # pragma: no cover
329 flavors = get_nova_client().flavors.list()
331 return next((a for a in flavors if a.name == name))
332 except StopIteration:
333 log.exception('No flavor matched')
336 def delete_flavor(flavor_id): # pragma: no cover
338 get_nova_client().flavors.delete(flavor_id)
339 except Exception: # pylint: disable=broad-except
340 log.exception("Error [delete_flavor(nova_client, %s)]", flavor_id)
346 def delete_keypair(nova_client, key): # pragma: no cover
348 nova_client.keypairs.delete(key=key)
350 except Exception: # pylint: disable=broad-except
351 log.exception("Error [delete_keypair(nova_client)]")
355 # *********************************************
357 # *********************************************
358 def create_neutron_net(shade_client, network_name, shared=False,
359 admin_state_up=True, external=False, provider=None,
361 """Create a neutron network.
363 :param network_name:(string) name of the network being created.
364 :param shared:(bool) whether the network is shared.
365 :param admin_state_up:(bool) set the network administrative state.
366 :param external:(bool) whether this network is externally accessible.
367 :param provider:(dict) a dict of network provider options.
368 :param project_id:(string) specify the project ID this network
369 will be created on (admin-only).
370 :returns:(string) the network id.
373 networks = shade_client.create_network(
374 name=network_name, shared=shared, admin_state_up=admin_state_up,
375 external=external, provider=provider, project_id=project_id)
376 return networks['id']
377 except exc.OpenStackCloudException as o_exc:
378 log.error("Error [create_neutron_net(shade_client)]."
379 "Exception message, '%s'", o_exc.orig_message)
383 def delete_neutron_net(shade_client, network_id):
385 return shade_client.delete_network(network_id)
386 except exc.OpenStackCloudException:
387 log.error("Error [delete_neutron_net(shade_client, '%s')]", network_id)
391 def create_neutron_subnet(shade_client, network_name_or_id, cidr=None,
392 ip_version=4, enable_dhcp=False, subnet_name=None,
393 tenant_id=None, allocation_pools=None,
394 gateway_ip=None, disable_gateway_ip=False,
395 dns_nameservers=None, host_routes=None,
396 ipv6_ra_mode=None, ipv6_address_mode=None,
397 use_default_subnetpool=False):
398 """Create a subnet on a specified network.
400 :param network_name_or_id:(string) the unique name or ID of the
401 attached network. If a non-unique name is
402 supplied, an exception is raised.
403 :param cidr:(string) the CIDR.
404 :param ip_version:(int) the IP version.
405 :param enable_dhcp:(bool) whether DHCP is enable.
406 :param subnet_name:(string) the name of the subnet.
407 :param tenant_id:(string) the ID of the tenant who owns the network.
408 :param allocation_pools: A list of dictionaries of the start and end
409 addresses for the allocation pools.
410 :param gateway_ip:(string) the gateway IP address.
411 :param disable_gateway_ip:(bool) whether gateway IP address is enabled.
412 :param dns_nameservers: A list of DNS name servers for the subnet.
413 :param host_routes: A list of host route dictionaries for the subnet.
414 :param ipv6_ra_mode:(string) IPv6 Router Advertisement mode.
415 Valid values are: 'dhcpv6-stateful',
416 'dhcpv6-stateless', or 'slaac'.
417 :param ipv6_address_mode:(string) IPv6 address mode.
418 Valid values are: 'dhcpv6-stateful',
419 'dhcpv6-stateless', or 'slaac'.
420 :param use_default_subnetpool:(bool) use the default subnetpool for
421 ``ip_version`` to obtain a CIDR. It is
422 required to pass ``None`` to the ``cidr``
423 argument when enabling this option.
424 :returns:(string) the subnet id.
427 subnet = shade_client.create_subnet(
428 network_name_or_id, cidr=cidr, ip_version=ip_version,
429 enable_dhcp=enable_dhcp, subnet_name=subnet_name,
430 tenant_id=tenant_id, allocation_pools=allocation_pools,
431 gateway_ip=gateway_ip, disable_gateway_ip=disable_gateway_ip,
432 dns_nameservers=dns_nameservers, host_routes=host_routes,
433 ipv6_ra_mode=ipv6_ra_mode, ipv6_address_mode=ipv6_address_mode,
434 use_default_subnetpool=use_default_subnetpool)
436 except exc.OpenStackCloudException as o_exc:
437 log.error("Error [create_neutron_subnet(shade_client)]. "
438 "Exception message: %s", o_exc.orig_message)
442 def create_neutron_router(shade_client, name=None, admin_state_up=True,
443 ext_gateway_net_id=None, enable_snat=None,
444 ext_fixed_ips=None, project_id=None):
445 """Create a logical router.
447 :param name:(string) the router name.
448 :param admin_state_up:(bool) the administrative state of the router.
449 :param ext_gateway_net_id:(string) network ID for the external gateway.
450 :param enable_snat:(bool) enable Source NAT (SNAT) attribute.
451 :param ext_fixed_ips: List of dictionaries of desired IP and/or subnet
452 on the external network.
453 :param project_id:(string) project ID for the router.
455 :returns:(string) the router id.
458 router = shade_client.create_router(
459 name, admin_state_up, ext_gateway_net_id, enable_snat,
460 ext_fixed_ips, project_id)
462 except exc.OpenStackCloudException as o_exc:
463 log.error("Error [create_neutron_router(shade_client)]. "
464 "Exception message: %s", o_exc.orig_message)
467 def delete_neutron_router(shade_client, router_id):
469 return shade_client.delete_router(router_id)
470 except exc.OpenStackCloudException as o_exc:
471 log.error("Error [delete_neutron_router(shade_client, '%s')]. "
472 "Exception message: %s", router_id, o_exc.orig_message)
476 def remove_gateway_router(neutron_client, router_id): # pragma: no cover
478 neutron_client.remove_gateway_router(router_id)
480 except Exception: # pylint: disable=broad-except
481 log.error("Error [remove_gateway_router(neutron_client, '%s')]",
486 def remove_router_interface(shade_client, router, subnet_id=None,
488 """Detach a subnet from an internal router interface.
490 At least one of subnet_id or port_id must be supplied. If you specify both
491 subnet and port ID, the subnet ID must correspond to the subnet ID of the
492 first IP address on the port specified by the port ID.
493 Otherwise an error occurs.
495 :param router: The dict object of the router being changed
496 :param subnet_id:(string) The ID of the subnet to use for the interface
497 :param port_id:(string) The ID of the port to use for the interface
498 :returns: True on success
501 shade_client.remove_router_interface(
502 router, subnet_id=subnet_id, port_id=port_id)
504 except exc.OpenStackCloudException as o_exc:
505 log.error("Error [remove_interface_router(shade_client)]. "
506 "Exception message: %s", o_exc.orig_message)
510 def create_floating_ip(shade_client, network_name_or_id=None, server=None,
511 fixed_address=None, nat_destination=None,
512 port=None, wait=False, timeout=60):
513 """Allocate a new floating IP from a network or a pool.
515 :param network_name_or_id: Name or ID of the network
516 that the floating IP should come from.
517 :param server: Server dict for the server to create
518 the IP for and to which it should be attached.
519 :param fixed_address: Fixed IP to attach the floating ip to.
520 :param nat_destination: Name or ID of the network
521 that the fixed IP to attach the floating
523 :param port: The port ID that the floating IP should be
524 attached to. Specifying a port conflicts with specifying a
525 server,fixed_address or nat_destination.
526 :param wait: Whether to wait for the IP to be active.Only applies
527 if a server is provided.
528 :param timeout: How long to wait for the IP to be active.Only
529 applies if a server is provided.
531 :returns:Floating IP id and address
534 fip = shade_client.create_floating_ip(
535 network=network_name_or_id, server=server,
536 fixed_address=fixed_address, nat_destination=nat_destination,
537 port=port, wait=wait, timeout=timeout)
538 return {'fip_addr': fip['floating_ip_address'], 'fip_id': fip['id']}
539 except exc.OpenStackCloudException as o_exc:
540 log.error("Error [create_floating_ip(shade_client)]. "
541 "Exception message: %s", o_exc.orig_message)
544 def delete_floating_ip(shade_client, floating_ip_id, retry=1):
546 return shade_client.delete_floating_ip(floating_ip_id=floating_ip_id,
548 except exc.OpenStackCloudException as o_exc:
549 log.error("Error [delete_floating_ip(shade_client,'%s')]. "
550 "Exception message: %s", floating_ip_id, o_exc.orig_message)
554 def create_security_group_rule(shade_client, secgroup_name_or_id,
555 port_range_min=None, port_range_max=None,
556 protocol=None, remote_ip_prefix=None,
557 remote_group_id=None, direction='ingress',
558 ethertype='IPv4', project_id=None):
559 """Create a new security group rule
561 :param secgroup_name_or_id:(string) The security group name or ID to
562 associate with this security group rule. If a
563 non-unique group name is given, an exception is
565 :param port_range_min:(int) The minimum port number in the range that is
566 matched by the security group rule. If the protocol
567 is TCP or UDP, this value must be less than or equal
568 to the port_range_max attribute value. If nova is
569 used by the cloud provider for security groups, then
570 a value of None will be transformed to -1.
571 :param port_range_max:(int) The maximum port number in the range that is
572 matched by the security group rule. The
573 port_range_min attribute constrains the
574 port_range_max attribute. If nova is used by the
575 cloud provider for security groups, then a value of
576 None will be transformed to -1.
577 :param protocol:(string) The protocol that is matched by the security group
578 rule. Valid values are None, tcp, udp, and icmp.
579 :param remote_ip_prefix:(string) The remote IP prefix to be associated with
580 this security group rule. This attribute matches
581 the specified IP prefix as the source IP address of
583 :param remote_group_id:(string) The remote group ID to be associated with
584 this security group rule.
585 :param direction:(string) Ingress or egress: The direction in which the
586 security group rule is applied.
587 :param ethertype:(string) Must be IPv4 or IPv6, and addresses represented
588 in CIDR must match the ingress or egress rules.
589 :param project_id:(string) Specify the project ID this security group will
590 be created on (admin-only).
592 :returns: True on success.
596 shade_client.create_security_group_rule(
597 secgroup_name_or_id, port_range_min=port_range_min,
598 port_range_max=port_range_max, protocol=protocol,
599 remote_ip_prefix=remote_ip_prefix, remote_group_id=remote_group_id,
600 direction=direction, ethertype=ethertype, project_id=project_id)
602 except exc.OpenStackCloudException as op_exc:
603 log.error("Failed to create_security_group_rule(shade_client). "
604 "Exception message: %s", op_exc.orig_message)
608 def create_security_group_full(shade_client, sg_name,
609 sg_description, project_id=None):
610 security_group = shade_client.get_security_group(sg_name)
613 log.info("Using existing security group '%s'...", sg_name)
614 return security_group['id']
616 log.info("Creating security group '%s'...", sg_name)
618 security_group = shade_client.create_security_group(
619 sg_name, sg_description, project_id=project_id)
620 except (exc.OpenStackCloudException,
621 exc.OpenStackCloudUnavailableFeature) as op_exc:
622 log.error("Error [create_security_group(shade_client, %s, %s)]. "
623 "Exception message: %s", sg_name, sg_description,
627 log.debug("Security group '%s' with ID=%s created successfully.",
628 security_group['name'], security_group['id'])
630 log.debug("Adding ICMP rules in security group '%s'...", sg_name)
631 if not create_security_group_rule(shade_client, security_group['id'],
632 direction='ingress', protocol='icmp'):
633 log.error("Failed to create the security group rule...")
634 shade_client.delete_security_group(sg_name)
637 log.debug("Adding SSH rules in security group '%s'...", sg_name)
638 if not create_security_group_rule(shade_client, security_group['id'],
639 direction='ingress', protocol='tcp',
641 port_range_max='22'):
642 log.error("Failed to create the security group rule...")
643 shade_client.delete_security_group(sg_name)
646 if not create_security_group_rule(shade_client, security_group['id'],
647 direction='egress', protocol='tcp',
649 port_range_max='22'):
650 log.error("Failed to create the security group rule...")
651 shade_client.delete_security_group(sg_name)
653 return security_group['id']
656 # *********************************************
658 # *********************************************
659 def get_image_id(glance_client, image_name): # pragma: no cover
660 images = glance_client.images.list()
661 return next((i.id for i in images if i.name == image_name), None)
664 def create_image(glance_client, image_name, file_path, disk_format,
665 container_format, min_disk, min_ram, protected, tag,
666 public, **kwargs): # pragma: no cover
667 if not os.path.isfile(file_path):
668 log.error("Error: file %s does not exist.", file_path)
671 image_id = get_image_id(glance_client, image_name)
672 if image_id is not None:
673 log.info("Image %s already exists.", image_name)
675 log.info("Creating image '%s' from '%s'...", image_name, file_path)
677 image = glance_client.images.create(
678 name=image_name, visibility=public, disk_format=disk_format,
679 container_format=container_format, min_disk=min_disk,
680 min_ram=min_ram, tags=tag, protected=protected, **kwargs)
682 with open(file_path) as image_data:
683 glance_client.images.upload(image_id, image_data)
685 except Exception: # pylint: disable=broad-except
687 "Error [create_glance_image(glance_client, '%s', '%s', '%s')]",
688 image_name, file_path, public)
692 def delete_image(glance_client, image_id): # pragma: no cover
694 glance_client.images.delete(image_id)
696 except Exception: # pylint: disable=broad-except
697 log.exception("Error [delete_flavor(glance_client, %s)]", image_id)
703 def list_images(shade_client=None):
704 if shade_client is None:
705 shade_client = get_shade_client()
708 return shade_client.list_images()
709 except exc.OpenStackCloudException as o_exc:
710 log.error("Error [list_images(shade_client)]."
711 "Exception message, '%s'", o_exc.orig_message)
715 # *********************************************
717 # *********************************************
718 def get_volume_id(volume_name): # pragma: no cover
719 volumes = get_cinder_client().volumes.list()
720 return next((v.id for v in volumes if v.name == volume_name), None)
723 def create_volume(cinder_client, volume_name, volume_size,
724 volume_image=False): # pragma: no cover
727 volume = cinder_client.volumes.create(name=volume_name,
729 imageRef=volume_image)
731 volume = cinder_client.volumes.create(name=volume_name,
734 except Exception: # pylint: disable=broad-except
735 log.exception("Error [create_volume(cinder_client, %s)]",
736 (volume_name, volume_size))
740 def delete_volume(cinder_client, volume_id,
741 forced=False): # pragma: no cover
745 cinder_client.volumes.detach(volume_id)
746 except Exception: # pylint: disable=broad-except
747 log.error(sys.exc_info()[0])
748 cinder_client.volumes.force_delete(volume_id)
751 volume = get_cinder_client().volumes.get(volume_id)
752 if volume.status.lower() == 'available':
754 cinder_client.volumes.delete(volume_id)
756 except Exception: # pylint: disable=broad-except
757 log.exception("Error [delete_volume(cinder_client, '%s')]", volume_id)
761 def detach_volume(server_id, volume_id): # pragma: no cover
763 get_nova_client().volumes.delete_server_volume(server_id, volume_id)
765 except Exception: # pylint: disable=broad-except
766 log.exception("Error [detach_server_volume(nova_client, '%s', '%s')]",
767 server_id, volume_id)