--- /dev/null
+
+#
+# Copyright 2016 RIFT.IO Inc
+#
+# 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 itertools
+import logging
+import os
+import glanceclient
+import keystoneclient.v3.client as keystone_client
+from keystoneauth1 import (
+ identity as keystone_identity,
+ session as keystone_session
+ )
+
+from gi.repository import RwcalYang
+
+logger = logging.getLogger(name=__name__)
+
+
+class OpenstackImageError(Exception):
+ pass
+
+
+class OpenstackNonUniqueImageError(OpenstackImageError):
+ pass
+
+
+class OpenstackImageCreateError(Exception):
+ pass
+
+
+class OpenstackImageDeleteError(Exception):
+ pass
+
+
+class InvalidImageError(Exception):
+ pass
+
+
+class OpenstackAccount(object):
+ def __init__(self, auth_url, tenant, username, password):
+ self.auth_url = auth_url
+ self.tenant = tenant
+ self.username = username
+ self.password = password
+
+
+class OpenstackImage(object):
+ """ This value class encapsultes the RIFT-relevent glance image fields """
+
+ FIELDS = ["id", "name", "checksum", "disk_format",
+ "container_format", "size", "properties", "status"]
+ OPTIONAL_FIELDS = ["id", "checksum", "location"]
+
+ def __init__(self, name, disk_format, container_format, size,
+ properties=None, id=None, checksum=None, status="saving",
+ location=None):
+ self.name = name
+ self.disk_format = disk_format
+ self.container_format = container_format
+ self.size = size
+ self.properties = properties if properties is not None else {}
+ self.status = status
+
+ self.id = id
+ self.checksum = checksum
+
+ @classmethod
+ def from_image_response(cls, image):
+ """ Convert a image response from glance into a OpenstackImage
+
+ Arguments:
+ image - A glance image object (from glance_client.images.list() for example)
+
+ Returns:
+ An instance of OpenstackImage
+
+ Raises:
+ OpenstackImageError - Could not convert the response into a OpenstackImage object
+ """
+ missing_fields = [field for field in cls.FIELDS
+ if field not in cls.OPTIONAL_FIELDS and not hasattr(image, field)]
+ if missing_fields:
+ raise OpenstackImageError(
+ "Openstack image is missing required fields: %s" % missing_fields
+ )
+
+ kwargs = {field: getattr(image, field) for field in cls.FIELDS}
+
+ return cls(**kwargs)
+
+
+class OpenstackKeystoneClient(object):
+ """ This class wraps the Keystone Client """
+ def __init__(self, ks_client):
+ self._ks_client = ks_client
+
+ @property
+ def auth_token(self):
+ return self._ks_client.auth_token
+
+ @classmethod
+ def from_openstack_account(cls, os_account):
+ ks_client = keystone_client.Client(
+ insecure=True,
+ auth_url=os_account.auth_url,
+ username=os_account.username,
+ password=os_account.password,
+ tenant_name=os_account.tenant
+ )
+
+ return cls(ks_client)
+
+ @property
+ def glance_endpoint(self):
+ """ Return the glance endpoint from the keystone service """
+ glance_ep = self._ks_client.service_catalog.url_for(
+ service_type='image',
+ endpoint_type='publicURL'
+ )
+
+ return glance_ep
+
+
+class OpenstackGlanceClient(object):
+ def __init__(self, log, glance_client):
+ self._log = log
+ self._client = glance_client
+
+ @classmethod
+ def from_ks_client(cls, log, ks_client):
+ """ Create a OpenstackGlanceClient from a keystone client instance
+
+ Arguments:
+ log - logger instance
+ ks_client - A keystone client instance
+ """
+
+ glance_ep = ks_client.glance_endpoint
+ glance_client = glanceclient.Client(
+ '1',
+ glance_ep,
+ token=ks_client.auth_token,
+ )
+
+ return cls(log, glance_client)
+
+ @classmethod
+ def from_token(cls, log, host, port, token):
+ """ Create a OpenstackGlanceClient instance using a keystone auth token
+
+ Arguments:
+ log - logger instance
+ host - the glance host
+ port - the glance port
+ token - the keystone token
+
+ Returns:
+ A OpenstackGlanceClient instance
+ """
+ endpoint = "http://{}:{}".format(host, port)
+ glance_client = glanceclient.Client("1", endpoint, token=token)
+ return cls(log, glance_client)
+
+ def get_image_list(self):
+ """ Return the list of images from the Glance server
+
+ Returns:
+ A list of OpenstackImage instances
+ """
+ images = []
+ for image in itertools.chain(
+ self._client.images.list(is_public=False),
+ self._client.images.list(is_public=True)
+ ):
+ images.append(OpenstackImage.from_image_response(image))
+
+ return images
+
+ def get_image_data(self, image_id):
+ """ Return a image bytes generator from a image id
+
+ Arguments:
+ image_id - An image id that exists on the glance server
+
+ Returns:
+ An generator which produces the image data bytestrings
+
+ Raises:
+ OpenstackImageError - Could not find the image id
+ """
+
+ try:
+ self._client.images.get(image_id)
+ except Exception as e:
+ msg = "Failed to find image from image: %s" % image_id
+ self._log.exception(msg)
+ raise OpenstackImageError(msg) from e
+
+ img_data = self._client.images.data(image_id)
+ return img_data
+
+ def find_active_image(self, id=None, name=None, checksum=None):
+ """ Find an active images on the glance server
+
+ Arguments:
+ id - the image id to match
+ name - the image name to match
+ checksum - the image checksum to match
+
+ Returns:
+ A OpenstackImage instance
+
+ Raises:
+ OpenstackImageError - could not find a matching image
+ with matching image name and checksum
+ """
+ if id is None and name is None:
+ raise ValueError("image id or image name must be provided")
+
+ self._log.debug("attempting to find active image with id %s name %s and checksum %s",
+ id, name, checksum)
+
+ found_image = None
+
+ image_list = self.get_image_list()
+ self._log.debug("got image list from openstack: %s", image_list)
+ for image in self.get_image_list():
+ self._log.debug(image)
+ if image.status != "active":
+ continue
+
+ if id is not None:
+ if image.id != id:
+ continue
+
+ if name is not None:
+ if image.name != name:
+ continue
+
+ if checksum is not None:
+ if image.checksum != checksum:
+ continue
+
+ if found_image is not None:
+ raise OpenstackNonUniqueImageError(
+ "Found multiple images that matched the criteria. Use image id to disambiguate."
+ )
+
+ found_image = image
+
+ if found_image is None:
+ raise OpenstackImageError(
+ "could not find an active image with id %s name %s and checksum %s" %
+ (id, name, checksum))
+
+ return OpenstackImage.from_image_response(found_image)
+
+ def create_image_from_hdl(self, image, file_hdl):
+ """ Create an image on the glance server a file handle
+
+ Arguments:
+ image - An OpenstackImage instance
+ file_hdl - An open image file handle
+
+ Raises:
+ OpenstackImageCreateError - Could not upload the image
+ """
+ try:
+ self._client.images.create(
+ name=image.name,
+ is_public="False",
+ disk_format=image.disk_format,
+ container_format=image.container_format,
+ data=file_hdl
+ )
+ except Exception as e:
+ msg = "Failed to Openstack upload image"
+ self._log.exception(msg)
+ raise OpenstackImageCreateError(msg) from e
+
+ def create_image_from_url(self, image_url, image_name, image_checksum=None,
+ disk_format=None, container_format=None):
+ """ Create an image on the glance server from a image url
+
+ Arguments:
+ image_url - An HTTP image url
+ image_name - An openstack image name (filename with proper extension)
+ image checksum - The image md5 checksum
+
+ Raises:
+ OpenstackImageCreateError - Could not create the image
+ """
+ def disk_format_from_image_name(image_name):
+ _, image_ext = os.path.splitext(image_name)
+ if not image_ext:
+ raise InvalidImageError("image name must have an extension")
+
+ # Strip off the .
+ image_ext = image_ext[1:]
+
+ if not hasattr(RwcalYang.DiskFormat, image_ext.upper()):
+ raise InvalidImageError("unknown image extension for disk format: %s", image_ext)
+
+ disk_format = image_ext.lower()
+ return disk_format
+
+ # If the disk format was not provided, attempt to match via the file
+ # extension.
+ if disk_format is None:
+ disk_format = disk_format_from_image_name(image_name)
+
+ if container_format is None:
+ container_format = "bare"
+
+ create_args = dict(
+ location=image_url,
+ name=image_name,
+ is_public="True",
+ disk_format=disk_format,
+ container_format=container_format,
+ )
+
+ if image_checksum is not None:
+ create_args["checksum"] = image_checksum
+
+ try:
+ self._log.debug("creating an image from url: %s", create_args)
+ image = self._client.images.create(**create_args)
+ except Exception as e:
+ msg = "Failed to create image from url in openstack"
+ self._log.exception(msg)
+ raise OpenstackImageCreateError(msg) from e
+
+ return OpenstackImage.from_image_response(image)
+
+ def delete_image_from_id(self, image_id):
+ self._log.info("Deleting image from catalog: %s", image_id)
+ try:
+ image = self._client.images.delete(image_id)
+ except Exception as e:
+ msg = "Failed to delete image %s in openstack" % image_id
+ self._log.exception(msg)
+ raise OpenstackImageDeleteError(msg)