nova/nova/api/openstack/compute/contrib/virtual_storage_arrays.py

700 lines
23 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Zadara Storage Inc.
# Copyright (c) 2011 OpenStack LLC.
#
# 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.
""" The virtul storage array extension"""
import webob
from webob import exc
from nova.api.openstack import common
from nova.api.openstack.compute.contrib import volumes
from nova.api.openstack import extensions
from nova.api.openstack.compute import servers
from nova.api.openstack import wsgi
from nova.api.openstack import xmlutil
from nova import compute
from nova.compute import instance_types
from nova import network
from nova import exception
from nova import flags
from nova import log as logging
from nova import vsa
from nova import volume
FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
authorize = extensions.extension_authorizer('compute',
'virtual_storage_arrays')
def _vsa_view(context, vsa, details=False, instances=None):
"""Map keys for vsa summary/detailed view."""
d = {}
d['id'] = vsa.get('id')
d['name'] = vsa.get('name')
d['displayName'] = vsa.get('display_name')
d['displayDescription'] = vsa.get('display_description')
d['createTime'] = vsa.get('created_at')
d['status'] = vsa.get('status')
if 'vsa_instance_type' in vsa:
d['vcType'] = vsa['vsa_instance_type'].get('name', None)
else:
d['vcType'] = vsa['instance_type_id']
d['vcCount'] = vsa.get('vc_count')
d['driveCount'] = vsa.get('vol_count')
d['ipAddress'] = None
for instance in instances:
fixed_addr = None
floating_addr = None
if instance['fixed_ips']:
fixed = instance['fixed_ips'][0]
fixed_addr = fixed['address']
if fixed['floating_ips']:
floating_addr = fixed['floating_ips'][0]['address']
if floating_addr:
d['ipAddress'] = floating_addr
break
else:
d['ipAddress'] = d['ipAddress'] or fixed_addr
return d
def make_vsa(elem):
elem.set('id')
elem.set('name')
elem.set('displayName')
elem.set('displayDescription')
elem.set('createTime')
elem.set('status')
elem.set('vcType')
elem.set('vcCount')
elem.set('driveCount')
elem.set('ipAddress')
class VsaTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('vsa', selector='vsa')
make_vsa(root)
return xmlutil.MasterTemplate(root, 1)
class VsaSetTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('vsaSet')
elem = xmlutil.SubTemplateElement(root, 'vsa', selector='vsaSet')
make_vsa(elem)
return xmlutil.MasterTemplate(root, 1)
class VsaController(object):
"""The Virtual Storage Array API controller for the OpenStack API."""
def __init__(self):
self.vsa_api = vsa.API()
self.compute_api = compute.API()
self.network_api = network.API()
super(VsaController, self).__init__()
def _get_instances_by_vsa_id(self, context, id):
return self.compute_api.get_all(context,
search_opts={'metadata': dict(vsa_id=str(id))})
def _items(self, req, details):
"""Return summary or detailed list of VSAs."""
context = req.environ['nova.context']
authorize(context)
vsas = self.vsa_api.get_all(context)
limited_list = common.limited(vsas, req)
vsa_list = []
for vsa in limited_list:
instances = self._get_instances_by_vsa_id(context, vsa.get('id'))
vsa_list.append(_vsa_view(context, vsa, details, instances))
return {'vsaSet': vsa_list}
@wsgi.serializers(xml=VsaSetTemplate)
def index(self, req):
"""Return a short list of VSAs."""
return self._items(req, details=False)
@wsgi.serializers(xml=VsaSetTemplate)
def detail(self, req):
"""Return a detailed list of VSAs."""
return self._items(req, details=True)
@wsgi.serializers(xml=VsaTemplate)
def show(self, req, id):
"""Return data about the given VSA."""
context = req.environ['nova.context']
authorize(context)
try:
vsa = self.vsa_api.get(context, vsa_id=id)
except exception.NotFound:
raise exc.HTTPNotFound()
instances = self._get_instances_by_vsa_id(context, vsa.get('id'))
return {'vsa': _vsa_view(context, vsa, True, instances)}
@wsgi.serializers(xml=VsaTemplate)
def create(self, req, body):
"""Create a new VSA."""
context = req.environ['nova.context']
authorize(context)
if not body or 'vsa' not in body:
LOG.debug(_("No body provided"), context=context)
raise exc.HTTPUnprocessableEntity()
vsa = body['vsa']
display_name = vsa.get('displayName')
vc_type = vsa.get('vcType', FLAGS.default_vsa_instance_type)
try:
instance_type = instance_types.get_instance_type_by_name(vc_type)
except exception.NotFound:
raise exc.HTTPNotFound()
LOG.audit(_("Create VSA %(display_name)s of type %(vc_type)s"),
locals(), context=context)
args = dict(display_name=display_name,
display_description=vsa.get('displayDescription'),
instance_type=instance_type,
storage=vsa.get('storage'),
shared=vsa.get('shared'),
availability_zone=vsa.get('placement', {}).\
get('AvailabilityZone'))
vsa = self.vsa_api.create(context, **args)
instances = self._get_instances_by_vsa_id(context, vsa.get('id'))
return {'vsa': _vsa_view(context, vsa, True, instances)}
def delete(self, req, id):
"""Delete a VSA."""
context = req.environ['nova.context']
authorize(context)
LOG.audit(_("Delete VSA with id: %s"), id, context=context)
try:
self.vsa_api.delete(context, vsa_id=id)
except exception.NotFound:
raise exc.HTTPNotFound()
def associate_address(self, req, id, body):
""" /zadr-vsa/{vsa_id}/associate_address
auto or manually associate an IP to VSA
"""
context = req.environ['nova.context']
authorize(context)
if body is None:
ip = 'auto'
else:
ip = body.get('ipAddress', 'auto')
LOG.audit(_("Associate address %(ip)s to VSA %(id)s"),
locals(), context=context)
try:
instances = self._get_instances_by_vsa_id(context, id)
if instances is None or len(instances) == 0:
raise exc.HTTPNotFound()
for instance in instances:
self.network_api.allocate_for_instance(context, instance,
vpn=False)
# Placeholder
return
except exception.NotFound:
raise exc.HTTPNotFound()
def disassociate_address(self, req, id, body):
""" /zadr-vsa/{vsa_id}/disassociate_address
auto or manually associate an IP to VSA
"""
context = req.environ['nova.context']
authorize(context)
if body is None:
ip = 'auto'
else:
ip = body.get('ipAddress', 'auto')
LOG.audit(_("Disassociate address from VSA %(id)s"),
locals(), context=context)
# Placeholder
def make_volume(elem):
volumes.make_volume(elem)
elem.set('name')
elem.set('vsaId')
class VsaVolumeDriveController(volumes.VolumeController):
"""The base class for VSA volumes & drives.
A child resource of the VSA object. Allows operations with
volumes and drives created to/from particular VSA
"""
def __init__(self):
self.volume_api = volume.API()
self.vsa_api = vsa.API()
super(VsaVolumeDriveController, self).__init__()
def _translation(self, context, vol, vsa_id, details):
if details:
translation = volumes._translate_volume_detail_view
else:
translation = volumes._translate_volume_summary_view
d = translation(context, vol)
d['vsaId'] = vsa_id
d['name'] = vol['name']
return d
def _check_volume_ownership(self, context, vsa_id, id):
obj = self.object
try:
volume_ref = self.volume_api.get(context, id)
except exception.NotFound:
LOG.error(_("%(obj)s with ID %(id)s not found"), locals())
raise
own_vsa_id = self.volume_api.get_volume_metadata_value(volume_ref,
self.direction)
if own_vsa_id != vsa_id:
LOG.error(_("%(obj)s with ID %(id)s belongs to VSA %(own_vsa_id)s"\
" and not to VSA %(vsa_id)s."), locals())
raise exception.Invalid()
def _items(self, req, vsa_id, details):
"""Return summary or detailed list of volumes for particular VSA."""
context = req.environ['nova.context']
authorize(context)
vols = self.volume_api.get_all(context,
search_opts={'metadata': {self.direction: str(vsa_id)}})
limited_list = common.limited(vols, req)
res = [self._translation(context, vol, vsa_id, details) \
for vol in limited_list]
return {self.objects: res}
def index(self, req, vsa_id):
"""Return a short list of volumes created from particular VSA."""
LOG.audit(_("Index. vsa_id=%(vsa_id)s"), locals())
return self._items(req, vsa_id, details=False)
def detail(self, req, vsa_id):
"""Return a detailed list of volumes created from particular VSA."""
LOG.audit(_("Detail. vsa_id=%(vsa_id)s"), locals())
return self._items(req, vsa_id, details=True)
def create(self, req, vsa_id, body):
"""Create a new volume from VSA."""
LOG.audit(_("Create. vsa_id=%(vsa_id)s, body=%(body)s"), locals())
context = req.environ['nova.context']
authorize(context)
if not body:
raise exc.HTTPUnprocessableEntity()
vol = body[self.object]
size = vol['size']
LOG.audit(_("Create volume of %(size)s GB from VSA ID %(vsa_id)s"),
locals(), context=context)
try:
# create is supported for volumes only (drives created through VSA)
volume_type = self.vsa_api.get_vsa_volume_type(context)
except exception.NotFound:
raise exc.HTTPNotFound()
new_volume = self.volume_api.create(context,
size,
vol.get('displayName'),
vol.get('displayDescription'),
None,
volume_type=volume_type,
metadata=dict(from_vsa_id=str(vsa_id)))
return {self.object: self._translation(context, new_volume,
vsa_id, True)}
def update(self, req, vsa_id, id, body):
"""Update a volume."""
context = req.environ['nova.context']
authorize(context)
try:
self._check_volume_ownership(context, vsa_id, id)
except exception.NotFound:
raise exc.HTTPNotFound()
except exception.Invalid:
raise exc.HTTPBadRequest()
vol = body[self.object]
updatable_fields = [{'displayName': 'display_name'},
{'displayDescription': 'display_description'},
{'status': 'status'},
{'providerLocation': 'provider_location'},
{'providerAuth': 'provider_auth'}]
changes = {}
for field in updatable_fields:
key = field.keys()[0]
val = field[key]
if key in vol:
changes[val] = vol[key]
obj = self.object
LOG.audit(_("Update %(obj)s with id: %(id)s, changes: %(changes)s"),
locals(), context=context)
try:
volume = self.volume_api.get(context, id)
self.volume_api.update(context, volume, fields=changes)
except exception.NotFound:
raise exc.HTTPNotFound()
return webob.Response(status_int=202)
def delete(self, req, vsa_id, id):
"""Delete a volume."""
context = req.environ['nova.context']
authorize(context)
LOG.audit(_("Delete. vsa_id=%(vsa_id)s, id=%(id)s"), locals())
try:
self._check_volume_ownership(context, vsa_id, id)
except exception.NotFound:
raise exc.HTTPNotFound()
except exception.Invalid:
raise exc.HTTPBadRequest()
return super(VsaVolumeDriveController, self).delete(req, id)
def show(self, req, vsa_id, id):
"""Return data about the given volume."""
context = req.environ['nova.context']
authorize(context)
LOG.audit(_("Show. vsa_id=%(vsa_id)s, id=%(id)s"), locals())
try:
self._check_volume_ownership(context, vsa_id, id)
except exception.NotFound:
raise exc.HTTPNotFound()
except exception.Invalid:
raise exc.HTTPBadRequest()
return super(VsaVolumeDriveController, self).show(req, id)
class VsaVolumeTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('volume', selector='volume')
make_volume(root)
return xmlutil.MasterTemplate(root, 1)
class VsaVolumesTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('volumes')
elem = xmlutil.SubTemplateElement(root, 'volume', selector='volumes')
make_volume(elem)
return xmlutil.MasterTemplate(root, 1)
class VsaVolumeController(VsaVolumeDriveController):
"""The VSA volume API controller for the Openstack API.
A child resource of the VSA object. Allows operations with volumes created
by particular VSA
"""
def __init__(self):
self.direction = 'from_vsa_id'
self.objects = 'volumes'
self.object = 'volume'
super(VsaVolumeController, self).__init__()
@wsgi.serializers(xml=VsaVolumesTemplate)
def index(self, req, vsa_id):
return super(VsaVolumeController, self).index(req, vsa_id)
@wsgi.serializers(xml=VsaVolumesTemplate)
def detail(self, req, vsa_id):
return super(VsaVolumeController, self).detail(req, vsa_id)
@wsgi.serializers(xml=VsaVolumeTemplate)
def create(self, req, vsa_id, body):
return super(VsaVolumeController, self).create(req, vsa_id, body)
@wsgi.serializers(xml=VsaVolumeTemplate)
def update(self, req, vsa_id, id, body):
return super(VsaVolumeController, self).update(req, vsa_id, id, body)
@wsgi.serializers(xml=VsaVolumeTemplate)
def show(self, req, vsa_id, id):
return super(VsaVolumeController, self).show(req, vsa_id, id)
class VsaDriveTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('drive', selector='drive')
make_volume(root)
return xmlutil.MasterTemplate(root, 1)
class VsaDrivesTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('drives')
elem = xmlutil.SubTemplateElement(root, 'drive', selector='drives')
make_volume(elem)
return xmlutil.MasterTemplate(root, 1)
class VsaDriveController(VsaVolumeDriveController):
"""The VSA Drive API controller for the Openstack API.
A child resource of the VSA object. Allows operations with drives created
for particular VSA
"""
def __init__(self):
self.direction = 'to_vsa_id'
self.objects = 'drives'
self.object = 'drive'
super(VsaDriveController, self).__init__()
def create(self, req, vsa_id, body):
"""Create a new drive for VSA. Should be done through VSA APIs"""
raise exc.HTTPBadRequest()
def update(self, req, vsa_id, id, body):
"""Update a drive. Should be done through VSA APIs"""
raise exc.HTTPBadRequest()
def delete(self, req, vsa_id, id):
"""Delete a volume. Should be done through VSA APIs"""
raise exc.HTTPBadRequest()
@wsgi.serializers(xml=VsaDrivesTemplate)
def index(self, req, vsa_id):
return super(VsaDriveController, self).index(req, vsa_id)
@wsgi.serializers(xml=VsaDrivesTemplate)
def detail(self, req, vsa_id):
return super(VsaDriveController, self).detail(req, vsa_id)
@wsgi.serializers(xml=VsaDriveTemplate)
def show(self, req, vsa_id, id):
return super(VsaDriveController, self).show(req, vsa_id, id)
def make_vpool(elem):
elem.set('id')
elem.set('vsaId')
elem.set('name')
elem.set('displayName')
elem.set('displayDescription')
elem.set('driveCount')
elem.set('protection')
elem.set('stripeSize')
elem.set('stripeWidth')
elem.set('createTime')
elem.set('status')
drive_ids = xmlutil.SubTemplateElement(elem, 'driveIds')
drive_id = xmlutil.SubTemplateElement(drive_ids, 'driveId',
selector='driveIds')
drive_id.text = xmlutil.Selector()
class VsaVPoolTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('vpool', selector='vpool')
make_vpool(root)
return xmlutil.MasterTemplate(root, 1)
class VsaVPoolsTemplate(xmlutil.TemplateBuilder):
def construct(self):
root = xmlutil.TemplateElement('vpools')
elem = xmlutil.SubTemplateElement(root, 'vpool', selector='vpools')
make_vpool(elem)
return xmlutil.MasterTemplate(root, 1)
class VsaVPoolController(object):
"""The vPool VSA API controller for the OpenStack API."""
def __init__(self):
self.vsa_api = vsa.API()
super(VsaVPoolController, self).__init__()
@wsgi.serializers(xml=VsaVPoolsTemplate)
def index(self, req, vsa_id):
"""Return a short list of vpools created from particular VSA."""
return {'vpools': []}
def create(self, req, vsa_id, body):
"""Create a new vPool for VSA."""
raise exc.HTTPBadRequest()
def update(self, req, vsa_id, id, body):
"""Update vPool parameters."""
raise exc.HTTPBadRequest()
def delete(self, req, vsa_id, id):
"""Delete a vPool."""
raise exc.HTTPBadRequest()
def show(self, req, vsa_id, id):
"""Return data about the given vPool."""
raise exc.HTTPBadRequest()
class VsaVCController(servers.Controller):
"""The VSA Virtual Controller API controller for the OpenStack API."""
def __init__(self):
self.vsa_api = vsa.API()
self.compute_api = compute.API()
self.vsa_id = None # VP-TODO: temporary ugly hack
super(VsaVCController, self).__init__()
def _get_servers(self, req, is_detail):
"""Returns a list of servers, taking into account any search
options specified.
"""
if self.vsa_id is None:
super(VsaVCController, self)._get_servers(req, is_detail)
context = req.environ['nova.context']
search_opts = {'metadata': dict(vsa_id=str(self.vsa_id))}
instance_list = self.compute_api.get_all(
context, search_opts=search_opts)
limited_list = self._limit_items(instance_list, req)
servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
return dict(servers=servers)
@wsgi.serializers(xml=servers.MinimalServersTemplate)
def index(self, req, vsa_id):
"""Return list of instances for particular VSA."""
LOG.audit(_("Index instances for VSA %s"), vsa_id)
self.vsa_id = vsa_id # VP-TODO: temporary ugly hack
result = super(VsaVCController, self).detail(req)
self.vsa_id = None
return result
def create(self, req, vsa_id, body):
"""Create a new instance for VSA."""
raise exc.HTTPBadRequest()
def update(self, req, vsa_id, id, body):
"""Update VSA instance."""
raise exc.HTTPBadRequest()
def delete(self, req, vsa_id, id):
"""Delete VSA instance."""
raise exc.HTTPBadRequest()
@wsgi.serializers(xml=servers.ServerTemplate)
def show(self, req, vsa_id, id):
"""Return data about the given instance."""
return super(VsaVCController, self).show(req, id)
class Virtual_storage_arrays(extensions.ExtensionDescriptor):
"""Virtual Storage Arrays support"""
name = "VSAs"
alias = "zadr-vsa"
namespace = "http://docs.openstack.org/compute/ext/vsa/api/v1.1"
updated = "2011-08-25T00:00:00+00:00"
def get_resources(self):
resources = []
res = extensions.ResourceExtension(
'zadr-vsa',
VsaController(),
collection_actions={'detail': 'GET'},
member_actions={'add_capacity': 'POST',
'remove_capacity': 'POST',
'associate_address': 'POST',
'disassociate_address': 'POST'})
resources.append(res)
res = extensions.ResourceExtension('volumes',
VsaVolumeController(),
collection_actions={'detail': 'GET'},
parent=dict(
member_name='vsa',
collection_name='zadr-vsa'))
resources.append(res)
res = extensions.ResourceExtension('drives',
VsaDriveController(),
collection_actions={'detail': 'GET'},
parent=dict(
member_name='vsa',
collection_name='zadr-vsa'))
resources.append(res)
res = extensions.ResourceExtension('vpools',
VsaVPoolController(),
parent=dict(
member_name='vsa',
collection_name='zadr-vsa'))
resources.append(res)
res = extensions.ResourceExtension('instances',
VsaVCController(),
parent=dict(
member_name='vsa',
collection_name='zadr-vsa'))
resources.append(res)
return resources