Merge "Support RBAC security groups in dashboard"
This commit is contained in:
commit
12bb9fe518
@ -20,6 +20,7 @@
|
|||||||
import collections
|
import collections
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
import copy
|
import copy
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
@ -329,6 +330,7 @@ class SecurityGroupManager(object):
|
|||||||
* name
|
* name
|
||||||
* description
|
* description
|
||||||
* tenant_id
|
* tenant_id
|
||||||
|
* shared: A boolean indicates whether this security group is shared
|
||||||
* rules: A list of SecurityGroupRule objects
|
* rules: A list of SecurityGroupRule objects
|
||||||
|
|
||||||
SecurityGroupRule object should have the following attributes
|
SecurityGroupRule object should have the following attributes
|
||||||
@ -354,6 +356,23 @@ class SecurityGroupManager(object):
|
|||||||
self.client = neutronclient(request)
|
self.client = neutronclient(request)
|
||||||
|
|
||||||
def _list(self, **filters):
|
def _list(self, **filters):
|
||||||
|
if (filters.get("tenant_id") and
|
||||||
|
is_extension_supported(
|
||||||
|
self.request, 'security-groups-shared-filtering')):
|
||||||
|
# NOTE(hangyang): First, we get the SGs owned by but not shared
|
||||||
|
# to the requester(tenant_id)
|
||||||
|
filters["shared"] = False
|
||||||
|
secgroups_owned = self.client.list_security_groups(**filters)
|
||||||
|
# NOTE(hangyang): Second, we get the SGs shared to the
|
||||||
|
# requester. For a requester with an admin role, this second
|
||||||
|
# API call also only returns SGs shared to the requester's tenant
|
||||||
|
# instead of all the SGs shared to any tenant.
|
||||||
|
filters.pop("tenant_id")
|
||||||
|
filters["shared"] = True
|
||||||
|
secgroups_rbac = self.client.list_security_groups(**filters)
|
||||||
|
return [SecurityGroup(sg) for sg in
|
||||||
|
itertools.chain(secgroups_owned.get('security_groups'),
|
||||||
|
secgroups_rbac.get('security_groups'))]
|
||||||
secgroups = self.client.list_security_groups(**filters)
|
secgroups = self.client.list_security_groups(**filters)
|
||||||
return [SecurityGroup(sg) for sg in secgroups.get('security_groups')]
|
return [SecurityGroup(sg) for sg in secgroups.get('security_groups')]
|
||||||
|
|
||||||
|
@ -115,6 +115,20 @@ class SecurityGroupsTable(tables.DataTable):
|
|||||||
security_group_id = tables.Column("id",
|
security_group_id = tables.Column("id",
|
||||||
verbose_name=_("Security Group ID"))
|
verbose_name=_("Security Group ID"))
|
||||||
description = tables.Column("description", verbose_name=_("Description"))
|
description = tables.Column("description", verbose_name=_("Description"))
|
||||||
|
shared = tables.Column("shared", verbose_name=_("Shared"))
|
||||||
|
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
try:
|
||||||
|
is_shared_supported = api.neutron.is_extension_supported(
|
||||||
|
self.request, 'security-groups-shared-filtering')
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(
|
||||||
|
self.request,
|
||||||
|
_('Failed to check if shared field is supported.'))
|
||||||
|
is_shared_supported = False
|
||||||
|
if not is_shared_supported:
|
||||||
|
del self.columns['shared']
|
||||||
|
|
||||||
def sanitize_id(self, obj_id):
|
def sanitize_id(self, obj_id):
|
||||||
return filters.get_int_or_uuid(obj_id)
|
return filters.get_int_or_uuid(obj_id)
|
||||||
|
@ -60,7 +60,8 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
self.edit_url = reverse(SG_ADD_RULE_VIEW, args=[sec_group.id])
|
self.edit_url = reverse(SG_ADD_RULE_VIEW, args=[sec_group.id])
|
||||||
self.update_url = reverse(SG_UPDATE_VIEW, args=[sec_group.id])
|
self.update_url = reverse(SG_UPDATE_VIEW, args=[sec_group.id])
|
||||||
|
|
||||||
@test.create_mocks({api.neutron: ('security_group_list',),
|
@test.create_mocks({api.neutron: ('security_group_list',
|
||||||
|
'is_extension_supported'),
|
||||||
quotas: ('tenant_quota_usages',)})
|
quotas: ('tenant_quota_usages',)})
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
sec_groups = self.security_groups.list()
|
sec_groups = self.security_groups.list()
|
||||||
@ -68,6 +69,7 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
quota_data['security_group']['available'] = 10
|
quota_data['security_group']['available'] = 10
|
||||||
|
|
||||||
self.mock_security_group_list.return_value = sec_groups
|
self.mock_security_group_list.return_value = sec_groups
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
self.mock_tenant_quota_usages.return_value = quota_data
|
self.mock_tenant_quota_usages.return_value = quota_data
|
||||||
|
|
||||||
res = self.client.get(INDEX_URL)
|
res = self.client.get(INDEX_URL)
|
||||||
@ -93,7 +95,8 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
self.mock_tenant_quota_usages, 2,
|
self.mock_tenant_quota_usages, 2,
|
||||||
mock.call(test.IsHttpRequest(), targets=('security_group', )))
|
mock.call(test.IsHttpRequest(), targets=('security_group', )))
|
||||||
|
|
||||||
@test.create_mocks({api.neutron: ('security_group_list',),
|
@test.create_mocks({api.neutron: ('security_group_list',
|
||||||
|
'is_extension_supported'),
|
||||||
quotas: ('tenant_quota_usages',)})
|
quotas: ('tenant_quota_usages',)})
|
||||||
def test_create_button_attributes(self):
|
def test_create_button_attributes(self):
|
||||||
sec_groups = self.security_groups.list()
|
sec_groups = self.security_groups.list()
|
||||||
@ -101,6 +104,7 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
quota_data['security_group']['available'] = 10
|
quota_data['security_group']['available'] = 10
|
||||||
|
|
||||||
self.mock_security_group_list.return_value = sec_groups
|
self.mock_security_group_list.return_value = sec_groups
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
self.mock_tenant_quota_usages.return_value = quota_data
|
self.mock_tenant_quota_usages.return_value = quota_data
|
||||||
|
|
||||||
res = self.client.get(INDEX_URL)
|
res = self.client.get(INDEX_URL)
|
||||||
@ -153,10 +157,14 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
self.mock_tenant_quota_usages, 3,
|
self.mock_tenant_quota_usages, 3,
|
||||||
mock.call(test.IsHttpRequest(), targets=('security_group', )))
|
mock.call(test.IsHttpRequest(), targets=('security_group', )))
|
||||||
|
|
||||||
|
@test.create_mocks({api.neutron: ('is_extension_supported',)})
|
||||||
def test_create_button_disabled_when_quota_exceeded_neutron_disabled(self):
|
def test_create_button_disabled_when_quota_exceeded_neutron_disabled(self):
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
self._test_create_button_disabled_when_quota_exceeded(False)
|
self._test_create_button_disabled_when_quota_exceeded(False)
|
||||||
|
|
||||||
|
@test.create_mocks({api.neutron: ('is_extension_supported',)})
|
||||||
def test_create_button_disabled_when_quota_exceeded_neutron_enabled(self):
|
def test_create_button_disabled_when_quota_exceeded_neutron_enabled(self):
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
self._test_create_button_disabled_when_quota_exceeded(True)
|
self._test_create_button_disabled_when_quota_exceeded(True)
|
||||||
|
|
||||||
def _add_security_group_rule_fixture(self, is_desc_support=True, **kwargs):
|
def _add_security_group_rule_fixture(self, is_desc_support=True, **kwargs):
|
||||||
@ -806,10 +814,12 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
self.mock_is_extension_supported.assert_called_once_with(
|
self.mock_is_extension_supported.assert_called_once_with(
|
||||||
test.IsHttpRequest(), 'standard-attr-description')
|
test.IsHttpRequest(), 'standard-attr-description')
|
||||||
|
|
||||||
@test.create_mocks({api.neutron: ('security_group_delete',)})
|
@test.create_mocks({api.neutron: ('security_group_delete',
|
||||||
|
'is_extension_supported')})
|
||||||
def test_delete_group(self):
|
def test_delete_group(self):
|
||||||
sec_group = self.security_groups.get(name="other_group")
|
sec_group = self.security_groups.get(name="other_group")
|
||||||
self.mock_security_group_delete.return_value = None
|
self.mock_security_group_delete.return_value = None
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
|
|
||||||
form_data = {"action": "security_groups__delete__%s" % sec_group.id}
|
form_data = {"action": "security_groups__delete__%s" % sec_group.id}
|
||||||
req = self.factory.post(INDEX_URL, form_data)
|
req = self.factory.post(INDEX_URL, form_data)
|
||||||
@ -820,10 +830,12 @@ class SecurityGroupsViewTests(test.TestCase):
|
|||||||
self.mock_security_group_delete.assert_called_once_with(
|
self.mock_security_group_delete.assert_called_once_with(
|
||||||
test.IsHttpRequest(), sec_group.id)
|
test.IsHttpRequest(), sec_group.id)
|
||||||
|
|
||||||
@test.create_mocks({api.neutron: ('security_group_delete',)})
|
@test.create_mocks({api.neutron: ('security_group_delete',
|
||||||
|
'is_extension_supported')})
|
||||||
def test_delete_group_exception(self):
|
def test_delete_group_exception(self):
|
||||||
sec_group = self.security_groups.get(name="other_group")
|
sec_group = self.security_groups.get(name="other_group")
|
||||||
self.mock_security_group_delete.side_effect = self.exceptions.nova
|
self.mock_security_group_delete.side_effect = self.exceptions.nova
|
||||||
|
self.mock_is_extension_supported.return_value = True
|
||||||
|
|
||||||
form_data = {"action": "security_groups__delete__%s" % sec_group.id}
|
form_data = {"action": "security_groups__delete__%s" % sec_group.id}
|
||||||
req = self.factory.post(INDEX_URL, form_data)
|
req = self.factory.post(INDEX_URL, form_data)
|
||||||
|
@ -487,21 +487,30 @@ def data(TEST):
|
|||||||
# Security group.
|
# Security group.
|
||||||
|
|
||||||
sec_group_1 = {'tenant_id': '1',
|
sec_group_1 = {'tenant_id': '1',
|
||||||
|
'shared': False,
|
||||||
'description': 'default',
|
'description': 'default',
|
||||||
'id': 'faad7c80-3b62-4440-967c-13808c37131d',
|
'id': 'faad7c80-3b62-4440-967c-13808c37131d',
|
||||||
'name': 'default'}
|
'name': 'default'}
|
||||||
sec_group_2 = {'tenant_id': '1',
|
sec_group_2 = {'tenant_id': '1',
|
||||||
|
'shared': False,
|
||||||
'description': 'NotDefault',
|
'description': 'NotDefault',
|
||||||
'id': '27a5c9a1-bdbb-48ac-833a-2e4b5f54b31d',
|
'id': '27a5c9a1-bdbb-48ac-833a-2e4b5f54b31d',
|
||||||
'name': 'other_group'}
|
'name': 'other_group'}
|
||||||
sec_group_3 = {'tenant_id': '1',
|
sec_group_3 = {'tenant_id': '1',
|
||||||
|
'shared': False,
|
||||||
'description': 'NotDefault',
|
'description': 'NotDefault',
|
||||||
'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95',
|
'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95',
|
||||||
'name': 'another_group'}
|
'name': 'another_group'}
|
||||||
sec_group_empty = {'tenant_id': '1',
|
sec_group_empty = {'tenant_id': '1',
|
||||||
|
'shared': False,
|
||||||
'description': 'SG without rules',
|
'description': 'SG without rules',
|
||||||
'id': 'f205f3bc-d402-4e40-b004-c62401e19b4b',
|
'id': 'f205f3bc-d402-4e40-b004-c62401e19b4b',
|
||||||
'name': 'empty_group'}
|
'name': 'empty_group'}
|
||||||
|
sec_group_shared = {'tenant_id': '1',
|
||||||
|
'shared': True,
|
||||||
|
'description': 'SG without rules',
|
||||||
|
'id': 'cca53e02-114e-4da3-917b-f19efa7cbc47',
|
||||||
|
'name': 'shared_group'}
|
||||||
|
|
||||||
def add_rule_to_group(secgroup, default_only=True):
|
def add_rule_to_group(secgroup, default_only=True):
|
||||||
rule_egress_ipv4 = {
|
rule_egress_ipv4 = {
|
||||||
@ -585,10 +594,11 @@ def data(TEST):
|
|||||||
add_rule_to_group(sec_group_1, default_only=False)
|
add_rule_to_group(sec_group_1, default_only=False)
|
||||||
add_rule_to_group(sec_group_2)
|
add_rule_to_group(sec_group_2)
|
||||||
add_rule_to_group(sec_group_3)
|
add_rule_to_group(sec_group_3)
|
||||||
# NOTE: sec_group_empty is a SG without rules,
|
# NOTE: sec_group_empty and sec_group_shared are SGs without rules,
|
||||||
# so we don't call add_rule_to_group.
|
# so we don't call add_rule_to_group.
|
||||||
|
|
||||||
groups = [sec_group_1, sec_group_2, sec_group_3, sec_group_empty]
|
groups = [sec_group_1, sec_group_2, sec_group_3,
|
||||||
|
sec_group_empty, sec_group_shared]
|
||||||
sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups])
|
sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups])
|
||||||
for sg in groups:
|
for sg in groups:
|
||||||
# Neutron API.
|
# Neutron API.
|
||||||
@ -684,12 +694,17 @@ def data(TEST):
|
|||||||
extension_6 = {"name": "Trunks",
|
extension_6 = {"name": "Trunks",
|
||||||
"alias": "trunk",
|
"alias": "trunk",
|
||||||
"description": "Provides support for trunk ports."}
|
"description": "Provides support for trunk ports."}
|
||||||
|
extension_7 = {"name": "Security group filtering on the shared field",
|
||||||
|
"alias": "security-groups-shared-filtering",
|
||||||
|
"description": "Support filtering security groups on "
|
||||||
|
"the shared field"}
|
||||||
TEST.api_extensions.add(extension_1)
|
TEST.api_extensions.add(extension_1)
|
||||||
TEST.api_extensions.add(extension_2)
|
TEST.api_extensions.add(extension_2)
|
||||||
TEST.api_extensions.add(extension_3)
|
TEST.api_extensions.add(extension_3)
|
||||||
TEST.api_extensions.add(extension_4)
|
TEST.api_extensions.add(extension_4)
|
||||||
TEST.api_extensions.add(extension_5)
|
TEST.api_extensions.add(extension_5)
|
||||||
TEST.api_extensions.add(extension_6)
|
TEST.api_extensions.add(extension_6)
|
||||||
|
TEST.api_extensions.add(extension_7)
|
||||||
|
|
||||||
# 1st agent.
|
# 1st agent.
|
||||||
agent_dict = {"binary": "neutron-openvswitch-agent",
|
agent_dict = {"binary": "neutron-openvswitch-agent",
|
||||||
|
@ -1162,24 +1162,51 @@ class NeutronApiSecurityGroupTests(test.APIMockTestCase):
|
|||||||
for (exprule, retrule) in zip(exp_rules, ret_sg.rules):
|
for (exprule, retrule) in zip(exp_rules, ret_sg.rules):
|
||||||
self._cmp_sg_rule(exprule, retrule)
|
self._cmp_sg_rule(exprule, retrule)
|
||||||
|
|
||||||
def _test_security_group_list(self, **params):
|
@mock.patch.object(api.neutron, 'is_extension_supported')
|
||||||
|
def _test_security_group_list(self, mock_is_extension_supported,
|
||||||
|
is_ext_supported=True, **params):
|
||||||
sgs = self.api_security_groups.list()
|
sgs = self.api_security_groups.list()
|
||||||
q_params = {'tenant_id': self.request.user.tenant_id}
|
mock_is_extension_supported.return_value = is_ext_supported
|
||||||
# if tenant_id is specified, the passed tenant_id should be sent.
|
if is_ext_supported:
|
||||||
q_params.update(params)
|
# First call to get the tenant owned SGs
|
||||||
# use deepcopy to ensure self.api_security_groups is not modified.
|
q_params_1 = {'tenant_id': self.request.user.tenant_id,
|
||||||
self.qclient.list_security_groups.return_value = {'security_groups':
|
'shared': False}
|
||||||
copy.deepcopy(sgs)}
|
# if tenant_id is specified, the passed tenant_id should be sent.
|
||||||
|
q_params_1.update(params)
|
||||||
|
# Second call to get shared SGs
|
||||||
|
q_params_2 = q_params_1.copy()
|
||||||
|
q_params_2.pop('tenant_id')
|
||||||
|
q_params_2['shared'] = True
|
||||||
|
# use deepcopy to ensure self.api_security_groups is not modified.
|
||||||
|
self.qclient.list_security_groups.side_effect = [
|
||||||
|
{'security_groups': copy.deepcopy(sgs[:4])},
|
||||||
|
{'security_groups': copy.deepcopy(sgs[-1:])},
|
||||||
|
]
|
||||||
|
rets = api.neutron.security_group_list(self.request, **params)
|
||||||
|
self.qclient.list_security_groups.assert_has_calls(
|
||||||
|
[mock.call(**q_params_1), mock.call(**q_params_2)])
|
||||||
|
else:
|
||||||
|
q_params = {'tenant_id': self.request.user.tenant_id}
|
||||||
|
# if tenant_id is specified, the passed tenant_id should be sent.
|
||||||
|
q_params.update(params)
|
||||||
|
# use deepcopy to ensure self.api_security_groups is not modified.
|
||||||
|
self.qclient.list_security_groups.return_value = {
|
||||||
|
'security_groups': copy.deepcopy(sgs)}
|
||||||
|
rets = api.neutron.security_group_list(self.request, **params)
|
||||||
|
|
||||||
rets = api.neutron.security_group_list(self.request, **params)
|
mock_is_extension_supported.assert_called_once_with(
|
||||||
|
self.request, 'security-groups-shared-filtering')
|
||||||
self.assertEqual(len(sgs), len(rets))
|
self.assertEqual(len(sgs), len(rets))
|
||||||
for (exp, ret) in zip(sgs, rets):
|
for (exp, ret) in zip(sgs, rets):
|
||||||
self._cmp_sg(exp, ret)
|
self._cmp_sg(exp, ret)
|
||||||
self.qclient.list_security_groups.assert_called_once_with(**q_params)
|
|
||||||
|
|
||||||
def test_security_group_list(self):
|
def test_security_group_list(self):
|
||||||
self._test_security_group_list()
|
self._test_security_group_list()
|
||||||
|
|
||||||
|
def test_security_group_list_no_shared(self):
|
||||||
|
# without the api extension to filter by the shared field
|
||||||
|
self._test_security_group_list(is_ext_supported=False)
|
||||||
|
|
||||||
def test_security_group_list_with_params(self):
|
def test_security_group_list_with_params(self):
|
||||||
self._test_security_group_list(name='sg1')
|
self._test_security_group_list(name='sg1')
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
[:bug:`1907843`] RBAC shared security groups can now be shown in the
|
||||||
|
Security Groups page. Previously only the security groups owned by the
|
||||||
|
login tenant can be displayed and used. Besides, a column for the shared
|
||||||
|
field is added to the Security Groups table.
|
Loading…
Reference in New Issue
Block a user