Browse Source

Add quota support to octavia's l7policy and l7rule

Current octavia has no l7policy and l7rule quota definitions. But
they are necessary for some scenarios. For example, implement
product design compatible with Neutron Lbaas.

Story: 2003382
Task: 24457
Change-Id: I09ee23dcb83f5f08a56e25cc05ff77caa3ad4230
changes/20/590620/30
Yang JianFeng 4 years ago
parent
commit
5d91913136
  1. 28
      api-ref/source/parameters.yaml
  2. 2
      api-ref/source/v2/examples/quota-update-curl
  3. 4
      api-ref/source/v2/examples/quota-update-request.json
  4. 4
      api-ref/source/v2/examples/quota-update-response.json
  5. 4
      api-ref/source/v2/examples/quotas-defaults-response.json
  6. 4
      api-ref/source/v2/examples/quotas-list-response.json
  7. 4
      api-ref/source/v2/examples/quotas-show-response.json
  8. 10
      api-ref/source/v2/quota.inc
  9. 2
      etc/octavia.conf
  10. 5
      octavia/api/root_controller.py
  11. 8
      octavia/api/v2/controllers/base.py
  12. 8
      octavia/api/v2/controllers/l7rule.py
  13. 6
      octavia/api/v2/types/quotas.py
  14. 6
      octavia/common/config.py
  15. 10
      octavia/common/data_models.py
  16. 2
      octavia/controller/worker/v1/flows/l7policy_flows.py
  17. 2
      octavia/controller/worker/v1/flows/l7rule_flows.py
  18. 136
      octavia/controller/worker/v1/tasks/database_tasks.py
  19. 2
      octavia/controller/worker/v2/flows/l7policy_flows.py
  20. 2
      octavia/controller/worker/v2/flows/l7rule_flows.py
  21. 136
      octavia/controller/worker/v2/tasks/database_tasks.py
  22. 40
      octavia/db/migration/alembic_migrations/versions/32e5c35b26a8_add_l7policy_and_l7rule_quota.py
  23. 4
      octavia/db/models.py
  24. 81
      octavia/db/repositories.py
  25. 3
      octavia/tests/functional/api/test_root_controller.py
  26. 10
      octavia/tests/functional/api/v2/test_l7rule.py
  27. 69
      octavia/tests/functional/api/v2/test_quotas.py
  28. 599
      octavia/tests/functional/db/test_repositories.py
  29. 87
      octavia/tests/unit/api/v2/types/test_quotas.py
  30. 62
      octavia/tests/unit/common/test_data_models.py
  31. 93
      octavia/tests/unit/controller/worker/v1/tasks/test_database_tasks_quota.py
  32. 92
      octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py
  33. 3
      releasenotes/notes/add-l7policy-and-l7rule-to-quota-4b873c77f1e608e6.yaml

28
api-ref/source/parameters.yaml

@ -1286,6 +1286,34 @@ quota-health_monitor-optional:
in: body
required: false
type: integer
quota-l7policy:
description: |
The configured l7policy quota limit. A setting of ``null`` means it is
using the deployment default quota. A setting of ``-1`` means unlimited.
in: body
required: true
type: integer
quota-l7policy-optional:
description: |
The configured l7policy quota limit. A setting of ``null`` means it is
using the deployment default quota. A setting of ``-1`` means unlimited.
in: body
required: false
type: integer
quota-l7rule:
description: |
The configured l7rule quota limit. A setting of ``null`` means it is
using the deployment default quota. A setting of ``-1`` means unlimited.
in: body
required: true
type: integer
quota-l7rule-optional:
description: |
The configured l7rule quota limit. A setting of ``null`` means it is
using the deployment default quota. A setting of ``-1`` means unlimited.
in: body
required: false
type: integer
quota-listener:
description: |
The configured listener quota limit. A setting of ``null`` means it is

2
api-ref/source/v2/examples/quota-update-curl

@ -1 +1 @@
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"quota":{"loadbalancer":10,"listener":-1,"member":50,"pool":-1,"healthmonitor":-1}}' http://198.51.100.10:9876/v2/lbaas/quotas/e3cd678b11784734bc366148aa37580e
curl -X PUT -H "Content-Type: application/json" -H "X-Auth-Token: <token>" -d '{"quota":{"loadbalancer":10,"listener":-1,"member":50,"pool":-1,"healthmonitor":-1,"l7policy":15,"l7rule":25}}' http://198.51.100.10:9876/v2/lbaas/quotas/e3cd678b11784734bc366148aa37580e

4
api-ref/source/v2/examples/quota-update-request.json

@ -4,6 +4,8 @@
"listener": -1,
"member": 50,
"pool": -1,
"healthmonitor": -1
"healthmonitor": -1,
"l7policy": 15,
"l7rule": 25
}
}

4
api-ref/source/v2/examples/quota-update-response.json

@ -4,6 +4,8 @@
"listener": -1,
"member": 50,
"pool": -1,
"healthmonitor": -1
"healthmonitor": -1,
"l7policy": 15,
"l7rule": 25
}
}

4
api-ref/source/v2/examples/quotas-defaults-response.json

@ -4,6 +4,8 @@
"listener": -1,
"member": -1,
"pool": -1,
"healthmonitor": -1
"healthmonitor": -1,
"l7policy": -1,
"l7rule": -1
}
}

