Add Azure support for Glance

This drivers defines Glance location format for Azure images
and support for adding info of Azure images inside glance.

Change-Id: I68954be5b926b7f390b275c459484051618d8ebd
Implements: blueprint azure-support
This commit is contained in:
Sanket 2017-08-22 15:02:23 +05:30
parent 59af73e47b
commit c9e0396752
5 changed files with 398 additions and 0 deletions

View File

@ -0,0 +1,165 @@
"""
Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com)
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.
"""
from azure.common.credentials import ServicePrincipalCredentials
from azure.mgmt.compute import ComputeManagementClient
from functools import partial
from glanceclient import Client
from keystoneauth1 import loading
from keystoneauth1 import session
import hashlib
import os
import sys
import uuid
def get_credentials(tenant_id, client_id, client_secret):
credentials = ServicePrincipalCredentials(
client_id=client_id, secret=client_secret, tenant=tenant_id)
return credentials
def _get_client(tenant_id, client_id, client_secret, subscription_id,
cls=None):
"""Returns Azure compute resource object for interacting with Azure API
:param tenant_id: string, tenant_id from azure account
:param client_id: string, client_id (application id)
:param client_secret: string, secret key of application
:param subscription_id: string, unique identification id of account
:return: :class:`Resource <Resource>` object
"""
credentials = get_credentials(tenant_id, client_id, client_secret)
client = cls(credentials, subscription_id)
return client
get_compute_client = partial(_get_client, cls=ComputeManagementClient)
def abort(message):
sys.exit(message)
def get_env_param(env_name):
if env_name in os.environ:
return os.environ[env_name]
abort("%s environment variable not set." % env_name)
def get_keystone_session(vendor_data):
username = vendor_data['username']
password = vendor_data['password']
project_name = vendor_data['tenant_name']
auth_url = vendor_data['auth_url']
loader = loading.get_plugin_loader('password')
auth = loader.load_from_options(
auth_url=auth_url, project_name=project_name,
username=username, password=password)
sess = session.Session(auth=auth)
return sess
def get_glance_client(vendor_data):
GLANCE_VERSION = '2'
glance_client = Client(GLANCE_VERSION,
session=get_keystone_session(vendor_data))
return glance_client
class GlanceOperator(object):
def __init__(self):
auth_url = get_env_param('OS_AUTH_URL')
project_name = os.environ.get('OS_PROJECT_NAME')
tenant_name = os.environ.get('OS_TENANT_NAME')
username = get_env_param('OS_USERNAME')
password = get_env_param('OS_PASSWORD')
if not project_name:
if not tenant_name:
raise Exception("Either OS_PROJECT_NAME or OS_TENANT_NAME is "
"required.")
project_name = tenant_name
self.vendor_data = {'username': username,
'password': password,
'auth_url': auth_url,
'tenant_name': project_name}
self.glance_client = get_glance_client(self.vendor_data)
def register_image(self, image):
locations = image.pop('locations')
response = self.glance_client.images.create(**image)
glance_id = response['id']
for location in locations:
self.glance_client.images.add_location(glance_id, location['url'],
location['metadata'])
print("Registered image %s" % image['name'])
class ImageProvider(object):
def __init__(self):
self.glance_operator = GlanceOperator()
def get_public_images(self):
raise NotImplementedError()
def register_images(self):
for image_info in self.get_public_images():
self.glance_operator.register_image(image_info)
class AzureImages(ImageProvider):
def __init__(self):
super(AzureImages, self).__init__()
tenant_id = get_env_param('AZURE_TENANT_ID')
client_id = get_env_param('AZURE_CLIENT_ID')
client_secret = get_env_param('AZURE_CLIENT_SECRET')
subscription_id = get_env_param('AZURE_SUBSCRIPTION_ID')
self.region = get_env_param('AZURE_REGION')
self.resource_group = get_env_param('AZURE_RESOURCE_GROUP')
self.compute_client = get_compute_client(
tenant_id, client_id, client_secret, subscription_id)
def _azure_to_openstack_formatter(self, image_info):
"""Converts Azure image data to Openstack image data format."""
image_uuid = self._get_image_uuid(image_info.id)
location_info = [
{
'url': 'azure://{0}/{1}'.format(image_info.id.strip('/'),
image_uuid),
'metadata': {'azure_link': image_info.id}
},
]
return {'id': image_uuid,
'name': image_info.name,
'container_format': 'bare',
'disk_format': 'raw',
'visibility': 'public',
'azure_link': image_info.id,
'locations': location_info}
def _get_image_uuid(self, azure_id):
md = hashlib.md5()
md.update(azure_id)
return str(uuid.UUID(bytes=md.digest()))
def get_public_images(self):
images = self.compute_client.images
response = images.list_by_resource_group(self.resource_group)
for result in response.advance_page():
image_response = images.get(self.resource_group, result.name)
yield self._azure_to_openstack_formatter(image_response)
if __name__ == '__main__':
az_images = AzureImages()
az_images.register_images()

