diff options
| author | Vishvananda Ishaya <vishvananda@gmail.com> | 2012-10-23 19:30:48 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@gmail.com> | 2012-10-28 11:34:05 -0700 |
| commit | 7e2b93acc59dea81d52684f7f659fcff32507e14 (patch) | |
| tree | 11bf7e96a3835d0002f133742dd9e1702c7cc6c6 /nova | |
| parent | 4e449c0843150b785bc61e87599d05ff242a8f4a (diff) | |
removes the nova-volume code from nova
This removes the majority of the nova-volume code from the codebase.
It updates relevent config options to default to cinder. It updates a
number of existing tests that were depending on code that was removed.
A few things still need to be removed:
* volume/driver.py & volume/iscsi.py
These files are used by the libvirt volume driver tests. These
tests should be updated to mock the relevant calls.
* scheduler/simple.py & scheduler/multi.py
These files should no longer be necessary so they can be removed
in a subsequent patch
* exception.py cleanup
Once the above files are removed there are a number of unused
exceptions which can be removed
* database calls and database tables
The database calls have not been removed and the tables have not
been dropped. This can be done in a separate migration
* additional config options and nova.conf.sample
There may be a few extra config options that can be removed and the
conf sample can be regenerated
Implements bp delete-nova-volume
Change-Id: I0b540e54dbabd26901a7530035a38583bb521fda
Diffstat (limited to 'nova')
84 files changed, 99 insertions, 17249 deletions
diff --git a/nova/api/openstack/compute/contrib/volumes.py b/nova/api/openstack/compute/contrib/volumes.py index 9940e3050..6eaa51079 100644 --- a/nova/api/openstack/compute/contrib/volumes.py +++ b/nova/api/openstack/compute/contrib/volumes.py @@ -29,7 +29,6 @@ from nova import flags from nova.openstack.common import log as logging from nova import utils from nova import volume -from nova.volume import volume_types LOG = logging.getLogger(__name__) @@ -227,12 +226,6 @@ class VolumeController(wsgi.Controller): vol = body['volume'] vol_type = vol.get('volume_type', None) - if vol_type: - try: - vol_type = volume_types.get_volume_type_by_name(context, - vol_type) - except exception.NotFound: - raise exc.HTTPNotFound() metadata = vol.get('metadata', None) diff --git a/nova/api/openstack/compute/contrib/volumetypes.py b/nova/api/openstack/compute/contrib/volumetypes.py deleted file mode 100644 index 036e3ff42..000000000 --- a/nova/api/openstack/compute/contrib/volumetypes.py +++ /dev/null @@ -1,225 +0,0 @@ -# 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 volume type & volume types extra specs extension""" - -from webob import exc - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import db -from nova import exception -from nova.volume import volume_types - - -authorize = extensions.extension_authorizer('compute', 'volumetypes') - - -def make_voltype(elem): - elem.set('id') - elem.set('name') - extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - elem.append(extra_specs) - - -class VolumeTypeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_type', selector='volume_type') - make_voltype(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_types') - elem = xmlutil.SubTemplateElement(root, 'volume_type', - selector='volume_types') - make_voltype(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesController(wsgi.Controller): - """ The volume types API controller for the OpenStack API """ - - @wsgi.serializers(xml=VolumeTypesTemplate) - def index(self, req): - """ Returns the list of volume types """ - context = req.environ['nova.context'] - authorize(context) - return {'volume_types': volume_types.get_all_types(context).values()} - - @wsgi.serializers(xml=VolumeTypeTemplate) - def create(self, req, body): - """Creates a new volume type.""" - context = req.environ['nova.context'] - authorize(context) - - if not self.is_valid_body(body, 'volume_type'): - raise exc.HTTPUnprocessableEntity() - - vol_type = body['volume_type'] - name = vol_type.get('name', None) - specs = vol_type.get('extra_specs', {}) - - if name is None or name == "": - raise exc.HTTPUnprocessableEntity() - - try: - volume_types.create(context, name, specs) - vol_type = volume_types.get_volume_type_by_name(context, name) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - @wsgi.serializers(xml=VolumeTypeTemplate) - def show(self, req, id): - """ Return a single volume type item """ - context = req.environ['nova.context'] - authorize(context) - - try: - vol_type = volume_types.get_volume_type(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume_type': vol_type} - - def delete(self, req, id): - """ Deletes an existing volume type """ - context = req.environ['nova.context'] - authorize(context) - - try: - vol_type = volume_types.get_volume_type(context, id) - volume_types.destroy(context, vol_type['name']) - except exception.NotFound: - raise exc.HTTPNotFound() - - -class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): - def construct(self): - tagname = xmlutil.Selector('key') - - def extraspec_sel(obj, do_raise=False): - # Have to extract the key and value for later use... - key, value = obj.items()[0] - return dict(key=key, value=value) - - root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) - root.text = 'value' - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecsController(object): - """ The volume type extra specs API controller for the OpenStack API """ - - def _get_extra_specs(self, context, vol_type_id): - extra_specs = db.volume_type_extra_specs_get(context, vol_type_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_body(self, body): - if body is None or body == "": - expl = _('No Request Body') - raise exc.HTTPBadRequest(explanation=expl) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def index(self, req, vol_type_id): - """ Returns the list of extra specs for a given volume type """ - context = req.environ['nova.context'] - authorize(context) - return self._get_extra_specs(context, vol_type_id) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def create(self, req, vol_type_id, body): - context = req.environ['nova.context'] - authorize(context) - self._check_body(body) - specs = body.get('extra_specs') - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - specs) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def update(self, req, vol_type_id, id, body): - context = req.environ['nova.context'] - authorize(context) - self._check_body(body) - if not id in body: - expl = _('Request body and URI mismatch') - raise exc.HTTPBadRequest(explanation=expl) - if len(body) > 1: - expl = _('Request body contains too many items') - raise exc.HTTPBadRequest(explanation=expl) - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - body) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def show(self, req, vol_type_id, id): - """ Return a single extra spec item """ - context = req.environ['nova.context'] - authorize(context) - specs = self._get_extra_specs(context, vol_type_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise exc.HTTPNotFound() - - def delete(self, req, vol_type_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - authorize(context) - db.volume_type_extra_specs_delete(context, vol_type_id, id) - - -class Volumetypes(extensions.ExtensionDescriptor): - """Volume types support""" - - name = "VolumeTypes" - alias = "os-volume-types" - namespace = "http://docs.openstack.org/compute/ext/volume_types/api/v1.1" - updated = "2011-08-24T00:00:00+00:00" - - def get_resources(self): - resources = [] - - res = extensions.ResourceExtension( - 'os-volume-types', - VolumeTypesController()) - resources.append(res) - - res = extensions.ResourceExtension('extra_specs', - VolumeTypeExtraSpecsController(), - parent=dict( - member_name='vol_type', - collection_name='os-volume-types')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/volume/__init__.py b/nova/api/openstack/volume/__init__.py deleted file mode 100644 index 3aca5bebf..000000000 --- a/nova/api/openstack/volume/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -WSGI middleware for OpenStack Volume API. -""" - -import nova.api.openstack -from nova.api.openstack.volume import extensions -from nova.api.openstack.volume import snapshots -from nova.api.openstack.volume import types -from nova.api.openstack.volume import versions -from nova.api.openstack.volume import volumes -from nova.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) - - -class APIRouter(nova.api.openstack.APIRouter): - """ - Routes requests on the OpenStack API to the appropriate controller - and method. - """ - ExtensionManager = extensions.ExtensionManager - - def _setup_routes(self, mapper, ext_mgr, init_only): - if init_only is None or 'versions' in init_only: - self.resources['versions'] = versions.create_resource() - mapper.connect("versions", "/", - controller=self.resources['versions'], - action='show') - - mapper.redirect("", "/") - - if init_only is None or 'volumes' in init_only: - self.resources['volumes'] = volumes.create_resource(ext_mgr) - mapper.resource("volume", "volumes", - controller=self.resources['volumes'], - collection={'detail': 'GET'}, - member={'action': 'POST'}) - - if init_only is None or 'types' in init_only: - self.resources['types'] = types.create_resource() - mapper.resource("type", "types", - controller=self.resources['types']) - - if init_only is None or 'snapshots' in init_only: - self.resources['snapshots'] = snapshots.create_resource(ext_mgr) - mapper.resource("snapshot", "snapshots", - controller=self.resources['snapshots'], - collection={'detail': 'GET'}, - member={'action': 'POST'}) diff --git a/nova/api/openstack/volume/contrib/__init__.py b/nova/api/openstack/volume/contrib/__init__.py deleted file mode 100644 index 8e01d88d0..000000000 --- a/nova/api/openstack/volume/contrib/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. - -"""Contrib contains extensions that are shipped with nova. - -It can't be called 'extensions' because that causes namespacing problems. - -""" - -from nova.api.openstack import extensions -from nova import flags -from nova.openstack.common import log as logging - - -FLAGS = flags.FLAGS -LOG = logging.getLogger(__name__) - - -def standard_extensions(ext_mgr): - extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__) - - -def select_extensions(ext_mgr): - extensions.load_standard_extensions(ext_mgr, LOG, __path__, __package__, - FLAGS.osapi_volume_ext_list) diff --git a/nova/api/openstack/volume/contrib/admin_actions.py b/nova/api/openstack/volume/contrib/admin_actions.py deleted file mode 100644 index 7e93283f7..000000000 --- a/nova/api/openstack/volume/contrib/admin_actions.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2012 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. - -import webob -from webob import exc - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova import db -from nova import exception -from nova.openstack.common import log as logging -from nova import volume - - -LOG = logging.getLogger(__name__) - - -class AdminController(wsgi.Controller): - """Abstract base class for AdminControllers.""" - - collection = None # api collection to extend - - # FIXME(clayg): this will be hard to keep up-to-date - # Concrete classes can expand or over-ride - valid_status = set([ - 'creating', - 'available', - 'deleting', - 'error', - 'error_deleting', - ]) - - def __init__(self, *args, **kwargs): - super(AdminController, self).__init__(*args, **kwargs) - # singular name of the resource - self.resource_name = self.collection.rstrip('s') - self.volume_api = volume.API() - - def _update(self, *args, **kwargs): - raise NotImplementedError() - - def _validate_status(self, status): - if status not in self.valid_status: - raise exc.HTTPBadRequest("Must specify a valid status") - - def authorize(self, context, action_name): - # e.g. "snapshot_admin_actions:reset_status" - action = '%s_admin_actions:%s' % (self.resource_name, action_name) - extensions.extension_authorizer('volume', action)(context) - - @wsgi.action('os-reset_status') - def _reset_status(self, req, id, body): - """Reset status on the resource.""" - context = req.environ['nova.context'] - self.authorize(context, 'reset_status') - try: - new_status = body['os-reset_status']['status'] - except (TypeError, KeyError): - raise exc.HTTPBadRequest("Must specify 'status'") - self._validate_status(new_status) - msg = _("Updating status of %(resource)s '%(id)s' to '%(status)s'") - LOG.debug(msg, {'resource': self.resource_name, 'id': id, - 'status': new_status}) - try: - self._update(context, id, {'status': new_status}) - except exception.NotFound, e: - raise exc.HTTPNotFound(e) - return webob.Response(status_int=202) - - -class VolumeAdminController(AdminController): - """AdminController for Volumes.""" - - collection = 'volumes' - valid_status = AdminController.valid_status.union( - set(['attaching', 'in-use', 'detaching'])) - - def _update(self, *args, **kwargs): - db.volume_update(*args, **kwargs) - - @wsgi.action('os-force_delete') - def _force_delete(self, req, id, body): - """Delete a resource, bypassing the check that it must be available.""" - context = req.environ['nova.context'] - self.authorize(context, 'force_delete') - try: - volume = self.volume_api.get(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - self.volume_api.delete(context, volume, force=True) - return webob.Response(status_int=202) - - -class SnapshotAdminController(AdminController): - """AdminController for Snapshots.""" - - collection = 'snapshots' - - def _update(self, *args, **kwargs): - db.snapshot_update(*args, **kwargs) - - -class Admin_actions(extensions.ExtensionDescriptor): - """Enable admin actions.""" - - name = "AdminActions" - alias = "os-admin-actions" - namespace = "http://docs.openstack.org/volume/ext/admin-actions/api/v1.1" - updated = "2012-08-25T00:00:00+00:00" - - def get_controller_extensions(self): - exts = [] - for class_ in (VolumeAdminController, SnapshotAdminController): - controller = class_() - extension = extensions.ControllerExtension( - self, class_.collection, controller) - exts.append(extension) - return exts diff --git a/nova/api/openstack/volume/contrib/image_create.py b/nova/api/openstack/volume/contrib/image_create.py deleted file mode 100644 index 840689799..000000000 --- a/nova/api/openstack/volume/contrib/image_create.py +++ /dev/null @@ -1,31 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright (c) 2012 NTT.
-# Copyright (c) 2012 OpenStack, LLC.
-# All Rights Reserved.
-#
-# 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 Create Volume from Image extension."""
-
-
-from nova.api.openstack import extensions
-
-
-class Image_create(extensions.ExtensionDescriptor):
- """Allow creating a volume from an image in the Create Volume v1 API"""
-
- name = "CreateVolumeExtension"
- alias = "os-image-create"
- namespace = "http://docs.openstack.org/volume/ext/image-create/api/v1"
- updated = "2012-08-13T00:00:00+00:00"
diff --git a/nova/api/openstack/volume/contrib/types_extra_specs.py b/nova/api/openstack/volume/contrib/types_extra_specs.py deleted file mode 100644 index 2e993ad8a..000000000 --- a/nova/api/openstack/volume/contrib/types_extra_specs.py +++ /dev/null @@ -1,149 +0,0 @@ -# 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 volume types extra specs extension""" - -import webob - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import db -from nova import exception -from nova.volume import volume_types - - -authorize = extensions.extension_authorizer('volume', 'types_extra_specs') - - -class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): - def construct(self): - tagname = xmlutil.Selector('key') - - def extraspec_sel(obj, do_raise=False): - # Have to extract the key and value for later use... - key, value = obj.items()[0] - return dict(key=key, value=value) - - root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) - root.text = 'value' - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypeExtraSpecsController(wsgi.Controller): - """ The volume type extra specs API controller for the OpenStack API """ - - def _get_extra_specs(self, context, type_id): - extra_specs = db.volume_type_extra_specs_get(context, type_id) - specs_dict = {} - for key, value in extra_specs.iteritems(): - specs_dict[key] = value - return dict(extra_specs=specs_dict) - - def _check_type(self, context, type_id): - try: - volume_types.get_volume_type(context, type_id) - except exception.NotFound as ex: - raise webob.exc.HTTPNotFound(explanation=unicode(ex)) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def index(self, req, type_id): - """ Returns the list of extra specs for a given volume type """ - context = req.environ['nova.context'] - authorize(context) - self._check_type(context, type_id) - return self._get_extra_specs(context, type_id) - - @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) - def create(self, req, type_id, body=None): - context = req.environ['nova.context'] - authorize(context) - - if not self.is_valid_body(body, 'extra_specs'): - raise webob.exc.HTTPUnprocessableEntity() - - self._check_type(context, type_id) - - specs = body['extra_specs'] - db.volume_type_extra_specs_update_or_create(context, - type_id, - specs) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def update(self, req, type_id, id, body=None): - context = req.environ['nova.context'] - authorize(context) - if not body: - raise webob.exc.HTTPUnprocessableEntity() - self._check_type(context, type_id) - if not id in body: - expl = _('Request body and URI mismatch') - raise webob.exc.HTTPBadRequest(explanation=expl) - if len(body) > 1: - expl = _('Request body contains too many items') - raise webob.exc.HTTPBadRequest(explanation=expl) - db.volume_type_extra_specs_update_or_create(context, - type_id, - body) - return body - - @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) - def show(self, req, type_id, id): - """Return a single extra spec item.""" - context = req.environ['nova.context'] - authorize(context) - self._check_type(context, type_id) - specs = self._get_extra_specs(context, type_id) - if id in specs['extra_specs']: - return {id: specs['extra_specs'][id]} - else: - raise webob.exc.HTTPNotFound() - - def delete(self, req, type_id, id): - """ Deletes an existing extra spec """ - context = req.environ['nova.context'] - self._check_type(context, type_id) - authorize(context) - db.volume_type_extra_specs_delete(context, type_id, id) - return webob.Response(status_int=202) - - -class Types_extra_specs(extensions.ExtensionDescriptor): - """Types extra specs support""" - - name = "TypesExtraSpecs" - alias = "os-types-extra-specs" - namespace = "http://docs.openstack.org/volume/ext/types-extra-specs/api/v1" - updated = "2011-08-24T00:00:00+00:00" - - def get_resources(self): - resources = [] - res = extensions.ResourceExtension('extra_specs', - VolumeTypeExtraSpecsController(), - parent=dict( - member_name='type', - collection_name='types')) - resources.append(res) - - return resources diff --git a/nova/api/openstack/volume/contrib/types_manage.py b/nova/api/openstack/volume/contrib/types_manage.py deleted file mode 100644 index e68093ce8..000000000 --- a/nova/api/openstack/volume/contrib/types_manage.py +++ /dev/null @@ -1,91 +0,0 @@ -# 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 volume types manage extension.""" - -import webob - -from nova.api.openstack import extensions -from nova.api.openstack.volume import types -from nova.api.openstack.volume.views import types as views_types -from nova.api.openstack import wsgi -from nova import exception -from nova.volume import volume_types - - -authorize = extensions.extension_authorizer('volume', 'types_manage') - - -class VolumeTypesManageController(wsgi.Controller): - """ The volume types API controller for the OpenStack API """ - - _view_builder_class = views_types.ViewBuilder - - @wsgi.action("create") - @wsgi.serializers(xml=types.VolumeTypeTemplate) - def _create(self, req, body): - """Creates a new volume type.""" - context = req.environ['nova.context'] - authorize(context) - - if not self.is_valid_body(body, 'volume_type'): - raise webob.exc.HTTPUnprocessableEntity() - - vol_type = body['volume_type'] - name = vol_type.get('name', None) - specs = vol_type.get('extra_specs', {}) - - if name is None or name == "": - raise webob.exc.HTTPUnprocessableEntity() - - try: - volume_types.create(context, name, specs) - vol_type = volume_types.get_volume_type_by_name(context, name) - except exception.VolumeTypeExists as err: - raise webob.exc.HTTPConflict(explanation=str(err)) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return self._view_builder.show(req, vol_type) - - @wsgi.action("delete") - def _delete(self, req, id): - """ Deletes an existing volume type """ - context = req.environ['nova.context'] - authorize(context) - - try: - vol_type = volume_types.get_volume_type(context, id) - volume_types.destroy(context, vol_type['name']) - except exception.NotFound: - raise webob.exc.HTTPNotFound() - - return webob.Response(status_int=202) - - -class Types_manage(extensions.ExtensionDescriptor): - """Types manage support""" - - name = "TypesManage" - alias = "os-types-manage" - namespace = "http://docs.openstack.org/volume/ext/types-manage/api/v1" - updated = "2011-08-24T00:00:00+00:00" - - def get_controller_extensions(self): - controller = VolumeTypesManageController() - extension = extensions.ControllerExtension(self, 'types', controller) - return [extension] diff --git a/nova/api/openstack/volume/contrib/volume_actions.py b/nova/api/openstack/volume/contrib/volume_actions.py deleted file mode 100644 index 8a453bfb1..000000000 --- a/nova/api/openstack/volume/contrib/volume_actions.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2012 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. - -import webob -from xml.dom import minidom - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova.openstack.common.rpc import common as rpc_common -from nova import volume - - -FLAGS = flags.FLAGS -LOG = logging.getLogger(__name__) - - -def authorize(context, action_name): - action = 'volume_actions:%s' % action_name - extensions.extension_authorizer('volume', action)(context) - - -class VolumeToImageSerializer(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('os-volume_upload_image', - selector='os-volume_upload_image') - root.set('id') - root.set('updated_at') - root.set('status') - root.set('display_description') - root.set('size') - root.set('volume_type') - root.set('image_id') - root.set('container_format') - root.set('disk_format') - root.set('image_name') - return xmlutil.MasterTemplate(root, 1) - - -class VolumeToImageDeserializer(wsgi.XMLDeserializer): - """Deserializer to handle xml-formatted requests""" - def default(self, string): - dom = minidom.parseString(string) - action_node = dom.childNodes[0] - action_name = action_node.tagName - - action_data = {} - attributes = ["force", "image_name", "container_format", "disk_format"] - for attr in attributes: - if action_node.hasAttribute(attr): - action_data[attr] = action_node.getAttribute(attr) - if 'force' in action_data and action_data['force'] == 'True': - action_data['force'] = True - return {'body': {action_name: action_data}} - - -class VolumeActionsController(wsgi.Controller): - def __init__(self, *args, **kwargs): - super(VolumeActionsController, self).__init__(*args, **kwargs) - self.volume_api = volume.API() - - @wsgi.response(202) - @wsgi.action('os-volume_upload_image') - @wsgi.serializers(xml=VolumeToImageSerializer) - @wsgi.deserializers(xml=VolumeToImageDeserializer) - def _volume_upload_image(self, req, id, body): - """Uploads the specified volume to image service.""" - context = req.environ['nova.context'] - try: - params = body['os-volume_upload_image'] - except (TypeError, KeyError): - msg = _("Invalid request body") - raise webob.exc.HTTPBadRequest(explanation=msg) - - if not params.get("image_name"): - msg = _("No image_name was specified in request.") - raise webob.exc.HTTPBadRequest(explanation=msg) - - force = params.get('force', False) - try: - volume = self.volume_api.get(context, id) - except exception.VolumeNotFound, error: - raise webob.exc.HTTPNotFound(explanation=unicode(error)) - authorize(context, "upload_image") - image_metadata = {"container_format": params.get("container_format", - "bare"), - "disk_format": params.get("disk_format", "raw"), - "name": params["image_name"]} - try: - response = self.volume_api.copy_volume_to_image(context, - volume, - image_metadata, - force) - except exception.InvalidVolume, error: - raise webob.exc.HTTPBadRequest(explanation=unicode(error)) - except ValueError, error: - raise webob.exc.HTTPBadRequest(explanation=unicode(error)) - except rpc_common.RemoteError as error: - msg = "%(err_type)s: %(err_msg)s" % {'err_type': error.exc_type, - 'err_msg': error.value} - raise webob.exc.HTTPBadRequest(explanation=msg) - return {'os-volume_upload_image': response} - - -class Volume_actions(extensions.ExtensionDescriptor): - """Enable volume actions - """ - - name = "VolumeActions" - alias = "os-volume-actions" - namespace = "http://docs.openstack.org/volume/ext/volume-actions/api/v1.1" - updated = "2012-05-31T00:00:00+00:00" - - def get_controller_extensions(self): - controller = VolumeActionsController() - extension = extensions.ControllerExtension(self, 'volumes', controller) - return [extension] diff --git a/nova/api/openstack/volume/extensions.py b/nova/api/openstack/volume/extensions.py deleted file mode 100644 index b21b9d145..000000000 --- a/nova/api/openstack/volume/extensions.py +++ /dev/null @@ -1,34 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 nova.api.openstack import extensions as base_extensions -from nova import flags -from nova.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) -FLAGS = flags.FLAGS - - -class ExtensionManager(base_extensions.ExtensionManager): - def __init__(self): - LOG.audit(_('Initializing extension manager.')) - self.cls_list = FLAGS.osapi_volume_extension - self.extensions = {} - self.plugins = [] - self.sorted_ext_list = [] - self._load_extensions() diff --git a/nova/api/openstack/volume/snapshots.py b/nova/api/openstack/volume/snapshots.py deleted file mode 100644 index 74c5f75e6..000000000 --- a/nova/api/openstack/volume/snapshots.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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 volumes snapshots api.""" - -import webob -from webob import exc - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova import utils -from nova import volume - - -LOG = logging.getLogger(__name__) - - -FLAGS = flags.FLAGS - - -def _translate_snapshot_detail_view(context, vol): - """Maps keys for snapshots details view.""" - - d = _translate_snapshot_summary_view(context, vol) - - # NOTE(gagupta): No additional data / lookups at the moment - return d - - -def _translate_snapshot_summary_view(context, vol): - """Maps keys for snapshots summary view.""" - d = {} - - # TODO(bcwaldon): remove str cast once we use uuids - d['id'] = str(vol['id']) - d['volume_id'] = str(vol['volume_id']) - d['status'] = vol['status'] - # NOTE(gagupta): We map volume_size as the snapshot size - d['size'] = vol['volume_size'] - d['created_at'] = vol['created_at'] - d['display_name'] = vol['display_name'] - d['display_description'] = vol['display_description'] - return d - - -def make_snapshot(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('created_at') - elem.set('display_name') - elem.set('display_description') - elem.set('volume_id') - - -class SnapshotTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshot', selector='snapshot') - make_snapshot(root) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotsTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('snapshots') - elem = xmlutil.SubTemplateElement(root, 'snapshot', - selector='snapshots') - make_snapshot(elem) - return xmlutil.MasterTemplate(root, 1) - - -class SnapshotsController(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self, ext_mgr=None): - self.volume_api = volume.API() - self.ext_mgr = ext_mgr - super(SnapshotsController, self).__init__() - - @wsgi.serializers(xml=SnapshotTemplate) - def show(self, req, id): - """Return data about the given snapshot.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get_snapshot(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'snapshot': _translate_snapshot_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a snapshot.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete snapshot with id: %s"), id, context=context) - - try: - snapshot = self.volume_api.get_snapshot(context, id) - self.volume_api.delete_snapshot(context, snapshot) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=SnapshotsTemplate) - def index(self, req): - """Returns a summary list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_summary_view) - - @wsgi.serializers(xml=SnapshotsTemplate) - def detail(self, req): - """Returns a detailed list of snapshots.""" - return self._items(req, entity_maker=_translate_snapshot_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of snapshots, transformed through entity_maker.""" - context = req.environ['nova.context'] - - search_opts = {} - search_opts.update(req.GET) - - snapshots = self.volume_api.get_all_snapshots(context, - search_opts=search_opts) - limited_list = common.limited(snapshots, req) - res = [entity_maker(context, snapshot) for snapshot in limited_list] - return {'snapshots': res} - - @wsgi.serializers(xml=SnapshotTemplate) - def create(self, req, body): - """Creates a new snapshot.""" - context = req.environ['nova.context'] - - if not self.is_valid_body(body, 'snapshot'): - raise exc.HTTPUnprocessableEntity() - - snapshot = body['snapshot'] - volume_id = snapshot['volume_id'] - - try: - volume = self.volume_api.get(context, volume_id) - except exception.VolumeNotFound as err: - raise exc.HTTPNotFound(explanation=unicode(err)) - - force = snapshot.get('force', False) - msg = _("Create snapshot from volume %s") - LOG.audit(msg, volume_id, context=context) - - if not utils.is_valid_boolstr(force): - msg = _("Invalid value '%s' for force. ") % force - raise exception.InvalidParameterValue(err=msg) - - if utils.bool_from_str(force): - new_snapshot = self.volume_api.create_snapshot_force(context, - volume, - snapshot.get('display_name'), - snapshot.get('display_description')) - else: - new_snapshot = self.volume_api.create_snapshot(context, - volume, - snapshot.get('display_name'), - snapshot.get('display_description')) - - retval = _translate_snapshot_detail_view(context, new_snapshot) - - return {'snapshot': retval} - - -def create_resource(ext_mgr): - return wsgi.Resource(SnapshotsController(ext_mgr)) diff --git a/nova/api/openstack/volume/types.py b/nova/api/openstack/volume/types.py deleted file mode 100644 index a39d2a1f6..000000000 --- a/nova/api/openstack/volume/types.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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 volume type & volume types extra specs extension""" - -from webob import exc - -from nova.api.openstack.volume.views import types as views_types -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova.volume import volume_types - - -def make_voltype(elem): - elem.set('id') - elem.set('name') - extra_specs = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') - elem.append(extra_specs) - - -class VolumeTypeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_type', selector='volume_type') - make_voltype(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume_types') - elem = xmlutil.SubTemplateElement(root, 'volume_type', - selector='volume_types') - make_voltype(elem) - return xmlutil.MasterTemplate(root, 1) - - -class VolumeTypesController(wsgi.Controller): - """ The volume types API controller for the OpenStack API """ - - _view_builder_class = views_types.ViewBuilder - - @wsgi.serializers(xml=VolumeTypesTemplate) - def index(self, req): - """ Returns the list of volume types """ - context = req.environ['nova.context'] - vol_types = volume_types.get_all_types(context).values() - return self._view_builder.index(req, vol_types) - - @wsgi.serializers(xml=VolumeTypeTemplate) - def show(self, req, id): - """ Return a single volume type item """ - context = req.environ['nova.context'] - - try: - vol_type = volume_types.get_volume_type(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - - # TODO(bcwaldon): remove str cast once we use uuids - vol_type['id'] = str(vol_type['id']) - return self._view_builder.show(req, vol_type) - - -def create_resource(): - return wsgi.Resource(VolumeTypesController()) diff --git a/nova/api/openstack/volume/versions.py b/nova/api/openstack/volume/versions.py deleted file mode 100644 index 68d34b1f9..000000000 --- a/nova/api/openstack/volume/versions.py +++ /dev/null @@ -1,83 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 OpenStack LLC. -# All Rights Reserved. -# -# 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 nova.api.openstack.compute import versions -from nova.api.openstack.volume.views import versions as views_versions -from nova.api.openstack import wsgi - - -VERSIONS = { - "v1.0": { - "id": "v1.0", - "status": "CURRENT", - "updated": "2012-01-04T11:33:21Z", - "links": [ - { - "rel": "describedby", - "type": "application/pdf", - "href": "http://jorgew.github.com/block-storage-api/" - "content/os-block-storage-1.0.pdf", - }, - { - "rel": "describedby", - "type": "application/vnd.sun.wadl+xml", - #(anthony) FIXME - "href": "http://docs.rackspacecloud.com/" - "servers/api/v1.1/application.wadl", - }, - ], - "media-types": [ - { - "base": "application/xml", - "type": "application/vnd.openstack.volume+xml;version=1", - }, - { - "base": "application/json", - "type": "application/vnd.openstack.volume+json;version=1", - } - ], - } -} - - -class Versions(versions.Versions): - @wsgi.serializers(xml=versions.VersionsTemplate, - atom=versions.VersionsAtomSerializer) - def index(self, req): - """Return all versions.""" - builder = views_versions.get_view_builder(req) - return builder.build_versions(VERSIONS) - - @wsgi.serializers(xml=versions.ChoicesTemplate) - @wsgi.response(300) - def multi(self, req): - """Return multiple choices.""" - builder = views_versions.get_view_builder(req) - return builder.build_choices(VERSIONS, req) - - -class VolumeVersionV1(object): - @wsgi.serializers(xml=versions.VersionTemplate, - atom=versions.VersionAtomSerializer) - def show(self, req): - builder = views_versions.get_view_builder(req) - return builder.build_version(VERSIONS['v1.0']) - - -def create_resource(): - return wsgi.Resource(VolumeVersionV1()) diff --git a/nova/api/openstack/volume/views/__init__.py b/nova/api/openstack/volume/views/__init__.py deleted file mode 100644 index d65c689a8..000000000 --- a/nova/api/openstack/volume/views/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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. diff --git a/nova/api/openstack/volume/views/types.py b/nova/api/openstack/volume/views/types.py deleted file mode 100644 index 8274a0c6f..000000000 --- a/nova/api/openstack/volume/views/types.py +++ /dev/null @@ -1,34 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, Inc. -# All Rights Reserved. -# -# 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 nova.api.openstack import common - - -class ViewBuilder(common.ViewBuilder): - - def show(self, request, volume_type, brief=False): - """Trim away extraneous volume type attributes.""" - trimmed = dict(id=volume_type.get('id'), - name=volume_type.get('name'), - extra_specs=volume_type.get('extra_specs')) - return trimmed if brief else dict(volume_type=trimmed) - - def index(self, request, volume_types): - """Index over trimmed volume types""" - volume_types_list = [self.show(request, volume_type, True) - for volume_type in volume_types] - return dict(volume_types=volume_types_list) diff --git a/nova/api/openstack/volume/views/versions.py b/nova/api/openstack/volume/views/versions.py deleted file mode 100644 index 2e659af6a..000000000 --- a/nova/api/openstack/volume/views/versions.py +++ /dev/null @@ -1,36 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 os - -from nova.api.openstack.compute.views import versions as compute_views - - -def get_view_builder(req): - base_url = req.application_url - return ViewBuilder(base_url) - - -class ViewBuilder(compute_views.ViewBuilder): - def generate_href(self, path=None): - """Create an url that refers to a specific version_number.""" - version_number = 'v1' - if path: - path = path.strip('/') - return os.path.join(self.base_url, version_number, path) - else: - return os.path.join(self.base_url, version_number) + '/' diff --git a/nova/api/openstack/volume/volumes.py b/nova/api/openstack/volume/volumes.py deleted file mode 100644 index e13f04036..000000000 --- a/nova/api/openstack/volume/volumes.py +++ /dev/null @@ -1,364 +0,0 @@ -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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 volumes api.""" - -import webob -from webob import exc -from xml.dom import minidom - -from nova.api.openstack import common -from nova.api.openstack import wsgi -from nova.api.openstack import xmlutil -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova import utils -from nova import volume -from nova.volume import volume_types - - -LOG = logging.getLogger(__name__) - - -FLAGS = flags.FLAGS - - -def _translate_attachment_detail_view(_context, vol): - """Maps keys for attachment details view.""" - - d = _translate_attachment_summary_view(_context, vol) - - # No additional data / lookups at the moment - - return d - - -def _translate_attachment_summary_view(_context, vol): - """Maps keys for attachment summary view.""" - d = {} - - volume_id = vol['id'] - - # NOTE(justinsb): We use the volume id as the id of the attachment object - d['id'] = volume_id - - d['volume_id'] = volume_id - d['server_id'] = vol['instance_uuid'] - if vol.get('mountpoint'): - d['device'] = vol['mountpoint'] - - return d - - -def _translate_volume_detail_view(context, vol, image_id=None): - """Maps keys for volumes details view.""" - - d = _translate_volume_summary_view(context, vol, image_id) - - # No additional data / lookups at the moment - - return d - - -def _translate_volume_summary_view(context, vol, image_id=None): - """Maps keys for volumes summary view.""" - d = {} - - d['id'] = vol['id'] - d['status'] = vol['status'] - d['size'] = vol['size'] - d['availability_zone'] = vol['availability_zone'] - d['created_at'] = vol['created_at'] - - d['attachments'] = [] - if vol['attach_status'] == 'attached': - attachment = _translate_attachment_detail_view(context, vol) - d['attachments'].append(attachment) - - d['display_name'] = vol['display_name'] - d['display_description'] = vol['display_description'] - - if vol['volume_type_id'] and vol.get('volume_type'): - d['volume_type'] = vol['volume_type']['name'] - else: - # TODO(bcwaldon): remove str cast once we use uuids - d['volume_type'] = str(vol['volume_type_id']) - - d['snapshot_id'] = vol['snapshot_id'] - - if image_id: - d['image_id'] = image_id - - LOG.audit(_("vol=%s"), vol, context=context) - - if vol.get('volume_metadata'): - metadata = vol.get('volume_metadata') - d['metadata'] = dict((item['key'], item['value']) for item in metadata) - else: - d['metadata'] = {} - - return d - - -def make_attachment(elem): - elem.set('id') - elem.set('server_id') - elem.set('volume_id') - elem.set('device') - - -def make_volume(elem): - elem.set('id') - elem.set('status') - elem.set('size') - elem.set('availability_zone') - elem.set('created_at') - elem.set('display_name') - elem.set('display_description') - elem.set('volume_type') - elem.set('snapshot_id') - - attachments = xmlutil.SubTemplateElement(elem, 'attachments') - attachment = xmlutil.SubTemplateElement(attachments, 'attachment', - selector='attachments') - make_attachment(attachment) - - # Attach metadata node - elem.append(common.MetadataTemplate()) - - -class VolumeTemplate(xmlutil.TemplateBuilder): - def construct(self): - root = xmlutil.TemplateElement('volume', selector='volume') - make_volume(root) - return xmlutil.MasterTemplate(root, 1) - - -class VolumesTemplate(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 CommonDeserializer(wsgi.MetadataXMLDeserializer): - """Common deserializer to handle xml-formatted volume requests. - - Handles standard volume attributes as well as the optional metadata - attribute - """ - - metadata_deserializer = common.MetadataXMLDeserializer() - - def _extract_volume(self, node): - """Marshal the volume attribute of a parsed request.""" - volume = {} - volume_node = self.find_first_child_named(node, 'volume') - - attributes = ['display_name', 'display_description', 'size', - 'volume_type', 'availability_zone'] - for attr in attributes: - if volume_node.getAttribute(attr): - volume[attr] = volume_node.getAttribute(attr) - - metadata_node = self.find_first_child_named(volume_node, 'metadata') - if metadata_node is not None: - volume['metadata'] = self.extract_metadata(metadata_node) - - return volume - - -class CreateDeserializer(CommonDeserializer): - """Deserializer to handle xml-formatted create volume requests. - - Handles standard volume attributes as well as the optional metadata - attribute - """ - - def default(self, string): - """Deserialize an xml-formatted volume create request.""" - dom = minidom.parseString(string) - volume = self._extract_volume(dom) - return {'body': {'volume': volume}} - - -class VolumeController(wsgi.Controller): - """The Volumes API controller for the OpenStack API.""" - - def __init__(self, ext_mgr): - self.volume_api = volume.API() - self.ext_mgr = ext_mgr - super(VolumeController, self).__init__() - - @wsgi.serializers(xml=VolumeTemplate) - def show(self, req, id): - """Return data about the given volume.""" - context = req.environ['nova.context'] - - try: - vol = self.volume_api.get(context, id) - except exception.NotFound: - raise exc.HTTPNotFound() - - return {'volume': _translate_volume_detail_view(context, vol)} - - def delete(self, req, id): - """Delete a volume.""" - context = req.environ['nova.context'] - - LOG.audit(_("Delete volume with id: %s"), id, context=context) - - try: - volume = self.volume_api.get(context, id) - self.volume_api.delete(context, volume) - except exception.NotFound: - raise exc.HTTPNotFound() - return webob.Response(status_int=202) - - @wsgi.serializers(xml=VolumesTemplate) - def index(self, req): - """Returns a summary list of volumes.""" - return self._items(req, entity_maker=_translate_volume_summary_view) - - @wsgi.serializers(xml=VolumesTemplate) - def detail(self, req): - """Returns a detailed list of volumes.""" - return self._items(req, entity_maker=_translate_volume_detail_view) - - def _items(self, req, entity_maker): - """Returns a list of volumes, transformed through entity_maker.""" - - search_opts = {} - search_opts.update(req.GET) - - context = req.environ['nova.context'] - remove_invalid_options(context, - search_opts, self._get_volume_search_options()) - - volumes = self.volume_api.get_all(context, search_opts=search_opts) - limited_list = common.limited(volumes, req) - res = [entity_maker(context, vol) for vol in limited_list] - return {'volumes': res} - - def _image_uuid_from_href(self, image_href): - # If the image href was generated by nova api, strip image_href - # down to an id. - try: - image_uuid = image_href.split('/').pop() - except (TypeError, AttributeError): - msg = _("Invalid imageRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - - if not utils.is_uuid_like(image_uuid): - msg = _("Invalid imageRef provided.") - raise exc.HTTPBadRequest(explanation=msg) - - return image_uuid - - @wsgi.serializers(xml=VolumeTemplate) - @wsgi.deserializers(xml=CreateDeserializer) - def create(self, req, body): - """Creates a new volume.""" - if not self.is_valid_body(body, 'volume'): - msg = _("Invalid request body. 'volume' not found") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - context = req.environ['nova.context'] - volume = body['volume'] - - kwargs = {} - - req_volume_type = volume.get('volume_type', None) - if req_volume_type: - try: - kwargs['volume_type'] = volume_types.get_volume_type_by_name( - context, req_volume_type) - except exception.NotFound: - raise exc.HTTPNotFound() - - kwargs['metadata'] = volume.get('metadata', None) - - snapshot_id = volume.get('snapshot_id') - if snapshot_id is not None: - kwargs['snapshot'] = self.volume_api.get_snapshot(context, - snapshot_id) - else: - kwargs['snapshot'] = None - - size = volume.get('size', None) - if size is None and kwargs['snapshot'] is not None: - size = kwargs['snapshot']['volume_size'] - - if size is None: - msg = _("Invalid request body. 'size' not found") - raise exc.HTTPUnprocessableEntity(explanation=msg) - - LOG.audit(_("Create volume of %s GB"), size, context=context) - - image_href = None - image_uuid = None - if self.ext_mgr.is_loaded('os-image-create'): - image_href = volume.get('imageRef') - if snapshot_id and image_href: - msg = _("Snapshot and image cannot be specified together.") - raise exc.HTTPBadRequest(explanation=msg) - if image_href: - image_uuid = self._image_uuid_from_href(image_href) - kwargs['image_id'] = image_uuid - - kwargs['availability_zone'] = volume.get('availability_zone', None) - - new_volume = self.volume_api.create(context, - size, - volume.get('display_name'), - volume.get('display_description'), - **kwargs) - - # TODO(vish): Instance should be None at db layer instead of - # trying to lazy load, but for now we turn it into - # a dict to avoid an error. - retval = _translate_volume_detail_view(context, dict(new_volume), - image_uuid) - - result = {'volume': retval} - - location = '%s/%s' % (req.url, new_volume['id']) - - return wsgi.ResponseObject(result, headers=dict(location=location)) - - def _get_volume_search_options(self): - """Return volume search options allowed by non-admin.""" - return ('name', 'status') - - -def create_resource(ext_mgr): - return wsgi.Resource(VolumeController(ext_mgr)) - - -def remove_invalid_options(context, search_options, allowed_search_options): - """Remove search options that are not valid for non-admin API/context.""" - if context.is_admin: - # Allow all options - return - # Otherwise, strip out all unknown options - unknown_options = [opt for opt in search_options - if opt not in allowed_search_options] - bad_options = ", ".join(unknown_options) - log_msg = _("Removing options '%(bad_options)s' from query") % locals() - LOG.debug(log_msg) - for opt in unknown_options: - search_options.pop(opt, None) diff --git a/nova/flags.py b/nova/flags.py index 3927e8f88..03e607c02 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -159,9 +159,6 @@ global_opts = [ cfg.StrOpt('scheduler_topic', default='scheduler', help='the topic scheduler nodes listen on'), - cfg.StrOpt('volume_topic', - default='volume', - help='the topic volume nodes listen on'), cfg.StrOpt('network_topic', default='network', help='the topic network nodes listen on'), @@ -169,7 +166,7 @@ global_opts = [ default=True, help='whether to rate limit the api'), cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi_compute', 'osapi_volume', 'metadata'], + default=['ec2', 'osapi_compute', 'metadata'], help='a list of APIs to enable by default'), cfg.StrOpt('ec2_host', default='$my_ip', @@ -197,16 +194,6 @@ global_opts = [ 'nova.api.openstack.compute.contrib.standard_extensions' ], help='osapi compute extension to load'), - cfg.ListOpt('osapi_volume_ext_list', - default=[], - help='Specify list of extensions to load when using osapi_' - 'volume_extension option with nova.api.openstack.' - 'volume.contrib.select_extensions'), - cfg.MultiStrOpt('osapi_volume_extension', - default=[ - 'nova.api.openstack.volume.contrib.standard_extensions' - ], - help='osapi volume extension to load'), cfg.StrOpt('osapi_path', default='/v1.1/', help='the path prefix used to call the openstack api server'), @@ -281,9 +268,6 @@ global_opts = [ cfg.StrOpt('network_manager', default='nova.network.manager.VlanManager', help='full class name for the Manager for network'), - cfg.StrOpt('volume_manager', - default='nova.volume.manager.VolumeManager', - help='full class name for the Manager for volume'), cfg.StrOpt('scheduler_manager', default='nova.scheduler.manager.SchedulerManager', help='full class name for the Manager for scheduler'), @@ -382,7 +366,7 @@ global_opts = [ default='nova.network.api.API', help='The full class name of the network API class to use'), cfg.StrOpt('volume_api_class', - default='nova.volume.api.API', + default='nova.volume.cinder.API', help='The full class name of the volume API class to use'), cfg.StrOpt('security_group_handler', default='nova.network.sg.NullSecurityGroupHandler', diff --git a/nova/network/manager.py b/nova/network/manager.py index a0a74bcfe..268c58238 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -65,11 +65,9 @@ from nova.network import rpcapi as network_rpcapi from nova.openstack.common import cfg from nova.openstack.common import excutils from nova.openstack.common import importutils -from nova.openstack.common import jsonutils from nova.openstack.common import lockutils from nova.openstack.common import log as logging from nova.openstack.common.notifier import api as notifier -from nova.openstack.common import rpc from nova.openstack.common import timeutils import nova.policy from nova import quota diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py index 90e1a004b..cba1ed935 100644 --- a/nova/scheduler/driver.py +++ b/nova/scheduler/driver.py @@ -87,20 +87,6 @@ def handle_schedule_error(context, ex, instance_uuid, request_spec): 'scheduler.run_instance', notifier.ERROR, payload) -def cast_to_volume_host(context, host, method, **kwargs): - """Cast request to a volume host queue""" - - volume_id = kwargs.get('volume_id', None) - if volume_id is not None: - now = timeutils.utcnow() - db.volume_update(context, volume_id, - {'host': host, 'scheduled_at': now}) - rpc.cast(context, - rpc.queue_get_for(context, FLAGS.volume_topic, host), - {"method": method, "args": kwargs}) - LOG.debug(_("Casted '%(method)s' to volume '%(host)s'") % locals()) - - def instance_update_db(context, instance_uuid): '''Clear the host and set the scheduled_at field of an Instance. @@ -127,13 +113,11 @@ def cast_to_compute_host(context, host, method, **kwargs): def cast_to_host(context, topic, host, method, **kwargs): """Generic cast to host""" - topic_mapping = { - FLAGS.compute_topic: cast_to_compute_host, - FLAGS.volume_topic: cast_to_volume_host} + topic_mapping = {FLAGS.compute_topic: cast_to_compute_host} func = topic_mapping.get(topic) if func: - func(context, host, method, **kwargs) + cast_to_compute_host(context, host, method, **kwargs) else: rpc.cast(context, rpc.queue_get_for(context, topic, host), diff --git a/nova/service.py b/nova/service.py index caff1672a..5a1532288 100644 --- a/nova/service.py +++ b/nova/service.py @@ -88,15 +88,6 @@ service_opts = [ cfg.IntOpt('metadata_workers', default=None, help='Number of workers for metadata service'), - cfg.StrOpt('osapi_volume_listen', - default="0.0.0.0", - help='IP address for OpenStack Volume API to listen'), - cfg.IntOpt('osapi_volume_listen_port', - default=8776, - help='port for os volume api to listen'), - cfg.IntOpt('osapi_volume_workers', - default=None, - help='Number of workers for OpenStack Volume API service'), ] FLAGS = flags.FLAGS diff --git a/nova/tests/api/ec2/test_cinder_cloud.py b/nova/tests/api/ec2/test_cinder_cloud.py index 389f71b7b..49ee9c152 100644 --- a/nova/tests/api/ec2/test_cinder_cloud.py +++ b/nova/tests/api/ec2/test_cinder_cloud.py @@ -18,7 +18,6 @@ # under the License. import copy -import shutil import tempfile from nova.api.ec2 import cloud @@ -85,8 +84,7 @@ class CinderCloudTestCase(test.TestCase): super(CinderCloudTestCase, self).setUp() vol_tmpdir = tempfile.mkdtemp() self.flags(compute_driver='nova.virt.fake.FakeDriver', - volume_api_class='nova.tests.fake_volume.API', - volumes_dir=vol_tmpdir) + volume_api_class='nova.tests.fake_volume.API') def fake_show(meh, context, id): return {'id': id, @@ -123,7 +121,6 @@ class CinderCloudTestCase(test.TestCase): self.compute = self.start_service('compute') self.scheduler = self.start_service('scheduler') self.network = self.start_service('network') - self.volume = self.start_service('volume') self.user_id = 'fake' self.project_id = 'fake' @@ -143,10 +140,6 @@ class CinderCloudTestCase(test.TestCase): '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6') def tearDown(self): - try: - shutil.rmtree(FLAGS.volumes_dir) - except OSError, e: - pass self.volume_api.reset_fake_api(self.context) super(CinderCloudTestCase, self).tearDown() fake.FakeImageService_reset() @@ -312,7 +305,7 @@ class CinderCloudTestCase(test.TestCase): kwargs = {'name': 'bdmtest-volume', 'description': 'bdm test volume description', 'status': 'available', - 'host': self.volume.host, + 'host': 'fake', 'size': 1, 'attach_status': 'detached', 'volume_id': values['id']} @@ -642,13 +635,12 @@ class CinderCloudTestCase(test.TestCase): kwargs = {'name': 'test-volume', 'description': 'test volume description', 'status': 'available', - 'host': self.volume.host, + 'host': 'fake', 'size': 1, 'attach_status': 'detached'} if volume_id: kwargs['volume_id'] = volume_id return self.volume_api.create_with_kwargs(self.context, **kwargs) - #return db.volume_create(self.context, kwargs) def _assert_volume_attached(self, vol, instance_uuid, mountpoint): self.assertEqual(vol['instance_uuid'], instance_uuid) diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index d86897dc1..1abde1069 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -22,7 +22,6 @@ import copy import datetime import functools import os -import shutil import string import tempfile @@ -46,7 +45,7 @@ from nova.tests import fake_network from nova.tests.image import fake from nova import utils from nova.virt import fake as fake_virt -from nova.volume import iscsi +from nova import volume LOG = logging.getLogger(__name__) @@ -97,10 +96,8 @@ class CloudTestCase(test.TestCase): super(CloudTestCase, self).setUp() vol_tmpdir = tempfile.mkdtemp() self.flags(compute_driver='nova.virt.fake.FakeDriver', + volume_api_class='nova.tests.fake_volume.API', volumes_dir=vol_tmpdir) - self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) - self.stubs.Set(iscsi.TgtAdm, 'remove_iscsi_target', - self.fake_remove_iscsi_target) def fake_show(meh, context, id): return {'id': id, @@ -137,13 +134,13 @@ class CloudTestCase(test.TestCase): self.compute = self.start_service('compute') self.scheduler = self.start_service('scheduler') self.network = self.start_service('network') - self.volume = self.start_service('volume') self.user_id = 'fake' self.project_id = 'fake' self.context = context.RequestContext(self.user_id, self.project_id, is_admin=True) + self.volume_api = volume.API() # NOTE(comstud): Make 'cast' behave like a 'call' which will # ensure that operations complete @@ -156,10 +153,7 @@ class CloudTestCase(test.TestCase): '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6') def tearDown(self): - try: - shutil.rmtree(FLAGS.volumes_dir) - except OSError, e: - pass + self.volume_api.reset_fake_api(self.context) super(CloudTestCase, self).tearDown() fake.FakeImageService_reset() @@ -681,63 +675,6 @@ class CloudTestCase(test.TestCase): self.cloud.delete_security_group(self.context, 'testgrp') - def test_describe_volumes(self): - """Makes sure describe_volumes works and filters results.""" - vol1 = db.volume_create(self.context, {'project_id': self.project_id}) - vol2 = db.volume_create(self.context, {'project_id': self.project_id}) - result = self.cloud.describe_volumes(self.context) - self.assertEqual(len(result['volumeSet']), 2) - volume_id = ec2utils.id_to_ec2_vol_id(vol2['id']) - result = self.cloud.describe_volumes(self.context, - volume_id=[volume_id]) - self.assertEqual(len(result['volumeSet']), 1) - self.assertEqual( - ec2utils.ec2_vol_id_to_uuid( - result['volumeSet'][0]['volumeId']), - vol2['id']) - db.volume_destroy(self.context, vol1['id']) - db.volume_destroy(self.context, vol2['id']) - - def test_create_volume_in_availability_zone(self): - """Makes sure create_volume works when we specify an availability - zone - """ - availability_zone = 'zone1:host1' - - result = self.cloud.create_volume(self.context, - size=1, - availability_zone=availability_zone) - volume_id = result['volumeId'] - availabilityZone = result['availabilityZone'] - self.assertEqual(availabilityZone, availability_zone) - result = self.cloud.describe_volumes(self.context) - self.assertEqual(len(result['volumeSet']), 1) - self.assertEqual(result['volumeSet'][0]['volumeId'], volume_id) - self.assertEqual(result['volumeSet'][0]['availabilityZone'], - availabilityZone) - - db.volume_destroy(self.context, ec2utils.ec2_vol_id_to_uuid(volume_id)) - - def test_create_volume_from_snapshot(self): - """Makes sure create_volume works when we specify a snapshot.""" - vol = db.volume_create(self.context, {'size': 1, - 'project_id': self.project_id}) - snap = db.snapshot_create(self.context, {'volume_id': vol['id'], - 'volume_size': vol['size'], - 'status': "available"}) - snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) - - result = self.cloud.create_volume(self.context, - snapshot_id=snapshot_id) - volume_id = result['volumeId'] - result = self.cloud.describe_volumes(self.context) - self.assertEqual(len(result['volumeSet']), 2) - self.assertEqual(result['volumeSet'][1]['volumeId'], volume_id) - - db.volume_destroy(self.context, ec2utils.ec2_vol_id_to_uuid(volume_id)) - db.snapshot_destroy(self.context, snap['id']) - db.volume_destroy(self.context, vol['id']) - def test_describe_availability_zones(self): """Makes sure describe_availability_zones works and filters results.""" service1 = db.service_create(self.context, {'host': 'host1_zones', @@ -772,63 +709,10 @@ class CloudTestCase(test.TestCase): result = self.cloud.describe_availability_zones(admin_ctxt, zone_name='verbose') - self.assertEqual(len(result['availabilityZoneInfo']), 15) + self.assertEqual(len(result['availabilityZoneInfo']), 13) db.service_destroy(self.context, service1['id']) db.service_destroy(self.context, service2['id']) - def test_describe_snapshots(self): - """Makes sure describe_snapshots works and filters results.""" - vol = db.volume_create(self.context, {}) - snap1 = db.snapshot_create(self.context, - {'volume_id': vol['id'], 'project_id': self.project_id}) - snap2 = db.snapshot_create(self.context, - {'volume_id': vol['id'], 'project_id': self.project_id}) - result = self.cloud.describe_snapshots(self.context) - self.assertEqual(len(result['snapshotSet']), 2) - snapshot_id = ec2utils.id_to_ec2_snap_id(snap2['id']) - result = self.cloud.describe_snapshots(self.context, - snapshot_id=[snapshot_id]) - self.assertEqual(len(result['snapshotSet']), 1) - self.assertEqual( - ec2utils.ec2_snap_id_to_uuid( - result['snapshotSet'][0]['snapshotId']), - snap2['id']) - db.snapshot_destroy(self.context, snap1['id']) - db.snapshot_destroy(self.context, snap2['id']) - db.volume_destroy(self.context, vol['id']) - - def test_create_snapshot(self): - """Makes sure create_snapshot works.""" - vol = db.volume_create(self.context, - {'status': "available", 'size': 0}) - volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) - - result = self.cloud.create_snapshot(self.context, - volume_id=volume_id) - snapshot_id = result['snapshotId'] - result = self.cloud.describe_snapshots(self.context) - self.assertEqual(len(result['snapshotSet']), 1) - self.assertEqual(result['snapshotSet'][0]['snapshotId'], snapshot_id) - - db.snapshot_destroy(self.context, ec2utils.ec2_id_to_id(snapshot_id)) - db.volume_destroy(self.context, vol['id']) - - def test_delete_snapshot(self): - """Makes sure delete_snapshot works.""" - vol = db.volume_create(self.context, - {'status': "available", 'size': 0}) - snap = db.snapshot_create(self.context, - {'volume_id': vol['id'], - 'status': "available", - 'volume_size': 0}) - snapshot_id = ec2utils.id_to_ec2_snap_id(snap['id']) - - result = self.cloud.delete_snapshot(self.context, - snapshot_id=snapshot_id) - self.assertTrue(result) - - db.volume_destroy(self.context, vol['id']) - def test_describe_instances(self): """Makes sure describe_instances works and filters results.""" self.flags(use_ipv6=True) @@ -1035,189 +919,6 @@ class CloudTestCase(test.TestCase): result = self.cloud.describe_instances(self.context) self.assertEqual(len(result['reservationSet']), 2) - def _block_device_mapping_create(self, instance_uuid, mappings): - volumes = [] - for bdm in mappings: - db.block_device_mapping_create(self.context, bdm) - if 'volume_id' in bdm: - values = {'id': bdm['volume_id']} - for bdm_key, vol_key in [('snapshot_id', 'snapshot_id'), - ('snapshot_size', 'volume_size'), - ('delete_on_termination', - 'delete_on_termination')]: - if bdm_key in bdm: - values[vol_key] = bdm[bdm_key] - vol = db.volume_create(self.context, values) - db.volume_attached(self.context, vol['id'], - instance_uuid, bdm['device_name']) - volumes.append(vol) - return volumes - - def _setUpBlockDeviceMapping(self): - image_uuid = 'cedef40a-ed67-4d10-800e-17455edce175' - inst1 = db.instance_create(self.context, - {'image_ref': image_uuid, - 'instance_type_id': 1, - 'root_device_name': '/dev/sdb1'}) - inst2 = db.instance_create(self.context, - {'image_ref': image_uuid, - 'instance_type_id': 1, - 'root_device_name': '/dev/sdc1'}) - - instance_id = inst1['id'] - instance_uuid = inst1['uuid'] - mappings0 = [ - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb1', - 'snapshot_id': '1', - 'volume_id': '2'}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb2', - 'volume_id': '3', - 'volume_size': 1}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb3', - 'delete_on_termination': True, - 'snapshot_id': '4', - 'volume_id': '5'}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb4', - 'delete_on_termination': False, - 'snapshot_id': '6', - 'volume_id': '7'}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb5', - 'snapshot_id': '8', - 'volume_id': '9', - 'volume_size': 0}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb6', - 'snapshot_id': '10', - 'volume_id': '11', - 'volume_size': 1}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb7', - 'no_device': True}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb8', - 'virtual_name': 'swap'}, - {'instance_uuid': instance_uuid, - 'device_name': '/dev/sdb9', - 'virtual_name': 'ephemeral3'}] - - volumes = self._block_device_mapping_create(instance_uuid, mappings0) - return (inst1, inst2, volumes) - - def _tearDownBlockDeviceMapping(self, inst1, inst2, volumes): - for vol in volumes: - db.volume_destroy(self.context, vol['id']) - for uuid in (inst1['uuid'], inst2['uuid']): - for bdm in db.block_device_mapping_get_all_by_instance( - self.context, uuid): - db.block_device_mapping_destroy(self.context, bdm['id']) - db.instance_destroy(self.context, inst2['uuid']) - db.instance_destroy(self.context, inst1['uuid']) - - _expected_instance_bdm1 = { - 'instanceId': 'i-00000001', - 'rootDeviceName': '/dev/sdb1', - 'rootDeviceType': 'ebs'} - - _expected_block_device_mapping0 = [ - {'deviceName': '/dev/sdb1', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': '2', - }}, - {'deviceName': '/dev/sdb2', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': '3', - }}, - {'deviceName': '/dev/sdb3', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': True, - 'volumeId': '5', - }}, - {'deviceName': '/dev/sdb4', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': '7', - }}, - {'deviceName': '/dev/sdb5', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': '9', - }}, - {'deviceName': '/dev/sdb6', - 'ebs': {'status': 'in-use', - 'deleteOnTermination': False, - 'volumeId': '11', }}] - # NOTE(yamahata): swap/ephemeral device case isn't supported yet. - - _expected_instance_bdm2 = { - 'instanceId': 'i-00000002', - 'rootDeviceName': '/dev/sdc1', - 'rootDeviceType': 'instance-store'} - - def test_format_instance_bdm(self): - (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() - - result = {} - self.cloud._format_instance_bdm(self.context, inst1['uuid'], - '/dev/sdb1', result) - self.assertSubDictMatch( - {'rootDeviceType': self._expected_instance_bdm1['rootDeviceType']}, - result) - self._assertEqualBlockDeviceMapping( - self._expected_block_device_mapping0, result['blockDeviceMapping']) - - result = {} - self.cloud._format_instance_bdm(self.context, inst2['uuid'], - '/dev/sdc1', result) - self.assertSubDictMatch( - {'rootDeviceType': self._expected_instance_bdm2['rootDeviceType']}, - result) - - self._tearDownBlockDeviceMapping(inst1, inst2, volumes) - - def _assertInstance(self, instance_id): - ec2_instance_id = ec2utils.id_to_ec2_inst_id(instance_id) - result = self.cloud.describe_instances(self.context, - instance_id=[ec2_instance_id]) - result = result['reservationSet'][0] - self.assertEqual(len(result['instancesSet']), 1) - result = result['instancesSet'][0] - self.assertEqual(result['instanceId'], ec2_instance_id) - return result - - def _assertEqualBlockDeviceMapping(self, expected, result): - self.assertEqual(len(expected), len(result)) - for x in expected: - found = False - for y in result: - if x['deviceName'] == y['deviceName']: - self.assertSubDictMatch(x, y) - found = True - break - self.assertTrue(found) - - def test_describe_instances_bdm(self): - """Make sure describe_instances works with root_device_name and - block device mappings - """ - (inst1, inst2, volumes) = self._setUpBlockDeviceMapping() - - result = self._assertInstance(inst1['uuid']) - self.assertSubDictMatch(self._expected_instance_bdm1, result) - self._assertEqualBlockDeviceMapping( - self._expected_block_device_mapping0, result['blockDeviceMapping']) - - result = self._assertInstance(inst2['uuid']) - self.assertSubDictMatch(self._expected_instance_bdm2, result) - - self._tearDownBlockDeviceMapping(inst1, inst2, volumes) - def test_describe_images(self): describe_images = self.cloud.describe_images @@ -1281,13 +982,17 @@ class CloudTestCase(test.TestCase): {'device': 'sdc3', 'virtual': 'swap'}, {'device': 'sdc4', 'virtual': 'swap'}] block_device_mapping1 = [ - {'device_name': '/dev/sdb1', 'snapshot_id': 01234567}, - {'device_name': '/dev/sdb2', 'volume_id': 01234567}, + {'device_name': '/dev/sdb1', + 'snapshot_id': 'ccec42a2-c220-4806-b762-6b12fbb592e3'}, + {'device_name': '/dev/sdb2', + 'volume_id': 'ccec42a2-c220-4806-b762-6b12fbb592e4'}, {'device_name': '/dev/sdb3', 'virtual_name': 'ephemeral5'}, {'device_name': '/dev/sdb4', 'no_device': True}, - {'device_name': '/dev/sdc1', 'snapshot_id': 12345678}, - {'device_name': '/dev/sdc2', 'volume_id': 12345678}, + {'device_name': '/dev/sdc1', + 'snapshot_id': 'ccec42a2-c220-4806-b762-6b12fbb592e5'}, + {'device_name': '/dev/sdc2', + 'volume_id': 'ccec42a2-c220-4806-b762-6b12fbb592e6'}, {'device_name': '/dev/sdc3', 'virtual_name': 'ephemeral6'}, {'device_name': '/dev/sdc4', 'no_device': True}] image1 = { @@ -1305,7 +1010,7 @@ class CloudTestCase(test.TestCase): mappings2 = [{'device': '/dev/sda1', 'virtual': 'root'}] block_device_mapping2 = [{'device_name': '/dev/sdb1', - 'snapshot_id': 01234567}] + 'snapshot_id': 'ccec42a2-c220-4806-b762-6b12fbb592e7'}] image2 = { 'id': '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6', 'name': 'fake_name', @@ -1338,11 +1043,7 @@ class CloudTestCase(test.TestCase): vol = self._volume_create(bdm['volume_id']) volumes.append(vol['id']) if 'snapshot_id' in bdm: - snap = db.snapshot_create(self.context, - {'id': bdm['snapshot_id'], - 'volume_id': 01234567, - 'status': "available", - 'volume_size': 1}) + snap = self._snapshot_create(bdm['snapshot_id']) snapshots.append(snap['id']) return (volumes, snapshots) @@ -1364,24 +1065,24 @@ class CloudTestCase(test.TestCase): _expected_bdms1 = [ {'deviceName': '/dev/sdb0', 'virtualName': 'ephemeral0'}, {'deviceName': '/dev/sdb1', 'ebs': {'snapshotId': - 'snap-00053977'}}, + 'snap-00000001'}}, {'deviceName': '/dev/sdb2', 'ebs': {'snapshotId': - 'vol-00053977'}}, + 'vol-00000001'}}, {'deviceName': '/dev/sdb3', 'virtualName': 'ephemeral5'}, # {'deviceName': '/dev/sdb4', 'noDevice': True}, {'deviceName': '/dev/sdc0', 'virtualName': 'swap'}, {'deviceName': '/dev/sdc1', 'ebs': {'snapshotId': - 'snap-00bc614e'}}, + 'snap-00000002'}}, {'deviceName': '/dev/sdc2', 'ebs': {'snapshotId': - 'vol-00bc614e'}}, + 'vol-00000002'}}, {'deviceName': '/dev/sdc3', 'virtualName': 'ephemeral6'}, # {'deviceName': '/dev/sdc4', 'noDevice': True} ] _expected_root_device_name2 = '/dev/sdb1' _expected_bdms2 = [{'deviceName': '/dev/sdb1', - 'ebs': {'snapshotId': 'snap-00053977'}}] + 'ebs': {'snapshotId': 'snap-00000003'}}] # NOTE(yamahata): # InstanceBlockDeviceMappingItemType @@ -2066,228 +1767,30 @@ class CloudTestCase(test.TestCase): self.assertTrue(result) def _volume_create(self, volume_id=None): - location = '10.0.2.15:3260' - iqn = 'iqn.2010-10.org.openstack:%s' % volume_id - kwargs = {'status': 'available', - 'host': self.volume.host, + kwargs = {'name': 'test-volume', + 'description': 'test volume description', + 'status': 'available', + 'host': 'fake', 'size': 1, - 'provider_location': '1 %s,fake %s' % (location, iqn), - 'attach_status': 'detached', } + 'attach_status': 'detached'} if volume_id: - kwargs['id'] = volume_id - return db.volume_create(self.context, kwargs) - - def _assert_volume_attached(self, vol, instance_uuid, mountpoint): - self.assertEqual(vol['instance_uuid'], instance_uuid) - self.assertEqual(vol['mountpoint'], mountpoint) - self.assertEqual(vol['status'], "in-use") - self.assertEqual(vol['attach_status'], "attached") - self.assertNotEqual(vol['attach_time'], None) - - def _assert_volume_detached(self, vol): - self.assertEqual(vol['instance_uuid'], None) - self.assertEqual(vol['mountpoint'], None) - self.assertEqual(vol['status'], "available") - self.assertEqual(vol['attach_status'], "detached") - self.assertEqual(vol['attach_time'], None) - - def test_stop_start_with_volume(self): - """Make sure run instance with block device mapping works""" - - # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) - - vol1 = self._volume_create() - vol2 = self._volume_create() - kwargs = {'image_id': 'ami-1', - 'instance_type': FLAGS.default_instance_type, - 'max_count': 1, - 'block_device_mapping': [{'device_name': '/dev/vdb', - 'volume_id': vol1['id'], - 'delete_on_termination': False}, - {'device_name': '/dev/vdc', - 'volume_id': vol2['id'], - 'delete_on_termination': True}, - ]} - ec2_instance_id = self._run_instance(**kwargs) - instance_uuid = ec2utils.ec2_instance_id_to_uuid(self.context, - ec2_instance_id) - instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) - - vols = db.volume_get_all_by_instance_uuid(self.context, instance_uuid) - self.assertEqual(len(vols), 2) - for vol in vols: - self.assertTrue(vol['id'] == vol1['id'] or vol['id'] == vol2['id']) - - vol = db.volume_get(self.context, vol1['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/vdb') - - vol = db.volume_get(self.context, vol2['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/vdc') - - result = self.cloud.stop_instances(self.context, [ec2_instance_id]) - self.assertTrue(result) - - vol = db.volume_get(self.context, vol1['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/vdb') - - vol = db.volume_get(self.context, vol2['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/vdc') - - self.cloud.start_instances(self.context, [ec2_instance_id]) - vols = db.volume_get_all_by_instance_uuid(self.context, instance_uuid) - self.assertEqual(len(vols), 2) - for vol in vols: - self.assertTrue(vol['id'] == vol1['id'] or vol['id'] == vol2['id']) - self.assertTrue(vol['mountpoint'] == '/dev/vdb' or - vol['mountpoint'] == '/dev/vdc') - self.assertEqual(vol['instance_uuid'], instance_uuid) - self.assertEqual(vol['status'], "in-use") - self.assertEqual(vol['attach_status'], "attached") - - self.cloud.terminate_instances(self.context, [ec2_instance_id]) - - admin_ctxt = context.get_admin_context(read_deleted="no") - vol = db.volume_get(admin_ctxt, vol1['id']) - self.assertFalse(vol['deleted']) - db.volume_destroy(self.context, vol1['id']) - - admin_ctxt = context.get_admin_context(read_deleted="only") - vol = db.volume_get(admin_ctxt, vol2['id']) - self.assertTrue(vol['deleted']) - - self._restart_compute_service() - - def test_stop_with_attached_volume(self): - """Make sure attach info is reflected to block device mapping""" - # enforce periodic tasks run in short time to avoid wait for 60s. - self._restart_compute_service(periodic_interval=0.3) - - vol1 = self._volume_create() - vol2 = self._volume_create() - kwargs = {'image_id': 'ami-1', - 'instance_type': FLAGS.default_instance_type, - 'max_count': 1, - 'block_device_mapping': [{'device_name': '/dev/sdb', - 'volume_id': vol1['id'], - 'delete_on_termination': True}]} - ec2_instance_id = self._run_instance(**kwargs) - instance_id = ec2utils.ec2_id_to_id(ec2_instance_id) - instance_uuid = ec2utils.ec2_instance_id_to_uuid(self.context, - ec2_instance_id) - - vols = db.volume_get_all_by_instance_uuid(self.context, instance_uuid) - self.assertEqual(len(vols), 1) - for vol in vols: - self.assertEqual(vol['id'], vol1['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/sdb') - - vol = db.volume_get(self.context, vol2['id']) - self._assert_volume_detached(vol) - - instance = db.instance_get(self.context, instance_id) - self.cloud.compute_api.attach_volume(self.context, - instance, - volume_id=vol2['id'], - device='/dev/vdc') - vol = db.volume_get(self.context, vol2['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/sdc') - - self.cloud.compute_api.detach_volume(self.context, - volume_id=vol1['id']) - vol = db.volume_get(self.context, vol1['id']) - self._assert_volume_detached(vol) - - result = self.cloud.stop_instances(self.context, [ec2_instance_id]) - self.assertTrue(result) - - vol = db.volume_get(self.context, vol2['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/sdc') - - self.cloud.start_instances(self.context, [ec2_instance_id]) - vols = db.volume_get_all_by_instance_uuid(self.context, instance_uuid) - self.assertEqual(len(vols), 1) - for vol in vols: - self.assertEqual(vol['id'], vol2['id']) - self._assert_volume_attached(vol, instance_uuid, '/dev/sdc') - - vol = db.volume_get(self.context, vol1['id']) - self._assert_volume_detached(vol) - - self.cloud.terminate_instances(self.context, [ec2_instance_id]) - - for vol_id in (vol1['id'], vol2['id']): - vol = db.volume_get(self.context, vol_id) - self.assertEqual(vol['id'], vol_id) - self._assert_volume_detached(vol) - db.volume_destroy(self.context, vol_id) - - self._restart_compute_service() + kwargs['volume_id'] = volume_id + return self.volume_api.create_with_kwargs(self.context, **kwargs) + + def _snapshot_create(self, snapshot_id=None): + kwargs = {'volume_id': 'ccec42a2-c220-4806-b762-6b12fbb592e4', + 'status': "available", + 'volume_size': 1} + if snapshot_id: + kwargs['snap_id'] = snapshot_id + return self.volume_api.create_snapshot_with_kwargs(self.context, + **kwargs) def _create_snapshot(self, ec2_volume_id): result = self.cloud.create_snapshot(self.context, volume_id=ec2_volume_id) return result['snapshotId'] - def test_run_with_snapshot(self): - """Makes sure run/stop/start instance with snapshot works.""" - vol = self._volume_create() - ec2_volume_id = ec2utils.id_to_ec2_vol_id(vol['id']) - - ec2_snapshot1_id = self._create_snapshot(ec2_volume_id) - snapshot1_id = ec2utils.ec2_snap_id_to_uuid(ec2_snapshot1_id) - ec2_snapshot2_id = self._create_snapshot(ec2_volume_id) - snapshot2_id = ec2utils.ec2_snap_id_to_uuid(ec2_snapshot2_id) - - kwargs = {'image_id': 'ami-1', - 'instance_type': FLAGS.default_instance_type, - 'max_count': 1, - 'block_device_mapping': [{'device_name': '/dev/vdb', - 'snapshot_id': snapshot1_id, - 'delete_on_termination': False, }, - {'device_name': '/dev/vdc', - 'snapshot_id': snapshot2_id, - 'delete_on_termination': True}]} - ec2_instance_id = self._run_instance(**kwargs) - instance_id = ec2utils.ec2_vol_id_to_uuid(ec2_instance_id) - instance_uuid = ec2utils.ec2_instance_id_to_uuid(self.context, - ec2_instance_id) - - vols = db.volume_get_all_by_instance_uuid(self.context, instance_uuid) - self.assertEqual(len(vols), 2) - vol1_id = None - vol2_id = None - for vol in vols: - snapshot_id = vol['snapshot_id'] - if snapshot_id == snapshot1_id: - vol1_id = vol['id'] - mountpoint = '/dev/vdb' - elif snapshot_id == snapshot2_id: - vol2_id = vol['id'] - mountpoint = '/dev/vdc' - else: - self.fail() - - self._assert_volume_attached(vol, instance_uuid, mountpoint) - - self.assertTrue(vol1_id) - self.assertTrue(vol2_id) - - self.cloud.terminate_instances(self.context, [ec2_instance_id]) - - admin_ctxt = context.get_admin_context(read_deleted="no") - vol = db.volume_get(admin_ctxt, vol1_id) - self._assert_volume_detached(vol) - self.assertFalse(vol['deleted']) - db.volume_destroy(self.context, vol1_id) - - admin_ctxt = context.get_admin_context(read_deleted="only") - vol = db.volume_get(admin_ctxt, vol2_id) - self.assertTrue(vol['deleted']) - - for snapshot_id in (ec2_snapshot1_id, ec2_snapshot2_id): - self.cloud.delete_snapshot(self.context, snapshot_id) - def _do_test_create_image(self, no_reboot): """Make sure that CreateImage works""" # enforce periodic tasks run in short time to avoid wait for 60s. @@ -2363,7 +1866,7 @@ class CloudTestCase(test.TestCase): self.assertEquals(bdm.get('deviceName'), 'sda1') self.assertTrue('ebs' in bdm) self.assertEquals(bdm['ebs'].get('snapshotId'), - 'snap-%08x' % snapshots[0]) + ec2utils.id_to_ec2_snap_id(snapshots[0])) self.assertEquals(created_image.get('kernelId'), 'aki-00000001') self.assertEquals(created_image.get('ramdiskId'), 'ari-00000002') self.assertEquals(created_image.get('rootDeviceType'), 'ebs') @@ -2371,11 +1874,6 @@ class CloudTestCase(test.TestCase): self.assertNotEqual(virt_driver.get('powered_off'), no_reboot) self.cloud.terminate_instances(self.context, [ec2_instance_id]) - for vol in volumes: - db.volume_destroy(self.context, vol) - for snap in snapshots: - db.snapshot_destroy(self.context, snap) - # TODO(yamahata): clean up snapshot created by CreateImage. self._restart_compute_service() diff --git a/nova/tests/api/ec2/test_ec2_validate.py b/nova/tests/api/ec2/test_ec2_validate.py index 9a14a7d7c..f46262c35 100644 --- a/nova/tests/api/ec2/test_ec2_validate.py +++ b/nova/tests/api/ec2/test_ec2_validate.py @@ -50,7 +50,6 @@ class EC2ValidateTestCase(test.TestCase): self.compute = self.start_service('compute') self.scheduter = self.start_service('scheduler') self.network = self.start_service('network') - self.volume = self.start_service('volume') self.image_service = fake.FakeImageService() self.user_id = 'fake' diff --git a/nova/tests/api/openstack/compute/contrib/test_snapshots.py b/nova/tests/api/openstack/compute/contrib/test_snapshots.py index ad259c302..6e76fc04a 100644 --- a/nova/tests/api/openstack/compute/contrib/test_snapshots.py +++ b/nova/tests/api/openstack/compute/contrib/test_snapshots.py @@ -25,7 +25,7 @@ from nova.openstack.common import log as logging from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack import fakes -from nova import volume +from nova.volume import cinder FLAGS = flags.FLAGS @@ -92,14 +92,16 @@ class SnapshotApiTest(test.TestCase): super(SnapshotApiTest, self).setUp() fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) - self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) - self.stubs.Set(volume.api.API, "create_snapshot_force", + self.stubs.Set(cinder.API, "create_snapshot", + stub_snapshot_create) + self.stubs.Set(cinder.API, "create_snapshot_force", stub_snapshot_create) - self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) - self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) - self.stubs.Set(volume.api.API, "get_all_snapshots", + self.stubs.Set(cinder.API, "delete_snapshot", + stub_snapshot_delete) + self.stubs.Set(cinder.API, "get_snapshot", stub_snapshot_get) + self.stubs.Set(cinder.API, "get_all_snapshots", stub_snapshot_get_all) - self.stubs.Set(volume.api.API, "get", fakes.stub_volume_get) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get) self.flags( osapi_compute_extension=[ 'nova.api.openstack.compute.contrib.select_extensions'], diff --git a/nova/tests/api/openstack/compute/contrib/test_volume_types.py b/nova/tests/api/openstack/compute/contrib/test_volume_types.py deleted file mode 100644 index af88cf601..000000000 --- a/nova/tests/api/openstack/compute/contrib/test_volume_types.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 lxml import etree -import webob - -from nova.api.openstack.compute.contrib import volumetypes -from nova import exception -from nova.openstack.common import log as logging -from nova import test -from nova.tests.api.openstack import fakes -from nova.volume import volume_types - - -LOG = logging.getLogger(__name__) -last_param = {} - - -def stub_volume_type(id): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) - - -def return_volume_types_get_all_types(context): - return dict(vol_type_1=stub_volume_type(1), - vol_type_2=stub_volume_type(2), - vol_type_3=stub_volume_type(3)) - - -def return_empty_volume_types_get_all_types(context): - return {} - - -def return_volume_types_get_volume_type(context, id): - if id == "777": - raise exception.VolumeTypeNotFound(volume_type_id=id) - return stub_volume_type(int(id)) - - -def return_volume_types_destroy(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - pass - - -def return_volume_types_create(context, name, specs): - pass - - -def return_volume_types_get_by_name(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - return stub_volume_type(int(name.split("_")[2])) - - -class VolumeTypesApiTest(test.TestCase): - def setUp(self): - super(VolumeTypesApiTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) - self.controller = volumetypes.VolumeTypesController() - - def test_volume_types_index(self): - self.stubs.Set(volume_types, 'get_all_types', - return_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.index(req) - - self.assertEqual(3, len(res_dict['volume_types'])) - - expected_names = ['vol_type_1', 'vol_type_2', 'vol_type_3'] - actual_names = map(lambda e: e['name'], res_dict['volume_types']) - self.assertEqual(set(actual_names), set(expected_names)) - for entry in res_dict['volume_types']: - self.assertEqual('value1', entry['extra_specs']['key1']) - - def test_volume_types_index_no_data(self): - self.stubs.Set(volume_types, 'get_all_types', - return_empty_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.index(req) - - self.assertEqual(0, len(res_dict['volume_types'])) - - def test_volume_types_show(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') - res_dict = self.controller.show(req, 1) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - def test_volume_types_show_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, '777') - - def test_volume_types_delete(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/1') - self.controller.delete(req, 1) - - def test_volume_types_delete_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, - req, '777') - - def test_create(self): - self.stubs.Set(volume_types, 'create', - return_volume_types_create) - self.stubs.Set(volume_types, 'get_volume_type_by_name', - return_volume_types_get_by_name) - - body = {"volume_type": {"name": "vol_type_1", - "extra_specs": {"key1": "value1"}}} - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - res_dict = self.controller.create(req, body) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - -class VolumeTypesSerializerTest(test.TestCase): - def _verify_volume_type(self, vtype, tree): - self.assertEqual('volume_type', tree.tag) - self.assertEqual(vtype['name'], tree.get('name')) - self.assertEqual(str(vtype['id']), tree.get('id')) - self.assertEqual(1, len(tree)) - extra_specs = tree[0] - self.assertEqual('extra_specs', extra_specs.tag) - seen = set(vtype['extra_specs'].keys()) - for child in extra_specs: - self.assertTrue(child.tag in seen) - self.assertEqual(vtype['extra_specs'][child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_index_serializer(self): - serializer = volumetypes.VolumeTypesTemplate() - - # Just getting some input data - vtypes = return_volume_types_get_all_types(None) - text = serializer.serialize({'volume_types': vtypes.values()}) - - print text - tree = etree.fromstring(text) - - self.assertEqual('volume_types', tree.tag) - self.assertEqual(len(vtypes), len(tree)) - for child in tree: - name = child.get('name') - self.assertTrue(name in vtypes) - self._verify_volume_type(vtypes[name], child) - - def test_voltype_serializer(self): - serializer = volumetypes.VolumeTypeTemplate() - - vtype = stub_volume_type(1) - text = serializer.serialize(dict(volume_type=vtype)) - - print text - tree = etree.fromstring(text) - - self._verify_volume_type(vtype, tree) - - -class VolumeTypesUnprocessableEntityTestCase(test.TestCase): - """ - Tests of places we throw 422 Unprocessable Entity from - """ - - def setUp(self): - super(VolumeTypesUnprocessableEntityTestCase, self).setUp() - self.controller = volumetypes.VolumeTypesController() - - def _unprocessable_volume_type_create(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/os-volume-types') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, body) - - def test_create_volume_type_no_body(self): - self._unprocessable_volume_type_create(body=None) - - def test_create_volume_type_missing_volume_type(self): - body = {'foo': {'a': 'b'}} - self._unprocessable_volume_type_create(body=body) - - def test_create_volume_type_malformed_entity(self): - body = {'volume_type': 'string'} - self._unprocessable_volume_type_create(body=body) diff --git a/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py b/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py deleted file mode 100644 index e9c4034f0..000000000 --- a/nova/tests/api/openstack/compute/contrib/test_volume_types_extra_specs.py +++ /dev/null @@ -1,198 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# Copyright 2011 University of Southern California -# All Rights Reserved. -# -# 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 lxml import etree -import webob - -from nova.api.openstack.compute.contrib import volumetypes -from nova import test -from nova.tests.api.openstack import fakes -import nova.wsgi - - -def return_create_volume_type_extra_specs(context, volume_type_id, - extra_specs): - return stub_volume_type_extra_specs() - - -def return_volume_type_extra_specs(context, volume_type_id): - return stub_volume_type_extra_specs() - - -def return_empty_volume_type_extra_specs(context, volume_type_id): - return {} - - -def delete_volume_type_extra_specs(context, volume_type_id, key): - pass - - -def stub_volume_type_extra_specs(): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return specs - - -class VolumeTypesExtraSpecsTest(test.TestCase): - - def setUp(self): - super(VolumeTypesExtraSpecsTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) - self.api_path = '/v2/fake/os-volume-types/1/extra_specs' - self.controller = volumetypes.VolumeTypeExtraSpecsController() - - def test_index(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.index(req, 1) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_index_no_data(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.index(req, 1) - - self.assertEqual(0, len(res_dict['extra_specs'])) - - def test_show(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - res_dict = self.controller.show(req, 1, 'key5') - - self.assertEqual('value5', res_dict['key5']) - - def test_show_spec_not_found(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key6') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, 1, 'key6') - - def test_delete(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_delete', - delete_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - self.controller.delete(req, 1, 'key5') - - def test_create(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"extra_specs": {"key1": "value1"}} - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.create(req, 1, body) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_create_empty_body(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, - req, 1, '') - - def test_update_item(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - res_dict = self.controller.update(req, 1, 'key1', body) - - self.assertEqual('value1', res_dict['key1']) - - def test_update_item_empty_body(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', '') - - def test_update_item_too_many_keys(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1", "key2": "value2"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', body) - - def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/bad') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'bad', body) - - -class VolumeTypeExtraSpecsSerializerTest(test.TestCase): - def test_index_create_serializer(self): - serializer = volumetypes.VolumeTypeExtraSpecsTemplate() - - # Just getting some input data - extra_specs = stub_volume_type_extra_specs() - text = serializer.serialize(dict(extra_specs=extra_specs)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('extra_specs', tree.tag) - self.assertEqual(len(extra_specs), len(tree)) - seen = set(extra_specs.keys()) - for child in tree: - self.assertTrue(child.tag in seen) - self.assertEqual(extra_specs[child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_update_show_serializer(self): - serializer = volumetypes.VolumeTypeExtraSpecTemplate() - - exemplar = dict(key1='value1') - text = serializer.serialize(exemplar) - - print text - tree = etree.fromstring(text) - - self.assertEqual('key1', tree.tag) - self.assertEqual('value1', tree.text) - self.assertEqual(0, len(tree)) diff --git a/nova/tests/api/openstack/compute/contrib/test_volumes.py b/nova/tests/api/openstack/compute/contrib/test_volumes.py index a07c3fa74..6c092cbd4 100644 --- a/nova/tests/api/openstack/compute/contrib/test_volumes.py +++ b/nova/tests/api/openstack/compute/contrib/test_volumes.py @@ -18,7 +18,6 @@ import datetime from lxml import etree import webob -import nova from nova.api.openstack.compute.contrib import volumes from nova.compute import api as compute_api from nova.compute import instance_types @@ -29,7 +28,7 @@ from nova.openstack.common import jsonutils from nova.openstack.common import timeutils from nova import test from nova.tests.api.openstack import fakes -from nova.volume import api as volume_api +from nova.volume import cinder from webob import exc @@ -148,9 +147,9 @@ class VolumeApiTest(test.TestCase): fakes.stub_out_rate_limiting(self.stubs) self.stubs.Set(db, 'volume_get', return_volume) - self.stubs.Set(volume_api.API, "delete", fakes.stub_volume_delete) - self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get) - self.stubs.Set(volume_api.API, "get_all", fakes.stub_volume_get_all) + self.stubs.Set(cinder.API, "delete", fakes.stub_volume_delete) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get) + self.stubs.Set(cinder.API, "get_all", fakes.stub_volume_get_all) self.flags( osapi_compute_extension=[ 'nova.api.openstack.compute.contrib.select_extensions'], @@ -160,7 +159,7 @@ class VolumeApiTest(test.TestCase): self.app = fakes.wsgi_app(init_only=('os-volumes',)) def test_volume_create(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) + self.stubs.Set(cinder.API, "create", fakes.stub_volume_create) vol = {"size": 100, "display_name": "Volume Test Name", @@ -202,7 +201,7 @@ class VolumeApiTest(test.TestCase): self.assertEqual(resp.status_int, 200) def test_volume_show_no_volume(self): - self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get_notfound) req = webob.Request.blank('/v2/fake/os-volumes/456') resp = req.get_response(self.app) @@ -215,7 +214,7 @@ class VolumeApiTest(test.TestCase): self.assertEqual(resp.status_int, 202) def test_volume_delete_no_volume(self): - self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound) + self.stubs.Set(cinder.API, "get", fakes.stub_volume_get_notfound) req = webob.Request.blank('/v2/fake/os-volumes/456') req.method = 'DELETE' diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index e07db3519..466fd3636 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -194,7 +194,6 @@ class ExtensionControllerTest(ExtensionTestCase): "UserData", "VirtualInterfaces", "Volumes", - "VolumeTypes", ] self.ext_list.sort() diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 58dc2df45..4f39e569e 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -30,7 +30,6 @@ from nova.api.openstack import compute from nova.api.openstack.compute import limits from nova.api.openstack.compute import versions from nova.api.openstack import urlmap -from nova.api.openstack import volume from nova.api.openstack import wsgi as os_wsgi from nova.compute import api as compute_api from nova.compute import instance_types diff --git a/nova/tests/api/openstack/volume/__init__.py b/nova/tests/api/openstack/volume/__init__.py deleted file mode 100644 index 7e04e7c73..000000000 --- a/nova/tests/api/openstack/volume/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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. - -# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work -from nova.tests import * diff --git a/nova/tests/api/openstack/volume/contrib/__init__.py b/nova/tests/api/openstack/volume/contrib/__init__.py deleted file mode 100644 index 7e04e7c73..000000000 --- a/nova/tests/api/openstack/volume/contrib/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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. - -# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work -from nova.tests import * diff --git a/nova/tests/api/openstack/volume/contrib/test_admin_actions.py b/nova/tests/api/openstack/volume/contrib/test_admin_actions.py deleted file mode 100644 index 4ade54779..000000000 --- a/nova/tests/api/openstack/volume/contrib/test_admin_actions.py +++ /dev/null @@ -1,184 +0,0 @@ -import webob - -from nova import context -from nova import db -from nova import exception -from nova.openstack.common import jsonutils -from nova import test -from nova.tests.api.openstack import fakes - - -def app(): - # no auth, just let environ['nova.context'] pass through - api = fakes.volume.APIRouter(init_only=('volumes', 'snapshots')) - mapper = fakes.urlmap.URLMap() - mapper['/v1'] = api - return mapper - - -class AdminActionsTest(test.TestCase): - - def test_reset_status_as_admin(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # current status is available - volume = db.volume_create(ctx, {'status': 'available'}) - req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # request status of 'error' - req.body = jsonutils.dumps({'os-reset_status': {'status': 'error'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # request is accepted - self.assertEquals(resp.status_int, 202) - volume = db.volume_get(ctx, volume['id']) - # status changed to 'error' - self.assertEquals(volume['status'], 'error') - - def test_reset_status_as_non_admin(self): - # current status is 'error' - volume = db.volume_create(context.get_admin_context(), - {'status': 'error'}) - req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # request changing status to available - req.body = jsonutils.dumps({'os-reset_status': {'status': - 'available'}}) - # non-admin context - req.environ['nova.context'] = context.RequestContext('fake', 'fake') - resp = req.get_response(app()) - # request is not authorized - self.assertEquals(resp.status_int, 403) - volume = db.volume_get(context.get_admin_context(), volume['id']) - # status is still 'error' - self.assertEquals(volume['status'], 'error') - - def test_malformed_reset_status_body(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # current status is available - volume = db.volume_create(ctx, {'status': 'available'}) - req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # malformed request body - req.body = jsonutils.dumps({'os-reset_status': {'x-status': 'bad'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # bad request - self.assertEquals(resp.status_int, 400) - volume = db.volume_get(ctx, volume['id']) - # status is still 'available' - self.assertEquals(volume['status'], 'available') - - def test_invalid_status_for_volume(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # current status is available - volume = db.volume_create(ctx, {'status': 'available'}) - req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # 'invalid' is not a valid status - req.body = jsonutils.dumps({'os-reset_status': {'status': 'invalid'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # bad request - self.assertEquals(resp.status_int, 400) - volume = db.volume_get(ctx, volume['id']) - # status is still 'available' - self.assertEquals(volume['status'], 'available') - - def test_reset_status_for_missing_volume(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # missing-volume-id - req = webob.Request.blank('/v1/fake/volumes/%s/action' % - 'missing-volume-id') - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # malformed request body - req.body = jsonutils.dumps({'os-reset_status': {'status': - 'available'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # not found - self.assertEquals(resp.status_int, 404) - self.assertRaises(exception.NotFound, db.volume_get, ctx, - 'missing-volume-id') - - def test_snapshot_reset_status(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # snapshot in 'error_deleting' - volume = db.volume_create(ctx, {}) - snapshot = db.snapshot_create(ctx, {'status': 'error_deleting', - 'volume_id': volume['id']}) - req = webob.Request.blank('/v1/fake/snapshots/%s/action' % - snapshot['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # request status of 'error' - req.body = jsonutils.dumps({'os-reset_status': {'status': 'error'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # request is accepted - self.assertEquals(resp.status_int, 202) - snapshot = db.snapshot_get(ctx, snapshot['id']) - # status changed to 'error' - self.assertEquals(snapshot['status'], 'error') - - def test_invalid_status_for_snapshot(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # snapshot in 'available' - volume = db.volume_create(ctx, {}) - snapshot = db.snapshot_create(ctx, {'status': 'available', - 'volume_id': volume['id']}) - req = webob.Request.blank('/v1/fake/snapshots/%s/action' % - snapshot['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - # 'attaching' is not a valid status for snapshots - req.body = jsonutils.dumps({'os-reset_status': {'status': - 'attaching'}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # request is accepted - print resp - self.assertEquals(resp.status_int, 400) - snapshot = db.snapshot_get(ctx, snapshot['id']) - # status is still 'available' - self.assertEquals(snapshot['status'], 'available') - - def test_force_delete(self): - # admin context - ctx = context.RequestContext('admin', 'fake', is_admin=True) - ctx.elevated() # add roles - # current status is creating - volume = db.volume_create(ctx, {'status': 'creating'}) - req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id']) - req.method = 'POST' - req.headers['content-type'] = 'application/json' - req.body = jsonutils.dumps({'os-force_delete': {}}) - # attach admin context to request - req.environ['nova.context'] = ctx - resp = req.get_response(app()) - # request is accepted - self.assertEquals(resp.status_int, 202) - # volume is deleted - self.assertRaises(exception.NotFound, db.volume_get, ctx, volume['id']) diff --git a/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py b/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py deleted file mode 100644 index edc127b8a..000000000 --- a/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py +++ /dev/null @@ -1,226 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# Copyright 2011 University of Southern California -# All Rights Reserved. -# -# 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 lxml import etree -import webob - -from nova.api.openstack.volume.contrib import types_extra_specs -from nova import test -from nova.tests.api.openstack import fakes -import nova.wsgi - - -def return_create_volume_type_extra_specs(context, volume_type_id, - extra_specs): - return stub_volume_type_extra_specs() - - -def return_volume_type_extra_specs(context, volume_type_id): - return stub_volume_type_extra_specs() - - -def return_empty_volume_type_extra_specs(context, volume_type_id): - return {} - - -def delete_volume_type_extra_specs(context, volume_type_id, key): - pass - - -def stub_volume_type_extra_specs(): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return specs - - -def volume_type_get(context, volume_type_id): - pass - - -class VolumeTypesExtraSpecsTest(test.TestCase): - - def setUp(self): - super(VolumeTypesExtraSpecsTest, self).setUp() - self.stubs.Set(nova.db, 'volume_type_get', volume_type_get) - self.api_path = '/v1/fake/os-volume-types/1/extra_specs' - self.controller = types_extra_specs.VolumeTypeExtraSpecsController() - - def test_index(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.index(req, 1) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_index_no_data(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.index(req, 1) - - self.assertEqual(0, len(res_dict['extra_specs'])) - - def test_show(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - res_dict = self.controller.show(req, 1, 'key5') - - self.assertEqual('value5', res_dict['key5']) - - def test_show_spec_not_found(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_get', - return_empty_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key6') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, 1, 'key6') - - def test_delete(self): - self.stubs.Set(nova.db, 'volume_type_extra_specs_delete', - delete_volume_type_extra_specs) - - req = fakes.HTTPRequest.blank(self.api_path + '/key5') - self.controller.delete(req, 1, 'key5') - - def test_create(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"extra_specs": {"key1": "value1"}} - - req = fakes.HTTPRequest.blank(self.api_path) - res_dict = self.controller.create(req, 1, body) - - self.assertEqual('value1', res_dict['extra_specs']['key1']) - - def test_update_item(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - res_dict = self.controller.update(req, 1, 'key1', body) - - self.assertEqual('value1', res_dict['key1']) - - def test_update_item_too_many_keys(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1", "key2": "value2"} - - req = fakes.HTTPRequest.blank(self.api_path + '/key1') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'key1', body) - - def test_update_item_body_uri_mismatch(self): - self.stubs.Set(nova.db, - 'volume_type_extra_specs_update_or_create', - return_create_volume_type_extra_specs) - body = {"key1": "value1"} - - req = fakes.HTTPRequest.blank(self.api_path + '/bad') - self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, - req, 1, 'bad', body) - - -class VolumeTypeExtraSpecsSerializerTest(test.TestCase): - def test_index_create_serializer(self): - serializer = types_extra_specs.VolumeTypeExtraSpecsTemplate() - - # Just getting some input data - extra_specs = stub_volume_type_extra_specs() - text = serializer.serialize(dict(extra_specs=extra_specs)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('extra_specs', tree.tag) - self.assertEqual(len(extra_specs), len(tree)) - seen = set(extra_specs.keys()) - for child in tree: - self.assertTrue(child.tag in seen) - self.assertEqual(extra_specs[child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_update_show_serializer(self): - serializer = types_extra_specs.VolumeTypeExtraSpecTemplate() - - exemplar = dict(key1='value1') - text = serializer.serialize(exemplar) - - print text - tree = etree.fromstring(text) - - self.assertEqual('key1', tree.tag) - self.assertEqual('value1', tree.text) - self.assertEqual(0, len(tree)) - - -class VolumeTypeExtraSpecsUnprocessableEntityTestCase(test.TestCase): - - """ - Tests of places we throw 422 Unprocessable Entity from - """ - - def setUp(self): - super(VolumeTypeExtraSpecsUnprocessableEntityTestCase, self).setUp() - self.controller = types_extra_specs.VolumeTypeExtraSpecsController() - - def _unprocessable_extra_specs_create(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/types/1/extra_specs') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, '1', body) - - def test_create_no_body(self): - self._unprocessable_extra_specs_create(body=None) - - def test_create_missing_volume(self): - body = {'foo': {'a': 'b'}} - self._unprocessable_extra_specs_create(body=body) - - def test_create_malformed_entity(self): - body = {'extra_specs': 'string'} - self._unprocessable_extra_specs_create(body=body) - - def _unprocessable_extra_specs_update(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/types/1/extra_specs') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.update, req, '1', body) - - def test_update_no_body(self): - self._unprocessable_extra_specs_update(body=None) - - def test_update_empty_body(self): - self._unprocessable_extra_specs_update(body={}) diff --git a/nova/tests/api/openstack/volume/contrib/test_types_manage.py b/nova/tests/api/openstack/volume/contrib/test_types_manage.py deleted file mode 100644 index f69038144..000000000 --- a/nova/tests/api/openstack/volume/contrib/test_types_manage.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 webob - -from nova.api.openstack.volume.contrib import types_manage -from nova import exception -from nova import test -from nova.tests.api.openstack import fakes -from nova.volume import volume_types - - -def stub_volume_type(id): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) - - -def return_volume_types_get_volume_type(context, id): - if id == "777": - raise exception.VolumeTypeNotFound(volume_type_id=id) - return stub_volume_type(int(id)) - - -def return_volume_types_destroy(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - pass - - -def return_volume_types_create(context, name, specs): - pass - - -def return_volume_types_get_by_name(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - return stub_volume_type(int(name.split("_")[2])) - - -class VolumeTypesManageApiTest(test.TestCase): - def setUp(self): - super(VolumeTypesManageApiTest, self).setUp() - self.controller = types_manage.VolumeTypesManageController() - - def test_volume_types_delete(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v1/fake/types/1') - self.controller._delete(req, 1) - - def test_volume_types_delete_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - self.stubs.Set(volume_types, 'destroy', - return_volume_types_destroy) - - req = fakes.HTTPRequest.blank('/v1/fake/types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete, - req, '777') - - def test_create(self): - self.stubs.Set(volume_types, 'create', - return_volume_types_create) - self.stubs.Set(volume_types, 'get_volume_type_by_name', - return_volume_types_get_by_name) - - body = {"volume_type": {"name": "vol_type_1", - "extra_specs": {"key1": "value1"}}} - req = fakes.HTTPRequest.blank('/v1/fake/types') - res_dict = self.controller._create(req, body) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - -class VolumeTypesUnprocessableEntityTestCase(test.TestCase): - - """ - Tests of places we throw 422 Unprocessable Entity from - """ - - def setUp(self): - super(VolumeTypesUnprocessableEntityTestCase, self).setUp() - self.controller = types_manage.VolumeTypesManageController() - - def _unprocessable_volume_type_create(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/types') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller._create, req, body) - - def test_create_no_body(self): - self._unprocessable_volume_type_create(body=None) - - def test_create_missing_volume(self): - body = {'foo': {'a': 'b'}} - self._unprocessable_volume_type_create(body=body) - - def test_create_malformed_entity(self): - body = {'volume_type': 'string'} - self._unprocessable_volume_type_create(body=body) diff --git a/nova/tests/api/openstack/volume/contrib/test_volume_actions.py b/nova/tests/api/openstack/volume/contrib/test_volume_actions.py deleted file mode 100644 index 4dd79d366..000000000 --- a/nova/tests/api/openstack/volume/contrib/test_volume_actions.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright 2012 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. - -import datetime -import webob - -from nova.api.openstack.volume.contrib import volume_actions -from nova import exception -from nova.openstack.common.rpc import common as rpc_common -from nova import test -from nova.tests.api.openstack import fakes -from nova.volume import api as volume_api - - -def stub_volume_get(self, context, volume_id): - volume = fakes.stub_volume(volume_id) - if volume_id == 5: - volume['status'] = 'in-use' - else: - volume['status'] = 'available' - return volume - - -def stub_upload_volume_to_image_service(self, context, volume, metadata, - force): - ret = {"id": volume['id'], - "updated_at": datetime.datetime(1, 1, 1, 1, 1, 1), - "status": 'uploading', - "display_description": volume['display_description'], - "size": volume['size'], - "volume_type": volume['volume_type'], - "image_id": 1, - "container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name'} - return ret - - -class VolumeImageActionsTest(test.TestCase): - def setUp(self): - super(VolumeImageActionsTest, self).setUp() - self.controller = volume_actions.VolumeActionsController() - - self.stubs.Set(volume_api.API, 'get', stub_volume_get) - - def test_copy_volume_to_image(self): - self.stubs.Set(volume_api.API, - "copy_volume_to_image", - stub_upload_volume_to_image_service) - - id = 1 - vol = {"container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name', - "force": True} - body = {"os-volume_upload_image": vol} - req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) - res_dict = self.controller._volume_upload_image(req, id, body) - expected = {'os-volume_upload_image': {'id': id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'status': 'uploading', - 'display_description': 'displaydesc', - 'size': 1, - 'volume_type': {'name': 'vol_type_name'}, - 'image_id': 1, - 'container_format': 'bare', - 'disk_format': 'raw', - 'image_name': 'image_name'}} - self.assertDictMatch(res_dict, expected) - - def test_copy_volume_to_image_volumenotfound(self): - def stub_volume_get_raise_exc(self, context, volume_id): - raise exception.VolumeNotFound(volume_id=volume_id) - - self.stubs.Set(volume_api.API, 'get', stub_volume_get_raise_exc) - - id = 1 - vol = {"container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name', - "force": True} - body = {"os-volume_upload_image": vol} - req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) - self.assertRaises(webob.exc.HTTPNotFound, - self.controller._volume_upload_image, - req, - id, - body) - - def test_copy_volume_to_image_invalidvolume(self): - def stub_upload_volume_to_image_service_raise(self, context, volume, - metadata, force): - raise exception.InvalidVolume - self.stubs.Set(volume_api.API, - "copy_volume_to_image", - stub_upload_volume_to_image_service_raise) - - id = 1 - vol = {"container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name', - "force": True} - body = {"os-volume_upload_image": vol} - req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller._volume_upload_image, - req, - id, - body) - - def test_copy_volume_to_image_valueerror(self): - def stub_upload_volume_to_image_service_raise(self, context, volume, - metadata, force): - raise ValueError - self.stubs.Set(volume_api.API, - "copy_volume_to_image", - stub_upload_volume_to_image_service_raise) - - id = 1 - vol = {"container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name', - "force": True} - body = {"os-volume_upload_image": vol} - req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller._volume_upload_image, - req, - id, - body) - - def test_copy_volume_to_image_remoteerror(self): - def stub_upload_volume_to_image_service_raise(self, context, volume, - metadata, force): - raise rpc_common.RemoteError - self.stubs.Set(volume_api.API, - "copy_volume_to_image", - stub_upload_volume_to_image_service_raise) - - id = 1 - vol = {"container_format": 'bare', - "disk_format": 'raw', - "image_name": 'image_name', - "force": True} - body = {"os-volume_upload_image": vol} - req = fakes.HTTPRequest.blank('/v1/tenant1/volumes/%s/action' % id) - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller._volume_upload_image, - req, - id, - body) diff --git a/nova/tests/api/openstack/volume/extensions/__init__.py b/nova/tests/api/openstack/volume/extensions/__init__.py deleted file mode 100644 index 848908a95..000000000 --- a/nova/tests/api/openstack/volume/extensions/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 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. diff --git a/nova/tests/api/openstack/volume/extensions/foxinsocks.py b/nova/tests/api/openstack/volume/extensions/foxinsocks.py deleted file mode 100644 index cf901472c..000000000 --- a/nova/tests/api/openstack/volume/extensions/foxinsocks.py +++ /dev/null @@ -1,94 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 webob.exc - -from nova.api.openstack import extensions -from nova.api.openstack import wsgi - - -class FoxInSocksController(object): - - def index(self, req): - return "Try to say this Mr. Knox, sir..." - - -class FoxInSocksServerControllerExtension(wsgi.Controller): - @wsgi.action('add_tweedle') - def _add_tweedle(self, req, id, body): - - return "Tweedle Beetle Added." - - @wsgi.action('delete_tweedle') - def _delete_tweedle(self, req, id, body): - - return "Tweedle Beetle Deleted." - - @wsgi.action('fail') - def _fail(self, req, id, body): - - raise webob.exc.HTTPBadRequest(explanation='Tweedle fail') - - -class FoxInSocksFlavorGooseControllerExtension(wsgi.Controller): - @wsgi.extends - def show(self, req, resp_obj, id): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing') - - -class FoxInSocksFlavorBandsControllerExtension(wsgi.Controller): - @wsgi.extends - def show(self, req, resp_obj, id): - #NOTE: This only handles JSON responses. - # You can use content type header to test for XML. - resp_obj.obj['big_bands'] = 'Pig Bands!' - - -class Foxinsocks(extensions.ExtensionDescriptor): - """The Fox In Socks Extension""" - - name = "Fox In Socks" - alias = "FOXNSOX" - namespace = "http://www.fox.in.socks/api/ext/pie/v1.0" - updated = "2011-01-22T13:25:27-06:00" - - def __init__(self, ext_mgr): - ext_mgr.register(self) - - def get_resources(self): - resources = [] - resource = extensions.ResourceExtension('foxnsocks', - FoxInSocksController()) - resources.append(resource) - return resources - - def get_controller_extensions(self): - extension_list = [] - - extension_set = [ - (FoxInSocksServerControllerExtension, 'servers'), - (FoxInSocksFlavorGooseControllerExtension, 'flavors'), - (FoxInSocksFlavorBandsControllerExtension, 'flavors'), - ] - for klass, collection in extension_set: - controller = klass() - ext = extensions.ControllerExtension(self, collection, controller) - extension_list.append(ext) - - return extension_list diff --git a/nova/tests/api/openstack/volume/test_extensions.py b/nova/tests/api/openstack/volume/test_extensions.py deleted file mode 100644 index e291e74dd..000000000 --- a/nova/tests/api/openstack/volume/test_extensions.py +++ /dev/null @@ -1,155 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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 iso8601 -from lxml import etree -import webob - -from nova.api.openstack import volume -from nova.api.openstack import xmlutil -from nova import flags -from nova.openstack.common import jsonutils -from nova import test - -FLAGS = flags.FLAGS -NS = "{http://docs.openstack.org/common/api/v1.0}" - - -class ExtensionTestCase(test.TestCase): - def setUp(self): - super(ExtensionTestCase, self).setUp() - ext_list = FLAGS.osapi_volume_extension[:] - fox = ('nova.tests.api.openstack.volume.extensions.' - 'foxinsocks.Foxinsocks') - if fox not in ext_list: - ext_list.append(fox) - self.flags(osapi_volume_extension=ext_list) - - -class ExtensionControllerTest(ExtensionTestCase): - - def setUp(self): - super(ExtensionControllerTest, self).setUp() - self.ext_list = [ - "TypesManage", - "TypesExtraSpecs", - ] - self.ext_list.sort() - - def test_list_extensions_json(self): - app = volume.APIRouter() - request = webob.Request.blank("/fake/extensions") - response = request.get_response(app) - self.assertEqual(200, response.status_int) - - # Make sure we have all the extensions, extra extensions being OK. - data = jsonutils.loads(response.body) - names = [str(x['name']) for x in data['extensions'] - if str(x['name']) in self.ext_list] - names.sort() - self.assertEqual(names, self.ext_list) - - # Ensure all the timestamps are valid according to iso8601 - for ext in data['extensions']: - iso8601.parse_date(ext['updated']) - - # Make sure that at least Fox in Sox is correct. - (fox_ext, ) = [ - x for x in data['extensions'] if x['alias'] == 'FOXNSOX'] - self.assertEqual(fox_ext, { - 'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0', - 'name': 'Fox In Socks', - 'updated': '2011-01-22T13:25:27-06:00', - 'description': 'The Fox In Socks Extension', - 'alias': 'FOXNSOX', - 'links': [] - }, - ) - - for ext in data['extensions']: - url = '/fake/extensions/%s' % ext['alias'] - request = webob.Request.blank(url) - response = request.get_response(app) - output = jsonutils.loads(response.body) - self.assertEqual(output['extension']['alias'], ext['alias']) - - def test_get_extension_json(self): - app = volume.APIRouter() - request = webob.Request.blank("/fake/extensions/FOXNSOX") - response = request.get_response(app) - self.assertEqual(200, response.status_int) - - data = jsonutils.loads(response.body) - self.assertEqual(data['extension'], { - "namespace": "http://www.fox.in.socks/api/ext/pie/v1.0", - "name": "Fox In Socks", - "updated": "2011-01-22T13:25:27-06:00", - "description": "The Fox In Socks Extension", - "alias": "FOXNSOX", - "links": []}) - - def test_get_non_existing_extension_json(self): - app = volume.APIRouter() - request = webob.Request.blank("/fake/extensions/4") - response = request.get_response(app) - self.assertEqual(404, response.status_int) - - def test_list_extensions_xml(self): - app = volume.APIRouter() - request = webob.Request.blank("/fake/extensions") - request.accept = "application/xml" - response = request.get_response(app) - self.assertEqual(200, response.status_int) - - root = etree.XML(response.body) - self.assertEqual(root.tag.split('extensions')[0], NS) - - # Make sure we have all the extensions, extras extensions being OK. - exts = root.findall('{0}extension'.format(NS)) - self.assert_(len(exts) >= len(self.ext_list)) - - # Make sure that at least Fox in Sox is correct. - (fox_ext, ) = [x for x in exts if x.get('alias') == 'FOXNSOX'] - self.assertEqual(fox_ext.get('name'), 'Fox In Socks') - self.assertEqual(fox_ext.get('namespace'), - 'http://www.fox.in.socks/api/ext/pie/v1.0') - self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00') - self.assertEqual(fox_ext.findtext('{0}description'.format(NS)), - 'The Fox In Socks Extension') - - xmlutil.validate_schema(root, 'extensions') - - def test_get_extension_xml(self): - app = volume.APIRouter() - request = webob.Request.blank("/fake/extensions/FOXNSOX") - request.accept = "application/xml" - response = request.get_response(app) - self.assertEqual(200, response.status_int) - xml = response.body - - root = etree.XML(xml) - self.assertEqual(root.tag.split('extension')[0], NS) - self.assertEqual(root.get('alias'), 'FOXNSOX') - self.assertEqual(root.get('name'), 'Fox In Socks') - self.assertEqual(root.get('namespace'), - 'http://www.fox.in.socks/api/ext/pie/v1.0') - self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00') - self.assertEqual(root.findtext('{0}description'.format(NS)), - 'The Fox In Socks Extension') - - xmlutil.validate_schema(root, 'extension') diff --git a/nova/tests/api/openstack/volume/test_router.py b/nova/tests/api/openstack/volume/test_router.py deleted file mode 100644 index 5934a21e5..000000000 --- a/nova/tests/api/openstack/volume/test_router.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2011 Denali Systems, Inc. -# All Rights Reserved. -# -# 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 nova.api.openstack import volume -from nova.api.openstack.volume import snapshots -from nova.api.openstack.volume import versions -from nova.api.openstack.volume import volumes -from nova.api.openstack import wsgi -from nova import flags -from nova.openstack.common import log as logging -from nova import test -from nova.tests.api.openstack import fakes - -FLAGS = flags.FLAGS - -LOG = logging.getLogger(__name__) - - -class FakeController(object): - def __init__(self, ext_mgr=None): - self.ext_mgr = ext_mgr - - def index(self, req): - return {} - - def detail(self, req): - return {} - - -def create_resource(ext_mgr): - return wsgi.Resource(FakeController(ext_mgr)) - - -def create_volume_resource(ext_mgr): - return wsgi.Resource(FakeController(ext_mgr)) - - -class VolumeRouterTestCase(test.TestCase): - def setUp(self): - super(VolumeRouterTestCase, self).setUp() - # NOTE(vish): versions is just returning text so, no need to stub. - self.stubs.Set(snapshots, 'create_resource', create_resource) - self.stubs.Set(volumes, 'create_resource', create_volume_resource) - self.app = volume.APIRouter() - - def test_versions(self): - req = fakes.HTTPRequest.blank('') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(302, response.status_int) - req = fakes.HTTPRequest.blank('/') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) - - def test_versions_dispatch(self): - req = fakes.HTTPRequest.blank('/') - req.method = 'GET' - req.content_type = 'application/json' - resource = versions.Versions() - result = resource.dispatch(resource.index, req, {}) - self.assertTrue(result) - - def test_volumes(self): - req = fakes.HTTPRequest.blank('/fake/volumes') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) - - def test_volumes_detail(self): - req = fakes.HTTPRequest.blank('/fake/volumes/detail') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) - - def test_types(self): - req = fakes.HTTPRequest.blank('/fake/types') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) - - def test_snapshots(self): - req = fakes.HTTPRequest.blank('/fake/snapshots') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) - - def test_snapshots_detail(self): - req = fakes.HTTPRequest.blank('/fake/snapshots/detail') - req.method = 'GET' - req.content_type = 'application/json' - response = req.get_response(self.app) - self.assertEqual(200, response.status_int) diff --git a/nova/tests/api/openstack/volume/test_snapshots.py b/nova/tests/api/openstack/volume/test_snapshots.py deleted file mode 100644 index c6e703f83..000000000 --- a/nova/tests/api/openstack/volume/test_snapshots.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright 2011 Denali Systems, Inc. -# All Rights Reserved. -# -# 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 lxml import etree -import webob - -from nova.api.openstack.volume import snapshots -from nova import db -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova.openstack.common import timeutils -from nova import test -from nova.tests.api.openstack import fakes -from nova import volume - -FLAGS = flags.FLAGS - -LOG = logging.getLogger(__name__) - - -def _get_default_snapshot_param(): - return { - 'id': 123, - 'volume_id': 12, - 'status': 'available', - 'volume_size': 100, - 'created_at': None, - 'display_name': 'Default name', - 'display_description': 'Default description', - } - - -def stub_snapshot_create(self, context, volume_id, name, description): - snapshot = _get_default_snapshot_param() - snapshot['volume_id'] = volume_id - snapshot['display_name'] = name - snapshot['display_description'] = description - return snapshot - - -def stub_snapshot_delete(self, context, snapshot): - if snapshot['id'] != 123: - raise exception.NotFound - - -def stub_snapshot_get(self, context, snapshot_id): - if snapshot_id != 123: - raise exception.NotFound - - param = _get_default_snapshot_param() - return param - - -def stub_snapshot_get_all(self, context, search_opts=None): - param = _get_default_snapshot_param() - return [param] - - -class SnapshotApiTest(test.TestCase): - def setUp(self): - super(SnapshotApiTest, self).setUp() - self.controller = snapshots.SnapshotsController() - - self.stubs.Set(db, 'snapshot_get_all_by_project', - fakes.stub_snapshot_get_all_by_project) - self.stubs.Set(db, 'snapshot_get_all', - fakes.stub_snapshot_get_all) - - def test_snapshot_create(self): - self.stubs.Set(volume.api.API, "create_snapshot", stub_snapshot_create) - self.stubs.Set(volume.api.API, 'get', fakes.stub_volume_get) - snapshot = {"volume_id": '12', - "force": False, - "display_name": "Snapshot Test Name", - "display_description": "Snapshot Test Desc"} - body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v1/snapshots') - resp_dict = self.controller.create(req, body) - - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['display_name'], - snapshot['display_name']) - self.assertEqual(resp_dict['snapshot']['display_description'], - snapshot['display_description']) - - def test_snapshot_create_force(self): - self.stubs.Set(volume.api.API, "create_snapshot_force", - stub_snapshot_create) - self.stubs.Set(volume.api.API, 'get', fakes.stub_volume_get) - snapshot = {"volume_id": '12', - "force": True, - "display_name": "Snapshot Test Name", - "display_description": "Snapshot Test Desc"} - body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v1/snapshots') - resp_dict = self.controller.create(req, body) - - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['display_name'], - snapshot['display_name']) - self.assertEqual(resp_dict['snapshot']['display_description'], - snapshot['display_description']) - - # Test invalid force paramter - snapshot = {"volume_id": 12, - "force": '**&&^^%%$$##@@'} - body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v1/snapshots') - self.assertRaises(exception.InvalidParameterValue, - self.controller.create, - req, - body) - - def test_snapshot_create_nonexistent_volume_id(self): - self.stubs.Set(volume.api.API, 'get', fakes.stub_volume_get_notfound) - - snapshot = {"volume_id": 13, - "force": False, - "display_name": "Snapshot Test Name", - "display_description": "Snapshot Test Desc"} - body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v1/snapshots') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.create, - req, - body) - - def test_snapshot_delete(self): - self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) - self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) - - snapshot_id = 123 - req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id) - resp = self.controller.delete(req, snapshot_id) - self.assertEqual(resp.status_int, 202) - - def test_snapshot_delete_invalid_id(self): - self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete) - snapshot_id = 234 - req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id) - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, - req, - snapshot_id) - - def test_snapshot_show(self): - self.stubs.Set(volume.api.API, "get_snapshot", stub_snapshot_get) - req = fakes.HTTPRequest.blank('/v1/snapshots/123') - resp_dict = self.controller.show(req, 123) - - self.assertTrue('snapshot' in resp_dict) - self.assertEqual(resp_dict['snapshot']['id'], '123') - - def test_snapshot_show_invalid_id(self): - snapshot_id = 234 - req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id) - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, - req, - snapshot_id) - - def test_snapshot_detail(self): - self.stubs.Set(volume.api.API, "get_all_snapshots", - stub_snapshot_get_all) - req = fakes.HTTPRequest.blank('/v1/snapshots/detail') - resp_dict = self.controller.detail(req) - - self.assertTrue('snapshots' in resp_dict) - resp_snapshots = resp_dict['snapshots'] - self.assertEqual(len(resp_snapshots), 1) - - resp_snapshot = resp_snapshots.pop() - self.assertEqual(resp_snapshot['id'], '123') - - def test_admin_list_snapshots_limited_to_project(self): - req = fakes.HTTPRequest.blank('/v1/fake/snapshots', - use_admin_context=True) - res = self.controller.index(req) - - self.assertTrue('snapshots' in res) - self.assertEqual(1, len(res['snapshots'])) - - def test_admin_list_snapshots_all_tenants(self): - req = fakes.HTTPRequest.blank('/v1/fake/snapshots?all_tenants=1', - use_admin_context=True) - res = self.controller.index(req) - self.assertTrue('snapshots' in res) - self.assertEqual(3, len(res['snapshots'])) - - def test_all_tenants_non_admin_gets_all_tenants(self): - req = fakes.HTTPRequest.blank('/v1/fake/snapshots?all_tenants=1') - res = self.controller.index(req) - self.assertTrue('snapshots' in res) - self.assertEqual(1, len(res['snapshots'])) - - def test_non_admin_get_by_project(self): - req = fakes.HTTPRequest.blank('/v1/fake/snapshots') - res = self.controller.index(req) - self.assertTrue('snapshots' in res) - self.assertEqual(1, len(res['snapshots'])) - - -class SnapshotSerializerTest(test.TestCase): - def _verify_snapshot(self, snap, tree): - self.assertEqual(tree.tag, 'snapshot') - - for attr in ('id', 'status', 'size', 'created_at', - 'display_name', 'display_description', 'volume_id'): - self.assertEqual(str(snap[attr]), tree.get(attr)) - - def test_snapshot_show_create_serializer(self): - serializer = snapshots.SnapshotTemplate() - raw_snapshot = dict( - id='snap_id', - status='snap_status', - size=1024, - created_at=timeutils.utcnow(), - display_name='snap_name', - display_description='snap_desc', - volume_id='vol_id', - ) - text = serializer.serialize(dict(snapshot=raw_snapshot)) - - print text - tree = etree.fromstring(text) - - self._verify_snapshot(raw_snapshot, tree) - - def test_snapshot_index_detail_serializer(self): - serializer = snapshots.SnapshotsTemplate() - raw_snapshots = [dict( - id='snap1_id', - status='snap1_status', - size=1024, - created_at=timeutils.utcnow(), - display_name='snap1_name', - display_description='snap1_desc', - volume_id='vol1_id', - ), - dict( - id='snap2_id', - status='snap2_status', - size=1024, - created_at=timeutils.utcnow(), - display_name='snap2_name', - display_description='snap2_desc', - volume_id='vol2_id', - )] - text = serializer.serialize(dict(snapshots=raw_snapshots)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('snapshots', tree.tag) - self.assertEqual(len(raw_snapshots), len(tree)) - for idx, child in enumerate(tree): - self._verify_snapshot(raw_snapshots[idx], child) - - -class SnapshotsUnprocessableEntityTestCase(test.TestCase): - - """ - Tests of places we throw 422 Unprocessable Entity from - """ - - def setUp(self): - super(SnapshotsUnprocessableEntityTestCase, self).setUp() - self.controller = snapshots.SnapshotsController() - - def _unprocessable_snapshot_create(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/snapshots') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, body) - - def test_create_no_body(self): - self._unprocessable_snapshot_create(body=None) - - def test_create_missing_snapshot(self): - body = {'foo': {'a': 'b'}} - self._unprocessable_snapshot_create(body=body) - - def test_create_malformed_entity(self): - body = {'snapshot': 'string'} - self._unprocessable_snapshot_create(body=body) diff --git a/nova/tests/api/openstack/volume/test_types.py b/nova/tests/api/openstack/volume/test_types.py deleted file mode 100644 index e7aa4cc0a..000000000 --- a/nova/tests/api/openstack/volume/test_types.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# aLL Rights Reserved. -# -# 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 lxml import etree -import webob - -from nova.api.openstack.volume import types -from nova.api.openstack.volume.views import types as views_types -from nova import exception -from nova.openstack.common import timeutils -from nova import test -from nova.tests.api.openstack import fakes -from nova.volume import volume_types - - -def stub_volume_type(id): - specs = { - "key1": "value1", - "key2": "value2", - "key3": "value3", - "key4": "value4", - "key5": "value5"} - return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) - - -def return_volume_types_get_all_types(context): - return dict(vol_type_1=stub_volume_type(1), - vol_type_2=stub_volume_type(2), - vol_type_3=stub_volume_type(3)) - - -def return_empty_volume_types_get_all_types(context): - return {} - - -def return_volume_types_get_volume_type(context, id): - if id == "777": - raise exception.VolumeTypeNotFound(volume_type_id=id) - return stub_volume_type(int(id)) - - -def return_volume_types_get_by_name(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - return stub_volume_type(int(name.split("_")[2])) - - -class VolumeTypesApiTest(test.TestCase): - def setUp(self): - super(VolumeTypesApiTest, self).setUp() - self.controller = types.VolumeTypesController() - - def test_volume_types_index(self): - self.stubs.Set(volume_types, 'get_all_types', - return_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v1/fake/types') - res_dict = self.controller.index(req) - - self.assertEqual(3, len(res_dict['volume_types'])) - - expected_names = ['vol_type_1', 'vol_type_2', 'vol_type_3'] - actual_names = map(lambda e: e['name'], res_dict['volume_types']) - self.assertEqual(set(actual_names), set(expected_names)) - for entry in res_dict['volume_types']: - self.assertEqual('value1', entry['extra_specs']['key1']) - - def test_volume_types_index_no_data(self): - self.stubs.Set(volume_types, 'get_all_types', - return_empty_volume_types_get_all_types) - - req = fakes.HTTPRequest.blank('/v1/fake/types') - res_dict = self.controller.index(req) - - self.assertEqual(0, len(res_dict['volume_types'])) - - def test_volume_types_show(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v1/fake/types/1') - res_dict = self.controller.show(req, 1) - - self.assertEqual(1, len(res_dict)) - self.assertEqual('1', res_dict['volume_type']['id']) - self.assertEqual('vol_type_1', res_dict['volume_type']['name']) - - def test_volume_types_show_not_found(self): - self.stubs.Set(volume_types, 'get_volume_type', - return_volume_types_get_volume_type) - - req = fakes.HTTPRequest.blank('/v1/fake/types/777') - self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, - req, '777') - - def test_view_builder_show(self): - view_builder = views_types.ViewBuilder() - - now = timeutils.isotime() - raw_volume_type = dict(name='new_type', - deleted=False, - created_at=now, - updated_at=now, - extra_specs={}, - deleted_at=None, - id=42) - - request = fakes.HTTPRequest.blank("/v1") - output = view_builder.show(request, raw_volume_type) - - self.assertTrue('volume_type' in output) - expected_volume_type = dict(name='new_type', - extra_specs={}, - id=42) - self.assertDictMatch(output['volume_type'], expected_volume_type) - - def test_view_builder_list(self): - view_builder = views_types.ViewBuilder() - - now = timeutils.isotime() - raw_volume_types = [] - for i in range(0, 10): - raw_volume_types.append(dict(name='new_type', - deleted=False, - created_at=now, - updated_at=now, - extra_specs={}, - deleted_at=None, - id=42 + i)) - - request = fakes.HTTPRequest.blank("/v1") - output = view_builder.index(request, raw_volume_types) - - self.assertTrue('volume_types' in output) - for i in range(0, 10): - expected_volume_type = dict(name='new_type', - extra_specs={}, - id=42 + i) - self.assertDictMatch(output['volume_types'][i], - expected_volume_type) - - -class VolumeTypesSerializerTest(test.TestCase): - def _verify_volume_type(self, vtype, tree): - self.assertEqual('volume_type', tree.tag) - self.assertEqual(vtype['name'], tree.get('name')) - self.assertEqual(str(vtype['id']), tree.get('id')) - self.assertEqual(1, len(tree)) - extra_specs = tree[0] - self.assertEqual('extra_specs', extra_specs.tag) - seen = set(vtype['extra_specs'].keys()) - for child in extra_specs: - self.assertTrue(child.tag in seen) - self.assertEqual(vtype['extra_specs'][child.tag], child.text) - seen.remove(child.tag) - self.assertEqual(len(seen), 0) - - def test_index_serializer(self): - serializer = types.VolumeTypesTemplate() - - # Just getting some input data - vtypes = return_volume_types_get_all_types(None) - text = serializer.serialize({'volume_types': vtypes.values()}) - - tree = etree.fromstring(text) - - self.assertEqual('volume_types', tree.tag) - self.assertEqual(len(vtypes), len(tree)) - for child in tree: - name = child.get('name') - self.assertTrue(name in vtypes) - self._verify_volume_type(vtypes[name], child) - - def test_voltype_serializer(self): - serializer = types.VolumeTypeTemplate() - - vtype = stub_volume_type(1) - text = serializer.serialize(dict(volume_type=vtype)) - - tree = etree.fromstring(text) - - self._verify_volume_type(vtype, tree) diff --git a/nova/tests/api/openstack/volume/test_volumes.py b/nova/tests/api/openstack/volume/test_volumes.py deleted file mode 100644 index e7a0e66f3..000000000 --- a/nova/tests/api/openstack/volume/test_volumes.py +++ /dev/null @@ -1,602 +0,0 @@ -# Copyright 2013 Josh Durgin -# All Rights Reserved. -# -# 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 datetime - -from lxml import etree -import webob - -from nova.api.openstack.volume import extensions -from nova.api.openstack.volume import volumes -from nova import db -from nova import exception -from nova import flags -from nova.openstack.common import timeutils -from nova import test -from nova.tests.api.openstack import fakes -from nova.tests.image import fake as fake_image -from nova.volume import api as volume_api - - -FLAGS = flags.FLAGS - -TEST_SNAPSHOT_UUID = '00000000-0000-0000-0000-000000000001' - - -def stub_snapshot_get(self, context, snapshot_id): - if snapshot_id != TEST_SNAPSHOT_UUID: - raise exception.NotFound - - return { - 'id': snapshot_id, - 'volume_id': 12, - 'status': 'available', - 'volume_size': 100, - 'created_at': None, - 'display_name': 'Default name', - 'display_description': 'Default description', - } - - -class VolumeApiTest(test.TestCase): - def setUp(self): - super(VolumeApiTest, self).setUp() - self.ext_mgr = extensions.ExtensionManager() - self.ext_mgr.extensions = {} - self.controller = volumes.VolumeController(self.ext_mgr) - - self.stubs.Set(db, 'volume_get_all', fakes.stub_volume_get_all) - self.stubs.Set(db, 'volume_get_all_by_project', - fakes.stub_volume_get_all_by_project) - self.stubs.Set(volume_api.API, 'get', fakes.stub_volume_get) - self.stubs.Set(volume_api.API, 'delete', fakes.stub_volume_delete) - - def test_volume_create(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) - - vol = {"size": 100, - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "zone1:host1"} - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - res = self.controller.create(req, body) - expected = {'volume': {'status': 'fakestatus', - 'display_description': 'Volume Test Desc', - 'availability_zone': 'zone1:host1', - 'display_name': 'Volume Test Name', - 'attachments': [{'device': '/', - 'server_id': 'fakeuuid', - 'id': '1', - 'volume_id': '1'}], - 'volume_type': 'vol_type_name', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': 100}} - self.assertEqual(res.obj, expected) - self.assertEqual(res.code, 200) - self.assertTrue('location' in res.headers) - - def test_volume_creation_fails_with_bad_size(self): - vol = {"size": '', - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "zone1:host1"} - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - self.assertRaises(exception.InvalidInput, - self.controller.create, - req, - body) - - def test_volume_create_no_body(self): - body = {} - req = fakes.HTTPRequest.blank('/v1/volumes') - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, - req, - body) - - def test_volume_create_with_image_id(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) - self.ext_mgr.extensions = {'os-image-create': 'fake'} - vol = {"size": '1', - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "nova", - "imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77'} - expected = {'volume': {'status': 'fakestatus', - 'display_description': 'Volume Test Desc', - 'availability_zone': 'nova', - 'display_name': 'Volume Test Name', - 'attachments': [{'device': '/', - 'server_id': 'fakeuuid', - 'id': '1', - 'volume_id': '1'}], - 'volume_type': 'vol_type_name', - 'image_id': 'c905cedb-7281-47e4-8a62-f26bc5fc4c77', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': '1'} - } - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - res = self.controller.create(req, body) - self.maxDiff = 4096 - self.assertEqual(res.obj, expected) - - def test_volume_create_with_image_id_and_snapshot_id(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) - self.stubs.Set(volume_api.API, "get_snapshot", stub_snapshot_get) - self.ext_mgr.extensions = {'os-image-create': 'fake'} - vol = {"size": '1', - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "nova", - "imageRef": 'c905cedb-7281-47e4-8a62-f26bc5fc4c77', - "snapshot_id": TEST_SNAPSHOT_UUID} - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, - req, - body) - - def test_volume_create_with_image_id_is_integer(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) - self.ext_mgr.extensions = {'os-image-create': 'fake'} - vol = {"size": '1', - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "nova", - "imageRef": 1234} - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, - req, - body) - - def test_volume_create_with_image_id_not_uuid_format(self): - self.stubs.Set(volume_api.API, "create", fakes.stub_volume_create) - self.ext_mgr.extensions = {'os-image-create': 'fake'} - vol = {"size": '1', - "display_name": "Volume Test Name", - "display_description": "Volume Test Desc", - "availability_zone": "nova", - "imageRef": '12345'} - body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v1/volumes') - self.assertRaises(webob.exc.HTTPBadRequest, - self.controller.create, - req, - body) - - def test_volume_list(self): - self.stubs.Set(volume_api.API, 'get_all', - fakes.stub_volume_get_all_by_project) - - req = fakes.HTTPRequest.blank('/v1/volumes') - res_dict = self.controller.index(req) - expected = {'volumes': [{'status': 'fakestatus', - 'display_description': 'displaydesc', - 'availability_zone': 'fakeaz', - 'display_name': 'displayname', - 'attachments': [{'device': '/', - 'server_id': 'fakeuuid', - 'id': '1', - 'volume_id': '1'}], - 'volume_type': 'vol_type_name', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': 1}]} - self.maxDiff = None - self.assertEqual(res_dict, expected) - - def test_volume_list_detail(self): - self.stubs.Set(volume_api.API, 'get_all', - fakes.stub_volume_get_all_by_project) - - req = fakes.HTTPRequest.blank('/v1/volumes/detail') - res_dict = self.controller.index(req) - expected = {'volumes': [{'status': 'fakestatus', - 'display_description': 'displaydesc', - 'availability_zone': 'fakeaz', - 'display_name': 'displayname', - 'attachments': [{'device': '/', - 'server_id': 'fakeuuid', - 'id': '1', - 'volume_id': '1'}], - 'volume_type': 'vol_type_name', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': 1}]} - self.assertEqual(res_dict, expected) - - def test_volume_show(self): - req = fakes.HTTPRequest.blank('/v1/volumes/1') - res_dict = self.controller.show(req, '1') - expected = {'volume': {'status': 'fakestatus', - 'display_description': 'displaydesc', - 'availability_zone': 'fakeaz', - 'display_name': 'displayname', - 'attachments': [{'device': '/', - 'server_id': 'fakeuuid', - 'id': '1', - 'volume_id': '1'}], - 'volume_type': 'vol_type_name', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': 1}} - self.assertEqual(res_dict, expected) - - def test_volume_show_no_attachments(self): - def stub_volume_get(self, context, volume_id): - return fakes.stub_volume(volume_id, attach_status='detached') - - self.stubs.Set(volume_api.API, 'get', stub_volume_get) - - req = fakes.HTTPRequest.blank('/v1/volumes/1') - res_dict = self.controller.show(req, '1') - expected = {'volume': {'status': 'fakestatus', - 'display_description': 'displaydesc', - 'availability_zone': 'fakeaz', - 'display_name': 'displayname', - 'attachments': [], - 'volume_type': 'vol_type_name', - 'snapshot_id': None, - 'metadata': {}, - 'id': '1', - 'created_at': datetime.datetime(1999, 1, 1, - 1, 1, 1), - 'size': 1}} - self.assertEqual(res_dict, expected) - - def test_volume_show_no_volume(self): - self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound) - - req = fakes.HTTPRequest.blank('/v1/volumes/1') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.show, - req, - 1) - - def test_volume_delete(self): - req = fakes.HTTPRequest.blank('/v1/volumes/1') - resp = self.controller.delete(req, 1) - self.assertEqual(resp.status_int, 202) - - def test_volume_delete_no_volume(self): - self.stubs.Set(volume_api.API, "get", fakes.stub_volume_get_notfound) - - req = fakes.HTTPRequest.blank('/v1/volumes/1') - self.assertRaises(webob.exc.HTTPNotFound, - self.controller.delete, - req, - 1) - - def test_admin_list_volumes_limited_to_project(self): - req = fakes.HTTPRequest.blank('/v1/fake/volumes', - use_admin_context=True) - res = self.controller.index(req) - - self.assertTrue('volumes' in res) - self.assertEqual(1, len(res['volumes'])) - - def test_admin_list_volumes_all_tenants(self): - req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1', - use_admin_context=True) - res = self.controller.index(req) - self.assertTrue('volumes' in res) - self.assertEqual(3, len(res['volumes'])) - - def test_all_tenants_non_admin_gets_all_tenants(self): - req = fakes.HTTPRequest.blank('/v1/fake/volumes?all_tenants=1') - res = self.controller.index(req) - self.assertTrue('volumes' in res) - self.assertEqual(1, len(res['volumes'])) - - def test_non_admin_get_by_project(self): - req = fakes.HTTPRequest.blank('/v1/fake/volumes') - res = self.controller.index(req) - self.assertTrue('volumes' in res) - self.assertEqual(1, len(res['volumes'])) - - -class VolumeSerializerTest(test.TestCase): - def _verify_volume_attachment(self, attach, tree): - for attr in ('id', 'volume_id', 'server_id', 'device'): - self.assertEqual(str(attach[attr]), tree.get(attr)) - - def _verify_volume(self, vol, tree): - self.assertEqual(tree.tag, 'volume') - - for attr in ('id', 'status', 'size', 'availability_zone', 'created_at', - 'display_name', 'display_description', 'volume_type', - 'snapshot_id'): - self.assertEqual(str(vol[attr]), tree.get(attr)) - - for child in tree: - self.assertTrue(child.tag in ('attachments', 'metadata')) - if child.tag == 'attachments': - self.assertEqual(1, len(child)) - self.assertEqual('attachment', child[0].tag) - self._verify_volume_attachment(vol['attachments'][0], child[0]) - elif child.tag == 'metadata': - not_seen = set(vol['metadata'].keys()) - for gr_child in child: - self.assertTrue(gr_child.get("key") in not_seen) - self.assertEqual(str(vol['metadata'][gr_child.get("key")]), - gr_child.text) - not_seen.remove(gr_child.get("key")) - self.assertEqual(0, len(not_seen)) - - def test_volume_show_create_serializer(self): - serializer = volumes.VolumeTemplate() - raw_volume = dict( - id='vol_id', - status='vol_status', - size=1024, - availability_zone='vol_availability', - created_at=timeutils.utcnow(), - attachments=[dict( - id='vol_id', - volume_id='vol_id', - server_id='instance_uuid', - device='/foo')], - display_name='vol_name', - display_description='vol_desc', - volume_type='vol_type', - snapshot_id='snap_id', - metadata=dict( - foo='bar', - baz='quux', - ), - ) - text = serializer.serialize(dict(volume=raw_volume)) - - print text - tree = etree.fromstring(text) - - self._verify_volume(raw_volume, tree) - - def test_volume_index_detail_serializer(self): - serializer = volumes.VolumesTemplate() - raw_volumes = [dict( - id='vol1_id', - status='vol1_status', - size=1024, - availability_zone='vol1_availability', - created_at=timeutils.utcnow(), - attachments=[dict( - id='vol1_id', - volume_id='vol1_id', - server_id='instance_uuid', - device='/foo1')], - display_name='vol1_name', - display_description='vol1_desc', - volume_type='vol1_type', - snapshot_id='snap1_id', - metadata=dict( - foo='vol1_foo', - bar='vol1_bar', - ), - ), - dict( - id='vol2_id', - status='vol2_status', - size=1024, - availability_zone='vol2_availability', - created_at=timeutils.utcnow(), - attachments=[dict( - id='vol2_id', - volume_id='vol2_id', - server_id='instance_uuid', - device='/foo2')], - display_name='vol2_name', - display_description='vol2_desc', - volume_type='vol2_type', - snapshot_id='snap2_id', - metadata=dict( - foo='vol2_foo', - bar='vol2_bar', - ), - )] - text = serializer.serialize(dict(volumes=raw_volumes)) - - print text - tree = etree.fromstring(text) - - self.assertEqual('volumes', tree.tag) - self.assertEqual(len(raw_volumes), len(tree)) - for idx, child in enumerate(tree): - self._verify_volume(raw_volumes[idx], child) - - -class TestVolumeCreateRequestXMLDeserializer(test.TestCase): - - def setUp(self): - super(TestVolumeCreateRequestXMLDeserializer, self).setUp() - self.deserializer = volumes.CreateDeserializer() - - def test_minimal_volume(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1"></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "size": "1", - }, - } - self.assertEquals(request['body'], expected) - - def test_display_name(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1" - display_name="Volume-xml"></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "size": "1", - "display_name": "Volume-xml", - }, - } - self.assertEquals(request['body'], expected) - - def test_display_description(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1" - display_name="Volume-xml" - display_description="description"></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "size": "1", - "display_name": "Volume-xml", - "display_description": "description", - }, - } - self.assertEquals(request['body'], expected) - - def test_volume_type(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1" - display_name="Volume-xml" - display_description="description" - volume_type="289da7f8-6440-407c-9fb4-7db01ec49164"></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "display_name": "Volume-xml", - "size": "1", - "display_name": "Volume-xml", - "display_description": "description", - "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", - }, - } - self.assertEquals(request['body'], expected) - - def test_availability_zone(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1" - display_name="Volume-xml" - display_description="description" - volume_type="289da7f8-6440-407c-9fb4-7db01ec49164" - availability_zone="us-east1"></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "size": "1", - "display_name": "Volume-xml", - "display_description": "description", - "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", - "availability_zone": "us-east1", - }, - } - self.assertEquals(request['body'], expected) - - def test_metadata(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - display_name="Volume-xml" - size="1"> - <metadata><meta key="Type">work</meta></metadata></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "display_name": "Volume-xml", - "size": "1", - "metadata": { - "Type": "work", - }, - }, - } - self.assertEquals(request['body'], expected) - - def test_full_volume(self): - self_request = """ -<volume xmlns="http://docs.openstack.org/compute/api/v1.1" - size="1" - display_name="Volume-xml" - display_description="description" - volume_type="289da7f8-6440-407c-9fb4-7db01ec49164" - availability_zone="us-east1"> - <metadata><meta key="Type">work</meta></metadata></volume>""" - request = self.deserializer.deserialize(self_request) - expected = { - "volume": { - "size": "1", - "display_name": "Volume-xml", - "display_description": "description", - "volume_type": "289da7f8-6440-407c-9fb4-7db01ec49164", - "availability_zone": "us-east1", - "metadata": { - "Type": "work", - }, - }, - } - self.maxDiff = None - self.assertEquals(request['body'], expected) - - -class VolumesUnprocessableEntityTestCase(test.TestCase): - - """ - Tests of places we throw 422 Unprocessable Entity from - """ - - def setUp(self): - super(VolumesUnprocessableEntityTestCase, self).setUp() - self.ext_mgr = extensions.ExtensionManager() - self.ext_mgr.extensions = {} - self.controller = volumes.VolumeController(self.ext_mgr) - - def _unprocessable_volume_create(self, body): - req = fakes.HTTPRequest.blank('/v2/fake/volumes') - req.method = 'POST' - - self.assertRaises(webob.exc.HTTPUnprocessableEntity, - self.controller.create, req, body) - - def test_create_no_body(self): - self._unprocessable_volume_create(body=None) - - def test_create_missing_volume(self): - body = {'foo': {'a': 'b'}} - self._unprocessable_volume_create(body=body) - - def test_create_malformed_entity(self): - body = {'volume': 'string'} - self._unprocessable_volume_create(body=body) diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index d989c488e..72886d1a6 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -59,7 +59,7 @@ from nova.tests.db.fakes import FakeModel from nova.tests import fake_network from nova.tests.image import fake as fake_image from nova import utils -import nova.volume +from nova.volume import cinder QUOTAS = quota.QUOTAS @@ -653,9 +653,9 @@ class ComputeTestCase(BaseTestCase): def fake_volume_get(self, context, volume_id): return {'id': volume_id} - self.stubs.Set(nova.volume.api.API, 'get', fake_volume_get) - self.stubs.Set(nova.volume.api.API, 'check_attach', fake_check_attach) - self.stubs.Set(nova.volume.api.API, 'reserve_volume', + self.stubs.Set(cinder.API, 'get', fake_volume_get) + self.stubs.Set(cinder.API, 'check_attach', fake_check_attach) + self.stubs.Set(cinder.API, 'reserve_volume', fake_reserve_volume) self.compute_api.attach_volume(self.context, instance, 1, @@ -2192,11 +2192,16 @@ class ComputeTestCase(BaseTestCase): topic = rpc.queue_get_for(c, FLAGS.compute_topic, instance['host']) # creating volume testdata - volume_id = db.volume_create(c, {'size': 1})['id'] + volume_id = 'fake' values = {'instance_uuid': inst_uuid, 'device_name': '/dev/vdc', 'delete_on_termination': False, 'volume_id': volume_id} db.block_device_mapping_create(c, values) + def fake_volume_get(self, context, volume_id): + return {'id': volume_id} + + self.stubs.Set(cinder.API, 'get', fake_volume_get) + # creating mocks self.mox.StubOutWithMock(rpc, 'call') @@ -2242,7 +2247,6 @@ class ComputeTestCase(BaseTestCase): for bdms in db.block_device_mapping_get_all_by_instance( c, inst_uuid): db.block_device_mapping_destroy(c, bdms['id']) - db.volume_destroy(c, volume_id) db.instance_destroy(c, inst_uuid) def test_live_migration_works_correctly(self): @@ -2291,7 +2295,6 @@ class ComputeTestCase(BaseTestCase): db.instance_update(c, inst_uuid, {'task_state': task_states.MIGRATING, 'power_state': power_state.PAUSED}) - v_ref = db.volume_create(c, {'size': 1, 'instance_id': inst_id}) fix_addr = db.fixed_ip_create(c, {'address': '1.1.1.1', 'instance_uuid': inst_ref['uuid']}) fix_ref = db.fixed_ip_get_by_address(c, fix_addr) @@ -2326,7 +2329,6 @@ class ComputeTestCase(BaseTestCase): # cleanup db.instance_destroy(c, inst_uuid) - db.volume_destroy(c, v_ref['id']) db.floating_ip_destroy(c, flo_addr) def test_run_kill_vm(self): @@ -3376,8 +3378,7 @@ class ComputeAPITestCase(BaseTestCase): instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) - volume_id = db.volume_create(context.get_admin_context(), - {'size': 1})['id'] + volume_id = 'fake' volume = {'instance_uuid': instance['uuid'], 'device_name': '/dev/vdc', 'delete_on_termination': False, @@ -3402,8 +3403,7 @@ class ComputeAPITestCase(BaseTestCase): instance = jsonutils.to_primitive(self._create_fake_instance()) self.compute.run_instance(self.context, instance=instance) - volume_id = db.volume_create(context.get_admin_context(), - {'size': 1})['id'] + volume_id = 'fake' volume = {'instance_uuid': instance['uuid'], 'device_name': '/dev/vdc', 'delete_on_termination': False, @@ -4580,9 +4580,9 @@ class ComputeAPITestCase(BaseTestCase): def fake_rpc_attach_volume(self, context, **kwargs): called['fake_rpc_attach_volume'] = True - self.stubs.Set(nova.volume.api.API, 'get', fake_volume_get) - self.stubs.Set(nova.volume.api.API, 'check_attach', fake_check_attach) - self.stubs.Set(nova.volume.api.API, 'reserve_volume', + self.stubs.Set(cinder.API, 'get', fake_volume_get) + self.stubs.Set(cinder.API, 'check_attach', fake_check_attach) + self.stubs.Set(cinder.API, 'reserve_volume', fake_reserve_volume) self.stubs.Set(compute_rpcapi.ComputeAPI, 'attach_volume', fake_rpc_attach_volume) @@ -4611,9 +4611,9 @@ class ComputeAPITestCase(BaseTestCase): def fake_rpc_attach_volume(self, context, **kwargs): called['fake_rpc_attach_volume'] = True - self.stubs.Set(nova.volume.api.API, 'get', fake_volume_get) - self.stubs.Set(nova.volume.api.API, 'check_attach', fake_check_attach) - self.stubs.Set(nova.volume.api.API, 'reserve_volume', + self.stubs.Set(cinder.API, 'get', fake_volume_get) + self.stubs.Set(cinder.API, 'check_attach', fake_check_attach) + self.stubs.Set(cinder.API, 'reserve_volume', fake_reserve_volume) self.stubs.Set(compute_rpcapi.ComputeAPI, 'attach_volume', fake_rpc_attach_volume) @@ -4623,37 +4623,35 @@ class ComputeAPITestCase(BaseTestCase): admin = context.get_admin_context() instance = self._create_fake_instance() - # Create a volume and attach it to our instance - volume_id = db.volume_create(admin, {'size': 1})['id'] + volume_id = 'fake' values = {'instance_uuid': instance['uuid'], 'device_name': '/dev/vdc', 'delete_on_termination': False, 'volume_id': volume_id, } db.block_device_mapping_create(admin, values) - db.volume_attached(admin, volume_id, instance["uuid"], - "/dev/vdc") + + def fake_volume_get(self, context, volume): + return {'id': volume_id} + self.stubs.Set(cinder.API, "get", fake_volume_get) # Stub out and record whether it gets detached result = {"detached": False} def fake_detach(self, context, volume): result["detached"] = volume["id"] == volume_id - self.stubs.Set(nova.volume.api.API, "detach", fake_detach) + self.stubs.Set(cinder.API, "detach", fake_detach) + + def fake_terminate_connection(self, context, volume, connector): + return {} + self.stubs.Set(cinder.API, "terminate_connection", + fake_terminate_connection) # Kill the instance and check that it was detached self.compute.terminate_instance(admin, instance=instance) self.assertTrue(result["detached"]) def test_inject_network_info(self): - instance = self._create_fake_instance() - self.compute_api.attach_volume(self.context, instance, 1, device=None) - self.assertTrue(called.get('fake_check_attach')) - self.assertTrue(called.get('fake_reserve_volume')) - self.assertTrue(called.get('fake_reserve_volume')) - self.assertTrue(called.get('fake_rpc_attach_volume')) - - def test_inject_network_info(self): instance = self._create_fake_instance(params={'host': FLAGS.host}) self.compute.run_instance(self.context, instance=jsonutils.to_primitive(instance)) diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py index 32f34b3e7..0b50d45e2 100644 --- a/nova/tests/fake_flags.py +++ b/nova/tests/fake_flags.py @@ -26,7 +26,6 @@ flags.DECLARE('iscsi_num_targets', 'nova.volume.driver') flags.DECLARE('network_size', 'nova.network.manager') flags.DECLARE('num_networks', 'nova.network.manager') flags.DECLARE('policy_file', 'nova.policy') -flags.DECLARE('volume_driver', 'nova.volume.manager') def set_defaults(conf): @@ -44,7 +43,6 @@ def set_defaults(conf): conf.set_default('sqlite_synchronous', False) conf.set_default('use_ipv6', True) conf.set_default('verbose', True) - conf.set_default('volume_driver', 'nova.volume.driver.FakeISCSIDriver') conf.set_default('api_paste_config', '$state_path/etc/nova/api-paste.ini') conf.set_default('rpc_response_timeout', 5) conf.set_default('rpc_cast_timeout', 5) diff --git a/nova/tests/fake_volume.py b/nova/tests/fake_volume.py index 37aaa83b4..54fd85fe5 100644 --- a/nova/tests/fake_volume.py +++ b/nova/tests/fake_volume.py @@ -27,21 +27,21 @@ class fake_volume(): instance_uuid = '4a3cd441-b9c2-11e1-afa6-0800200c9a66' def __init__(self, size, name, - description, id, snapshot, + description, volume_id, snapshot, volume_type, metadata, availability_zone): snapshot_id = None if snapshot is not None: snapshot_id = snapshot['id'] - if id is None: - id = str(utils.gen_uuid()) + if volume_id is None: + volume_id = str(utils.gen_uuid()) self.vol = { 'created_at': timeutils.utcnow(), 'deleted_at': None, 'updated_at': timeutils.utcnow(), 'uuid': 'WTF', 'deleted': False, - 'id': id, + 'id': volume_id, 'user_id': self.user_uuid, 'project_id': 'fake-project-id', 'snapshot_id': snapshot_id, @@ -133,14 +133,17 @@ class API(object): return v.vol def create_with_kwargs(self, context, **kwargs): + volume_id = kwargs.get('volume_id', None) + print volume_id v = fake_volume(kwargs['size'], kwargs['name'], kwargs['description'], - str(kwargs.get('volume_id', None)), + str(volume_id), None, None, None, None) + print v.vol['id'] if kwargs.get('status', None) is not None: v.vol['status'] = kwargs['status'] if kwargs['host'] is not None: @@ -175,6 +178,7 @@ class API(object): def check_attach(self, context, volume): if volume['status'] != 'available': msg = _("status must be available") + msg = "%s" % volume raise exception.InvalidVolume(reason=msg) if volume['attach_status'] == 'attached': msg = _("already attached") diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index f566a5020..9581a0e7e 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -329,14 +329,6 @@ "updated": "%(timestamp)s" }, { - "alias": "os-volume-types", - "description": "%(text)s", - "links": [], - "name": "VolumeTypes", - "namespace": "http://docs.openstack.org/compute/ext/volume_types/api/v1.1", - "updated": "%(timestamp)s" - }, - { "alias": "os-volumes", "description": "%(text)s", "links": [], diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 20e650d7c..e8246aad8 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -123,9 +123,6 @@ <extension alias="os-virtual-interfaces" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/virtual_interfaces/api/v1.1" name="VirtualInterfaces"> <description>%(text)s</description> </extension> - <extension alias="os-volume-types" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/volume_types/api/v1.1" name="VolumeTypes"> - <description>%(text)s</description> - </extension> <extension alias="os-volumes" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/volumes/api/v1.1" name="Volumes"> <description>%(text)s</description> </extension> diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index d81013920..f3bd944da 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -75,7 +75,6 @@ class _IntegratedTestBase(test.TestCase): # set up services self.compute = self.start_service('compute') self.scheduler = self.start_service('cert') - self.volume = self.start_service('volume') self.network = self.start_service('network') self.scheduler = self.start_service('scheduler') @@ -101,13 +100,11 @@ class _IntegratedTestBase(test.TestCase): # Ensure tests only listen on localhost f['ec2_listen'] = '127.0.0.1' f['osapi_compute_listen'] = '127.0.0.1' - f['osapi_volume_listen'] = '127.0.0.1' f['metadata_listen'] = '127.0.0.1' # Auto-assign ports to allow concurrent tests f['ec2_listen_port'] = 0 f['osapi_compute_listen_port'] = 0 - f['osapi_volume_listen_port'] = 0 f['metadata_listen_port'] = 0 f['fake_network'] = True diff --git a/nova/tests/integrated/test_volumes.py b/nova/tests/integrated/test_volumes.py deleted file mode 100644 index fe70c3ce8..000000000 --- a/nova/tests/integrated/test_volumes.py +++ /dev/null @@ -1,181 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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 time -import unittest - -from nova.openstack.common.log import logging -from nova import service -from nova.tests.integrated.api import client -from nova.tests.integrated import integrated_helpers -from nova.volume import driver - - -LOG = logging.getLogger(__name__) - - -class VolumesTest(integrated_helpers._IntegratedTestBase): - def setUp(self): - super(VolumesTest, self).setUp() - driver.LoggingVolumeDriver.clear_logs() - - def _start_api_service(self): - self.osapi = service.WSGIService("osapi_volume") - self.osapi.start() - self.auth_url = 'http://%s:%s/v1' % (self.osapi.host, self.osapi.port) - LOG.warn(self.auth_url) - - def _get_flags(self): - f = super(VolumesTest, self)._get_flags() - f['use_local_volumes'] = False # Avoids calling local_path - f['volume_driver'] = 'nova.volume.driver.LoggingVolumeDriver' - return f - - def test_get_volumes_summary(self): - """Simple check that listing volumes works.""" - volumes = self.api.get_volumes(False) - for volume in volumes: - LOG.debug("volume: %s" % volume) - - def test_get_volumes(self): - """Simple check that listing volumes works.""" - volumes = self.api.get_volumes() - for volume in volumes: - LOG.debug("volume: %s" % volume) - - def _poll_while(self, volume_id, continue_states, max_retries=5): - """Poll (briefly) while the state is in continue_states.""" - retries = 0 - while True: - try: - found_volume = self.api.get_volume(volume_id) - except client.OpenStackApiNotFoundException: - found_volume = None - LOG.debug("Got 404, proceeding") - break - - LOG.debug("Found %s" % found_volume) - - self.assertEqual(volume_id, found_volume['id']) - - if not found_volume['status'] in continue_states: - break - - time.sleep(1) - retries = retries + 1 - if retries > max_retries: - break - return found_volume - - def test_create_and_delete_volume(self): - """Creates and deletes a volume.""" - - # Create volume - created_volume = self.api.post_volume({'volume': {'size': 1}}) - LOG.debug("created_volume: %s" % created_volume) - self.assertTrue(created_volume['id']) - created_volume_id = created_volume['id'] - - # Check it's there - found_volume = self.api.get_volume(created_volume_id) - self.assertEqual(created_volume_id, found_volume['id']) - - # It should also be in the all-volume list - volumes = self.api.get_volumes() - volume_names = [volume['id'] for volume in volumes] - self.assertTrue(created_volume_id in volume_names) - - # Wait (briefly) for creation. Delay is due to the 'message queue' - found_volume = self._poll_while(created_volume_id, ['creating']) - - # It should be available... - self.assertEqual('available', found_volume['status']) - - # Delete the volume - self.api.delete_volume(created_volume_id) - - # Wait (briefly) for deletion. Delay is due to the 'message queue' - found_volume = self._poll_while(created_volume_id, ['deleting']) - - # Should be gone - self.assertFalse(found_volume) - - LOG.debug("Logs: %s" % driver.LoggingVolumeDriver.all_logs()) - - create_actions = driver.LoggingVolumeDriver.logs_like( - 'create_volume', - id=created_volume_id) - LOG.debug("Create_Actions: %s" % create_actions) - - self.assertEquals(1, len(create_actions)) - create_action = create_actions[0] - self.assertEquals(create_action['id'], created_volume_id) - self.assertEquals(create_action['availability_zone'], 'nova') - self.assertEquals(create_action['size'], 1) - - export_actions = driver.LoggingVolumeDriver.logs_like( - 'create_export', - id=created_volume_id) - self.assertEquals(1, len(export_actions)) - export_action = export_actions[0] - self.assertEquals(export_action['id'], created_volume_id) - self.assertEquals(export_action['availability_zone'], 'nova') - - delete_actions = driver.LoggingVolumeDriver.logs_like( - 'delete_volume', - id=created_volume_id) - self.assertEquals(1, len(delete_actions)) - delete_action = export_actions[0] - self.assertEquals(delete_action['id'], created_volume_id) - - def test_create_volume_with_metadata(self): - """Creates a volume with metadata.""" - - # Create volume - metadata = {'key1': 'value1', - 'key2': 'value2'} - created_volume = self.api.post_volume( - {'volume': {'size': 1, - 'metadata': metadata}}) - LOG.debug("created_volume: %s" % created_volume) - self.assertTrue(created_volume['id']) - created_volume_id = created_volume['id'] - - # Check it's there and metadata present - found_volume = self.api.get_volume(created_volume_id) - self.assertEqual(created_volume_id, found_volume['id']) - self.assertEqual(metadata, found_volume['metadata']) - - def test_create_volume_in_availability_zone(self): - """Creates a volume in availability_zone.""" - - # Create volume - availability_zone = 'zone1:host1' - created_volume = self.api.post_volume( - {'volume': {'size': 1, - 'availability_zone': availability_zone}}) - LOG.debug("created_volume: %s" % created_volume) - self.assertTrue(created_volume['id']) - created_volume_id = created_volume['id'] - - # Check it's there and availability zone present - found_volume = self.api.get_volume(created_volume_id) - self.assertEqual(created_volume_id, found_volume['id']) - self.assertEqual(availability_zone, found_volume['availability_zone']) - -if __name__ == "__main__": - unittest.main() diff --git a/nova/tests/scheduler/test_scheduler.py b/nova/tests/scheduler/test_scheduler.py index c6b2d7dd3..6a0e93b7d 100644 --- a/nova/tests/scheduler/test_scheduler.py +++ b/nova/tests/scheduler/test_scheduler.py @@ -175,18 +175,6 @@ class SchedulerManagerTestCase(test.TestCase): self.manager.run_instance(self.context, request_spec, None, None, None, None, {}) - def test_create_volume_no_valid_host_puts_volume_in_error(self): - self._mox_schedule_method_helper('schedule_create_volume') - self.mox.StubOutWithMock(db, 'volume_update') - - self.manager.driver.schedule_create_volume(self.context, '1', '2', - None).AndRaise(exception.NoValidHost(reason='')) - db.volume_update(self.context, '1', {'status': 'error'}) - - self.mox.ReplayAll() - self.assertRaises(exception.NoValidHost, self.manager.create_volume, - self.context, '1', '2') - def test_prep_resize_no_valid_host_back_in_active_state(self): fake_instance_uuid = 'fake-instance-id' inst = {"vm_state": "", "task_state": ""} @@ -305,13 +293,10 @@ class SchedulerTestCase(test.TestCase): self.assertEqual(result, ['host2']) def _live_migration_instance(self): - volume1 = {'id': 31338} - volume2 = {'id': 31339} return {'id': 31337, 'uuid': 'fake_uuid', 'name': 'fake-instance', 'host': 'fake_host1', - 'volumes': [volume1, volume2], 'power_state': power_state.RUNNING, 'memory_mb': 1024, 'root_gb': 1024, @@ -656,48 +641,6 @@ class SchedulerDriverModuleTestCase(test.TestCase): super(SchedulerDriverModuleTestCase, self).setUp() self.context = context.RequestContext('fake_user', 'fake_project') - def test_cast_to_volume_host_update_db_with_volume_id(self): - host = 'fake_host1' - method = 'fake_method' - fake_kwargs = {'volume_id': 31337, - 'extra_arg': 'meow'} - queue = 'fake_queue' - - self.mox.StubOutWithMock(timeutils, 'utcnow') - self.mox.StubOutWithMock(db, 'volume_update') - self.mox.StubOutWithMock(rpc, 'queue_get_for') - self.mox.StubOutWithMock(rpc, 'cast') - - timeutils.utcnow().AndReturn('fake-now') - db.volume_update(self.context, 31337, - {'host': host, 'scheduled_at': 'fake-now'}) - rpc.queue_get_for(self.context, 'volume', host).AndReturn(queue) - rpc.cast(self.context, queue, - {'method': method, - 'args': fake_kwargs}) - - self.mox.ReplayAll() - driver.cast_to_volume_host(self.context, host, method, - **fake_kwargs) - - def test_cast_to_volume_host_update_db_without_volume_id(self): - host = 'fake_host1' - method = 'fake_method' - fake_kwargs = {'extra_arg': 'meow'} - queue = 'fake_queue' - - self.mox.StubOutWithMock(rpc, 'queue_get_for') - self.mox.StubOutWithMock(rpc, 'cast') - - rpc.queue_get_for(self.context, 'volume', host).AndReturn(queue) - rpc.cast(self.context, queue, - {'method': method, - 'args': fake_kwargs}) - - self.mox.ReplayAll() - driver.cast_to_volume_host(self.context, host, method, - **fake_kwargs) - def test_cast_to_compute_host_update_db_with_instance_uuid(self): host = 'fake_host1' method = 'fake_method' @@ -753,19 +696,6 @@ class SchedulerDriverModuleTestCase(test.TestCase): driver.cast_to_host(self.context, 'compute', host, method, **fake_kwargs) - def test_cast_to_host_volume_topic(self): - host = 'fake_host1' - method = 'fake_method' - fake_kwargs = {'extra_arg': 'meow'} - - self.mox.StubOutWithMock(driver, 'cast_to_volume_host') - driver.cast_to_volume_host(self.context, host, method, - **fake_kwargs) - - self.mox.ReplayAll() - driver.cast_to_host(self.context, 'volume', host, method, - **fake_kwargs) - def test_cast_to_host_unknown_topic(self): host = 'fake_host1' method = 'fake_method' diff --git a/nova/tests/test_cinder.py b/nova/tests/test_cinder.py index 18532d642..3302aedb8 100644 --- a/nova/tests/test_cinder.py +++ b/nova/tests/test_cinder.py @@ -118,9 +118,6 @@ class CinderTestCase(test.TestCase): self.fake_client_factory = FakeClientFactory() self.stubs.Set(cinder.cinder_client, "Client", self.fake_client_factory) - self.flags( - volume_api_class='nova.volume.cinder.API', - ) self.api = cinder.API() catalog = [{ "type": "volume", diff --git a/nova/tests/test_iscsi.py b/nova/tests/test_iscsi.py deleted file mode 100644 index 09a6e9e89..000000000 --- a/nova/tests/test_iscsi.py +++ /dev/null @@ -1,121 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Red Hat, 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 os.path -import shutil -import string -import tempfile - -from nova import test -from nova.volume import iscsi - - -class TargetAdminTestCase(object): - - def setUp(self): - self.cmds = [] - - self.tid = 1 - self.target_name = 'iqn.2011-09.org.foo.bar:blaa' - self.lun = 10 - self.path = '/foo' - self.vol_id = 'blaa' - - self.script_template = None - self.stubs.Set(os.path, 'isfile', lambda _: True) - self.stubs.Set(os, 'unlink', lambda _: '') - self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) - - def fake_get_target(obj, iqn): - return 1 - - def get_script_params(self): - return {'tid': self.tid, - 'target_name': self.target_name, - 'lun': self.lun, - 'path': self.path} - - def get_script(self): - return self.script_template % self.get_script_params() - - def fake_execute(self, *cmd, **kwargs): - self.cmds.append(string.join(cmd)) - return "", None - - def clear_cmds(self): - cmds = [] - - def verify_cmds(self, cmds): - self.assertEqual(len(cmds), len(self.cmds)) - for a, b in zip(cmds, self.cmds): - self.assertEqual(a, b) - - def verify(self): - script = self.get_script() - cmds = [] - for line in script.split('\n'): - if not line.strip(): - continue - cmds.append(line) - self.verify_cmds(cmds) - - def run_commands(self): - tgtadm = iscsi.get_target_admin() - tgtadm.set_execute(self.fake_execute) - tgtadm.create_iscsi_target(self.target_name, self.tid, - self.lun, self.path) - tgtadm.show_target(self.tid, iqn=self.target_name) - tgtadm.remove_iscsi_target(self.tid, self.lun, self.vol_id) - - def test_target_admin(self): - self.clear_cmds() - self.run_commands() - self.verify() - - -class TgtAdmTestCase(test.TestCase, TargetAdminTestCase): - - def setUp(self): - super(TgtAdmTestCase, self).setUp() - TargetAdminTestCase.setUp(self) - self.persist_tempdir = tempfile.mkdtemp() - self.flags(iscsi_helper='tgtadm') - self.flags(volumes_dir=self.persist_tempdir) - self.script_template = "\n".join([ - 'tgt-admin --update iqn.2011-09.org.foo.bar:blaa', - 'tgt-admin --delete iqn.2010-10.org.openstack:volume-blaa']) - - def tearDown(self): - try: - shutil.rmtree(self.persist_tempdir) - except OSError: - pass - super(TgtAdmTestCase, self).tearDown() - - -class IetAdmTestCase(test.TestCase, TargetAdminTestCase): - - def setUp(self): - super(IetAdmTestCase, self).setUp() - TargetAdminTestCase.setUp(self) - self.flags(iscsi_helper='ietadm') - self.script_template = "\n".join([ - 'ietadm --op new --tid=%(tid)s --params Name=%(target_name)s', - 'ietadm --op new --tid=%(tid)s --lun=%(lun)s ' - '--params Path=%(path)s,Type=fileio', - 'ietadm --op show --tid=%(tid)s', - 'ietadm --op delete --tid=%(tid)s --lun=%(lun)s', - 'ietadm --op delete --tid=%(tid)s']) diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index c84440e58..f8bc3c339 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -155,7 +155,6 @@ class LibvirtVolumeTestCase(test.TestCase): } def test_libvirt_volume_driver_serial(self): - vol_driver = volume_driver.VolumeDriver() libvirt_driver = volume.LibvirtVolumeDriver(self.fake_conn) name = 'volume-00000001' vol = {'id': 1, 'name': name} diff --git a/nova/tests/test_netapp.py b/nova/tests/test_netapp.py deleted file mode 100644 index 79a8526ee..000000000 --- a/nova/tests/test_netapp.py +++ /dev/null @@ -1,1380 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# All Rights Reserved. -# -# 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. -""" -Tests for NetApp volume driver - -""" - -import BaseHTTPServer -import httplib -import StringIO - -from lxml import etree - -from nova.openstack.common import log as logging -from nova import test -from nova.volume import netapp - - -LOG = logging.getLogger(__name__) - - -WSDL_HEADER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" - xmlns:na="http://www.netapp.com/management/v1" - xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" - xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="NetAppDfm" - targetNamespace="http://www.netapp.com/management/v1">""" - -WSDL_TYPES = """<types> -<xsd:schema attributeFormDefault="unqualified" elementFormDefault="qualified" - targetNamespace="http://www.netapp.com/management/v1"> -<xsd:element name="ApiProxy"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Request" type="na:Request"/> - <xsd:element name="Target" type="xsd:string"/> - <xsd:element minOccurs="0" name="Timeout" type="xsd:integer"/> - <xsd:element minOccurs="0" name="Username" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="ApiProxyResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Response" type="na:Response"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditBegin"> - <xsd:complexType> - <xsd:all> - <xsd:element name="DatasetNameOrId" type="na:ObjNameOrId"/> - <xsd:element minOccurs="0" name="Force" type="xsd:boolean"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditBeginResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="EditLockId" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditCommit"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="AssumeConfirmation" - type="xsd:boolean"/> - <xsd:element name="EditLockId" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditCommitResult"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="IsProvisioningFailure" - type="xsd:boolean"/> - <xsd:element minOccurs="0" name="JobIds" type="na:ArrayOfJobInfo"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditRollback"> - <xsd:complexType> - <xsd:all> - <xsd:element name="EditLockId" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetEditRollbackResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DatasetListInfoIterEnd"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetListInfoIterEndResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DatasetListInfoIterNext"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Maximum" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetListInfoIterNextResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Datasets" type="na:ArrayOfDatasetInfo"/> - <xsd:element name="Records" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetListInfoIterStart"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="ObjectNameOrId" - type="na:ObjNameOrId"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetListInfoIterStartResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Records" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterEnd"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterEndResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterNext"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Maximum" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterNextResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="DatasetMembers" - type="na:ArrayOfDatasetMemberInfo"/> - <xsd:element name="Records" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterStart"> - <xsd:complexType> - <xsd:all> - <xsd:element name="DatasetNameOrId" type="na:ObjNameOrId"/> - <xsd:element minOccurs="0" name="IncludeExportsInfo" - type="xsd:boolean"/> - <xsd:element minOccurs="0" name="IncludeIndirect" - type="xsd:boolean"/> - <xsd:element minOccurs="0" name="MemberType" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetMemberListInfoIterStartResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Records" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetProvisionMember"> - <xsd:complexType> - <xsd:all> - <xsd:element name="EditLockId" type="xsd:integer"/> - <xsd:element name="ProvisionMemberRequestInfo" - type="na:ProvisionMemberRequestInfo"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetProvisionMemberResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DatasetRemoveMember"> - <xsd:complexType> - <xsd:all> - <xsd:element name="DatasetMemberParameters" - type="na:ArrayOfDatasetMemberParameter"/> - <xsd:element minOccurs="0" name="Destroy" type="xsd:boolean"/> - <xsd:element name="EditLockId" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DatasetRemoveMemberResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterEnd"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterEndResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterNext"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Maximum" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterNextResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="ProgressEvents" - type="na:ArrayOfDpJobProgressEventInfo"/> - <xsd:element name="Records" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterStart"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="JobId" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DpJobProgressEventListIterStartResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Records" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DfmAbout"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="IncludeDirectorySizeInfo" - type="xsd:boolean"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="DfmAboutResult"> - <xsd:complexType> - <xsd:all/> - </xsd:complexType> -</xsd:element> -<xsd:element name="HostListInfoIterEnd"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="HostListInfoIterEndResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="HostListInfoIterNext"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Maximum" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="HostListInfoIterNextResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Hosts" type="na:ArrayOfHostInfo"/> - <xsd:element name="Records" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="HostListInfoIterStart"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="ObjectNameOrId" - type="na:ObjNameOrId"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="HostListInfoIterStartResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Records" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="LunListInfoIterEnd"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="LunListInfoIterEndResult"> - <xsd:complexType/> -</xsd:element> -<xsd:element name="LunListInfoIterNext"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Maximum" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="LunListInfoIterNextResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Luns" type="na:ArrayOfLunInfo"/> - <xsd:element name="Records" type="xsd:integer"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="LunListInfoIterStart"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="ObjectNameOrId" - type="na:ObjNameOrId"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="LunListInfoIterStartResult"> - <xsd:complexType> - <xsd:all> - <xsd:element name="Records" type="xsd:integer"/> - <xsd:element name="Tag" type="xsd:string"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="StorageServiceDatasetProvision"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="AssumeConfirmation" - type="xsd:boolean"/> - <xsd:element name="DatasetName" type="na:ObjName"/> - <xsd:element name="StorageServiceNameOrId" type="na:ObjNameOrId"/> - <xsd:element minOccurs="0" name="StorageSetDetails" - type="na:ArrayOfStorageSetInfo"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:element name="StorageServiceDatasetProvisionResult"> - <xsd:complexType> - <xsd:all> - <xsd:element minOccurs="0" name="ConformanceAlerts" - type="na:ArrayOfConformanceAlert"/> - <xsd:element name="DatasetId" type="na:ObjId"/> - <xsd:element minOccurs="0" name="DryRunResults" - type="na:ArrayOfDryRunResult"/> - </xsd:all> - </xsd:complexType> -</xsd:element> -<xsd:complexType name="ArrayOfDatasetInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="DatasetInfo" - type="na:DatasetInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfDatasetMemberInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="DatasetMemberInfo" - type="na:DatasetMemberInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfDatasetMemberParameter"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="DatasetMemberParameter" - type="na:DatasetMemberParameter"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfDfmMetadataField"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="DfmMetadataField" - type="na:DfmMetadataField"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfDpJobProgressEventInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="DpJobProgressEventInfo" - type="na:DpJobProgressEventInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfHostInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="HostInfo" type="na:HostInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfJobInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="JobInfo" type="na:JobInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfLunInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="LunInfo" type="na:LunInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="ArrayOfStorageSetInfo"> - <xsd:sequence> - <xsd:element maxOccurs="unbounded" name="StorageSetInfo" - type="na:StorageSetInfo"/> - </xsd:sequence> -</xsd:complexType> -<xsd:complexType name="DatasetExportInfo"> - <xsd:all> - <xsd:element minOccurs="0" name="DatasetExportProtocol" - type="na:DatasetExportProtocol"/> - <xsd:element minOccurs="0" name="DatasetLunMappingInfo" - type="na:DatasetLunMappingInfo"/> - </xsd:all> -</xsd:complexType> -<xsd:simpleType name="DatasetExportProtocol"> - <xsd:restriction base="xsd:string"/> -</xsd:simpleType> -<xsd:complexType name="DatasetInfo"> - <xsd:all> - <xsd:element name="DatasetId" type="na:ObjId"/> - <xsd:element name="DatasetName" type="na:ObjName"/> - <xsd:element name="DatasetMetadata" type="na:ArrayOfDfmMetadataField"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="DatasetLunMappingInfo"> - <xsd:all> - <xsd:element name="IgroupOsType" type="xsd:string"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="DatasetMemberInfo"> - <xsd:all> - <xsd:element name="MemberId" type="na:ObjId"/> - <xsd:element name="MemberName" type="na:ObjName"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="DatasetMemberParameter"> - <xsd:all> - <xsd:element name="ObjectNameOrId" type="na:ObjNameOrId"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="DfmMetadataField"> - <xsd:all> - <xsd:element name="FieldName" type="xsd:string"/> - <xsd:element name="FieldValue" type="xsd:string"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="DpJobProgressEventInfo"> - <xsd:all> - <xsd:element name="EventStatus" type="na:ObjStatus"/> - <xsd:element name="EventType" type="xsd:string"/> - <xsd:element minOccurs="0" name="ProgressLunInfo" - type="na:ProgressLunInfo"/> - </xsd:all> -</xsd:complexType> -<xsd:simpleType name="DpPolicyNodeName"> - <xsd:restriction base="xsd:string"/> -</xsd:simpleType> -<xsd:simpleType name="HostId"> - <xsd:restriction base="xsd:integer"/> -</xsd:simpleType> -<xsd:complexType name="HostInfo"> - <xsd:all> - <xsd:element name="HostAddress" type="xsd:string"/> - <xsd:element name="HostId" type="na:HostId"/> - <xsd:element name="HostName" type="xsd:string"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="JobInfo"> - <xsd:all> - <xsd:element name="JobId" type="xsd:integer"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="LunInfo"> - <xsd:all> - <xsd:element name="HostId" type="na:ObjId"/> - <xsd:element name="LunPath" type="na:ObjName"/> - </xsd:all> -</xsd:complexType> -<xsd:simpleType name="ObjId"> - <xsd:restriction base="xsd:integer"/> -</xsd:simpleType> -<xsd:simpleType name="ObjName"> - <xsd:restriction base="xsd:string"/> -</xsd:simpleType> -<xsd:simpleType name="ObjNameOrId"> - <xsd:restriction base="xsd:string"/> -</xsd:simpleType> -<xsd:simpleType name="ObjStatus"> - <xsd:restriction base="xsd:string"/> -</xsd:simpleType> -<xsd:complexType name="ProgressLunInfo"> - <xsd:all> - <xsd:element name="LunPathId" type="na:ObjId"/> - <xsd:element name="LunName" type="na:ObjName"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="ProvisionMemberRequestInfo"> - <xsd:all> - <xsd:element minOccurs="0" name="Description" type="xsd:string"/> - <xsd:element minOccurs="0" name="MaximumSnapshotSpace" - type="xsd:integer"/> - <xsd:element name="Name" type="xsd:string"/> - <xsd:element name="Size" type="xsd:integer"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="Request"> - <xsd:all> - <xsd:element minOccurs="0" name="Args"> - <xsd:complexType> - <xsd:sequence> - <xsd:any maxOccurs="unbounded" minOccurs="0"/> - </xsd:sequence> - </xsd:complexType> - </xsd:element> - <xsd:element name="Name" type="xsd:string"> - </xsd:element> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="Response"> - <xsd:all> - <xsd:element minOccurs="0" name="Errno" type="xsd:integer"/> - <xsd:element minOccurs="0" name="Reason" type="xsd:string"/> - <xsd:element minOccurs="0" name="Results"> - <xsd:complexType> - <xsd:sequence> - <xsd:any maxOccurs="unbounded" minOccurs="0"/> - </xsd:sequence> - </xsd:complexType> - </xsd:element> - <xsd:element name="Status" type="xsd:string"/> - </xsd:all> -</xsd:complexType> -<xsd:complexType name="StorageSetInfo"> - <xsd:all> - <xsd:element minOccurs="0" name="DatasetExportInfo" - type="na:DatasetExportInfo"/> - <xsd:element minOccurs="0" name="DpNodeName" - type="na:DpPolicyNodeName"/> - <xsd:element minOccurs="0" name="ServerNameOrId" - type="na:ObjNameOrId"/> - </xsd:all> -</xsd:complexType> -</xsd:schema></types>""" - -WSDL_TRAILER = """<service name="DfmService"> -<port binding="na:DfmBinding" name="DfmPort"> -<soap:address location="https://HOST_NAME:8488/apis/soap/v1"/> -</port></service></definitions>""" - -RESPONSE_PREFIX = """<?xml version="1.0" encoding="UTF-8"?> -<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" - xmlns:na="http://www.netapp.com/management/v1"><env:Header/><env:Body>""" - -RESPONSE_SUFFIX = """</env:Body></env:Envelope>""" - -APIS = ['ApiProxy', 'DatasetListInfoIterStart', 'DatasetListInfoIterNext', - 'DatasetListInfoIterEnd', 'DatasetEditBegin', 'DatasetEditCommit', - 'DatasetProvisionMember', 'DatasetRemoveMember', 'DfmAbout', - 'DpJobProgressEventListIterStart', 'DpJobProgressEventListIterNext', - 'DpJobProgressEventListIterEnd', 'DatasetMemberListInfoIterStart', - 'DatasetMemberListInfoIterNext', 'DatasetMemberListInfoIterEnd', - 'HostListInfoIterStart', 'HostListInfoIterNext', 'HostListInfoIterEnd', - 'LunListInfoIterStart', 'LunListInfoIterNext', 'LunListInfoIterEnd', - 'StorageServiceDatasetProvision'] - -iter_count = 0 -iter_table = {} - - -class FakeDfmServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """HTTP handler that fakes enough stuff to allow the driver to run""" - - def do_GET(s): - """Respond to a GET request.""" - if '/dfm.wsdl' != s.path: - s.send_response(404) - s.end_headers - return - s.send_response(200) - s.send_header("Content-Type", "application/wsdl+xml") - s.end_headers() - out = s.wfile - out.write(WSDL_HEADER) - out.write(WSDL_TYPES) - for api in APIS: - out.write('<message name="%sRequest">' % api) - out.write('<part element="na:%s" name="parameters"/>' % api) - out.write('</message>') - out.write('<message name="%sResponse">' % api) - out.write('<part element="na:%sResult" name="results"/>' % api) - out.write('</message>') - out.write('<portType name="DfmInterface">') - for api in APIS: - out.write('<operation name="%s">' % api) - out.write('<input message="na:%sRequest"/>' % api) - out.write('<output message="na:%sResponse"/>' % api) - out.write('</operation>') - out.write('</portType>') - out.write('<binding name="DfmBinding" type="na:DfmInterface">') - out.write('<soap:binding style="document" ' + - 'transport="http://schemas.xmlsoap.org/soap/http"/>') - for api in APIS: - out.write('<operation name="%s">' % api) - out.write('<soap:operation soapAction="urn:%s"/>' % api) - out.write('<input><soap:body use="literal"/></input>') - out.write('<output><soap:body use="literal"/></output>') - out.write('</operation>') - out.write('</binding>') - out.write(WSDL_TRAILER) - - def do_POST(s): - """Respond to a POST request.""" - if '/apis/soap/v1' != s.path: - s.send_response(404) - s.end_headers - return - request_xml = s.rfile.read(int(s.headers['Content-Length'])) - ntap_ns = 'http://www.netapp.com/management/v1' - nsmap = {'env': 'http://schemas.xmlsoap.org/soap/envelope/', - 'na': ntap_ns} - root = etree.fromstring(request_xml) - - body = root.xpath('/env:Envelope/env:Body', namespaces=nsmap)[0] - request = body.getchildren()[0] - tag = request.tag - if not tag.startswith('{' + ntap_ns + '}'): - s.send_response(500) - s.end_headers - return - api = tag[(2 + len(ntap_ns)):] - global iter_count - global iter_table - if 'DatasetListInfoIterStart' == api: - iter_name = 'dataset_%s' % iter_count - iter_count = iter_count + 1 - iter_table[iter_name] = 0 - body = """<na:DatasetListInfoIterStartResult> - <na:Records>1</na:Records> - <na:Tag>%s</na:Tag> - </na:DatasetListInfoIterStartResult>""" % iter_name - elif 'DatasetListInfoIterNext' == api: - tags = body.xpath('na:DatasetListInfoIterNext/na:Tag', - namespaces=nsmap) - iter_name = tags[0].text - if iter_table[iter_name]: - body = """<na:DatasetListInfoIterNextResult> - <na:Datasets></na:Datasets> - <na:Records>0</na:Records> - </na:DatasetListInfoIterNextResult>""" - else: - iter_table[iter_name] = 1 - body = """<na:DatasetListInfoIterNextResult> - <na:Datasets> - <na:DatasetInfo> - <na:DatasetId>0</na:DatasetId> - <na:DatasetMetadata> - <na:DfmMetadataField> - <na:FieldName>OpenStackProject</na:FieldName> - <na:FieldValue>testproj</na:FieldValue> - </na:DfmMetadataField> - <na:DfmMetadataField> - <na:FieldName>OpenStackVolType</na:FieldName> - <na:FieldValue></na:FieldValue> - </na:DfmMetadataField> - </na:DatasetMetadata> - <na:DatasetName>OpenStack_testproj</na:DatasetName> - </na:DatasetInfo> - </na:Datasets> - <na:Records>1</na:Records> - </na:DatasetListInfoIterNextResult>""" - elif 'DatasetListInfoIterEnd' == api: - body = """<na:DatasetListInfoIterEndResult/>""" - elif 'DatasetEditBegin' == api: - body = """<na:DatasetEditBeginResult> - <na:EditLockId>0</na:EditLockId> - </na:DatasetEditBeginResult>""" - elif 'DatasetEditCommit' == api: - body = """<na:DatasetEditCommitResult> - <na:IsProvisioningFailure>false</na:IsProvisioningFailure> - <na:JobIds> - <na:JobInfo> - <na:JobId>0</na:JobId> - </na:JobInfo> - </na:JobIds> - </na:DatasetEditCommitResult>""" - elif 'DatasetProvisionMember' == api: - body = """<na:DatasetProvisionMemberResult/>""" - elif 'DatasetRemoveMember' == api: - body = """<na:DatasetRemoveMemberResult/>""" - elif 'DfmAbout' == api: - body = """<na:DfmAboutResult/>""" - elif 'DpJobProgressEventListIterStart' == api: - iter_name = 'dpjobprogress_%s' % iter_count - iter_count = iter_count + 1 - iter_table[iter_name] = 0 - body = """<na:DpJobProgressEventListIterStartResult> - <na:Records>2</na:Records> - <na:Tag>%s</na:Tag> - </na:DpJobProgressEventListIterStartResult>""" % iter_name - elif 'DpJobProgressEventListIterNext' == api: - tags = body.xpath('na:DpJobProgressEventListIterNext/na:Tag', - namespaces=nsmap) - iter_name = tags[0].text - if iter_table[iter_name]: - body = """<na:DpJobProgressEventListIterNextResult/>""" - else: - iter_table[iter_name] = 1 - name = ('filer:/OpenStack_testproj/volume-00000001/' - 'volume-00000001') - body = """<na:DpJobProgressEventListIterNextResult> - <na:ProgressEvents> - <na:DpJobProgressEventInfo> - <na:EventStatus>normal</na:EventStatus> - <na:EventType>lun-create</na:EventType> - <na:ProgressLunInfo> - <na:LunPathId>0</na:LunPathId> - <na:LunName>%s</na:LunName> - </na:ProgressLunInfo> - </na:DpJobProgressEventInfo> - <na:DpJobProgressEventInfo> - <na:EventStatus>normal</na:EventStatus> - <na:EventType>job-end</na:EventType> - </na:DpJobProgressEventInfo> - </na:ProgressEvents> - <na:Records>2</na:Records> - </na:DpJobProgressEventListIterNextResult>""" % name - elif 'DpJobProgressEventListIterEnd' == api: - body = """<na:DpJobProgressEventListIterEndResult/>""" - elif 'DatasetMemberListInfoIterStart' == api: - iter_name = 'datasetmember_%s' % iter_count - iter_count = iter_count + 1 - iter_table[iter_name] = 0 - body = """<na:DatasetMemberListInfoIterStartResult> - <na:Records>1</na:Records> - <na:Tag>%s</na:Tag> - </na:DatasetMemberListInfoIterStartResult>""" % iter_name - elif 'DatasetMemberListInfoIterNext' == api: - tags = body.xpath('na:DatasetMemberListInfoIterNext/na:Tag', - namespaces=nsmap) - iter_name = tags[0].text - if iter_table[iter_name]: - body = """<na:DatasetMemberListInfoIterNextResult> - <na:DatasetMembers></na:DatasetMembers> - <na:Records>0</na:Records> - </na:DatasetMemberListInfoIterNextResult>""" - else: - iter_table[iter_name] = 1 - name = ('filer:/OpenStack_testproj/volume-00000001/' - 'volume-00000001') - body = """<na:DatasetMemberListInfoIterNextResult> - <na:DatasetMembers> - <na:DatasetMemberInfo> - <na:MemberId>0</na:MemberId> - <na:MemberName>%s</na:MemberName> - </na:DatasetMemberInfo> - </na:DatasetMembers> - <na:Records>1</na:Records> - </na:DatasetMemberListInfoIterNextResult>""" % name - elif 'DatasetMemberListInfoIterEnd' == api: - body = """<na:DatasetMemberListInfoIterEndResult/>""" - elif 'HostListInfoIterStart' == api: - body = """<na:HostListInfoIterStartResult> - <na:Records>1</na:Records> - <na:Tag>host</na:Tag> - </na:HostListInfoIterStartResult>""" - elif 'HostListInfoIterNext' == api: - body = """<na:HostListInfoIterNextResult> - <na:Hosts> - <na:HostInfo> - <na:HostAddress>1.2.3.4</na:HostAddress> - <na:HostId>0</na:HostId> - <na:HostName>filer</na:HostName> - </na:HostInfo> - </na:Hosts> - <na:Records>1</na:Records> - </na:HostListInfoIterNextResult>""" - elif 'HostListInfoIterEnd' == api: - body = """<na:HostListInfoIterEndResult/>""" - elif 'LunListInfoIterStart' == api: - body = """<na:LunListInfoIterStartResult> - <na:Records>1</na:Records> - <na:Tag>lun</na:Tag> - </na:LunListInfoIterStartResult>""" - elif 'LunListInfoIterNext' == api: - path = 'OpenStack_testproj/volume-00000001/volume-00000001' - body = """<na:LunListInfoIterNextResult> - <na:Luns> - <na:LunInfo> - <na:HostId>0</na:HostId> - <na:LunPath>%s</na:LunPath> - </na:LunInfo> - </na:Luns> - <na:Records>1</na:Records> - </na:LunListInfoIterNextResult>""" % path - elif 'LunListInfoIterEnd' == api: - body = """<na:LunListInfoIterEndResult/>""" - elif 'ApiProxy' == api: - names = body.xpath('na:ApiProxy/na:Request/na:Name', - namespaces=nsmap) - proxy = names[0].text - if 'igroup-list-info' == proxy: - igroup = 'openstack-iqn.1993-08.org.debian:01:23456789' - initiator = 'iqn.1993-08.org.debian:01:23456789' - proxy_body = """<initiator-groups> - <initiator-group-info> - <initiator-group-name>%s</initiator-group-name> - <initiator-group-type>iscsi</initiator-group-type> - <initiator-group-os-type>linux</initiator-group-os-type> - <initiators> - <initiator-info> - <initiator-name>%s</initiator-name> - </initiator-info> - </initiators> - </initiator-group-info> - </initiator-groups>""" % (igroup, initiator) - elif 'igroup-create' == proxy: - proxy_body = '' - elif 'igroup-add' == proxy: - proxy_body = '' - elif 'lun-map-list-info' == proxy: - proxy_body = '<initiator-groups/>' - elif 'lun-map' == proxy: - proxy_body = '<lun-id-assigned>0</lun-id-assigned>' - elif 'lun-unmap' == proxy: - proxy_body = '' - elif 'iscsi-portal-list-info' == proxy: - proxy_body = """<iscsi-portal-list-entries> - <iscsi-portal-list-entry-info> - <ip-address>1.2.3.4</ip-address> - <ip-port>3260</ip-port> - <tpgroup-tag>1000</tpgroup-tag> - </iscsi-portal-list-entry-info> - </iscsi-portal-list-entries>""" - elif 'iscsi-node-get-name' == proxy: - target = 'iqn.1992-08.com.netapp:sn.111111111' - proxy_body = '<node-name>%s</node-name>' % target - else: - # Unknown proxy API - s.send_response(500) - s.end_headers - return - api = api + ':' + proxy - proxy_header = '<na:ApiProxyResult><na:Response><na:Results>' - proxy_trailer = """</na:Results><na:Status>passed</na:Status> - </na:Response></na:ApiProxyResult>""" - body = proxy_header + proxy_body + proxy_trailer - else: - # Unknown API - s.send_response(500) - s.end_headers - return - s.send_response(200) - s.send_header("Content-Type", "text/xml; charset=utf-8") - s.end_headers() - s.wfile.write(RESPONSE_PREFIX) - s.wfile.write(body) - s.wfile.write(RESPONSE_SUFFIX) - - -class FakeHttplibSocket(object): - """A fake socket implementation for httplib.HTTPResponse""" - def __init__(self, value): - self._rbuffer = StringIO.StringIO(value) - self._wbuffer = StringIO.StringIO('') - oldclose = self._wbuffer.close - - def newclose(): - self.result = self._wbuffer.getvalue() - oldclose() - self._wbuffer.close = newclose - - def makefile(self, mode, _other): - """Returns the socket's internal buffer""" - if mode == 'r' or mode == 'rb': - return self._rbuffer - if mode == 'w' or mode == 'wb': - return self._wbuffer - - -class FakeHTTPConnection(object): - """A fake httplib.HTTPConnection for netapp tests - - Requests made via this connection actually get translated and routed into - the fake Dfm handler above, we then turn the response into - the httplib.HTTPResponse that the caller expects. - """ - def __init__(self, host, timeout=None): - self.host = host - - def request(self, method, path, data=None, headers=None): - if not headers: - headers = {} - req_str = '%s %s HTTP/1.1\r\n' % (method, path) - for key, value in headers.iteritems(): - req_str += "%s: %s\r\n" % (key, value) - if data: - req_str += '\r\n%s' % data - - # NOTE(vish): normally the http transport normailizes from unicode - sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8")) - # NOTE(vish): stop the server from trying to look up address from - # the fake socket - FakeDfmServerHandler.address_string = lambda x: '127.0.0.1' - self.app = FakeDfmServerHandler(sock, '127.0.0.1:8088', None) - - self.sock = FakeHttplibSocket(sock.result) - self.http_response = httplib.HTTPResponse(self.sock) - - def set_debuglevel(self, level): - pass - - def getresponse(self): - self.http_response.begin() - return self.http_response - - def getresponsebody(self): - return self.sock.result - - -class NetAppDriverTestCase(test.TestCase): - """Test case for NetAppISCSIDriver""" - STORAGE_SERVICE = 'Openstack Service' - STORAGE_SERVICE_PREFIX = 'Openstack Service-' - PROJECT_ID = 'testproj' - VOLUME_NAME = 'volume-00000001' - VOLUME_TYPE = '' - VOLUME_SIZE = 2147483648L # 2 GB - INITIATOR = 'iqn.1993-08.org.debian:01:23456789' - - def setUp(self): - super(NetAppDriverTestCase, self).setUp() - driver = netapp.NetAppISCSIDriver() - self.stubs.Set(httplib, 'HTTPConnection', FakeHTTPConnection) - driver._create_client(wsdl_url='http://localhost:8088/dfm.wsdl', - login='root', password='password', - hostname='localhost', port=8088, cache=False) - driver._set_storage_service(self.STORAGE_SERVICE) - driver._set_storage_service_prefix(self.STORAGE_SERVICE_PREFIX) - driver._set_vfiler('') - self.driver = driver - - def test_connect(self): - self.driver.check_for_setup_error() - - def test_create_destroy(self): - self.driver._discover_luns() - self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID, - self.VOLUME_TYPE, self.VOLUME_SIZE) - self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID) - - def test_map_unmap(self): - self.driver._discover_luns() - self.driver._provision(self.VOLUME_NAME, None, self.PROJECT_ID, - self.VOLUME_TYPE, self.VOLUME_SIZE) - volume = {'name': self.VOLUME_NAME, 'project_id': self.PROJECT_ID, - 'id': 0, 'provider_auth': None} - updates = self.driver._get_export(volume) - self.assertTrue(updates['provider_location']) - volume['provider_location'] = updates['provider_location'] - connector = {'initiator': self.INITIATOR} - connection_info = self.driver.initialize_connection(volume, connector) - self.assertEqual(connection_info['driver_volume_type'], 'iscsi') - properties = connection_info['data'] - self.driver.terminate_connection(volume, connector) - self.driver._remove_destroy(self.VOLUME_NAME, self.PROJECT_ID) - - -WSDL_HEADER_CMODE = """<?xml version="1.0" encoding="UTF-8"?> -<definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" - xmlns:na="http://cloud.netapp.com/" -xmlns:xsd="http://www.w3.org/2001/XMLSchema" -xmlns="http://schemas.xmlsoap.org/wsdl/" -targetNamespace="http://cloud.netapp.com/" name="CloudStorageService"> -""" - -WSDL_TYPES_CMODE = """<types> -<xs:schema xmlns:na="http://cloud.netapp.com/" -xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" -targetNamespace="http://cloud.netapp.com/"> - - <xs:element name="ProvisionLun"> - <xs:complexType> - <xs:all> - <xs:element name="Name" type="xs:string"/> - <xs:element name="Size" type="xsd:long"/> - <xs:element name="Metadata" type="na:Metadata" minOccurs="0" - maxOccurs="unbounded"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="ProvisionLunResult"> - <xs:complexType> - <xs:all> - <xs:element name="Lun" type="na:Lun"/> - </xs:all> - </xs:complexType> - </xs:element> - - <xs:element name="DestroyLun"> - <xs:complexType> - <xs:all> - <xs:element name="Handle" type="xsd:string"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="DestroyLunResult"> - <xs:complexType> - <xs:all/> - </xs:complexType> - </xs:element> - - <xs:element name="CloneLun"> - <xs:complexType> - <xs:all> - <xs:element name="Handle" type="xsd:string"/> - <xs:element name="NewName" type="xsd:string"/> - <xs:element name="Metadata" type="na:Metadata" minOccurs="0" - maxOccurs="unbounded"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="CloneLunResult"> - <xs:complexType> - <xs:all> - <xs:element name="Lun" type="na:Lun"/> - </xs:all> - </xs:complexType> - </xs:element> - - <xs:element name="MapLun"> - <xs:complexType> - <xs:all> - <xs:element name="Handle" type="xsd:string"/> - <xs:element name="InitiatorType" type="xsd:string"/> - <xs:element name="InitiatorName" type="xsd:string"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="MapLunResult"> - <xs:complexType> - <xs:all/> - </xs:complexType> - </xs:element> - - <xs:element name="UnmapLun"> - <xs:complexType> - <xs:all> - <xs:element name="Handle" type="xsd:string"/> - <xs:element name="InitiatorType" type="xsd:string"/> - <xs:element name="InitiatorName" type="xsd:string"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="UnmapLunResult"> - <xs:complexType> - <xs:all/> - </xs:complexType> - </xs:element> - - <xs:element name="ListLuns"> - <xs:complexType> - <xs:all> - <xs:element name="NameFilter" type="xsd:string" minOccurs="0"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="ListLunsResult"> - <xs:complexType> - <xs:all> - <xs:element name="Lun" type="na:Lun" minOccurs="0" - maxOccurs="unbounded"/> - </xs:all> - </xs:complexType> - </xs:element> - - <xs:element name="GetLunTargetDetails"> - <xs:complexType> - <xs:all> - <xs:element name="Handle" type="xsd:string"/> - <xs:element name="InitiatorType" type="xsd:string"/> - <xs:element name="InitiatorName" type="xsd:string"/> - </xs:all> - </xs:complexType> - </xs:element> - <xs:element name="GetLunTargetDetailsResult"> - <xs:complexType> - <xs:all> - <xs:element name="TargetDetails" type="na:TargetDetails" - minOccurs="0" maxOccurs="unbounded"/> - </xs:all> - </xs:complexType> - </xs:element> - - <xs:complexType name="Metadata"> - <xs:sequence> - <xs:element name="Key" type="xs:string"/> - <xs:element name="Value" type="xs:string"/> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="Lun"> - <xs:sequence> - <xs:element name="Name" type="xs:string"/> - <xs:element name="Size" type="xs:long"/> - <xs:element name="Handle" type="xs:string"/> - <xs:element name="Metadata" type="na:Metadata" minOccurs="0" - maxOccurs="unbounded"/> - </xs:sequence> - </xs:complexType> - - <xs:complexType name="TargetDetails"> - <xs:sequence> - <xs:element name="Address" type="xs:string"/> - <xs:element name="Port" type="xs:int"/> - <xs:element name="Portal" type="xs:int"/> - <xs:element name="Iqn" type="xs:string"/> - <xs:element name="LunNumber" type="xs:int"/> - </xs:sequence> - </xs:complexType> - - </xs:schema></types>""" - -WSDL_TRAILER_CMODE = """<service name="CloudStorageService"> - <port name="CloudStoragePort" binding="na:CloudStorageBinding"> - <soap:address location="http://hostname:8080/ws/ntapcloud"/> - </port> - </service> -</definitions>""" - -RESPONSE_PREFIX_CMODE = """<?xml version='1.0' encoding='UTF-8'?> -<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> -<soapenv:Body>""" - -RESPONSE_SUFFIX_CMODE = """</soapenv:Body></soapenv:Envelope>""" - -CMODE_APIS = ['ProvisionLun', 'DestroyLun', 'CloneLun', 'MapLun', 'UnmapLun', - 'ListLuns', 'GetLunTargetDetails'] - - -class FakeCMODEServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """HTTP handler that fakes enough stuff to allow the driver to run""" - - def do_GET(s): - """Respond to a GET request.""" - if '/ntap_cloud.wsdl' != s.path: - s.send_response(404) - s.end_headers - return - s.send_response(200) - s.send_header("Content-Type", "application/wsdl+xml") - s.end_headers() - out = s.wfile - out.write(WSDL_HEADER_CMODE) - out.write(WSDL_TYPES_CMODE) - for api in CMODE_APIS: - out.write('<message name="%sRequest">' % api) - out.write('<part element="na:%s" name="req"/>' % api) - out.write('</message>') - out.write('<message name="%sResponse">' % api) - out.write('<part element="na:%sResult" name="res"/>' % api) - out.write('</message>') - out.write('<portType name="CloudStorage">') - for api in CMODE_APIS: - out.write('<operation name="%s">' % api) - out.write('<input message="na:%sRequest"/>' % api) - out.write('<output message="na:%sResponse"/>' % api) - out.write('</operation>') - out.write('</portType>') - out.write('<binding name="CloudStorageBinding" ' - 'type="na:CloudStorage">') - out.write('<soap:binding style="document" ' + - 'transport="http://schemas.xmlsoap.org/soap/http"/>') - for api in CMODE_APIS: - out.write('<operation name="%s">' % api) - out.write('<soap:operation soapAction=""/>') - out.write('<input><soap:body use="literal"/></input>') - out.write('<output><soap:body use="literal"/></output>') - out.write('</operation>') - out.write('</binding>') - out.write(WSDL_TRAILER_CMODE) - - def do_POST(s): - """Respond to a POST request.""" - if '/ws/ntapcloud' != s.path: - s.send_response(404) - s.end_headers - return - request_xml = s.rfile.read(int(s.headers['Content-Length'])) - ntap_ns = 'http://cloud.netapp.com/' - nsmap = {'soapenv': 'http://schemas.xmlsoap.org/soap/envelope/', - 'na': ntap_ns} - root = etree.fromstring(request_xml) - - body = root.xpath('/soapenv:Envelope/soapenv:Body', - namespaces=nsmap)[0] - request = body.getchildren()[0] - tag = request.tag - if not tag.startswith('{' + ntap_ns + '}'): - s.send_response(500) - s.end_headers - return - api = tag[(2 + len(ntap_ns)):] - if 'ProvisionLun' == api: - body = """<ns:ProvisionLunResult xmlns:ns= - "http://cloud.netapp.com/"> - <Lun><Name>lun1</Name><Size>20</Size> - <Handle>1d9c006c-a406-42f6-a23f-5ed7a6dc33e3</Handle> - <Metadata><Key>OsType</Key> - <Value>linux</Value></Metadata></Lun> - </ns:ProvisionLunResult>""" - elif 'DestroyLun' == api: - body = """<ns:DestroyLunResult xmlns:ns="http://cloud.netapp.com/" - />""" - elif 'CloneLun' == api: - body = """<ns:CloneLunResult xmlns:ns="http://cloud.netapp.com/"> - <Lun><Name>lun2</Name><Size>2</Size> - <Handle>98ea1791d228453899d422b4611642c3</Handle> - <Metadata><Key>OsType</Key> - <Value>linux</Value></Metadata> - </Lun></ns:CloneLunResult>""" - elif 'MapLun' == api: - body = """<ns1:MapLunResult xmlns:ns="http://cloud.netapp.com/" - />""" - elif 'Unmap' == api: - body = """<ns1:UnmapLunResult xmlns:ns="http://cloud.netapp.com/" - />""" - elif 'ListLuns' == api: - body = """<ns:ListLunsResult xmlns:ns="http://cloud.netapp.com/"> - <Lun> - <Name>lun1</Name> - <Size>20</Size> - <Handle>asdjdnsd</Handle> - </Lun> - </ns:ListLunsResult>""" - elif 'GetLunTargetDetails' == api: - body = """<ns:GetLunTargetDetailsResult - xmlns:ns="http://cloud.netapp.com/"> - <TargetDetail> - <Address>1.2.3.4</Address> - <Port>3260</Port> - <Portal>1000</Portal> - <Iqn>iqn.199208.com.netapp:sn.123456789</Iqn> - <LunNumber>0</LunNumber> - </TargetDetail> - </ns:GetLunTargetDetailsResult>""" - else: - # Unknown API - s.send_response(500) - s.end_headers - return - s.send_response(200) - s.send_header("Content-Type", "text/xml; charset=utf-8") - s.end_headers() - s.wfile.write(RESPONSE_PREFIX_CMODE) - s.wfile.write(body) - s.wfile.write(RESPONSE_SUFFIX_CMODE) - - -class FakeCmodeHTTPConnection(object): - """A fake httplib.HTTPConnection for netapp tests - - Requests made via this connection actually get translated and routed into - the fake Dfm handler above, we then turn the response into - the httplib.HTTPResponse that the caller expects. - """ - def __init__(self, host, timeout=None): - self.host = host - - def request(self, method, path, data=None, headers=None): - if not headers: - headers = {} - req_str = '%s %s HTTP/1.1\r\n' % (method, path) - for key, value in headers.iteritems(): - req_str += "%s: %s\r\n" % (key, value) - if data: - req_str += '\r\n%s' % data - - # NOTE(vish): normally the http transport normailizes from unicode - sock = FakeHttplibSocket(req_str.decode("latin-1").encode("utf-8")) - # NOTE(vish): stop the server from trying to look up address from - # the fake socket - FakeCMODEServerHandler.address_string = lambda x: '127.0.0.1' - self.app = FakeCMODEServerHandler(sock, '127.0.0.1:8080', None) - - self.sock = FakeHttplibSocket(sock.result) - self.http_response = httplib.HTTPResponse(self.sock) - - def set_debuglevel(self, level): - pass - - def getresponse(self): - self.http_response.begin() - return self.http_response - - def getresponsebody(self): - return self.sock.result - - -class NetAppCmodeISCSIDriverTestCase(test.TestCase): - """Test case for NetAppISCSIDriver""" - volume = { - 'name': 'lun1', 'size': 1, 'volume_name': 'lun1', - 'os_type': 'linux', 'provider_location': 'lun1', - 'id': 'lun1', 'provider_auth': None, 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None - } - snapshot = { - 'name': 'lun2', 'size': 1, 'volume_name': 'lun1', - 'volume_size': 1, 'project_id': 'project' - } - volume_sec = { - 'name': 'vol_snapshot', 'size': 1, 'volume_name': 'lun1', - 'os_type': 'linux', 'provider_location': 'lun1', - 'id': 'lun1', 'provider_auth': None, 'project_id': 'project', - 'display_name': None, 'display_description': 'lun1', - 'volume_type_id': None - } - - def setUp(self): - super(NetAppCmodeISCSIDriverTestCase, self).setUp() - driver = netapp.NetAppCmodeISCSIDriver() - self.stubs.Set(httplib, 'HTTPConnection', FakeCmodeHTTPConnection) - driver._create_client(wsdl_url='http://localhost:8080/ntap_cloud.wsdl', - login='root', password='password', - hostname='localhost', port=8080, cache=False) - self.driver = driver - - def test_connect(self): - self.driver.check_for_setup_error() - - def test_create_destroy(self): - self.driver.create_volume(self.volume) - self.driver.delete_volume(self.volume) - - def test_create_vol_snapshot_destroy(self): - self.driver.create_volume(self.volume) - self.driver.create_snapshot(self.snapshot) - self.driver.create_volume_from_snapshot(self.volume_sec, self.snapshot) - self.driver.delete_snapshot(self.snapshot) - self.driver.delete_volume(self.volume) - - def test_map_unmap(self): - self.driver.create_volume(self.volume) - updates = self.driver.create_export(None, self.volume) - self.assertTrue(updates['provider_location']) - self.volume['provider_location'] = updates['provider_location'] - connector = {'initiator': 'init1'} - connection_info = self.driver.initialize_connection(self.volume, - connector) - self.assertEqual(connection_info['driver_volume_type'], 'iscsi') - properties = connection_info['data'] - self.driver.terminate_connection(self.volume, connector) - self.driver.delete_volume(self.volume) diff --git a/nova/tests/test_netapp_nfs.py b/nova/tests/test_netapp_nfs.py deleted file mode 100644 index 1a8824386..000000000 --- a/nova/tests/test_netapp_nfs.py +++ /dev/null @@ -1,234 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# All Rights Reserved. -# -# 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. -"""Unit tests for the NetApp-specific NFS driver module (netapp_nfs)""" - -from nova import context -from nova import exception -from nova import test - -from nova.volume import netapp -from nova.volume import netapp_nfs -from nova.volume import nfs - -from mox import IgnoreArg -from mox import IsA -from mox import MockObject - -import mox -import suds -import types - - -class FakeVolume(object): - def __init__(self, size=0): - self.size = size - self.id = hash(self) - self.name = None - - def __getitem__(self, key): - return self.__dict__[key] - - -class FakeSnapshot(object): - def __init__(self, volume_size=0): - self.volume_name = None - self.name = None - self.volume_id = None - self.volume_size = volume_size - self.user_id = None - self.status = None - - def __getitem__(self, key): - return self.__dict__[key] - - -class FakeResponce(object): - def __init__(self, status): - """ - :param status: Either 'failed' or 'passed' - """ - self.Status = status - - if status == 'failed': - self.Reason = 'Sample error' - - -class NetappNfsDriverTestCase(test.TestCase): - """Test case for NetApp specific NFS clone driver""" - - def setUp(self): - self._driver = netapp_nfs.NetAppNFSDriver() - super(NetappNfsDriverTestCase, self).setUp() - - def test_check_for_setup_error(self): - mox = self.mox - drv = self._driver - - # check exception raises when flags are not set - self.assertRaises(exception.NovaException, - drv.check_for_setup_error) - - # set required flags - self.flags(netapp_wsdl_url='val', - netapp_login='val', - netapp_password='val', - netapp_server_hostname='val', - netapp_server_port='val') - - mox.StubOutWithMock(nfs.NfsDriver, 'check_for_setup_error') - nfs.NfsDriver.check_for_setup_error() - mox.ReplayAll() - - drv.check_for_setup_error() - - def test_do_setup(self): - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, 'check_for_setup_error') - mox.StubOutWithMock(netapp_nfs.NetAppNFSDriver, '_get_client') - - drv.check_for_setup_error() - netapp_nfs.NetAppNFSDriver._get_client() - - mox.ReplayAll() - - drv.do_setup(IsA(context.RequestContext)) - - def test_create_snapshot(self): - """Test snapshot can be created and deleted""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_clone_volume') - drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg()) - mox.ReplayAll() - - drv.create_snapshot(FakeSnapshot()) - - def test_create_volume_from_snapshot(self): - """Tests volume creation from snapshot""" - drv = self._driver - mox = self.mox - volume = FakeVolume(1) - snapshot = FakeSnapshot(2) - - self.assertRaises(exception.NovaException, - drv.create_volume_from_snapshot, - volume, - snapshot) - - snapshot = FakeSnapshot(1) - - location = '127.0.0.1:/nfs' - expected_result = {'provider_location': location} - mox.StubOutWithMock(drv, '_clone_volume') - mox.StubOutWithMock(drv, '_get_volume_location') - drv._clone_volume(IgnoreArg(), IgnoreArg(), IgnoreArg()) - drv._get_volume_location(IgnoreArg()).AndReturn(location) - - mox.ReplayAll() - - loc = drv.create_volume_from_snapshot(volume, snapshot) - - self.assertEquals(loc, expected_result) - - def _prepare_delete_snapshot_mock(self, snapshot_exists): - drv = self._driver - mox = self.mox - - mox.StubOutWithMock(drv, '_get_provider_location') - mox.StubOutWithMock(drv, '_volume_not_present') - - if snapshot_exists: - mox.StubOutWithMock(drv, '_execute') - mox.StubOutWithMock(drv, '_get_volume_path') - - drv._get_provider_location(IgnoreArg()) - drv._volume_not_present(IgnoreArg(), IgnoreArg())\ - .AndReturn(not snapshot_exists) - - if snapshot_exists: - drv._get_volume_path(IgnoreArg(), IgnoreArg()) - drv._execute('rm', None, run_as_root=True) - - mox.ReplayAll() - - return mox - - def test_delete_existing_snapshot(self): - drv = self._driver - self._prepare_delete_snapshot_mock(True) - - drv.delete_snapshot(FakeSnapshot()) - - def test_delete_missing_snapshot(self): - drv = self._driver - self._prepare_delete_snapshot_mock(False) - - drv.delete_snapshot(FakeSnapshot()) - - def _prepare_clone_mock(self, status): - drv = self._driver - mox = self.mox - - volume = FakeVolume() - setattr(volume, 'provider_location', '127.0.0.1:/nfs') - - drv._client = MockObject(suds.client.Client) - drv._client.factory = MockObject(suds.client.Factory) - drv._client.service = MockObject(suds.client.ServiceSelector) - - # ApiProxy() method is generated by ServiceSelector at runtime from the - # XML, so mocking is impossible. - setattr(drv._client.service, - 'ApiProxy', - types.MethodType(lambda *args, **kwargs: FakeResponce(status), - suds.client.ServiceSelector)) - mox.StubOutWithMock(drv, '_get_host_id') - mox.StubOutWithMock(drv, '_get_full_export_path') - - drv._get_host_id(IgnoreArg()).AndReturn('10') - drv._get_full_export_path(IgnoreArg(), IgnoreArg()).AndReturn('/nfs') - - return mox - - def test_successfull_clone_volume(self): - drv = self._driver - mox = self._prepare_clone_mock('passed') - - mox.ReplayAll() - - volume_name = 'volume_name' - clone_name = 'clone_name' - volume_id = volume_name + str(hash(volume_name)) - - drv._clone_volume(volume_name, clone_name, volume_id) - - def test_failed_clone_volume(self): - drv = self._driver - mox = self._prepare_clone_mock('failed') - - mox.ReplayAll() - - volume_name = 'volume_name' - clone_name = 'clone_name' - volume_id = volume_name + str(hash(volume_name)) - - self.assertRaises(exception.NovaException, - drv._clone_volume, - volume_name, clone_name, volume_id) diff --git a/nova/tests/test_nexenta.py b/nova/tests/test_nexenta.py deleted file mode 100644 index aac877cc1..000000000 --- a/nova/tests/test_nexenta.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2011 Nexenta Systems, Inc. -# All Rights Reserved. -# -# 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. -""" -Unit tests for OpenStack Nova volume driver -""" - -import base64 -import urllib2 - -import nova.flags -import nova.test -from nova.volume import nexenta -from nova.volume.nexenta import jsonrpc -from nova.volume.nexenta import volume - -FLAGS = nova.flags.FLAGS - - -class TestNexentaDriver(nova.test.TestCase): - TEST_VOLUME_NAME = 'volume1' - TEST_VOLUME_NAME2 = 'volume2' - TEST_SNAPSHOT_NAME = 'snapshot1' - TEST_VOLUME_REF = { - 'name': TEST_VOLUME_NAME, - 'size': 1, - } - TEST_VOLUME_REF2 = { - 'name': TEST_VOLUME_NAME2, - 'size': 1, - } - TEST_SNAPSHOT_REF = { - 'name': TEST_SNAPSHOT_NAME, - 'volume_name': TEST_VOLUME_NAME, - } - - def setUp(self): - super(TestNexentaDriver, self).setUp() - self.flags( - nexenta_host='1.1.1.1', - nexenta_volume='nova', - nexenta_target_prefix='iqn:', - nexenta_target_group_prefix='nova/', - nexenta_blocksize='8K', - nexenta_sparse=True, - ) - self.nms_mock = self.mox.CreateMockAnything() - for mod in ['volume', 'zvol', 'iscsitarget', - 'stmf', 'scsidisk', 'snapshot']: - setattr(self.nms_mock, mod, self.mox.CreateMockAnything()) - self.stubs.Set(jsonrpc, 'NexentaJSONProxy', - lambda *_, **__: self.nms_mock) - self.drv = volume.NexentaDriver() - self.drv.do_setup({}) - - def test_setup_error(self): - self.nms_mock.volume.object_exists('nova').AndReturn(True) - self.mox.ReplayAll() - self.drv.check_for_setup_error() - - def test_setup_error_fail(self): - self.nms_mock.volume.object_exists('nova').AndReturn(False) - self.mox.ReplayAll() - self.assertRaises(LookupError, self.drv.check_for_setup_error) - - def test_local_path(self): - self.assertRaises(NotImplementedError, self.drv.local_path, '') - - def test_create_volume(self): - self.nms_mock.zvol.create('nova/volume1', '1G', '8K', True) - self.mox.ReplayAll() - self.drv.create_volume(self.TEST_VOLUME_REF) - - def test_delete_volume(self): - self.nms_mock.zvol.destroy('nova/volume1', '') - self.mox.ReplayAll() - self.drv.delete_volume(self.TEST_VOLUME_REF) - - def test_create_snapshot(self): - self.nms_mock.zvol.create_snapshot('nova/volume1', 'snapshot1', '') - self.mox.ReplayAll() - self.drv.create_snapshot(self.TEST_SNAPSHOT_REF) - - def test_create_volume_from_snapshot(self): - self.nms_mock.zvol.clone('nova/volume1@snapshot1', 'nova/volume2') - self.mox.ReplayAll() - self.drv.create_volume_from_snapshot(self.TEST_VOLUME_REF2, - self.TEST_SNAPSHOT_REF) - - def test_delete_snapshot(self): - self.nms_mock.snapshot.destroy('nova/volume1@snapshot1', '') - self.mox.ReplayAll() - self.drv.delete_snapshot(self.TEST_SNAPSHOT_REF) - - _CREATE_EXPORT_METHODS = [ - ('iscsitarget', 'create_target', ({'target_name': 'iqn:volume1'},), - u'Unable to create iscsi target\n' - u' iSCSI target iqn.1986-03.com.sun:02:nova-volume1 already' - u' configured\n' - u' itadm create-target failed with error 17\n', - ), - ('stmf', 'create_targetgroup', ('nova/volume1',), - u'Unable to create targetgroup: stmfadm: nova/volume1:' - u' already exists\n', - ), - ('stmf', 'add_targetgroup_member', ('nova/volume1', 'iqn:volume1'), - u'Unable to add member to targetgroup: stmfadm:' - u' iqn.1986-03.com.sun:02:nova-volume1: already exists\n', - ), - ('scsidisk', 'create_lu', ('nova/volume1', {}), - u"Unable to create lu with zvol 'nova/volume1':\n" - u" sbdadm: filename /dev/zvol/rdsk/nova/volume1: in use\n", - ), - ('scsidisk', 'add_lun_mapping_entry', ('nova/volume1', { - 'target_group': 'nova/volume1', 'lun': '0'}), - u"Unable to add view to zvol 'nova/volume1' (LUNs in use: ):\n" - u" stmfadm: view entry exists\n", - ), - ] - - def _stub_export_method(self, module, method, args, error, fail=False): - m = getattr(self.nms_mock, module) - m = getattr(m, method) - mock = m(*args) - if fail: - mock.AndRaise(nexenta.NexentaException(error)) - - def _stub_all_export_methods(self, fail=False): - for params in self._CREATE_EXPORT_METHODS: - self._stub_export_method(*params, fail=fail) - - def test_create_export(self): - self._stub_all_export_methods() - self.mox.ReplayAll() - retval = self.drv.create_export({}, self.TEST_VOLUME_REF) - self.assertEquals(retval, - {'provider_location': - '%s:%s,1 %s%s' % (FLAGS.nexenta_host, - FLAGS.nexenta_iscsi_target_portal_port, - FLAGS.nexenta_target_prefix, - self.TEST_VOLUME_NAME)}) - - def __get_test(i): - def _test_create_export_fail(self): - for params in self._CREATE_EXPORT_METHODS[:i]: - self._stub_export_method(*params) - self._stub_export_method(*self._CREATE_EXPORT_METHODS[i], - fail=True) - self.mox.ReplayAll() - self.assertRaises(nexenta.NexentaException, - self.drv.create_export, {}, self.TEST_VOLUME_REF) - return _test_create_export_fail - - for i in range(len(_CREATE_EXPORT_METHODS)): - locals()['test_create_export_fail_%d' % i] = __get_test(i) - - def test_ensure_export(self): - self._stub_all_export_methods(fail=True) - self.mox.ReplayAll() - self.drv.ensure_export({}, self.TEST_VOLUME_REF) - - def test_remove_export(self): - self.nms_mock.scsidisk.delete_lu('nova/volume1') - self.nms_mock.stmf.destroy_targetgroup('nova/volume1') - self.nms_mock.iscsitarget.delete_target('iqn:volume1') - self.mox.ReplayAll() - self.drv.remove_export({}, self.TEST_VOLUME_REF) - - def test_remove_export_fail_0(self): - self.nms_mock.scsidisk.delete_lu('nova/volume1') - self.nms_mock.stmf.destroy_targetgroup('nova/volume1').AndRaise( - nexenta.NexentaException()) - self.nms_mock.iscsitarget.delete_target('iqn:volume1') - self.mox.ReplayAll() - self.drv.remove_export({}, self.TEST_VOLUME_REF) - - def test_remove_export_fail_1(self): - self.nms_mock.scsidisk.delete_lu('nova/volume1') - self.nms_mock.stmf.destroy_targetgroup('nova/volume1') - self.nms_mock.iscsitarget.delete_target('iqn:volume1').AndRaise( - nexenta.NexentaException()) - self.mox.ReplayAll() - self.drv.remove_export({}, self.TEST_VOLUME_REF) - - -class TestNexentaJSONRPC(nova.test.TestCase): - URL = 'http://example.com/' - URL_S = 'https://example.com/' - USER = 'user' - PASSWORD = 'password' - HEADERS = {'Authorization': 'Basic %s' % (base64.b64encode( - ':'.join((USER, PASSWORD))),), - 'Content-Type': 'application/json'} - REQUEST = 'the request' - - def setUp(self): - super(TestNexentaJSONRPC, self).setUp() - self.proxy = jsonrpc.NexentaJSONProxy( - self.URL, self.USER, self.PASSWORD, auto=True) - self.mox.StubOutWithMock(urllib2, 'Request', True) - self.mox.StubOutWithMock(urllib2, 'urlopen') - self.resp_mock = self.mox.CreateMockAnything() - self.resp_info_mock = self.mox.CreateMockAnything() - self.resp_mock.info().AndReturn(self.resp_info_mock) - urllib2.urlopen(self.REQUEST).AndReturn(self.resp_mock) - - def test_call(self): - urllib2.Request(self.URL, - '{"object": null, "params": ["arg1", "arg2"], "method": null}', - self.HEADERS).AndReturn(self.REQUEST) - self.resp_info_mock.status = '' - self.resp_mock.read().AndReturn( - '{"error": null, "result": "the result"}') - self.mox.ReplayAll() - result = self.proxy('arg1', 'arg2') - self.assertEquals("the result", result) - - def test_call_deep(self): - urllib2.Request(self.URL, - '{"object": "obj1.subobj", "params": ["arg1", "arg2"],' - ' "method": "meth"}', - self.HEADERS).AndReturn(self.REQUEST) - self.resp_info_mock.status = '' - self.resp_mock.read().AndReturn( - '{"error": null, "result": "the result"}') - self.mox.ReplayAll() - result = self.proxy.obj1.subobj.meth('arg1', 'arg2') - self.assertEquals("the result", result) - - def test_call_auto(self): - urllib2.Request(self.URL, - '{"object": null, "params": ["arg1", "arg2"], "method": null}', - self.HEADERS).AndReturn(self.REQUEST) - urllib2.Request(self.URL_S, - '{"object": null, "params": ["arg1", "arg2"], "method": null}', - self.HEADERS).AndReturn(self.REQUEST) - self.resp_info_mock.status = 'EOF in headers' - self.resp_mock.read().AndReturn( - '{"error": null, "result": "the result"}') - urllib2.urlopen(self.REQUEST).AndReturn(self.resp_mock) - self.mox.ReplayAll() - result = self.proxy('arg1', 'arg2') - self.assertEquals("the result", result) - - def test_call_error(self): - urllib2.Request(self.URL, - '{"object": null, "params": ["arg1", "arg2"], "method": null}', - self.HEADERS).AndReturn(self.REQUEST) - self.resp_info_mock.status = '' - self.resp_mock.read().AndReturn( - '{"error": {"message": "the error"}, "result": "the result"}') - self.mox.ReplayAll() - self.assertRaises(jsonrpc.NexentaJSONException, - self.proxy, 'arg1', 'arg2') - - def test_call_fail(self): - urllib2.Request(self.URL, - '{"object": null, "params": ["arg1", "arg2"], "method": null}', - self.HEADERS).AndReturn(self.REQUEST) - self.resp_info_mock.status = 'EOF in headers' - self.proxy.auto = False - self.mox.ReplayAll() - self.assertRaises(jsonrpc.NexentaJSONException, - self.proxy, 'arg1', 'arg2') diff --git a/nova/tests/test_nfs.py b/nova/tests/test_nfs.py deleted file mode 100644 index d0d235b1b..000000000 --- a/nova/tests/test_nfs.py +++ /dev/null @@ -1,569 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# All Rights Reserved. -# -# 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. -"""Unit tests for the NFS driver module""" - -import __builtin__ -import errno -import os - -import mox as mox_lib -from mox import IgnoreArg -from mox import IsA -from mox import stubout - -from nova import context -from nova import exception -from nova.exception import ProcessExecutionError -from nova import test - -from nova.volume import nfs - - -class DumbVolume(object): - fields = {} - - def __setitem__(self, key, value): - self.fields[key] = value - - def __getitem__(self, item): - return self.fields[item] - - -class NfsDriverTestCase(test.TestCase): - """Test case for NFS driver""" - - TEST_NFS_EXPORT1 = 'nfs-host1:/export' - TEST_NFS_EXPORT2 = 'nfs-host2:/export' - TEST_SIZE_IN_GB = 1 - TEST_MNT_POINT = '/mnt/nfs' - TEST_MNT_POINT_BASE = '/mnt/test' - TEST_LOCAL_PATH = '/mnt/nfs/volume-123' - TEST_FILE_NAME = 'test.txt' - TEST_SHARES_CONFIG_FILE = '/etc/cinder/test-shares.conf' - ONE_GB_IN_BYTES = 1024 * 1024 * 1024 - - def setUp(self): - self._driver = nfs.NfsDriver() - super(NfsDriverTestCase, self).setUp() - - def stub_out_not_replaying(self, obj, attr_name): - attr_to_replace = getattr(obj, attr_name) - stub = mox_lib.MockObject(attr_to_replace) - self.stubs.Set(obj, attr_name, stub) - - def test_path_exists_should_return_true(self): - """_path_exists should return True if stat returns 0""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_execute') - drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True) - - mox.ReplayAll() - - self.assertTrue(drv._path_exists(self.TEST_FILE_NAME)) - - def test_path_exists_should_return_false(self): - """_path_exists should return True if stat doesn't return 0""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_execute') - drv._execute('stat', self.TEST_FILE_NAME, run_as_root=True).\ - AndRaise(ProcessExecutionError( - stderr="stat: cannot stat `test.txt': No such file or directory")) - - mox.ReplayAll() - - self.assertFalse(drv._path_exists(self.TEST_FILE_NAME)) - - def test_local_path(self): - """local_path common use case""" - self.flags(nfs_mount_point_base=self.TEST_MNT_POINT_BASE) - drv = self._driver - - volume = DumbVolume() - volume['provider_location'] = self.TEST_NFS_EXPORT1 - volume['name'] = 'volume-123' - - self.assertEqual( - '/mnt/test/2f4f60214cf43c595666dd815f0360a4/volume-123', - drv.local_path(volume)) - - def test_mount_nfs_should_mount_correctly(self): - """_mount_nfs common case usage""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1, - self.TEST_MNT_POINT, run_as_root=True) - - mox.ReplayAll() - - drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT) - - def test_mount_nfs_should_suppress_already_mounted_error(self): - """_mount_nfs should suppress already mounted error if ensure=True - """ - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1, - self.TEST_MNT_POINT, run_as_root=True).\ - AndRaise(ProcessExecutionError( - stderr='is busy or already mounted')) - - mox.ReplayAll() - - drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True) - - def test_mount_nfs_should_reraise_already_mounted_error(self): - """_mount_nfs should not suppress already mounted error if ensure=False - """ - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('mount', '-t', 'nfs', self.TEST_NFS_EXPORT1, - self.TEST_MNT_POINT, run_as_root=True).\ - AndRaise(ProcessExecutionError(stderr='is busy or already mounted')) - - mox.ReplayAll() - - self.assertRaises(ProcessExecutionError, drv._mount_nfs, - self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, - ensure=False) - - def test_mount_nfs_should_create_mountpoint_if_not_yet(self): - """_mount_nfs should create mountpoint if it doesn't exist""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_MNT_POINT).AndReturn(False) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('mkdir', '-p', self.TEST_MNT_POINT) - drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg()) - - mox.ReplayAll() - - drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT) - - def test_mount_nfs_should_not_create_mountpoint_if_already(self): - """_mount_nfs should not create mountpoint if it already exists""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_MNT_POINT).AndReturn(True) - - mox.StubOutWithMock(drv, '_execute') - drv._execute(*([IgnoreArg()] * 5), run_as_root=IgnoreArg()) - - mox.ReplayAll() - - drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT) - - def test_get_hash_str(self): - """_get_hash_str should calculation correct value""" - drv = self._driver - - self.assertEqual('2f4f60214cf43c595666dd815f0360a4', - drv._get_hash_str(self.TEST_NFS_EXPORT1)) - - def test_get_mount_point_for_share(self): - """_get_mount_point_for_share should calculate correct value""" - drv = self._driver - - self.flags(nfs_mount_point_base=self.TEST_MNT_POINT_BASE) - - self.assertEqual('/mnt/test/2f4f60214cf43c595666dd815f0360a4', - drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1)) - - def test_get_available_capacity_with_df(self): - """_get_available_capacity should calculate correct value""" - mox = self.mox - drv = self._driver - - df_avail = 1490560 - df_head = 'Filesystem 1K-blocks Used Available Use% Mounted on\n' - df_data = 'nfs-host:/export 2620544 996864 %d 41%% /mnt' % df_avail - df_output = df_head + df_data - - self.flags(nfs_disk_util='df') - - mox.StubOutWithMock(drv, '_get_mount_point_for_share') - drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\ - AndReturn(self.TEST_MNT_POINT) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT, - run_as_root=True).AndReturn((df_output, None)) - - mox.ReplayAll() - - self.assertEquals(df_avail, - drv._get_available_capacity(self.TEST_NFS_EXPORT1)) - - def test_get_available_capacity_with_du(self): - """_get_available_capacity should calculate correct value""" - mox = self.mox - drv = self._driver - - self.flags(nfs_disk_util='du') - - df_total_size = 2620544 - df_used_size = 996864 - df_avail_size = 1490560 - df_title = 'Filesystem 1-blocks Used Available Use% Mounted on\n' - df_mnt_data = 'nfs-host:/export %d %d %d 41%% /mnt' % (df_total_size, - df_used_size, - df_avail_size) - df_output = df_title + df_mnt_data - - du_used = 490560 - du_output = '%d /mnt' % du_used - - mox.StubOutWithMock(drv, '_get_mount_point_for_share') - drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\ - AndReturn(self.TEST_MNT_POINT) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('df', '-P', '-B', '1', self.TEST_MNT_POINT, - run_as_root=True).\ - AndReturn((df_output, None)) - drv._execute('du', '-sb', '--apparent-size', - '--exclude', '*snapshot*', - self.TEST_MNT_POINT, - run_as_root=True).AndReturn((du_output, None)) - - mox.ReplayAll() - - self.assertEquals(df_total_size - du_used, - drv._get_available_capacity(self.TEST_NFS_EXPORT1)) - - def test_load_shares_config(self): - mox = self.mox - drv = self._driver - - self.flags(nfs_shares_config=self.TEST_SHARES_CONFIG_FILE) - - mox.StubOutWithMock(__builtin__, 'open') - config_data = [] - config_data.append(self.TEST_NFS_EXPORT1) - config_data.append('#' + self.TEST_NFS_EXPORT2) - config_data.append('') - __builtin__.open(self.TEST_SHARES_CONFIG_FILE).AndReturn(config_data) - mox.ReplayAll() - - shares = drv._load_shares_config() - - self.assertEqual([self.TEST_NFS_EXPORT1], shares) - - def test_ensure_share_mounted(self): - """_ensure_share_mounted simple use case""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_get_mount_point_for_share') - drv._get_mount_point_for_share(self.TEST_NFS_EXPORT1).\ - AndReturn(self.TEST_MNT_POINT) - - mox.StubOutWithMock(drv, '_mount_nfs') - drv._mount_nfs(self.TEST_NFS_EXPORT1, self.TEST_MNT_POINT, ensure=True) - - mox.ReplayAll() - - drv._ensure_share_mounted(self.TEST_NFS_EXPORT1) - - def test_ensure_shares_mounted_should_save_mounting_successfully(self): - """_ensure_shares_mounted should save share if mounted with success""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_load_shares_config') - drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1]) - mox.StubOutWithMock(drv, '_ensure_share_mounted') - drv._ensure_share_mounted(self.TEST_NFS_EXPORT1) - - mox.ReplayAll() - - drv._ensure_shares_mounted() - - self.assertEqual(1, len(drv._mounted_shares)) - self.assertEqual(self.TEST_NFS_EXPORT1, drv._mounted_shares[0]) - - def test_ensure_shares_mounted_should_not_save_mounting_with_error(self): - """_ensure_shares_mounted should not save share if failed to mount""" - mox = self.mox - drv = self._driver - - mox.StubOutWithMock(drv, '_load_shares_config') - drv._load_shares_config().AndReturn([self.TEST_NFS_EXPORT1]) - mox.StubOutWithMock(drv, '_ensure_share_mounted') - drv._ensure_share_mounted(self.TEST_NFS_EXPORT1).AndRaise(Exception()) - - mox.ReplayAll() - - drv._ensure_shares_mounted() - - self.assertEqual(0, len(drv._mounted_shares)) - - def test_setup_should_throw_error_if_shares_config_not_configured(self): - """do_setup should throw error if shares config is not configured """ - drv = self._driver - - self.flags(nfs_shares_config=self.TEST_SHARES_CONFIG_FILE) - - self.assertRaises(exception.NfsException, - drv.do_setup, IsA(context.RequestContext)) - - def test_setup_should_throw_exception_if_nfs_client_is_not_installed(self): - """do_setup should throw error if nfs client is not installed """ - mox = self.mox - drv = self._driver - - self.flags(nfs_shares_config=self.TEST_SHARES_CONFIG_FILE) - - mox.StubOutWithMock(os.path, 'exists') - os.path.exists(self.TEST_SHARES_CONFIG_FILE).AndReturn(True) - mox.StubOutWithMock(drv, '_execute') - drv._execute('mount.nfs', check_exit_code=False).\ - AndRaise(OSError(errno.ENOENT, 'No such file or directory')) - - mox.ReplayAll() - - self.assertRaises(exception.NfsException, - drv.do_setup, IsA(context.RequestContext)) - - def test_find_share_should_throw_error_if_there_is_no_mounted_shares(self): - """_find_share should throw error if there is no mounted shares""" - drv = self._driver - - drv._mounted_shares = [] - - self.assertRaises(exception.NotFound, drv._find_share, - self.TEST_SIZE_IN_GB) - - def test_find_share(self): - """_find_share simple use case""" - mox = self.mox - drv = self._driver - - drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2] - - mox.StubOutWithMock(drv, '_get_available_capacity') - drv._get_available_capacity(self.TEST_NFS_EXPORT1).\ - AndReturn(2 * self.ONE_GB_IN_BYTES) - drv._get_available_capacity(self.TEST_NFS_EXPORT2).\ - AndReturn(3 * self.ONE_GB_IN_BYTES) - - mox.ReplayAll() - - self.assertEqual(self.TEST_NFS_EXPORT2, - drv._find_share(self.TEST_SIZE_IN_GB)) - - def test_find_share_should_throw_error_if_there_is_no_enough_place(self): - """_find_share should throw error if there is no share to host vol""" - mox = self.mox - drv = self._driver - - drv._mounted_shares = [self.TEST_NFS_EXPORT1, self.TEST_NFS_EXPORT2] - - mox.StubOutWithMock(drv, '_get_available_capacity') - drv._get_available_capacity(self.TEST_NFS_EXPORT1).\ - AndReturn(0) - drv._get_available_capacity(self.TEST_NFS_EXPORT2).\ - AndReturn(0) - - mox.ReplayAll() - - self.assertRaises(exception.NfsNoSuitableShareFound, drv._find_share, - self.TEST_SIZE_IN_GB) - - def _simple_volume(self): - volume = DumbVolume() - volume['provider_location'] = '127.0.0.1:/mnt' - volume['name'] = 'volume_name' - volume['size'] = 10 - - return volume - - def test_create_sparsed_volume(self): - mox = self.mox - drv = self._driver - volume = self._simple_volume() - - self.flags(nfs_sparsed_volumes=True) - - mox.StubOutWithMock(drv, '_create_sparsed_file') - mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') - - drv._create_sparsed_file(IgnoreArg(), IgnoreArg()) - drv._set_rw_permissions_for_all(IgnoreArg()) - - mox.ReplayAll() - - drv._do_create_volume(volume) - - def test_create_nonsparsed_volume(self): - mox = self.mox - drv = self._driver - volume = self._simple_volume() - - self.flags(nfs_sparsed_volumes=False) - - mox.StubOutWithMock(drv, '_create_regular_file') - mox.StubOutWithMock(drv, '_set_rw_permissions_for_all') - - drv._create_regular_file(IgnoreArg(), IgnoreArg()) - drv._set_rw_permissions_for_all(IgnoreArg()) - - mox.ReplayAll() - - drv._do_create_volume(volume) - - def test_create_volume_should_ensure_nfs_mounted(self): - """create_volume should ensure shares provided in config are mounted""" - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(nfs, 'LOG') - self.stub_out_not_replaying(drv, '_find_share') - self.stub_out_not_replaying(drv, '_do_create_volume') - - mox.StubOutWithMock(drv, '_ensure_shares_mounted') - drv._ensure_shares_mounted() - - mox.ReplayAll() - - volume = DumbVolume() - volume['size'] = self.TEST_SIZE_IN_GB - drv.create_volume(volume) - - def test_create_volume_should_return_provider_location(self): - """create_volume should return provider_location with found share """ - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(nfs, 'LOG') - self.stub_out_not_replaying(drv, '_ensure_shares_mounted') - self.stub_out_not_replaying(drv, '_do_create_volume') - - mox.StubOutWithMock(drv, '_find_share') - drv._find_share(self.TEST_SIZE_IN_GB).AndReturn(self.TEST_NFS_EXPORT1) - - mox.ReplayAll() - - volume = DumbVolume() - volume['size'] = self.TEST_SIZE_IN_GB - result = drv.create_volume(volume) - self.assertEqual(self.TEST_NFS_EXPORT1, result['provider_location']) - - def test_delete_volume(self): - """delete_volume simple test case""" - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(drv, '_ensure_share_mounted') - - volume = DumbVolume() - volume['name'] = 'volume-123' - volume['provider_location'] = self.TEST_NFS_EXPORT1 - - mox.StubOutWithMock(drv, 'local_path') - drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH) - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(True) - - mox.StubOutWithMock(drv, '_execute') - drv._execute('rm', '-f', self.TEST_LOCAL_PATH, run_as_root=True) - - mox.ReplayAll() - - drv.delete_volume(volume) - - def test_delete_should_ensure_share_mounted(self): - """delete_volume should ensure that corresponding share is mounted""" - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(drv, '_execute') - - volume = DumbVolume() - volume['name'] = 'volume-123' - volume['provider_location'] = self.TEST_NFS_EXPORT1 - - mox.StubOutWithMock(drv, '_ensure_share_mounted') - drv._ensure_share_mounted(self.TEST_NFS_EXPORT1) - - mox.ReplayAll() - - drv.delete_volume(volume) - - def test_delete_should_not_delete_if_provider_location_not_provided(self): - """delete_volume shouldn't try to delete if provider_location missed""" - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(drv, '_ensure_share_mounted') - - volume = DumbVolume() - volume['name'] = 'volume-123' - volume['provider_location'] = None - - mox.StubOutWithMock(drv, '_execute') - - mox.ReplayAll() - - drv.delete_volume(volume) - - def test_delete_should_not_delete_if_there_is_no_file(self): - """delete_volume should not try to delete if file missed""" - mox = self.mox - drv = self._driver - - self.stub_out_not_replaying(drv, '_ensure_share_mounted') - - volume = DumbVolume() - volume['name'] = 'volume-123' - volume['provider_location'] = self.TEST_NFS_EXPORT1 - - mox.StubOutWithMock(drv, 'local_path') - drv.local_path(volume).AndReturn(self.TEST_LOCAL_PATH) - - mox.StubOutWithMock(drv, '_path_exists') - drv._path_exists(self.TEST_LOCAL_PATH).AndReturn(False) - - mox.StubOutWithMock(drv, '_execute') - - mox.ReplayAll() - - drv.delete_volume(volume) diff --git a/nova/tests/test_plugin_api_extensions.py b/nova/tests/test_plugin_api_extensions.py index af30c10d1..a40dd3276 100644 --- a/nova/tests/test_plugin_api_extensions.py +++ b/nova/tests/test_plugin_api_extensions.py @@ -72,7 +72,6 @@ class APITestCase(test.TestCase): # Marking out the default extension paths makes this test MUCH faster. self.flags(osapi_compute_extension=[]) - self.flags(osapi_volume_extension=[]) found = False mgr = computeextensions.ExtensionManager() diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py index dd86c7c03..5ec753efe 100644 --- a/nova/tests/test_quota.py +++ b/nova/tests/test_quota.py @@ -127,27 +127,6 @@ class QuotaIntegrationTestCase(test.TestCase): image_href=image_uuid) db.instance_destroy(self.context, instance['uuid']) - def test_too_many_volumes(self): - volume_ids = [] - for i in range(FLAGS.quota_volumes): - volume_id = self._create_volume() - volume_ids.append(volume_id) - self.assertRaises(exception.QuotaError, - volume.API().create, - self.context, 10, '', '', None) - for volume_id in volume_ids: - db.volume_destroy(self.context, volume_id) - - def test_too_many_gigabytes(self): - volume_ids = [] - volume_id = self._create_volume(size=20) - volume_ids.append(volume_id) - self.assertRaises(exception.QuotaError, - volume.API().create, - self.context, 10, '', '', None) - for volume_id in volume_ids: - db.volume_destroy(self.context, volume_id) - def test_too_many_addresses(self): address = '192.168.0.100' db.floating_ip_create(context.get_admin_context(), @@ -720,8 +699,6 @@ class DbQuotaDriverTestCase(test.TestCase): self.flags(quota_instances=10, quota_cores=20, quota_ram=50 * 1024, - quota_volumes=10, - quota_gigabytes=1000, quota_floating_ips=10, quota_metadata_items=128, quota_injected_files=5, diff --git a/nova/tests/test_rbd.py b/nova/tests/test_rbd.py deleted file mode 100644 index 8e90f3ae8..000000000 --- a/nova/tests/test_rbd.py +++ /dev/null @@ -1,161 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Josh Durgin -# All Rights Reserved. -# -# 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 nova import db -from nova import exception -from nova.openstack.common import log as logging -from nova.openstack.common import timeutils -from nova import test -from nova.tests.image import fake as fake_image -from nova.tests.test_volume import DriverTestCase -from nova.volume.driver import RBDDriver - -LOG = logging.getLogger(__name__) - - -class RBDTestCase(test.TestCase): - - def setUp(self): - super(RBDTestCase, self).setUp() - - def fake_execute(*args): - pass - self.driver = RBDDriver(execute=fake_execute) - - def test_good_locations(self): - locations = [ - 'rbd://fsid/pool/image/snap', - 'rbd://%2F/%2F/%2F/%2F', - ] - map(self.driver._parse_location, locations) - - def test_bad_locations(self): - locations = [ - 'rbd://image', - 'http://path/to/somewhere/else', - 'rbd://image/extra', - 'rbd://image/', - 'rbd://fsid/pool/image/', - 'rbd://fsid/pool/image/snap/', - 'rbd://///', - ] - for loc in locations: - self.assertRaises(exception.ImageUnacceptable, - self.driver._parse_location, - loc) - self.assertFalse(self.driver._is_cloneable(loc)) - - def test_cloneable(self): - self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc') - location = 'rbd://abc/pool/image/snap' - self.assertTrue(self.driver._is_cloneable(location)) - - def test_uncloneable_different_fsid(self): - self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc') - location = 'rbd://def/pool/image/snap' - self.assertFalse(self.driver._is_cloneable(location)) - - def test_uncloneable_unreadable(self): - def fake_exc(*args): - raise exception.ProcessExecutionError() - self.stubs.Set(self.driver, '_get_fsid', lambda: 'abc') - self.stubs.Set(self.driver, '_execute', fake_exc) - location = 'rbd://abc/pool/image/snap' - self.assertFalse(self.driver._is_cloneable(location)) - - -class FakeRBDDriver(RBDDriver): - - def _clone(self): - pass - - def _resize(self): - pass - - -class ManagedRBDTestCase(DriverTestCase): - driver_name = "nova.tests.test_rbd.FakeRBDDriver" - - def setUp(self): - super(ManagedRBDTestCase, self).setUp() - fake_image.stub_out_image_service(self.stubs) - - def _clone_volume_from_image(self, expected_status, - clone_works=True): - """Try to clone a volume from an image, and check the status - afterwards""" - def fake_clone_image(volume, image_location): - pass - - def fake_clone_error(volume, image_location): - raise exception.NovaException() - - self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True) - if clone_works: - self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_image) - else: - self.stubs.Set(self.volume.driver, 'clone_image', fake_clone_error) - - image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' - volume_id = 1 - # creating volume testdata - db.volume_create(self.context, {'id': volume_id, - 'updated_at': timeutils.utcnow(), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'creating', - 'instance_uuid': None, - 'host': 'dummy'}) - try: - if clone_works: - self.volume.create_volume(self.context, - volume_id, - image_id=image_id) - else: - self.assertRaises(exception.NovaException, - self.volume.create_volume, - self.context, - volume_id, - image_id=image_id) - - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], expected_status) - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - - def test_clone_image_status_available(self): - """Verify that before cloning, an image is in the available state.""" - self._clone_volume_from_image('available', True) - - def test_clone_image_status_error(self): - """Verify that before cloning, an image is in the available state.""" - self._clone_volume_from_image('error', False) - - def test_clone_success(self): - self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True) - self.stubs.Set(self.volume.driver, 'clone_image', lambda a, b: True) - image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' - self.assertTrue(self.volume.driver.clone_image({}, image_id)) - - def test_clone_bad_image_id(self): - self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: True) - self.assertFalse(self.volume.driver.clone_image({}, None)) - - def test_clone_uncloneable(self): - self.stubs.Set(self.volume.driver, '_is_cloneable', lambda x: False) - self.assertFalse(self.volume.driver.clone_image({}, 'dne')) diff --git a/nova/tests/test_solidfire.py b/nova/tests/test_solidfire.py deleted file mode 100644 index 87a211da5..000000000 --- a/nova/tests/test_solidfire.py +++ /dev/null @@ -1,208 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# 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 nova import exception -from nova.openstack.common import log as logging -from nova import test -from nova.volume.solidfire import SolidFire - -LOG = logging.getLogger(__name__) - - -class SolidFireVolumeTestCase(test.TestCase): - def fake_issue_api_request(obj, method, params): - if method is 'GetClusterInfo': - LOG.info('Called Fake GetClusterInfo...') - results = {'result': {'clusterInfo': - {'name': 'fake-cluster', - 'mvip': '1.1.1.1', - 'svip': '1.1.1.1', - 'uniqueID': 'unqid', - 'repCount': 2, - 'attributes': {}}}} - return results - - elif method is 'AddAccount': - LOG.info('Called Fake AddAccount...') - return {'result': {'accountID': 25}, 'id': 1} - - elif method is 'GetAccountByName': - LOG.info('Called Fake GetAccountByName...') - results = {'result': {'account': - {'accountID': 25, - 'username': params['username'], - 'status': 'active', - 'initiatorSecret': '123456789012', - 'targetSecret': '123456789012', - 'attributes': {}, - 'volumes': [6, 7, 20]}}, - "id": 1} - return results - - elif method is 'CreateVolume': - LOG.info('Called Fake CreateVolume...') - return {'result': {'volumeID': 5}, 'id': 1} - - elif method is 'DeleteVolume': - LOG.info('Called Fake DeleteVolume...') - return {'result': {}, 'id': 1} - - elif method is 'ListVolumesForAccount': - test_name = 'OS-VOLID-a720b3c0-d1f0-11e1-9b23-0800200c9a66' - LOG.info('Called Fake ListVolumesForAccount...') - result = {'result': { - 'volumes': [{'volumeID': 5, - 'name': test_name, - 'accountID': 25, - 'sliceCount': 1, - 'totalSize': 1048576 * 1024, - 'enable512e': True, - 'access': "readWrite", - 'status': "active", - 'attributes':None, - 'qos': None, - 'iqn': test_name}]}} - return result - - else: - LOG.error('Crap, unimplemented API call in Fake:%s' % method) - - def fake_issue_api_request_no_volume(obj, method, params): - if method is 'ListVolumesForAccount': - LOG.info('Called Fake ListVolumesForAccount...') - return {'result': {'volumes': []}} - else: - return obj.fake_issue_api_request(method, params) - - def fake_issue_api_request_fails(obj, method, params): - return {'error': {'code': 000, - 'name': 'DummyError', - 'message': 'This is a fake error response'}, - 'id': 1} - - def fake_volume_get(obj, key, default=None): - return {'qos': 'fast'} - - def test_create_volume(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - testvol = {'project_id': 'testprjid', - 'name': 'testvol', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} - sfv = SolidFire() - model_update = sfv.create_volume(testvol) - - def test_create_volume_with_qos(self): - preset_qos = {} - preset_qos['qos'] = 'fast' - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - - testvol = {'project_id': 'testprjid', - 'name': 'testvol', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66', - 'metadata': [preset_qos]} - - sfv = SolidFire() - model_update = sfv.create_volume(testvol) - - def test_create_volume_fails(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_fails) - testvol = {'project_id': 'testprjid', - 'name': 'testvol', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} - sfv = SolidFire() - self.assertRaises(exception.SolidFireAPIDataException, - sfv.create_volume, testvol) - - def test_create_sfaccount(self): - sfv = SolidFire() - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - account = sfv._create_sfaccount('project-id') - self.assertNotEqual(account, None) - - def test_create_sfaccount_fails(self): - sfv = SolidFire() - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_fails) - account = sfv._create_sfaccount('project-id') - self.assertEqual(account, None) - - def test_get_sfaccount_by_name(self): - sfv = SolidFire() - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - account = sfv._get_sfaccount_by_name('some-name') - self.assertNotEqual(account, None) - - def test_get_sfaccount_by_name_fails(self): - sfv = SolidFire() - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_fails) - account = sfv._get_sfaccount_by_name('some-name') - self.assertEqual(account, None) - - def test_delete_volume(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - testvol = {'project_id': 'testprjid', - 'name': 'test_volume', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} - sfv = SolidFire() - model_update = sfv.delete_volume(testvol) - - def test_delete_volume_fails_no_volume(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_no_volume) - testvol = {'project_id': 'testprjid', - 'name': 'no-name', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} - sfv = SolidFire() - self.assertRaises(exception.VolumeNotFound, - sfv.delete_volume, testvol) - - def test_delete_volume_fails_account_lookup(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_fails) - testvol = {'project_id': 'testprjid', - 'name': 'no-name', - 'size': 1, - 'id': 'a720b3c0-d1f0-11e1-9b23-0800200c9a66'} - sfv = SolidFire() - self.assertRaises(exception.SfAccountNotFound, - sfv.delete_volume, - testvol) - - def test_get_cluster_info(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request) - sfv = SolidFire() - sfv._get_cluster_info() - - def test_get_cluster_info_fail(self): - self.stubs.Set(SolidFire, '_issue_api_request', - self.fake_issue_api_request_fails) - sfv = SolidFire() - self.assertRaises(exception.SolidFireAPIException, - sfv._get_cluster_info) diff --git a/nova/tests/test_storwize_svc.py b/nova/tests/test_storwize_svc.py deleted file mode 100644 index 513043256..000000000 --- a/nova/tests/test_storwize_svc.py +++ /dev/null @@ -1,1376 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 IBM, Inc. -# Copyright (c) 2012 OpenStack LLC. -# All Rights Reserved. -# -# 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. -# -# Authors: -# Ronen Kat <ronenkat@il.ibm.com> -# Avishay Traeger <avishay@il.ibm.com> - -""" -Tests for the IBM Storwize V7000 and SVC volume driver. -""" - -import random -import socket - -from nova import exception -from nova import flags -from nova.openstack.common import excutils -from nova.openstack.common import log as logging -from nova import test -from nova.volume import storwize_svc - -FLAGS = flags.FLAGS - -LOG = logging.getLogger(__name__) - - -class StorwizeSVCManagementSimulator: - def __init__(self, pool_name): - self._flags = {"storwize_svc_volpool_name": pool_name} - self._volumes_list = {} - self._hosts_list = {} - self._mappings_list = {} - self._fcmappings_list = {} - self._next_cmd_error = { - "lsportip": "", - "lsnodecanister": "", - "mkvdisk": "", - "lsvdisk": "", - "lsfcmap": "", - "prestartfcmap": "", - "startfcmap": "", - "rmfcmap": "", - } - self._errors = { - "CMMVC5701E": ("", "CMMVC5701E No object ID was specified."), - "CMMVC6035E": ("", "CMMVC6035E The action failed as the " + - "object already exists."), - "CMMVC5753E": ("", "CMMVC5753E The specified object does not " + - "exist or is not a suitable candidate."), - "CMMVC5707E": ("", "CMMVC5707E Required parameters are missing."), - "CMMVC6581E": ("", "CMMVC6581E The command has failed because " + - "the maximum number of allowed iSCSI " + - "qualified names (IQNs) has been reached, " + - "or the IQN is already assigned or is not " + - "valid."), - "CMMVC5754E": ("", "CMMVC5754E The specified object does not " + - "exist, or the name supplied does not meet " + - "the naming rules."), - "CMMVC6071E": ("", "CMMVC6071E The VDisk-to-host mapping was " + - "not created because the VDisk is already " + - "mapped to a host."), - "CMMVC5879E": ("", "CMMVC5879E The VDisk-to-host mapping was " + - "not created because a VDisk is already " + - "mapped to this host with this SCSI LUN."), - "CMMVC5840E": ("", "CMMVC5840E The virtual disk (VDisk) was " + - "not deleted because it is mapped to a " + - "host or because it is part of a FlashCopy " + - "or Remote Copy mapping, or is involved in " + - "an image mode migrate."), - "CMMVC6527E": ("", "CMMVC6527E The name that you have entered " + - "is not valid. The name can contain letters, " + - "numbers, spaces, periods, dashes, and " + - "underscores. The name must begin with a " + - "letter or an underscore. The name must not " + - "begin or end with a space."), - "CMMVC5871E": ("", "CMMVC5871E The action failed because one or " + - "more of the configured port names is in a " + - "mapping."), - "CMMVC5924E": ("", "CMMVC5924E The FlashCopy mapping was not " + - "created because the source and target " + - "virtual disks (VDisks) are different sizes."), - "CMMVC6303E": ("", "CMMVC6303E The create failed because the " + - "source and target VDisks are the same."), - "CMMVC7050E": ("", "CMMVC7050E The command failed because at " + - "least one node in the I/O group does not " + - "support compressed VDisks."), - } - - # Find an unused ID - def _find_unused_id(self, d): - ids = [] - for k, v in d.iteritems(): - ids.append(int(v["id"])) - ids.sort() - for index, n in enumerate(ids): - if n > index: - return str(index) - return str(len(ids)) - - # Check if name is valid - def _is_invalid_name(self, name): - if (name[0] == " ") or (name[-1] == " "): - return True - for c in name: - if ((not c.isalnum()) and (c != " ") and (c != ".") - and (c != "-") and (c != "_")): - return True - return False - - # Convert argument string to dictionary - def _cmd_to_dict(self, cmd): - arg_list = cmd.split() - no_param_args = [ - "autodelete", - "autoexpand", - "bytes", - "compressed", - "force", - "nohdr", - ] - one_param_args = [ - "cleanrate", - "delim", - "filtervalue", - "grainsize", - "host", - "iogrp", - "iscsiname", - "mdiskgrp", - "name", - "rsize", - "scsi", - "size", - "source", - "target", - "unit", - "easytier", - "warning", - ] - - # Handle the special case of lsnode which is a two-word command - # Use the one word version of the command internally - if arg_list[0] == "svcinfo" and arg_list[1] == "lsnode": - ret = {"cmd": "lsnodecanister"} - arg_list.pop(0) - else: - ret = {"cmd": arg_list[0]} - - skip = False - for i in range(1, len(arg_list)): - if skip: - skip = False - continue - if arg_list[i][0] == "-": - if arg_list[i][1:] in no_param_args: - ret[arg_list[i][1:]] = True - elif arg_list[i][1:] in one_param_args: - ret[arg_list[i][1:]] = arg_list[i + 1] - skip = True - else: - raise exception.InvalidInput( - reason=_('unrecognized argument %s') % arg_list[i]) - else: - ret["obj"] = arg_list[i] - return ret - - # Generic function for printing information - def _print_info_cmd(self, rows, delim=" ", nohdr=False, **kwargs): - if nohdr: - del rows[0] - - for index in range(len(rows)): - rows[index] = delim.join(rows[index]) - return ("%s" % "\n".join(rows), "") - - # Print mostly made-up stuff in the correct syntax - def _cmd_lsmdiskgrp(self, **kwargs): - rows = [None] * 3 - rows[0] = ["id", "name", "status", "mdisk_count", - "vdisk_count capacity", "extent_size", "free_capacity", - "virtual_capacity", "used_capacity", "real_capacity", - "overallocation", "warning", "easy_tier", - "easy_tier_status"] - rows[1] = ["1", self._flags["storwize_svc_volpool_name"], "online", - "1", str(len(self._volumes_list)), "3.25TB", "256", - "3.21TB", "1.54TB", "264.97MB", "35.58GB", "47", "80", - "auto", "inactive"] - rows[2] = ["2", "volpool2", "online", - "1", "0", "3.25TB", "256", - "3.21TB", "1.54TB", "264.97MB", "35.58GB", "47", "80", - "auto", "inactive"] - return self._print_info_cmd(rows=rows, **kwargs) - - # Print mostly made-up stuff in the correct syntax - def _cmd_lsnodecanister(self, **kwargs): - rows = [None] * 3 - rows[0] = ["id", "name", "UPS_serial_number", "WWNN", "status", - "IO_group_id", "IO_group_name", "config_node", - "UPS_unique_id", "hardware", "iscsi_name", "iscsi_alias", - "panel_name", "enclosure_id", "canister_id", - "enclosure_serial_number"] - rows[1] = ["5", "node1", "", "123456789ABCDEF0", "online", "0", - "io_grp0", - "yes", "123456789ABCDEF0", "100", - "iqn.1982-01.com.ibm:1234.sim.node1", "", "01-1", "1", "1", - "0123ABC"] - rows[2] = ["6", "node2", "", "123456789ABCDEF1", "online", "0", - "io_grp0", - "no", "123456789ABCDEF1", "100", - "iqn.1982-01.com.ibm:1234.sim.node2", "", "01-2", "1", "2", - "0123ABC"] - - if self._next_cmd_error["lsnodecanister"] == "header_mismatch": - rows[0].pop(2) - self._next_cmd_error["lsnodecanister"] = "" - if self._next_cmd_error["lsnodecanister"] == "remove_field": - for row in rows: - row.pop(0) - self._next_cmd_error["lsnodecanister"] = "" - - return self._print_info_cmd(rows=rows, **kwargs) - - # Print mostly made-up stuff in the correct syntax - def _cmd_lsportip(self, **kwargs): - if self._next_cmd_error["lsportip"] == "ip_no_config": - self._next_cmd_error["lsportip"] = "" - ip_addr1 = "" - ip_addr2 = "" - gw = "" - else: - ip_addr1 = "1.234.56.78" - ip_addr2 = "1.234.56.79" - gw = "1.234.56.1" - - rows = [None] * 17 - rows[0] = ["id", "node_id", "node_name", "IP_address", "mask", - "gateway", "IP_address_6", "prefix_6", "gateway_6", "MAC", - "duplex", "state", "speed", "failover"] - rows[1] = ["1", "5", "node1", ip_addr1, "255.255.255.0", - gw, "", "", "", "01:23:45:67:89:00", "Full", - "online", "1Gb/s", "no"] - rows[2] = ["1", "5", "node1", "", "", "", "", "", "", - "01:23:45:67:89:00", "Full", "online", "1Gb/s", "yes"] - rows[3] = ["2", "5", "node1", "", "", "", "", "", "", - "01:23:45:67:89:01", "Full", "unconfigured", "1Gb/s", "no"] - rows[4] = ["2", "5", "node1", "", "", "", "", "", "", - "01:23:45:67:89:01", "Full", "unconfigured", "1Gb/s", "yes"] - rows[5] = ["3", "5", "node1", "", "", "", "", "", "", "", "", - "unconfigured", "", "no"] - rows[6] = ["3", "5", "node1", "", "", "", "", "", "", "", "", - "unconfigured", "", "yes"] - rows[7] = ["4", "5", "node1", "", "", "", "", "", "", "", "", - "unconfigured", "", "no"] - rows[8] = ["4", "5", "node1", "", "", "", "", "", "", "", "", - "unconfigured", "", "yes"] - rows[9] = ["1", "6", "node2", ip_addr2, "255.255.255.0", - gw, "", "", "", "01:23:45:67:89:02", "Full", - "online", "1Gb/s", "no"] - rows[10] = ["1", "6", "node2", "", "", "", "", "", "", - "01:23:45:67:89:02", "Full", "online", "1Gb/s", "yes"] - rows[11] = ["2", "6", "node2", "", "", "", "", "", "", - "01:23:45:67:89:03", "Full", "unconfigured", "1Gb/s", "no"] - rows[12] = ["2", "6", "node2", "", "", "", "", "", "", - "01:23:45:67:89:03", "Full", "unconfigured", "1Gb/s", - "yes"] - rows[13] = ["3", "6", "node2", "", "", "", "", "", "", "", "", - "unconfigured", "", "no"] - rows[14] = ["3", "6", "node2", "", "", "", "", "", "", "", "", - "unconfigured", "", "yes"] - rows[15] = ["4", "6", "node2", "", "", "", "", "", "", "", "", - "unconfigured", "", "no"] - rows[16] = ["4", "6", "node2", "", "", "", "", "", "", "", "", - "unconfigured", "", "yes"] - - if self._next_cmd_error["lsportip"] == "header_mismatch": - rows[0].pop(2) - self._next_cmd_error["lsportip"] = "" - if self._next_cmd_error["lsportip"] == "remove_field": - for row in rows: - row.pop(1) - self._next_cmd_error["lsportip"] = "" - - return self._print_info_cmd(rows=rows, **kwargs) - - # Create a vdisk - def _cmd_mkvdisk(self, **kwargs): - # We only save the id/uid, name, and size - all else will be made up - volume_info = {} - volume_info["id"] = self._find_unused_id(self._volumes_list) - volume_info["uid"] = ("ABCDEF" * 3) + ("0" * 14) + volume_info["id"] - - if "name" in kwargs: - volume_info["name"] = kwargs["name"].strip('\'\"') - else: - volume_info["name"] = "vdisk" + volume_info["id"] - - # Assume size and unit are given, store it in bytes - capacity = int(kwargs["size"]) - unit = kwargs["unit"] - - if unit == "b": - cap_bytes = capacity - elif unit == "kb": - cap_bytes = capacity * pow(1024, 1) - elif unit == "mb": - cap_bytes = capacity * pow(1024, 2) - elif unit == "gb": - cap_bytes = capacity * pow(1024, 3) - elif unit == "tb": - cap_bytes = capacity * pow(1024, 4) - elif unit == "pb": - cap_bytes = capacity * pow(1024, 5) - volume_info["cap_bytes"] = str(cap_bytes) - volume_info["capacity"] = str(capacity) + unit.upper() - - if "easytier" in kwargs: - if kwargs["easytier"] == "on": - volume_info["easy_tier"] = "on" - else: - volume_info["easy_tier"] = "off" - - if "rsize" in kwargs: - # Fake numbers - volume_info["used_capacity"] = "0.75MB" - volume_info["real_capacity"] = "36.98MB" - volume_info["free_capacity"] = "36.23MB" - volume_info["used_capacity_bytes"] = "786432" - volume_info["real_capacity_bytes"] = "38776340" - volume_info["free_capacity_bytes"] = "37989908" - if "warning" in kwargs: - volume_info["warning"] = kwargs["warning"].rstrip('%') - else: - volume_info["warning"] = "80" - if "autoexpand" in kwargs: - volume_info["autoexpand"] = "on" - else: - volume_info["autoexpand"] = "off" - if "grainsize" in kwargs: - volume_info["grainsize"] = kwargs["grainsize"] - else: - volume_info["grainsize"] = "32" - if "compressed" in kwargs: - if self._next_cmd_error["mkvdisk"] == "no_compression": - self._next_cmd_error["mkvdisk"] = "" - return self._errors["CMMVC7050E"] - volume_info["compressed_copy"] = "yes" - else: - volume_info["compressed_copy"] = "no" - else: - volume_info["used_capacity"] = volume_info["capacity"] - volume_info["real_capacity"] = volume_info["capacity"] - volume_info["free_capacity"] = "0.00MB" - volume_info["used_capacity_bytes"] = volume_info["cap_bytes"] - volume_info["real_capacity_bytes"] = volume_info["cap_bytes"] - volume_info["free_capacity_bytes"] = "0" - volume_info["warning"] = "" - volume_info["autoexpand"] = "" - volume_info["grainsize"] = "" - volume_info["compressed_copy"] = "no" - - if volume_info["name"] in self._volumes_list: - return self._errors["CMMVC6035E"] - else: - self._volumes_list[volume_info["name"]] = volume_info - return ("Virtual Disk, id [%s], successfully created" % - (volume_info["id"]), "") - - # Delete a vdisk - def _cmd_rmvdisk(self, **kwargs): - force = 0 - if "force" in kwargs: - force = 1 - - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - vol_name = kwargs["obj"].strip('\'\"') - - if not vol_name in self._volumes_list: - return self._errors["CMMVC5753E"] - - if force == 0: - for k, mapping in self._mappings_list.iteritems(): - if mapping["vol"] == vol_name: - return self._errors["CMMVC5840E"] - for k, fcmap in self._fcmappings_list.iteritems(): - if ((fcmap["source"] == vol_name) or - (fcmap["target"] == vol_name)): - return self._errors["CMMVC5840E"] - - del self._volumes_list[vol_name] - return ("", "") - - def _get_fcmap_info(self, vol_name): - ret_vals = { - "fc_id": "", - "fc_name": "", - "fc_map_count": "0", - } - for k, fcmap in self._fcmappings_list.iteritems(): - if ((fcmap["source"] == vol_name) or - (fcmap["target"] == vol_name)): - ret_vals["fc_id"] = fcmap["id"] - ret_vals["fc_name"] = fcmap["name"] - ret_vals["fc_map_count"] = "1" - return ret_vals - - # List information about vdisks - def _cmd_lsvdisk(self, **kwargs): - if "obj" not in kwargs: - rows = [] - rows.append(["id", "name", "IO_group_id", "IO_group_name", - "status", "mdisk_grp_id", "mdisk_grp_name", - "capacity", "type", "FC_id", "FC_name", "RC_id", - "RC_name", "vdisk_UID", "fc_map_count", "copy_count", - "fast_write_state", "se_copy_count", "RC_change"]) - - for k, vol in self._volumes_list.iteritems(): - if (("filtervalue" not in kwargs) or - (kwargs["filtervalue"] == "name=" + vol["name"])): - fcmap_info = self._get_fcmap_info(vol["name"]) - - if "bytes" in kwargs: - cap = vol["cap_bytes"] - else: - cap = vol["capacity"] - rows.append([str(vol["id"]), vol["name"], "0", "io_grp0", - "online", "0", - self._flags["storwize_svc_volpool_name"], - cap, "striped", - fcmap_info["fc_id"], fcmap_info["fc_name"], - "", "", vol["uid"], - fcmap_info["fc_map_count"], "1", "empty", - "1", "no"]) - - return self._print_info_cmd(rows=rows, **kwargs) - - else: - if kwargs["obj"] not in self._volumes_list: - return self._errors["CMMVC5754E"] - vol = self._volumes_list[kwargs["obj"]] - fcmap_info = self._get_fcmap_info(vol["name"]) - if "bytes" in kwargs: - cap = vol["cap_bytes"] - cap_u = vol["used_capacity_bytes"] - cap_r = vol["real_capacity_bytes"] - cap_f = vol["free_capacity_bytes"] - else: - cap = vol["capacity"] - cap_u = vol["used_capacity"] - cap_r = vol["real_capacity"] - cap_f = vol["free_capacity"] - rows = [] - - rows.append(["id", str(vol["id"])]) - rows.append(["name", vol["name"]]) - rows.append(["IO_group_id", "0"]) - rows.append(["IO_group_name", "io_grp0"]) - rows.append(["status", "online"]) - rows.append(["mdisk_grp_id", "0"]) - rows.append(["mdisk_grp_name", - self._flags["storwize_svc_volpool_name"]]) - rows.append(["capacity", cap]) - rows.append(["type", "striped"]) - rows.append(["formatted", "no"]) - rows.append(["mdisk_id", ""]) - rows.append(["mdisk_name", ""]) - rows.append(["FC_id", fcmap_info["fc_id"]]) - rows.append(["FC_name", fcmap_info["fc_name"]]) - rows.append(["RC_id", ""]) - rows.append(["RC_name", ""]) - rows.append(["vdisk_UID", vol["uid"]]) - rows.append(["throttling", "0"]) - - if self._next_cmd_error["lsvdisk"] == "blank_pref_node": - rows.append(["preferred_node_id", ""]) - self._next_cmd_error["lsvdisk"] = "" - elif self._next_cmd_error["lsvdisk"] == "no_pref_node": - self._next_cmd_error["lsvdisk"] = "" - else: - rows.append(["preferred_node_id", "6"]) - rows.append(["fast_write_state", "empty"]) - rows.append(["cache", "readwrite"]) - rows.append(["udid", ""]) - rows.append(["fc_map_count", fcmap_info["fc_map_count"]]) - rows.append(["sync_rate", "50"]) - rows.append(["copy_count", "1"]) - rows.append(["se_copy_count", "0"]) - rows.append(["mirror_write_priority", "latency"]) - rows.append(["RC_change", "no"]) - rows.append(["used_capacity", cap_u]) - rows.append(["real_capacity", cap_r]) - rows.append(["free_capacity", cap_f]) - rows.append(["autoexpand", vol["autoexpand"]]) - rows.append(["warning", vol["warning"]]) - rows.append(["grainsize", vol["grainsize"]]) - rows.append(["easy_tier", vol["easy_tier"]]) - rows.append(["compressed_copy", vol["compressed_copy"]]) - - if "nohdr" in kwargs: - for index in range(len(rows)): - rows[index] = " ".join(rows[index][1:]) - - if "delim" in kwargs: - for index in range(len(rows)): - rows[index] = kwargs["delim"].join(rows[index]) - - return ("%s" % "\n".join(rows), "") - - # Make a host - def _cmd_mkhost(self, **kwargs): - host_info = {} - host_info["id"] = self._find_unused_id(self._hosts_list) - - if "name" in kwargs: - host_name = kwargs["name"].strip('\'\"') - else: - host_name = "host" + str(host_info["id"]) - host_info["host_name"] = host_name - - if "iscsiname" not in kwargs: - return self._errors["CMMVC5707E"] - host_info["iscsi_name"] = kwargs["iscsiname"].strip('\'\"') - - if self._is_invalid_name(host_name): - return self._errors["CMMVC6527E"] - - if host_name in self._hosts_list: - return self._errors["CMMVC6035E"] - - for k, v in self._hosts_list.iteritems(): - if v["iscsi_name"] == host_info["iscsi_name"]: - return self._errors["CMMVC6581E"] - - self._hosts_list[host_name] = host_info - return ("Host, id [%s], successfully created" % - (host_info["id"]), "") - - # Remove a host - def _cmd_rmhost(self, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - - host_name = kwargs["obj"].strip('\'\"') - if host_name not in self._hosts_list: - return self._errors["CMMVC5753E"] - - for k, v in self._mappings_list.iteritems(): - if (v["host"] == host_name): - return self._errors["CMMVC5871E"] - - del self._hosts_list[host_name] - return ("", "") - - # List information about hosts - def _cmd_lshost(self, **kwargs): - if "obj" not in kwargs: - rows = [] - rows.append(["id", "name", "port_count", "iogrp_count", "status"]) - - found = False - for k, host in self._hosts_list.iteritems(): - filterstr = "name=" + host["host_name"] - if (("filtervalue" not in kwargs) or - (kwargs["filtervalue"] == filterstr)): - rows.append([host["id"], host["host_name"], "1", "4", - "offline"]) - found = True - if found: - return self._print_info_cmd(rows=rows, **kwargs) - else: - return ("", "") - else: - if kwargs["obj"] not in self._hosts_list: - return self._errors["CMMVC5754E"] - host = self._hosts_list[kwargs["obj"]] - rows = [] - rows.append(["id", host["id"]]) - rows.append(["name", host["host_name"]]) - rows.append(["port_count", "1"]) - rows.append(["type", "generic"]) - rows.append(["mask", "1111"]) - rows.append(["iogrp_count", "4"]) - rows.append(["status", "offline"]) - rows.append(["iscsi_name", host["iscsi_name"]]) - rows.append(["node_logged_in_count", "0"]) - rows.append(["state", "offline"]) - - if "nohdr" in kwargs: - for index in range(len(rows)): - rows[index] = " ".join(rows[index][1:]) - - if "delim" in kwargs: - for index in range(len(rows)): - rows[index] = kwargs["delim"].join(rows[index]) - - return ("%s" % "\n".join(rows), "") - - # Create a vdisk-host mapping - def _cmd_mkvdiskhostmap(self, **kwargs): - mapping_info = {} - mapping_info["id"] = self._find_unused_id(self._mappings_list) - - if "host" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["host"] = kwargs["host"].strip('\'\"') - - if "scsi" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["lun"] = kwargs["scsi"].strip('\'\"') - - if "obj" not in kwargs: - return self._errors["CMMVC5707E"] - mapping_info["vol"] = kwargs["obj"].strip('\'\"') - - if not mapping_info["vol"] in self._volumes_list: - return self._errors["CMMVC5753E"] - - if not mapping_info["host"] in self._hosts_list: - return self._errors["CMMVC5754E"] - - if mapping_info["vol"] in self._mappings_list: - return self._errors["CMMVC6071E"] - - for k, v in self._mappings_list.iteritems(): - if ((v["host"] == mapping_info["host"]) and - (v["lun"] == mapping_info["lun"])): - return self._errors["CMMVC5879E"] - - self._mappings_list[mapping_info["vol"]] = mapping_info - return ("Virtual Disk to Host map, id [%s], successfully created" - % (mapping_info["id"]), "") - - # Delete a vdisk-host mapping - def _cmd_rmvdiskhostmap(self, **kwargs): - if "host" not in kwargs: - return self._errors["CMMVC5707E"] - host = kwargs["host"].strip('\'\"') - - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - vol = kwargs["obj"].strip('\'\"') - - if not vol in self._mappings_list: - return self._errors["CMMVC5753E"] - - if self._mappings_list[vol]["host"] != host: - return self._errors["CMMVC5753E"] - - del self._mappings_list[vol] - return ("", "") - - # List information about vdisk-host mappings - def _cmd_lshostvdiskmap(self, **kwargs): - index = 1 - no_hdr = 0 - delimeter = "" - host_name = kwargs["obj"] - - if host_name not in self._hosts_list: - return self._errors["CMMVC5754E"] - - rows = [] - rows.append(["id", "name", "SCSI_id", "vdisk_id", "vdisk_name", - "vdisk_UID"]) - - for k, mapping in self._mappings_list.iteritems(): - if (host_name == "") or (mapping["host"] == host_name): - volume = self._volumes_list[mapping["vol"]] - rows.append([mapping["id"], mapping["host"], - mapping["lun"], volume["id"], - volume["name"], volume["uid"]]) - - return self._print_info_cmd(rows=rows, **kwargs) - - # Create a FlashCopy mapping - def _cmd_mkfcmap(self, **kwargs): - source = "" - target = "" - - if "source" not in kwargs: - return self._errors["CMMVC5707E"] - source = kwargs["source"].strip('\'\"') - if not source in self._volumes_list: - return self._errors["CMMVC5754E"] - - if "target" not in kwargs: - return self._errors["CMMVC5707E"] - target = kwargs["target"].strip('\'\"') - if not target in self._volumes_list: - return self._errors["CMMVC5754E"] - - if source == target: - return self._errors["CMMVC6303E"] - - if (self._volumes_list[source]["cap_bytes"] != - self._volumes_list[target]["cap_bytes"]): - return self._errors["CMMVC5924E"] - - fcmap_info = {} - fcmap_info["source"] = source - fcmap_info["target"] = target - fcmap_info["id"] = self._find_unused_id(self._fcmappings_list) - fcmap_info["name"] = "fcmap" + fcmap_info["id"] - fcmap_info["status"] = "idle_or_copied" - fcmap_info["progress"] = "0" - self._fcmappings_list[target] = fcmap_info - - return("FlashCopy Mapping, id [" + fcmap_info["id"] + - "], successfully created", "") - - # Same function used for both prestartfcmap and startfcmap - def _cmd_gen_startfcmap(self, mode, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - id_num = kwargs["obj"] - - if mode == "pre": - if self._next_cmd_error["prestartfcmap"] == "bad_id": - id_num = -1 - self._next_cmd_error["prestartfcmap"] = "" - else: - if self._next_cmd_error["startfcmap"] == "bad_id": - id_num = -1 - self._next_cmd_error["startfcmap"] = "" - - for k, fcmap in self._fcmappings_list.iteritems(): - if fcmap["id"] == id_num: - if mode == "pre": - fcmap["status"] = "preparing" - else: - fcmap["status"] = "copying" - fcmap["progress"] = "0" - return ("", "") - return self._errors["CMMVC5753E"] - - # Same function used for both stopfcmap and rmfcmap - # Assumes it is called with "-force <fc_map_id>" - def _cmd_stoprmfcmap(self, mode, **kwargs): - if "obj" not in kwargs: - return self._errors["CMMVC5701E"] - id_num = kwargs["obj"] - - if self._next_cmd_error["rmfcmap"] == "bad_id": - id_num = -1 - self._next_cmd_error["rmfcmap"] = "" - - to_delete = None - found = False - for k, fcmap in self._fcmappings_list.iteritems(): - if fcmap["id"] == id_num: - found = True - if mode == "rm": - to_delete = k - - if to_delete: - del self._fcmappings_list[to_delete] - - if found: - return ("", "") - else: - return self._errors["CMMVC5753E"] - - def _cmd_lsfcmap(self, **kwargs): - rows = [] - rows.append(["id", "name", "source_vdisk_id", "source_vdisk_name", - "target_vdisk_id", "target_vdisk_name", "group_id", - "group_name", "status", "progress", "copy_rate", - "clean_progress", "incremental", "partner_FC_id", - "partner_FC_name", "restoring", "start_time", - "rc_controlled"]) - - # Assume we always get a filtervalue argument - filter_key = kwargs["filtervalue"].split("=")[0] - filter_value = kwargs["filtervalue"].split("=")[1] - to_delete = [] - for k, v in self._fcmappings_list.iteritems(): - if str(v[filter_key]) == filter_value: - source = self._volumes_list[v["source"]] - target = self._volumes_list[v["target"]] - old_status = v["status"] - if old_status == "preparing": - new_status = "prepared" - if self._next_cmd_error["lsfcmap"] == "bogus_prepare": - new_status = "bogus" - elif (old_status == "copying") and (v["progress"] == "0"): - new_status = "copying" - v["progress"] = "50" - elif (old_status == "copying") and (v["progress"] == "50"): - new_status = "idle_or_copied" - to_delete.append(k) - else: - new_status = old_status - v["status"] = new_status - - if ((self._next_cmd_error["lsfcmap"] == "speed_up") or - (self._next_cmd_error["lsfcmap"] == "bogus_prepare")): - print_status = new_status - self._next_cmd_error["lsfcmap"] = "" - else: - print_status = old_status - - rows.append([v["id"], v["name"], source["id"], - source["name"], target["id"], target["name"], "", - "", print_status, v["progress"], "50", "100", - "off", "", "", "no", "", "no"]) - - for d in to_delete: - del self._fcmappings_list[k] - - return self._print_info_cmd(rows=rows, **kwargs) - - # The main function to run commands on the management simulator - def execute_command(self, cmd, check_exit_code=True): - try: - kwargs = self._cmd_to_dict(cmd) - except IndexError: - return self._errors["CMMVC5707E"] - - command = kwargs["cmd"] - del kwargs["cmd"] - arg_list = cmd.split() - - if command == "lsmdiskgrp": - out, err = self._cmd_lsmdiskgrp(**kwargs) - elif command == "lsnodecanister": - out, err = self._cmd_lsnodecanister(**kwargs) - elif command == "lsportip": - out, err = self._cmd_lsportip(**kwargs) - elif command == "mkvdisk": - out, err = self._cmd_mkvdisk(**kwargs) - elif command == "rmvdisk": - out, err = self._cmd_rmvdisk(**kwargs) - elif command == "lsvdisk": - out, err = self._cmd_lsvdisk(**kwargs) - elif command == "mkhost": - out, err = self._cmd_mkhost(**kwargs) - elif command == "rmhost": - out, err = self._cmd_rmhost(**kwargs) - elif command == "lshost": - out, err = self._cmd_lshost(**kwargs) - elif command == "mkvdiskhostmap": - out, err = self._cmd_mkvdiskhostmap(**kwargs) - elif command == "rmvdiskhostmap": - out, err = self._cmd_rmvdiskhostmap(**kwargs) - elif command == "lshostvdiskmap": - out, err = self._cmd_lshostvdiskmap(**kwargs) - elif command == "mkfcmap": - out, err = self._cmd_mkfcmap(**kwargs) - elif command == "prestartfcmap": - out, err = self._cmd_gen_startfcmap(mode="pre", **kwargs) - elif command == "startfcmap": - out, err = self._cmd_gen_startfcmap(mode="start", **kwargs) - elif command == "stopfcmap": - out, err = self._cmd_stoprmfcmap(mode="stop", **kwargs) - elif command == "rmfcmap": - out, err = self._cmd_stoprmfcmap(mode="rm", **kwargs) - elif command == "lsfcmap": - out, err = self._cmd_lsfcmap(**kwargs) - else: - out, err = ("", "ERROR: Unsupported command") - - if (check_exit_code) and (len(err) != 0): - raise exception.ProcessExecutionError(exit_code=1, - stdout=out, - stderr=err, - cmd=' '.join(cmd)) - - return (out, err) - - # After calling this function, the next call to the specified command will - # result in in the error specified - def error_injection(self, cmd, error): - self._next_cmd_error[cmd] = error - - -class StorwizeSVCFakeDriver(storwize_svc.StorwizeSVCDriver): - def set_fake_storage(self, fake): - self.fake_storage = fake - - def _run_ssh(self, cmd, check_exit_code=True): - try: - LOG.debug(_('Run CLI command: %s') % cmd) - ret = self.fake_storage.execute_command(cmd, check_exit_code) - (stdout, stderr) = ret - LOG.debug(_('CLI output:\n stdout: %(out)s\n stderr: %(err)s') % - {'out': stdout, 'err': stderr}) - - except exception.ProcessExecutionError as e: - with excutils.save_and_reraise_exception(): - LOG.debug(_('CLI Exception output:\n stdout: %(out)s\n ' - 'stderr: %(err)s') % {'out': e.stdout, - 'err': e.stderr}) - - return ret - - -class StorwizeSVCDriverTestCase(test.TestCase): - def setUp(self): - super(StorwizeSVCDriverTestCase, self).setUp() - self.USESIM = 1 - if self.USESIM == 1: - self.flags( - san_ip="hostname", - san_login="user", - san_password="pass", - storwize_svc_flashcopy_timeout="20", - ) - self.sim = StorwizeSVCManagementSimulator("volpool") - self.driver = StorwizeSVCFakeDriver() - self.driver.set_fake_storage(self.sim) - else: - self.flags( - san_ip="-1.-1.-1.-1", - san_login="user", - san_password="password", - storwize_svc_volpool_name="pool", - ) - self.driver = storwize_svc.StorwizeSVCDriver() - - self.driver.do_setup(None) - self.driver.check_for_setup_error() - - def test_storwize_svc_volume_tests(self): - self.flags(storwize_svc_vol_rsize="-1") - volume = {} - volume["name"] = "test1_volume%s" % random.randint(10000, 99999) - volume["size"] = 10 - volume["id"] = 1 - self.driver.create_volume(volume) - # Make sure that the volume has been created - is_volume_defined = self.driver._is_volume_defined(volume["name"]) - self.assertEqual(is_volume_defined, True) - self.driver.delete_volume(volume) - - if self.USESIM == 1: - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=True) - self.driver.create_volume(volume) - is_volume_defined = self.driver._is_volume_defined(volume["name"]) - self.assertEqual(is_volume_defined, True) - self.driver.delete_volume(volume) - FLAGS.reset() - - def test_storwize_svc_ip_connectivity(self): - # Check for missing san_ip - self.flags(san_ip=None) - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - - if self.USESIM != 1: - # Check for invalid ip - self.flags(san_ip="-1.-1.-1.-1") - self.assertRaises(socket.gaierror, - self.driver.check_for_setup_error) - - # Check for unreachable IP - self.flags(san_ip="1.1.1.1") - self.assertRaises(socket.error, - self.driver.check_for_setup_error) - - def test_storwize_svc_connectivity(self): - # Make sure we detect if the pool doesn't exist - no_exist_pool = "i-dont-exist-%s" % random.randint(10000, 99999) - self.flags(storwize_svc_volpool_name=no_exist_pool) - self.assertRaises(exception.InvalidInput, - self.driver.check_for_setup_error) - FLAGS.reset() - - # Check the case where the user didn't configure IP addresses - # as well as receiving unexpected results from the storage - if self.USESIM == 1: - self.sim.error_injection("lsnodecanister", "header_mismatch") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsnodecanister", "remove_field") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "ip_no_config") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "header_mismatch") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - self.sim.error_injection("lsportip", "remove_field") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.check_for_setup_error) - - # Check with bad parameters - self.flags(san_password=None) - self.flags(san_private_key=None) - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_vol_rsize="invalid") - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_vol_warning="invalid") - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_vol_autoexpand="invalid") - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_vol_grainsize=str(42)) - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_flashcopy_timeout=str(601)) - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - self.flags(storwize_svc_vol_compression=True) - self.flags(storwize_svc_vol_rsize="-1") - self.assertRaises(exception.InvalidInput, - self.driver._check_flags) - FLAGS.reset() - - # Finally, check with good parameters - self.driver.check_for_setup_error() - - def test_storwize_svc_flashcopy(self): - volume1 = {} - volume1["name"] = "test1_volume%s" % random.randint(10000, 99999) - volume1["size"] = 10 - volume1["id"] = 10 - self.driver.create_volume(volume1) - - snapshot = {} - snapshot["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot["volume_name"] = volume1["name"] - - # Test timeout and volume cleanup - self.flags(storwize_svc_flashcopy_timeout=str(1)) - self.assertRaises(exception.InvalidSnapshot, - self.driver.create_snapshot, snapshot) - is_volume_defined = self.driver._is_volume_defined(snapshot["name"]) - self.assertEqual(is_volume_defined, False) - FLAGS.reset() - - # Test bogus statuses - if self.USESIM == 1: - self.sim.error_injection("lsfcmap", "bogus_prepare") - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_snapshot, snapshot) - - # Test prestartfcmap, startfcmap, and rmfcmap failing - if self.USESIM == 1: - self.sim.error_injection("prestartfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) - self.sim.error_injection("lsfcmap", "speed_up") - self.sim.error_injection("startfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) - self.sim.error_injection("prestartfcmap", "bad_id") - self.sim.error_injection("rmfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_snapshot, snapshot) - - # Test successful snapshot - self.driver.create_snapshot(snapshot) - - # Ensure snapshot is defined - is_volume_defined = self.driver._is_volume_defined(snapshot["name"]) - self.assertEqual(is_volume_defined, True) - - # Try to create a snapshot from an non-existing volume - should fail - snapshot2 = {} - snapshot2["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot2["volume_name"] = "undefined-vol" - self.assertRaises(exception.VolumeNotFound, - self.driver.create_snapshot, - snapshot2) - - # Create volume from snapshot - volume2 = {} - volume2["name"] = "snap2vol_volume%s" % random.randint(10000, 99999) - - # Create volume from snapshot into an existsing volume - self.assertRaises(exception.InvalidSnapshot, - self.driver.create_volume_from_snapshot, - volume1, - snapshot) - - # Try to create a volume from a non-existing snapshot - self.assertRaises(exception.SnapshotNotFound, - self.driver.create_volume_from_snapshot, - volume2, - snapshot2) - - # Fail the snapshot - if self.USESIM == 1: - self.sim.error_injection("prestartfcmap", "bad_id") - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_volume_from_snapshot, volume2, snapshot) - - # Succeed - if self.USESIM == 1: - self.sim.error_injection("lsfcmap", "speed_up") - self.driver.create_volume_from_snapshot(volume2, snapshot) - - # Ensure volume is defined - is_volume_defined = self.driver._is_volume_defined(volume2["name"]) - self.assertEqual(is_volume_defined, True) - - self.driver._delete_volume(volume2, True) - self.driver._delete_snapshot(snapshot, True) - - # Check with target with different size - volume3 = {} - volume3["name"] = "test3_volume%s" % random.randint(10000, 99999) - volume3["size"] = 11 - volume3["id"] = 11 - self.driver.create_volume(volume3) - snapshot["name"] = volume3["name"] - self.assertRaises(exception.InvalidSnapshot, - self.driver.create_snapshot, snapshot) - self.driver._delete_volume(volume1, True) - self.driver._delete_volume(volume3, True) - - # Snapshot volume that doesn't exist - snapshot = {} - snapshot["name"] = "snap_volume%s" % random.randint(10000, 99999) - snapshot["volume_name"] = "no_exist" - self.assertRaises(exception.VolumeNotFound, - self.driver.create_snapshot, snapshot) - - def test_storwize_svc_volumes(self): - # Create a first volume - volume = {} - volume["name"] = "test1_volume%s" % random.randint(10000, 99999) - volume["size"] = 10 - volume["id"] = 1 - - self.driver.create_volume(volume) - - self.driver.ensure_export(None, volume) - - # Do nothing - self.driver.create_export(None, volume) - self.driver.remove_export(None, volume) - self.assertRaises(NotImplementedError, - self.driver.check_for_export, None, volume["id"]) - - # Make sure volume attributes are as they should be - attributes = self.driver._get_volume_attributes(volume["name"]) - attr_size = float(attributes["capacity"]) / 1073741824 # bytes to GB - self.assertEqual(attr_size, float(volume["size"])) - pool = storwize_svc.FLAGS.storwize_svc_volpool_name - self.assertEqual(attributes["mdisk_grp_name"], pool) - - # Try to create the volume again (should fail) - self.assertRaises(exception.ProcessExecutionError, - self.driver.create_volume, volume) - - # Try to delete a volume that doesn't exist (should not fail) - vol_no_exist = {"name": "i_dont_exist"} - self.driver.delete_volume(vol_no_exist) - # Ensure export for volume that doesn't exist (should not fail) - self.driver.ensure_export(None, vol_no_exist) - - # Delete the volume - self.driver.delete_volume(volume) - - def _create_test_vol(self): - volume = {} - volume["name"] = "testparam_volume%s" % random.randint(10000, 99999) - volume["size"] = 1 - volume["id"] = 1 - self.driver.create_volume(volume) - - attrs = self.driver._get_volume_attributes(volume["name"]) - self.driver.delete_volume(volume) - return attrs - - def test_storwize_svc_volume_params(self): - # Option test matrix - # Option Value Covered by test # - # rsize -1 1 - # rsize 2% 2,3 - # warning 0 2 - # warning 80% 3 - # autoexpand True 2 - # autoexpand False 3 - # grainsize 32 2 - # grainsize 256 3 - # compression True 4 - # compression False 2,3 - # easytier True 1,3 - # easytier False 2 - - # Test 1 - self.flags(storwize_svc_vol_rsize="-1") - self.flags(storwize_svc_vol_easytier=True) - attrs = self._create_test_vol() - self.assertEquals(attrs["free_capacity"], "0") - self.assertEquals(attrs["easy_tier"], "on") - FLAGS.reset() - - # Test 2 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=False) - self.flags(storwize_svc_vol_warning="0") - self.flags(storwize_svc_vol_autoexpand=True) - self.flags(storwize_svc_vol_grainsize="32") - self.flags(storwize_svc_vol_easytier=False) - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "no") - self.assertEquals(attrs["warning"], "0") - self.assertEquals(attrs["autoexpand"], "on") - self.assertEquals(attrs["grainsize"], "32") - self.assertEquals(attrs["easy_tier"], "off") - FLAGS.reset() - - # Test 3 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=False) - self.flags(storwize_svc_vol_warning="80%") - self.flags(storwize_svc_vol_autoexpand=False) - self.flags(storwize_svc_vol_grainsize="256") - self.flags(storwize_svc_vol_easytier=True) - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "no") - self.assertEquals(attrs["warning"], "80") - self.assertEquals(attrs["autoexpand"], "off") - self.assertEquals(attrs["grainsize"], "256") - self.assertEquals(attrs["easy_tier"], "on") - FLAGS.reset() - - # Test 4 - self.flags(storwize_svc_vol_rsize="2%") - self.flags(storwize_svc_vol_compression=True) - try: - attrs = self._create_test_vol() - self.assertNotEqual(attrs["capacity"], attrs["real_capacity"]) - self.assertEquals(attrs["compressed_copy"], "yes") - except exception.ProcessExecutionError as e: - if "CMMVC7050E" not in e.stderr: - raise exception.ProcessExecutionError(exit_code=e.exit_code, - stdout=e.stdout, - stderr=e.stderr, - cmd=e.cmd) - if self.USESIM == 1: - self.sim.error_injection("mkvdisk", "no_compression") - self.assertRaises(exception.ProcessExecutionError, - self._create_test_vol) - FLAGS.reset() - - def test_storwize_svc_unicode_host_and_volume_names(self): - volume1 = {} - volume1["name"] = u"unicode1_volume%s" % random.randint(10000, 99999) - volume1["size"] = 2 - volume1["id"] = 1 - self.driver.create_volume(volume1) - # Make sure that the volumes have been created - is_volume_defined = self.driver._is_volume_defined(volume1["name"]) - self.assertEqual(is_volume_defined, True) - conn = {} - conn["initiator"] = u"unicode:init:%s" % random.randint(10000, 99999) - conn["ip"] = "10.10.10.10" # Bogus ip for testing - self.driver.initialize_connection(volume1, conn) - self.driver.terminate_connection(volume1, conn) - self.driver.delete_volume(volume1) - - def test_storwize_svc_host_maps(self): - # Create two volumes to be used in mappings - volume1 = {} - volume1["name"] = "test1_volume%s" % random.randint(10000, 99999) - volume1["size"] = 2 - volume1["id"] = 1 - self.driver.create_volume(volume1) - volume2 = {} - volume2["name"] = "test2_volume%s" % random.randint(10000, 99999) - volume2["size"] = 2 - volume2["id"] = 1 - self.driver.create_volume(volume2) - - # Check case where no hosts exist - if self.USESIM == 1: - ret = self.driver._get_host_from_iscsiname("foo") - self.assertEquals(ret, None) - ret = self.driver._is_host_defined("foo") - self.assertEquals(ret, False) - - # Make sure that the volumes have been created - is_volume_defined = self.driver._is_volume_defined(volume1["name"]) - self.assertEqual(is_volume_defined, True) - is_volume_defined = self.driver._is_volume_defined(volume2["name"]) - self.assertEqual(is_volume_defined, True) - - # Initialize connection from the first volume to a host - # Add some characters to the initiator name that should be converted - # when used for the host name - conn = {} - conn["initiator"] = "test:init:%s" % random.randint(10000, 99999) - conn["ip"] = "10.10.10.10" # Bogus ip for testing - self.driver.initialize_connection(volume1, conn) - - # Initialize again, should notice it and do nothing - self.driver.initialize_connection(volume1, conn) - - # Try to delete the 1st volume (should fail because it is mapped) - self.assertRaises(exception.ProcessExecutionError, - self.driver.delete_volume, volume1) - - # Test no preferred node - self.driver.terminate_connection(volume1, conn) - if self.USESIM == 1: - self.sim.error_injection("lsvdisk", "no_pref_node") - self.driver.initialize_connection(volume1, conn) - - # Initialize connection from the second volume to the host with no - # preferred node set if in simulation mode, otherwise, just - # another initialize connection. - if self.USESIM == 1: - self.sim.error_injection("lsvdisk", "blank_pref_node") - self.driver.initialize_connection(volume2, conn) - - # Try to remove connection from host that doesn't exist (should fail) - conn_no_exist = {"initiator": "i_dont_exist"} - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.terminate_connection, volume1, conn_no_exist) - - # Try to remove connection from volume that isn't mapped (should print - # message but NOT fail) - vol_no_exist = {"name": "i_dont_exist"} - self.driver.terminate_connection(vol_no_exist, conn) - - # Remove the mapping from the 1st volume and delete it - self.driver.terminate_connection(volume1, conn) - self.driver.delete_volume(volume1) - vol_def = self.driver._is_volume_defined(volume1["name"]) - self.assertEqual(vol_def, False) - - # Make sure our host still exists - host_name = self.driver._get_host_from_iscsiname(conn["initiator"]) - host_def = self.driver._is_host_defined(host_name) - self.assertEquals(host_def, True) - - # Remove the mapping from the 2nd volume and delete it. The host should - # be automatically removed because there are no more mappings. - self.driver.terminate_connection(volume2, conn) - self.driver.delete_volume(volume2) - vol_def = self.driver._is_volume_defined(volume2["name"]) - self.assertEqual(vol_def, False) - - # Check if our host still exists (it should not) - ret = self.driver._get_host_from_iscsiname(conn["initiator"]) - self.assertEquals(ret, None) - ret = self.driver._is_host_defined(host_name) - self.assertEquals(ret, False) diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py deleted file mode 100644 index 7b14d8a99..000000000 --- a/nova/tests/test_volume.py +++ /dev/null @@ -1,931 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. -""" -Tests for Volume Code. - -""" - -import cStringIO -import datetime - -import mox -import os -import shutil -import tempfile - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova.openstack.common import importutils -from nova.openstack.common.notifier import api as notifier_api -from nova.openstack.common.notifier import test_notifier -from nova.openstack.common import rpc -import nova.policy -from nova import quota -from nova import test -from nova.tests.image import fake as fake_image -import nova.volume.api -from nova.volume import iscsi - -QUOTAS = quota.QUOTAS - - -FLAGS = flags.FLAGS - - -class VolumeTestCase(test.TestCase): - """Test Case for volumes.""" - - def setUp(self): - super(VolumeTestCase, self).setUp() - self.compute = importutils.import_object(FLAGS.compute_manager) - vol_tmpdir = tempfile.mkdtemp() - self.flags(compute_driver='nova.virt.fake.FakeDriver', - volumes_dir=vol_tmpdir, - notification_driver=[test_notifier.__name__]) - self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) - self.volume = importutils.import_object(FLAGS.volume_manager) - self.context = context.get_admin_context() - instance = db.instance_create(self.context, {}) - self.instance_id = instance['id'] - self.instance_uuid = instance['uuid'] - test_notifier.NOTIFICATIONS = [] - fake_image.stub_out_image_service(self.stubs) - - def tearDown(self): - try: - shutil.rmtree(FLAGS.volumes_dir) - except OSError: - pass - db.instance_destroy(self.context, self.instance_uuid) - notifier_api._reset_drivers() - super(VolumeTestCase, self).tearDown() - - def fake_get_target(obj, iqn): - return 1 - - @staticmethod - def _create_volume(size=0, snapshot_id=None, image_id=None, metadata=None): - """Create a volume object.""" - vol = {} - vol['size'] = size - vol['snapshot_id'] = snapshot_id - vol['image_id'] = image_id - vol['user_id'] = 'fake' - vol['project_id'] = 'fake' - vol['availability_zone'] = FLAGS.storage_availability_zone - vol['status'] = "creating" - vol['attach_status'] = "detached" - if metadata is not None: - vol['metadata'] = metadata - return db.volume_create(context.get_admin_context(), vol) - - def test_ec2_uuid_mapping(self): - ec2_vol = db.ec2_volume_create(context.get_admin_context(), - 'aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaaaaa', 5) - self.assertEqual(5, ec2_vol['id']) - self.assertEqual('aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaaaaa', - db.get_volume_uuid_by_ec2_id(context.get_admin_context(), 5)) - - ec2_vol = db.ec2_volume_create(context.get_admin_context(), - 'aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaaaaa', 1) - self.assertEqual(1, ec2_vol['id']) - - ec2_vol = db.ec2_volume_create(context.get_admin_context(), - 'aaaaaaaa-bbbb-bbbb-bbbb-aaaaaaaaazzz') - self.assertEqual(6, ec2_vol['id']) - - def test_create_delete_volume(self): - """Test volume can be created and deleted.""" - # Need to stub out reserve, commit, and rollback - def fake_reserve(context, expire=None, **deltas): - return ["RESERVATION"] - - def fake_commit(context, reservations): - pass - - def fake_rollback(context, reservations): - pass - - self.stubs.Set(QUOTAS, "reserve", fake_reserve) - self.stubs.Set(QUOTAS, "commit", fake_commit) - self.stubs.Set(QUOTAS, "rollback", fake_rollback) - - volume = self._create_volume() - volume_id = volume['id'] - self.assertEquals(len(test_notifier.NOTIFICATIONS), 0) - self.volume.create_volume(self.context, volume_id) - self.assertEquals(len(test_notifier.NOTIFICATIONS), 2) - self.assertEqual(volume_id, db.volume_get(context.get_admin_context(), - volume_id).id) - - self.volume.delete_volume(self.context, volume_id) - self.assertEquals(len(test_notifier.NOTIFICATIONS), 4) - self.assertRaises(exception.NotFound, - db.volume_get, - self.context, - volume_id) - - def test_create_delete_volume_with_metadata(self): - """Test volume can be created and deleted.""" - test_meta = {'fake_key': 'fake_value'} - volume = self._create_volume('0', None, metadata=test_meta) - volume_id = volume['id'] - self.volume.create_volume(self.context, volume_id) - result_meta = { - volume.volume_metadata[0].key: volume.volume_metadata[0].value} - self.assertEqual(result_meta, test_meta) - - self.volume.delete_volume(self.context, volume_id) - self.assertRaises(exception.NotFound, - db.volume_get, - self.context, - volume_id) - - def _do_test_create_over_quota(self, resource, expected): - """Test volume creation over quota.""" - - def fake_reserve(context, **deltas): - kwargs = dict(overs=[resource], - quotas=dict(gigabytes=1000, volumes=10), - usages=dict(gigabytes=dict(reserved=1, in_use=999), - volumes=dict(reserved=1, in_use=9))) - raise exception.OverQuota(**kwargs) - - def fake_commit(context, reservations): - self.fail('should not commit over quota') - - self.stubs.Set(QUOTAS, 'reserve', fake_reserve) - self.stubs.Set(QUOTAS, 'commit', fake_commit) - - volume_api = nova.volume.api.API() - - self.assertRaises(expected, - volume_api.create, - self.context, - 2, - 'name', - 'description') - - def test_create_volumes_over_quota(self): - self._do_test_create_over_quota('volumes', - exception.VolumeLimitExceeded) - - def test_create_gigabytes_over_quota(self): - self._do_test_create_over_quota('gigabytes', - exception.VolumeSizeTooLarge) - - def test_delete_busy_volume(self): - """Test volume survives deletion if driver reports it as busy.""" - volume = self._create_volume() - volume_id = volume['id'] - self.volume.create_volume(self.context, volume_id) - - self.mox.StubOutWithMock(self.volume.driver, 'delete_volume') - self.volume.driver.delete_volume(mox.IgnoreArg()).AndRaise( - exception.VolumeIsBusy) - self.mox.ReplayAll() - res = self.volume.delete_volume(self.context, volume_id) - self.assertEqual(True, res) - volume_ref = db.volume_get(context.get_admin_context(), volume_id) - self.assertEqual(volume_id, volume_ref.id) - self.assertEqual("available", volume_ref.status) - - self.mox.UnsetStubs() - self.volume.delete_volume(self.context, volume_id) - - def test_create_volume_from_snapshot(self): - """Test volume can be created from a snapshot.""" - volume_src = self._create_volume() - self.volume.create_volume(self.context, volume_src['id']) - snapshot_id = self._create_snapshot(volume_src['id']) - self.volume.create_snapshot(self.context, volume_src['id'], - snapshot_id) - volume_dst = self._create_volume(0, snapshot_id) - self.volume.create_volume(self.context, volume_dst['id'], snapshot_id) - self.assertEqual(volume_dst['id'], - db.volume_get( - context.get_admin_context(), - volume_dst['id']).id) - self.assertEqual(snapshot_id, db.volume_get( - context.get_admin_context(), - volume_dst['id']).snapshot_id) - - self.volume.delete_volume(self.context, volume_dst['id']) - self.volume.delete_snapshot(self.context, snapshot_id) - self.volume.delete_volume(self.context, volume_src['id']) - - def test_too_big_volume(self): - """Ensure failure if a too large of a volume is requested.""" - # FIXME(vish): validation needs to move into the data layer in - # volume_create - return True - try: - volume = self._create_volume('1001') - self.volume.create_volume(self.context, volume) - self.fail("Should have thrown TypeError") - except TypeError: - pass - - def test_run_attach_detach_volume(self): - """Make sure volume can be attached and detached from instance.""" - inst = {} - inst['image_id'] = 1 - inst['reservation_id'] = 'r-fakeres' - inst['launch_time'] = '10' - inst['user_id'] = 'fake' - inst['project_id'] = 'fake' - inst['instance_type_id'] = '2' # m1.tiny - inst['ami_launch_index'] = 0 - instance = db.instance_create(self.context, {}) - instance_id = instance['id'] - instance_uuid = instance['uuid'] - mountpoint = "/dev/sdf" - volume = self._create_volume() - volume_id = volume['id'] - self.volume.create_volume(self.context, volume_id) - if FLAGS.fake_tests: - db.volume_attached(self.context, volume_id, instance_uuid, - mountpoint) - else: - self.compute.attach_volume(self.context, - instance_uuid, - volume_id, - mountpoint) - vol = db.volume_get(context.get_admin_context(), volume_id) - self.assertEqual(vol['status'], "in-use") - self.assertEqual(vol['attach_status'], "attached") - self.assertEqual(vol['mountpoint'], mountpoint) - self.assertEqual(vol['instance_uuid'], instance_uuid) - self.assertNotEqual(vol['attach_time'], None) - - self.assertRaises(exception.VolumeAttached, - self.volume.delete_volume, - self.context, - volume_id) - if FLAGS.fake_tests: - db.volume_detached(self.context, volume_id) - else: - self.compute.detach_volume(self.context, - instance_uuid, - volume_id) - vol = db.volume_get(self.context, volume_id) - self.assertEqual(vol['status'], "available") - self.assertEqual(vol['attach_time'], None) - - self.volume.delete_volume(self.context, volume_id) - self.assertRaises(exception.VolumeNotFound, - db.volume_get, - self.context, - volume_id) - db.instance_destroy(self.context, instance_uuid) - - def test_concurrent_volumes_get_different_targets(self): - """Ensure multiple concurrent volumes get different targets.""" - volume_ids = [] - targets = [] - - def _check(volume_id): - """Make sure targets aren't duplicated.""" - volume_ids.append(volume_id) - admin_context = context.get_admin_context() - iscsi_target = db.volume_get_iscsi_target_num(admin_context, - volume_id) - self.assert_(iscsi_target not in targets) - targets.append(iscsi_target) - - total_slots = FLAGS.iscsi_num_targets - for _index in xrange(total_slots): - self._create_volume() - for volume_id in volume_ids: - self.volume.delete_volume(self.context, volume_id) - - def test_multi_node(self): - # TODO(termie): Figure out how to test with two nodes, - # each of them having a different FLAG for storage_node - # This will allow us to test cross-node interactions - pass - - @staticmethod - def _create_snapshot(volume_id, size='0'): - """Create a snapshot object.""" - snap = {} - snap['volume_size'] = size - snap['user_id'] = 'fake' - snap['project_id'] = 'fake' - snap['volume_id'] = volume_id - snap['status'] = "creating" - return db.snapshot_create(context.get_admin_context(), snap)['id'] - - def test_create_delete_snapshot(self): - """Test snapshot can be created and deleted.""" - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - snapshot_id = self._create_snapshot(volume['id']) - self.volume.create_snapshot(self.context, volume['id'], snapshot_id) - self.assertEqual(snapshot_id, - db.snapshot_get(context.get_admin_context(), - snapshot_id).id) - - self.volume.delete_snapshot(self.context, snapshot_id) - self.assertRaises(exception.NotFound, - db.snapshot_get, - self.context, - snapshot_id) - self.volume.delete_volume(self.context, volume['id']) - - def test_cant_delete_volume_in_use(self): - """Test volume can't be deleted in invalid stats.""" - # create a volume and assign to host - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - volume['status'] = 'in-use' - volume['host'] = 'fakehost' - - volume_api = nova.volume.api.API() - - # 'in-use' status raises InvalidVolume - self.assertRaises(exception.InvalidVolume, - volume_api.delete, - self.context, - volume) - - # clean up - self.volume.delete_volume(self.context, volume['id']) - - def test_force_delete_volume(self): - """Test volume can be forced to delete.""" - # create a volume and assign to host - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - volume['status'] = 'error_deleting' - volume['host'] = 'fakehost' - - volume_api = nova.volume.api.API() - - # 'error_deleting' volumes can't be deleted - self.assertRaises(exception.InvalidVolume, - volume_api.delete, - self.context, - volume) - - # delete with force - volume_api.delete(self.context, volume, force=True) - - # status is deleting - volume = db.volume_get(context.get_admin_context(), volume['id']) - self.assertEquals(volume['status'], 'deleting') - - # clean up - self.volume.delete_volume(self.context, volume['id']) - - def test_cant_delete_volume_with_snapshots(self): - """Test snapshot can be created and deleted.""" - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - snapshot_id = self._create_snapshot(volume['id']) - self.volume.create_snapshot(self.context, volume['id'], snapshot_id) - self.assertEqual(snapshot_id, - db.snapshot_get(context.get_admin_context(), - snapshot_id).id) - - volume['status'] = 'available' - volume['host'] = 'fakehost' - - volume_api = nova.volume.api.API() - - self.assertRaises(exception.InvalidVolume, - volume_api.delete, - self.context, - volume) - self.volume.delete_snapshot(self.context, snapshot_id) - self.volume.delete_volume(self.context, volume['id']) - - def test_can_delete_errored_snapshot(self): - """Test snapshot can be created and deleted.""" - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - snapshot_id = self._create_snapshot(volume['id']) - self.volume.create_snapshot(self.context, volume['id'], snapshot_id) - snapshot = db.snapshot_get(context.get_admin_context(), - snapshot_id) - - volume_api = nova.volume.api.API() - - snapshot['status'] = 'badstatus' - self.assertRaises(exception.InvalidVolume, - volume_api.delete_snapshot, - self.context, - snapshot) - - snapshot['status'] = 'error' - self.volume.delete_snapshot(self.context, snapshot_id) - self.volume.delete_volume(self.context, volume['id']) - - def test_create_snapshot_force(self): - """Test snapshot in use can be created forcibly.""" - - def fake_cast(ctxt, topic, msg): - pass - self.stubs.Set(rpc, 'cast', fake_cast) - - volume = self._create_volume() - self.volume.create_volume(self.context, volume['id']) - db.volume_attached(self.context, volume['id'], self.instance_uuid, - '/dev/sda1') - - volume_api = nova.volume.api.API() - volume = volume_api.get(self.context, volume['id']) - self.assertRaises(exception.InvalidVolume, - volume_api.create_snapshot, - self.context, volume, - 'fake_name', 'fake_description') - snapshot_ref = volume_api.create_snapshot_force(self.context, - volume, - 'fake_name', - 'fake_description') - db.snapshot_destroy(self.context, snapshot_ref['id']) - db.volume_destroy(self.context, volume['id']) - - def test_delete_busy_snapshot(self): - """Test snapshot can be created and deleted.""" - volume = self._create_volume() - volume_id = volume['id'] - self.volume.create_volume(self.context, volume_id) - snapshot_id = self._create_snapshot(volume_id) - self.volume.create_snapshot(self.context, volume_id, snapshot_id) - - self.mox.StubOutWithMock(self.volume.driver, 'delete_snapshot') - self.volume.driver.delete_snapshot(mox.IgnoreArg()).AndRaise( - exception.SnapshotIsBusy) - self.mox.ReplayAll() - self.volume.delete_snapshot(self.context, snapshot_id) - snapshot_ref = db.snapshot_get(self.context, snapshot_id) - self.assertEqual(snapshot_id, snapshot_ref.id) - self.assertEqual("available", snapshot_ref.status) - - self.mox.UnsetStubs() - self.volume.delete_snapshot(self.context, snapshot_id) - self.volume.delete_volume(self.context, volume_id) - - def test_create_volume_usage_notification(self): - """Ensure create volume generates appropriate usage notification""" - volume = self._create_volume() - volume_id = volume['id'] - self.assertEquals(len(test_notifier.NOTIFICATIONS), 0) - self.volume.create_volume(self.context, volume_id) - self.assertEquals(len(test_notifier.NOTIFICATIONS), 2) - msg = test_notifier.NOTIFICATIONS[0] - self.assertEquals(msg['event_type'], 'volume.create.start') - payload = msg['payload'] - self.assertEquals(payload['status'], 'creating') - msg = test_notifier.NOTIFICATIONS[1] - self.assertEquals(msg['priority'], 'INFO') - self.assertEquals(msg['event_type'], 'volume.create.end') - payload = msg['payload'] - self.assertEquals(payload['tenant_id'], volume['project_id']) - self.assertEquals(payload['user_id'], volume['user_id']) - self.assertEquals(payload['volume_id'], volume['id']) - self.assertEquals(payload['status'], 'available') - self.assertEquals(payload['size'], volume['size']) - self.assertTrue('display_name' in payload) - self.assertTrue('snapshot_id' in payload) - self.assertTrue('launched_at' in payload) - self.assertTrue('created_at' in payload) - self.volume.delete_volume(self.context, volume_id) - - def _do_test_create_volume_with_size(self, size): - def fake_reserve(context, expire=None, **deltas): - return ["RESERVATION"] - - def fake_commit(context, reservations): - pass - - def fake_rollback(context, reservations): - pass - - self.stubs.Set(QUOTAS, "reserve", fake_reserve) - self.stubs.Set(QUOTAS, "commit", fake_commit) - self.stubs.Set(QUOTAS, "rollback", fake_rollback) - - volume_api = nova.volume.api.API() - - volume = volume_api.create(self.context, - size, - 'name', - 'description') - self.assertEquals(volume['size'], int(size)) - - def test_create_volume_int_size(self): - """Test volume creation with int size.""" - self._do_test_create_volume_with_size(2) - - def test_create_volume_string_size(self): - """Test volume creation with string size.""" - self._do_test_create_volume_with_size('2') - - def test_create_volume_with_bad_size(self): - def fake_reserve(context, expire=None, **deltas): - return ["RESERVATION"] - - def fake_commit(context, reservations): - pass - - def fake_rollback(context, reservations): - pass - - self.stubs.Set(QUOTAS, "reserve", fake_reserve) - self.stubs.Set(QUOTAS, "commit", fake_commit) - self.stubs.Set(QUOTAS, "rollback", fake_rollback) - - volume_api = nova.volume.api.API() - - self.assertRaises(exception.InvalidInput, - volume_api.create, - self.context, - '2Gb', - 'name', - 'description') - - def test_begin_roll_detaching_volume(self): - """Test begin_detaching and roll_detaching functions.""" - volume = self._create_volume() - volume_api = nova.volume.api.API() - volume_api.begin_detaching(self.context, volume) - volume = db.volume_get(self.context, volume['id']) - self.assertEqual(volume['status'], "detaching") - volume_api.roll_detaching(self.context, volume) - volume = db.volume_get(self.context, volume['id']) - self.assertEqual(volume['status'], "in-use") - - def _create_volume_from_image(self, expected_status, - fakeout_copy_image_to_volume=False): - """Call copy image to volume, Test the status of volume after calling - copying image to volume.""" - def fake_local_path(volume): - return dst_path - - def fake_copy_image_to_volume(context, volume, image_id): - pass - - dst_fd, dst_path = tempfile.mkstemp() - os.close(dst_fd) - self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) - if fakeout_copy_image_to_volume: - self.stubs.Set(self.volume, '_copy_image_to_volume', - fake_copy_image_to_volume) - - image_id = 'c905cedb-7281-47e4-8a62-f26bc5fc4c77' - volume_id = 1 - # creating volume testdata - db.volume_create(self.context, {'id': volume_id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'creating', - 'instance_uuid': None, - 'host': 'dummy'}) - try: - self.volume.create_volume(self.context, - volume_id, - image_id=image_id) - - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], expected_status) - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - os.unlink(dst_path) - - def test_create_volume_from_image_status_downloading(self): - """Verify that before copying image to volume, it is in downloading - state.""" - self._create_volume_from_image('downloading', True) - - def test_create_volume_from_image_status_available(self): - """Verify that before copying image to volume, it is in available - state.""" - self._create_volume_from_image('available') - - def test_create_volume_from_image_exception(self): - """Verify that create volume from image, the volume status is - 'downloading'.""" - dst_fd, dst_path = tempfile.mkstemp() - os.close(dst_fd) - - self.stubs.Set(self.volume.driver, 'local_path', lambda x: dst_path) - - image_id = 'aaaaaaaa-0000-0000-0000-000000000000' - # creating volume testdata - volume_id = 1 - db.volume_create(self.context, {'id': volume_id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'creating', - 'host': 'dummy'}) - - self.assertRaises(exception.ImageNotFound, - self.volume.create_volume, - self.context, - volume_id, - None, - image_id) - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], "error") - # cleanup - db.volume_destroy(self.context, volume_id) - os.unlink(dst_path) - - def test_copy_volume_to_image_status_available(self): - dst_fd, dst_path = tempfile.mkstemp() - os.close(dst_fd) - - def fake_local_path(volume): - return dst_path - - self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) - - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - # creating volume testdata - volume_id = 1 - db.volume_create(self.context, {'id': volume_id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'uploading', - 'instance_uuid': None, - 'host': 'dummy'}) - - try: - # start test - self.volume.copy_volume_to_image(self.context, - volume_id, - image_id) - - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], 'available') - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - os.unlink(dst_path) - - def test_copy_volume_to_image_status_use(self): - dst_fd, dst_path = tempfile.mkstemp() - os.close(dst_fd) - - def fake_local_path(volume): - return dst_path - - self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) - - #image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - image_id = 'a440c04b-79fa-479c-bed1-0b816eaec379' - # creating volume testdata - volume_id = 1 - db.volume_create(self.context, - {'id': volume_id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'uploading', - 'instance_uuid': - 'b21f957d-a72f-4b93-b5a5-45b1161abb02', - 'host': 'dummy'}) - - try: - # start test - self.volume.copy_volume_to_image(self.context, - volume_id, - image_id) - - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], 'in-use') - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - os.unlink(dst_path) - - def test_copy_volume_to_image_exception(self): - dst_fd, dst_path = tempfile.mkstemp() - os.close(dst_fd) - - def fake_local_path(volume): - return dst_path - - self.stubs.Set(self.volume.driver, 'local_path', fake_local_path) - - image_id = 'aaaaaaaa-0000-0000-0000-000000000000' - # creating volume testdata - volume_id = 1 - db.volume_create(self.context, {'id': volume_id, - 'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1), - 'display_description': 'Test Desc', - 'size': 20, - 'status': 'in-use', - 'host': 'dummy'}) - - try: - # start test - self.assertRaises(exception.ImageNotFound, - self.volume.copy_volume_to_image, - self.context, - volume_id, - image_id) - - volume = db.volume_get(self.context, volume_id) - self.assertEqual(volume['status'], 'available') - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - os.unlink(dst_path) - - def test_create_volume_from_exact_sized_image(self): - """Verify that an image which is exactly the same size as the - volume, will work correctly.""" - class _FakeImageService: - def __init__(self, db_driver=None, image_service=None): - pass - - def show(self, context, image_id): - return {'size': 2 * 1024 * 1024 * 1024} - - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - - try: - volume_id = None - volume_api = nova.volume.api.API( - image_service=_FakeImageService()) - volume = volume_api.create(self.context, 2, 'name', 'description', - image_id=1) - volume_id = volume['id'] - self.assertEqual(volume['status'], 'creating') - - finally: - # cleanup - db.volume_destroy(self.context, volume_id) - - def test_create_volume_from_oversized_image(self): - """Verify that an image which is too big will fail correctly.""" - class _FakeImageService: - def __init__(self, db_driver=None, image_service=None): - pass - - def show(self, context, image_id): - return {'size': 2 * 1024 * 1024 * 1024 + 1} - - image_id = '70a599e0-31e7-49b7-b260-868f441e862b' - - volume_api = nova.volume.api.API(image_service=_FakeImageService()) - - self.assertRaises(exception.InvalidInput, - volume_api.create, - self.context, 2, - 'name', 'description', image_id=1) - - -class DriverTestCase(test.TestCase): - """Base Test class for Drivers.""" - driver_name = "nova.volume.driver.FakeBaseDriver" - - def setUp(self): - super(DriverTestCase, self).setUp() - vol_tmpdir = tempfile.mkdtemp() - self.flags(volume_driver=self.driver_name, - volumes_dir=vol_tmpdir) - self.volume = importutils.import_object(FLAGS.volume_manager) - self.context = context.get_admin_context() - self.output = "" - self.stubs.Set(iscsi.TgtAdm, '_get_target', self.fake_get_target) - - def _fake_execute(_command, *_args, **_kwargs): - """Fake _execute.""" - return self.output, None - self.volume.driver.set_execute(_fake_execute) - - instance = db.instance_create(self.context, {}) - self.instance_id = instance['id'] - self.instance_uuid = instance['uuid'] - - def tearDown(self): - try: - shutil.rmtree(FLAGS.volumes_dir) - except OSError: - pass - super(DriverTestCase, self).tearDown() - - def fake_get_target(obj, iqn): - return 1 - - def _attach_volume(self): - """Attach volumes to an instance.""" - return [] - - def _detach_volume(self, volume_id_list): - """Detach volumes from an instance.""" - for volume_id in volume_id_list: - db.volume_detached(self.context, volume_id) - self.volume.delete_volume(self.context, volume_id) - - -class VolumeDriverTestCase(DriverTestCase): - """Test case for VolumeDriver""" - driver_name = "nova.volume.driver.VolumeDriver" - - def test_delete_busy_volume(self): - """Test deleting a busy volume.""" - self.stubs.Set(self.volume.driver, '_volume_not_present', - lambda x: False) - self.stubs.Set(self.volume.driver, '_delete_volume', - lambda x, y: False) - # Want DriverTestCase._fake_execute to return 'o' so that - # volume.driver.delete_volume() raises the VolumeIsBusy exception. - self.output = 'o' - self.assertRaises(exception.VolumeIsBusy, - self.volume.driver.delete_volume, - {'name': 'test1', 'size': 1024}) - # when DriverTestCase._fake_execute returns something other than - # 'o' volume.driver.delete_volume() does not raise an exception. - self.output = 'x' - self.volume.driver.delete_volume({'name': 'test1', 'size': 1024}) - - -class ISCSITestCase(DriverTestCase): - """Test Case for ISCSIDriver""" - driver_name = "nova.volume.driver.ISCSIDriver" - - def _attach_volume(self): - """Attach volumes to an instance. """ - volume_id_list = [] - for index in xrange(3): - vol = {} - vol['size'] = 0 - vol_ref = db.volume_create(self.context, vol) - self.volume.create_volume(self.context, vol_ref['id']) - vol_ref = db.volume_get(self.context, vol_ref['id']) - - # each volume has a different mountpoint - mountpoint = "/dev/sd" + chr((ord('b') + index)) - db.volume_attached(self.context, vol_ref['id'], self.instance_uuid, - mountpoint) - volume_id_list.append(vol_ref['id']) - - return volume_id_list - - def test_check_for_export_with_no_volume(self): - self.volume.check_for_export(self.context, self.instance_id) - - -class VolumePolicyTestCase(test.TestCase): - - def setUp(self): - super(VolumePolicyTestCase, self).setUp() - - nova.policy.reset() - nova.policy.init() - - self.context = context.get_admin_context() - - def tearDown(self): - super(VolumePolicyTestCase, self).tearDown() - nova.policy.reset() - - def _set_rules(self, rules): - nova.common.policy.set_brain(nova.common.policy.HttpBrain(rules)) - - def test_check_policy(self): - self.mox.StubOutWithMock(nova.policy, 'enforce') - target = { - 'project_id': self.context.project_id, - 'user_id': self.context.user_id, - } - nova.policy.enforce(self.context, 'volume:attach', target) - self.mox.ReplayAll() - nova.volume.api.check_policy(self.context, 'attach') - - def test_check_policy_with_target(self): - self.mox.StubOutWithMock(nova.policy, 'enforce') - target = { - 'project_id': self.context.project_id, - 'user_id': self.context.user_id, - 'id': 2, - } - nova.policy.enforce(self.context, 'volume:attach', target) - self.mox.ReplayAll() - nova.volume.api.check_policy(self.context, 'attach', {'id': 2}) diff --git a/nova/tests/test_volume_types.py b/nova/tests/test_volume_types.py deleted file mode 100644 index 5e178d5e1..000000000 --- a/nova/tests/test_volume_types.py +++ /dev/null @@ -1,167 +0,0 @@ -# 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. -""" -Unit Tests for volume types code -""" -import time - -from nova import context -from nova.db.sqlalchemy import models -from nova.db.sqlalchemy import session as sql_session -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova import test -from nova.volume import volume_types - -FLAGS = flags.FLAGS -LOG = logging.getLogger(__name__) - - -class VolumeTypeTestCase(test.TestCase): - """Test cases for volume type code""" - def setUp(self): - super(VolumeTypeTestCase, self).setUp() - - self.ctxt = context.get_admin_context() - self.vol_type1_name = str(int(time.time())) - self.vol_type1_specs = dict( - type="physical drive", - drive_type="SAS", - size="300", - rpm="7200", - visible="True") - - def test_volume_type_create_then_destroy(self): - """Ensure volume types can be created and deleted""" - prev_all_vtypes = volume_types.get_all_types(self.ctxt) - - volume_types.create(self.ctxt, - self.vol_type1_name, - self.vol_type1_specs) - new = volume_types.get_volume_type_by_name(self.ctxt, - self.vol_type1_name) - - LOG.info(_("Given data: %s"), self.vol_type1_specs) - LOG.info(_("Result data: %s"), new) - - for k, v in self.vol_type1_specs.iteritems(): - self.assertEqual(v, new['extra_specs'][k], - 'one of fields does not match') - - new_all_vtypes = volume_types.get_all_types(self.ctxt) - self.assertEqual(len(prev_all_vtypes) + 1, - len(new_all_vtypes), - 'drive type was not created') - - volume_types.destroy(self.ctxt, self.vol_type1_name) - new_all_vtypes = volume_types.get_all_types(self.ctxt) - self.assertEqual(prev_all_vtypes, - new_all_vtypes, - 'drive type was not deleted') - - def test_get_all_volume_types(self): - """Ensures that all volume types can be retrieved""" - session = sql_session.get_session() - total_volume_types = session.query(models.VolumeTypes).count() - vol_types = volume_types.get_all_types(self.ctxt) - self.assertEqual(total_volume_types, len(vol_types)) - - def test_non_existent_vol_type_shouldnt_delete(self): - """Ensures that volume type creation fails with invalid args""" - self.assertRaises(exception.VolumeTypeNotFoundByName, - volume_types.destroy, self.ctxt, "sfsfsdfdfs") - - def test_repeated_vol_types_shouldnt_raise(self): - """Ensures that volume duplicates don't raise""" - new_name = self.vol_type1_name + "dup" - volume_types.create(self.ctxt, new_name) - volume_types.destroy(self.ctxt, new_name) - volume_types.create(self.ctxt, new_name) - - def test_invalid_volume_types_params(self): - """Ensures that volume type creation fails with invalid args""" - self.assertRaises(exception.InvalidVolumeType, - volume_types.destroy, self.ctxt, None) - self.assertRaises(exception.InvalidVolumeType, - volume_types.get_volume_type, self.ctxt, None) - self.assertRaises(exception.InvalidVolumeType, - volume_types.get_volume_type_by_name, - self.ctxt, None) - - def test_volume_type_get_by_id_and_name(self): - """Ensure volume types get returns same entry""" - volume_types.create(self.ctxt, - self.vol_type1_name, - self.vol_type1_specs) - new = volume_types.get_volume_type_by_name(self.ctxt, - self.vol_type1_name) - - new2 = volume_types.get_volume_type(self.ctxt, new['id']) - self.assertEqual(new, new2) - - def test_volume_type_search_by_extra_spec(self): - """Ensure volume types get by extra spec returns correct type""" - volume_types.create(self.ctxt, "type1", {"key1": "val1", - "key2": "val2"}) - volume_types.create(self.ctxt, "type2", {"key2": "val2", - "key3": "val3"}) - volume_types.create(self.ctxt, "type3", {"key3": "another_value", - "key4": "val4"}) - - vol_types = volume_types.get_all_types(self.ctxt, - search_opts={'extra_specs': {"key1": "val1"}}) - LOG.info("vol_types: %s" % vol_types) - self.assertEqual(len(vol_types), 1) - self.assertTrue("type1" in vol_types.keys()) - self.assertEqual(vol_types['type1']['extra_specs'], - {"key1": "val1", "key2": "val2"}) - - vol_types = volume_types.get_all_types(self.ctxt, - search_opts={'extra_specs': {"key2": "val2"}}) - LOG.info("vol_types: %s" % vol_types) - self.assertEqual(len(vol_types), 2) - self.assertTrue("type1" in vol_types.keys()) - self.assertTrue("type2" in vol_types.keys()) - - vol_types = volume_types.get_all_types(self.ctxt, - search_opts={'extra_specs': {"key3": "val3"}}) - LOG.info("vol_types: %s" % vol_types) - self.assertEqual(len(vol_types), 1) - self.assertTrue("type2" in vol_types.keys()) - - def test_volume_type_search_by_extra_spec_multiple(self): - """Ensure volume types get by extra spec returns correct type""" - volume_types.create(self.ctxt, "type1", {"key1": "val1", - "key2": "val2", - "key3": "val3"}) - volume_types.create(self.ctxt, "type2", {"key2": "val2", - "key3": "val3"}) - volume_types.create(self.ctxt, "type3", {"key1": "val1", - "key3": "val3", - "key4": "val4"}) - - vol_types = volume_types.get_all_types(self.ctxt, - search_opts={'extra_specs': {"key1": "val1", - "key3": "val3"}}) - LOG.info("vol_types: %s" % vol_types) - self.assertEqual(len(vol_types), 2) - self.assertTrue("type1" in vol_types.keys()) - self.assertTrue("type3" in vol_types.keys()) - self.assertEqual(vol_types['type1']['extra_specs'], - {"key1": "val1", "key2": "val2", "key3": "val3"}) - self.assertEqual(vol_types['type3']['extra_specs'], - {"key1": "val1", "key3": "val3", "key4": "val4"}) diff --git a/nova/tests/test_volume_types_extra_specs.py b/nova/tests/test_volume_types_extra_specs.py deleted file mode 100644 index ed7840e0a..000000000 --- a/nova/tests/test_volume_types_extra_specs.py +++ /dev/null @@ -1,130 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# Copyright 2011 University of Southern California -# 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. -""" -Unit Tests for volume types extra specs code -""" - -from nova import context -from nova import db -from nova import test - - -class VolumeTypeExtraSpecsTestCase(test.TestCase): - - def setUp(self): - super(VolumeTypeExtraSpecsTestCase, self).setUp() - self.context = context.get_admin_context() - self.vol_type1 = dict(name="TEST: Regular volume test") - self.vol_type1_specs = dict(vol_extra1="value1", - vol_extra2="value2", - vol_extra3=3) - self.vol_type1['extra_specs'] = self.vol_type1_specs - ref = db.volume_type_create(self.context, self.vol_type1) - self.volume_type1_id = ref.id - for k, v in self.vol_type1_specs.iteritems(): - self.vol_type1_specs[k] = str(v) - - self.vol_type2_noextra = dict(name="TEST: Volume type without extra") - ref = db.volume_type_create(self.context, self.vol_type2_noextra) - self.vol_type2_id = ref.id - - def tearDown(self): - # Remove the volume type from the database - db.volume_type_destroy(context.get_admin_context(), - self.vol_type1['name']) - db.volume_type_destroy(context.get_admin_context(), - self.vol_type2_noextra['name']) - super(VolumeTypeExtraSpecsTestCase, self).tearDown() - - def test_volume_type_specs_get(self): - expected_specs = self.vol_type1_specs.copy() - actual_specs = db.volume_type_extra_specs_get( - context.get_admin_context(), - self.volume_type1_id) - self.assertEquals(expected_specs, actual_specs) - - def test_volume_type_extra_specs_delete(self): - expected_specs = self.vol_type1_specs.copy() - del expected_specs['vol_extra2'] - db.volume_type_extra_specs_delete(context.get_admin_context(), - self.volume_type1_id, - 'vol_extra2') - actual_specs = db.volume_type_extra_specs_get( - context.get_admin_context(), - self.volume_type1_id) - self.assertEquals(expected_specs, actual_specs) - - def test_volume_type_extra_specs_update(self): - expected_specs = self.vol_type1_specs.copy() - expected_specs['vol_extra3'] = "4" - db.volume_type_extra_specs_update_or_create( - context.get_admin_context(), - self.volume_type1_id, - dict(vol_extra3=4)) - actual_specs = db.volume_type_extra_specs_get( - context.get_admin_context(), - self.volume_type1_id) - self.assertEquals(expected_specs, actual_specs) - - def test_volume_type_extra_specs_create(self): - expected_specs = self.vol_type1_specs.copy() - expected_specs['vol_extra4'] = 'value4' - expected_specs['vol_extra5'] = 'value5' - db.volume_type_extra_specs_update_or_create( - context.get_admin_context(), - self.volume_type1_id, - dict(vol_extra4="value4", - vol_extra5="value5")) - actual_specs = db.volume_type_extra_specs_get( - context.get_admin_context(), - self.volume_type1_id) - self.assertEquals(expected_specs, actual_specs) - - def test_volume_type_get_with_extra_specs(self): - volume_type = db.volume_type_get( - context.get_admin_context(), - self.volume_type1_id) - self.assertEquals(volume_type['extra_specs'], - self.vol_type1_specs) - - volume_type = db.volume_type_get( - context.get_admin_context(), - self.vol_type2_id) - self.assertEquals(volume_type['extra_specs'], {}) - - def test_volume_type_get_by_name_with_extra_specs(self): - volume_type = db.volume_type_get_by_name( - context.get_admin_context(), - self.vol_type1['name']) - self.assertEquals(volume_type['extra_specs'], - self.vol_type1_specs) - - volume_type = db.volume_type_get_by_name( - context.get_admin_context(), - self.vol_type2_noextra['name']) - self.assertEquals(volume_type['extra_specs'], {}) - - def test_volume_type_get_all(self): - expected_specs = self.vol_type1_specs.copy() - - types = db.volume_type_get_all(context.get_admin_context()) - - self.assertEquals( - types[self.vol_type1['name']]['extra_specs'], expected_specs) - - self.assertEquals( - types[self.vol_type2_noextra['name']]['extra_specs'], {}) diff --git a/nova/tests/test_volume_utils.py b/nova/tests/test_volume_utils.py deleted file mode 100644 index 89ad7c3db..000000000 --- a/nova/tests/test_volume_utils.py +++ /dev/null @@ -1,91 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# 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. - -"""Tests For miscellaneous util methods used with volume.""" - -from nova import context -from nova import db -from nova import flags -from nova.openstack.common import importutils -from nova.openstack.common import log as logging -from nova.openstack.common.notifier import api as notifier_api -from nova.openstack.common.notifier import test_notifier -from nova import test -from nova.tests import fake_network -from nova.volume import utils as volume_utils - - -LOG = logging.getLogger(__name__) -FLAGS = flags.FLAGS - - -class UsageInfoTestCase(test.TestCase): - - def setUp(self): - super(UsageInfoTestCase, self).setUp() - self.flags(compute_driver='nova.virt.fake.FakeDriver', - host='fake', - notification_driver=[test_notifier.__name__]) - fake_network.set_stub_network_methods(self.stubs) - - self.volume = importutils.import_object(FLAGS.volume_manager) - self.user_id = 'fake' - self.project_id = 'fake' - self.snapshot_id = 'fake' - self.volume_size = 0 - self.context = context.RequestContext(self.user_id, self.project_id) - test_notifier.NOTIFICATIONS = [] - - def tearDown(self): - notifier_api._reset_drivers() - super(UsageInfoTestCase, self).tearDown() - - def _create_volume(self, params={}): - """Create a test volume""" - vol = {} - vol['snapshot_id'] = self.snapshot_id - vol['user_id'] = self.user_id - vol['project_id'] = self.project_id - vol['host'] = FLAGS.host - vol['availability_zone'] = FLAGS.storage_availability_zone - vol['status'] = "creating" - vol['attach_status'] = "detached" - vol['size'] = self.volume_size - vol.update(params) - return db.volume_create(self.context, vol)['id'] - - def test_notify_usage_exists(self): - """Ensure 'exists' notification generates appropriate usage data.""" - volume_id = self._create_volume() - volume = db.volume_get(self.context, volume_id) - volume_utils.notify_usage_exists(self.context, volume) - self.assertEquals(len(test_notifier.NOTIFICATIONS), 1) - msg = test_notifier.NOTIFICATIONS[0] - self.assertEquals(msg['priority'], 'INFO') - self.assertEquals(msg['event_type'], 'volume.exists') - payload = msg['payload'] - self.assertEquals(payload['tenant_id'], self.project_id) - self.assertEquals(payload['user_id'], self.user_id) - self.assertEquals(payload['snapshot_id'], self.snapshot_id) - self.assertEquals(payload['volume_id'], volume.id) - self.assertEquals(payload['size'], self.volume_size) - for attr in ('display_name', 'created_at', 'launched_at', - 'status', 'audit_period_beginning', - 'audit_period_ending'): - self.assertTrue(attr in payload, - msg="Key %s not in payload" % attr) - db.volume_destroy(context.get_admin_context(), volume['id']) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index a7bfa7548..11e8844c4 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -170,7 +170,7 @@ class XenAPIVolumeTestCase(stubs.XenAPITestBase): vol['user_id'] = 'fake' vol['project_id'] = 'fake' vol['host'] = 'localhost' - vol['availability_zone'] = FLAGS.storage_availability_zone + vol['availability_zone'] = FLAGS.node_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" return db.volume_create(self.context, vol) diff --git a/nova/tests/test_xensm.py b/nova/tests/test_xensm.py deleted file mode 100644 index 2f2108aa4..000000000 --- a/nova/tests/test_xensm.py +++ /dev/null @@ -1,140 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2010 Citrix 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 express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Test suite for Xen Storage Manager Volume Driver.""" - - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova.tests.xenapi import stubs -from nova.virt.xenapi import fake as xenapi_fake -from nova.volume import xensm - -LOG = logging.getLogger(__name__) - -FLAGS = flags.FLAGS - - -class XenSMTestCase(stubs.XenAPITestBase): - """Unit tests for Xen Storage Manager Volume operations.""" - - def _get_sm_backend_params(self): - config_params = ("name_label=testsmbackend " - "server=localhost " - "serverpath=/tmp/nfspath") - params = dict(flavor_id=1, - sr_uuid=None, - sr_type='nfs', - config_params=config_params) - return params - - def setUp(self): - super(XenSMTestCase, self).setUp() - self.user_id = 'fake' - self.project_id = 'fake' - self.context = context.RequestContext(self.user_id, self.project_id) - self.flags(compute_driver='xenapi.XenAPIDriver', - xenapi_connection_url='http://test_url', - xenapi_connection_username='test_user', - xenapi_connection_password='test_pass') - stubs.stubout_session(self.stubs, xenapi_fake.SessionBase) - xenapi_fake.reset() - self.driver = xensm.XenSMDriver() - self.driver.db = db - - def _setup_step(self, ctxt): - # Create a fake backend conf - params = self._get_sm_backend_params() - beconf = db.sm_backend_conf_create(ctxt, - params) - # Call setup, the one time operation that will create a backend SR - self.driver.do_setup(ctxt) - return beconf - - def test_do_setup(self): - ctxt = context.get_admin_context() - beconf = self._setup_step(ctxt) - beconf = db.sm_backend_conf_get(ctxt, beconf['id']) - self.assertIsInstance(beconf['sr_uuid'], basestring) - - def _create_volume(self, size=0): - """Create a volume object.""" - vol = {} - vol['size'] = size - vol['user_id'] = 'fake' - vol['project_id'] = 'fake' - vol['host'] = 'localhost' - vol['availability_zone'] = FLAGS.storage_availability_zone - vol['status'] = "creating" - vol['attach_status'] = "detached" - return db.volume_create(self.context, vol) - - def test_create_volume(self): - ctxt = context.get_admin_context() - beconf = self._setup_step(ctxt) - volume = self._create_volume() - self.driver.create_volume(volume) - db.sm_volume_get(ctxt, volume['id']) - - def test_local_path(self): - ctxt = context.get_admin_context() - volume = self._create_volume() - val = self.driver.local_path(volume) - self.assertIsInstance(val, basestring) - - def test_delete_volume(self): - ctxt = context.get_admin_context() - beconf = self._setup_step(ctxt) - volume = self._create_volume() - self.driver.create_volume(volume) - self.driver.delete_volume(volume) - self.assertRaises(exception.NotFound, - db.sm_volume_get, - ctxt, - volume['id']) - - def test_delete_volume_raises_notfound(self): - ctxt = context.get_admin_context() - beconf = self._setup_step(ctxt) - self.assertRaises(exception.NotFound, - self.driver.delete_volume, - {'id': "FA15E-1D"}) - - def _get_expected_conn(self, beconf, vol): - expected = {} - expected['volume_id'] = unicode(vol['id']) - expected['flavor_id'] = beconf['flavor_id'] - expected['sr_uuid'] = unicode(beconf['sr_uuid']) - expected['sr_type'] = unicode(beconf['sr_type']) - return expected - - def test_initialize_connection(self): - ctxt = context.get_admin_context() - beconf = self._setup_step(ctxt) - beconf = db.sm_backend_conf_get(ctxt, beconf['id']) - volume = self._create_volume() - self.driver.create_volume(volume) - expected = self._get_expected_conn(beconf, volume) - conn = self.driver.initialize_connection(volume, 'fakeConnector') - res = {} - for key in ['volume_id', 'flavor_id', 'sr_uuid', 'sr_type']: - res[key] = conn['data'][key] - self.assertDictMatch(expected, res) - self.assertEqual(set(conn['data']['introduce_sr_keys']), - set([u'sr_type', u'server', u'serverpath'])) diff --git a/nova/tests/volume/test_HpSanISCSIDriver.py b/nova/tests/volume/test_HpSanISCSIDriver.py deleted file mode 100644 index d6d3f3488..000000000 --- a/nova/tests/volume/test_HpSanISCSIDriver.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright 2012 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. -from nova import exception -from nova.openstack.common import log as logging -from nova import test -from nova.volume import san - -LOG = logging.getLogger(__name__) - - -class HpSanISCSITestCase(test.TestCase): - - def setUp(self): - super(HpSanISCSITestCase, self).setUp() - self.stubs.Set(san.HpSanISCSIDriver, "_cliq_run", - self._fake_cliq_run) - self.stubs.Set(san.HpSanISCSIDriver, "_get_iscsi_properties", - self._fake_get_iscsi_properties) - self.driver = san.HpSanISCSIDriver() - self.volume_name = "fakevolume" - self.connector = {'ip': '10.0.0.2', - 'initiator': 'iqn.1993-08.org.debian:01:222', - 'host': 'fakehost'} - self.properties = {'target_discoverd': True, - 'target_portal': '10.0.1.6:3260', - 'target_iqn': - 'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev', - 'volume_id': 1} - - def _fake_get_iscsi_properties(self, volume): - return self.properties - - def _fake_cliq_run(self, verb, cliq_args): - """Return fake results for the various methods.""" - - def create_volume(cliq_args): - """ - input = "createVolume description="fake description" - clusterName=Cluster01 volumeName=fakevolume - thinProvision=0 output=XML size=1GB" - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." - name="CliqSuccess" processingTime="181" result="0"/> - </gauche>""" - self.assertEqual(cliq_args['volumeName'], self.volume_name) - self.assertEqual(cliq_args['thinProvision'], '1') - self.assertEqual(cliq_args['size'], '1GB') - return output, None - - def delete_volume(cliq_args): - """ - input = "deleteVolume volumeName=fakevolume prompt=false - output=XML" - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." - name="CliqSuccess" processingTime="164" result="0"/> - </gauche>""" - self.assertEqual(cliq_args['volumeName'], self.volume_name) - self.assertEqual(cliq_args['prompt'], 'false') - return output, None - - def assign_volume(cliq_args): - """ - input = "assignVolumeToServer volumeName=fakevolume - serverName=fakehost - output=XML" - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." - name="CliqSuccess" processingTime="174" result="0"/> - </gauche>""" - self.assertEqual(cliq_args['volumeName'], self.volume_name) - self.assertEqual(cliq_args['serverName'], self.connector['host']) - return output, None - - def unassign_volume(cliq_args): - """ - input = "unassignVolumeToServer volumeName=fakevolume - serverName=fakehost output=XML - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." - name="CliqSuccess" processingTime="205" result="0"/> - </gauche>""" - self.assertEqual(cliq_args['volumeName'], self.volume_name) - self.assertEqual(cliq_args['serverName'], self.connector['host']) - return output, None - - def get_cluster_info(cliq_args): - """ - input = "getClusterInfo clusterName=Cluster01 searchDepth=1 - verbose=0 output=XML" - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." name="CliqSuccess" - processingTime="1164" result="0"> - <cluster blockSize="1024" description="" - maxVolumeSizeReplication1="622957690" - maxVolumeSizeReplication2="311480287" - minVolumeSize="262144" name="Cluster01" - pageSize="262144" spaceTotal="633697992" - storageNodeCount="2" unprovisionedSpace="622960574" - useVip="true"> - <nsm ipAddress="10.0.1.7" name="111-vsa"/> - <nsm ipAddress="10.0.1.8" name="112-vsa"/> - <vip ipAddress="10.0.1.6" subnetMask="255.255.255.0"/> - </cluster></response></gauche>""" - return output, None - - def get_volume_info(cliq_args): - """ - input = "getVolumeInfo volumeName=fakevolume output=XML" - """ - output = """<gauche version="1.0"> - <response description="Operation succeeded." name="CliqSuccess" - processingTime="87" result="0"> - <volume autogrowPages="4" availability="online" - blockSize="1024" bytesWritten="0" checkSum="false" - clusterName="Cluster01" created="2011-02-08T19:56:53Z" - deleting="false" description="" groupName="Group01" - initialQuota="536870912" isPrimary="true" - iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:fakev" - maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca" - minReplication="1" name="vol-b" parity="0" replication="2" - reserveQuota="536870912" scratchQuota="4194304" - serialNumber="9fa5c8b2cca54b2948a63d8" - size="1073741824" stridePages="32" thinProvision="true"> - <status description="OK" value="2"/> - <permission access="rw" authGroup="api-1" - chapName="chapusername" chapRequired="true" - id="25369" initiatorSecret="" iqn="" - iscsiEnabled="true" loadBalance="true" - targetSecret="supersecret"/> - </volume></response></gauche>""" - return output, None - - def test_error(cliq_args): - output = """<gauche version="1.0"> - <response description="Volume '134234' not found." - name="CliqVolumeNotFound" processingTime="1083" - result="8000100c"/> - </gauche>""" - return output, None - - self.assertEqual(cliq_args['output'], 'XML') - try: - verbs = {'createVolume': create_volume, - 'deleteVolume': delete_volume, - 'assignVolumeToServer': assign_volume, - 'unassignVolumeToServer': unassign_volume, - 'getClusterInfo': get_cluster_info, - 'getVolumeInfo': get_volume_info, - 'testError': test_error} - except KeyError: - raise NotImplementedError() - - return verbs[verb](cliq_args) - - def test_create_volume(self): - volume = {'name': self.volume_name, 'size': 1} - model_update = self.driver.create_volume(volume) - expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0" - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(model_update['provider_location'], expected_location) - - def test_delete_volume(self): - volume = {'name': self.volume_name} - self.driver.delete_volume(volume) - - def test_initialize_connection(self): - volume = {'name': self.volume_name} - result = self.driver.initialize_connection(volume, self.connector) - self.assertEqual(result['driver_volume_type'], 'iscsi') - self.assertDictMatch(result['data'], self.properties) - - def test_terminate_connection(self): - volume = {'name': self.volume_name} - self.driver.terminate_connection(volume, self.connector) - - def test_create_snapshot(self): - try: - self.driver.create_snapshot("") - except NotImplementedError: - pass - - def test_create_volume_from_snapshot(self): - try: - self.driver.create_volume_from_snapshot("", "") - except NotImplementedError: - pass - - def test_cliq_error(self): - try: - self.driver._cliq_run_xml("testError", {}) - except exception.NovaException: - pass diff --git a/nova/volume/api.py b/nova/volume/api.py deleted file mode 100644 index 6beb771f3..000000000 --- a/nova/volume/api.py +++ /dev/null @@ -1,511 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -""" -Handles all requests relating to volumes. -""" - -import functools - -from nova.db import base -from nova import exception -from nova import flags -from nova.image import glance -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.openstack.common import rpc -from nova.openstack.common import timeutils -import nova.policy -from nova import quota -from nova.scheduler import rpcapi as scheduler_rpcapi - -volume_host_opt = cfg.BoolOpt('snapshot_same_host', - default=True, - help='Create volume from snapshot at the host where snapshot resides') - -FLAGS = flags.FLAGS -FLAGS.register_opt(volume_host_opt) -flags.DECLARE('storage_availability_zone', 'nova.volume.manager') - -LOG = logging.getLogger(__name__) -GB = 1048576 * 1024 - -QUOTAS = quota.QUOTAS - - -def wrap_check_policy(func): - """Check policy corresponding to the wrapped methods prior to execution - - This decorator requires the first 3 args of the wrapped function - to be (self, context, volume) - """ - @functools.wraps(func) - def wrapped(self, context, target_obj, *args, **kwargs): - check_policy(context, func.__name__, target_obj) - return func(self, context, target_obj, *args, **kwargs) - - return wrapped - - -def check_policy(context, action, target_obj=None): - target = { - 'project_id': context.project_id, - 'user_id': context.user_id, - } - target.update(target_obj or {}) - _action = 'volume:%s' % action - nova.policy.enforce(context, _action, target) - - -class API(base.Base): - """API for interacting with the volume manager.""" - - def __init__(self, image_service=None, **kwargs): - self.image_service = (image_service or - glance.get_default_image_service()) - self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI() - super(API, self).__init__(**kwargs) - - def create(self, context, size, name, description, snapshot=None, - image_id=None, volume_type=None, metadata=None, - availability_zone=None): - check_policy(context, 'create') - if snapshot is not None: - if snapshot['status'] != "available": - msg = _("status must be available") - raise exception.InvalidSnapshot(reason=msg) - if not size: - size = snapshot['volume_size'] - - snapshot_id = snapshot['id'] - else: - snapshot_id = None - - def as_int(s): - try: - return int(s) - except ValueError: - return s - - # tolerate size as stringified int - size = as_int(size) - - if not isinstance(size, int) or size <= 0: - msg = (_("Volume size '%s' must be an integer and greater than 0") - % size) - raise exception.InvalidInput(reason=msg) - - reservations = None - try: - reservations = QUOTAS.reserve(context, volumes=1, gigabytes=size) - except exception.OverQuota as e: - overs = e.kwargs['overs'] - usages = e.kwargs['usages'] - quotas = e.kwargs['quotas'] - - def _consumed(name): - return (usages[name]['reserved'] + usages[name]['in_use']) - - pid = context.project_id - if 'gigabytes' in overs: - consumed = _consumed('gigabytes') - quota = quotas['gigabytes'] - LOG.warn(_("Quota exceeded for %(pid)s, tried to create " - "%(size)sG volume (%(consumed)dG of %(quota)dG " - "already consumed)") % locals()) - raise exception.VolumeSizeTooLarge() - elif 'volumes' in overs: - consumed = _consumed('volumes') - LOG.warn(_("Quota exceeded for %(pid)s, tried to create " - "volume (%(consumed)d volumes already consumed)") - % locals()) - raise exception.VolumeLimitExceeded(allowed=quotas['volumes']) - - if image_id: - # check image existence - image_meta = self.image_service.show(context, image_id) - image_size_in_gb = (int(image_meta['size']) + GB - 1) / GB - #check image size is not larger than volume size. - if image_size_in_gb > size: - msg = _('Size of specified image is larger than volume size.') - raise exception.InvalidInput(reason=msg) - - if availability_zone is None: - availability_zone = FLAGS.storage_availability_zone - - if volume_type is None: - volume_type_id = None - else: - volume_type_id = volume_type.get('id', None) - - options = { - 'size': size, - 'user_id': context.user_id, - 'project_id': context.project_id, - 'snapshot_id': snapshot_id, - 'availability_zone': availability_zone, - 'status': "creating", - 'attach_status': "detached", - 'display_name': name, - 'display_description': description, - 'volume_type_id': volume_type_id, - 'metadata': metadata, - } - volume = self.db.volume_create(context, options) - - if reservations: - QUOTAS.commit(context, reservations) - - self._cast_create_volume(context, volume['id'], - snapshot_id, image_id) - return volume - - def _cast_create_volume(self, context, volume_id, - snapshot_id, image_id): - - # NOTE(Rongze Zhu): It is a simple solution for bug 1008866 - # If snapshot_id is set, make the call create volume directly to - # the volume host where the snapshot resides instead of passing it - # through the scheduer. So snapshot can be copy to new volume. - - if snapshot_id and FLAGS.snapshot_same_host: - snapshot_ref = self.db.snapshot_get(context, snapshot_id) - src_volume_ref = self.db.volume_get(context, - snapshot_ref['volume_id']) - topic = rpc.queue_get_for(context, - FLAGS.volume_topic, - src_volume_ref['host']) - rpc.cast(context, - topic, - {"method": "create_volume", - "args": {"volume_id": volume_id, - "snapshot_id": snapshot_id, - "image_id": image_id}}) - - else: - self.scheduler_rpcapi.create_volume( - context, volume_id, snapshot_id, image_id) - - @wrap_check_policy - def delete(self, context, volume, force=False): - volume_id = volume['id'] - if not volume['host']: - # NOTE(vish): scheduling failed, so delete it - # Note(zhiteng): update volume quota reservation - try: - reservations = QUOTAS.reserve(context, volumes=-1, - gigabytes=-volume['size']) - except Exception: - reservations = None - LOG.exception(_("Failed to update quota for deleting volume.")) - - self.db.volume_destroy(context, volume_id) - - if reservations: - QUOTAS.commit(context, reservations) - return - if not force and volume['status'] not in ["available", "error"]: - msg = _("Volume status must be available or error") - raise exception.InvalidVolume(reason=msg) - - snapshots = self.db.snapshot_get_all_for_volume(context, volume_id) - if len(snapshots): - msg = _("Volume still has %d dependent snapshots") % len(snapshots) - raise exception.InvalidVolume(reason=msg) - - now = timeutils.utcnow() - self.db.volume_update(context, volume_id, {'status': 'deleting', - 'terminated_at': now}) - host = volume['host'] - rpc.cast(context, - rpc.queue_get_for(context, FLAGS.volume_topic, host), - {"method": "delete_volume", - "args": {"volume_id": volume_id}}) - - @wrap_check_policy - def update(self, context, volume, fields): - self.db.volume_update(context, volume['id'], fields) - - def get(self, context, volume_id): - rv = self.db.volume_get(context, volume_id) - volume = dict(rv.iteritems()) - check_policy(context, 'get', volume) - return volume - - def get_all(self, context, search_opts=None): - check_policy(context, 'get_all') - - if search_opts is None: - search_opts = {} - - if (context.is_admin and 'all_tenants' in search_opts): - # Need to remove all_tenants to pass the filtering below. - del search_opts['all_tenants'] - volumes = self.db.volume_get_all(context) - else: - volumes = self.db.volume_get_all_by_project(context, - context.project_id) - if search_opts: - LOG.debug(_("Searching by: %s") % str(search_opts)) - - def _check_metadata_match(volume, searchdict): - volume_metadata = {} - for i in volume.get('volume_metadata'): - volume_metadata[i['key']] = i['value'] - - for k, v in searchdict.iteritems(): - if (k not in volume_metadata.keys() or - volume_metadata[k] != v): - return False - return True - - # search_option to filter_name mapping. - filter_mapping = {'metadata': _check_metadata_match} - - result = [] - for volume in volumes: - # go over all filters in the list - for opt, values in search_opts.iteritems(): - try: - filter_func = filter_mapping[opt] - except KeyError: - # no such filter - ignore it, go to next filter - continue - else: - if filter_func(volume, values): - result.append(volume) - break - volumes = result - return volumes - - def get_snapshot(self, context, snapshot_id): - check_policy(context, 'get_snapshot') - rv = self.db.snapshot_get(context, snapshot_id) - return dict(rv.iteritems()) - - def get_all_snapshots(self, context, search_opts=None): - check_policy(context, 'get_all_snapshots') - - search_opts = search_opts or {} - - if (context.is_admin and 'all_tenants' in search_opts): - # Need to remove all_tenants to pass the filtering below. - del search_opts['all_tenants'] - return self.db.snapshot_get_all(context) - else: - return self.db.snapshot_get_all_by_project(context, - context.project_id) - - @wrap_check_policy - def check_attach(self, context, volume): - # TODO(vish): abstract status checking? - if volume['status'] != "available": - msg = _("status must be available") - raise exception.InvalidVolume(reason=msg) - if volume['attach_status'] == "attached": - msg = _("already attached") - raise exception.InvalidVolume(reason=msg) - - @wrap_check_policy - def check_detach(self, context, volume): - # TODO(vish): abstract status checking? - if volume['status'] == "available": - msg = _("already detached") - raise exception.InvalidVolume(reason=msg) - - @wrap_check_policy - def reserve_volume(self, context, volume): - self.db.volume_update(context, volume['id'], {"status": "attaching"}) - - @wrap_check_policy - def unreserve_volume(self, context, volume): - if volume['status'] == "attaching": - self.db.volume_update(context, - volume['id'], - {"status": "available"}) - - @wrap_check_policy - def begin_detaching(self, context, volume): - self.db.volume_update(context, volume['id'], {"status": "detaching"}) - - @wrap_check_policy - def roll_detaching(self, context, volume): - if volume['status'] == "detaching": - self.db.volume_update(context, - volume['id'], - {"status": "in-use"}) - - @wrap_check_policy - def attach(self, context, volume, instance_uuid, mountpoint): - host = volume['host'] - queue = rpc.queue_get_for(context, FLAGS.volume_topic, host) - return rpc.call(context, queue, - {"method": "attach_volume", - "args": {"volume_id": volume['id'], - "instance_uuid": instance_uuid, - "mountpoint": mountpoint}}) - - @wrap_check_policy - def detach(self, context, volume): - host = volume['host'] - queue = rpc.queue_get_for(context, FLAGS.volume_topic, host) - return rpc.call(context, queue, - {"method": "detach_volume", - "args": {"volume_id": volume['id']}}) - - @wrap_check_policy - def initialize_connection(self, context, volume, connector): - host = volume['host'] - queue = rpc.queue_get_for(context, FLAGS.volume_topic, host) - return rpc.call(context, queue, - {"method": "initialize_connection", - "args": {"volume_id": volume['id'], - "connector": connector}}) - - @wrap_check_policy - def terminate_connection(self, context, volume, connector): - self.unreserve_volume(context, volume) - host = volume['host'] - queue = rpc.queue_get_for(context, FLAGS.volume_topic, host) - return rpc.call(context, queue, - {"method": "terminate_connection", - "args": {"volume_id": volume['id'], - "connector": connector}}) - - def _create_snapshot(self, context, volume, name, description, - force=False): - check_policy(context, 'create_snapshot', volume) - - if ((not force) and (volume['status'] != "available")): - msg = _("must be available") - raise exception.InvalidVolume(reason=msg) - - options = { - 'volume_id': volume['id'], - 'user_id': context.user_id, - 'project_id': context.project_id, - 'status': "creating", - 'progress': '0%', - 'volume_size': volume['size'], - 'display_name': name, - 'display_description': description} - - snapshot = self.db.snapshot_create(context, options) - host = volume['host'] - rpc.cast(context, - rpc.queue_get_for(context, FLAGS.volume_topic, host), - {"method": "create_snapshot", - "args": {"volume_id": volume['id'], - "snapshot_id": snapshot['id']}}) - return snapshot - - def create_snapshot(self, context, volume, name, description): - return self._create_snapshot(context, volume, name, description, - False) - - def create_snapshot_force(self, context, volume, name, description): - return self._create_snapshot(context, volume, name, description, - True) - - @wrap_check_policy - def delete_snapshot(self, context, snapshot): - if snapshot['status'] not in ["available", "error"]: - msg = _("Volume Snapshot status must be available or error") - raise exception.InvalidVolume(reason=msg) - self.db.snapshot_update(context, snapshot['id'], - {'status': 'deleting'}) - volume = self.db.volume_get(context, snapshot['volume_id']) - host = volume['host'] - rpc.cast(context, - rpc.queue_get_for(context, FLAGS.volume_topic, host), - {"method": "delete_snapshot", - "args": {"snapshot_id": snapshot['id']}}) - - @wrap_check_policy - def get_volume_metadata(self, context, volume): - """Get all metadata associated with a volume.""" - rv = self.db.volume_metadata_get(context, volume['id']) - return dict(rv.iteritems()) - - @wrap_check_policy - def delete_volume_metadata(self, context, volume, key): - """Delete the given metadata item from a volume.""" - self.db.volume_metadata_delete(context, volume['id'], key) - - @wrap_check_policy - def update_volume_metadata(self, context, volume, metadata, delete=False): - """Updates or creates volume metadata. - - If delete is True, metadata items that are not specified in the - `metadata` argument will be deleted. - - """ - if delete: - _metadata = metadata - else: - _metadata = self.get_volume_metadata(context, volume['id']) - _metadata.update(metadata) - - self.db.volume_metadata_update(context, volume['id'], _metadata, True) - return _metadata - - def get_volume_metadata_value(self, volume, key): - """Get value of particular metadata key.""" - metadata = volume.get('volume_metadata') - if metadata: - for i in volume['volume_metadata']: - if i['key'] == key: - return i['value'] - return None - - def _check_volume_availability(self, context, volume, force): - """Check if the volume can be used.""" - if volume['status'] not in ['available', 'in-use']: - msg = _('Volume status must be available/in-use.') - raise exception.InvalidVolume(reason=msg) - if not force and 'in-use' == volume['status']: - msg = _('Volume status is in-use.') - raise exception.InvalidVolume(reason=msg) - - @wrap_check_policy - def copy_volume_to_image(self, context, volume, metadata, force): - """Create a new image from the specified volume.""" - self._check_volume_availability(context, volume, force) - - recv_metadata = self.image_service.create(context, metadata) - self.update(context, volume, {'status': 'uploading'}) - rpc.cast(context, - rpc.queue_get_for(context, - FLAGS.volume_topic, - volume['host']), - {"method": "copy_volume_to_image", - "args": {"volume_id": volume['id'], - "image_id": recv_metadata['id']}}) - - response = {"id": volume['id'], - "updated_at": volume['updated_at'], - "status": 'uploading', - "display_description": volume['display_description'], - "size": volume['size'], - "volume_type": volume['volume_type'], - "image_id": recv_metadata['id'], - "container_format": recv_metadata['container_format'], - "disk_format": recv_metadata['disk_format'], - "image_name": recv_metadata.get('name', None) - } - return response diff --git a/nova/volume/manager.py b/nova/volume/manager.py deleted file mode 100644 index fafdcd5be..000000000 --- a/nova/volume/manager.py +++ /dev/null @@ -1,452 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. -""" -Volume manager manages creating, attaching, detaching, and persistent storage. - -Persistent storage volumes keep their state independent of instances. You can -attach to an instance, terminate the instance, spawn a new instance (even -one from a different image) and re-attach the volume with the same data -intact. - -**Related Flags** - -:volume_topic: What :mod:`rpc` topic to listen to (default: `volume`). -:volume_manager: The module name of a class derived from - :class:`manager.Manager` (default: - :class:`nova.volume.manager.Manager`). -:storage_availability_zone: Defaults to `nova`. -:volume_driver: Used by :class:`Manager`. Defaults to - :class:`nova.volume.driver.ISCSIDriver`. -:volume_group: Name of the group that will contain exported volumes (default: - `nova-volumes`) -:num_shell_tries: Number of times to attempt to run commands (default: 3) - -""" - -from nova import context -from nova import exception -from nova import flags -from nova.image import glance -from nova import manager -from nova.openstack.common import cfg -from nova.openstack.common import excutils -from nova.openstack.common import importutils -from nova.openstack.common import log as logging -from nova.openstack.common import timeutils -from nova import quota -from nova import utils -from nova.volume import utils as volume_utils - - -LOG = logging.getLogger(__name__) - -QUOTAS = quota.QUOTAS - -volume_manager_opts = [ - cfg.StrOpt('storage_availability_zone', - default='nova', - help='availability zone of this service'), - cfg.StrOpt('volume_driver', - default='nova.volume.driver.ISCSIDriver', - help='Driver to use for volume creation'), - cfg.BoolOpt('use_local_volumes', - default=True, - help='if True, will not discover local volumes'), - cfg.BoolOpt('volume_force_update_capabilities', - default=False, - help='if True will force update capabilities on each check'), - ] - -FLAGS = flags.FLAGS -FLAGS.register_opts(volume_manager_opts) - - -class VolumeManager(manager.SchedulerDependentManager): - """Manages attachable block storage devices.""" - def __init__(self, volume_driver=None, *args, **kwargs): - """Load the driver from the one specified in args, or from flags.""" - if not volume_driver: - volume_driver = FLAGS.volume_driver - self.driver = importutils.import_object(volume_driver) - super(VolumeManager, self).__init__(service_name='volume', - *args, **kwargs) - # NOTE(vish): Implementation specific db handling is done - # by the driver. - self.driver.db = self.db - self._last_volume_stats = [] - - def init_host(self): - """Do any initialization that needs to be run if this is a - standalone service.""" - - ctxt = context.get_admin_context() - self.driver.do_setup(ctxt) - self.driver.check_for_setup_error() - - volumes = self.db.volume_get_all_by_host(ctxt, self.host) - LOG.debug(_("Re-exporting %s volumes"), len(volumes)) - for volume in volumes: - if volume['status'] in ['available', 'in-use']: - self.driver.ensure_export(ctxt, volume) - else: - LOG.info(_("volume %s: skipping export"), volume['name']) - - LOG.debug(_('Resuming any in progress delete operations')) - for volume in volumes: - if volume['status'] == 'deleting': - LOG.info(_("Resuming delete on volume: %s"), volume['id']) - self.delete_volume(ctxt, volume['id']) - - def create_volume(self, context, volume_id, snapshot_id=None, - image_id=None, reservations=None): - """Creates and exports the volume.""" - context = context.elevated() - volume_ref = self.db.volume_get(context, volume_id) - self._notify_about_volume_usage(context, volume_ref, "create.start") - LOG.info(_("volume %s: creating"), volume_ref['name']) - - self.db.volume_update(context, - volume_id, - {'host': self.host}) - # NOTE(vish): so we don't have to get volume from db again - # before passing it to the driver. - volume_ref['host'] = self.host - - status = 'available' - model_update = False - - try: - vol_name = volume_ref['name'] - vol_size = volume_ref['size'] - LOG.debug(_("volume %(vol_name)s: creating lv of" - " size %(vol_size)sG") % locals()) - if snapshot_id is None and image_id is None: - model_update = self.driver.create_volume(volume_ref) - elif snapshot_id is not None: - snapshot_ref = self.db.snapshot_get(context, snapshot_id) - model_update = self.driver.create_volume_from_snapshot( - volume_ref, - snapshot_ref) - else: - # create the volume from an image - image_service, image_id = \ - glance.get_remote_image_service(context, - image_id) - image_location = image_service.get_location(context, image_id) - cloned = self.driver.clone_image(volume_ref, image_location) - if not cloned: - model_update = self.driver.create_volume(volume_ref) - status = 'downloading' - - if model_update: - self.db.volume_update(context, volume_ref['id'], model_update) - - LOG.debug(_("volume %s: creating export"), volume_ref['name']) - model_update = self.driver.create_export(context, volume_ref) - if model_update: - self.db.volume_update(context, volume_ref['id'], model_update) - except Exception: - with excutils.save_and_reraise_exception(): - self.db.volume_update(context, - volume_ref['id'], {'status': 'error'}) - - now = timeutils.utcnow() - volume_ref = self.db.volume_update(context, - volume_ref['id'], {'status': status, - 'launched_at': now}) - LOG.debug(_("volume %s: created successfully"), volume_ref['name']) - self._reset_stats() - self._notify_about_volume_usage(context, volume_ref, "create.end") - - if image_id and not cloned: - #copy the image onto the volume. - self._copy_image_to_volume(context, volume_ref, image_id) - return volume_id - - def delete_volume(self, context, volume_id): - """Deletes and unexports volume.""" - context = context.elevated() - volume_ref = self.db.volume_get(context, volume_id) - if volume_ref['attach_status'] == "attached": - # Volume is still attached, need to detach first - raise exception.VolumeAttached(volume_id=volume_id) - if volume_ref['host'] != self.host: - raise exception.InvalidVolume( - reason=_("Volume is not local to this node")) - - self._notify_about_volume_usage(context, volume_ref, "delete.start") - self._reset_stats() - try: - LOG.debug(_("volume %s: removing export"), volume_ref['name']) - self.driver.remove_export(context, volume_ref) - LOG.debug(_("volume %s: deleting"), volume_ref['name']) - self.driver.delete_volume(volume_ref) - except exception.VolumeIsBusy: - LOG.debug(_("volume %s: volume is busy"), volume_ref['name']) - self.driver.ensure_export(context, volume_ref) - self.db.volume_update(context, volume_ref['id'], - {'status': 'available'}) - return True - except Exception: - with excutils.save_and_reraise_exception(): - self.db.volume_update(context, - volume_ref['id'], - {'status': 'error_deleting'}) - - # Get reservations - try: - reservations = QUOTAS.reserve(context, volumes=-1, - gigabytes=-volume_ref['size']) - except Exception: - reservations = None - LOG.exception(_("Failed to update usages deleting volume")) - - volume_ref = self.db.volume_destroy(context, volume_id) - LOG.debug(_("volume %s: deleted successfully"), volume_ref['name']) - self._notify_about_volume_usage(context, volume_ref, "delete.end") - - # Commit the reservations - if reservations: - QUOTAS.commit(context, reservations) - - return True - - def create_snapshot(self, context, volume_id, snapshot_id): - """Creates and exports the snapshot.""" - context = context.elevated() - snapshot_ref = self.db.snapshot_get(context, snapshot_id) - LOG.info(_("snapshot %s: creating"), snapshot_ref['name']) - - try: - snap_name = snapshot_ref['name'] - LOG.debug(_("snapshot %(snap_name)s: creating") % locals()) - model_update = self.driver.create_snapshot(snapshot_ref) - if model_update: - self.db.snapshot_update(context, snapshot_ref['id'], - model_update) - - except Exception: - with excutils.save_and_reraise_exception(): - self.db.snapshot_update(context, - snapshot_ref['id'], - {'status': 'error'}) - - self.db.snapshot_update(context, - snapshot_ref['id'], {'status': 'available', - 'progress': '100%'}) - LOG.debug(_("snapshot %s: created successfully"), snapshot_ref['name']) - return snapshot_id - - def delete_snapshot(self, context, snapshot_id): - """Deletes and unexports snapshot.""" - context = context.elevated() - snapshot_ref = self.db.snapshot_get(context, snapshot_id) - - try: - LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name']) - self.driver.delete_snapshot(snapshot_ref) - except exception.SnapshotIsBusy: - LOG.debug(_("snapshot %s: snapshot is busy"), snapshot_ref['name']) - self.db.snapshot_update(context, - snapshot_ref['id'], - {'status': 'available'}) - return True - except Exception: - with excutils.save_and_reraise_exception(): - self.db.snapshot_update(context, - snapshot_ref['id'], - {'status': 'error_deleting'}) - - self.db.snapshot_destroy(context, snapshot_id) - LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name']) - return True - - def attach_volume(self, context, volume_id, instance_uuid, mountpoint): - """Updates db to show volume is attached""" - # TODO(vish): refactor this into a more general "reserve" - if not utils.is_uuid_like(instance_uuid): - raise exception.InvalidUUID(instance_uuid) - - try: - self.driver.attach_volume(context, - volume_id, - instance_uuid, - mountpoint) - except Exception: - with excutils.save_and_reraise_exception(): - self.db.volume_update(context, - volume_id, - {'status': 'error_attaching'}) - - self.db.volume_attached(context, - volume_id, - instance_uuid, - mountpoint) - - def detach_volume(self, context, volume_id): - """Updates db to show volume is detached""" - # TODO(vish): refactor this into a more general "unreserve" - try: - self.driver.detach_volume(context, volume_id) - except Exception: - with excutils.save_and_reraise_exception(): - self.db.volume_update(context, - volume_id, - {'status': 'error_detaching'}) - - self.db.volume_detached(context, volume_id) - - def _copy_image_to_volume(self, context, volume, image_id): - """Downloads Glance image to the specified volume. """ - volume_id = volume['id'] - payload = {'volume_id': volume_id, 'image_id': image_id} - try: - image_service, image_id = glance.get_remote_image_service(context, - image_id) - self.driver.copy_image_to_volume(context, volume, image_service, - image_id) - LOG.debug(_("Downloaded image %(image_id)s to %(volume_id)s " - "successfully") % locals()) - self.db.volume_update(context, volume_id, - {'status': 'available'}) - except Exception, error: - with excutils.save_and_reraise_exception(): - payload['message'] = unicode(error) - self.db.volume_update(context, volume_id, {'status': 'error'}) - - def copy_volume_to_image(self, context, volume_id, image_id): - """Uploads the specified volume to Glance.""" - payload = {'volume_id': volume_id, 'image_id': image_id} - try: - volume = self.db.volume_get(context, volume_id) - self.driver.ensure_export(context.elevated(), volume) - image_service, image_id = glance.get_remote_image_service(context, - image_id) - self.driver.copy_volume_to_image(context, volume, image_service, - image_id) - LOG.debug(_("Uploaded volume %(volume_id)s to " - "image (%(image_id)s) successfully") % locals()) - except Exception, error: - with excutils.save_and_reraise_exception(): - payload['message'] = unicode(error) - finally: - if volume['instance_uuid'] is None: - self.db.volume_update(context, volume_id, - {'status': 'available'}) - else: - self.db.volume_update(context, volume_id, - {'status': 'in-use'}) - - def initialize_connection(self, context, volume_id, connector): - """Prepare volume for connection from host represented by connector. - - This method calls the driver initialize_connection and returns - it to the caller. The connector parameter is a dictionary with - information about the host that will connect to the volume in the - following format:: - - { - 'ip': ip, - 'initiator': initiator, - } - - ip: the ip address of the connecting machine - - initiator: the iscsi initiator name of the connecting machine. - This can be None if the connecting machine does not support iscsi - connections. - - driver is responsible for doing any necessary security setup and - returning a connection_info dictionary in the following format:: - - { - 'driver_volume_type': driver_volume_type, - 'data': data, - } - - driver_volume_type: a string to identify the type of volume. This - can be used by the calling code to determine the - strategy for connecting to the volume. This could - be 'iscsi', 'rbd', 'sheepdog', etc. - - data: this is the data that the calling code will use to connect - to the volume. Keep in mind that this will be serialized to - json in various places, so it should not contain any non-json - data types. - """ - volume_ref = self.db.volume_get(context, volume_id) - return self.driver.initialize_connection(volume_ref, connector) - - def terminate_connection(self, context, volume_id, connector): - """Cleanup connection from host represented by connector. - - The format of connector is the same as for initialize_connection. - """ - volume_ref = self.db.volume_get(context, volume_id) - self.driver.terminate_connection(volume_ref, connector) - - def check_for_export(self, context, instance_id): - """Make sure whether volume is exported.""" - instance_ref = self.db.instance_get(context, instance_id) - volumes = self.db.volume_get_all_by_instance_uuid(context, - instance_ref['uuid']) - - for volume in volumes: - self.driver.check_for_export(context, volume['id']) - - def _volume_stats_changed(self, stat1, stat2): - if FLAGS.volume_force_update_capabilities: - return True - if len(stat1) != len(stat2): - return True - for (k, v) in stat1.iteritems(): - if (k, v) not in stat2.iteritems(): - return True - return False - - @manager.periodic_task - def _report_driver_status(self, context): - volume_stats = self.driver.get_volume_stats(refresh=True) - if volume_stats: - LOG.info(_("Checking volume capabilities")) - - if self._volume_stats_changed(self._last_volume_stats, - volume_stats): - LOG.info(_("New capabilities found: %s"), volume_stats) - self._last_volume_stats = volume_stats - - # This will grab info about the host and queue it - # to be sent to the Schedulers. - self.update_service_capabilities(self._last_volume_stats) - else: - # avoid repeating fanouts - self.update_service_capabilities(None) - - def _reset_stats(self): - LOG.info(_("Clear capabilities")) - self._last_volume_stats = [] - - def notification(self, context, event): - LOG.info(_("Notification {%s} received"), event) - self._reset_stats() - - def _notify_about_volume_usage(self, context, volume, event_suffix, - extra_usage_info=None): - volume_utils.notify_about_volume_usage( - context, volume, event_suffix, - extra_usage_info=extra_usage_info, host=self.host) diff --git a/nova/volume/netapp.py b/nova/volume/netapp.py deleted file mode 100644 index b1b3915ab..000000000 --- a/nova/volume/netapp.py +++ /dev/null @@ -1,1291 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# Copyright (c) 2012 OpenStack LLC. -# All Rights Reserved. -# -# 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. -""" -Volume driver for NetApp storage systems. - -This driver requires NetApp OnCommand 5.0 and one or more Data -ONTAP 7-mode storage systems with installed iSCSI licenses. - -""" - -import time - -import suds -from suds import client -from suds.sax import text - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.volume import driver -from nova.volume import volume_types - -LOG = logging.getLogger(__name__) - -netapp_opts = [ - cfg.StrOpt('netapp_wsdl_url', - default=None, - help='URL of the WSDL file for the DFM server'), - cfg.StrOpt('netapp_login', - default=None, - help='User name for the DFM server'), - cfg.StrOpt('netapp_password', - default=None, - help='Password for the DFM server'), - cfg.StrOpt('netapp_server_hostname', - default=None, - help='Hostname for the DFM server'), - cfg.IntOpt('netapp_server_port', - default=8088, - help='Port number for the DFM server'), - cfg.StrOpt('netapp_storage_service', - default=None, - help=('Storage service to use for provisioning ' - '(when volume_type=None)')), - cfg.StrOpt('netapp_storage_service_prefix', - default=None, - help=('Prefix of storage service name to use for ' - 'provisioning (volume_type name will be appended)')), - cfg.StrOpt('netapp_vfiler', - default=None, - help='Vfiler to use for provisioning'), - ] - -FLAGS = flags.FLAGS -FLAGS.register_opts(netapp_opts) - - -class DfmDataset(object): - def __init__(self, id, name, project, type): - self.id = id - self.name = name - self.project = project - self.type = type - - -class DfmLun(object): - def __init__(self, dataset, lunpath, id): - self.dataset = dataset - self.lunpath = lunpath - self.id = id - - -class NetAppISCSIDriver(driver.ISCSIDriver): - """NetApp iSCSI volume driver.""" - - IGROUP_PREFIX = 'openstack-' - DATASET_PREFIX = 'OpenStack_' - DATASET_METADATA_PROJECT_KEY = 'OpenStackProject' - DATASET_METADATA_VOL_TYPE_KEY = 'OpenStackVolType' - - def __init__(self, *args, **kwargs): - super(NetAppISCSIDriver, self).__init__(*args, **kwargs) - self.discovered_luns = [] - self.discovered_datasets = [] - self.lun_table = {} - - def _check_fail(self, request, response): - """Utility routine to handle checking ZAPI failures.""" - if 'failed' == response.Status: - name = request.Name - reason = response.Reason - msg = _('API %(name)s failed: %(reason)s') - raise exception.VolumeBackendAPIException(data=msg % locals()) - - def _create_client(self, **kwargs): - """Instantiate a web services client. - - This method creates a "suds" client to make web services calls to the - DFM server. Note that the WSDL file is quite large and may take - a few seconds to parse. - """ - wsdl_url = kwargs['wsdl_url'] - LOG.debug(_('Using WSDL: %s') % wsdl_url) - if kwargs['cache']: - self.client = client.Client(wsdl_url, username=kwargs['login'], - password=kwargs['password']) - else: - self.client = client.Client(wsdl_url, username=kwargs['login'], - password=kwargs['password'], - cache=None) - soap_url = 'http://%s:%s/apis/soap/v1' % (kwargs['hostname'], - kwargs['port']) - LOG.debug(_('Using DFM server: %s') % soap_url) - self.client.set_options(location=soap_url) - - def _set_storage_service(self, storage_service): - """Set the storage service to use for provisioning.""" - LOG.debug(_('Using storage service: %s') % storage_service) - self.storage_service = storage_service - - def _set_storage_service_prefix(self, storage_service_prefix): - """Set the storage service prefix to use for provisioning.""" - LOG.debug(_('Using storage service prefix: %s') % - storage_service_prefix) - self.storage_service_prefix = storage_service_prefix - - def _set_vfiler(self, vfiler): - """Set the vfiler to use for provisioning.""" - LOG.debug(_('Using vfiler: %s') % vfiler) - self.vfiler = vfiler - - def _check_flags(self): - """Ensure that the flags we care about are set.""" - required_flags = ['netapp_wsdl_url', 'netapp_login', 'netapp_password', - 'netapp_server_hostname', 'netapp_server_port'] - for flag in required_flags: - if not getattr(FLAGS, flag, None): - raise exception.InvalidInput(reason=_('%s is not set') % flag) - if not (FLAGS.netapp_storage_service or - FLAGS.netapp_storage_service_prefix): - raise exception.InvalidInput(reason=_('Either ' - 'netapp_storage_service or netapp_storage_service_prefix must ' - 'be set')) - - def do_setup(self, context): - """Setup the NetApp Volume driver. - - Called one time by the manager after the driver is loaded. - Validate the flags we care about and setup the suds (web services) - client. - """ - self._check_flags() - self._create_client(wsdl_url=FLAGS.netapp_wsdl_url, - login=FLAGS.netapp_login, password=FLAGS.netapp_password, - hostname=FLAGS.netapp_server_hostname, - port=FLAGS.netapp_server_port, cache=True) - self._set_storage_service(FLAGS.netapp_storage_service) - self._set_storage_service_prefix(FLAGS.netapp_storage_service_prefix) - self._set_vfiler(FLAGS.netapp_vfiler) - - def check_for_setup_error(self): - """Check that the driver is working and can communicate. - - Invoke a web services API to make sure we can talk to the server. - Also perform the discovery of datasets and LUNs from DFM. - """ - self.client.service.DfmAbout() - LOG.debug(_("Connected to DFM server")) - self._discover_luns() - - def _get_datasets(self): - """Get the list of datasets from DFM.""" - server = self.client.service - res = server.DatasetListInfoIterStart(IncludeMetadata=True) - tag = res.Tag - datasets = [] - try: - while True: - res = server.DatasetListInfoIterNext(Tag=tag, Maximum=100) - if not res.Datasets: - break - datasets.extend(res.Datasets.DatasetInfo) - finally: - server.DatasetListInfoIterEnd(Tag=tag) - return datasets - - def _discover_dataset_luns(self, dataset, volume): - """Discover all of the LUNs in a dataset.""" - server = self.client.service - res = server.DatasetMemberListInfoIterStart( - DatasetNameOrId=dataset.id, - IncludeExportsInfo=True, - IncludeIndirect=True, - MemberType='lun_path') - tag = res.Tag - suffix = None - if volume: - suffix = '/' + volume - try: - while True: - res = server.DatasetMemberListInfoIterNext(Tag=tag, - Maximum=100) - if (not hasattr(res, 'DatasetMembers') or - not res.DatasetMembers): - break - for member in res.DatasetMembers.DatasetMemberInfo: - if suffix and not member.MemberName.endswith(suffix): - continue - # MemberName is the full LUN path in this format: - # host:/volume/qtree/lun - lun = DfmLun(dataset, member.MemberName, member.MemberId) - self.discovered_luns.append(lun) - finally: - server.DatasetMemberListInfoIterEnd(Tag=tag) - - def _discover_luns(self): - """Discover the LUNs from DFM. - - Discover all of the OpenStack-created datasets and LUNs in the DFM - database. - """ - datasets = self._get_datasets() - self.discovered_datasets = [] - self.discovered_luns = [] - for dataset in datasets: - if not dataset.DatasetName.startswith(self.DATASET_PREFIX): - continue - if (not hasattr(dataset, 'DatasetMetadata') or - not dataset.DatasetMetadata): - continue - project = None - type = None - for field in dataset.DatasetMetadata.DfmMetadataField: - if field.FieldName == self.DATASET_METADATA_PROJECT_KEY: - project = field.FieldValue - elif field.FieldName == self.DATASET_METADATA_VOL_TYPE_KEY: - type = field.FieldValue - if not project: - continue - ds = DfmDataset(dataset.DatasetId, dataset.DatasetName, - project, type) - self.discovered_datasets.append(ds) - self._discover_dataset_luns(ds, None) - dataset_count = len(self.discovered_datasets) - lun_count = len(self.discovered_luns) - msg = _("Discovered %(dataset_count)s datasets and %(lun_count)s LUNs") - LOG.debug(msg % locals()) - self.lun_table = {} - - def _get_job_progress(self, job_id): - """Get progress of one running DFM job. - - Obtain the latest progress report for the job and return the - list of progress events. - """ - server = self.client.service - res = server.DpJobProgressEventListIterStart(JobId=job_id) - tag = res.Tag - event_list = [] - try: - while True: - res = server.DpJobProgressEventListIterNext(Tag=tag, - Maximum=100) - if not hasattr(res, 'ProgressEvents'): - break - event_list += res.ProgressEvents.DpJobProgressEventInfo - finally: - server.DpJobProgressEventListIterEnd(Tag=tag) - return event_list - - def _wait_for_job(self, job_id): - """Wait until a job terminates. - - Poll the job until it completes or an error is detected. Return the - final list of progress events if it completes successfully. - """ - while True: - events = self._get_job_progress(job_id) - for event in events: - if event.EventStatus == 'error': - msg = _('Job failed: %s') % (event.ErrorMessage) - raise exception.VolumeBackendAPIException(data=msg) - if event.EventType == 'job-end': - return events - time.sleep(5) - - def _dataset_name(self, project, ss_type): - """Return the dataset name for a given project and volume type.""" - _project = project.replace(' ', '_').replace('-', '_') - dataset_name = self.DATASET_PREFIX + _project - if not ss_type: - return dataset_name - _type = ss_type.replace(' ', '_').replace('-', '_') - return dataset_name + '_' + _type - - def _get_dataset(self, dataset_name): - """Lookup a dataset by name in the list of discovered datasets.""" - for dataset in self.discovered_datasets: - if dataset.name == dataset_name: - return dataset - return None - - def _create_dataset(self, dataset_name, project, ss_type): - """Create a new dataset using the storage service. - - The export settings are set to create iSCSI LUNs aligned for Linux. - Returns the ID of the new dataset. - """ - if ss_type and not self.storage_service_prefix: - msg = _('Attempt to use volume_type without specifying ' - 'netapp_storage_service_prefix flag.') - raise exception.VolumeBackendAPIException(data=msg) - if not (ss_type or self.storage_service): - msg = _('You must set the netapp_storage_service flag in order to ' - 'create volumes with no volume_type.') - raise exception.VolumeBackendAPIException(data=msg) - storage_service = self.storage_service - if ss_type: - storage_service = self.storage_service_prefix + ss_type - - factory = self.client.factory - - lunmap = factory.create('DatasetLunMappingInfo') - lunmap.IgroupOsType = 'linux' - export = factory.create('DatasetExportInfo') - export.DatasetExportProtocol = 'iscsi' - export.DatasetLunMappingInfo = lunmap - detail = factory.create('StorageSetInfo') - detail.DpNodeName = 'Primary data' - detail.DatasetExportInfo = export - if hasattr(self, 'vfiler') and self.vfiler: - detail.ServerNameOrId = self.vfiler - details = factory.create('ArrayOfStorageSetInfo') - details.StorageSetInfo = [detail] - field1 = factory.create('DfmMetadataField') - field1.FieldName = self.DATASET_METADATA_PROJECT_KEY - field1.FieldValue = project - field2 = factory.create('DfmMetadataField') - field2.FieldName = self.DATASET_METADATA_VOL_TYPE_KEY - field2.FieldValue = ss_type - metadata = factory.create('ArrayOfDfmMetadataField') - metadata.DfmMetadataField = [field1, field2] - - res = self.client.service.StorageServiceDatasetProvision( - StorageServiceNameOrId=storage_service, - DatasetName=dataset_name, - AssumeConfirmation=True, - StorageSetDetails=details, - DatasetMetadata=metadata) - - ds = DfmDataset(res.DatasetId, dataset_name, project, ss_type) - self.discovered_datasets.append(ds) - return ds - - def _provision(self, name, description, project, ss_type, size): - """Provision a LUN through provisioning manager. - - The LUN will be created inside a dataset associated with the project. - If the dataset doesn't already exist, we create it using the storage - service specified in the nova conf. - """ - dataset_name = self._dataset_name(project, ss_type) - dataset = self._get_dataset(dataset_name) - if not dataset: - dataset = self._create_dataset(dataset_name, project, ss_type) - - info = self.client.factory.create('ProvisionMemberRequestInfo') - info.Name = name - if description: - info.Description = description - info.Size = size - info.MaximumSnapshotSpace = 2 * long(size) - - server = self.client.service - lock_id = server.DatasetEditBegin(DatasetNameOrId=dataset.id) - try: - server.DatasetProvisionMember(EditLockId=lock_id, - ProvisionMemberRequestInfo=info) - res = server.DatasetEditCommit(EditLockId=lock_id, - AssumeConfirmation=True) - except (suds.WebFault, Exception): - server.DatasetEditRollback(EditLockId=lock_id) - msg = _('Failed to provision dataset member') - raise exception.VolumeBackendAPIException(data=msg) - - lun_id = None - lunpath = None - - for info in res.JobIds.JobInfo: - events = self._wait_for_job(info.JobId) - for event in events: - if event.EventType != 'lun-create': - continue - lunpath = event.ProgressLunInfo.LunName - lun_id = event.ProgressLunInfo.LunPathId - - if not lun_id: - msg = _('No LUN was created by the provision job') - raise exception.VolumeBackendAPIException(data=msg) - - lun = DfmLun(dataset, lunpath, lun_id) - self.discovered_luns.append(lun) - self.lun_table[name] = lun - - def _get_ss_type(self, volume): - """Get the storage service type for a volume.""" - id = volume['volume_type_id'] - if not id: - return None - volume_type = volume_types.get_volume_type(None, id) - if not volume_type: - return None - return volume_type['name'] - - def _remove_destroy(self, name, project): - """Remove the LUN from the dataset, also destroying it. - - Remove the LUN from the dataset and destroy the actual LUN on the - storage system. - """ - lun = self._lookup_lun_for_volume(name, project) - member = self.client.factory.create('DatasetMemberParameter') - member.ObjectNameOrId = lun.id - members = self.client.factory.create('ArrayOfDatasetMemberParameter') - members.DatasetMemberParameter = [member] - - server = self.client.service - lock_id = server.DatasetEditBegin(DatasetNameOrId=lun.dataset.id) - try: - server.DatasetRemoveMember(EditLockId=lock_id, Destroy=True, - DatasetMemberParameters=members) - server.DatasetEditCommit(EditLockId=lock_id, - AssumeConfirmation=True) - except (suds.WebFault, Exception): - server.DatasetEditRollback(EditLockId=lock_id) - msg = _('Failed to remove and delete dataset member') - raise exception.VolumeBackendAPIException(data=msg) - - def create_volume(self, volume): - """Driver entry point for creating a new volume.""" - default_size = '104857600' # 100 MB - gigabytes = 1073741824L # 2^30 - name = volume['name'] - project = volume['project_id'] - display_name = volume['display_name'] - display_description = volume['display_description'] - description = None - if display_name: - if display_description: - description = display_name + "\n" + display_description - else: - description = display_name - elif display_description: - description = display_description - if int(volume['size']) == 0: - size = default_size - else: - size = str(int(volume['size']) * gigabytes) - ss_type = self._get_ss_type(volume) - self._provision(name, description, project, ss_type, size) - - def _lookup_lun_for_volume(self, name, project): - """Lookup the LUN that corresponds to the give volume. - - Initial lookups involve a table scan of all of the discovered LUNs, - but later lookups are done instantly from the hashtable. - """ - if name in self.lun_table: - return self.lun_table[name] - lunpath_suffix = '/' + name - for lun in self.discovered_luns: - if lun.dataset.project != project: - continue - if lun.lunpath.endswith(lunpath_suffix): - self.lun_table[name] = lun - return lun - msg = _("No entry in LUN table for volume %s") % (name) - raise exception.VolumeBackendAPIException(data=msg) - - def delete_volume(self, volume): - """Driver entry point for destroying existing volumes.""" - name = volume['name'] - project = volume['project_id'] - self._remove_destroy(name, project) - - def _get_lun_details(self, lun_id): - """Given the ID of a LUN, get the details about that LUN.""" - server = self.client.service - res = server.LunListInfoIterStart(ObjectNameOrId=lun_id) - tag = res.Tag - try: - res = server.LunListInfoIterNext(Tag=tag, Maximum=1) - if hasattr(res, 'Luns') and res.Luns.LunInfo: - return res.Luns.LunInfo[0] - finally: - server.LunListInfoIterEnd(Tag=tag) - msg = _('Failed to get LUN details for LUN ID %s') - raise exception.VolumeBackendAPIException(data=msg % lun_id) - - def _get_host_details(self, host_id): - """Given the ID of a host, get the details about it. - - A "host" is a storage system here. - """ - server = self.client.service - res = server.HostListInfoIterStart(ObjectNameOrId=host_id) - tag = res.Tag - try: - res = server.HostListInfoIterNext(Tag=tag, Maximum=1) - if hasattr(res, 'Hosts') and res.Hosts.HostInfo: - return res.Hosts.HostInfo[0] - finally: - server.HostListInfoIterEnd(Tag=tag) - msg = _('Failed to get host details for host ID %s') - raise exception.VolumeBackendAPIException(data=msg % host_id) - - def _get_iqn_for_host(self, host_id): - """Get the iSCSI Target Name for a storage system.""" - request = self.client.factory.create('Request') - request.Name = 'iscsi-node-get-name' - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - return response.Results['node-name'][0] - - def _api_elem_is_empty(self, elem): - """Return true if the API element should be considered empty. - - Helper routine to figure out if a list returned from a proxy API - is empty. This is necessary because the API proxy produces nasty - looking XML. - """ - if not type(elem) is list: - return True - if 0 == len(elem): - return True - child = elem[0] - if isinstance(child, text.Text): - return True - if type(child) is str: - return True - return False - - def _get_target_portal_for_host(self, host_id, host_address): - """Get iSCSI target portal for a storage system. - - Get the iSCSI Target Portal details for a particular IP address - on a storage system. - """ - request = self.client.factory.create('Request') - request.Name = 'iscsi-portal-list-info' - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - portal = {} - portals = response.Results['iscsi-portal-list-entries'] - if self._api_elem_is_empty(portals): - return portal - portal_infos = portals[0]['iscsi-portal-list-entry-info'] - for portal_info in portal_infos: - portal['address'] = portal_info['ip-address'][0] - portal['port'] = portal_info['ip-port'][0] - portal['portal'] = portal_info['tpgroup-tag'][0] - if host_address == portal['address']: - break - return portal - - def _get_export(self, volume): - """Get the iSCSI export details for a volume. - - Looks up the LUN in DFM based on the volume and project name, then get - the LUN's ID. We store that value in the database instead of the iSCSI - details because we will not have the true iSCSI details until masking - time (when initialize_connection() is called). - """ - name = volume['name'] - project = volume['project_id'] - lun = self._lookup_lun_for_volume(name, project) - return {'provider_location': lun.id} - - def ensure_export(self, context, volume): - """Driver entry point to get the export info for an existing volume.""" - return self._get_export(volume) - - def create_export(self, context, volume): - """Driver entry point to get the export info for a new volume.""" - return self._get_export(volume) - - def remove_export(self, context, volume): - """Driver exntry point to remove an export for a volume. - - Since exporting is idempotent in this driver, we have nothing - to do for unexporting. - """ - pass - - def _find_igroup_for_initiator(self, host_id, initiator_name): - """Get the igroup for an initiator. - - Look for an existing igroup (initiator group) on the storage system - containing a given iSCSI initiator and return the name of the igroup. - """ - request = self.client.factory.create('Request') - request.Name = 'igroup-list-info' - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - igroups = response.Results['initiator-groups'] - if self._api_elem_is_empty(igroups): - return None - igroup_infos = igroups[0]['initiator-group-info'] - for igroup_info in igroup_infos: - if ('iscsi' != igroup_info['initiator-group-type'][0] or - 'linux' != igroup_info['initiator-group-os-type'][0]): - continue - igroup_name = igroup_info['initiator-group-name'][0] - if not igroup_name.startswith(self.IGROUP_PREFIX): - continue - initiators = igroup_info['initiators'][0]['initiator-info'] - for initiator in initiators: - if initiator_name == initiator['initiator-name'][0]: - return igroup_name - return None - - def _create_igroup(self, host_id, initiator_name): - """Create a new igroup. - - Create a new igroup (initiator group) on the storage system to hold - the given iSCSI initiator. The group will only have 1 member and will - be named "openstack-${initiator_name}". - """ - igroup_name = self.IGROUP_PREFIX + initiator_name - request = self.client.factory.create('Request') - request.Name = 'igroup-create' - igroup_create_xml = ( - '<initiator-group-name>%s</initiator-group-name>' - '<initiator-group-type>iscsi</initiator-group-type>' - '<os-type>linux</os-type><ostype>linux</ostype>') - request.Args = text.Raw(igroup_create_xml % igroup_name) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - request = self.client.factory.create('Request') - request.Name = 'igroup-add' - igroup_add_xml = ( - '<initiator-group-name>%s</initiator-group-name>' - '<initiator>%s</initiator>') - request.Args = text.Raw(igroup_add_xml % (igroup_name, initiator_name)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - return igroup_name - - def _get_lun_mappping(self, host_id, lunpath, igroup_name): - """Get the mapping between a LUN and an igroup. - - Check if a given LUN is already mapped to the given igroup (initiator - group). If the LUN is mapped, also return the LUN number for the - mapping. - """ - request = self.client.factory.create('Request') - request.Name = 'lun-map-list-info' - request.Args = text.Raw('<path>%s</path>' % (lunpath)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - igroups = response.Results['initiator-groups'] - if self._api_elem_is_empty(igroups): - return {'mapped': False} - igroup_infos = igroups[0]['initiator-group-info'] - for igroup_info in igroup_infos: - if igroup_name == igroup_info['initiator-group-name'][0]: - return {'mapped': True, 'lun_num': igroup_info['lun-id'][0]} - return {'mapped': False} - - def _map_initiator(self, host_id, lunpath, igroup_name): - """Map a LUN to an igroup. - - Map the given LUN to the given igroup (initiator group). Return the LUN - number that the LUN was mapped to (the filer will choose the lowest - available number). - """ - request = self.client.factory.create('Request') - request.Name = 'lun-map' - lun_map_xml = ('<initiator-group>%s</initiator-group>' - '<path>%s</path>') - request.Args = text.Raw(lun_map_xml % (igroup_name, lunpath)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - return response.Results['lun-id-assigned'][0] - - def _unmap_initiator(self, host_id, lunpath, igroup_name): - """Unmap the given LUN from the given igroup (initiator group).""" - request = self.client.factory.create('Request') - request.Name = 'lun-unmap' - lun_unmap_xml = ('<initiator-group>%s</initiator-group>' - '<path>%s</path>') - request.Args = text.Raw(lun_unmap_xml % (igroup_name, lunpath)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - - def _ensure_initiator_mapped(self, host_id, lunpath, initiator_name): - """Ensure that a LUN is mapped to a particular initiator. - - Check if a LUN is mapped to a given initiator already and create - the mapping if it is not. A new igroup will be created if needed. - Returns the LUN number for the mapping between the LUN and initiator - in both cases. - """ - lunpath = '/vol/' + lunpath - igroup_name = self._find_igroup_for_initiator(host_id, initiator_name) - if not igroup_name: - igroup_name = self._create_igroup(host_id, initiator_name) - - mapping = self._get_lun_mappping(host_id, lunpath, igroup_name) - if mapping['mapped']: - return mapping['lun_num'] - return self._map_initiator(host_id, lunpath, igroup_name) - - def _ensure_initiator_unmapped(self, host_id, lunpath, initiator_name): - """Ensure that a LUN is not mapped to a particular initiator. - - Check if a LUN is mapped to a given initiator and remove the - mapping if it is. This does not destroy the igroup. - """ - lunpath = '/vol/' + lunpath - igroup_name = self._find_igroup_for_initiator(host_id, initiator_name) - if not igroup_name: - return - - mapping = self._get_lun_mappping(host_id, lunpath, igroup_name) - if mapping['mapped']: - self._unmap_initiator(host_id, lunpath, igroup_name) - - def initialize_connection(self, volume, connector): - """Driver entry point to attach a volume to an instance. - - Do the LUN masking on the storage system so the initiator can access - the LUN on the target. Also return the iSCSI properties so the - initiator can find the LUN. This implementation does not call - _get_iscsi_properties() to get the properties because cannot store the - LUN number in the database. We only find out what the LUN number will - be during this method call so we construct the properties dictionary - ourselves. - """ - initiator_name = connector['initiator'] - lun_id = volume['provider_location'] - if not lun_id: - msg = _("No LUN ID for volume %s") % volume['name'] - raise exception.VolumeBackendAPIException(data=msg) - lun = self._get_lun_details(lun_id) - lun_num = self._ensure_initiator_mapped(lun.HostId, lun.LunPath, - initiator_name) - host = self._get_host_details(lun.HostId) - portal = self._get_target_portal_for_host(host.HostId, - host.HostAddress) - if not portal: - msg = _('Failed to get target portal for filer: %s') - raise exception.VolumeBackendAPIException(data=msg % host.HostName) - - iqn = self._get_iqn_for_host(host.HostId) - if not iqn: - msg = _('Failed to get target IQN for filer: %s') - raise exception.VolumeBackendAPIException(data=msg % host.HostName) - - properties = {} - properties['target_discovered'] = False - (address, port) = (portal['address'], portal['port']) - properties['target_portal'] = '%s:%s' % (address, port) - properties['target_iqn'] = iqn - properties['target_lun'] = lun_num - properties['volume_id'] = volume['id'] - - auth = volume['provider_auth'] - if auth: - (auth_method, auth_username, auth_secret) = auth.split() - - properties['auth_method'] = auth_method - properties['auth_username'] = auth_username - properties['auth_password'] = auth_secret - - return { - 'driver_volume_type': 'iscsi', - 'data': properties, - } - - def terminate_connection(self, volume, connector): - """Driver entry point to unattach a volume from an instance. - - Unmask the LUN on the storage system so the given intiator can no - longer access it. - """ - initiator_name = connector['initiator'] - lun_id = volume['provider_location'] - if not lun_id: - msg = _('No LUN ID for volume %s') % volume['name'] - raise exception.VolumeBackendAPIException(data=msg) - lun = self._get_lun_details(lun_id) - self._ensure_initiator_unmapped(lun.HostId, lun.LunPath, - initiator_name) - - def _is_clone_done(self, host_id, clone_op_id, volume_uuid): - """Check the status of a clone operation. - - Return True if done, False otherwise. - """ - request = self.client.factory.create('Request') - request.Name = 'clone-list-status' - clone_list_status_xml = ( - '<clone-id><clone-id-info>' - '<clone-op-id>%s</clone-op-id>' - '<volume-uuid>%s</volume-uuid>' - '</clone-id-info></clone-id>') - request.Args = text.Raw(clone_list_status_xml % (clone_op_id, - volume_uuid)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - status = response.Results['status'] - if self._api_elem_is_empty(status): - return False - ops_info = status[0]['ops-info'][0] - state = ops_info['clone-state'][0] - return 'completed' == state - - def _clone_lun(self, host_id, src_path, dest_path, snap): - """Create a clone of a NetApp LUN. - - The clone initially consumes no space and is not space reserved. - """ - request = self.client.factory.create('Request') - request.Name = 'clone-start' - clone_start_xml = ( - '<source-path>%s</source-path><no-snap>%s</no-snap>' - '<destination-path>%s</destination-path>') - if snap: - no_snap = 'false' - else: - no_snap = 'true' - request.Args = text.Raw(clone_start_xml % (src_path, no_snap, - dest_path)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - clone_id = response.Results['clone-id'][0] - clone_id_info = clone_id['clone-id-info'][0] - clone_op_id = clone_id_info['clone-op-id'][0] - volume_uuid = clone_id_info['volume-uuid'][0] - while not self._is_clone_done(host_id, clone_op_id, volume_uuid): - time.sleep(5) - - def _refresh_dfm_luns(self, host_id): - """Refresh the LUN list for one filer in DFM.""" - server = self.client.service - server.DfmObjectRefresh(ObjectNameOrId=host_id, ChildType='lun_path') - while True: - time.sleep(15) - res = server.DfmMonitorTimestampList(HostNameOrId=host_id) - for timestamp in res.DfmMonitoringTimestamp: - if 'lun' != timestamp.MonitorName: - continue - if timestamp.LastMonitoringTimestamp: - return - - def _destroy_lun(self, host_id, lun_path): - """Destroy a LUN on the filer.""" - request = self.client.factory.create('Request') - request.Name = 'lun-offline' - path_xml = '<path>%s</path>' - request.Args = text.Raw(path_xml % lun_path) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - request = self.client.factory.create('Request') - request.Name = 'lun-destroy' - request.Args = text.Raw(path_xml % lun_path) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - - def _resize_volume(self, host_id, vol_name, new_size): - """Resize the volume by the amount requested.""" - request = self.client.factory.create('Request') - request.Name = 'volume-size' - volume_size_xml = ( - '<volume>%s</volume><new-size>%s</new-size>') - request.Args = text.Raw(volume_size_xml % (vol_name, new_size)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - - def _create_qtree(self, host_id, vol_name, qtree_name): - """Create a qtree the filer.""" - request = self.client.factory.create('Request') - request.Name = 'qtree-create' - qtree_create_xml = ( - '<mode>0755</mode><volume>%s</volume><qtree>%s</qtree>') - request.Args = text.Raw(qtree_create_xml % (vol_name, qtree_name)) - response = self.client.service.ApiProxy(Target=host_id, - Request=request) - self._check_fail(request, response) - - def create_snapshot(self, snapshot): - """Driver entry point for creating a snapshot. - - This driver implements snapshots by using efficient single-file - (LUN) cloning. - """ - vol_name = snapshot['volume_name'] - snapshot_name = snapshot['name'] - project = snapshot['project_id'] - lun = self._lookup_lun_for_volume(vol_name, project) - lun_id = lun.id - lun = self._get_lun_details(lun_id) - extra_gb = snapshot['volume_size'] - new_size = '+%dg' % extra_gb - self._resize_volume(lun.HostId, lun.VolumeName, new_size) - # LunPath is the partial LUN path in this format: volume/qtree/lun - lun_path = str(lun.LunPath) - lun_name = lun_path[lun_path.rfind('/') + 1:] - qtree_path = '/vol/%s/%s' % (lun.VolumeName, lun.QtreeName) - src_path = '%s/%s' % (qtree_path, lun_name) - dest_path = '%s/%s' % (qtree_path, snapshot_name) - self._clone_lun(lun.HostId, src_path, dest_path, True) - - def delete_snapshot(self, snapshot): - """Driver entry point for deleting a snapshot.""" - vol_name = snapshot['volume_name'] - snapshot_name = snapshot['name'] - project = snapshot['project_id'] - lun = self._lookup_lun_for_volume(vol_name, project) - lun_id = lun.id - lun = self._get_lun_details(lun_id) - lun_path = '/vol/%s/%s/%s' % (lun.VolumeName, lun.QtreeName, - snapshot_name) - self._destroy_lun(lun.HostId, lun_path) - extra_gb = snapshot['volume_size'] - new_size = '-%dg' % extra_gb - self._resize_volume(lun.HostId, lun.VolumeName, new_size) - - def create_volume_from_snapshot(self, volume, snapshot): - """Driver entry point for creating a new volume from a snapshot. - - Many would call this "cloning" and in fact we use cloning to implement - this feature. - """ - vol_size = volume['size'] - snap_size = snapshot['volume_size'] - if vol_size != snap_size: - msg = _('Cannot create volume of size %(vol_size)s from ' - 'snapshot of size %(snap_size)s') - raise exception.VolumeBackendAPIException(data=msg % locals()) - vol_name = snapshot['volume_name'] - snapshot_name = snapshot['name'] - project = snapshot['project_id'] - lun = self._lookup_lun_for_volume(vol_name, project) - lun_id = lun.id - dataset = lun.dataset - old_type = dataset.type - new_type = self._get_ss_type(volume) - if new_type != old_type: - msg = _('Cannot create volume of type %(new_type)s from ' - 'snapshot of type %(old_type)s') - raise exception.VolumeBackendAPIException(data=msg % locals()) - lun = self._get_lun_details(lun_id) - extra_gb = vol_size - new_size = '+%dg' % extra_gb - self._resize_volume(lun.HostId, lun.VolumeName, new_size) - clone_name = volume['name'] - self._create_qtree(lun.HostId, lun.VolumeName, clone_name) - src_path = '/vol/%s/%s/%s' % (lun.VolumeName, lun.QtreeName, - snapshot_name) - dest_path = '/vol/%s/%s/%s' % (lun.VolumeName, clone_name, clone_name) - self._clone_lun(lun.HostId, src_path, dest_path, False) - self._refresh_dfm_luns(lun.HostId) - self._discover_dataset_luns(dataset, clone_name) - - def check_for_export(self, context, volume_id): - raise NotImplementedError() - - -class NetAppLun(object): - """Represents a LUN on NetApp storage.""" - - def __init__(self, handle, name, size, metadata_dict): - self.handle = handle - self.name = name - self.size = size - self.metadata = metadata_dict - - def get_metadata_property(self, prop): - """Get the metadata property of a LUN.""" - if prop in self.metadata: - return self.metadata[prop] - name = self.name - msg = _("No metadata property %(prop)s defined for the LUN %(name)s") - LOG.debug(msg % locals()) - - -class NetAppCmodeISCSIDriver(driver.ISCSIDriver): - """NetApp C-mode iSCSI volume driver.""" - - def __init__(self, *args, **kwargs): - super(NetAppCmodeISCSIDriver, self).__init__(*args, **kwargs) - self.lun_table = {} - - def _create_client(self, **kwargs): - """Instantiate a web services client. - - This method creates a "suds" client to make web services calls to the - DFM server. Note that the WSDL file is quite large and may take - a few seconds to parse. - """ - wsdl_url = kwargs['wsdl_url'] - LOG.debug(_('Using WSDL: %s') % wsdl_url) - if kwargs['cache']: - self.client = client.Client(wsdl_url, username=kwargs['login'], - password=kwargs['password']) - else: - self.client = client.Client(wsdl_url, username=kwargs['login'], - password=kwargs['password'], - cache=None) - - def _check_flags(self): - """Ensure that the flags we care about are set.""" - required_flags = ['netapp_wsdl_url', 'netapp_login', 'netapp_password', - 'netapp_server_hostname', 'netapp_server_port'] - for flag in required_flags: - if not getattr(FLAGS, flag, None): - msg = _('%s is not set') % flag - raise exception.InvalidInput(data=msg) - - def do_setup(self, context): - """Setup the NetApp Volume driver. - - Called one time by the manager after the driver is loaded. - Validate the flags we care about and setup the suds (web services) - client. - """ - self._check_flags() - self._create_client(wsdl_url=FLAGS.netapp_wsdl_url, - login=FLAGS.netapp_login, password=FLAGS.netapp_password, - hostname=FLAGS.netapp_server_hostname, - port=FLAGS.netapp_server_port, cache=True) - - def check_for_setup_error(self): - """Check that the driver is working and can communicate. - - Discovers the LUNs on the NetApp server. - """ - self.lun_table = {} - luns = self.client.service.ListLuns() - for lun in luns: - meta_dict = {} - if hasattr(lun, 'Metadata'): - meta_dict = self._create_dict_from_meta(lun.Metadata) - discovered_lun = NetAppLun(lun.Handle, lun.Name, lun.Size, - meta_dict) - self._add_lun_to_table(discovered_lun) - LOG.debug(_("Success getting LUN list from server")) - - def create_volume(self, volume): - """Driver entry point for creating a new volume.""" - default_size = '104857600' # 100 MB - gigabytes = 1073741824L # 2^30 - name = volume['name'] - if int(volume['size']) == 0: - size = default_size - else: - size = str(int(volume['size']) * gigabytes) - extra_args = {} - extra_args['OsType'] = 'linux' - extra_args['QosType'] = self._get_qos_type(volume) - extra_args['Container'] = volume['project_id'] - extra_args['Display'] = volume['display_name'] - extra_args['Description'] = volume['display_description'] - extra_args['SpaceReserved'] = True - server = self.client.service - metadata = self._create_metadata_list(extra_args) - lun = server.ProvisionLun(Name=name, Size=size, - Metadata=metadata) - LOG.debug(_("Created LUN with name %s") % name) - self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name, - lun.Size, self._create_dict_from_meta(lun.Metadata))) - - def delete_volume(self, volume): - """Driver entry point for destroying existing volumes.""" - name = volume['name'] - handle = self._get_lun_handle(name) - self.client.service.DestroyLun(Handle=handle) - LOG.debug(_("Destroyed LUN %s") % handle) - self.lun_table.pop(name) - - def ensure_export(self, context, volume): - """Driver entry point to get the export info for an existing volume.""" - handle = self._get_lun_handle(volume['name']) - return {'provider_location': handle} - - def create_export(self, context, volume): - """Driver entry point to get the export info for a new volume.""" - handle = self._get_lun_handle(volume['name']) - return {'provider_location': handle} - - def remove_export(self, context, volume): - """Driver exntry point to remove an export for a volume. - - Since exporting is idempotent in this driver, we have nothing - to do for unexporting. - """ - pass - - def initialize_connection(self, volume, connector): - """Driver entry point to attach a volume to an instance. - - Do the LUN masking on the storage system so the initiator can access - the LUN on the target. Also return the iSCSI properties so the - initiator can find the LUN. This implementation does not call - _get_iscsi_properties() to get the properties because cannot store the - LUN number in the database. We only find out what the LUN number will - be during this method call so we construct the properties dictionary - ourselves. - """ - initiator_name = connector['initiator'] - handle = volume['provider_location'] - server = self.client.service - server.MapLun(Handle=handle, InitiatorType="iscsi", - InitiatorName=initiator_name) - msg = _("Mapped LUN %(handle)s to the initiator %(initiator_name)s") - LOG.debug(msg % locals()) - - target_details_list = server.GetLunTargetDetails(Handle=handle, - InitiatorType="iscsi", InitiatorName=initiator_name) - msg = _("Succesfully fetched target details for LUN %(handle)s and " - "initiator %(initiator_name)s") - LOG.debug(msg % locals()) - - if not target_details_list: - msg = _('Failed to get LUN target details for the LUN %s') - raise exception.VolumeBackendAPIException(msg % handle) - target_details = target_details_list[0] - if not target_details.Address and target_details.Port: - msg = _('Failed to get target portal for the LUN %s') - raise exception.VolumeBackendAPIException(msg % handle) - iqn = target_details.Iqn - if not iqn: - msg = _('Failed to get target IQN for the LUN %s') - raise exception.VolumeBackendAPIException(msg % handle) - - properties = {} - properties['target_discovered'] = False - (address, port) = (target_details.Address, target_details.Port) - properties['target_portal'] = '%s:%s' % (address, port) - properties['target_iqn'] = iqn - properties['target_lun'] = target_details.LunNumber - properties['volume_id'] = volume['id'] - - auth = volume['provider_auth'] - if auth: - (auth_method, auth_username, auth_secret) = auth.split() - properties['auth_method'] = auth_method - properties['auth_username'] = auth_username - properties['auth_password'] = auth_secret - - return { - 'driver_volume_type': 'iscsi', - 'data': properties, - } - - def terminate_connection(self, volume, connector): - """Driver entry point to unattach a volume from an instance. - - Unmask the LUN on the storage system so the given intiator can no - longer access it. - """ - initiator_name = connector['initiator'] - handle = volume['provider_location'] - self.client.service.UnmapLun(Handle=handle, InitiatorType="iscsi", - InitiatorName=initiator_name) - msg = _("Unmapped LUN %(handle)s from the initiator " - "%(initiator_name)s") - LOG.debug(msg % locals()) - - def create_snapshot(self, snapshot): - """Driver entry point for creating a snapshot. - - This driver implements snapshots by using efficient single-file - (LUN) cloning. - """ - vol_name = snapshot['volume_name'] - snapshot_name = snapshot['name'] - lun = self.lun_table[vol_name] - extra_args = {'SpaceReserved': False} - self._clone_lun(lun.handle, snapshot_name, extra_args) - - def delete_snapshot(self, snapshot): - """Driver entry point for deleting a snapshot.""" - handle = self._get_lun_handle(snapshot['name']) - self.client.service.DestroyLun(Handle=handle) - LOG.debug(_("Destroyed LUN %s") % handle) - - def create_volume_from_snapshot(self, volume, snapshot): - """Driver entry point for creating a new volume from a snapshot. - - Many would call this "cloning" and in fact we use cloning to implement - this feature. - """ - snapshot_name = snapshot['name'] - lun = self.lun_table[snapshot_name] - new_name = volume['name'] - extra_args = {} - extra_args['OsType'] = 'linux' - extra_args['QosType'] = self._get_qos_type(volume) - extra_args['Container'] = volume['project_id'] - extra_args['Display'] = volume['display_name'] - extra_args['Description'] = volume['display_description'] - extra_args['SpaceReserved'] = True - self._clone_lun(lun.handle, new_name, extra_args) - - def check_for_export(self, context, volume_id): - raise NotImplementedError() - - def _get_qos_type(self, volume): - """Get the storage service type for a volume.""" - type_id = volume['volume_type_id'] - if not type_id: - return None - volume_type = volume_types.get_volume_type(None, type_id) - if not volume_type: - return None - return volume_type['name'] - - def _add_lun_to_table(self, lun): - """Adds LUN to cache table.""" - if not isinstance(lun, NetAppLun): - msg = _("Object is not a NetApp LUN.") - raise exception.VolumeBackendAPIException(data=msg) - self.lun_table[lun.name] = lun - - def _clone_lun(self, handle, new_name, extra_args): - """Clone LUN with the given handle to the new name.""" - server = self.client.service - metadata = self._create_metadata_list(extra_args) - lun = server.CloneLun(Handle=handle, NewName=new_name, - Metadata=metadata) - LOG.debug(_("Cloned LUN with new name %s") % new_name) - self._add_lun_to_table(NetAppLun(lun.Handle, lun.Name, - lun.Size, self._create_dict_from_meta(lun.Metadata))) - - def _create_metadata_list(self, extra_args): - """Creates metadata from kwargs.""" - metadata = [] - for key in extra_args.keys(): - meta = self.client.factory.create("Metadata") - meta.Key = key - meta.Value = extra_args[key] - metadata.append(meta) - return metadata - - def _get_lun_handle(self, name): - """Get the details for a LUN from our cache table.""" - if not name in self.lun_table: - LOG.warn(_("Could not find handle for LUN named %s") % name) - return None - return self.lun_table[name].handle - - def _create_dict_from_meta(self, metadata): - """Creates dictionary from metadata array.""" - meta_dict = {} - if not metadata: - return meta_dict - for meta in metadata: - meta_dict[meta.Key] = meta.Value - return meta_dict diff --git a/nova/volume/netapp_nfs.py b/nova/volume/netapp_nfs.py deleted file mode 100644 index 27d278aa3..000000000 --- a/nova/volume/netapp_nfs.py +++ /dev/null @@ -1,267 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# All Rights Reserved. -# -# 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. -""" -Volume driver for NetApp NFS storage. -""" - -import os -import suds -import time - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.volume.netapp import netapp_opts -from nova.volume import nfs - -from suds.sax import text - -LOG = logging.getLogger(__name__) - -netapp_nfs_opts = [ - cfg.IntOpt('synchronous_snapshot_create', - default=0, - help='Does snapshot creation call returns immediately') - ] - -FLAGS = flags.FLAGS -FLAGS.register_opts(netapp_opts) -FLAGS.register_opts(netapp_nfs_opts) - - -class NetAppNFSDriver(nfs.NfsDriver): - """Executes commands relating to Volumes.""" - def __init__(self, *args, **kwargs): - # NOTE(vish): db is set by Manager - self._execute = None - self._context = None - super(NetAppNFSDriver, self).__init__(*args, **kwargs) - - def set_execute(self, execute): - self._execute = execute - - def do_setup(self, context): - self._context = context - self.check_for_setup_error() - self._client = NetAppNFSDriver._get_client() - - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met""" - NetAppNFSDriver._check_dfm_flags() - super(NetAppNFSDriver, self).check_for_setup_error() - - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - vol_size = volume.size - snap_size = snapshot.volume_size - - if vol_size != snap_size: - msg = _('Cannot create volume of size %(vol_size)s from ' - 'snapshot of size %(snap_size)s') - raise exception.NovaException(msg % locals()) - - self._clone_volume(snapshot.name, volume.name, snapshot.volume_id) - share = self._get_volume_location(snapshot.volume_id) - - return {'provider_location': share} - - def create_snapshot(self, snapshot): - """Creates a snapshot.""" - self._clone_volume(snapshot['volume_name'], - snapshot['name'], - snapshot['volume_id']) - - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - nfs_mount = self._get_provider_location(snapshot.volume_id) - - if self._volume_not_present(nfs_mount, snapshot.name): - return True - - self._execute('rm', self._get_volume_path(nfs_mount, snapshot.name), - run_as_root=True) - - @staticmethod - def _check_dfm_flags(): - """Raises error if any required configuration flag for OnCommand proxy - is missing.""" - required_flags = ['netapp_wsdl_url', - 'netapp_login', - 'netapp_password', - 'netapp_server_hostname', - 'netapp_server_port'] - for flag in required_flags: - if not getattr(FLAGS, flag, None): - raise exception.NovaException(_('%s is not set') % flag) - - @staticmethod - def _get_client(): - """Creates SOAP _client for ONTAP-7 DataFabric Service.""" - client = suds.client.Client(FLAGS.netapp_wsdl_url, - username=FLAGS.netapp_login, - password=FLAGS.netapp_password) - soap_url = 'http://%s:%s/apis/soap/v1' % ( - FLAGS.netapp_server_hostname, - FLAGS.netapp_server_port) - client.set_options(location=soap_url) - - return client - - def _get_volume_location(self, volume_id): - """Returns NFS mount address as <nfs_ip_address>:<nfs_mount_dir>""" - nfs_server_ip = self._get_host_ip(volume_id) - export_path = self._get_export_path(volume_id) - return (nfs_server_ip + ':' + export_path) - - def _clone_volume(self, volume_name, clone_name, volume_id): - """Clones mounted volume with OnCommand proxy API""" - host_id = self._get_host_id(volume_id) - export_path = self._get_full_export_path(volume_id, host_id) - - request = self._client.factory.create('Request') - request.Name = 'clone-start' - - clone_start_args = ('<source-path>%s/%s</source-path>' - '<destination-path>%s/%s</destination-path>') - - request.Args = text.Raw(clone_start_args % (export_path, - volume_name, - export_path, - clone_name)) - - resp = self._client.service.ApiProxy(Target=host_id, - Request=request) - - if resp.Status == 'passed' and FLAGS.synchronous_snapshot_create: - clone_id = resp.Results['clone-id'][0] - clone_id_info = clone_id['clone-id-info'][0] - clone_operation_id = int(clone_id_info['clone-op-id'][0]) - - self._wait_for_clone_finished(clone_operation_id, host_id) - elif resp.Status == 'failed': - raise exception.NovaException(resp.Reason) - - def _wait_for_clone_finished(self, clone_operation_id, host_id): - """ - Polls ONTAP7 for clone status. Returns once clone is finished. - :param clone_operation_id: Identifier of ONTAP clone operation - """ - clone_list_options = ('<clone-id>' - '<clone-id-info>' - '<clone-op-id>%d</clone-op-id>' - '<volume-uuid></volume-uuid>' - '</clone-id>' - '</clone-id-info>') - - request = self._client.factory.create('Request') - request.Name = 'clone-list-status' - request.Args = text.Raw(clone_list_options % clone_operation_id) - - resp = self._client.service.ApiProxy(Target=host_id, Request=request) - - while resp.Status != 'passed': - time.sleep(1) - resp = self._client.service.ApiProxy(Target=host_id, - Request=request) - - def _get_provider_location(self, volume_id): - """ - Returns provider location for given volume - :param volume_id: - """ - volume = self.db.volume_get(self._context, volume_id) - return volume.provider_location - - def _get_host_ip(self, volume_id): - """Returns IP address for the given volume""" - return self._get_provider_location(volume_id).split(':')[0] - - def _get_export_path(self, volume_id): - """Returns NFS export path for the given volume""" - return self._get_provider_location(volume_id).split(':')[1] - - def _get_host_id(self, volume_id): - """Returns ID of the ONTAP-7 host""" - host_ip = self._get_host_ip(volume_id) - server = self._client.service - - resp = server.HostListInfoIterStart(ObjectNameOrId=host_ip) - tag = resp.Tag - - try: - res = server.HostListInfoIterNext(Tag=tag, Maximum=1) - if hasattr(res, 'Hosts') and res.Hosts.HostInfo: - return res.Hosts.HostInfo[0].HostId - finally: - server.HostListInfoIterEnd(Tag=tag) - - def _get_full_export_path(self, volume_id, host_id): - """Returns full path to the NFS share, e.g. /vol/vol0/home""" - export_path = self._get_export_path(volume_id) - command_args = '<pathname>%s</pathname>' - - request = self._client.factory.create('Request') - request.Name = 'nfs-exportfs-storage-path' - request.Args = text.Raw(command_args % export_path) - - resp = self._client.service.ApiProxy(Target=host_id, - Request=request) - - if resp.Status == 'passed': - return resp.Results['actual-pathname'][0] - elif resp.Status == 'failed': - raise exception.NovaException(resp.Reason) - - def _volume_not_present(self, nfs_mount, volume_name): - """ - Check if volume exists - """ - try: - self._try_execute('ls', self._get_volume_path(nfs_mount, - volume_name)) - except exception.ProcessExecutionError: - # If the volume isn't present - return True - return False - - def _try_execute(self, *command, **kwargs): - # NOTE(vish): Volume commands can partially fail due to timing, but - # running them a second time on failure will usually - # recover nicely. - tries = 0 - while True: - try: - self._execute(*command, **kwargs) - return True - except exception.ProcessExecutionError: - tries = tries + 1 - if tries >= FLAGS.num_shell_tries: - raise - LOG.exception(_("Recovering from a failed execute. " - "Try number %s"), tries) - time.sleep(tries ** 2) - - def _get_volume_path(self, nfs_share, volume_name): - """Get volume path (local fs path) for given volume name on given nfs - share - @param nfs_share string, example 172.18.194.100:/var/nfs - @param volume_name string, - example volume-91ee65ec-c473-4391-8c09-162b00c68a8c - """ - return os.path.join(self._get_mount_point_for_share(nfs_share), - volume_name) diff --git a/nova/volume/nexenta/__init__.py b/nova/volume/nexenta/__init__.py deleted file mode 100644 index 3050df8f6..000000000 --- a/nova/volume/nexenta/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2011 Nexenta Systems, Inc. -# All Rights Reserved. -# -# 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. -""" -:mod:`nexenta` -- Package contains Nexenta-specific modules -===================================================================== - -.. automodule:: nexenta -.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> -""" - - -class NexentaException(Exception): - MESSAGE = _('Nexenta SA returned the error') - - def __init__(self, error=None): - super(NexentaException, self).__init__(self.message, error) - - def __str__(self): - return '%s: %s' % self.args diff --git a/nova/volume/nexenta/jsonrpc.py b/nova/volume/nexenta/jsonrpc.py deleted file mode 100644 index e0d9c810a..000000000 --- a/nova/volume/nexenta/jsonrpc.py +++ /dev/null @@ -1,84 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2011 Nexenta Systems, Inc. -# All Rights Reserved. -# -# 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. -""" -:mod:`nexenta.jsonrpc` -- Nexenta-specific JSON RPC client -===================================================================== - -.. automodule:: nexenta.jsonrpc -.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> -""" - -import urllib2 - -from nova.openstack.common import jsonutils -from nova.openstack.common import log as logging -from nova.volume import nexenta - -LOG = logging.getLogger(__name__) - - -class NexentaJSONException(nexenta.NexentaException): - pass - - -class NexentaJSONProxy(object): - def __init__(self, url, user, password, auto=False, obj=None, method=None): - self.url = url - self.user = user - self.password = password - self.auto = auto - self.obj = obj - self.method = method - - def __getattr__(self, name): - if not self.obj: - obj, method = name, None - elif not self.method: - obj, method = self.obj, name - else: - obj, method = '%s.%s' % (self.obj, self.method), name - return NexentaJSONProxy(self.url, self.user, self.password, self.auto, - obj, method) - - def __call__(self, *args): - data = jsonutils.dumps({'object': self.obj, - 'method': self.method, - 'params': args}) - auth = ('%s:%s' % (self.user, self.password)).encode('base64')[:-1] - headers = {'Content-Type': 'application/json', - 'Authorization': 'Basic %s' % (auth,)} - LOG.debug(_('Sending JSON data: %s'), data) - request = urllib2.Request(self.url, data, headers) - response_obj = urllib2.urlopen(request) - if response_obj.info().status == 'EOF in headers': - if self.auto and self.url.startswith('http://'): - LOG.info(_('Auto switching to HTTPS connection to %s'), - self.url) - self.url = 'https' + self.url[4:] - request = urllib2.Request(self.url, data, headers) - response_obj = urllib2.urlopen(request) - else: - LOG.error(_('No headers in server response')) - raise NexentaJSONException(_('Bad response from server')) - - response_data = response_obj.read() - LOG.debug(_('Got response: %s'), response_data) - response = jsonutils.loads(response_data) - if response.get('error') is not None: - raise NexentaJSONException(response['error'].get('message', '')) - else: - return response.get('result') diff --git a/nova/volume/nexenta/volume.py b/nova/volume/nexenta/volume.py deleted file mode 100644 index 9bb6364a9..000000000 --- a/nova/volume/nexenta/volume.py +++ /dev/null @@ -1,282 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright 2011 Nexenta Systems, Inc. -# All Rights Reserved. -# -# 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. -""" -:mod:`nexenta.volume` -- Driver to store volumes on Nexenta Appliance -===================================================================== - -.. automodule:: nexenta.volume -.. moduleauthor:: Yuriy Taraday <yorik.sar@gmail.com> -""" - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.volume import driver -from nova.volume import nexenta -from nova.volume.nexenta import jsonrpc - -LOG = logging.getLogger(__name__) -FLAGS = flags.FLAGS - -nexenta_opts = [ - cfg.StrOpt('nexenta_host', - default='', - help='IP address of Nexenta SA'), - cfg.IntOpt('nexenta_rest_port', - default=2000, - help='HTTP port to connect to Nexenta REST API server'), - cfg.StrOpt('nexenta_rest_protocol', - default='auto', - help='Use http or https for REST connection (default auto)'), - cfg.StrOpt('nexenta_user', - default='admin', - help='User name to connect to Nexenta SA'), - cfg.StrOpt('nexenta_password', - default='nexenta', - help='Password to connect to Nexenta SA'), - cfg.IntOpt('nexenta_iscsi_target_portal_port', - default=3260, - help='Nexenta target portal port'), - cfg.StrOpt('nexenta_volume', - default='nova', - help='pool on SA that will hold all volumes'), - cfg.StrOpt('nexenta_target_prefix', - default='iqn.1986-03.com.sun:02:nova-', - help='IQN prefix for iSCSI targets'), - cfg.StrOpt('nexenta_target_group_prefix', - default='nova/', - help='prefix for iSCSI target groups on SA'), - cfg.StrOpt('nexenta_blocksize', - default='', - help='block size for volumes (blank=default,8KB)'), - cfg.BoolOpt('nexenta_sparse', - default=False, - help='flag to create sparse volumes'), -] -FLAGS.register_opts(nexenta_opts) - - -class NexentaDriver(driver.ISCSIDriver): # pylint: disable=R0921 - """Executes volume driver commands on Nexenta Appliance.""" - - def __init__(self): - super(NexentaDriver, self).__init__() - - def do_setup(self, context): - protocol = FLAGS.nexenta_rest_protocol - auto = protocol == 'auto' - if auto: - protocol = 'http' - self.nms = jsonrpc.NexentaJSONProxy( - '%s://%s:%s/rest/nms/' % (protocol, FLAGS.nexenta_host, - FLAGS.nexenta_rest_port), - FLAGS.nexenta_user, FLAGS.nexenta_password, auto=auto) - - def check_for_setup_error(self): - """Verify that the volume for our zvols exists. - - :raise: :py:exc:`LookupError` - """ - if not self.nms.volume.object_exists(FLAGS.nexenta_volume): - raise LookupError(_("Volume %s does not exist in Nexenta SA"), - FLAGS.nexenta_volume) - - @staticmethod - def _get_zvol_name(volume_name): - """Return zvol name that corresponds given volume name.""" - return '%s/%s' % (FLAGS.nexenta_volume, volume_name) - - @staticmethod - def _get_target_name(volume_name): - """Return iSCSI target name to access volume.""" - return '%s%s' % (FLAGS.nexenta_target_prefix, volume_name) - - @staticmethod - def _get_target_group_name(volume_name): - """Return Nexenta iSCSI target group name for volume.""" - return '%s%s' % (FLAGS.nexenta_target_group_prefix, volume_name) - - def create_volume(self, volume): - """Create a zvol on appliance. - - :param volume: volume reference - """ - self.nms.zvol.create( - self._get_zvol_name(volume['name']), - '%sG' % (volume['size'],), - FLAGS.nexenta_blocksize, FLAGS.nexenta_sparse) - - def delete_volume(self, volume): - """Destroy a zvol on appliance. - - :param volume: volume reference - """ - try: - self.nms.zvol.destroy(self._get_zvol_name(volume['name']), '') - except nexenta.NexentaException as exc: - if "zvol has children" in exc.args[1]: - raise exception.VolumeIsBusy - else: - raise - - def create_snapshot(self, snapshot): - """Create snapshot of existing zvol on appliance. - - :param snapshot: shapshot reference - """ - self.nms.zvol.create_snapshot( - self._get_zvol_name(snapshot['volume_name']), - snapshot['name'], '') - - def create_volume_from_snapshot(self, volume, snapshot): - """Create new volume from other's snapshot on appliance. - - :param volume: reference of volume to be created - :param snapshot: reference of source snapshot - """ - self.nms.zvol.clone( - '%s@%s' % (self._get_zvol_name(snapshot['volume_name']), - snapshot['name']), - self._get_zvol_name(volume['name'])) - - def delete_snapshot(self, snapshot): - """Delete volume's snapshot on appliance. - - :param snapshot: shapshot reference - """ - try: - self.nms.snapshot.destroy( - '%s@%s' % (self._get_zvol_name(snapshot['volume_name']), - snapshot['name']), - '') - except nexenta.NexentaException as exc: - if "snapshot has dependent clones" in exc.args[1]: - raise exception.SnapshotIsBusy - else: - raise - - def local_path(self, volume): - """Return local path to existing local volume. - - We never have local volumes, so it raises NotImplementedError. - - :raise: :py:exc:`NotImplementedError` - """ - LOG.error(_("Call to local_path should not happen." - " Verify that use_local_volumes flag is turned off.")) - raise NotImplementedError - - def _do_export(self, _ctx, volume, ensure=False): - """Do all steps to get zvol exported as LUN 0 at separate target. - - :param volume: reference of volume to be exported - :param ensure: if True, ignore errors caused by already existing - resources - :return: iscsiadm-formatted provider location string - """ - zvol_name = self._get_zvol_name(volume['name']) - target_name = self._get_target_name(volume['name']) - target_group_name = self._get_target_group_name(volume['name']) - - try: - self.nms.iscsitarget.create_target({'target_name': target_name}) - except nexenta.NexentaException as exc: - if not ensure or 'already configured' not in exc.args[1]: - raise - else: - LOG.info(_('Ignored target creation error "%s"' - ' while ensuring export'), exc) - try: - self.nms.stmf.create_targetgroup(target_group_name) - except nexenta.NexentaException as exc: - if not ensure or 'already exists' not in exc.args[1]: - raise - else: - LOG.info(_('Ignored target group creation error "%s"' - ' while ensuring export'), exc) - try: - self.nms.stmf.add_targetgroup_member(target_group_name, - target_name) - except nexenta.NexentaException as exc: - if not ensure or 'already exists' not in exc.args[1]: - raise - else: - LOG.info(_('Ignored target group member addition error "%s"' - ' while ensuring export'), exc) - try: - self.nms.scsidisk.create_lu(zvol_name, {}) - except nexenta.NexentaException as exc: - if not ensure or 'in use' not in exc.args[1]: - raise - else: - LOG.info(_('Ignored LU creation error "%s"' - ' while ensuring export'), exc) - try: - self.nms.scsidisk.add_lun_mapping_entry(zvol_name, { - 'target_group': target_group_name, - 'lun': '0'}) - except nexenta.NexentaException as exc: - if not ensure or 'view entry exists' not in exc.args[1]: - raise - else: - LOG.info(_('Ignored LUN mapping entry addition error "%s"' - ' while ensuring export'), exc) - return '%s:%s,1 %s' % (FLAGS.nexenta_host, - FLAGS.nexenta_iscsi_target_portal_port, - target_name) - - def create_export(self, _ctx, volume): - """Create new export for zvol. - - :param volume: reference of volume to be exported - :return: iscsiadm-formatted provider location string - """ - loc = self._do_export(_ctx, volume, ensure=False) - return {'provider_location': loc} - - def ensure_export(self, _ctx, volume): - """Recreate parts of export if necessary. - - :param volume: reference of volume to be exported - """ - self._do_export(_ctx, volume, ensure=True) - - def remove_export(self, _ctx, volume): - """Destroy all resources created to export zvol. - - :param volume: reference of volume to be unexported - """ - zvol_name = self._get_zvol_name(volume['name']) - target_name = self._get_target_name(volume['name']) - target_group_name = self._get_target_group_name(volume['name']) - self.nms.scsidisk.delete_lu(zvol_name) - - try: - self.nms.stmf.destroy_targetgroup(target_group_name) - except nexenta.NexentaException as exc: - # We assume that target group is already gone - LOG.warn(_('Got error trying to destroy target group' - ' %(target_group)s, assuming it is already gone: %(exc)s'), - {'target_group': target_group_name, 'exc': exc}) - try: - self.nms.iscsitarget.delete_target(target_name) - except nexenta.NexentaException as exc: - # We assume that target is gone as well - LOG.warn(_('Got error trying to delete target %(target)s,' - ' assuming it is already gone: %(exc)s'), - {'target': target_name, 'exc': exc}) diff --git a/nova/volume/nfs.py b/nova/volume/nfs.py deleted file mode 100644 index 7a6ca8307..000000000 --- a/nova/volume/nfs.py +++ /dev/null @@ -1,293 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 NetApp, Inc. -# All Rights Reserved. -# -# 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 errno -import hashlib -import os - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.virt.libvirt import volume_nfs -from nova.volume import driver - -LOG = logging.getLogger(__name__) - -volume_opts = [ - cfg.StrOpt('nfs_shares_config', - default=None, - help='File with the list of available nfs shares'), - cfg.StrOpt('nfs_disk_util', - default='df', - help='Use du or df for free space calculation'), - cfg.BoolOpt('nfs_sparsed_volumes', - default=True, - help=('Create volumes as sparsed files which take no space.' - 'If set to False volume is created as regular file.' - 'In such case volume creation takes a lot of time.')) -] - -FLAGS = flags.FLAGS -FLAGS.register_opts(volume_opts) -FLAGS.register_opts(volume_nfs.volume_opts) - - -class NfsDriver(driver.VolumeDriver): - """NFS based volume driver. Creates file on NFS share for using it - as block device on hypervisor.""" - - def do_setup(self, context): - """Any initialization the volume driver does while starting""" - super(NfsDriver, self).do_setup(context) - - config = FLAGS.nfs_shares_config - if not config: - LOG.warn(_("There's no NFS config file configured ")) - if not config or not os.path.exists(config): - msg = _("NFS config file doesn't exist") - LOG.warn(msg) - raise exception.NfsException(msg) - - try: - self._execute('mount.nfs', check_exit_code=False) - except OSError as exc: - if exc.errno == errno.ENOENT: - raise exception.NfsException('mount.nfs is not installed') - else: - raise - - def check_for_setup_error(self): - """Just to override parent behavior""" - pass - - def create_volume(self, volume): - """Creates a volume""" - - self._ensure_shares_mounted() - - volume['provider_location'] = self._find_share(volume['size']) - - LOG.info(_('casted to %s') % volume['provider_location']) - - self._do_create_volume(volume) - - return {'provider_location': volume['provider_location']} - - def delete_volume(self, volume): - """Deletes a logical volume.""" - - if not volume['provider_location']: - LOG.warn(_('Volume %s does not have provider_location specified, ' - 'skipping'), volume['name']) - return - - self._ensure_share_mounted(volume['provider_location']) - - mounted_path = self.local_path(volume) - - if not self._path_exists(mounted_path): - volume = volume['name'] - - LOG.warn(_('Trying to delete non-existing volume %(volume)s at ' - 'path %(mounted_path)s') % locals()) - return - - self._execute('rm', '-f', mounted_path, run_as_root=True) - - def ensure_export(self, ctx, volume): - """Synchronously recreates an export for a logical volume.""" - self._ensure_share_mounted(volume['provider_location']) - - def create_export(self, ctx, volume): - """Exports the volume. Can optionally return a Dictionary of changes - to the volume object to be persisted.""" - pass - - def remove_export(self, ctx, volume): - """Removes an export for a logical volume.""" - pass - - def check_for_export(self, context, volume_id): - """Make sure volume is exported.""" - pass - - def initialize_connection(self, volume, connector): - """Allow connection to connector and return connection info.""" - data = {'export': volume['provider_location'], - 'name': volume['name']} - return { - 'driver_volume_type': 'nfs', - 'data': data - } - - def terminate_connection(self, volume, connector): - """Disallow connection from connector""" - pass - - def local_path(self, volume): - """Get volume path (mounted locally fs path) for given volume - :param volume: volume reference - """ - nfs_share = volume['provider_location'] - return os.path.join(self._get_mount_point_for_share(nfs_share), - volume['name']) - - def _create_sparsed_file(self, path, size): - """Creates file with 0 disk usage""" - self._execute('truncate', '-s', self._sizestr(size), - path, run_as_root=True) - - def _create_regular_file(self, path, size): - """Creates regular file of given size. Takes a lot of time for large - files""" - KB = 1024 - MB = KB * 1024 - GB = MB * 1024 - - block_size_mb = 1 - block_count = size * GB / (block_size_mb * MB) - - self._execute('dd', 'if=/dev/zero', 'of=%s' % path, - 'bs=%dM' % block_size_mb, - 'count=%d' % block_count, - run_as_root=True) - - def _set_rw_permissions_for_all(self, path): - """Sets 666 permissions for the path""" - self._execute('chmod', 'ugo+rw', path, run_as_root=True) - - def _do_create_volume(self, volume): - """Create a volume on given nfs_share - :param volume: volume reference - """ - volume_path = self.local_path(volume) - volume_size = volume['size'] - - if FLAGS.nfs_sparsed_volumes: - self._create_sparsed_file(volume_path, volume_size) - else: - self._create_regular_file(volume_path, volume_size) - - self._set_rw_permissions_for_all(volume_path) - - def _ensure_shares_mounted(self): - """Look for NFS shares in the flags and tries to mount them locally""" - self._mounted_shares = [] - - for share in self._load_shares_config(): - try: - self._ensure_share_mounted(share) - self._mounted_shares.append(share) - except Exception, exc: - LOG.warning('Exception during mounting %s' % (exc,)) - - LOG.debug('Available shares %s' % str(self._mounted_shares)) - - def _load_shares_config(self): - return [share.strip() for share in open(FLAGS.nfs_shares_config) - if share and not share.startswith('#')] - - def _ensure_share_mounted(self, nfs_share): - """Mount NFS share - :param nfs_share: - """ - mount_path = self._get_mount_point_for_share(nfs_share) - self._mount_nfs(nfs_share, mount_path, ensure=True) - - def _find_share(self, volume_size_for): - """Choose NFS share among available ones for given volume size. Current - implementation looks for greatest capacity - :param volume_size_for: int size in Gb - """ - - if not self._mounted_shares: - raise exception.NfsNoSharesMounted() - - greatest_size = 0 - greatest_share = None - - for nfs_share in self._mounted_shares: - capacity = self._get_available_capacity(nfs_share) - if capacity > greatest_size: - greatest_share = nfs_share - greatest_size = capacity - - if volume_size_for * 1024 * 1024 * 1024 > greatest_size: - raise exception.NfsNoSuitableShareFound( - volume_size=volume_size_for) - return greatest_share - - def _get_mount_point_for_share(self, nfs_share): - """ - :param nfs_share: example 172.18.194.100:/var/nfs - """ - return os.path.join(FLAGS.nfs_mount_point_base, - self._get_hash_str(nfs_share)) - - def _get_available_capacity(self, nfs_share): - """Calculate available space on the NFS share - :param nfs_share: example 172.18.194.100:/var/nfs - """ - mount_point = self._get_mount_point_for_share(nfs_share) - - out, _ = self._execute('df', '-P', '-B', '1', mount_point, - run_as_root=True) - out = out.splitlines()[1] - - available = 0 - - if FLAGS.nfs_disk_util == 'df': - available = int(out.split()[3]) - else: - size = int(out.split()[1]) - out, _ = self._execute('du', '-sb', '--apparent-size', - '--exclude', '*snapshot*', mount_point, - run_as_root=True) - used = int(out.split()[0]) - available = size - used - - return available - - def _mount_nfs(self, nfs_share, mount_path, ensure=False): - """Mount NFS share to mount path""" - if not self._path_exists(mount_path): - self._execute('mkdir', '-p', mount_path) - - try: - self._execute('mount', '-t', 'nfs', nfs_share, mount_path, - run_as_root=True) - except exception.ProcessExecutionError as exc: - if ensure and 'already mounted' in exc.stderr: - LOG.warn(_("%s is already mounted"), nfs_share) - else: - raise - - def _path_exists(self, path): - """Check given path """ - try: - self._execute('stat', path, run_as_root=True) - return True - except exception.ProcessExecutionError as exc: - if 'No such file or directory' in exc.stderr: - return False - else: - raise - - def _get_hash_str(self, base_str): - """returns string that represents hash of base_str (in a hex format)""" - return hashlib.md5(base_str).hexdigest() diff --git a/nova/volume/san.py b/nova/volume/san.py deleted file mode 100644 index cf4507f31..000000000 --- a/nova/volume/san.py +++ /dev/null @@ -1,651 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. -""" -Drivers for san-stored volumes. - -The unique thing about a SAN is that we don't expect that we can run the volume -controller on the SAN hardware. We expect to access it over SSH or some API. -""" - -import base64 -import httplib -import os -import paramiko -import random -import socket -import string -import uuid - -from lxml import etree - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import jsonutils -from nova.openstack.common import log as logging -from nova import utils -import nova.volume.driver - - -LOG = logging.getLogger(__name__) - -san_opts = [ - cfg.BoolOpt('san_thin_provision', - default=True, - help='Use thin provisioning for SAN volumes?'), - cfg.StrOpt('san_ip', - default='', - help='IP address of SAN controller'), - cfg.StrOpt('san_login', - default='admin', - help='Username for SAN controller'), - cfg.StrOpt('san_password', - default='', - help='Password for SAN controller'), - cfg.StrOpt('san_private_key', - default='', - help='Filename of private key to use for SSH authentication'), - cfg.StrOpt('san_clustername', - default='', - help='Cluster name to use for creating volumes'), - cfg.IntOpt('san_ssh_port', - default=22, - help='SSH port to use with SAN'), - cfg.BoolOpt('san_is_local', - default=False, - help='Execute commands locally instead of over SSH; ' - 'use if the volume service is running on the SAN device'), - cfg.StrOpt('san_zfs_volume_base', - default='rpool/', - help='The ZFS path under which to create zvols for volumes.'), - ] - -FLAGS = flags.FLAGS -FLAGS.register_opts(san_opts) - - -class SanISCSIDriver(nova.volume.driver.ISCSIDriver): - """Base class for SAN-style storage volumes - - A SAN-style storage value is 'different' because the volume controller - probably won't run on it, so we need to access is over SSH or another - remote protocol. - """ - - def __init__(self, execute=None, *args, **kwargs): - if execute is None: - execute = self._execute - super(SanISCSIDriver, self).__init__(execute, *args, **kwargs) - self.run_local = FLAGS.san_is_local - - def _build_iscsi_target_name(self, volume): - return "%s%s" % (FLAGS.iscsi_target_prefix, volume['name']) - - def _connect_to_ssh(self): - ssh = paramiko.SSHClient() - #TODO(justinsb): We need a better SSH key policy - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - if FLAGS.san_password: - ssh.connect(FLAGS.san_ip, - port=FLAGS.san_ssh_port, - username=FLAGS.san_login, - password=FLAGS.san_password) - elif FLAGS.san_private_key: - privatekeyfile = os.path.expanduser(FLAGS.san_private_key) - # It sucks that paramiko doesn't support DSA keys - privatekey = paramiko.RSAKey.from_private_key_file(privatekeyfile) - ssh.connect(FLAGS.san_ip, - port=FLAGS.san_ssh_port, - username=FLAGS.san_login, - pkey=privatekey) - else: - msg = _("Specify san_password or san_private_key") - raise exception.InvalidInput(reason=msg) - return ssh - - def _execute(self, *cmd, **kwargs): - if self.run_local: - return utils.execute(*cmd, **kwargs) - else: - check_exit_code = kwargs.pop('check_exit_code', True) - command = ' '.join(cmd) - return self._run_ssh(command, check_exit_code) - - def _run_ssh(self, command, check_exit_code=True): - #TODO(justinsb): SSH connection caching (?) - ssh = self._connect_to_ssh() - - #TODO(justinsb): Reintroduce the retry hack - ret = utils.ssh_execute(ssh, command, check_exit_code=check_exit_code) - - ssh.close() - - return ret - - def ensure_export(self, context, volume): - """Synchronously recreates an export for a logical volume.""" - pass - - def create_export(self, context, volume): - """Exports the volume.""" - pass - - def remove_export(self, context, volume): - """Removes an export for a logical volume.""" - pass - - def check_for_setup_error(self): - """Returns an error if prerequisites aren't met.""" - if not self.run_local: - if not (FLAGS.san_password or FLAGS.san_private_key): - raise exception.InvalidInput( - reason=_('Specify san_password or san_private_key')) - - # The san_ip must always be set, because we use it for the target - if not (FLAGS.san_ip): - raise exception.InvalidInput(reason=_("san_ip must be set")) - - -def _collect_lines(data): - """Split lines from data into an array, trimming them """ - matches = [] - for line in data.splitlines(): - match = line.strip() - matches.append(match) - - return matches - - -def _get_prefixed_values(data, prefix): - """Collect lines which start with prefix; with trimming""" - matches = [] - for line in data.splitlines(): - line = line.strip() - if line.startswith(prefix): - match = line[len(prefix):] - match = match.strip() - matches.append(match) - - return matches - - -class SolarisISCSIDriver(SanISCSIDriver): - """Executes commands relating to Solaris-hosted ISCSI volumes. - - Basic setup for a Solaris iSCSI server: - - pkg install storage-server SUNWiscsit - - svcadm enable stmf - - svcadm enable -r svc:/network/iscsi/target:default - - pfexec itadm create-tpg e1000g0 ${MYIP} - - pfexec itadm create-target -t e1000g0 - - - Then grant the user that will be logging on lots of permissions. - I'm not sure exactly which though: - - zfs allow justinsb create,mount,destroy rpool - - usermod -P'File System Management' justinsb - - usermod -P'Primary Administrator' justinsb - - Also make sure you can login using san_login & san_password/san_private_key - """ - - def _execute(self, *cmd, **kwargs): - new_cmd = ['pfexec'] - new_cmd.extend(cmd) - return super(SolarisISCSIDriver, self)._execute(*new_cmd, - **kwargs) - - def _view_exists(self, luid): - (out, _err) = self._execute('/usr/sbin/stmfadm', - 'list-view', '-l', luid, - check_exit_code=False) - if "no views found" in out: - return False - - if "View Entry:" in out: - return True - - msg = _("Cannot parse list-view output: %s") % out - raise exception.VolumeBackendAPIException(data=msg) - - def _get_target_groups(self): - """Gets list of target groups from host.""" - (out, _err) = self._execute('/usr/sbin/stmfadm', 'list-tg') - matches = _get_prefixed_values(out, 'Target group: ') - LOG.debug("target_groups=%s" % matches) - return matches - - def _target_group_exists(self, target_group_name): - return target_group_name not in self._get_target_groups() - - def _get_target_group_members(self, target_group_name): - (out, _err) = self._execute('/usr/sbin/stmfadm', - 'list-tg', '-v', target_group_name) - matches = _get_prefixed_values(out, 'Member: ') - LOG.debug("members of %s=%s" % (target_group_name, matches)) - return matches - - def _is_target_group_member(self, target_group_name, iscsi_target_name): - return iscsi_target_name in ( - self._get_target_group_members(target_group_name)) - - def _get_iscsi_targets(self): - (out, _err) = self._execute('/usr/sbin/itadm', 'list-target') - matches = _collect_lines(out) - - # Skip header - if len(matches) != 0: - assert 'TARGET NAME' in matches[0] - matches = matches[1:] - - targets = [] - for line in matches: - items = line.split() - assert len(items) == 3 - targets.append(items[0]) - - LOG.debug("_get_iscsi_targets=%s" % (targets)) - return targets - - def _iscsi_target_exists(self, iscsi_target_name): - return iscsi_target_name in self._get_iscsi_targets() - - def _build_zfs_poolname(self, volume): - zfs_poolname = '%s%s' % (FLAGS.san_zfs_volume_base, volume['name']) - return zfs_poolname - - def create_volume(self, volume): - """Creates a volume.""" - if int(volume['size']) == 0: - sizestr = '100M' - else: - sizestr = '%sG' % volume['size'] - - zfs_poolname = self._build_zfs_poolname(volume) - - # Create a zfs volume - cmd = ['/usr/sbin/zfs', 'create'] - if FLAGS.san_thin_provision: - cmd.append('-s') - cmd.extend(['-V', sizestr]) - cmd.append(zfs_poolname) - self._execute(*cmd) - - def _get_luid(self, volume): - zfs_poolname = self._build_zfs_poolname(volume) - zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname - - (out, _err) = self._execute('/usr/sbin/sbdadm', 'list-lu') - - lines = _collect_lines(out) - - # Strip headers - if len(lines) >= 1: - if lines[0] == '': - lines = lines[1:] - - if len(lines) >= 4: - assert 'Found' in lines[0] - assert '' == lines[1] - assert 'GUID' in lines[2] - assert '------------------' in lines[3] - - lines = lines[4:] - - for line in lines: - items = line.split() - assert len(items) == 3 - if items[2] == zvol_name: - luid = items[0].strip() - return luid - - msg = _('LUID not found for %(zfs_poolname)s. ' - 'Output=%(out)s') % locals() - raise exception.VolumeBackendAPIException(data=msg) - - def _is_lu_created(self, volume): - luid = self._get_luid(volume) - return luid - - def delete_volume(self, volume): - """Deletes a volume.""" - zfs_poolname = self._build_zfs_poolname(volume) - self._execute('/usr/sbin/zfs', 'destroy', zfs_poolname) - - def local_path(self, volume): - # TODO(justinsb): Is this needed here? - escaped_group = FLAGS.volume_group.replace('-', '--') - escaped_name = volume['name'].replace('-', '--') - return "/dev/mapper/%s-%s" % (escaped_group, escaped_name) - - def ensure_export(self, context, volume): - """Synchronously recreates an export for a logical volume.""" - #TODO(justinsb): On bootup, this is called for every volume. - # It then runs ~5 SSH commands for each volume, - # most of which fetch the same info each time - # This makes initial start stupid-slow - return self._do_export(volume, force_create=False) - - def create_export(self, context, volume): - return self._do_export(volume, force_create=True) - - def _do_export(self, volume, force_create): - # Create a Logical Unit (LU) backed by the zfs volume - zfs_poolname = self._build_zfs_poolname(volume) - - if force_create or not self._is_lu_created(volume): - zvol_name = '/dev/zvol/rdsk/%s' % zfs_poolname - self._execute('/usr/sbin/sbdadm', 'create-lu', zvol_name) - - luid = self._get_luid(volume) - iscsi_name = self._build_iscsi_target_name(volume) - target_group_name = 'tg-%s' % volume['name'] - - # Create an iSCSI target, mapped to just this volume - if force_create or not self._target_group_exists(target_group_name): - self._execute('/usr/sbin/stmfadm', 'create-tg', target_group_name) - - # Yes, we add the initiatior before we create it! - # Otherwise, it complains that the target is already active - if force_create or not self._is_target_group_member(target_group_name, - iscsi_name): - self._execute('/usr/sbin/stmfadm', - 'add-tg-member', '-g', target_group_name, iscsi_name) - - if force_create or not self._iscsi_target_exists(iscsi_name): - self._execute('/usr/sbin/itadm', 'create-target', '-n', iscsi_name) - - if force_create or not self._view_exists(luid): - self._execute('/usr/sbin/stmfadm', - 'add-view', '-t', target_group_name, luid) - - #TODO(justinsb): Is this always 1? Does it matter? - iscsi_portal_interface = '1' - iscsi_portal = FLAGS.san_ip + ":3260," + iscsi_portal_interface - - db_update = {} - db_update['provider_location'] = ("%s %s" % - (iscsi_portal, - iscsi_name)) - - return db_update - - def remove_export(self, context, volume): - """Removes an export for a logical volume.""" - - # This is the reverse of _do_export - luid = self._get_luid(volume) - iscsi_name = self._build_iscsi_target_name(volume) - target_group_name = 'tg-%s' % volume['name'] - - if self._view_exists(luid): - self._execute('/usr/sbin/stmfadm', 'remove-view', '-l', luid, '-a') - - if self._iscsi_target_exists(iscsi_name): - self._execute('/usr/sbin/stmfadm', 'offline-target', iscsi_name) - self._execute('/usr/sbin/itadm', 'delete-target', iscsi_name) - - # We don't delete the tg-member; we delete the whole tg! - - if self._target_group_exists(target_group_name): - self._execute('/usr/sbin/stmfadm', 'delete-tg', target_group_name) - - if self._is_lu_created(volume): - self._execute('/usr/sbin/sbdadm', 'delete-lu', luid) - - -class HpSanISCSIDriver(SanISCSIDriver): - """Executes commands relating to HP/Lefthand SAN ISCSI volumes. - - We use the CLIQ interface, over SSH. - - Rough overview of CLIQ commands used: - - :createVolume: (creates the volume) - - :getVolumeInfo: (to discover the IQN etc) - - :getClusterInfo: (to discover the iSCSI target IP address) - - :assignVolumeChap: (exports it with CHAP security) - - The 'trick' here is that the HP SAN enforces security by default, so - normally a volume mount would need both to configure the SAN in the volume - layer and do the mount on the compute layer. Multi-layer operations are - not catered for at the moment in the nova architecture, so instead we - share the volume using CHAP at volume creation time. Then the mount need - only use those CHAP credentials, so can take place exclusively in the - compute layer. - """ - - def _cliq_run(self, verb, cliq_args): - """Runs a CLIQ command over SSH, without doing any result parsing""" - cliq_arg_strings = [] - for k, v in cliq_args.items(): - cliq_arg_strings.append(" %s=%s" % (k, v)) - cmd = verb + ''.join(cliq_arg_strings) - - return self._run_ssh(cmd) - - def _cliq_run_xml(self, verb, cliq_args, check_cliq_result=True): - """Runs a CLIQ command over SSH, parsing and checking the output""" - cliq_args['output'] = 'XML' - (out, _err) = self._cliq_run(verb, cliq_args) - - LOG.debug(_("CLIQ command returned %s"), out) - - result_xml = etree.fromstring(out) - if check_cliq_result: - response_node = result_xml.find("response") - if response_node is None: - msg = (_("Malformed response to CLIQ command " - "%(verb)s %(cliq_args)s. Result=%(out)s") % - locals()) - raise exception.VolumeBackendAPIException(data=msg) - - result_code = response_node.attrib.get("result") - - if result_code != "0": - msg = (_("Error running CLIQ command %(verb)s %(cliq_args)s. " - " Result=%(out)s") % - locals()) - raise exception.VolumeBackendAPIException(data=msg) - - return result_xml - - def _cliq_get_cluster_info(self, cluster_name): - """Queries for info about the cluster (including IP)""" - cliq_args = {} - cliq_args['clusterName'] = cluster_name - cliq_args['searchDepth'] = '1' - cliq_args['verbose'] = '0' - - result_xml = self._cliq_run_xml("getClusterInfo", cliq_args) - - return result_xml - - def _cliq_get_cluster_vip(self, cluster_name): - """Gets the IP on which a cluster shares iSCSI volumes""" - cluster_xml = self._cliq_get_cluster_info(cluster_name) - - vips = [] - for vip in cluster_xml.findall("response/cluster/vip"): - vips.append(vip.attrib.get('ipAddress')) - - if len(vips) == 1: - return vips[0] - - _xml = etree.tostring(cluster_xml) - msg = (_("Unexpected number of virtual ips for cluster " - " %(cluster_name)s. Result=%(_xml)s") % - locals()) - raise exception.VolumeBackendAPIException(data=msg) - - def _cliq_get_volume_info(self, volume_name): - """Gets the volume info, including IQN""" - cliq_args = {} - cliq_args['volumeName'] = volume_name - result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args) - - # Result looks like this: - #<gauche version="1.0"> - # <response description="Operation succeeded." name="CliqSuccess" - # processingTime="87" result="0"> - # <volume autogrowPages="4" availability="online" blockSize="1024" - # bytesWritten="0" checkSum="false" clusterName="Cluster01" - # created="2011-02-08T19:56:53Z" deleting="false" description="" - # groupName="Group01" initialQuota="536870912" isPrimary="true" - # iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:vol-b" - # maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca" - # minReplication="1" name="vol-b" parity="0" replication="2" - # reserveQuota="536870912" scratchQuota="4194304" - # serialNumber="9fa5c8b2cca54b2948a63d833097e1ca0000000000006316" - # size="1073741824" stridePages="32" thinProvision="true"> - # <status description="OK" value="2"/> - # <permission access="rw" - # authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7" - # chapName="chapusername" chapRequired="true" id="25369" - # initiatorSecret="" iqn="" iscsiEnabled="true" - # loadBalance="true" targetSecret="supersecret"/> - # </volume> - # </response> - #</gauche> - - # Flatten the nodes into a dictionary; use prefixes to avoid collisions - volume_attributes = {} - - volume_node = result_xml.find("response/volume") - for k, v in volume_node.attrib.items(): - volume_attributes["volume." + k] = v - - status_node = volume_node.find("status") - if not status_node is None: - for k, v in status_node.attrib.items(): - volume_attributes["status." + k] = v - - # We only consider the first permission node - permission_node = volume_node.find("permission") - if not permission_node is None: - for k, v in status_node.attrib.items(): - volume_attributes["permission." + k] = v - - LOG.debug(_("Volume info: %(volume_name)s => %(volume_attributes)s") % - locals()) - return volume_attributes - - def create_volume(self, volume): - """Creates a volume.""" - cliq_args = {} - cliq_args['clusterName'] = FLAGS.san_clustername - #TODO(justinsb): Should we default to inheriting thinProvision? - cliq_args['thinProvision'] = '1' if FLAGS.san_thin_provision else '0' - cliq_args['volumeName'] = volume['name'] - if int(volume['size']) == 0: - cliq_args['size'] = '100MB' - else: - cliq_args['size'] = '%sGB' % volume['size'] - - self._cliq_run_xml("createVolume", cliq_args) - - volume_info = self._cliq_get_volume_info(volume['name']) - cluster_name = volume_info['volume.clusterName'] - iscsi_iqn = volume_info['volume.iscsiIqn'] - - #TODO(justinsb): Is this always 1? Does it matter? - cluster_interface = '1' - - cluster_vip = self._cliq_get_cluster_vip(cluster_name) - iscsi_portal = cluster_vip + ":3260," + cluster_interface - - model_update = {} - - # NOTE(jdg): LH volumes always at lun 0 ? - model_update['provider_location'] = ("%s %s %s" % - (iscsi_portal, - iscsi_iqn, - 0)) - - return model_update - - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - raise NotImplementedError() - - def create_snapshot(self, snapshot): - """Creates a snapshot.""" - raise NotImplementedError() - - def delete_volume(self, volume): - """Deletes a volume.""" - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['prompt'] = 'false' # Don't confirm - - self._cliq_run_xml("deleteVolume", cliq_args) - - def local_path(self, volume): - # TODO(justinsb): Is this needed here? - msg = _("local_path not supported") - raise exception.VolumeBackendAPIException(data=msg) - - def initialize_connection(self, volume, connector): - """Assigns the volume to a server. - - Assign any created volume to a compute node/host so that it can be - used from that host. HP VSA requires a volume to be assigned - to a server. - - This driver returns a driver_volume_type of 'iscsi'. - The format of the driver data is defined in _get_iscsi_properties. - Example return value:: - - { - 'driver_volume_type': 'iscsi' - 'data': { - 'target_discovered': True, - 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001', - 'target_portal': '127.0.0.1:3260', - 'volume_id': 1, - } - } - - """ - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['serverName'] = connector['host'] - self._cliq_run_xml("assignVolumeToServer", cliq_args) - - iscsi_properties = self._get_iscsi_properties(volume) - return { - 'driver_volume_type': 'iscsi', - 'data': iscsi_properties - } - - def terminate_connection(self, volume, connector): - """Unassign the volume from the host.""" - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['serverName'] = connector['host'] - self._cliq_run_xml("unassignVolumeToServer", cliq_args) diff --git a/nova/volume/solidfire.py b/nova/volume/solidfire.py deleted file mode 100644 index 8c4ca6684..000000000 --- a/nova/volume/solidfire.py +++ /dev/null @@ -1,424 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. -""" -Drivers for san-stored volumes. - -The unique thing about a SAN is that we don't expect that we can run the volume -controller on the SAN hardware. We expect to access it over SSH or some API. -""" - -import base64 -import httplib -import json -import random -import socket -import string -import uuid - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import log as logging -from nova.volume.san import SanISCSIDriver - - -LOG = logging.getLogger(__name__) - -sf_opts = [ - cfg.BoolOpt('sf_emulate_512', - default=True, - help='Set 512 byte emulation on volume creation; '), - - cfg.StrOpt('sf_mvip', - default='', - help='IP address of SolidFire MVIP'), - - cfg.StrOpt('sf_login', - default='admin', - help='Username for SF Cluster Admin'), - - cfg.StrOpt('sf_password', - default='', - help='Password for SF Cluster Admin'), - - cfg.BoolOpt('sf_allow_tenant_qos', - default=True, - help='Allow tenants to specify QOS on create'), ] - -FLAGS = flags.FLAGS -FLAGS.register_opts(sf_opts) - - -class SolidFire(SanISCSIDriver): - - sf_qos_dict = {'slow': {'minIOPS': 100, - 'maxIOPS': 200, - 'burstIOPS': 200}, - 'medium': {'minIOPS': 200, - 'maxIOPS': 400, - 'burstIOPS': 400}, - 'fast': {'minIOPS': 500, - 'maxIOPS': 1000, - 'burstIOPS': 1000}, - 'performant': {'minIOPS': 2000, - 'maxIOPS': 4000, - 'burstIOPS': 4000}, - 'off': None} - - def __init__(self, *args, **kwargs): - super(SolidFire, self).__init__(*args, **kwargs) - - def _issue_api_request(self, method_name, params): - """All API requests to SolidFire device go through this method - - Simple json-rpc web based API calls. - each call takes a set of paramaters (dict) - and returns results in a dict as well. - """ - - host = FLAGS.san_ip - # For now 443 is the only port our server accepts requests on - port = 443 - - # NOTE(john-griffith): Probably don't need this, but the idea is - # we provide a request_id so we can correlate - # responses with requests - request_id = int(uuid.uuid4()) # just generate a random number - - cluster_admin = FLAGS.san_login - cluster_password = FLAGS.san_password - - command = {'method': method_name, - 'id': request_id} - - if params is not None: - command['params'] = params - - payload = json.dumps(command, ensure_ascii=False) - payload.encode('utf-8') - # we use json-rpc, webserver needs to see json-rpc in header - header = {'Content-Type': 'application/json-rpc; charset=utf-8'} - - if cluster_password is not None: - # base64.encodestring includes a newline character - # in the result, make sure we strip it off - auth_key = base64.encodestring('%s:%s' % (cluster_admin, - cluster_password))[:-1] - header['Authorization'] = 'Basic %s' % auth_key - - LOG.debug(_("Payload for SolidFire API call: %s"), payload) - connection = httplib.HTTPSConnection(host, port) - connection.request('POST', '/json-rpc/1.0', payload, header) - response = connection.getresponse() - data = {} - - if response.status != 200: - connection.close() - raise exception.SolidFireAPIException(status=response.status) - - else: - data = response.read() - try: - data = json.loads(data) - - except (TypeError, ValueError), exc: - connection.close() - msg = _("Call to json.loads() raised an exception: %s") % exc - raise exception.SfJsonEncodeFailure(msg) - - connection.close() - - LOG.debug(_("Results of SolidFire API call: %s"), data) - return data - - def _get_volumes_by_sfaccount(self, account_id): - params = {'accountID': account_id} - data = self._issue_api_request('ListVolumesForAccount', params) - if 'result' in data: - return data['result']['volumes'] - - def _get_sfaccount_by_name(self, sf_account_name): - sfaccount = None - params = {'username': sf_account_name} - data = self._issue_api_request('GetAccountByName', params) - if 'result' in data and 'account' in data['result']: - LOG.debug(_('Found solidfire account: %s'), sf_account_name) - sfaccount = data['result']['account'] - return sfaccount - - def _create_sfaccount(self, nova_project_id): - """Create account on SolidFire device if it doesn't already exist. - - We're first going to check if the account already exits, if it does - just return it. If not, then create it. - """ - - sf_account_name = socket.getfqdn() + '-' + nova_project_id - sfaccount = self._get_sfaccount_by_name(sf_account_name) - if sfaccount is None: - LOG.debug(_('solidfire account: %s does not exist, create it...'), - sf_account_name) - chap_secret = self._generate_random_string(12) - params = {'username': sf_account_name, - 'initiatorSecret': chap_secret, - 'targetSecret': chap_secret, - 'attributes': {}} - data = self._issue_api_request('AddAccount', params) - if 'result' in data: - sfaccount = self._get_sfaccount_by_name(sf_account_name) - - return sfaccount - - def _get_cluster_info(self): - params = {} - data = self._issue_api_request('GetClusterInfo', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - return data['result'] - - def _do_export(self, volume): - """Gets the associated account, retrieves CHAP info and updates.""" - - sfaccount_name = '%s-%s' % (socket.getfqdn(), volume['project_id']) - sfaccount = self._get_sfaccount_by_name(sfaccount_name) - - model_update = {} - model_update['provider_auth'] = ('CHAP %s %s' - % (sfaccount['username'], - sfaccount['targetSecret'])) - - return model_update - - def _generate_random_string(self, length): - """Generates random_string to use for CHAP password.""" - - char_set = string.ascii_uppercase + string.digits - return ''.join(random.sample(char_set, length)) - - def _do_volume_create(self, project_id, params): - cluster_info = self._get_cluster_info() - iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260' - sfaccount = self._create_sfaccount(project_id) - chap_secret = sfaccount['targetSecret'] - - params['accountID'] = sfaccount['accountID'] - data = self._issue_api_request('CreateVolume', params) - - if 'result' not in data or 'volumeID' not in data['result']: - raise exception.SolidFireAPIDataException(data=data) - - volume_id = data['result']['volumeID'] - - volume_list = self._get_volumes_by_sfaccount(sfaccount['accountID']) - - iqn = None - for v in volume_list: - if v['volumeID'] == volume_id: - iqn = v['iqn'] - break - - model_update = {} - - # NOTE(john-griffith): SF volumes are always at lun 0 - model_update['provider_location'] = ('%s %s %s' - % (iscsi_portal, iqn, 0)) - model_update['provider_auth'] = ('CHAP %s %s' - % (sfaccount['username'], - chap_secret)) - - return model_update - - def create_volume(self, volume): - """Create volume on SolidFire device. - - The account is where CHAP settings are derived from, volume is - created and exported. Note that the new volume is immediately ready - for use. - - One caveat here is that an existing user account must be specified - in the API call to create a new volume. We use a set algorithm to - determine account info based on passed in nova volume object. First - we check to see if the account already exists (and use it), or if it - does not already exist, we'll go ahead and create it. - - For now, we're just using very basic settings, QOS is - turned off, 512 byte emulation is off etc. Will be - looking at extensions for these things later, or - this module can be hacked to suit needs. - """ - GB = 1048576 * 1024 - slice_count = 1 - attributes = {} - qos = {} - qos_keys = ['minIOPS', 'maxIOPS', 'burstIOPS'] - valid_presets = self.sf_qos_dict.keys() - - if FLAGS.sf_allow_tenant_qos and \ - volume.get('volume_metadata')is not None: - - #First look to see if they included a preset - presets = [i.value for i in volume.get('volume_metadata') - if i.key == 'sf-qos' and i.value in valid_presets] - if len(presets) > 0: - if len(presets) > 1: - LOG.warning(_('More than one valid preset was ' - 'detected, using %s') % presets[0]) - qos = self.sf_qos_dict[presets[0]] - else: - #if there was no preset, look for explicit settings - for i in volume.get('volume_metadata'): - if i.key in qos_keys: - qos[i.key] = int(i.value) - - params = {'name': 'OS-VOLID-%s' % volume['id'], - 'accountID': None, - 'sliceCount': slice_count, - 'totalSize': volume['size'] * GB, - 'enable512e': FLAGS.sf_emulate_512, - 'attributes': attributes, - 'qos': qos} - - return self._do_volume_create(volume['project_id'], params) - - def delete_volume(self, volume, is_snapshot=False): - """Delete SolidFire Volume from device. - - SolidFire allows multipe volumes with same name, - volumeID is what's guaranteed unique. - - """ - - LOG.debug(_("Enter SolidFire delete_volume...")) - sf_account_name = socket.getfqdn() + '-' + volume['project_id'] - sfaccount = self._get_sfaccount_by_name(sf_account_name) - if sfaccount is None: - raise exception.SfAccountNotFound(account_name=sf_account_name) - - params = {'accountID': sfaccount['accountID']} - data = self._issue_api_request('ListVolumesForAccount', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - if is_snapshot: - seek = 'OS-SNAPID-%s' % (volume['id']) - else: - seek = 'OS-VOLID-%s' % volume['id'] - #params = {'name': 'OS-VOLID-:%s' % volume['id'], - - found_count = 0 - volid = -1 - for v in data['result']['volumes']: - if v['name'] == seek: - found_count += 1 - volid = v['volumeID'] - - if found_count == 0: - raise exception.VolumeNotFound(volume_id=volume['id']) - - if found_count > 1: - LOG.debug(_("Deleting volumeID: %s"), volid) - raise exception.DuplicateSfVolumeNames(vol_name=volume['id']) - - params = {'volumeID': volid} - data = self._issue_api_request('DeleteVolume', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - LOG.debug(_("Leaving SolidFire delete_volume")) - - def ensure_export(self, context, volume): - LOG.debug(_("Executing SolidFire ensure_export...")) - return self._do_export(volume) - - def create_export(self, context, volume): - LOG.debug(_("Executing SolidFire create_export...")) - return self._do_export(volume) - - def _do_create_snapshot(self, snapshot, snapshot_name): - """Creates a snapshot.""" - LOG.debug(_("Enter SolidFire create_snapshot...")) - sf_account_name = socket.getfqdn() + '-' + snapshot['project_id'] - sfaccount = self._get_sfaccount_by_name(sf_account_name) - if sfaccount is None: - raise exception.SfAccountNotFound(account_name=sf_account_name) - - params = {'accountID': sfaccount['accountID']} - data = self._issue_api_request('ListVolumesForAccount', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - found_count = 0 - volid = -1 - for v in data['result']['volumes']: - if v['name'] == 'OS-VOLID-%s' % snapshot['volume_id']: - found_count += 1 - volid = v['volumeID'] - - if found_count == 0: - raise exception.VolumeNotFound(volume_id=snapshot['volume_id']) - if found_count != 1: - raise exception.DuplicateSfVolumeNames( - vol_name='OS-VOLID-%s' % snapshot['volume_id']) - - params = {'volumeID': int(volid), - 'name': snapshot_name, - 'attributes': {'OriginatingVolume': volid}} - - data = self._issue_api_request('CloneVolume', params) - if 'result' not in data: - raise exception.SolidFireAPIDataException(data=data) - - return (data, sfaccount) - - def delete_snapshot(self, snapshot): - self.delete_volume(snapshot, True) - - def create_snapshot(self, snapshot): - snapshot_name = 'OS-SNAPID-%s' % ( - snapshot['id']) - (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name) - - def create_volume_from_snapshot(self, volume, snapshot): - cluster_info = self._get_cluster_info() - iscsi_portal = cluster_info['clusterInfo']['svip'] + ':3260' - sfaccount = self._create_sfaccount(snapshot['project_id']) - chap_secret = sfaccount['targetSecret'] - snapshot_name = 'OS-VOLID-%s' % volume['id'] - - (data, sf_account) = self._do_create_snapshot(snapshot, snapshot_name) - - if 'result' not in data or 'volumeID' not in data['result']: - raise exception.SolidFireAPIDataException(data=data) - - volume_id = data['result']['volumeID'] - volume_list = self._get_volumes_by_sfaccount(sf_account['accountID']) - iqn = None - for v in volume_list: - if v['volumeID'] == volume_id: - iqn = v['iqn'] - break - - model_update = {} - - # NOTE(john-griffith): SF volumes are always at lun 0 - model_update['provider_location'] = ('%s %s %s' - % (iscsi_portal, iqn, 0)) - model_update['provider_auth'] = ('CHAP %s %s' - % (sfaccount['username'], - chap_secret)) - return model_update diff --git a/nova/volume/storwize_svc.py b/nova/volume/storwize_svc.py deleted file mode 100644 index 52a735018..000000000 --- a/nova/volume/storwize_svc.py +++ /dev/null @@ -1,1233 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 IBM, Inc. -# Copyright (c) 2012 OpenStack LLC. -# All Rights Reserved. -# -# 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. -# -# Authors: -# Ronen Kat <ronenkat@il.ibm.com> -# Avishay Traeger <avishay@il.ibm.com> - -""" -Volume driver for IBM Storwize V7000 and SVC storage systems. - -Notes: -1. If you specify both a password and a key file, this driver will use the - key file only. -2. When using a key file for authentication, it is up to the user or - system administrator to store the private key in a safe manner. -3. The defaults for creating volumes are "-rsize 2% -autoexpand - -grainsize 256 -warning 0". These can be changed in the configuration - file or by using volume types(recommended only for advanced users). - -Limitations: -1. The driver was not tested with SVC or clustered configurations of Storwize - V7000. -2. The driver expects CLI output in English, error messages may be in a - localized format. -""" - -import random -import re -import string -import time - -from nova import exception -from nova import flags -from nova.openstack.common import cfg -from nova.openstack.common import excutils -from nova.openstack.common import log as logging -from nova.volume import san - -LOG = logging.getLogger(__name__) - -storwize_svc_opts = [ - cfg.StrOpt('storwize_svc_volpool_name', - default='volpool', - help='Storage system storage pool for volumes'), - cfg.StrOpt('storwize_svc_vol_rsize', - default='2%', - help='Storage system space-efficiency parameter for volumes'), - cfg.StrOpt('storwize_svc_vol_warning', - default='0', - help='Storage system threshold for volume capacity warnings'), - cfg.BoolOpt('storwize_svc_vol_autoexpand', - default=True, - help='Storage system autoexpand parameter for volumes ' - '(True/False)'), - cfg.StrOpt('storwize_svc_vol_grainsize', - default='256', - help='Storage system grain size parameter for volumes ' - '(32/64/128/256)'), - cfg.BoolOpt('storwize_svc_vol_compression', - default=False, - help='Storage system compression option for volumes'), - cfg.BoolOpt('storwize_svc_vol_easytier', - default=True, - help='Enable Easy Tier for volumes'), - cfg.StrOpt('storwize_svc_flashcopy_timeout', - default='120', - help='Maximum number of seconds to wait for FlashCopy to be ' - 'prepared. Maximum value is 600 seconds (10 minutes).'), -] - -FLAGS = flags.FLAGS -FLAGS.register_opts(storwize_svc_opts) - - -class StorwizeSVCDriver(san.SanISCSIDriver): - """IBM Storwize V7000 and SVC iSCSI volume driver.""" - - def __init__(self, *args, **kwargs): - super(StorwizeSVCDriver, self).__init__(*args, **kwargs) - self.iscsi_ipv4_conf = None - self.iscsi_ipv6_conf = None - - # Build cleanup transaltion tables for hosts names to follow valid - # host names for Storwizew V7000 and SVC storage systems. - invalid_ch_in_host = '' - for num in range(0, 128): - ch = chr(num) - if ((not ch.isalnum()) and (ch != ' ') and (ch != '.') - and (ch != '-') and (ch != '_')): - invalid_ch_in_host = invalid_ch_in_host + ch - self._string_host_name_filter = string.maketrans(invalid_ch_in_host, - '-' * len(invalid_ch_in_host)) - - self._unicode_host_name_filter = dict((ord(unicode(char)), u'-') - for char in invalid_ch_in_host) - - def _get_hdr_dic(self, header, row, delim): - """Return CLI row data as a dictionary indexed by names from header. - - Create a dictionary object from the data row string using the header - string. The strings are converted to columns using the delimiter in - delim. - """ - - attributes = header.split(delim) - values = row.split(delim) - self._driver_assert(len(values) == len(attributes), - _('_get_hdr_dic: attribute headers and values do not match.\n ' - 'Headers: %(header)s\n Values: %(row)s') - % {'header': str(header), - 'row': str(row)}) - dic = {} - for attribute, value in map(None, attributes, values): - dic[attribute] = value - return dic - - def _driver_assert(self, assert_condition, exception_message): - """Internal assertion mechanism for CLI output.""" - if not assert_condition: - LOG.error(exception_message) - raise exception.VolumeBackendAPIException(data=exception_message) - - def check_for_setup_error(self): - """Check that we have all configuration details from the storage.""" - - LOG.debug(_('enter: check_for_setup_error')) - - # Validate that the pool exists - ssh_cmd = 'lsmdiskgrp -delim ! -nohdr' - out, err = self._run_ssh(ssh_cmd) - self._driver_assert(len(out) > 0, - _('check_for_setup_error: failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - search_text = '!%s!' % FLAGS.storwize_svc_volpool_name - if search_text not in out: - raise exception.InvalidInput( - reason=(_('pool %s doesn\'t exist') - % FLAGS.storwize_svc_volpool_name)) - - storage_nodes = {} - # Get the iSCSI names of the Storwize/SVC nodes - ssh_cmd = 'svcinfo lsnode -delim !' - out, err = self._run_ssh(ssh_cmd) - self._driver_assert(len(out) > 0, - _('check_for_setup_error: failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - - nodes = out.strip().split('\n') - self._driver_assert(len(nodes) > 0, - _('check_for_setup_error: failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - header = nodes.pop(0) - for node_line in nodes: - try: - node_data = self._get_hdr_dic(header, node_line, '!') - except exception.VolumeBackendAPIException as e: - with excutils.save_and_reraise_exception(): - LOG.error(_('check_for_setup_error: ' - 'failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - node = {} - try: - node['id'] = node_data['id'] - node['name'] = node_data['name'] - node['iscsi_name'] = node_data['iscsi_name'] - node['status'] = node_data['status'] - node['ipv4'] = [] - node['ipv6'] = [] - if node['iscsi_name'] != '': - storage_nodes[node['id']] = node - except KeyError as e: - LOG.error(_('Did not find expected column name in ' - 'svcinfo lsnode: %s') % str(e)) - exception_message = ( - _('check_for_setup_error: Unexpected CLI output.\n ' - 'Details: %(msg)s\n' - 'Command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'msg': str(e), - 'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - raise exception.VolumeBackendAPIException( - data=exception_message) - - # Get the iSCSI IP addresses of the Storwize/SVC nodes - ssh_cmd = 'lsportip -delim !' - out, err = self._run_ssh(ssh_cmd) - self._driver_assert(len(out) > 0, - _('check_for_setup_error: failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - - portips = out.strip().split('\n') - self._driver_assert(len(portips) > 0, - _('check_for_setup_error: failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - header = portips.pop(0) - for portip_line in portips: - try: - port_data = self._get_hdr_dic(header, portip_line, '!') - except exception.VolumeBackendAPIException as e: - with excutils.save_and_reraise_exception(): - LOG.error(_('check_for_setup_error: ' - 'failed with unexpected CLI output.\n ' - 'Command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - try: - port_node_id = port_data['node_id'] - port_ipv4 = port_data['IP_address'] - port_ipv6 = port_data['IP_address_6'] - except KeyError as e: - LOG.error(_('Did not find expected column name in ' - 'lsportip: %s') % str(e)) - exception_message = ( - _('check_for_setup_error: Unexpected CLI output.\n ' - 'Details: %(msg)s\n' - 'Command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'msg': str(e), - 'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - raise exception.VolumeBackendAPIException( - data=exception_message) - - if port_node_id in storage_nodes: - node = storage_nodes[port_node_id] - if len(port_ipv4) > 0: - node['ipv4'].append(port_ipv4) - if len(port_ipv6) > 0: - node['ipv6'].append(port_ipv6) - else: - raise exception.VolumeBackendAPIException( - data=_('check_for_setup_error: ' - 'fail to storage configuration: unknown ' - 'storage node %(node_id)s from CLI output.\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'node_id': port_node_id, - 'out': str(out), - 'err': str(err)}) - - iscsi_ipv4_conf = [] - iscsi_ipv6_conf = [] - for node_key in storage_nodes: - node = storage_nodes[node_key] - if 'ipv4' in node and len(node['iscsi_name']) > 0: - iscsi_ipv4_conf.append({'iscsi_name': node['iscsi_name'], - 'ip': node['ipv4'], - 'node_id': node['id']}) - if 'ipv6' in node and len(node['iscsi_name']) > 0: - iscsi_ipv6_conf.append({'iscsi_name': node['iscsi_name'], - 'ip': node['ipv6'], - 'node_id': node['id']}) - if (len(node['ipv4']) == 0) and (len(node['ipv6']) == 0): - raise exception.VolumeBackendAPIException( - data=_('check_for_setup_error: ' - 'fail to storage configuration: storage ' - 'node %s has no IP addresses configured') - % node['id']) - - # Make sure we have at least one IPv4 address with a iSCSI name - # TODO(ronenkat) need to expand this to support IPv6 - self._driver_assert(len(iscsi_ipv4_conf) > 0, - _('could not obtain IP address and iSCSI name from the storage. ' - 'Please verify that the storage is configured for iSCSI.\n ' - 'Storage nodes: %(nodes)s\n portips: %(portips)s') - % {'nodes': nodes, 'portips': portips}) - - self.iscsi_ipv4_conf = iscsi_ipv4_conf - self.iscsi_ipv6_conf = iscsi_ipv6_conf - - LOG.debug(_('leave: check_for_setup_error')) - - def _check_num_perc(self, value): - """Return True if value is either a number or a percentage.""" - if value.endswith('%'): - value = value[0:-1] - return value.isdigit() - - def _check_flags(self): - """Ensure that the flags are set properly.""" - - required_flags = ['san_ip', 'san_ssh_port', 'san_login', - 'storwize_svc_volpool_name'] - for flag in required_flags: - if not getattr(FLAGS, flag, None): - raise exception.InvalidInput( - reason=_('%s is not set') % flag) - - # Ensure that either password or keyfile were set - if not (FLAGS.san_password or FLAGS.san_private_key): - raise exception.InvalidInput( - reason=_('Password or SSH private key is required for ' - 'authentication: set either san_password or ' - 'san_private_key option')) - - # Check that rsize is a number or percentage - rsize = FLAGS.storwize_svc_vol_rsize - if not self._check_num_perc(rsize) and (rsize != '-1'): - raise exception.InvalidInput( - reason=_('Illegal value specified for storwize_svc_vol_rsize: ' - 'set to either a number or a percentage')) - - # Check that warning is a number or percentage - warning = FLAGS.storwize_svc_vol_warning - if not self._check_num_perc(warning): - raise exception.InvalidInput( - reason=_('Illegal value specified for ' - 'storwize_svc_vol_warning: ' - 'set to either a number or a percentage')) - - # Check that grainsize is 32/64/128/256 - grainsize = FLAGS.storwize_svc_vol_grainsize - if grainsize not in ['32', '64', '128', '256']: - raise exception.InvalidInput( - reason=_('Illegal value specified for ' - 'storwize_svc_vol_grainsize: set to either ' - '\'32\', \'64\', \'128\', or \'256\'')) - - # Check that flashcopy_timeout is numeric and 32/64/128/256 - flashcopy_timeout = FLAGS.storwize_svc_flashcopy_timeout - if not (flashcopy_timeout.isdigit() and int(flashcopy_timeout) > 0 and - int(flashcopy_timeout) <= 600): - raise exception.InvalidInput( - reason=_('Illegal value %s specified for ' - 'storwize_svc_flashcopy_timeout: ' - 'valid values are between 0 and 600') - % flashcopy_timeout) - - # Check that rsize is set - volume_compression = FLAGS.storwize_svc_vol_compression - if ((volume_compression == True) and - (FLAGS.storwize_svc_vol_rsize == '-1')): - raise exception.InvalidInput( - reason=_('If compression is set to True, rsize must ' - 'also be set (not equal to -1)')) - - def do_setup(self, context): - """Validate the flags.""" - LOG.debug(_('enter: do_setup')) - self._check_flags() - LOG.debug(_('leave: do_setup')) - - def create_volume(self, volume): - """Create a new volume - uses the internal method.""" - return self._create_volume(volume, units='gb') - - def _create_volume(self, volume, units='gb'): - """Create a new volume.""" - - name = volume['name'] - model_update = None - - LOG.debug(_('enter: create_volume: volume %s ') % name) - - size = int(volume['size']) - - if FLAGS.storwize_svc_vol_autoexpand == True: - autoex = '-autoexpand' - else: - autoex = '' - - if FLAGS.storwize_svc_vol_easytier == True: - easytier = '-easytier on' - else: - easytier = '-easytier off' - - # Set space-efficient options - if FLAGS.storwize_svc_vol_rsize.strip() == '-1': - ssh_cmd_se_opt = '' - else: - ssh_cmd_se_opt = ('-rsize %(rsize)s %(autoex)s -warning %(warn)s' % - {'rsize': FLAGS.storwize_svc_vol_rsize, - 'autoex': autoex, - 'warn': FLAGS.storwize_svc_vol_warning}) - if FLAGS.storwize_svc_vol_compression: - ssh_cmd_se_opt = ssh_cmd_se_opt + ' -compressed' - else: - ssh_cmd_se_opt = ssh_cmd_se_opt + (' -grainsize %(grain)s' % - {'grain': FLAGS.storwize_svc_vol_grainsize}) - - ssh_cmd = ('mkvdisk -name %(name)s -mdiskgrp %(mdiskgrp)s ' - '-iogrp 0 -size %(size)s -unit ' - '%(unit)s %(easytier)s %(ssh_cmd_se_opt)s' - % {'name': name, - 'mdiskgrp': FLAGS.storwize_svc_volpool_name, - 'size': size, 'unit': units, 'easytier': easytier, - 'ssh_cmd_se_opt': ssh_cmd_se_opt}) - out, err = self._run_ssh(ssh_cmd) - self._driver_assert(len(out.strip()) > 0, - _('create volume %(name)s - did not find ' - 'success message in CLI output.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'name': name, 'out': str(out), 'err': str(err)}) - - # Ensure that the output is as expected - match_obj = re.search('Virtual Disk, id \[([0-9]+)\], ' - 'successfully created', out) - # Make sure we got a "successfully created" message with vdisk id - self._driver_assert(match_obj is not None, - _('create volume %(name)s - did not find ' - 'success message in CLI output.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'name': name, 'out': str(out), 'err': str(err)}) - - LOG.debug(_('leave: create_volume: volume %(name)s ') % {'name': name}) - - def delete_volume(self, volume): - self._delete_volume(volume, False) - - def _delete_volume(self, volume, force_opt): - """Driver entry point for destroying existing volumes.""" - - name = volume['name'] - LOG.debug(_('enter: delete_volume: volume %(name)s ') % {'name': name}) - - if force_opt: - force_flag = '-force' - else: - force_flag = '' - - volume_defined = self._is_volume_defined(name) - # Try to delete volume only if found on the storage - if volume_defined: - out, err = self._run_ssh('rmvdisk %(force)s %(name)s' - % {'force': force_flag, - 'name': name}) - # No output should be returned from rmvdisk - self._driver_assert(len(out.strip()) == 0, - _('delete volume %(name)s - non empty output from CLI.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'name': name, - 'out': str(out), - 'err': str(err)}) - else: - # Log that volume does not exist - LOG.info(_('warning: tried to delete volume %(name)s but ' - 'it does not exist.') % {'name': name}) - - LOG.debug(_('leave: delete_volume: volume %(name)s ') % {'name': name}) - - def ensure_export(self, context, volume): - """Check that the volume exists on the storage. - - The system does not "export" volumes as a Linux iSCSI target does, - and therefore we just check that the volume exists on the storage. - """ - volume_defined = self._is_volume_defined(volume['name']) - if not volume_defined: - LOG.error(_('ensure_export: volume %s not found on storage') - % volume['name']) - - def create_export(self, context, volume): - model_update = None - return model_update - - def remove_export(self, context, volume): - pass - - def check_for_export(self, context, volume_id): - raise NotImplementedError() - - def initialize_connection(self, volume, connector): - """Perform the necessary work so that an iSCSI connection can be made. - - To be able to create an iSCSI connection from a given iSCSI name to a - volume, we must: - 1. Translate the given iSCSI name to a host name - 2. Create new host on the storage system if it does not yet exist - 2. Map the volume to the host if it is not already done - 3. Return iSCSI properties, including the IP address of the preferred - node for this volume and the LUN number. - """ - LOG.debug(_('enter: initialize_connection: volume %(vol)s with ' - 'connector %(conn)s') % {'vol': str(volume), - 'conn': str(connector)}) - - initiator_name = connector['initiator'] - volume_name = volume['name'] - - host_name = self._get_host_from_iscsiname(initiator_name) - # Check if a host is defined for the iSCSI initiator name - if host_name is None: - # Host does not exist - add a new host to Storwize/SVC - host_name = self._create_new_host('host%s' % initiator_name, - initiator_name) - # Verify that create_new_host succeeded - self._driver_assert(host_name is not None, - _('_create_new_host failed to return the host name.')) - - lun_id = self._map_vol_to_host(volume_name, host_name) - - # Get preferred path - # Only IPv4 for now because lack of OpenStack support - # TODO(ronenkat): Add support for IPv6 - volume_attributes = self._get_volume_attributes(volume_name) - if (volume_attributes is not None and - 'preferred_node_id' in volume_attributes): - preferred_node = volume_attributes['preferred_node_id'] - preferred_node_entry = None - for node in self.iscsi_ipv4_conf: - if node['node_id'] == preferred_node: - preferred_node_entry = node - break - if preferred_node_entry is None: - preferred_node_entry = self.iscsi_ipv4_conf[0] - LOG.error(_('initialize_connection: did not find preferred ' - 'node %(node)s for volume %(vol)s in iSCSI ' - 'configuration') % {'node': preferred_node, - 'vol': volume_name}) - else: - # Get 1st node - preferred_node_entry = self.iscsi_ipv4_conf[0] - LOG.error( - _('initialize_connection: did not find a preferred node ' - 'for volume %s in iSCSI configuration') % volume_name) - - properties = {} - # We didn't use iSCSI discover, as in server-based iSCSI - properties['target_discovered'] = False - # We take the first IP address for now. Ideally, OpenStack will - # support multipath for improved performance. - properties['target_portal'] = ('%s:%s' % - (preferred_node_entry['ip'][0], '3260')) - properties['target_iqn'] = preferred_node_entry['iscsi_name'] - properties['target_lun'] = lun_id - properties['volume_id'] = volume['id'] - - LOG.debug(_('leave: initialize_connection:\n volume: %(vol)s\n ' - 'connector %(conn)s\n properties: %(prop)s') - % {'vol': str(volume), - 'conn': str(connector), - 'prop': str(properties)}) - - return {'driver_volume_type': 'iscsi', 'data': properties, } - - def terminate_connection(self, volume, connector): - """Cleanup after an iSCSI connection has been terminated. - - When we clean up a terminated connection between a given iSCSI name - and volume, we: - 1. Translate the given iSCSI name to a host name - 2. Remove the volume-to-host mapping if it exists - 3. Delete the host if it has no more mappings (hosts are created - automatically by this driver when mappings are created) - """ - LOG.debug(_('enter: terminate_connection: volume %(vol)s with ' - 'connector %(conn)s') % {'vol': str(volume), - 'conn': str(connector)}) - - vol_name = volume['name'] - initiator_name = connector['initiator'] - host_name = self._get_host_from_iscsiname(initiator_name) - # Verify that _get_host_from_iscsiname returned the host. - # This should always succeed as we terminate an existing connection. - self._driver_assert(host_name is not None, - _('_get_host_from_iscsiname failed to return the host name ' - 'for iscsi name %s') % initiator_name) - - # Check if vdisk-host mapping exists, remove if it does - mapping_data = self._get_hostvdisk_mappings(host_name) - if vol_name in mapping_data: - out, err = self._run_ssh('rmvdiskhostmap -host %s %s' - % (host_name, vol_name)) - # Verify CLI behaviour - no output is returned from - # rmvdiskhostmap - self._driver_assert(len(out.strip()) == 0, - _('delete mapping of volume %(vol)s to host %(host)s ' - '- non empty output from CLI.\n ' - 'stdout: %(out)s\n stderr: %(err)s') - % {'vol': vol_name, - 'host': host_name, - 'out': str(out), - 'err': str(err)}) - del mapping_data[vol_name] - else: - LOG.error(_('terminate_connection: no mapping of volume ' - '%(vol)s to host %(host)s found') % - {'vol': vol_name, 'host': host_name}) - - # If this host has no more mappings, delete it - if not mapping_data: - self._delete_host(host_name) - - LOG.debug(_('leave: terminate_connection: volume %(vol)s with ' - 'connector %(conn)s') % {'vol': str(volume), - 'conn': str(connector)}) - - def _flashcopy_cleanup(self, fc_map_id, source, target): - """Clean up a failed FlashCopy operation.""" - - try: - out, err = self._run_ssh('stopfcmap -force %s' % fc_map_id) - out, err = self._run_ssh('rmfcmap -force %s' % fc_map_id) - except exception.ProcessExecutionError as e: - LOG.error(_('_run_flashcopy: fail to cleanup failed FlashCopy ' - 'mapping %(fc_map_id)% ' - 'from %(source)s to %(target)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'fc_map_id': fc_map_id, - 'source': source, - 'target': target, - 'out': e.stdout, - 'err': e.stderr}) - - def _run_flashcopy(self, source, target): - """Create a FlashCopy mapping from the source to the target.""" - - LOG.debug( - _('enter: _run_flashcopy: execute FlashCopy from source ' - '%(source)s to target %(target)s') % {'source': source, - 'target': target}) - - fc_map_cli_cmd = ('mkfcmap -source %s -target %s -autodelete ' - '-cleanrate 0' % (source, target)) - out, err = self._run_ssh(fc_map_cli_cmd) - self._driver_assert(len(out.strip()) > 0, - _('create FC mapping from %(source)s to %(target)s - ' - 'did not find success message in CLI output.\n' - ' stdout: %(out)s\n stderr: %(err)s\n') - % {'source': source, - 'target': target, - 'out': str(out), - 'err': str(err)}) - - # Ensure that the output is as expected - match_obj = re.search('FlashCopy Mapping, id \[([0-9]+)\], ' - 'successfully created', out) - # Make sure we got a "successfully created" message with vdisk id - self._driver_assert(match_obj is not None, - _('create FC mapping from %(source)s to %(target)s - ' - 'did not find success message in CLI output.\n' - ' stdout: %(out)s\n stderr: %(err)s\n') - % {'source': source, - 'target': target, - 'out': str(out), - 'err': str(err)}) - - try: - fc_map_id = match_obj.group(1) - self._driver_assert(fc_map_id is not None, - _('create FC mapping from %(source)s to %(target)s - ' - 'did not find mapping id in CLI output.\n' - ' stdout: %(out)s\n stderr: %(err)s\n') - % {'source': source, - 'target': target, - 'out': str(out), - 'err': str(err)}) - except IndexError: - self._driver_assert(False, - _('create FC mapping from %(source)s to %(target)s - ' - 'did not find mapping id in CLI output.\n' - ' stdout: %(out)s\n stderr: %(err)s\n') - % {'source': source, - 'target': target, - 'out': str(out), - 'err': str(err)}) - try: - out, err = self._run_ssh('prestartfcmap %s' % fc_map_id) - except exception.ProcessExecutionError as e: - with excutils.save_and_reraise_exception(): - LOG.error(_('_run_flashcopy: fail to prepare FlashCopy ' - 'from %(source)s to %(target)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'source': source, - 'target': target, - 'out': e.stdout, - 'err': e.stderr}) - self._flashcopy_cleanup(fc_map_id, source, target) - - mapping_ready = False - wait_time = 5 - # Allow waiting of up to timeout (set as parameter) - max_retries = (int(FLAGS.storwize_svc_flashcopy_timeout) - / wait_time) + 1 - for try_number in range(1, max_retries): - mapping_attributes = self._get_flashcopy_mapping_attributes( - fc_map_id) - if (mapping_attributes is None or - 'status' not in mapping_attributes): - break - if mapping_attributes['status'] == 'prepared': - mapping_ready = True - break - elif mapping_attributes['status'] != 'preparing': - # Unexpected mapping status - exception_msg = (_('unexecpted mapping status %(status)s ' - 'for mapping %(id)s. Attributes: ' - '%(attr)s') - % {'status': mapping_attributes['status'], - 'id': fc_map_id, - 'attr': mapping_attributes}) - raise exception.VolumeBackendAPIException( - data=exception_msg) - # Need to wait for mapping to be prepared, wait a few seconds - time.sleep(wait_time) - - if not mapping_ready: - exception_msg = (_('mapping %(id)s prepare failed to complete ' - 'within the alloted %(to)s seconds timeout. ' - 'Terminating') % {'id': fc_map_id, - 'to': FLAGS.storwize_svc_flashcopy_timeout}) - LOG.error(_('_run_flashcopy: fail to start FlashCopy ' - 'from %(source)s to %(target)s with ' - 'exception %(ex)s') - % {'source': source, - 'target': target, - 'ex': exception_msg}) - self._flashcopy_cleanup(fc_map_id, source, target) - raise exception.InvalidSnapshot( - reason=_('_run_flashcopy: %s') % exception_msg) - - try: - out, err = self._run_ssh('startfcmap %s' % fc_map_id) - except exception.ProcessExecutionError as e: - with excutils.save_and_reraise_exception(): - LOG.error(_('_run_flashcopy: fail to start FlashCopy ' - 'from %(source)s to %(target)s.\n' - 'stdout: %(out)s\n stderr: %(err)s') - % {'source': source, - 'target': target, - 'out': e.stdout, - 'err': e.stderr}) - self._flashcopy_cleanup(fc_map_id, source, target) - - LOG.debug(_('leave: _run_flashcopy: FlashCopy started from ' - '%(source)s to %(target)s') % {'source': source, - 'target': target}) - - def create_volume_from_snapshot(self, volume, snapshot): - """Create a new snapshot from volume.""" - - source_volume = snapshot['name'] - tgt_volume = volume['name'] - - LOG.debug(_('enter: create_volume_from_snapshot: snapshot %(tgt)s ' - 'from volume %(src)s') % {'tgt': tgt_volume, - 'src': source_volume}) - - src_volume_attributes = self._get_volume_attributes(source_volume) - if src_volume_attributes is None: - exception_msg = (_('create_volume_from_snapshot: source volume %s ' - 'does not exist') % source_volume) - LOG.error(exception_msg) - raise exception.SnapshotNotFound(exception_msg, - volume_id=source_volume) - - self._driver_assert('capacity' in src_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' - '%(attr)s') % {'src': source_volume, - 'attr': src_volume_attributes}) - src_volume_size = src_volume_attributes['capacity'] - - tgt_volume_attributes = self._get_volume_attributes(tgt_volume) - # Does the snapshot target exist? - if tgt_volume_attributes is not None: - exception_msg = (_('create_volume_from_snapshot: target volume %s ' - 'already exists, cannot create') % tgt_volume) - LOG.error(exception_msg) - raise exception.InvalidSnapshot(reason=exception_msg) - - snapshot_volume = {} - snapshot_volume['name'] = tgt_volume - snapshot_volume['size'] = src_volume_size - - self._create_volume(snapshot_volume, units='b') - - try: - self._run_flashcopy(source_volume, tgt_volume) - except Exception: - with excutils.save_and_reraise_exception(): - # Clean up newly-created snapshot if the FlashCopy failed - self._delete_volume(snapshot_volume, True) - - LOG.debug( - _('leave: create_volume_from_snapshot: %s created successfully') - % tgt_volume) - - def create_snapshot(self, snapshot): - """Create a new snapshot using FlashCopy.""" - - src_volume = snapshot['volume_name'] - tgt_volume = snapshot['name'] - - # Flag to keep track of created volumes in case FlashCopy - tgt_volume_created = False - - LOG.debug(_('enter: create_snapshot: snapshot %(tgt)s from ' - 'volume %(src)s') % {'tgt': tgt_volume, - 'src': src_volume}) - - src_volume_attributes = self._get_volume_attributes(src_volume) - if src_volume_attributes is None: - exception_msg = ( - _('create_snapshot: source volume %s does not exist') - % src_volume) - LOG.error(exception_msg) - raise exception.VolumeNotFound(exception_msg, - volume_id=src_volume) - - self._driver_assert('capacity' in src_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' - '%(attr)s') % {'src': src_volume, - 'attr': src_volume_attributes}) - - source_volume_size = src_volume_attributes['capacity'] - - tgt_volume_attributes = self._get_volume_attributes(tgt_volume) - # Does the snapshot target exist? - snapshot_volume = {} - if tgt_volume_attributes is None: - # No, create a new snapshot volume - snapshot_volume['name'] = tgt_volume - snapshot_volume['size'] = source_volume_size - self._create_volume(snapshot_volume, units='b') - tgt_volume_created = True - else: - # Yes, target exists, verify exact same size as source - self._driver_assert('capacity' in tgt_volume_attributes, - _('create_volume_from_snapshot: cannot get source ' - 'volume %(src)s capacity from volume attributes ' - '%(attr)s') % {'src': tgt_volume, - 'attr': tgt_volume_attributes}) - target_volume_size = tgt_volume_attributes['capacity'] - if target_volume_size != source_volume_size: - exception_msg = ( - _('create_snapshot: source %(src)s and target ' - 'volume %(tgt)s have different capacities ' - '(source:%(ssize)s target:%(tsize)s)') % - {'src': src_volume, - 'tgt': tgt_volume, - 'ssize': source_volume_size, - 'tsize': target_volume_size}) - LOG.error(exception_msg) - raise exception.InvalidSnapshot(reason=exception_msg) - - try: - self._run_flashcopy(src_volume, tgt_volume) - except exception.InvalidSnapshot: - with excutils.save_and_reraise_exception(): - # Clean up newly-created snapshot if the FlashCopy failed - if tgt_volume_created: - self._delete_volume(snapshot_volume, True) - - LOG.debug(_('leave: create_snapshot: %s created successfully') - % tgt_volume) - - def delete_snapshot(self, snapshot): - self._delete_snapshot(snapshot, False) - - def _delete_snapshot(self, snapshot, force_opt): - """Delete a snapshot from the storage.""" - LOG.debug(_('enter: delete_snapshot: snapshot %s') % snapshot) - - snapshot_defined = self._is_volume_defined(snapshot['name']) - if snapshot_defined: - if force_opt: - self._delete_volume(snapshot, force_opt) - else: - self.delete_volume(snapshot) - - LOG.debug(_('leave: delete_snapshot: snapshot %s') % snapshot) - - def _get_host_from_iscsiname(self, iscsi_name): - """List the hosts defined in the storage. - - Return the host name with the given iSCSI name, or None if there is - no host name with that iSCSI name. - """ - - LOG.debug(_('enter: _get_host_from_iscsiname: iSCSI initiator %s') - % iscsi_name) - - # Get list of host in the storage - ssh_cmd = 'lshost -delim !' - out, err = self._run_ssh(ssh_cmd) - - if (len(out.strip()) == 0): - return None - - err_msg = _('_get_host_from_iscsiname: ' - 'failed with unexpected CLI output.\n' - ' command: %(cmd)s\n stdout: %(out)s\n ' - 'stderr: %(err)s') % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)} - host_lines = out.strip().split('\n') - self._driver_assert(len(host_lines) > 0, err_msg) - header = host_lines.pop(0).split('!') - self._driver_assert('name' in header, err_msg) - name_index = header.index('name') - - hosts = map(lambda x: x.split('!')[name_index], host_lines) - hostname = None - - # For each host, get its details and check for its iSCSI name - for host in hosts: - ssh_cmd = 'lshost -delim ! %s' % host - out, err = self._run_ssh(ssh_cmd) - self._driver_assert(len(out) > 0, - _('_get_host_from_iscsiname: ' - 'Unexpected response from CLI output. ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - for attrib_line in out.split('\n'): - # If '!' not found, return the string and two empty strings - attrib_name, foo, attrib_value = attrib_line.partition('!') - if attrib_name == 'iscsi_name': - if iscsi_name == attrib_value: - hostname = host - break - if hostname is not None: - break - - LOG.debug(_('leave: _get_host_from_iscsiname: iSCSI initiator %s') - % iscsi_name) - - return hostname - - def _create_new_host(self, host_name, initiator_name): - """Create a new host on the storage system. - - We modify the given host name, replace any invalid characters and - adding a random suffix to avoid conflicts due to the translation. The - host is associated with the given iSCSI initiator name. - """ - - LOG.debug(_('enter: _create_new_host: host %(name)s with iSCSI ' - 'initiator %(init)s') % {'name': host_name, - 'init': initiator_name}) - - if isinstance(host_name, unicode): - host_name = host_name.translate(self._unicode_host_name_filter) - elif isinstance(host_name, str): - host_name = host_name.translate(self._string_host_name_filter) - else: - msg = _('_create_new_host: cannot clean host name. Host name ' - 'is not unicode or string') - LOG.error(msg) - raise exception.NoValidHost(reason=msg) - - # Add 5 digit random suffix to the host name to avoid - # conflicts in host names after removing invalid characters - # for Storwize/SVC names - host_name = '%s_%s' % (host_name, random.randint(10000, 99999)) - out, err = self._run_ssh('mkhost -name "%s" -iscsiname "%s"' - % (host_name, initiator_name)) - self._driver_assert(len(out.strip()) > 0 and - 'successfully created' in out, - _('create host %(name)s with iSCSI initiator %(init)s - ' - 'did not find success message in CLI output.\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'name': host_name, - 'init': initiator_name, - 'out': str(out), - 'err': str(err)}) - - LOG.debug(_('leave: _create_new_host: host %(host)s with iSCSI ' - 'initiator %(init)s') % {'host': host_name, - 'init': initiator_name}) - - return host_name - - def _delete_host(self, host_name): - """Delete a host and associated iSCSI initiator name.""" - - LOG.debug(_('enter: _delete_host: host %s ') % host_name) - - # Check if host exists on system, expect to find the host - is_defined = self._is_host_defined(host_name) - if is_defined: - # Delete host - out, err = self._run_ssh('rmhost %s ' % host_name) - else: - LOG.info(_('warning: tried to delete host %(name)s but ' - 'it does not exist.') % {'name': host_name}) - - LOG.debug(_('leave: _delete_host: host %s ') % host_name) - - def _is_volume_defined(self, volume_name): - """Check if volume is defined.""" - LOG.debug(_('enter: _is_volume_defined: volume %s ') % volume_name) - volume_attributes = self._get_volume_attributes(volume_name) - LOG.debug(_('leave: _is_volume_defined: volume %(vol)s with %(str)s ') - % {'vol': volume_name, - 'str': volume_attributes is not None}) - if volume_attributes is None: - return False - else: - return True - - def _is_host_defined(self, host_name): - """Check if a host is defined on the storage.""" - - LOG.debug(_('enter: _is_host_defined: host %s ') % host_name) - - # Get list of hosts with the name %host_name% - # We expect zero or one line if host does not exist, - # two lines if it does exist, otherwise error - out, err = self._run_ssh('lshost -filtervalue name=%s -delim !' - % host_name) - if len(out.strip()) == 0: - return False - - lines = out.strip().split('\n') - self._driver_assert(len(lines) <= 2, - _('_is_host_defined: Unexpected response from CLI output.\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'out': str(out), - 'err': str(err)}) - - if len(lines) == 2: - host_info = self._get_hdr_dic(lines[0], lines[1], '!') - host_name_from_storage = host_info['name'] - # Make sure we got the data for the right host - self._driver_assert(host_name_from_storage == host_name, - _('Data received for host %(host1)s instead of host ' - '%(host2)s.\n ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'host1': host_name_from_storage, - 'host2': host_name, - 'out': str(out), - 'err': str(err)}) - else: # 0 or 1 lines - host_name_from_storage = None - - LOG.debug(_('leave: _is_host_defined: host %(host)s with %(str)s ') % { - 'host': host_name, - 'str': host_name_from_storage is not None}) - - if host_name_from_storage is None: - return False - else: - return True - - def _get_hostvdisk_mappings(self, host_name): - """Return the defined storage mappings for a host.""" - - return_data = {} - ssh_cmd = 'lshostvdiskmap -delim ! %s' % host_name - out, err = self._run_ssh(ssh_cmd) - - mappings = out.strip().split('\n') - if len(mappings) > 0: - header = mappings.pop(0) - for mapping_line in mappings: - mapping_data = self._get_hdr_dic(header, mapping_line, '!') - return_data[mapping_data['vdisk_name']] = mapping_data - - return return_data - - def _map_vol_to_host(self, volume_name, host_name): - """Create a mapping between a volume to a host.""" - - LOG.debug(_('enter: _map_vol_to_host: volume %(vol)s to ' - 'host %(host)s') % {'vol': volume_name, - 'host': host_name}) - - # Check if this volume is already mapped to this host - mapping_data = self._get_hostvdisk_mappings(host_name) - - mapped_flag = False - result_lun = '-1' - if volume_name in mapping_data: - mapped_flag = True - result_lun = mapping_data[volume_name]['SCSI_id'] - else: - lun_used = [] - for k, v in mapping_data.iteritems(): - lun_used.append(int(v['SCSI_id'])) - lun_used.sort() - # Assume all luns are taken to this point, and then try to find - # an unused one - result_lun = str(len(lun_used)) - for index, n in enumerate(lun_used): - if n > index: - result_lun = str(index) - - # Volume is not mapped to host, create a new LUN - if not mapped_flag: - out, err = self._run_ssh('mkvdiskhostmap -host %s -scsi %s %s' - % (host_name, result_lun, volume_name)) - self._driver_assert(len(out.strip()) > 0 and - 'successfully created' in out, - _('_map_vol_to_host: mapping host %(host)s to ' - 'volume %(vol)s with LUN ' - '%(lun)s - did not find success message in CLI output. ' - 'stdout: %(out)s\n stderr: %(err)s\n') - % {'host': host_name, - 'vol': volume_name, - 'lun': result_lun, - 'out': str(out), - 'err': str(err)}) - - LOG.debug(_('leave: _map_vol_to_host: LUN %(lun)s, volume %(vol)s, ' - 'host %(host)s') % {'lun': result_lun, 'vol': volume_name, - 'host': host_name}) - - return result_lun - - def _get_flashcopy_mapping_attributes(self, fc_map_id): - """Return the attributes of a FlashCopy mapping. - - Returns the attributes for the specified FlashCopy mapping, or - None if the mapping does not exist. - An exception is raised if the information from system can not - be parsed or matched to a single FlashCopy mapping (this case - should not happen under normal conditions). - """ - - LOG.debug(_('enter: _get_flashcopy_mapping_attributes: mapping %s') - % fc_map_id) - # Get the lunid to be used - - fc_ls_map_cmd = ('lsfcmap -filtervalue id=%s -delim !' % fc_map_id) - out, err = self._run_ssh(fc_ls_map_cmd) - self._driver_assert(len(out) > 0, - _('_get_flashcopy_mapping_attributes: ' - 'Unexpected response from CLI output. ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': fc_ls_map_cmd, - 'out': str(out), - 'err': str(err)}) - - # Get list of FlashCopy mappings - # We expect zero or one line if mapping does not exist, - # two lines if it does exist, otherwise error - lines = out.strip().split('\n') - self._driver_assert(len(lines) <= 2, - _('_get_flashcopy_mapping_attributes: ' - 'Unexpected response from CLI output. ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': fc_ls_map_cmd, - 'out': str(out), - 'err': str(err)}) - - if len(lines) == 2: - attributes = self._get_hdr_dic(lines[0], lines[1], '!') - else: # 0 or 1 lines - attributes = None - - LOG.debug(_('leave: _get_flashcopy_mapping_attributes: mapping ' - '%(id)s, attributes %(attr)s') % - {'id': fc_map_id, - 'attr': attributes}) - - return attributes - - def _get_volume_attributes(self, volume_name): - """Return volume attributes, or None if volume does not exist - - Exception is raised if the information from system can not be - parsed/matched to a single volume. - """ - - LOG.debug(_('enter: _get_volume_attributes: volume %s') - % volume_name) - # Get the lunid to be used - - try: - ssh_cmd = 'lsvdisk -bytes -delim ! %s ' % volume_name - out, err = self._run_ssh(ssh_cmd) - except exception.ProcessExecutionError as e: - # Didn't get details from the storage, return None - LOG.error(_('CLI Exception output:\n command: %(cmd)s\n ' - 'stdout: %(out)s\n stderr: %(err)s') % - {'cmd': ssh_cmd, - 'out': e.stdout, - 'err': e.stderr}) - return None - - self._driver_assert(len(out) > 0, - ('_get_volume_attributes: ' - 'Unexpected response from CLI output. ' - 'Command: %(cmd)s\n stdout: %(out)s\n stderr: %(err)s') - % {'cmd': ssh_cmd, - 'out': str(out), - 'err': str(err)}) - attributes = {} - for attrib_line in out.split('\n'): - # If '!' not found, return the string and two empty strings - attrib_name, foo, attrib_value = attrib_line.partition('!') - if attrib_name is not None and attrib_name.strip() > 0: - attributes[attrib_name] = attrib_value - - LOG.debug(_('leave: _get_volume_attributes:\n volume %(vol)s\n ' - 'attributes: %(attr)s') - % {'vol': volume_name, - 'attr': str(attributes)}) - - return attributes diff --git a/nova/volume/utils.py b/nova/volume/utils.py deleted file mode 100644 index c1367114d..000000000 --- a/nova/volume/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 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. - -"""Volume-related Utilities and helpers.""" - -from nova import flags -from nova.openstack.common import log as logging -from nova.openstack.common.notifier import api as notifier_api -from nova.openstack.common import timeutils -from nova import utils - - -FLAGS = flags.FLAGS -LOG = logging.getLogger(__name__) - - -def notify_usage_exists(context, volume_ref, current_period=False): - """ Generates 'exists' notification for a volume for usage auditing - purposes. - - Generates usage for last completed period, unless 'current_period' - is True.""" - begin, end = utils.last_completed_audit_period() - if current_period: - audit_start = end - audit_end = timeutils.utcnow() - else: - audit_start = begin - audit_end = end - - extra_usage_info = dict(audit_period_beginning=str(audit_start), - audit_period_ending=str(audit_end)) - - notify_about_volume_usage( - context, volume_ref, 'exists', extra_usage_info=extra_usage_info) - - -def _usage_from_volume(context, volume_ref, **kw): - def null_safe_str(s): - return str(s) if s else '' - - usage_info = dict( - tenant_id=volume_ref['project_id'], - user_id=volume_ref['user_id'], - volume_id=volume_ref['id'], - volume_type=volume_ref['volume_type'], - display_name=volume_ref['display_name'], - launched_at=null_safe_str(volume_ref['launched_at']), - created_at=null_safe_str(volume_ref['created_at']), - status=volume_ref['status'], - snapshot_id=volume_ref['snapshot_id'], - size=volume_ref['size']) - - usage_info.update(kw) - return usage_info - - -def notify_about_volume_usage(context, volume, event_suffix, - extra_usage_info=None, host=None): - if not host: - host = FLAGS.host - - if not extra_usage_info: - extra_usage_info = {} - - usage_info = _usage_from_volume( - context, volume, **extra_usage_info) - - notifier_api.notify(context, 'volume.%s' % host, - 'volume.%s' % event_suffix, - notifier_api.INFO, usage_info) diff --git a/nova/volume/volume_types.py b/nova/volume/volume_types.py deleted file mode 100644 index 67d824465..000000000 --- a/nova/volume/volume_types.py +++ /dev/null @@ -1,125 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2011 Zadara Storage Inc. -# Copyright (c) 2011 OpenStack LLC. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright (c) 2010 Citrix Systems, Inc. -# Copyright 2011 Ken Pepple -# -# 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. - -"""Built-in volume type properties.""" - -from nova import context -from nova import db -from nova import exception -from nova import flags -from nova.openstack.common import log as logging - -FLAGS = flags.FLAGS -LOG = logging.getLogger(__name__) - - -def create(context, name, extra_specs={}): - """Creates volume types.""" - try: - db.volume_type_create(context, - dict(name=name, - extra_specs=extra_specs)) - except exception.DBError, e: - LOG.exception(_('DB error: %s') % e) - raise exception.VolumeTypeCreateFailed(name=name, - extra_specs=extra_specs) - - -def destroy(context, name): - """Marks volume types as deleted.""" - if name is None: - msg = _("name cannot be None") - raise exception.InvalidVolumeType(reason=msg) - else: - db.volume_type_destroy(context, name) - - -def get_all_types(context, inactive=0, search_opts={}): - """Get all non-deleted volume_types. - - Pass true as argument if you want deleted volume types returned also. - - """ - vol_types = db.volume_type_get_all(context, inactive) - - if search_opts: - LOG.debug(_("Searching by: %s") % str(search_opts)) - - def _check_extra_specs_match(vol_type, searchdict): - for k, v in searchdict.iteritems(): - if (k not in vol_type['extra_specs'].keys() - or vol_type['extra_specs'][k] != v): - return False - return True - - # search_option to filter_name mapping. - filter_mapping = {'extra_specs': _check_extra_specs_match} - - result = {} - for type_name, type_args in vol_types.iteritems(): - # go over all filters in the list - for opt, values in search_opts.iteritems(): - try: - filter_func = filter_mapping[opt] - except KeyError: - # no such filter - ignore it, go to next filter - continue - else: - if filter_func(type_args, values): - result[type_name] = type_args - break - vol_types = result - return vol_types - - -def get_volume_type(ctxt, id): - """Retrieves single volume type by id.""" - if id is None: - msg = _("id cannot be None") - raise exception.InvalidVolumeType(reason=msg) - - if ctxt is None: - ctxt = context.get_admin_context() - - return db.volume_type_get(ctxt, id) - - -def get_volume_type_by_name(context, name): - """Retrieves single volume type by name.""" - if name is None: - msg = _("name cannot be None") - raise exception.InvalidVolumeType(reason=msg) - - return db.volume_type_get_by_name(context, name) - - -def is_key_value_present(volume_type_id, key, value, volume_type=None): - if volume_type_id is None: - return False - - if volume_type is None: - volume_type = get_volume_type(context.get_admin_context(), - volume_type_id) - if (volume_type.get('extra_specs') is None or - volume_type['extra_specs'].get(key) != value): - return False - else: - return True diff --git a/nova/volume/xensm.py b/nova/volume/xensm.py deleted file mode 100644 index 7e9a4b0ee..000000000 --- a/nova/volume/xensm.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (c) 2011 Citrix 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 express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from nova import exception -from nova import flags -from nova.openstack.common import log as logging -from nova import utils -from nova.virt.xenapi import driver as xenapi_conn -from nova.virt.xenapi import volumeops -import nova.volume.driver - -LOG = logging.getLogger(__name__) -FLAGS = flags.FLAGS - - -class XenSMDriver(nova.volume.driver.VolumeDriver): - - def _convert_config_params(self, conf_str): - params = dict([item.split("=") for item in conf_str.split()]) - return params - - def _get_introduce_sr_keys(self, params): - if 'name_label' in params: - del params['name_label'] - keys = params.keys() - keys.append('sr_type') - return keys - - def _create_storage_repo(self, context, backend_ref): - """Either creates or introduces SR on host - depending on whether it exists in xapi db.""" - params = self._convert_config_params(backend_ref['config_params']) - if 'name_label' in params: - label = params['name_label'] - del params['name_label'] - else: - label = 'SR-' + str(backend_ref['id']) - - params['sr_type'] = backend_ref['sr_type'] - - if backend_ref['sr_uuid'] is None: - # run the sr create command - try: - LOG.debug(_('SR name = %s') % label) - LOG.debug(_('Params: %s') % str(params)) - sr_uuid = self._volumeops.create_sr(label, params) - # update sr_uuid and created in db - except Exception as ex: - LOG.debug(_("Failed to create sr %s...continuing") % - str(backend_ref['id'])) - msg = _('Create failed') - raise exception.VolumeBackendAPIException(data=msg) - - LOG.debug(_('SR UUID of new SR is: %s') % sr_uuid) - try: - self.db.sm_backend_conf_update(context, - backend_ref['id'], - dict(sr_uuid=sr_uuid)) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to update db") - raise exception.VolumeBackendAPIException(data=msg) - - else: - # sr introduce, if not already done - try: - self._volumeops.introduce_sr(backend_ref['sr_uuid'], label, - params) - except Exception as ex: - LOG.exception(ex) - LOG.debug(_("Failed to introduce sr %s...continuing") - % str(backend_ref['id'])) - - def _create_storage_repos(self, context): - """Create/Introduce storage repositories at start.""" - backends = self.db.sm_backend_conf_get_all(context) - for backend in backends: - try: - self._create_storage_repo(context, backend) - except Exception as ex: - LOG.exception(ex) - msg = _('Failed to reach backend %d') % backend['id'] - raise exception.VolumeBackendAPIException(data=msg) - - def __init__(self, *args, **kwargs): - """Connect to the hypervisor.""" - - # This driver leverages Xen storage manager, and hence requires - # hypervisor to be Xen - if not FLAGS.compute_driver.endswith('XenAPIDriver'): - msg = (_('XenSMDriver requires xenapi connection, using %s') % - FLAGS.compute_driver) - raise exception.VolumeBackendAPIException(data=msg) - - url = FLAGS.xenapi_connection_url - username = FLAGS.xenapi_connection_username - password = FLAGS.xenapi_connection_password - try: - session = xenapi_conn.XenAPISession(url, username, password) - self._volumeops = volumeops.VolumeOps(session) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to initiate session") - raise exception.VolumeBackendAPIException(data=msg) - - super(XenSMDriver, self).__init__(execute=utils.execute, - sync_exec=utils.execute, - *args, **kwargs) - - def do_setup(self, ctxt): - """Setup includes creating or introducing storage repos - existing in the database and destroying deleted ones.""" - - # TODO(renukaapte) purge storage repos - self.ctxt = ctxt - self._create_storage_repos(ctxt) - - def create_volume(self, volume): - """Creates a logical volume. Can optionally return a Dictionary of - changes to the volume object to be persisted.""" - - # For now the scheduling logic will be to try to fit the volume in - # the first available backend. - # TODO(renukaapte) better scheduling once APIs are in place - sm_vol_rec = None - backends = self.db.sm_backend_conf_get_all(self.ctxt) - for backend in backends: - # Ensure that storage repo exists, if not create. - # This needs to be done because if nova compute and - # volume are both running on this host, then, as a - # part of detach_volume, compute could potentially forget SR - self._create_storage_repo(self.ctxt, backend) - sm_vol_rec = self._volumeops.create_volume_for_sm(volume, - backend['sr_uuid']) - if sm_vol_rec: - LOG.debug(_('Volume will be created in backend - %d') - % backend['id']) - break - - if sm_vol_rec: - # Update db - sm_vol_rec['id'] = volume['id'] - sm_vol_rec['backend_id'] = backend['id'] - try: - self.db.sm_volume_create(self.ctxt, sm_vol_rec) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to update volume in db") - raise exception.VolumeBackendAPIException(data=msg) - - else: - msg = _('Unable to create volume') - raise exception.VolumeBackendAPIException(data=msg) - - def delete_volume(self, volume): - - vol_rec = self.db.sm_volume_get(self.ctxt, volume['id']) - if not vol_rec: - raise exception.NotFound(_("Volume %s does not exist"), - volume['id']) - try: - # If compute runs on this node, detach could have disconnected SR - backend_ref = self.db.sm_backend_conf_get(self.ctxt, - vol_rec['backend_id']) - self._create_storage_repo(self.ctxt, backend_ref) - self._volumeops.delete_volume_for_sm(vol_rec['vdi_uuid']) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to delete vdi") - raise exception.VolumeBackendAPIException(data=msg) - - try: - self.db.sm_volume_delete(self.ctxt, volume['id']) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to delete volume in db") - raise exception.VolumeBackendAPIException(data=msg) - - def local_path(self, volume): - return str(volume['id']) - - def undiscover_volume(self, volume): - """Undiscover volume on a remote host.""" - pass - - def discover_volume(self, context, volume): - return str(volume['id']) - - def check_for_setup_error(self): - pass - - def create_export(self, context, volume): - """Exports the volume.""" - pass - - def remove_export(self, context, volume): - """Removes an export for a logical volume.""" - pass - - def ensure_export(self, context, volume): - """Safely, synchronously recreates an export for a logical volume.""" - pass - - def initialize_connection(self, volume, connector): - try: - xensm_properties = dict(self.db.sm_volume_get(self.ctxt, - volume['id'])) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to find volume in db") - raise exception.VolumeBackendAPIException(data=msg) - - # Keep the volume id key consistent with what ISCSI driver calls it - xensm_properties['volume_id'] = xensm_properties['id'] - del xensm_properties['id'] - - try: - backend_conf = self.db.sm_backend_conf_get(self.ctxt, - xensm_properties['backend_id']) - except Exception as ex: - LOG.exception(ex) - msg = _("Failed to find backend in db") - raise exception.VolumeBackendAPIException(data=msg) - - params = self._convert_config_params(backend_conf['config_params']) - - xensm_properties['flavor_id'] = backend_conf['flavor_id'] - xensm_properties['sr_uuid'] = backend_conf['sr_uuid'] - xensm_properties['sr_type'] = backend_conf['sr_type'] - xensm_properties.update(params) - _introduce_sr_keys = self._get_introduce_sr_keys(params) - xensm_properties['introduce_sr_keys'] = _introduce_sr_keys - return { - 'driver_volume_type': 'xensm', - 'data': xensm_properties - } - - def terminate_connection(self, volume, connector): - pass |