4
api-ref/source/v2/examples/quotas-list-response.json

@ -6,7 +6,9 @@
"healthmonitor": -1,
"listener": null,
"project_id": "e3cd678b11784734bc366148aa37580e",
"pool": null
"pool": null,
"l7policy": 3,
"l7rule": null
}
]
}

4
api-ref/source/v2/examples/quotas-show-response.json

@ -4,6 +4,8 @@
"listener": -1,
"member": 50,
"pool": -1,
"healthmonitor": -1
"healthmonitor": -1,
"l7policy": 20,
"l7rule": -1
}
}

10
api-ref/source/v2/quota.inc

@ -51,6 +51,8 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- healthmonitor: quota-health_monitor
- l7policy: quota-l7policy
- l7rule: quota-l7rule
- listener: quota-listener
- loadbalancer: quota-load_balancer
- member: quota-member
@ -99,6 +101,8 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- healthmonitor: quota-health_monitor
- l7policy: quota-l7policy
- l7rule: quota-l7rule
- listener: quota-listener
- loadbalancer: quota-load_balancer
- member: quota-member
@ -156,6 +160,8 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- healthmonitor: quota-health_monitor
- l7policy: quota-l7policy
- l7rule: quota-l7rule
- listener: quota-listener
- loadbalancer: quota-load_balancer
- member: quota-member
@ -206,6 +212,8 @@ Request
.. rest_parameters:: ../parameters.yaml
- healthmonitor: quota-health_monitor-optional
- l7policy: quota-l7policy-optional
- l7rule: quota-l7rule-optional
- listener: quota-listener-optional
- loadbalancer: quota-load_balancer-optional
- member: quota-member-optional
@ -230,6 +238,8 @@ Response Parameters
.. rest_parameters:: ../parameters.yaml
- healthmonitor: quota-health_monitor
- l7policy: quota-l7policy
- l7rule: quota-l7rule
- listener: quota-listener
- loadbalancer: quota-load_balancer
- member: quota-member

2
etc/octavia.conf

@ -588,6 +588,8 @@
# default_member_quota = -1
# default_pool_quota = -1
# default_health_monitor_quota = -1
# default_l7policy_quota = -1
# default_l7rule_quota = -1
[audit]
# Enable auditing of API requests.

5
octavia/api/root_controller.py

@ -119,6 +119,9 @@ class RootController(object):
self._add_a_version(versions, 'v2.17', 'v2', 'SUPPORTED',
'2020-04-29T00:00:00Z', host_url)
# Pool TLS versions
self._add_a_version(versions, 'v2.18', 'v2', 'CURRENT',
self._add_a_version(versions, 'v2.18', 'v2', 'SUPPORTED',
'2020-04-29T01:00:00Z', host_url)
# Add quota support to octavia's l7policy and l7rule
self._add_a_version(versions, 'v2.19', 'v2', 'CURRENT',
'2020-05-12T00:00:00Z', host_url)
return {'versions': versions}

8
octavia/api/v2/controllers/base.py

@ -184,7 +184,9 @@ class BaseController(pecan_rest.RestController):
listener=CONF.quotas.default_listener_quota,
pool=CONF.quotas.default_pool_quota,
health_monitor=CONF.quotas.default_health_monitor_quota,
member=CONF.quotas.default_member_quota)
member=CONF.quotas.default_member_quota,
l7policy=CONF.quotas.default_l7policy_quota,
l7rule=CONF.quotas.default_l7rule_quota)
return quotas
def _get_db_quotas(self, session, project_id):
@ -213,6 +215,10 @@ class BaseController(pecan_rest.RestController):
default_health_monitor_quota)
if db_quotas.member is None:
db_quotas.member = CONF.quotas.default_member_quota
if db_quotas.l7policy is None:
db_quotas.l7policy = CONF.quotas.default_l7policy_quota
if db_quotas.l7rule is None:
db_quotas.l7rule = CONF.quotas.default_l7rule_quota
return db_quotas
def _auth_get_all(self, context, project_id):

8
octavia/api/v2/controllers/l7rule.py

@ -146,6 +146,14 @@ class L7RuleController(base.BaseController):
lock_session = db_api.get_session(autocommit=False)
try:
if self.repositories.check_quota_met(
context.session,
lock_session,
data_models.L7Rule,
l7rule.project_id):
raise exceptions.QuotaException(
resource=data_models.L7Rule._name())
l7rule_dict = db_prepare.create_l7rule(
l7rule.to_dict(render_unsets=True), self.l7policy_id)

6
octavia/api/v2/types/quotas.py

