shade/shade/__init__.py

1428 lines
54 KiB
Python

# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import hashlib
import logging
import operator
import os
import time
from cinderclient.v1 import client as cinder_client
from dogpile import cache
import glanceclient
import glanceclient.exc
from ironicclient import client as ironic_client
from ironicclient import exceptions as ironic_exceptions
from keystoneclient import auth as ksc_auth
from keystoneclient import session as ksc_session
from novaclient import client as nova_client
from novaclient import exceptions as nova_exceptions
from novaclient.v2 import floating_ips
from neutronclient.v2_0 import client as neutron_client
import os_client_config
import pbr.version
import swiftclient.client as swift_client
import swiftclient.exceptions as swift_exceptions
import troveclient.client as trove_client
from shade import meta
__version__ = pbr.version.VersionInfo('shade').version_string()
OBJECT_MD5_KEY = 'x-object-meta-x-shade-md5'
OBJECT_SHA256_KEY = 'x-object-meta-x-shade-sha256'
IMAGE_MD5_KEY = 'owner_specified.shade.md5'
IMAGE_SHA256_KEY = 'owner_specified.shade.sha256'
OBJECT_CONTAINER_ACLS = {
'public': ".r:*,.rlistings",
'private': '',
}
class OpenStackCloudException(Exception):
def __init__(self, message, extra_data=None):
self.message = message
self.extra_data = extra_data
class OpenStackCloudTimeout(OpenStackCloudException):
pass
def openstack_clouds(config=None):
if not config:
config = os_client_config.OpenStackConfig()
return [OpenStackCloud(f.name, f.region, **f.config)
for f in config.get_all_clouds()]
def openstack_cloud(debug=False, **kwargs):
cloud_config = os_client_config.OpenStackConfig().get_one_cloud(
**kwargs)
return OpenStackCloud(
cloud_config.name, cloud_config.region,
debug=debug, **cloud_config.config)
def operator_cloud(**kwargs):
cloud_config = os_client_config.OpenStackConfig().get_one_cloud(**kwargs)
return OperatorCloud(
cloud_config.name, cloud_config.region, **cloud_config.config)
def _get_service_values(kwargs, service_key):
return {k[:-(len(service_key) + 1)]: kwargs[k]
for k in kwargs.keys() if k.endswith(service_key)}
def _iterate_timeout(timeout, message):
"""Iterate and raise an exception on timeout.
This is a generator that will continually yield and sleep for 2
seconds, and if the timeout is reached, will raise an exception
with <message>.
"""
start = time.time()
count = 0
while (timeout is None) or (time.time() < start + timeout):
count += 1
yield count
time.sleep(2)
raise OpenStackCloudTimeout(message)
class OpenStackCloud(object):
"""Represent a connection to an OpenStack Cloud.
OpenStackCloud is the entry point for all cloud operations, regardless
of which OpenStack service those operations may ultimately come from.
The operations on an OpenStackCloud are resource oriented rather than
REST API operation oriented. For instance, one will request a Floating IP
and that Floating IP will be actualized either via neutron or via nova
depending on how this particular cloud has decided to arrange itself.
:param cloud: A Cloud Configuration object, obtained from os-client-config
:type cloud: :py:class:`os_client_config.config.OpenStackConfig`
:param string region: The region of the cloud that all operations should
be performed against.
:param string auth_plugin: The name of the keystone auth_plugin to be used
:param bool verify: The verification arguments to pass to requests. True
tells requests to verify SSL requests, False to not
verify. (optional, defaults to True)
:param string cacert: A path to a CA Cert bundle that can be used as part
of verifying SSL requests. If this is set, verify
is set to True. (optional)
:param string cert: A path to a client certificate to pass to requests.
(optional)
:param string key: A path to a client key to pass to requests. (optional)
:param bool debug: Enable or disable debug logging (optional, defaults to
False)
:param int cache_interval: How long to cache items fetched from the cloud.
Value will be passed to dogpile.cache. None
means to just use the default in dogpile.cache.
(optional, defaults to None)
"""
def __init__(self, cloud, region='',
auth_plugin='password',
verify=True, cacert=None, cert=None, key=None,
debug=False, cache_interval=None, **kwargs):
self.name = cloud
self.region = region
self.auth_plugin = auth_plugin
self.auth = kwargs.get('auth')
self.region_name = kwargs.get('region_name', region)
self._auth_token = kwargs.get('auth_token', None)
self.service_types = _get_service_values(kwargs, 'service_type')
self.service_names = _get_service_values(kwargs, 'service_name')
self.endpoints = _get_service_values(kwargs, 'endpoint')
self.api_versions = _get_service_values(kwargs, 'api_version')
self.endpoint_type = kwargs.get('endpoint_type', 'publicURL')
self.private = kwargs.get('private', False)
if cacert:
if not os.path.exists(cacert):
raise OpenStackCloudException(
"CA Cert {0} does not exist".format(cacert))
verify = cacert
self.verify = verify
if cert:
if not os.path.exists(cert):
raise OpenStackCloudException(
"Client Cert {0} does not exist".format(cert))
if key:
if not os.path.exists(key):
raise OpenStackCloudException(
"Client key {0} does not exist".format(key))
cert = (cert, key)
self.cert = cert
self._cache = cache.make_region().configure(
'dogpile.cache.memory', expiration_time=cache_interval)
self._container_cache = dict()
self._file_hash_cache = dict()
self.debug = debug
self._keystone_session = None
self._cinder_client = None
self._glance_client = None
self._glance_endpoint = None
self._ironic_client = None
self._neutron_client = None
self._nova_client = None
self._swift_client = None
self._trove_client = None
self.log = logging.getLogger('shade')
log_level = logging.INFO
if self.debug:
log_level = logging.DEBUG
self.log.setLevel(log_level)
self.log.addHandler(logging.StreamHandler())
def get_service_type(self, service):
return self.service_types.get(service, service)
def get_service_name(self, service):
return self.service_names.get(service, None)
def _get_nova_api_version(self):
return self.api_versions['compute']
@property
def nova_client(self):
if self._nova_client is None:
# Make the connection
try:
self._nova_client = nova_client.Client(
self._get_nova_api_version(),
session=self.keystone_session,
service_name=self.get_service_name('compute'),
region_name=self.region_name)
except Exception:
self.log.debug("Couldn't construct nova object", exc_info=True)
raise
if self._nova_client is None:
raise OpenStackCloudException(
"Failed to instantiate nova client."
" This could mean that your credentials are wrong.")
return self._nova_client
@property
def keystone_session(self):
if self._keystone_session is None:
# keystoneclient does crazy things with logging that are
# none of them interesting
keystone_logging = logging.getLogger('keystoneclient')
keystone_logging.addHandler(logging.NullHandler())
try:
auth_plugin = ksc_auth.get_plugin_class(self.auth_plugin)
except Exception as e:
self.log.debug("keystone auth plugin failure", exc_info=True)
raise OpenStackCloudException(
"Could not find auth plugin: {plugin}".format(
plugin=self.auth_plugin))
try:
keystone_auth = auth_plugin(**self.auth)
except Exception as e:
self.log.debug(
"keystone couldn't construct plugin", exc_info=True)
raise OpenStackCloudException(
"Error constructing auth plugin: {plugin}".format(
plugin=self.auth_plugin))
try:
self._keystone_session = ksc_session.Session(
auth=keystone_auth,
verify=self.verify,
cert=self.cert)
except Exception as e:
self.log.debug("keystone unknown issue", exc_info=True)
raise OpenStackCloudException(
"Error authenticating to the keystone: %s " % e.message)
return self._keystone_session
@property
def service_catalog(self):
return self.keystone_session.auth.get_access(
self.keystone_session).service_catalog.get_data()
@property
def auth_token(self):
if not self._auth_token:
self._auth_token = self.keystone_session.get_token()
return self._auth_token
def _get_glance_api_version(self):
if 'image' in self.api_versions:
return self.api_versions['image']
# Yay. We get to guess ...
# Get rid of trailing '/' if present
endpoint = self._get_glance_endpoint()
if endpoint.endswith('/'):
endpoint = endpoint[:-1]
url_bits = endpoint.split('/')
if url_bits[-1].startswith('v'):
return url_bits[-1][1]
return '1' # Who knows? Let's just try 1 ...
def _get_glance_endpoint(self):
if self._glance_endpoint is None:
self._glance_endpoint = self.get_endpoint(
service_type=self.get_service_type('image'))
return self._glance_endpoint
@property
def glance_client(self):
if self._glance_client is None:
token = self.auth_token
endpoint = self._get_glance_endpoint()
glance_api_version = self._get_glance_api_version()
try:
self._glance_client = glanceclient.Client(
glance_api_version, endpoint, token=token,
session=self.keystone_session)
except Exception as e:
self.log.debug("glance unknown issue", exc_info=True)
raise OpenStackCloudException(
"Error in connecting to glance: %s" % e.message)
if not self._glance_client:
raise OpenStackCloudException("Error connecting to glance")
return self._glance_client
@property
def swift_client(self):
if self._swift_client is None:
token = self.auth_token
endpoint = self.get_endpoint(
service_type=self.get_service_type('object-store'))
self._swift_client = swift_client.Connection(
preauthurl=endpoint,
preauthtoken=token,
os_options=dict(region_name=self.region_name),
)
return self._swift_client
@property
def cinder_client(self):
if self._cinder_client is None:
# Make the connection
self._cinder_client = cinder_client.Client(
session=self.keystone_session,
region_name=self.region_name)
if self._cinder_client is None:
raise OpenStackCloudException(
"Failed to instantiate cinder client."
" This could mean that your credentials are wrong.")
return self._cinder_client
def _get_trove_api_version(self, endpoint):
if 'database' in self.api_versions:
return self.api_versions['database']
# Yay. We get to guess ...
# Get rid of trailing '/' if present
if endpoint.endswith('/'):
endpoint = endpoint[:-1]
url_bits = endpoint.split('/')
for bit in url_bits:
if bit.startswith('v'):
return bit[1:]
return '1.0' # Who knows? Let's just try 1.0 ...
@property
def trove_client(self):
if self._trove_client is None:
endpoint = self.get_endpoint(
service_type=self.get_service_type('database'))
trove_api_version = self._get_trove_api_version(endpoint)
# Make the connection - can't use keystone session until there
# is one
self._trove_client = trove_client.Client(
trove_api_version,
session=self.keystone_session,
region_name=self.region_name,
service_type=self.get_service_type('database'),
)
if self._trove_client is None:
raise OpenStackCloudException(
"Failed to instantiate Trove client."
" This could mean that your credentials are wrong.")
return self._trove_client
@property
def neutron_client(self):
if self._neutron_client is None:
self._neutron_client = neutron_client.Client(
token=self.auth_token,
session=self.keystone_session,
region_name=self.region_name)
return self._neutron_client
def get_name(self):
return self.name
def get_region(self):
return self.region_name
@property
def flavor_cache(self):
@self._cache.cache_on_arguments()
def _flavor_cache():
return {flavor.id: flavor for flavor in
self.nova_client.flavors.list()}
return _flavor_cache()
def get_flavor_name(self, flavor_id):
flavor = self.flavor_cache.get(flavor_id, None)
if flavor:
return flavor.name
return None
def get_flavor(self, name_or_id):
for id, flavor in self.flavor_cache.items():
if name_or_id in (id, flavor.name):
return flavor
return None
def get_flavor_by_ram(self, ram, include=None):
for flavor in sorted(
self.flavor_cache.values(),
key=operator.attrgetter('ram')):
if (flavor.ram >= ram and
(not include or include in flavor.name)):
return flavor
raise OpenStackCloudException(
"Cloud not find a flavor with {ram} and '{include}'".format(
ram=ram, include=include))
def get_endpoint(self, service_type):
if service_type in self.endpoints:
return self.endpoints[service_type]
try:
endpoint = self.keystone_session.get_endpoint(
service_type=service_type,
interface=self.endpoint_type,
region_name=self.region_name)
except Exception as e:
self.log.debug("keystone cannot get endpoint", exc_info=True)
raise OpenStackCloudException(
"Error getting %s endpoint: %s" % (service_type, e.message))
return endpoint
def list_servers(self):
return self.nova_client.servers.list()
def list_server_dicts(self):
return [self.get_openstack_vars(server)
for server in self.list_servers()]
def list_keypairs(self):
return self.nova_client.keypairs.list()
def list_keypair_dicts(self):
return [meta.obj_to_dict(keypair)
for keypair in self.list_keypairs()]
def create_keypair(self, name, public_key):
return self.nova_client.keypairs.create(name, public_key)
def delete_keypair(self, name):
return self.nova_client.keypairs.delete(name)
@property
def extension_cache(self):
if not self._extension_cache:
self._extension_cache = set()
try:
resp, body = self.nova_client.client.get('/extensions')
if resp.status_code == 200:
for x in body['extensions']:
self._extension_cache.add(x['alias'])
except nova_exceptions.NotFound:
pass
return self._extension_cache
def has_extension(self, extension_name):
return extension_name in self.extension_cache
def list_networks(self):
return self.neutron_client.list_networks()['networks']
def get_network(self, name_or_id):
for network in self.list_networks():
if name_or_id in (network['id'], network['name']):
return network
return None
def _get_images_from_cloud(self, filter_deleted):
# First, try to actually get images from glance, it's more efficient
images = dict()
try:
# This can fail both because we don't have glanceclient installed
# and because the cloud may not expose the glance API publically
image_list = self.glance_client.images.list()
except (OpenStackCloudException,
glanceclient.exc.HTTPInternalServerError):
# We didn't have glance, let's try nova
# If this doesn't work - we just let the exception propagate
image_list = self.nova_client.images.list()
for image in image_list:
# The cloud might return DELETED for invalid images.
# While that's cute and all, that's an implementation detail.
if not filter_deleted:
images[image.id] = image
elif image.status != 'DELETED':
images[image.id] = image
return images
def _reset_image_cache(self):
self._image_cache = None
def list_images(self, filter_deleted=True):
"""Get available glance images.
:param filter_deleted: Control whether deleted images are returned.
:returns: A dictionary of glance images indexed by image UUID.
"""
@self._cache.cache_on_arguments()
def _list_images():
return self._get_images_from_cloud(filter_deleted)
return _list_images()
def get_image_name(self, image_id, exclude=None):
image = self.get_image(image_id, exclude)
if image:
return image.id
self._image_cache[image_id] = None
return None
def get_image_id(self, image_name, exclude=None):
image = self.get_image(image_name, exclude)
if image:
return image.id
return None
def get_image(self, name_or_id, exclude=None):
for (image_id, image) in self.list_images().items():
if image_id == name_or_id:
return image
if (image is not None and
name_or_id in image.name and (
not exclude or exclude not in image.name)):
return image
return None
def get_image_dict(self, name_or_id, exclude=None):
image = self.get_image(name_or_id, exclude)
if not image:
return image
if getattr(image, 'validate', None):
# glanceclient returns a "warlock" object if you use v2
return meta.warlock_to_dict(image)
else:
# glanceclient returns a normal object if you use v1
return meta.obj_to_dict(image)
def create_image_snapshot(self, name, **metadata):
image = self.nova_client.servers.create_image(
name, **metadata)
if image:
return meta.obj_to_dict(image)
return None
def create_image(
self, name, filename, container='images',
md5=None, sha256=None,
disk_format=None, container_format=None,
wait=False, timeout=3600, **kwargs):
if not md5 or not sha256:
(md5, sha256) = self._get_file_hashes(filename)
current_image = self.get_image_dict(name)
if (current_image and current_image.get(IMAGE_MD5_KEY, '') == md5
and current_image.get(IMAGE_SHA256_KEY, '') == sha256):
self.log.debug(
"image {name} exists and is up to date".format(name=name))
return current_image
kwargs[IMAGE_MD5_KEY] = md5
kwargs[IMAGE_SHA256_KEY] = sha256
# This makes me want to die inside
glance_api_version = self._get_glance_api_version()
if glance_api_version == '2':
return self._upload_image_v2(
name, filename, container,
current_image=current_image,
wait=wait, timeout=timeout, **kwargs)
elif glance_api_version == '1':
image_kwargs = dict(properties=kwargs)
if disk_format:
image_kwargs['disk_format'] = disk_format
if container_format:
image_kwargs['container_format'] = container_format
return self._upload_image_v1(name, filename, **image_kwargs)
def _upload_image_v1(self, name, filename, **image_kwargs):
image = self.glance_client.images.create(name=name, **image_kwargs)
image.update(data=open(filename, 'rb'))
return self.get_image_dict(image.id)
def _upload_image_v2(
self, name, filename, container, current_image=None,
wait=True, timeout=None, **image_properties):
self.create_object(
container, name, filename,
md5=image_properties.get('md5', None),
sha256=image_properties.get('sha256', None))
if not current_image:
current_image = self.get_image(name)
# TODO(mordred): Can we do something similar to what nodepool does
# using glance properties to not delete then upload but instead make a
# new "good" image and then mark the old one as "bad"
# self.glance_client.images.delete(current_image)
task = self.glance_client.tasks.create(
type='import', input=dict(
import_from='{container}/{name}'.format(
container=container, name=name),
image_properties=dict(name=name)))
if wait:
for count in _iterate_timeout(
timeout,
"Timeout waiting for the image to import."):
try:
status = self.glance_client.tasks.get(task.id)
except glanceclient.exc.HTTPServiceUnavailable:
# Intermittent failure - catch and try again
continue
if status.status == 'success':
self._reset_image_cache()
image = self.get_image(status.result['image_id'])
self.update_image_properties(
image=image,
**image_properties)
return self.get_image_dict(status.result['image_id'])
if status.status == 'failure':
raise OpenStackCloudException(
"Image creation failed: {message}".format(
message=status.message),
extra_data=status)
else:
return meta.warlock_to_dict(task)
def update_image_properties(
self, image=None, name_or_id=None, **properties):
if not image:
image = self.get_image(name_or_id)
img_props = {}
for k, v in properties.iteritems():
if v and k in ['ramdisk', 'kernel']:
v = self.get_image_id(v)
k = '{0}_id'.format(k)
img_props[k] = v
# This makes me want to die inside
if self._get_glance_api_version() == '2':
return self._update_image_properties_v2(image, img_props)
else:
return self._update_image_properties_v1(image, img_props)
def _update_image_properties_v2(self, image, properties):
img_props = {}
for k, v in properties.iteritems():
if image.get(k, None) != v:
img_props[k] = str(v)
if not img_props:
return False
self.glance_client.images.update(image.id, **img_props)
return True
def _update_image_properties_v1(self, image, properties):
img_props = {}
for k, v in properties.iteritems():
if image.properties.get(k, None) != v:
img_props[k] = v
if not img_props:
return False
image.update(properties=img_props)
return True
def create_volume(self, wait=True, timeout=None, **volkwargs):
"""Create a volume.
:param wait: If true, waits for volume to be created.
:param timeout: Seconds to wait for volume creation. None is forever.
:param volkwargs: Keyword arguments as expected for cinder client.
:returns: The created volume object.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
cinder = self.cinder_client
try:
volume = cinder.volumes.create(**volkwargs)
except Exception as e:
self.log.debug("Volume creation failed", exc_info=True)
raise OpenStackCloudException(
"Error in creating volume: %s" % e.message)
if volume.status == 'error':
raise OpenStackCloudException("Error in creating volume")
if wait:
vol_id = volume.id
for count in _iterate_timeout(
timeout,
"Timeout waiting for the volume to be available."):
volume = self.get_volume(vol_id, cache=False, error=False)
if not volume:
continue
if volume.status == 'available':
return volume
if volume.status == 'error':
raise OpenStackCloudException(
"Error in creating volume, please check logs")
def delete_volume(self, name_or_id=None, wait=True, timeout=None):
"""Delete a volume.
:param name_or_id: Name or unique ID of the volume.
:param wait: If true, waits for volume to be deleted.
:param timeout: Seconds to wait for volume deletion. None is forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
cinder = self.cinder_client
volume = self.get_volume(name_or_id, cache=False)
try:
cinder.volumes.delete(volume.id)
except Exception as e:
self.log.debug("Volume deletion failed", exc_info=True)
raise OpenStackCloudException(
"Error in deleting volume: %s" % e.message)
if wait:
for count in _iterate_timeout(
timeout,
"Timeout waiting for the volume to be deleted."):
if not self.volume_exists(volume.id):
return
def _get_volumes_from_cloud(self):
try:
return self.cinder_client.volumes.list()
except Exception:
return []
def list_volumes(self, cache=True):
@self._cache.cache_on_arguments()
def _list_volumes():
return self._get_volumes_from_cloud()
return _list_volumes()
def get_volumes(self, server, cache=True):
volumes = []
for volume in self.list_volumes(cache=cache):
for attach in volume.attachments:
if attach['server_id'] == server.id:
volumes.append(volume)
return volumes
def get_volume_id(self, name_or_id):
image = self.get_volume(name_or_id)
if image:
return image.id
return None
def get_volume(self, name_or_id, cache=True, error=True):
for v in self.list_volumes(cache=cache):
if name_or_id in (v.display_name, v.id):
return v
if error:
raise OpenStackCloudException(
"Error finding volume from %s" % name_or_id)
return None
def volume_exists(self, name_or_id):
return self.get_volume(
name_or_id, cache=False, error=False) is not None
def get_volume_attach_device(self, volume, server_id):
"""Return the device name a volume is attached to for a server.
This can also be used to verify if a volume is attached to
a particular server.
:param volume: Volume object
:param server_id: ID of server to check
:returns: Device name if attached, None if volume is not attached.
"""
for attach in volume.attachments:
if server_id == attach['server_id']:
return attach['device']
return None
def detach_volume(self, server, volume, wait=True, timeout=None):
"""Detach a volume from a server.
:param server: The server object to detach from.
:param volume: The volume object to detach.
:param wait: If true, waits for volume to be detached.
:param timeout: Seconds to wait for volume detachment. None is forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
nova = self.nova_client
dev = self.get_volume_attach_device(volume, server.id)
if not dev:
raise OpenStackCloudException(
"Volume %s is not attached to server %s"
% (volume.id, server.id)
)
try:
nova.volumes.delete_server_volume(server.id, volume.id)
except Exception as e:
self.log.debug("nova volume detach failed", exc_info=True)
raise OpenStackCloudException(
"Error detaching volume %s from server %s: %s" %
(volume.id, server.id, e)
)
if wait:
for count in _iterate_timeout(
timeout,
"Timeout waiting for volume %s to detach." % volume.id):
try:
vol = self.get_volume(volume.id, cache=False)
except Exception:
self.log.debug(
"Error getting volume info %s" % volume.id,
exc_info=True)
continue
if vol.status == 'available':
return
if vol.status == 'error':
raise OpenStackCloudException(
"Error in detaching volume %s" % volume.id
)
def attach_volume(self, server, volume, device=None,
wait=True, timeout=None):
"""Attach a volume to a server.
This will attach a volume, described by the passed in volume
object (as returned by get_volume()), to the server described by
the passed in server object (as returned by get_server()) on the
named device on the server.
If the volume is already attached to the server, or generally not
available, then an exception is raised. To re-attach to a server,
but under a different device, the user must detach it first.
:param server: The server object to attach to.
:param volume: The volume object to attach.
:param device: The device name where the volume will attach.
:param wait: If true, waits for volume to be attached.
:param timeout: Seconds to wait for volume attachment. None is forever.
:raises: OpenStackCloudTimeout if wait time exceeded.
:raises: OpenStackCloudException on operation error.
"""
nova = self.nova_client
dev = self.get_volume_attach_device(volume, server.id)
if dev:
raise OpenStackCloudException(
"Volume %s already attached to server %s on device %s"
% (volume.id, server.id, dev)
)
if volume.status != 'available':
raise OpenStackCloudException(
"Volume %s is not available. Status is '%s'"
% (volume.id, volume.status)
)
try:
nova.volumes.create_server_volume(server.id, volume.id, device)
except Exception as e:
self.log.debug(
"nova volume attach of %s failed" % volume.id, exc_info=True)
raise OpenStackCloudException(
"Error attaching volume %s to server %s: %s" %
(volume.id, server.id, e)
)
if wait:
for count in _iterate_timeout(
timeout,
"Timeout waiting for volume %s to attach." % volume.id):
try:
vol = self.get_volume(volume.id, cache=False)
except Exception:
self.log.debug(
"Error getting volume info %s" % volume.id,
exc_info=True)
continue
if self.get_volume_attach_device(vol, server.id):
return
# TODO(Shrews) check to see if a volume can be in error status
# and also attached. If so, we should move this
# above the get_volume_attach_device call
if vol.status == 'error':
raise OpenStackCloudException(
"Error in attaching volume %s" % volume.id
)
def get_server_id(self, name_or_id):
server = self.get_server(name_or_id)
if server:
return server.id
return None
def get_server_private_ip(self, server):
return meta.get_server_private_ip(server)
def get_server_public_ip(self, server):
return meta.get_server_public_ip(server)
def get_server(self, name_or_id):
for server in self.list_servers():
if name_or_id in (server.name, server.id):
return server
return None
def get_server_dict(self, name_or_id):
server = self.get_server(name_or_id)
if not server:
return server
return self.get_openstack_vars(server)
def get_server_meta(self, server):
server_vars = meta.get_hostvars_from_server(self, server)
groups = meta.get_groups_from_server(self, server, server_vars)
return dict(server_vars=server_vars, groups=groups)
def get_security_group(self, name_or_id):
for secgroup in self.nova_client.security_groups.list():
if name_or_id in (secgroup.name, secgroup.id):
return secgroup
return None
def get_openstack_vars(self, server):
return meta.get_hostvars_from_server(self, server)
def add_ip_from_pool(self, server, pools):
# instantiate FloatingIPManager object
floating_ip_obj = floating_ips.FloatingIPManager(self.nova_client)
# empty dict and list
usable_floating_ips = {}
# get the list of all floating IPs. Mileage may
# vary according to Nova Compute configuration
# per cloud provider
all_floating_ips = floating_ip_obj.list()
# iterate through all pools of IP address. Empty
# string means all and is the default value
for pool in pools:
# temporary list per pool
pool_ips = []
# loop through all floating IPs
for f_ip in all_floating_ips:
# if not reserved and the correct pool, add
if f_ip.instance_id is None and (f_ip.pool == pool):
pool_ips.append(f_ip.ip)
# only need one
break
# if the list is empty, add for this pool
if not pool_ips:
try:
new_ip = self.nova_client.floating_ips.create(pool)
except Exception:
self.log.debug(
"nova floating ip create failed", exc_info=True)
raise OpenStackCloudException(
"Unable to create floating ip in pool %s" % pool)
pool_ips.append(new_ip.ip)
# Add to the main list
usable_floating_ips[pool] = pool_ips
# finally, add ip(s) to instance for each pool
for pool in usable_floating_ips:
for ip in usable_floating_ips[pool]:
self.add_ip_list(server, [ip])
# We only need to assign one ip - but there is an inherent
# race condition and some other cloud operation may have
# stolen an available floating ip
break
def add_ip_list(self, server, ips):
# add ip(s) to instance
for ip in ips:
try:
server.add_floating_ip(ip)
except Exception as e:
self.log.debug(
"nova floating ip add failed", exc_info=True)
raise OpenStackCloudException(
"Error attaching IP {ip} to instance {id}: {msg} ".format(
ip=ip, id=server.id, msg=e.message))
def add_auto_ip(self, server):
try:
new_ip = self.nova_client.floating_ips.create()
except Exception as e:
self.log.debug(
"nova floating ip create failed", exc_info=True)
raise OpenStackCloudException(
"Unable to create floating ip: %s" % (e.message))
try:
self.add_ip_list(server, [new_ip])
except OpenStackCloudException:
# Clean up - we auto-created this ip, and it's not attached
# to the server, so the cloud will not know what to do with it
self.nova_client.floating_ips.delete(new_ip)
raise
def add_ips_to_server(self, server, auto_ip=True, ips=None, ip_pool=None):
if ip_pool:
self.add_ip_from_pool(server, ip_pool)
elif ips:
self.add_ip_list(server, ips)
elif auto_ip:
if self.get_server_public_ip(server):
return server
self.add_auto_ip(server)
else:
return server
# this may look redundant, but if there is now a
# floating IP, then it needs to be obtained from
# a recent server object if the above code path exec'd
try:
server = self.nova_client.servers.get(server.id)
except Exception as e:
self.log.debug("nova info failed", exc_info=True)
raise OpenStackCloudException(
"Error in getting info from instance: %s " % e.message)
return server
def create_server(self, auto_ip=True, ips=None, ip_pool=None,
root_volume=None, terminate_volume=False,
wait=False, timeout=180, **bootkwargs):
if root_volume:
if terminate_volume:
suffix = ':::1'
else:
suffix = ':::0'
volume_id = self.get_volume_id(root_volume) + suffix
if 'block_device_mapping' not in bootkwargs:
bootkwargs['block_device_mapping'] = dict()
bootkwargs['block_device_mapping']['vda'] = volume_id
try:
server = self.nova_client.servers.create(**bootkwargs)
server = self.nova_client.servers.get(server.id)
except Exception as e:
self.log.debug("nova instance create failed", exc_info=True)
raise OpenStackCloudException(
"Error in creating instance: %s" % e.message)
if server.status == 'ERROR':
raise OpenStackCloudException(
"Error in creating the server.")
if wait:
for count in _iterate_timeout(
timeout,
"Timeout waiting for the server to come up."):
try:
server = self.nova_client.servers.get(server.id)
except Exception:
continue
if server.status == 'ACTIVE':
return self.add_ips_to_server(
server, auto_ip, ips, ip_pool)
if server.status == 'ERROR':
raise OpenStackCloudException(
"Error in creating the server",
extra_data=dict(
server=meta.obj_to_dict(server)))
return server
def delete_server(self, name, wait=False, timeout=180):
server_list = self.nova_client.servers.list(True, {'name': name})
if server_list:
server = [x for x in server_list if x.name == name]
self.nova_client.servers.delete(server.pop())
if not wait:
return
for count in _iterate_timeout(
timeout,
"Timed out waiting for server to get deleted."):
server = self.nova_client.servers.list(True, {'name': name})
if not server:
return
def get_container(self, name, skip_cache=False):
if skip_cache or name not in self._container_cache:
try:
container = self.swift_client.head_container(name)
self._container_cache[name] = container
except swift_exceptions.ClientException as e:
if e.http_status == 404:
return None
self.log.debug("swift container fetch failed", exc_info=True)
raise OpenStackCloudException(
"Container fetch failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
return self._container_cache[name]
def create_container(self, name, public=False):
container = self.get_container(name)
if container:
return container
try:
self.swift_client.put_container(name)
if public:
self.set_container_access(name, 'public')
return self.get_container(name, skip_cache=True)
except swift_exceptions.ClientException as e:
self.log.debug("swift container create failed", exc_info=True)
raise OpenStackCloudException(
"Container creation failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
def delete_container(self, name):
try:
self.swift_client.delete_container(name)
except swift_exceptions.ClientException as e:
if e.http_status == 404:
return
self.log.debug("swift container delete failed", exc_info=True)
raise OpenStackCloudException(
"Container deletion failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
def update_container(self, name, headers):
try:
self.swift_client.post_container(name, headers)
except swift_exceptions.ClientException as e:
self.log.debug("swift container update failed", exc_info=True)
raise OpenStackCloudException(
"Container update failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
def set_container_access(self, name, access):
if access not in OBJECT_CONTAINER_ACLS:
raise OpenStackCloudException(
"Invalid container access specified: %s. Must be one of %s"
% (access, list(OBJECT_CONTAINER_ACLS.keys())))
header = {'x-container-read': OBJECT_CONTAINER_ACLS[access]}
self.update_container(name, header)
def get_container_access(self, name):
container = self.get_container(name, skip_cache=True)
if not container:
raise OpenStackCloudException("Container not found: %s" % name)
acl = container.get('x-container-read', '')
try:
return [p for p, a in OBJECT_CONTAINER_ACLS.items()
if acl == a].pop()
except IndexError:
raise OpenStackCloudException(
"Could not determine container access for ACL: %s." % acl)
def _get_file_hashes(self, filename):
if filename not in self._file_hash_cache:
md5 = hashlib.md5()
sha256 = hashlib.sha256()
with open(filename, 'rb') as file_obj:
for chunk in iter(lambda: file_obj.read(8192), b''):
md5.update(chunk)
sha256.update(chunk)
self._file_hash_cache[filename] = dict(
md5=md5.hexdigest(), sha256=sha256.hexdigest())
return (self._file_hash_cache[filename]['md5'],
self._file_hash_cache[filename]['sha256'])
def is_object_stale(
self, container, name, filename, file_md5=None, file_sha256=None):
metadata = self.get_object_metadata(container, name)
if not metadata:
self.log.debug(
"swift stale check, no object: {container}/{name}".format(
container=container, name=name))
return True
if file_md5 is None or file_sha256 is None:
(file_md5, file_sha256) = self._get_file_hashes(filename)
if metadata.get(OBJECT_MD5_KEY, '') != file_md5:
self.log.debug(
"swift md5 mismatch: {filename}!={container}/{name}".format(
filename=filename, container=container, name=name))
return True
if metadata.get(OBJECT_SHA256_KEY, '') != file_sha256:
self.log.debug(
"swift sha256 mismatch: {filename}!={container}/{name}".format(
filename=filename, container=container, name=name))
return True
self.log.debug(
"swift object up to date: {container}/{name}".format(
container=container, name=name))
return False
def create_object(
self, container, name, filename=None,
md5=None, sha256=None, **headers):
if not filename:
filename = name
if self.is_object_stale(container, name, filename, md5, sha256):
with open(filename, 'r') as fileobj:
self.log.debug(
"swift uploading {filename} to {container}/{name}".format(
filename=filename, container=container, name=name))
self.swift_client.put_object(container, name, contents=fileobj)
(md5, sha256) = self._get_file_hashes(filename)
headers[OBJECT_MD5_KEY] = md5
headers[OBJECT_SHA256_KEY] = sha256
self.swift_client.post_object(container, name, headers=headers)
def delete_object(self, container, name):
if not self.get_object_metadata(container, name):
return
try:
self.swift_client.delete_object(container, name)
except swift_exceptions.ClientException as e:
raise OpenStackCloudException(
"Object deletion failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
def get_object_metadata(self, container, name):
try:
return self.swift_client.head_object(container, name)
except swift_exceptions.ClientException as e:
if e.http_status == 404:
return None
self.log.debug("swift metadata fetch failed", exc_info=True)
raise OpenStackCloudException(
"Object metadata fetch failed: %s (%s/%s)" % (
e.http_reason, e.http_host, e.http_path))
class OperatorCloud(OpenStackCloud):
@property
def auth_token(self):
if self.auth_plugin in (None, "None", ''):
return self._auth_token
if not self._auth_token:
self._auth_token = self.keystone_session.get_token()
return self._auth_token
@property
def ironic_client(self):
if self._ironic_client is None:
ironic_logging = logging.getLogger('ironicclient')
ironic_logging.addHandler(logging.NullHandler())
token = self.auth_token
if self.auth_plugin in (None, "None", ''):
# TODO: This needs to be improved logic wise, perhaps a list,
# or enhancement of the data stuctures with-in the library
# to allow for things aside password authentication, or no
# authentication if so desired by the user.
#
# Attempt to utilize a pre-stored endpoint in the auth
# dict as the endpoint.
endpoint = self.auth['endpoint']
else:
endpoint = self.get_endpoint(service_type='baremetal')
try:
self._ironic_client = ironic_client.Client(
'1', endpoint, token=token)
except Exception as e:
self.log.debug("ironic auth failed", exc_info=True)
raise OpenStackCloudException(
"Error in connecting to ironic: %s" % e.message)
return self._ironic_client
def list_nics(self):
return self.ironic_client.port.list()
def list_nics_for_machine(self, uuid):
return self.ironic_client.node.list_ports(uuid)
def get_nic_by_mac(self, mac):
try:
return self.ironic_client.port.get(mac)
except ironic_exceptions.ClientException:
return None
def list_machines(self):
return self.ironic_client.node.list()
def get_machine_by_uuid(self, uuid):
try:
return self.ironic_client.node.get(uuid)
except ironic_exceptions.ClientException:
return None
def get_machine_by_mac(self, mac):
try:
port = self.ironic_client.port.get(mac)
return self.ironic_client.node.get(port.node_uuid)
except ironic_exceptions.ClientException:
return None
def register_machine(self, nics, **kwargs):
try:
machine = self.ironic_client.node.create(**kwargs)
except Exception as e:
self.log.debug("ironic machine registration failed", exc_info=True)
raise OpenStackCloudException(
"Error registering machine with Ironic: %s" % e.message)
created_nics = []
try:
for row in nics:
nic = self.ironic_client.port.create(address=row['mac'],
node_uuid=machine.uuid)
created_nics.append(nic.uuid)
except Exception as e:
self.log.debug("ironic NIC registration failed", exc_info=True)
# TODO(mordred) Handle failures here
for uuid in created_nics:
self.ironic_client.port.delete(uuid)
self.ironic_client.node.delete(machine.uuid)
raise OpenStackCloudException(
"Error registering NICs with Ironic: %s" % e.message)
return machine
def unregister_machine(self, nics, uuid):
for nic in nics:
try:
self.ironic_client.port.delete(
self.ironic_client.port.get_by_address(nic['mac']))
except Exception as e:
self.log.debug(
"ironic NIC unregistration failed", exc_info=True)
raise OpenStackCloudException(e.message)
try:
self.ironic_client.node.delete(uuid)
except Exception as e:
self.log.debug(
"ironic machine unregistration failed", exc_info=True)
raise OpenStackCloudException(
"Error unregistering machine from Ironic: %s" % e.message)
def validate_node(self, uuid):
try:
ifaces = self.ironic_client.node.validate(uuid)
except Exception as e:
self.log.debug(
"ironic node validation call failed", exc_info=True)
raise OpenStackCloudException(e.message)
if not ifaces.deploy or not ifaces.power:
raise OpenStackCloudException(
"ironic node %s failed to validate. "
"(deploy: %s, power: %s)" % (ifaces.deploy, ifaces.power))
def node_set_provision_state(self, uuid, state, configdrive=None):
try:
self.ironic_client.node.set_provision_state(
uuid,
state,
configdrive
)
except Exception as e:
self.log.debug(
"ironic node failed change provision state to %s" % state,
exc_info=True)
raise OpenStackCloudException(e.message)
def activate_node(self, uuid, configdrive=None):
self.node_set_provision_state(uuid, 'active', configdrive)
def deactivate_node(self, uuid):
self.node_set_provision_state(uuid, 'deleted')
def set_node_instance_info(self, uuid, patch):
try:
self.ironic_client.node.update(uuid, patch)
except Exception as e:
self.log.debug(
"Failed to update instance_info", exc_info=True)
raise OpenStackCloudException(e.message)
def purge_node_instance_info(self, uuid):
patch = []
patch.append({'op': 'remove', 'path': '/instance_info'})
try:
return self.ironic_client.node.update(uuid, patch)
except Exception as e:
self.log.debug(
"Failed to delete instance_info", exc_info=True)
raise OpenStackCloudException(e.message)