self.resources['volumes'] = volumes.create_resource()
mapper.resource("volume", "volumes",
controller=self.resources['volumes'],
- collection={'detail': 'GET'})
+ collection={'detail': 'GET'},
+ member={'action': 'POST'})
self.resources['types'] = types.create_resource()
mapper.resource("type", "types",
--- /dev/null
+# 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.
+
+"""The Extended Snapshot Attributes API extension."""
+
+from webob import exc
+
+from cinder.api.openstack import extensions
+from cinder.api.openstack import wsgi
+from cinder.api.openstack import xmlutil
+from cinder import volume
+from cinder import db
+from cinder import exception
+from cinder import flags
+from cinder import log as logging
+
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger(__name__)
+authorize = extensions.soft_extension_authorizer('volume',
+ 'extended_snapshot_attributes')
+
+
+class ExtendedSnapshotAttributesController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(ExtendedSnapshotAttributesController, self).__init__(*args,
+ **kwargs)
+ self.volume_api = volume.API()
+
+ def _get_snapshots(self, context):
+ snapshots = self.volume_api.get_all_snapshots(context)
+ rval = dict((snapshot['id'], snapshot) for snapshot in snapshots)
+ return rval
+
+ def _extend_snapshot(self, context, snapshot, data):
+ for attr in ['project_id', 'progress']:
+ key = "%s:%s" % (Extended_snapshot_attributes.alias, attr)
+ snapshot[key] = data[attr]
+
+ @wsgi.extends
+ def show(self, req, resp_obj, id):
+ context = req.environ['cinder.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=ExtendedSnapshotAttributeTemplate())
+
+ try:
+ snapshot = self.volume_api.get_snapshot(context, id)
+ except exception.NotFound:
+ explanation = _("Snapshot not found.")
+ raise exc.HTTPNotFound(explanation=explanation)
+
+ self._extend_snapshot(context, resp_obj.obj['snapshot'], snapshot)
+
+ @wsgi.extends
+ def detail(self, req, resp_obj):
+ context = req.environ['cinder.context']
+ if authorize(context):
+ # Attach our slave template to the response object
+ resp_obj.attach(xml=ExtendedSnapshotAttributesTemplate())
+
+ snapshots = list(resp_obj.obj.get('snapshots', []))
+ db_snapshots = self._get_snapshots(context)
+
+ for snapshot_object in snapshots:
+ try:
+ snapshot_data = db_snapshots[snapshot_object['id']]
+ except KeyError:
+ continue
+
+ self._extend_snapshot(context, snapshot_object, snapshot_data)
+
+
+class Extended_snapshot_attributes(extensions.ExtensionDescriptor):
+ """Extended SnapshotAttributes support."""
+
+ name = "ExtendedSnapshotAttributes"
+ alias = "os-extended-snapshot-attributes"
+ namespace = ("http://docs.openstack.org/volume/ext/"
+ "extended_snapshot_attributes/api/v1")
+ updated = "2012-06-19T00:00:00+00:00"
+
+ def get_controller_extensions(self):
+ controller = ExtendedSnapshotAttributesController()
+ extension = extensions.ControllerExtension(self, 'snapshots',
+ controller)
+ return [extension]
+
+
+def make_snapshot(elem):
+ elem.set('{%s}project_id' % Extended_snapshot_attributes.namespace,
+ '%s:project_id' % Extended_snapshot_attributes.alias)
+ elem.set('{%s}progress' % Extended_snapshot_attributes.namespace,
+ '%s:progress' % Extended_snapshot_attributes.alias)
+
+
+class ExtendedSnapshotAttributeTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('snapshot', selector='snapshot')
+ make_snapshot(root)
+ alias = Extended_snapshot_attributes.alias
+ namespace = Extended_snapshot_attributes.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
+
+
+class ExtendedSnapshotAttributesTemplate(xmlutil.TemplateBuilder):
+ def construct(self):
+ root = xmlutil.TemplateElement('snapshots')
+ elem = xmlutil.SubTemplateElement(root, 'snapshot',
+ selector='snapshots')
+ make_snapshot(elem)
+ alias = Extended_snapshot_attributes.alias
+ namespace = Extended_snapshot_attributes.namespace
+ return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
--- /dev/null
+# 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 os.path
+import traceback
+
+import webob
+from webob import exc
+
+from cinder.api.openstack import common
+from cinder.api.openstack import extensions
+from cinder.api.openstack import wsgi
+from cinder import volume
+from cinder import exception
+from cinder import flags
+from cinder import log as logging
+
+
+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 VolumeActionsController(wsgi.Controller):
+ def __init__(self, *args, **kwargs):
+ super(VolumeActionsController, self).__init__(*args, **kwargs)
+ self.volume_api = volume.API()
+
+ @wsgi.action('os-attach')
+ def _attach(self, req, id, body):
+ """Add attachment metadata."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+
+ instance_uuid = body['os-attach']['instance_uuid']
+ mountpoint = body['os-attach']['mountpoint']
+
+ self.volume_api.attach(context, volume,
+ instance_uuid, mountpoint)
+ return webob.Response(status_int=202)
+
+ @wsgi.action('os-detach')
+ def _detach(self, req, id, body):
+ """Clear attachment metadata."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+ self.volume_api.detach(context, volume)
+ return webob.Response(status_int=202)
+
+ @wsgi.action('os-reserve')
+ def _reserve(self, req, id, body):
+ """Mark volume as reserved."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+ self.volume_api.reserve_volume(context, volume)
+ return webob.Response(status_int=202)
+
+ @wsgi.action('os-unreserve')
+ def _unreserve(self, req, id, body):
+ """Unmark volume as reserved."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+ self.volume_api.unreserve_volume(context, volume)
+ return webob.Response(status_int=202)
+
+ @wsgi.action('os-initialize_connection')
+ def _initialize_connection(self, req, id, body):
+ """Initialize volume attachment."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+ connector = body['os-initialize_connection']['connector']
+ info = self.volume_api.initialize_connection(context,
+ volume,
+ connector)
+ return {'connection_info': info}
+
+ @wsgi.action('os-terminate_connection')
+ def _terminate_connection(self, req, id, body):
+ """Terminate volume attachment."""
+ context = req.environ['cinder.context']
+ volume = self.volume_api.get(context, id)
+ connector = body['os-terminate_connection']['connector']
+ self.volume_api.terminate_connection(context, volume, connector)
+ return webob.Response(status_int=202)
+
+
+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]
FLAGS = flags.FLAGS
-def _translate_snapshot_detail_view(context, vol):
+def _translate_snapshot_detail_view(context, snapshot):
"""Maps keys for snapshots details view."""
- d = _translate_snapshot_summary_view(context, vol)
+ d = _translate_snapshot_summary_view(context, snapshot)
# NOTE(gagupta): No additional data / lookups at the moment
return d
-def _translate_snapshot_summary_view(context, vol):
+def _translate_snapshot_summary_view(context, snapshot):
"""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']
+ d['id'] = snapshot['id']
+ d['created_at'] = snapshot['created_at']
+ d['display_name'] = snapshot['display_name']
+ d['display_description'] = snapshot['display_description']
+ d['volume_id'] = snapshot['volume_id']
+ d['status'] = snapshot['status']
+ d['size'] = snapshot['volume_size']
+
return d
from cinder.api import openstack as openstack_api
from cinder.api.openstack import auth
from cinder.api.openstack import urlmap
+from cinder.api.openstack import volume
from cinder.api.openstack.volume import versions
from cinder.api.openstack import wsgi as os_wsgi
from cinder import context
return self.application
-def wsgi_app(inner_app_v2=None, fake_auth=True, fake_auth_context=None,
+def wsgi_app(inner_app_v1=None, fake_auth=True, fake_auth_context=None,
use_no_auth=False, ext_mgr=None):
- if not inner_app_v2:
- inner_app_v2 = compute.APIRouter(ext_mgr)
+ if not inner_app_v1:
+ inner_app_v1 = volume.APIRouter(ext_mgr)
if fake_auth:
if fake_auth_context is not None:
ctxt = fake_auth_context
else:
ctxt = context.RequestContext('fake', 'fake', auth_token=True)
- api_v2 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
- limits.RateLimitingMiddleware(inner_app_v2)))
+ api_v1 = openstack_api.FaultWrapper(api_auth.InjectContext(ctxt,
+ inner_app_v1))
elif use_no_auth:
- api_v2 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
- limits.RateLimitingMiddleware(inner_app_v2)))
+ api_v1 = openstack_api.FaultWrapper(auth.NoAuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app_v1)))
else:
- api_v2 = openstack_api.FaultWrapper(auth.AuthMiddleware(
- limits.RateLimitingMiddleware(inner_app_v2)))
+ api_v1 = openstack_api.FaultWrapper(auth.AuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app_v1)))
mapper = urlmap.URLMap()
- mapper['/v2'] = api_v2
- mapper['/v1.1'] = api_v2
+ mapper['/v1'] = api_v1
mapper['/'] = openstack_api.FaultWrapper(versions.Versions())
return mapper
@classmethod
def blank(cls, *args, **kwargs):
- kwargs['base_url'] = 'http://localhost/v2'
+ kwargs['base_url'] = 'http://localhost/v1'
use_admin_context = kwargs.pop('use_admin_context', False)
out = webob.Request.blank(*args, **kwargs)
out.environ['cinder.context'] = FakeRequestContext('fake_user', 'fake',
--- /dev/null
+# 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 lxml import etree
+import webob
+import json
+
+from cinder.api.openstack.volume.contrib import extended_snapshot_attributes
+from cinder import volume
+from cinder import exception
+from cinder import flags
+from cinder import test
+from cinder.tests.api.openstack import fakes
+
+
+FLAGS = flags.FLAGS
+
+
+UUID1 = '00000000-0000-0000-0000-000000000001'
+UUID2 = '00000000-0000-0000-0000-000000000002'
+
+
+def _get_default_snapshot_param():
+ return {
+ 'id': UUID1,
+ 'volume_id': 12,
+ 'status': 'available',
+ 'volume_size': 100,
+ 'created_at': None,
+ 'display_name': 'Default name',
+ 'display_description': 'Default description',
+ 'project_id': 'fake',
+ 'progress': '0%'
+ }
+
+
+def fake_snapshot_get(self, context, snapshot_id):
+ param = _get_default_snapshot_param()
+ return param
+
+
+def fake_snapshot_get_all(self, context):
+ param = _get_default_snapshot_param()
+ return [param]
+
+
+class ExtendedSnapshotAttributesTest(test.TestCase):
+ content_type = 'application/json'
+ prefix = 'os-extended-snapshot-attributes:'
+
+ def setUp(self):
+ super(ExtendedSnapshotAttributesTest, self).setUp()
+ self.stubs.Set(volume.api.API, 'get_snapshot', fake_snapshot_get)
+ self.stubs.Set(volume.api.API, 'get_all_snapshots',
+ fake_snapshot_get_all)
+
+ def _make_request(self, url):
+ req = webob.Request.blank(url)
+ req.headers['Accept'] = self.content_type
+ res = req.get_response(fakes.wsgi_app())
+ return res
+
+ def _get_snapshot(self, body):
+ return json.loads(body).get('snapshot')
+
+ def _get_snapshots(self, body):
+ return json.loads(body).get('snapshots')
+
+ def assertSnapshotAttributes(self, snapshot, project_id, progress):
+ self.assertEqual(snapshot.get('%sproject_id' % self.prefix),
+ project_id)
+ self.assertEqual(snapshot.get('%sprogress' % self.prefix), progress)
+
+ def test_show(self):
+ url = '/v1/fake/snapshots/%s' % UUID2
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ self.assertSnapshotAttributes(self._get_snapshot(res.body),
+ project_id='fake',
+ progress='0%')
+
+ def test_detail(self):
+ url = '/v1/fake/snapshots/detail'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 200)
+ for i, snapshot in enumerate(self._get_snapshots(res.body)):
+ self.assertSnapshotAttributes(snapshot,
+ project_id='fake',
+ progress='0%')
+
+ def test_no_instance_passthrough_404(self):
+
+ def fake_snapshot_get(*args, **kwargs):
+ raise exception.InstanceNotFound()
+
+ self.stubs.Set(volume.api.API, 'get_snapshot', fake_snapshot_get)
+ url = '/v1/fake/snapshots/70f6db34-de8d-4fbd-aafb-4065bdfa6115'
+ res = self._make_request(url)
+
+ self.assertEqual(res.status_int, 404)
+
+
+class ExtendedSnapshotAttributesXmlTest(ExtendedSnapshotAttributesTest):
+ content_type = 'application/xml'
+ ext = extended_snapshot_attributes
+ prefix = '{%s}' % ext.Extended_snapshot_attributes.namespace
+
+ def _get_snapshot(self, body):
+ return etree.XML(body)
+
+ def _get_snapshots(self, body):
+ return etree.XML(body).getchildren()
--- /dev/null
+# 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 json
+
+import webob
+
+from cinder.api.openstack import volume as volume_api
+from cinder import volume
+from cinder import context
+from cinder import exception
+from cinder import flags
+from cinder import test
+from cinder.tests.api.openstack import fakes
+from cinder import utils
+
+
+FLAGS = flags.FLAGS
+
+
+def fake_volume_api(*args, **kwargs):
+ return True
+
+
+def fake_volume_get(*args, **kwargs):
+ return {'id': 'fake', 'host': 'fake'}
+
+
+class VolumeActionsTest(test.TestCase):
+
+ _actions = ('os-detach', 'os-reserve', 'os-unreserve')
+
+ _methods = ('attach', 'detach', 'reserve_volume', 'unreserve_volume')
+
+ def setUp(self):
+ super(VolumeActionsTest, self).setUp()
+ self.stubs.Set(volume.API, 'get', fake_volume_api)
+ self.UUID = utils.gen_uuid()
+ for _method in self._methods:
+ self.stubs.Set(volume.API, _method, fake_volume_api)
+
+ self.stubs.Set(volume.API, 'get', fake_volume_get)
+
+ def test_simple_api_actions(self):
+ app = fakes.wsgi_app()
+ for _action in self._actions:
+ req = webob.Request.blank('/v1/fake/volumes/%s/action' %
+ self.UUID)
+ req.method = 'POST'
+ req.body = json.dumps({_action: None})
+ req.content_type = 'application/json'
+ res = req.get_response(app)
+ self.assertEqual(res.status_int, 202)
+
+ def test_initialize_connection(self):
+ def fake_initialize_connection(*args, **kwargs):
+ return {}
+ self.stubs.Set(volume.API, 'initialize_connection',
+ fake_initialize_connection)
+
+ body = {'os-initialize_connection': {'connector': 'fake'}}
+ req = webob.Request.blank('/v1/fake/volumes/1/action')
+ req.method = "POST"
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ output = json.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+
+ def test_terminate_connection(self):
+ def fake_terminate_connection(*args, **kwargs):
+ return {}
+ self.stubs.Set(volume.API, 'terminate_connection',
+ fake_terminate_connection)
+
+ body = {'os-terminate_connection': {'connector': 'fake'}}
+ req = webob.Request.blank('/v1/fake/volumes/1/action')
+ req.method = "POST"
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
+
+ def test_attach(self):
+ body = {'os-attach': {'instance_uuid': 'fake',
+ 'mountpoint': '/dev/vdc'}}
+ req = webob.Request.blank('/v1/fake/volumes/1/action')
+ req.method = "POST"
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 202)
from cinder import volume
from cinder.tests.api.openstack import fakes
-FLAGS = flags.FLAGS
+FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
+UUID = '00000000-0000-0000-0000-000000000001'
+INVALID_UUID = '00000000-0000-0000-0000-000000000002'
+
def _get_default_snapshot_param():
return {
- 'id': 123,
+ 'id': UUID,
'volume_id': 12,
'status': 'available',
'volume_size': 100,
def stub_snapshot_delete(self, context, snapshot):
- if snapshot['id'] != 123:
+ if snapshot['id'] != UUID:
raise exception.NotFound
def stub_snapshot_get(self, context, snapshot_id):
- if snapshot_id != 123:
+ if snapshot_id != UUID:
raise exception.NotFound
param = _get_default_snapshot_param()
def test_snapshot_delete(self):
self.stubs.Set(volume.api.API, "delete_snapshot", stub_snapshot_delete)
- snapshot_id = 123
- req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id)
+ snapshot_id = UUID
+ req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % 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)
+ snapshot_id = INVALID_UUID
+ req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % snapshot_id)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete,
req,
snapshot_id)
def test_snapshot_show(self):
- req = fakes.HTTPRequest.blank('/v1/snapshots/123')
- resp_dict = self.controller.show(req, 123)
+ req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % UUID)
+ resp_dict = self.controller.show(req, UUID)
self.assertTrue('snapshot' in resp_dict)
- self.assertEqual(resp_dict['snapshot']['id'], '123')
+ self.assertEqual(resp_dict['snapshot']['id'], UUID)
def test_snapshot_show_invalid_id(self):
- snapshot_id = 234
- req = fakes.HTTPRequest.blank('/v1/snapshots/%d' % snapshot_id)
+ snapshot_id = INVALID_UUID
+ req = fakes.HTTPRequest.blank('/v1/snapshots/%s' % snapshot_id)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req,
self.assertEqual(len(resp_snapshots), 1)
resp_snapshot = resp_snapshots.pop()
- self.assertEqual(resp_snapshot['id'], '123')
+ self.assertEqual(resp_snapshot['id'], UUID)
class SnapshotSerializerTest(test.TestCase):
"volume:get_all_snapshots": [],
"volume_extension:types_manage": [],
- "volume_extension:types_extra_specs": []
+ "volume_extension:types_extra_specs": [],
+ "volume_extension:extended_snapshot_attributes": []
}
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"
+ # TODO(sleepsonthefloor): Is this 'elevated' appropriate?
if not utils.is_uuid_like(instance_uuid):
raise exception.InvalidUUID(instance_uuid)
- self.db.volume_attached(context,
+ self.db.volume_attached(context.elevated(),
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"
- self.db.volume_detached(context, volume_id)
+ # TODO(sleepsonthefloor): Is this 'elevated' appropriate?
+ self.db.volume_detached(context.elevated(), volume_id)
def initialize_connection(self, context, volume_id, connector):
"""Prepare volume for connection from host represented by connector.
"volume:get_all_snapshots": [],
"volume_extension:types_manage": [["rule:admin_api"]],
- "volume_extension:types_extra_specs": [["rule:admin_api"]]
+ "volume_extension:types_extra_specs": [["rule:admin_api"]],
+ "volume_extension:extended_snapshot_attributes": []
}
commands = /bin/bash run_tests.sh -N -P {posargs}
[testenv:pep8]
-deps = pep8
+deps = pep8==1.1
commands = pep8 --repeat --show-source cinder setup.py
[testenv:venv]