View File

@ -0,0 +1,25 @@
"""
Copyright 2017 Platform9 Systems Inc.(http://www.platform9.com)
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.
"""
from oslo_config import cfg
azure_group = cfg.OptGroup(name='azure',
title='Options to connect to Azure cloud')
azure_opts = [
cfg.StrOpt('tenant_id', help='Tenant id of Azure account'),
cfg.StrOpt('client_id', help='Azure client id'),
cfg.StrOpt('client_secret', help='Azure Client secret', secret=True),
cfg.StrOpt('subscription_id', help='Azure subscription id'),
cfg.StrOpt('region', help='Azure region'),
cfg.StrOpt('resource_group', help="Azure resource group"),
]

View File

@ -0,0 +1,159 @@
"""
Copyright (c) 2017 Platform9 Systems Inc. (http://www.platform9.com)
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 logging
from oslo_utils import units
from glance_store._drivers.azure import config
from glance_store._drivers.azure import utils
from glance_store import capabilities
from glance_store import driver
from glance_store import exceptions
from glance_store.i18n import _
from glance_store import location
from six.moves import urllib
LOG = logging.getLogger(__name__)
MAX_REDIRECTS = 5
STORE_SCHEME = 'azure'
class StoreLocation(location.StoreLocation):
"""Class describing Azure URI."""
uri_attrs = ['subscriptions', 'providers', 'resourcegroups', 'images']
def __init__(self, store_specs, conf):
super(StoreLocation, self).__init__(store_specs, conf)
self._sorted_uri_attrs = sorted(self.uri_attrs)
def process_specs(self):
self.scheme = self.specs.get('scheme', STORE_SCHEME)
for attr in self.uri_attrs:
setattr(self, attr, self.specs.get(attr))
def get_uri(self):
_uri_path = []
for attr in self.uri_attrs:
_uri_path.extend([attr.capitalize(), getattr(self, attr)])
return "{0}://{1}/{2}".format(self.scheme, "/".join(_uri_path),
self.glance_id)
def _parse_attrs(self, attrs_info):
attrs_list = attrs_info.strip('/').split('/')
self.glance_id = attrs_list.pop()
attrs_dict = {
attrs_list[i].lower(): attrs_list[i + 1]
for i in range(0, len(attrs_list), 2)
}
if self._sorted_uri_attrs != sorted(attrs_dict.keys()):
raise exceptions.BadStoreUri(
message="Image URI should contain required attributes")
for k, v in attrs_dict.items():
setattr(self, k, v)
def parse_uri(self, uri):
"""Parse URLs based on Azure scheme """
LOG.debug('Parse uri %s' % (uri, ))
if not uri.startswith('%s://' % STORE_SCHEME):
reason = (_("URI %(uri)s must start with %(scheme)s://") % {
'uri': uri,
'scheme': STORE_SCHEME
})
LOG.error(reason)
raise exceptions.BadStoreUri(message=reason)
pieces = urllib.parse.urlparse(uri)
self.scheme = pieces.scheme
self._parse_attrs(pieces.netloc + pieces.path)
self.image_name = self.images
class Store(driver.Store):
"""An implementation of the Azure Backend Adapter"""
_CAPABILITIES = (capabilities.BitMasks.RW_ACCESS |
capabilities.BitMasks.DRIVER_REUSABLE)
def __init__(self, conf):
super(Store, self).__init__(conf)
self.scheme = STORE_SCHEME
conf.register_group(config.azure_group)
conf.register_opts(config.azure_opts, group=config.azure_group)
self.tenant_id = conf.azure.tenant_id
self.client_id = conf.azure.client_id
self.client_secret = conf.azure.client_secret
self.subscription_id = conf.azure.subscription_id
self.resource_group = conf.azure.resource_group
self._azure_client = None
LOG.info('Initialized Azure Glance Store driver')
def get_schemes(self):
return (STORE_SCHEME, )
@property
def azure_client(self):
if self._azure_client is None:
self._azure_client = utils.get_compute_client(
self.tenant_id, self.client_id, self.client_secret,
self.subscription_id)
return self._azure_client
@capabilities.check
def get(self, location, offset=0, chunk_size=None, context=None):
"""Takes a `glance_store.location.Location` object that indicates
where to find the image file, and returns a tuple of generator
(for reading the image file) and image_size
:param location `glance_store.location.Location` object, supplied
from glance_store.location.get_location_from_uri()
"""
return '%s://generic' % self.scheme, self.get_size(location, context)
def get_size(self, location, context=None):
image_name = location.store_location.image_name.split("/")[-1]
response = utils.get_image(self.azure_client, self.resource_group,
image_name)
size = response.storage_profile.os_disk.disk_size_gb
if size is None:
return 1
return size * units.Gi
@capabilities.check
def add(self, image_id, image_file, image_size, context=None,
verifier=None):
"""Stores an image file with supplied identifier to the backend
storage system and returns a tuple containing information
about the stored image.
:param image_id: The opaque image identifier
:param image_file: The image data to write, as a file-like object
:param image_size: The size of the image data to write, in bytes
:retval: tuple of URL in backing store, bytes written, checksum
and a dictionary with storage system specific information
:raises: `glance_store.exceptions.Duplicate` if the image already
existed
"""
# Adding images is not suppported yet
raise NotImplementedError("This operation is not supported in Azure")
@capabilities.check
def delete(self, location, context=None):
"""Takes a `glance_store.location.Location` object that indicates
where to find the image file to delete
:param location: `glance_store.location.Location` object, supplied
from glance_store.location.get_location_from_uri()
:raises NotFound if image does not exist
"""
# This method works for Azure public images as we just need to delete
# entry from glance catalog.
# For Private images we will need extra handling here.
LOG.info("Delete image %s" % location.get_store_uri())

View File

@ -0,0 +1,49 @@
"""
Copyright (c) 2017 Platform9 Systems 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 expressed or implied. See the
License for the specific language governing permissions and limitations
under the License.
"""
from azure.common.credentials import ServicePrincipalCredentials
from azure.mgmt.compute import ComputeManagementClient
from functools import partial
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def get_credentials(tenant_id, client_id, client_secret):
credentials = ServicePrincipalCredentials(
client_id=client_id, secret=client_secret, tenant=tenant_id)
return credentials
def _get_client(tenant_id, client_id, client_secret, subscription_id,
cls=None):
"""Returns Azure compute resource object for interacting with Azure API
:param tenant_id: string, tenant_id from azure account
:param client_id: string, client_id (application id)
:param client_secret: string, secret key of application
:param subscription_id: string, unique identification id of account
:return: :class:`Resource <Resource>` object
"""
credentials = get_credentials(tenant_id, client_id, client_secret)
client = cls(credentials, subscription_id)
return client
get_compute_client = partial(_get_client, cls=ComputeManagementClient)
def get_image(compute, resource_group, name):
"""Return image info from Azure
"""
return compute.images.get(resource_group, name)