Add Share instances Admin API

Add admin API which allows:
- Get share instances of given share
- Get all share instances
- Get share instance details by id
- Reset share instance state
- Force-delete share instance

Add appropriate unit and tempest tests.

APIImpact
Change-Id: Ie96dfc18b491cb4d9705da4eaca5bc4ce43225ea
Partially-Implements: blueprint share-instances
This commit is contained in:
Igor Malinovskiy 2015-08-31 13:41:17 +03:00
parent b46f92c53a
commit 91e438b40e
17 changed files with 479 additions and 4 deletions

View File

@ -30,6 +30,9 @@ class AdminActionsTest(base.BaseSharesAdminTest):
cls.states = ["error", "available"]
cls.bad_status = "error_deleting"
cls.sh = cls.create_share()
cls.sh_instance = (
cls.shares_client.get_instances_of_share(cls.sh["id"])[0]
)
if CONF.share.run_snapshot_tests:
cls.sn = cls.create_snapshot_wait_for_active(cls.sh["id"])
@ -39,6 +42,14 @@ class AdminActionsTest(base.BaseSharesAdminTest):
self.shares_client.reset_state(self.sh["id"], status=status)
self.shares_client.wait_for_share_status(self.sh["id"], status)
@test.attr(type=["gate", ])
def test_reset_share_instance_state(self):
id = self.sh_instance["id"]
for status in self.states:
self.shares_client.reset_state(
id, s_type="share_instances", status=status)
self.shares_client.wait_for_share_instance_status(id, status)
@test.attr(type=["gate", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -63,6 +74,29 @@ class AdminActionsTest(base.BaseSharesAdminTest):
self.shares_client.force_delete(share["id"])
self.shares_client.wait_for_resource_deletion(share_id=share["id"])
@test.attr(type=["gate", ])
def test_force_delete_share_instance(self):
share = self.create_share(cleanup_in_class=False)
instances = self.shares_client.get_instances_of_share(share["id"])
# Check that instance was created
self.assertEqual(1, len(instances))
instance = instances[0]
# Change status from 'available' to 'error_deleting'
self.shares_client.reset_state(
instance["id"], s_type="share_instances", status=self.bad_status)
# Check that status was changed
check_status = self.shares_client.get_share_instance(instance["id"])
self.assertEqual(self.bad_status, check_status["status"])
# Share with status 'error_deleting' should be deleted
self.shares_client.force_delete(
instance["id"], s_type="share_instances")
self.shares_client.wait_for_resource_deletion(
share_instance_id=instance["id"])
@test.attr(type=["gate", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")

View File

@ -30,6 +30,9 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
def resource_setup(cls):
super(AdminActionsNegativeTest, cls).resource_setup()
cls.sh = cls.create_share()
cls.sh_instance = (
cls.shares_client.get_instances_of_share(cls.sh["id"])[0]
)
if CONF.share.run_snapshot_tests:
cls.sn = cls.create_snapshot_wait_for_active(cls.sh["id"])
cls.member_shares_client = clients.Manager().shares_client
@ -39,6 +42,11 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.assertRaises(lib_exc.NotFound,
self.shares_client.reset_state, "fake")
@test.attr(type=["gate", "negative", ])
def test_reset_nonexistent_share_instance_state(self):
self.assertRaises(lib_exc.NotFound, self.shares_client.reset_state,
"fake", s_type="share_instances")
@test.attr(type=["gate", "negative", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -52,6 +60,16 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.shares_client.reset_state,
self.sh["id"], status="fake")
@test.attr(type=["gate", "negative", ])
def test_reset_share_instance_state_to_unacceptable_state(self):
self.assertRaises(
lib_exc.BadRequest,
self.shares_client.reset_state,
self.sh_instance["id"],
s_type="share_instances",
status="fake"
)
@test.attr(type=["gate", "negative", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -67,6 +85,13 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.member_shares_client.reset_state,
self.sh["id"])
@test.attr(type=["gate", "negative", ])
def test_try_reset_share_instance_state_with_member(self):
# Even if member from another tenant, it should be unauthorized
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.reset_state,
self.sh_instance["id"], s_type="share_instances")
@test.attr(type=["gate", "negative", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -81,6 +106,13 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.assertRaises(lib_exc.NotFound,
self.shares_client.force_delete, "fake")
@test.attr(type=["gate", "negative", ])
def test_force_delete_nonexistent_share_instance(self):
self.assertRaises(lib_exc.NotFound,
self.shares_client.force_delete,
"fake",
s_type="share_instances")
@test.attr(type=["gate", "negative", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -97,6 +129,13 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.member_shares_client.force_delete,
self.sh["id"])
@test.attr(type=["gate", "negative", ])
def test_try_force_delete_share_instance_with_member(self):
# If a non-admin tries to do force_delete, it should be unauthorized
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.force_delete,
self.sh_instance["id"], s_type="share_instances")
@test.attr(type=["gate", "negative", ])
@testtools.skipUnless(CONF.share.run_snapshot_tests,
"Snapshot tests are disabled.")
@ -105,3 +144,24 @@ class AdminActionsNegativeTest(base.BaseSharesAdminTest):
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.force_delete,
self.sn["id"], s_type="snapshots")
@test.attr(type=["gate", "negative", ])
def test_try_get_share_instance_with_member(self):
# If a non-admin tries to get instance, it should be unauthorized
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.get_share_instance,
self.sh_instance["id"])
@test.attr(type=["gate", "negative", ])
def test_try_list_share_instance_with_member(self):
# If a non-admin tries to list instances, it should be unauthorized
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.list_share_instances)
@test.attr(type=["gate", "negative", ])
def test_try_get_instances_of_share_with_member(self):
# If a non-admin tries to list instances of given share, it should be
# unauthorized
self.assertRaises(lib_exc.Forbidden,
self.member_shares_client.get_instances_of_share,
self.sh['id'])

View File

@ -36,7 +36,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="1.3",
default="1.4",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",

View File

@ -48,6 +48,9 @@ class SharesClient(rest_client.RestClient):
self.build_timeout = CONF.share.build_timeout
self.API_MICROVERSIONS_HEADER = 'x-openstack-manila-api-version'
def _get_version_dict(self, version):
return {self.API_MICROVERSIONS_HEADER: version}
def send_microversion_request(self, version=None):
"""Prepare and send the HTTP GET Request to the base URL.
@ -144,6 +147,27 @@ class SharesClient(rest_client.RestClient):
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_instances_of_share(self, share_id):
resp, body = self.get("shares/%s/instances" % share_id,
headers=self._get_version_dict('1.4'),
extra_headers=True)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def list_share_instances(self):
resp, body = self.get("share_instances",
headers=self._get_version_dict('1.4'),
extra_headers=True)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def get_share_instance(self, instance_id):
resp, body = self.get("share_instances/%s" % instance_id,
headers=self._get_version_dict('1.4'),
extra_headers=True)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def create_access_rule(self, share_id, access_type="ip",
access_to="0.0.0.0", access_level=None):
post_body = {
@ -262,6 +286,28 @@ class SharesClient(rest_client.RestClient):
(share_name, status, self.build_timeout))
raise exceptions.TimeoutException(message)
def wait_for_share_instance_status(self, instance_id, status):
"""Waits for a share to reach a given status."""
body = self.get_share_instance(instance_id)
instance_status = body['status']
start = int(time.time())
while instance_status != status:
time.sleep(self.build_interval)
body = self.get_share(instance_id)
instance_status = body['status']
if instance_status == status:
return
elif 'error' in instance_status.lower():
raise share_exceptions.\
ShareInstanceBuildErrorException(id=instance_id)
if int(time.time()) - start >= self.build_timeout:
message = ('Share instance %s failed to reach %s status within'
' the required time (%s s).' %
(instance_id, status, self.build_timeout))
raise exceptions.TimeoutException(message)
def wait_for_snapshot_status(self, snapshot_id, status):
"""Waits for a snapshot to reach a given status."""
body = self.get_snapshot(snapshot_id)
@ -375,6 +421,9 @@ class SharesClient(rest_client.RestClient):
else:
return self._is_resource_deleted(
self.get_share, kwargs.get("share_id"))
elif "share_instance_id" in kwargs:
return self._is_resource_deleted(
self.get_share_instance, kwargs.get("share_instance_id"))
elif "snapshot_id" in kwargs:
return self._is_resource_deleted(
self.get_snapshot, kwargs.get("snapshot_id"))

View File

@ -20,6 +20,10 @@ class ShareBuildErrorException(exceptions.TempestException):
message = "Share %(share_id)s failed to build and is in ERROR status"
class ShareInstanceBuildErrorException(exceptions.TempestException):
message = "Share instance %(id)s failed to build and is in ERROR status"
class AccessRuleBuildErrorException(exceptions.TempestException):
message = "Share's rule with id %(rule_id)s is in ERROR status"

View File

@ -26,6 +26,9 @@
"share:delete_share_metadata": "rule:default",
"share:update_share_metadata": "rule:default",
"share_instance:index": "rule:admin_api",
"share_instance:show": "rule:admin_api",
"share_extension:quotas:show": "",
"share_extension:quotas:update": "rule:admin_api",
"share_extension:quotas:delete": "rule:admin_api",
@ -35,6 +38,8 @@
"share_extension:share_admin_actions:reset_status": "rule:admin_api",
"share_extension:snapshot_admin_actions:force_delete": "rule:admin_api",
"share_extension:snapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:share_instance_admin_actions:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "rule:admin_api",
"share_extension:services": "rule:admin_api",
"share_extension:availability_zones": "",

View File

@ -113,6 +113,21 @@ class ShareAdminController(AdminController):
return self.share_api.delete(*args, **kwargs)
class ShareInstancesAdminController(AdminController):
"""AdminController for Share instances."""
collection = 'share_instances'
def _get(self, *args, **kwargs):
return db.share_instance_get(*args, **kwargs)
def _update(self, *args, **kwargs):
db.share_instance_update(*args, **kwargs)
def _delete(self, *args, **kwargs):
return self.share_api.delete_instance(*args, **kwargs)
class SnapshotAdminController(AdminController):
"""AdminController for Snapshots."""
@ -133,11 +148,13 @@ class Admin_actions(extensions.ExtensionDescriptor):
name = "AdminActions"
alias = "os-admin-actions"
updated = "2012-08-25T00:00:00+00:00"
updated = "2015-08-03T00:00:00+00:00"
def get_controller_extensions(self):
exts = []
for class_ in (ShareAdminController, SnapshotAdminController):
controllers = (ShareAdminController, SnapshotAdminController,
ShareInstancesAdminController)
for class_ in controllers:
controller = class_()
extension = extensions.ControllerExtension(
self, class_.collection, controller)

View File

@ -48,6 +48,7 @@ REST_API_VERSION_HISTORY = """
* 1.1 - Versions API updated to reflect beginning of microversions epoch.
* 1.2 - Share create() doesn't ignore availability_zone field of share.
* 1.3 - Snapshots become optional feature.
* 1.4 - Share instances admin API
"""
@ -55,7 +56,7 @@ REST_API_VERSION_HISTORY = """
# The default api version request is defined to be the
# the minimum version of the API supported.
_MIN_API_VERSION = "1.0"
_MAX_API_VERSION = "1.3"
_MAX_API_VERSION = "1.4"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -40,3 +40,7 @@ user documentation.
---
Snapshots become optional and share payload now has
boolean attr 'snapshot_support'.
1.4
---
Share instances admin API and update of Admin Actions extension.

View File

@ -26,6 +26,7 @@ import manila.api.openstack
from manila.api.v1 import limits
from manila.api.v1 import scheduler_stats
from manila.api.v1 import security_service
from manila.api.v1 import share_instances
from manila.api.v1 import share_metadata
from manila.api.v1 import share_networks
from manila.api.v1 import share_servers
@ -59,6 +60,18 @@ class APIRouter(manila.api.openstack.APIRouter):
collection={'detail': 'GET'},
member={'action': 'POST'})
self.resources['share_instances'] = share_instances.create_resource()
mapper.resource("share_instance", "share_instances",
controller=self.resources['share_instances'],
collection={'detail': 'GET'},
member={'action': 'POST'})
mapper.connect("share_instance",
"/{project_id}/shares/{share_id}/instances",
controller=self.resources['share_instances'],
action='get_share_instances',
conditions={"method": ['GET']})
self.resources['snapshots'] = share_snapshots.create_resource()
mapper.resource("snapshot", "snapshots",
controller=self.resources['snapshots'],

View File

@ -0,0 +1,77 @@
# Copyright 2015 Mirantis 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 webob import exc
from manila.api.openstack import wsgi
from manila.api.views import share_instance as instance_view
from manila import db
from manila import exception
from manila import policy
from manila import share
class ShareInstancesController(wsgi.Controller):
"""The share instances API controller for the OpenStack API."""
resource_name = 'share_instance'
_view_builder_class = instance_view.ViewBuilder
def __init__(self):
self.share_api = share.API()
super(ShareInstancesController, self).__init__()
def _authorize(self, context, action):
try:
policy.check_policy(context, self.resource_name, action)
except exception.PolicyNotAuthorized:
raise exc.HTTPForbidden()
@wsgi.Controller.api_version("1.4")
def index(self, req):
context = req.environ['manila.context']
self._authorize(context, 'index')
instances = db.share_instances_get_all(context)
return self._view_builder.detail_list(req, instances)
@wsgi.Controller.api_version("1.4")
def show(self, req, id):
context = req.environ['manila.context']
self._authorize(context, 'show')
try:
instance = db.share_instance_get(context, id)
except exception.NotFound:
raise exc.HTTPNotFound()
return self._view_builder.detail(req, instance)
@wsgi.Controller.api_version("1.4")
def get_share_instances(self, req, share_id):
context = req.environ['manila.context']
self._authorize(context, 'index')
try:
share = self.share_api.get(context, share_id)
except exception.NotFound:
raise exc.HTTPNotFound()
view = instance_view.ViewBuilder()
return view.detail_list(req, share.instances)
def create_resource():
return wsgi.Resource(ShareInstancesController())

View File

@ -0,0 +1,56 @@
# 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 manila.api import common
class ViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
_collection_name = 'share_instances'
def detail_list(self, request, instances):
"""Detailed view of a list of share instances."""
return self._list_view(self.detail, request, instances)
def detail(self, request, share_instance):
"""Detailed view of a single share instance."""
export_locations = [e['path'] for e in share_instance.export_locations]
instance_dict = {
'id': share_instance.get('id'),
'share_id': share_instance.get('share_id'),
'availability_zone': share_instance.get('availability_zone'),
'created_at': share_instance.get('created_at'),
'host': share_instance.get('host'),
'status': share_instance.get('status'),
'share_network_id': share_instance.get('share_network_id'),
'share_server_id': share_instance.get('share_server_id'),
'export_location': share_instance.get('export_location'),
'export_locations': export_locations,
}
return {'share_instance': instance_dict}
def _list_view(self, func, request, instances):
"""Provide a view for a list of share instances."""
instances_list = [func(request, instance)['share_instance']
for instance in instances]
instances_links = self._get_collection_links(request,
instances,
self._collection_name)
instances_dict = {self._collection_name: instances_list}
if instances_links:
instances_dict[self._collection_name] = instances_links
return instances_dict

View File

@ -303,6 +303,11 @@ def share_instance_update(context, instance_id, values, with_share_data=False):
with_share_data=with_share_data)
def share_instances_get_all(context):
"""Returns all share instances."""
return IMPL.share_instances_get_all(context)
def share_instances_get_all_by_share_server(context, share_server_id):
"""Returns all share instances with given share_server_id."""
return IMPL.share_instances_get_all_by_share_server(context,

View File

@ -1184,6 +1184,15 @@ def share_instance_get(context, share_instance_id, session=None,
return result
@require_admin_context
def share_instances_get_all(context):
session = get_session()
return (
model_query(context, models.ShareInstance, session=session,
read_deleted="no").all()
)
@require_context
def share_instance_delete(context, instance_id, session=None):
if session is None:

View File

@ -87,6 +87,14 @@ class AdminActionsTest(test.TestCase):
snapshot['id'])
return snapshot, req
def _setup_share_instance_data(self, instance=None):
if instance is None:
instance = db_utils.create_share(status=constants.STATUS_AVAILABLE,
size='1').instance
req = webob.Request.blank(
'/v1/fake/share_instances/%s/action' % instance['id'])
return instance, req
def _reset_status(self, ctxt, model, req, db_access_method,
valid_code, valid_status=None, body=None):
if body is None:
@ -130,6 +138,17 @@ class AdminActionsTest(test.TestCase):
self._reset_status(ctxt, snapshot, req, db.share_snapshot_get,
valid_code, valid_status)
@ddt.data(*fixture_reset_status_with_different_roles)
@ddt.unpack
def test_share_instances_reset_status_with_different_roles(self, role,
valid_code,
valid_status):
ctxt = self._get_context(role)
instance, req = self._setup_share_instance_data()
self._reset_status(ctxt, instance, req, db.share_instance_get,
valid_code, valid_status)
@ddt.data(*fixture_invalid_reset_status_body)
def test_share_invalid_reset_status_body(self, body):
share, req = self._setup_share_data()
@ -146,6 +165,14 @@ class AdminActionsTest(test.TestCase):
db.share_snapshot_get, 400,
constants.STATUS_AVAILABLE, body)
@ddt.data(*fixture_invalid_reset_status_body)
def test_share_instance_invalid_reset_status_body(self, body):
instance, req = self._setup_share_instance_data()
self._reset_status(self.admin_context, instance, req,
db.share_instance_get, 400,
constants.STATUS_AVAILABLE, body)
def test_share_reset_status_for_missing(self):
fake_share = {'id': 'missing-share-id'}
req = webob.Request.blank('/v1/fake/shares/%s/action' %
@ -201,3 +228,19 @@ class AdminActionsTest(test.TestCase):
ctxt = self._get_context('admin')
self._force_delete(ctxt, snapshot, req, db.share_snapshot_get, 404)
@ddt.data(*fixture_force_delete_with_different_roles)
@ddt.unpack
def test_instance_force_delete_with_different_roles(self, role, resp_code):
instance, req = self._setup_share_instance_data()
ctxt = self._get_context(role)
self._force_delete(ctxt, instance, req, db.share_instance_get,
resp_code)
def test_instance_force_delete_missing(self):
instance, req = self._setup_share_instance_data(
instance={'id': 'fake'})
ctxt = self._get_context('admin')
self._force_delete(ctxt, instance, req, db.share_instance_get, 404)

View File

@ -0,0 +1,93 @@
# 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 ddt
from oslo_config import cfg
from webob import exc as webob_exc
from manila.api.v1 import share_instances
from manila import context
from manila import test
from manila.tests.api import fakes
from manila.tests import db_utils
CONF = cfg.CONF
@ddt.ddt
class ShareInstancesApiTest(test.TestCase):
"""Share Api Test."""
def setUp(self):
super(ShareInstancesApiTest, self).setUp()
self.controller = share_instances.ShareInstancesController()
self.context = context.RequestContext('admin', 'fake', True)
def _get_request(self, uri, context=None):
if context is None:
context = self.context
req = fakes.HTTPRequest.blank('/shares', version="1.4")
req.environ['manila.context'] = context
return req
def _validate_ids_in_share_instances_list(self, expected, actual):
self.assertEqual(len(expected), len(actual))
self.assertEqual([i['id'] for i in expected],
[i['id'] for i in actual])
def test_index(self):
req = self._get_request('/share_instances')
share_instances_count = 3
test_instances = [
db_utils.create_share(size=s + 1).instance
for s in range(0, share_instances_count)
]
actual_result = self.controller.index(req)
self._validate_ids_in_share_instances_list(
test_instances, actual_result['share_instances'])
def test_show(self):
test_instance = db_utils.create_share(size=1).instance
id = test_instance['id']
actual_result = self.controller.show(self._get_request('fake'), id)
self.assertEqual(id, actual_result['share_instance']['id'])
def test_get_share_instances(self):
test_share = db_utils.create_share(size=1)
id = test_share['id']
req = self._get_request('fake')
actual_result = self.controller.get_share_instances(req, id)
self._validate_ids_in_share_instances_list(
[test_share.instance],
actual_result['share_instances']
)
@ddt.data('show', 'get_share_instances')
def test_not_found(self, target_method_name):
method = getattr(self.controller, target_method_name)
self.assertRaises(webob_exc.HTTPNotFound, method,
self._get_request('fake'), 'fake')
@ddt.data(('show', 2), ('get_share_instances', 2), ('index', 1))
@ddt.unpack
def test_access(self, target_method_name, args_count):
user_context = context.RequestContext('fake', 'fake')
req = self._get_request('fake', user_context)
target_method = getattr(self.controller, target_method_name)
args = [i for i in range(1, args_count)]
self.assertRaises(webob_exc.HTTPForbidden, target_method, req, *args)

View File

@ -18,6 +18,9 @@
"share:extend": "",
"share:shrink": "",
"share_instance:index": "rule:admin_api",
"share_instance:show": "rule:admin_api",
"share_network:create": "",
"share_network:index": "",
"share_network:detail": "",
@ -38,6 +41,8 @@
"share_extension:share_admin_actions:reset_status": "rule:admin_api",
"share_extension:snapshot_admin_actions:force_delete": "rule:admin_api",
"share_extension:snapshot_admin_actions:reset_status": "rule:admin_api",
"share_extension:share_instance_admin_actions:force_delete": "rule:admin_api",
"share_extension:share_instance_admin_actions:reset_status": "rule:admin_api",
"share_extension:types_manage": "",
"share_extension:types_extra_specs": "",
"share_extension:share_type_access": "",