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:
parent
59af73e47b
commit
c9e0396752
|
@ -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()
|
|
@ -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"),
|
||||
]
|
|
@ -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())
|
|
@ -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)
|
Loading…
Reference in New Issue