@ -36,6 +36,10 @@ class QuotaBase(base.BaseType):
# Misspelled version, deprecated in Rocky
health_monitor = wtypes.wsattr(wtypes.IntegerType(
minimum=consts.MIN_QUOTA, maximum=consts.MAX_QUOTA))
l7policy = wtypes.wsattr(wtypes.IntegerType(
minimum=consts.MIN_QUOTA, maximum=consts.MAX_QUOTA))
l7rule = wtypes.wsattr(wtypes.IntegerType(
minimum=consts.MIN_QUOTA, maximum=consts.MAX_QUOTA))
def to_dict(self, render_unsets=False):
quota_dict = super(QuotaBase, self).to_dict(render_unsets)
@ -70,6 +74,8 @@ class QuotaAllBase(base.BaseType):
healthmonitor = wtypes.wsattr(wtypes.IntegerType())
# Misspelled version, deprecated in Rocky, remove in T
health_monitor = wtypes.wsattr(wtypes.IntegerType())
l7policy = wtypes.wsattr(wtypes.IntegerType())
l7rule = wtypes.wsattr(wtypes.IntegerType())
_type_to_model_map = {'loadbalancer': 'load_balancer',
'healthmonitor': 'health_monitor'}

6
octavia/common/config.py

@ -721,6 +721,12 @@ quota_opts = [
cfg.IntOpt('default_health_monitor_quota',
default=constants.QUOTA_UNLIMITED,
help=_('Default per project health monitor quota.')),
cfg.IntOpt('default_l7policy_quota',
default=constants.QUOTA_UNLIMITED,
help=_('Default per project l7policy quota.')),
cfg.IntOpt('default_l7rule_quota',
default=constants.QUOTA_UNLIMITED,
help=_('Default per project l7rule quota.')),
]
audit_opts = [

10
octavia/common/data_models.py

@ -748,22 +748,30 @@ class Quotas(BaseDataModel):
pool=None,
health_monitor=None,
member=None,
l7policy=None,
l7rule=None,
in_use_health_monitor=None,
in_use_listener=None,
in_use_load_balancer=None,
in_use_member=None,
in_use_pool=None):
in_use_pool=None,
in_use_l7policy=None,
in_use_l7rule=None):
self.project_id = project_id
self.health_monitor = health_monitor
self.listener = listener
self.load_balancer = load_balancer
self.pool = pool
self.member = member
self.l7policy = l7policy
self.l7rule = l7rule
self.in_use_health_monitor = in_use_health_monitor
self.in_use_listener = in_use_listener
self.in_use_load_balancer = in_use_load_balancer
self.in_use_member = in_use_member
self.in_use_pool = in_use_pool
self.in_use_l7policy = in_use_l7policy
self.in_use_l7rule = in_use_l7rule
class Flavor(BaseDataModel):

2
octavia/controller/worker/v1/flows/l7policy_flows.py

@ -63,6 +63,8 @@ class L7PolicyFlows(object):
requires=constants.LOADBALANCER))
delete_l7policy_flow.add(database_tasks.DeleteL7PolicyInDB(
requires=constants.L7POLICY))
delete_l7policy_flow.add(database_tasks.DecrementL7policyQuota(
requires=constants.L7POLICY))
delete_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB(
requires=[constants.LOADBALANCER, constants.LISTENERS]))

2
octavia/controller/worker/v1/flows/l7rule_flows.py

