Disable 'Create Port' button if ports quota is exceeded

Ports has quota management in a project, if quota is exceeded,
will create failure and API returns
"Recoverable error: Quota exceeded for resources: ['port']".
So, it should be like creating a network and subnet to
perform quota checks on ports, and if the quota is exceeded,
add text descriptions to the create port button and disable it.

Change-Id: I31bd8f93c312179b86115544ba0fadc9a9ffec63
Closes-Bug:#1712556
This commit is contained in:
wei.ying 2017-08-28 14:01:41 +08:00
parent 233680f541
commit 0b65dbc913
5 changed files with 123 additions and 0 deletions

View File

@ -12,14 +12,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.networks.ports import \
tables as project_tables
from openstack_dashboard.dashboards.project.networks.ports.tabs \
import PortsTab as project_port_tab
from openstack_dashboard.usage import quotas
class DeletePort(project_tables.DeletePort):
@ -29,6 +34,21 @@ class DeletePort(project_tables.DeletePort):
class CreatePort(project_tables.CreatePort):
url = "horizon:admin:networks:addport"
def allowed(self, request, datum=None):
network = self.table._get_network()
tenant_id = network.tenant_id
usages = quotas.tenant_quota_usages(
request, tenant_id=tenant_id, targets=('ports', ))
if usages.get('ports', {}).get('available', 1) <= 0:
if "disabled" not in self.classes:
self.classes = [c for c in self.classes] + ["disabled"]
self.verbose_name = _("Create Port (Quota exceeded)")
else:
self.verbose_name = _("Create Port")
self.classes = [c for c in self.classes if c != "disabled"]
return True
class UpdatePort(project_tables.UpdatePort):
url = "horizon:admin:networks:editport"
@ -38,6 +58,19 @@ class PortsTable(project_tables.PortsTable):
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
link="horizon:admin:networks:ports:detail")
failure_url = reverse_lazy('horizon:admin:networks:index')
@memoized.memoized_method
def _get_network(self):
try:
network_id = self.kwargs['network_id']
network = api.neutron.network_get(self.request, network_id)
network.set_id_as_name_if_empty(length=0)
except Exception:
msg = _('Unable to retrieve details for network "%s".') \
% (network_id)
exceptions.handle(self.request, msg, redirect=self.failure_url)
return network
class Meta(object):
name = "ports"

View File

@ -25,6 +25,7 @@ from horizon import tables
from openstack_dashboard import api
from openstack_dashboard import policy
from openstack_dashboard.usage import quotas
LOG = logging.getLogger(__name__)
@ -82,6 +83,23 @@ class CreatePort(tables.LinkAction):
network_id = self.table.kwargs['network_id']
return reverse(self.url, args=(network_id,))
def allowed(self, request, datum=None):
usages = quotas.tenant_quota_usages(request, targets=('ports', ))
# when Settings.OPENSTACK_NEUTRON_NETWORK['enable_quotas'] = False
# usages["ports"] is empty
if usages.get('ports', {}).get('available', 1) <= 0:
if "disabled" not in self.classes:
self.classes = [c for c in self.classes] + ["disabled"]
self.verbose_name = _("Create Port (Quota exceeded)")
else:
# If the port is deleted, the usage of port will less than
# the quota again, so we need to redefine the status of the
# button.
self.verbose_name = _("Create Port")
self.classes = [c for c in self.classes if c != "disabled"]
return True
class DeletePort(policy.PolicyTargetMixin, tables.DeleteAction):
@staticmethod

View File

@ -1162,3 +1162,66 @@ class NetworkViewTests(test.TestCase, NetworkStubMixin):
six.text_type(create_action.verbose_name))
self.assertEqual((('network', 'create_subnet'),),
create_action.policy_rules)
@test.create_stubs({api.neutron: ('network_get',
'port_list',
'is_extension_supported',),
quotas: ('tenant_quota_usages',)})
def _test_port_create_button(self, quota_data):
network_id = self.networks.first().id
api.neutron.network_get(
IsA(http.HttpRequest), network_id) \
.MultipleTimes().AndReturn(self.networks.first())
api.neutron.port_list(
IsA(http.HttpRequest), network_id=network_id) \
.AndReturn(self.ports.list())
api.neutron.is_extension_supported(
IsA(http.HttpRequest), 'mac-learning') \
.AndReturn(False)
quotas.tenant_quota_usages(
IsA(http.HttpRequest), targets=('subnets', )) \
.MultipleTimes().AndReturn(quota_data)
quotas.tenant_quota_usages(
IsA(http.HttpRequest), targets=('ports',)) \
.MultipleTimes().AndReturn(quota_data)
self.mox.ReplayAll()
url = urlunquote(reverse('horizon:project:networks:ports_tab',
args=[network_id]))
res = self.client.get(url)
self.assertTemplateUsed(res, 'horizon/common/_detail.html')
ports = res.context['ports_table'].data
self.assertItemsEqual(ports, self.ports.list())
return self.getAndAssertTableAction(res, 'ports', 'create')
def test_port_create_button_disabled_when_quota_exceeded(self):
quota_data = self.neutron_quota_usages.first()
quota_data['ports']['available'] = 0
create_action = self._test_port_create_button(quota_data)
self.assertIn('disabled', create_action.classes,
'The create button should be disabled')
def test_port_create_button_enabled_when_quota_disabled(self):
# In case of enable_quotas False, neutron related items
# are not set in a response from tenant_quota_usages.
quota_data = {}
create_action = self._test_port_create_button(quota_data)
self.assertNotIn('disabled', create_action.classes,
'The create button should be enabled')
def test_create_port_button_attributes(self):
quota_data = self.neutron_quota_usages.first()
quota_data['ports']['available'] = 1
create_action = self._test_port_create_button(quota_data)
self.assertEqual(set(['ajax-modal']), set(create_action.classes))
self.assertEqual('horizon:project:networks:addport',
create_action.url)
self.assertEqual('Create Port',
six.text_type(create_action.verbose_name))
self.assertEqual((('network', 'create_port'),),
create_action.policy_rules)

View File

@ -572,6 +572,7 @@ def data(TEST):
# Quota Usages
quota_usage_data = {'networks': {'used': 0, 'quota': 5},
'subnets': {'used': 0, 'quota': 5},
'ports': {'used': 0, 'quota': 5},
'routers': {'used': 0, 'quota': 5},
}
quota_usage = usage_quotas.QuotaUsage()

View File

@ -229,6 +229,10 @@ def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None):
net_quota = neutron_quotas.get('subnet').limit
qs.add(base.QuotaSet({'subnets': net_quota}))
if 'port' not in disabled_quotas:
net_quota = neutron_quotas.get('port').limit
qs.add(base.QuotaSet({'ports': net_quota}))
if 'router' not in disabled_quotas:
router_quota = neutron_quotas.get('router').limit
qs.add(base.QuotaSet({'routers': router_quota}))
@ -364,6 +368,10 @@ def _get_tenant_network_usages(request, usages, disabled_quotas, tenant_id):
subnets = neutron.subnet_list(request, tenant_id=tenant_id)
usages.tally('subnets', len(subnets))
if 'port' not in disabled_quotas:
ports = neutron.port_list(request, tenant_id=tenant_id)
usages.tally('ports', len(ports))
if 'router' not in disabled_quotas:
routers = neutron.router_list(request, tenant_id=tenant_id)
usages.tally('routers', len(routers))