@ -65,6 +65,8 @@ class L7RuleFlows(object):
requires=constants.LOADBALANCER))
delete_l7rule_flow.add(database_tasks.DeleteL7RuleInDB(
requires=constants.L7RULE))
delete_l7rule_flow.add(database_tasks.DecrementL7ruleQuota(
requires=constants.L7RULE))
delete_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB(
requires=constants.L7POLICY))
delete_l7rule_flow.add(database_tasks.MarkLBAndListenersActiveInDB(

136
octavia/controller/worker/v1/tasks/database_tasks.py

@ -2671,6 +2671,142 @@ class CountPoolChildrenForQuota(BaseDatabaseTask):
return {'HM': health_mon_count, 'member': member_count}
class DecrementL7policyQuota(BaseDatabaseTask):
"""Decrements the l7policy quota for a project.
Since sqlalchemy will likely retry by itself always revert if it fails
"""
def execute(self, l7policy):
"""Decrements the l7policy quota.
:param l7policy: The l7policy to decrement the quota on.
:returns: None
"""
LOG.debug("Decrementing l7policy quota for "
"project: %s ", l7policy.project_id)
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.decrement_quota(lock_session,
data_models.L7Policy,
l7policy.project_id)
if l7policy.l7rules:
self.repos.decrement_quota(lock_session,
data_models.L7Rule,
l7policy.project_id,
quantity=len(l7policy.l7rules))
lock_session.commit()
except Exception:
with excutils.save_and_reraise_exception():
LOG.error('Failed to decrement l7policy quota for project: '
'%(proj)s the project may have excess quota in use.',
{'proj': l7policy.project_id})
lock_session.rollback()
def revert(self, l7policy, result, *args, **kwargs):
"""Re-apply the quota
:param l7policy: The l7policy to decrement the quota on.
:returns: None
"""
LOG.warning('Reverting decrement quota for l7policy on project'
' %(proj)s Project quota counts may be incorrect.',
{'proj': l7policy.project_id})
# Increment the quota back if this task wasn't the failure
if not isinstance(result, failure.Failure):
try:
session = db_apis.get_session()
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Policy,
l7policy.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
# Attempt to increment back the L7Rule quota
for i in range(len(l7policy.l7rules)):
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Rule,
l7policy.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
except Exception:
# Don't fail the revert flow
pass
class DecrementL7ruleQuota(BaseDatabaseTask):
"""Decrements the l7rule quota for a project.
Since sqlalchemy will likely retry by itself always revert if it fails
"""
def execute(self, l7rule):
"""Decrements the l7rule quota.
:param l7rule: The l7rule to decrement the quota on.
:returns: None
"""
LOG.debug("Decrementing l7rule quota for "
"project: %s ", l7rule.project_id)
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.decrement_quota(lock_session,
data_models.L7Rule,
l7rule.project_id)
lock_session.commit()
except Exception:
with excutils.save_and_reraise_exception():
LOG.error('Failed to decrement l7rule quota for project: '
'%(proj)s the project may have excess quota in use.',
{'proj': l7rule.project_id})
lock_session.rollback()
def revert(self, l7rule, result, *args, **kwargs):
"""Re-apply the quota
:param l7rule: The l7rule to decrement the quota on.
:returns: None
"""
LOG.warning('Reverting decrement quota for l7rule on project %(proj)s '
'Project quota counts may be incorrect.',
{'proj': l7rule.project_id})
# Increment the quota back if this task wasn't the failure
if not isinstance(result, failure.Failure):
try:
session = db_apis.get_session()
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Rule,
l7rule.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
except Exception:
# Don't fail the revert flow
pass
class UpdatePoolMembersOperatingStatusInDB(BaseDatabaseTask):
"""Updates the members of a pool operating status.

2
octavia/controller/worker/v2/flows/l7policy_flows.py

@ -60,6 +60,8 @@ class L7PolicyFlows(object):
requires=constants.LOADBALANCER_ID))
delete_l7policy_flow.add(database_tasks.DeleteL7PolicyInDB(
requires=constants.L7POLICY))
delete_l7policy_flow.add(database_tasks.DecrementL7policyQuota(
requires=constants.L7POLICY))
delete_l7policy_flow.add(database_tasks.MarkLBAndListenersActiveInDB(
requires=(constants.LOADBALANCER_ID, constants.LISTENERS)))

2
octavia/controller/worker/v2/flows/l7rule_flows.py

@ -64,6 +64,8 @@ class L7RuleFlows(object):
requires=constants.LOADBALANCER_ID))
delete_l7rule_flow.add(database_tasks.DeleteL7RuleInDB(
requires=constants.L7RULE))
delete_l7rule_flow.add(database_tasks.DecrementL7ruleQuota(
requires=constants.L7RULE))
delete_l7rule_flow.add(database_tasks.MarkL7PolicyActiveInDB(
requires=constants.L7POLICY))
delete_l7rule_flow.add(database_tasks.MarkLBAndListenersActiveInDB(

136
octavia/controller/worker/v2/tasks/database_tasks.py

@ -2860,6 +2860,142 @@ class CountPoolChildrenForQuota(BaseDatabaseTask):
return {'HM': health_mon_count, 'member': member_count}
class DecrementL7policyQuota(BaseDatabaseTask):
"""Decrements the l7policy quota for a project.
Since sqlalchemy will likely retry by itself always revert if it fails
"""
def execute(self, l7policy):
"""Decrements the l7policy quota.
:param l7policy: The l7policy to decrement the quota on.
:returns: None
"""
LOG.debug("Decrementing l7policy quota for "
"project: %s ", l7policy.project_id)
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.decrement_quota(lock_session,
data_models.L7Policy,
l7policy.project_id)
if l7policy.l7rules:
self.repos.decrement_quota(lock_session,
data_models.L7Rule,
l7policy.project_id,
quantity=len(l7policy.l7rules))
lock_session.commit()
except Exception:
with excutils.save_and_reraise_exception():
LOG.error('Failed to decrement l7policy quota for project: '
'%(proj)s the project may have excess quota in use.',
{'proj': l7policy.project_id})
lock_session.rollback()
def revert(self, l7policy, result, *args, **kwargs):
"""Re-apply the quota
:param l7policy: The l7policy to decrement the quota on.
:returns: None
"""
LOG.warning('Reverting decrement quota for l7policy on project'
' %(proj)s Project quota counts may be incorrect.',
{'proj': l7policy.project_id})
# Increment the quota back if this task wasn't the failure
if not isinstance(result, failure.Failure):
try:
session = db_apis.get_session()
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Policy,
l7policy.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
# Attempt to increment back the L7Rule quota
for i in range(len(l7policy.l7rules)):
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Rule,
l7policy.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
except Exception:
# Don't fail the revert flow
pass
class DecrementL7ruleQuota(BaseDatabaseTask):
"""Decrements the l7rule quota for a project.
Since sqlalchemy will likely retry by itself always revert if it fails
"""
def execute(self, l7rule):
"""Decrements the l7rule quota.
:param l7rule: The l7rule to decrement the quota on.
:returns: None
"""
LOG.debug("Decrementing l7rule quota for "
"project: %s ", l7rule.project_id)
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.decrement_quota(lock_session,
data_models.L7Rule,
l7rule.project_id)
lock_session.commit()
except Exception:
with excutils.save_and_reraise_exception():
LOG.error('Failed to decrement l7rule quota for project: '
'%(proj)s the project may have excess quota in use.',
{'proj': l7rule.project_id})
lock_session.rollback()
def revert(self, l7rule, result, *args, **kwargs):
"""Re-apply the quota
:param l7rule: The l7rule to decrement the quota on.
:returns: None
"""
LOG.warning('Reverting decrement quota for l7rule on project %(proj)s '
'Project quota counts may be incorrect.',
{'proj': l7rule.project_id})
# Increment the quota back if this task wasn't the failure
if not isinstance(result, failure.Failure):
try:
session = db_apis.get_session()
lock_session = db_apis.get_session(autocommit=False)
try:
self.repos.check_quota_met(session,
lock_session,
data_models.L7Rule,
l7rule.project_id)
lock_session.commit()
except Exception:
lock_session.rollback()
except Exception:
# Don't fail the revert flow
pass
class UpdatePoolMembersOperatingStatusInDB(BaseDatabaseTask):
"""Updates the members of a pool operating status.

40
octavia/db/migration/alembic_migrations/versions/32e5c35b26a8_add_l7policy_and_l7rule_quota.py

@ -0,0 +1,40 @@
# Copyright (c) 2018 China Telecom Corporation
# 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.
"""add l7policy and l7rule quota
Revision ID: 32e5c35b26a8
Revises: d3c8a090f3de
Create Date: 2018-08-10 09:13:59.383272
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '32e5c35b26a8'
down_revision = 'd3c8a090f3de'
def upgrade():
op.add_column(u'quotas',
sa.Column('l7policy', sa.Integer(), nullable=True))
op.add_column(u'quotas',
sa.Column('l7rule', sa.Integer(), nullable=True))
op.add_column(u'quotas',
sa.Column('in_use_l7policy', sa.Integer(), nullable=True))
op.add_column(u'quotas',
sa.Column('in_use_l7rule', sa.Integer(), nullable=True))

4
octavia/db/models.py

@ -750,11 +750,15 @@ class Quotas(base_models.BASE):
load_balancer = sa.Column(sa.Integer(), nullable=True)
member = sa.Column(sa.Integer(), nullable=True)
pool = sa.Column(sa.Integer(), nullable=True)
l7policy = sa.Column(sa.Integer(), nullable=True)
l7rule = sa.Column(sa.Integer(), nullable=True)
in_use_health_monitor = sa.Column(sa.Integer(), nullable=True)
in_use_listener = sa.Column(sa.Integer(), nullable=True)
in_use_load_balancer = sa.Column(sa.Integer(), nullable=True)
in_use_member = sa.Column(sa.Integer(), nullable=True)
in_use_pool = sa.Column(sa.Integer(), nullable=True)
in_use_l7policy = sa.Column(sa.Integer(), nullable=True)
in_use_l7rule = sa.Column(sa.Integer(), nullable=True)
class FlavorProfile(base_models.BASE, base_models.IdMixin,

81
octavia/db/repositories.py

@ -499,6 +499,48 @@ class Repositories(object):
quotas.in_use_member = member_count
return False
return True
if _class == data_models.L7Policy:
# Decide which quota to use
if quotas.l7policy is None:
l7policy_quota = CONF.quotas.default_l7policy_quota
else:
l7policy_quota = quotas.l7policy
# Get the current in use count
if not quotas.in_use_l7policy:
# This is to handle the upgrade case
l7policy_count = session.query(models.L7Policy).filter(
models.L7Policy.project_id == project_id,
models.L7Policy.provisioning_status !=
consts.DELETED).count() + count
else:
l7policy_count = quotas.in_use_l7policy + count
# Decide if the quota is met
if (l7policy_count <= l7policy_quota or
l7policy_quota == consts.QUOTA_UNLIMITED):
quotas.in_use_l7policy = l7policy_count
return False
return True
if _class == data_models.L7Rule:
# Decide which quota to use
if quotas.l7rule is None:
l7rule_quota = CONF.quotas.default_l7rule_quota
else:
l7rule_quota = quotas.l7rule
# Get the current in use count
if not quotas.in_use_l7rule:
# This is to handle the upgrade case
l7rule_count = session.query(models.L7Rule).filter(
models.L7Rule.project_id == project_id,
models.L7Rule.provisioning_status !=
consts.DELETED).count() + count
else:
l7rule_count = quotas.in_use_l7rule + count
# Decide if the quota is met
if (l7rule_count <= l7rule_quota or
l7rule_quota == consts.QUOTA_UNLIMITED):
quotas.in_use_l7rule = l7rule_count
return False
return True
except db_exception.DBDeadlock:
LOG.warning('Quota project lock timed out for project: %(proj)s',
{'proj': project_id})
@ -584,6 +626,28 @@ class Repositories(object):
'project: %(proj)s that would cause a '
'negative quota.',
{'clss': type(_class), 'proj': project_id})
if _class == data_models.L7Policy:
if (quotas.in_use_l7policy is not None and
quotas.in_use_l7policy > 0):
quotas.in_use_l7policy = (
quotas.in_use_l7policy - quantity)
else:
if not CONF.api_settings.auth_strategy == consts.NOAUTH:
LOG.warning('Quota decrement on %(clss)s called on '
'project: %(proj)s that would cause a '
'negative quota.',
{'clss': type(_class), 'proj': project_id})
if _class == data_models.L7Rule:
if (quotas.in_use_l7rule is not None and
quotas.in_use_l7rule > 0):
quotas.in_use_l7rule = (
quotas.in_use_l7rule - quantity)
else:
if not CONF.api_settings.auth_strategy == consts.NOAUTH:
LOG.warning('Quota decrement on %(clss)s called on '
'project: %(proj)s that would cause a '
'negative quota.',
{'clss': type(_class), 'proj': project_id})
except db_exception.DBDeadlock:
LOG.warning('Quota project lock timed out for project: %(proj)s',
{'proj': project_id})
@ -657,6 +721,13 @@ class Repositories(object):
self.sni.create(lock_session, **sni_container)
if l7policies_dict:
for policy_dict in l7policies_dict:
# Add l7policy quota check
if self.check_quota_met(session,
lock_session,
data_models.L7Policy,
lb_dict['project_id']):
raise exceptions.QuotaException(
resource=data_models.L7Policy._name())
l7rules_dict = policy_dict.pop('l7rules')
if policy_dict.get('redirect_pool'):
# Add pool quota check
@ -711,6 +782,14 @@ class Repositories(object):
policy_dm = self.l7policy.create(lock_session,
**policy_dict)
for rule_dict in l7rules_dict:
# Add l7rule quota check
if self.check_quota_met(
session,
lock_session,
data_models.L7Rule,
lb_dict['project_id']):
raise exceptions.QuotaException(
resource=data_models.L7Rule._name())
rule_dict['l7policy_id'] = policy_dm.id
self.l7rule.create(lock_session, **rule_dict)
lock_session.commit()
@ -1848,6 +1927,8 @@ class QuotasRepository(BaseRepository):
quotas.listener = None
quotas.member = None
quotas.pool = None
quotas.l7policy = None
quotas.l7rule = None
session.flush()

3
octavia/tests/functional/api/test_root_controller.py

@ -45,7 +45,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
def test_api_versions(self):
versions = self._get_versions_with_config()
version_ids = tuple(v.get('id') for v in versions)
self.assertEqual(19, len(version_ids))
self.assertEqual(20, len(version_ids))
self.assertIn('v2.0', version_ids)
self.assertIn('v2.1', version_ids)
self.assertIn('v2.2', version_ids)
@ -65,6 +65,7 @@ class TestRootController(base_db_test.OctaviaDBTestBase):
self.assertIn('v2.16', version_ids)
self.assertIn('v2.17', version_ids)
self.assertIn('v2.18', version_ids)
self.assertIn('v2.19', version_ids)
# Each version should have a 'self' 'href' to the API version URL
# [{u'rel': u'self', u'href': u'http://localhost/v2'}]

10
octavia/tests/functional/api/v2/test_l7rule.py

@ -19,6 +19,7 @@ from oslo_utils import uuidutils
from octavia.common import constants
import octavia.common.context
from octavia.common import data_models
from octavia.common import exceptions
from octavia.db import repositories
from octavia.tests.functional.api.v2 import base
@ -829,6 +830,15 @@ class TestL7Rule(base.BaseAPITest):
def test_create_bad_cases_with_ssl_rule_types(self):
self._test_bad_cases_with_ssl_rule_types()
def test_create_over_quota(self):
self.start_quota_mock(data_models.L7Rule)
l7rule = {'compare_type': 'REGEX',
'invert': False,
'type': 'PATH',
'value': '/images*',
'admin_state_up': True}
self.post(self.l7rules_path, self._build_body(l7rule), status=403)
def test_update(self):
api_l7rule = self.create_l7rule(
self.l7policy_id, constants.L7RULE_TYPE_PATH,

69
octavia/tests/functional/api/v2/test_quotas.py

@ -54,6 +54,14 @@ class TestQuotas(base.BaseAPITest):
group="quotas",
default_health_monitor_quota=random.randrange(
constants.QUOTA_UNLIMITED, 9000))
conf.config(
group="quotas",
default_l7policy_quota=random.randrange(
constants.QUOTA_UNLIMITED, 9000))
conf.config(
group="quotas",
default_l7rule_quota=random.randrange(
constants.QUOTA_UNLIMITED, 9000))
self.project_id = uuidutils.generate_uuid()
@ -65,13 +73,17 @@ class TestQuotas(base.BaseAPITest):
'pool': CONF.quotas.default_pool_quota,
'health_monitor':
CONF.quotas.default_health_monitor_quota,
'member': CONF.quotas.default_member_quota}
'member': CONF.quotas.default_member_quota,
'l7policy': CONF.quotas.default_l7policy_quota,
'l7rule': CONF.quotas.default_l7rule_quota}
self.assertEqual(expected['load_balancer'], observed['load_balancer'])
self.assertEqual(expected['listener'], observed['listener'])
self.assertEqual(expected['pool'], observed['pool'])
self.assertEqual(expected['health_monitor'],
observed['health_monitor'])
self.assertEqual(expected['member'], observed['member'])
self.assertEqual(expected['l7policy'], observed['l7policy'])
self.assertEqual(expected['l7rule'], observed['l7rule'])
def test_get_all_quotas_no_quotas(self):
response = self.get(self.QUOTAS_PATH)
@ -83,12 +95,14 @@ class TestQuotas(base.BaseAPITest):
project_id2 = uuidutils.generate_uuid()
quota_path1 = self.QUOTA_PATH.format(project_id=project_id1)
quota1 = {'load_balancer': constants.QUOTA_UNLIMITED, 'listener': 30,
'pool': 30, 'health_monitor': 30, 'member': 30}
'pool': 30, 'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}
body1 = {'quota': quota1}
self.put(quota_path1, body1, status=202)
quota_path2 = self.QUOTA_PATH.format(project_id=project_id2)
quota2 = {'load_balancer': 50, 'listener': 50, 'pool': 50,
'health_monitor': 50, 'member': 50}
'health_monitor': 50, 'member': 50, 'l7policy': 50,
'l7rule': 50}
body2 = {'quota': quota2}
self.put(quota_path2, body2, status=202)
@ -111,12 +125,14 @@ class TestQuotas(base.BaseAPITest):
project_id2 = uuidutils.generate_uuid()
quota_path1 = self.QUOTA_PATH.format(project_id=project_id1)
quota1 = {'load_balancer': constants.QUOTA_UNLIMITED, 'listener': 30,
'pool': 30, 'health_monitor': 30, 'member': 30}
'pool': 30, 'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}
body1 = {'quota': quota1}
self.put(quota_path1, body1, status=202)
quota_path2 = self.QUOTA_PATH.format(project_id=project_id2)
quota2 = {'loadbalancer': 50, 'listener': 50, 'pool': 50,
'healthmonitor': 50, 'member': 50}
'healthmonitor': 50, 'member': 50, 'l7policy': 50,
'l7rule': 50}
body2 = {'quota': quota2}
self.put(quota_path2, body2, status=202)
@ -139,12 +155,14 @@ class TestQuotas(base.BaseAPITest):
project_id2 = uuidutils.generate_uuid()
quota_path1 = self.QUOTA_PATH.format(project_id=project_id1)
quota1 = {'load_balancer': constants.QUOTA_UNLIMITED, 'listener': 30,
'pool': 30, 'health_monitor': 30, 'member': 30}
'pool': 30, 'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}
body1 = {'quota': quota1}
self.put(quota_path1, body1, status=202)
quota_path2 = self.QUOTA_PATH.format(project_id=project_id2)
quota2 = {'load_balancer': 50, 'listener': 50, 'pool': 50,
'health_monitor': 50, 'member': 50}
'health_monitor': 50, 'member': 50, 'l7policy': 50,
'l7rule': 50}
body2 = {'quota': quota2}
self.put(quota_path2, body2, status=202)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -160,7 +178,8 @@ class TestQuotas(base.BaseAPITest):
project_id1 = uuidutils.generate_uuid()
quota_path1 = self.QUOTA_PATH.format(project_id=project_id1)
quota1 = {'load_balancer': constants.QUOTA_UNLIMITED, 'listener': 30,
'pool': 30, 'health_monitor': 30, 'member': 30}
'pool': 30, 'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}
body1 = {'quota': quota1}
self.put(quota_path1, body1, status=202)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -193,12 +212,14 @@ class TestQuotas(base.BaseAPITest):
project_id2 = uuidutils.generate_uuid()
quota_path1 = self.QUOTA_PATH.format(project_id=project_id1)
quota1 = {'load_balancer': constants.QUOTA_UNLIMITED, 'listener': 30,
'pool': 30, 'health_monitor': 30, 'member': 30}
'pool': 30, 'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}
body1 = {'quota': quota1}
self.put(quota_path1, body1, status=202)
quota_path2 = self.QUOTA_PATH.format(project_id=project_id2)
quota2 = {'load_balancer': 50, 'listener': 50, 'pool': 50,
'health_monitor': 50, 'member': 50}
'health_monitor': 50, 'member': 50, 'l7policy': 50,
'l7rule': 50}
body2 = {'quota': quota2}
self.put(quota_path2, body2, status=202)
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
@ -701,7 +722,8 @@ class TestQuotas(base.BaseAPITest):
def test_custom_quotas(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json
@ -710,7 +732,8 @@ class TestQuotas(base.BaseAPITest):
def test_custom_quotas_quota_admin(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
@ -741,7 +764,8 @@ class TestQuotas(base.BaseAPITest):
def test_custom_quotas_not_Authorized_member(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF))
auth_strategy = self.conf.conf.api_settings.get('auth_strategy')
self.conf.config(group='api_settings', auth_strategy=constants.TESTING)
@ -770,11 +794,12 @@ class TestQuotas(base.BaseAPITest):
def test_custom_partial_quotas(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': None, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
expected_body = {'quota': {
'load_balancer': 30,
'listener': CONF.quotas.default_listener_quota, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30, 'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json
@ -784,11 +809,12 @@ class TestQuotas(base.BaseAPITest):
def test_custom_missing_quotas(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30,
'l7policy': 30, 'l7rule': 30}}
expected_body = {'quota': {
'load_balancer': 30,
'listener': CONF.quotas.default_listener_quota, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30, 'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json
@ -798,7 +824,8 @@ class TestQuotas(base.BaseAPITest):
def test_delete_custom_quotas(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json
@ -811,7 +838,8 @@ class TestQuotas(base.BaseAPITest):
def test_delete_custom_quotas_admin(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json
@ -846,7 +874,8 @@ class TestQuotas(base.BaseAPITest):
def test_delete_quotas_not_Authorized_member(self):
quota_path = self.QUOTA_PATH.format(project_id=self.project_id)
body = {'quota': {'load_balancer': 30, 'listener': 30, 'pool': 30,
'health_monitor': 30, 'member': 30}}
'health_monitor': 30, 'member': 30, 'l7policy': 30,
'l7rule': 30}}
self.put(quota_path, body, status=202)
response = self.get(quota_path)
quota_dict = response.json

599
octavia/tests/functional/db/test_repositories.py

@ -612,6 +612,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
l7rule = {'type': constants.L7RULE_TYPE_HOST_NAME,
'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO,
'value': 'localhost'}
l7rule2 = {'type': constants.L7RULE_TYPE_PATH,
'compare_type': constants.L7RULE_COMPARE_TYPE_CONTAINS,
'value': 'abc'}
r_health_monitor = {'type': constants.HEALTH_MONITOR_HTTP, 'delay': 1,
'timeout': 1, 'fall_threshold': 1,
'rise_threshold': 1, 'enabled': True}
@ -629,11 +632,16 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'redirect_pool_id': redirect_pool.get('id'),
'id': uuidutils.generate_uuid()}
l7rule['l7policy_id'] = l7policy.get('id')
l7policy2 = {'name': 'l7policy2', 'enabled': True,
'description': 'l7policy_description', 'position': 2,
'action': constants.L7POLICY_ACTION_REJECT,
'id': uuidutils.generate_uuid()}
l7rule2['l7policy_id'] = l7policy2.get('id')
listener = {'project_id': project_id, 'name': 'listener1',
'description': 'listener_description',
'protocol': constants.PROTOCOL_HTTP, 'protocol_port': 80,
'connection_limit': 1, 'enabled': True,
'default_pool': pool, 'l7policies': [l7policy],
'default_pool': pool, 'l7policies': [l7policy, l7policy2],
'provisioning_status': constants.PENDING_CREATE,
'operating_status': constants.ONLINE,
'id': uuidutils.generate_uuid()}
@ -646,6 +654,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'operating_status': constants.ONLINE,
'id': uuidutils.generate_uuid()}
l7policy['listener_id'] = listener.get('id')
l7policy2['listener_id'] = listener.get('id')
vip = {'ip_address': '192.0.2.1', 'port_id': uuidutils.generate_uuid(),
'subnet_id': uuidutils.generate_uuid()}
lb = {'name': 'lb1', 'description': 'desc1', 'enabled': True,
@ -661,6 +670,16 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
pool['load_balancer_id'] = lb.get('id')
redirect_pool['load_balancer_id'] = lb.get('id')
lb2_l7rule = {'type': constants.L7RULE_TYPE_HOST_NAME,
'compare_type': constants.L7RULE_COMPARE_TYPE_EQUAL_TO,
'value': 'localhost'}
lb2_l7policy = {'name': 'l7policy1', 'enabled': True,
'description': 'l7policy_description', 'position': 1,
'action': constants.L7POLICY_ACTION_REDIRECT_TO_URL,
'redirect_url': 'www.example.com',
'l7rules': [lb2_l7rule],
'id': uuidutils.generate_uuid()}
lb2_l7rule['l7policy_id'] = lb2_l7policy.get('id')
lb2_health_monitor = {'type': constants.HEALTH_MONITOR_HTTP,
'delay': 1, 'timeout': 1, 'fall_threshold': 1,
'rise_threshold': 1, 'enabled': True}
@ -681,10 +700,11 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'protocol': constants.PROTOCOL_HTTP,
'protocol_port': 83, 'connection_limit': 1,
'enabled': True,
'default_pool': lb2_pool,
'default_pool': lb2_pool, 'l7policies': [lb2_l7policy],
'provisioning_status': constants.PENDING_CREATE,
'operating_status': constants.ONLINE,
'id': uuidutils.generate_uuid()}
lb2_l7policy['listener_id'] = lb2_listener.get('id')
lb2 = {'name': 'lb2', 'description': 'desc2', 'enabled': True,
'topology': constants.TOPOLOGY_ACTIVE_STANDBY,
'vrrp_group': None,
@ -701,7 +721,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'listener': 10,
'pool': 10,
'health_monitor': 10,
'member': 10}
'member': 10,
'l7policy': 10,
'l7rule': 10}
self.repos.quotas.update(self.session, project_id, quota=quota)
lock_session = db_api.get_session(autocommit=False)
self.assertRaises(
@ -716,7 +738,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'listener': 0,
'pool': 10,
'health_monitor': 10,
'member': 10}
'member': 10,
'l7policy': 10,
'l7rule': 10}
self.repos.quotas.update(self.session, project_id, quota=quota)
lock_session = db_api.get_session(autocommit=False)
self.assertRaises(
@ -731,7 +755,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'listener': 10,
'pool': 0,
'health_monitor': 10,
'member': 10}
'member': 10,
'l7policy': 10,
'l7rule': 10}
self.repos.quotas.update(self.session, project_id, quota=quota)
lock_session = db_api.get_session(autocommit=False)
self.assertRaises(
@ -746,7 +772,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
'listener': 10,
'pool': 10,
'health_monitor': 0,
'member': 10}
'member': 10,
'l7policy': 10,
'l7rule': 10}
self.repos.quotas.update(self.session, project_id, quota=quota)
lock_sessio