From 5d919131362f197264dd84d75df5139921cf3b1f Mon Sep 17 00:00:00 2001 From: Yang JianFeng Date: Fri, 10 Aug 2018 03:16:59 +0000 Subject: [PATCH] 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 --- api-ref/source/parameters.yaml | 28 + api-ref/source/v2/examples/quota-update-curl | 2 +- .../v2/examples/quota-update-request.json | 4 +- .../v2/examples/quota-update-response.json | 4 +- .../v2/examples/quotas-defaults-response.json | 4 +- .../v2/examples/quotas-list-response.json | 4 +- .../v2/examples/quotas-show-response.json | 4 +- api-ref/source/v2/quota.inc | 10 + etc/octavia.conf | 2 + octavia/api/root_controller.py | 5 +- octavia/api/v2/controllers/base.py | 8 +- octavia/api/v2/controllers/l7rule.py | 8 + octavia/api/v2/types/quotas.py | 6 + octavia/common/config.py | 6 + octavia/common/data_models.py | 10 +- .../worker/v1/flows/l7policy_flows.py | 2 + .../worker/v1/flows/l7rule_flows.py | 2 + .../worker/v1/tasks/database_tasks.py | 136 ++++ .../worker/v2/flows/l7policy_flows.py | 2 + .../worker/v2/flows/l7rule_flows.py | 2 + .../worker/v2/tasks/database_tasks.py | 136 ++++ ...5c35b26a8_add_l7policy_and_l7rule_quota.py | 40 ++ octavia/db/models.py | 4 + octavia/db/repositories.py | 81 +++ .../functional/api/test_root_controller.py | 3 +- .../tests/functional/api/v2/test_l7rule.py | 10 + .../tests/functional/api/v2/test_quotas.py | 69 +- .../tests/functional/db/test_repositories.py | 599 +++++++++++++++++- .../tests/unit/api/v2/types/test_quotas.py | 87 +++ octavia/tests/unit/common/test_data_models.py | 62 ++ .../v1/tasks/test_database_tasks_quota.py | 93 +++ .../v2/tasks/test_database_tasks_quota.py | 92 +++ ...-and-l7rule-to-quota-4b873c77f1e608e6.yaml | 3 + 33 files changed, 1481 insertions(+), 47 deletions(-) create mode 100644 octavia/db/migration/alembic_migrations/versions/32e5c35b26a8_add_l7policy_and_l7rule_quota.py create mode 100644 octavia/tests/unit/api/v2/types/test_quotas.py create mode 100644 releasenotes/notes/add-l7policy-and-l7rule-to-quota-4b873c77f1e608e6.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 6e3d97ad46..b8887895d9 100644 --- a/api-ref/source/parameters.yaml +++ b/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 diff --git a/api-ref/source/v2/examples/quota-update-curl b/api-ref/source/v2/examples/quota-update-curl index 5a6c70f30c..dc348632d4 100644 --- a/api-ref/source/v2/examples/quota-update-curl +++ b/api-ref/source/v2/examples/quota-update-curl @@ -1 +1 @@ -curl -X PUT -H "Content-Type: application/json" -H "X-Auth-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: " -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 diff --git a/api-ref/source/v2/examples/quota-update-request.json b/api-ref/source/v2/examples/quota-update-request.json index 26eb1636c9..56387b880c 100644 --- a/api-ref/source/v2/examples/quota-update-request.json +++ b/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 } } diff --git a/api-ref/source/v2/examples/quota-update-response.json b/api-ref/source/v2/examples/quota-update-response.json index 26eb1636c9..56387b880c 100644 --- a/api-ref/source/v2/examples/quota-update-response.json +++ b/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 } } diff --git a/api-ref/source/v2/examples/quotas-defaults-response.json b/api-ref/source/v2/examples/quotas-defaults-response.json index 700bee58bb..fd2a432acf 100644 --- a/api-ref/source/v2/examples/quotas-defaults-response.json +++ b/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 } } diff --git a/api-ref/source/v2/examples/quotas-list-response.json b/api-ref/source/v2/examples/quotas-list-response.json index 91af74dc9e..ae1f615d98 100644 --- a/api-ref/source/v2/examples/quotas-list-response.json +++ b/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 } ] } diff --git a/api-ref/source/v2/examples/quotas-show-response.json b/api-ref/source/v2/examples/quotas-show-response.json index 3b05742617..a03731dd4b 100644 --- a/api-ref/source/v2/examples/quotas-show-response.json +++ b/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 } } diff --git a/api-ref/source/v2/quota.inc b/api-ref/source/v2/quota.inc index 26ed870e77..16929ca864 100644 --- a/api-ref/source/v2/quota.inc +++ b/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 diff --git a/etc/octavia.conf b/etc/octavia.conf index 906198913d..502260eeb0 100644 --- a/etc/octavia.conf +++ b/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. diff --git a/octavia/api/root_controller.py b/octavia/api/root_controller.py index 1b407143ab..9a3f50042b 100644 --- a/octavia/api/root_controller.py +++ b/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} diff --git a/octavia/api/v2/controllers/base.py b/octavia/api/v2/controllers/base.py index 633183bc05..4bad735b92 100644 --- a/octavia/api/v2/controllers/base.py +++ b/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): diff --git a/octavia/api/v2/controllers/l7rule.py b/octavia/api/v2/controllers/l7rule.py index fda37097e4..1e11c4226c 100644 --- a/octavia/api/v2/controllers/l7rule.py +++ b/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) diff --git a/octavia/api/v2/types/quotas.py b/octavia/api/v2/types/quotas.py index 08bff46c26..3b24490023 100644 --- a/octavia/api/v2/types/quotas.py +++ b/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'} diff --git a/octavia/common/config.py b/octavia/common/config.py index a0c6a1c4e9..326d974304 100644 --- a/octavia/common/config.py +++ b/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 = [ diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 0e465d7c2f..4631d5e103 100644 --- a/octavia/common/data_models.py +++ b/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): diff --git a/octavia/controller/worker/v1/flows/l7policy_flows.py b/octavia/controller/worker/v1/flows/l7policy_flows.py index a74dfb55e0..3d71a5e827 100644 --- a/octavia/controller/worker/v1/flows/l7policy_flows.py +++ b/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])) diff --git a/octavia/controller/worker/v1/flows/l7rule_flows.py b/octavia/controller/worker/v1/flows/l7rule_flows.py index 2cb234ea85..c33e9e99bf 100644 --- a/octavia/controller/worker/v1/flows/l7rule_flows.py +++ b/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( diff --git a/octavia/controller/worker/v1/tasks/database_tasks.py b/octavia/controller/worker/v1/tasks/database_tasks.py index dd8a932a03..8afd76246d 100644 --- a/octavia/controller/worker/v1/tasks/database_tasks.py +++ b/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. diff --git a/octavia/controller/worker/v2/flows/l7policy_flows.py b/octavia/controller/worker/v2/flows/l7policy_flows.py index 7b74114b8f..9b683e0088 100644 --- a/octavia/controller/worker/v2/flows/l7policy_flows.py +++ b/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))) diff --git a/octavia/controller/worker/v2/flows/l7rule_flows.py b/octavia/controller/worker/v2/flows/l7rule_flows.py index 76b9654ad1..39506204bb 100644 --- a/octavia/controller/worker/v2/flows/l7rule_flows.py +++ b/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( diff --git a/octavia/controller/worker/v2/tasks/database_tasks.py b/octavia/controller/worker/v2/tasks/database_tasks.py index b13d9388c4..876e125e8d 100644 --- a/octavia/controller/worker/v2/tasks/database_tasks.py +++ b/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. diff --git a/octavia/db/migration/alembic_migrations/versions/32e5c35b26a8_add_l7policy_and_l7rule_quota.py b/octavia/db/migration/alembic_migrations/versions/32e5c35b26a8_add_l7policy_and_l7rule_quota.py new file mode 100644 index 0000000000..7162230a30 --- /dev/null +++ b/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)) diff --git a/octavia/db/models.py b/octavia/db/models.py index 8b889c7512..8a792d4045 100644 --- a/octavia/db/models.py +++ b/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, diff --git a/octavia/db/repositories.py b/octavia/db/repositories.py index f47732dacb..e0f488fbf5 100644 --- a/octavia/db/repositories.py +++ b/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() diff --git a/octavia/tests/functional/api/test_root_controller.py b/octavia/tests/functional/api/test_root_controller.py index 1bf058c7b0..8c9792b9ab 100644 --- a/octavia/tests/functional/api/test_root_controller.py +++ b/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'}] diff --git a/octavia/tests/functional/api/v2/test_l7rule.py b/octavia/tests/functional/api/v2/test_l7rule.py index 87a2aef00c..6cfab21cab 100644 --- a/octavia/tests/functional/api/v2/test_l7rule.py +++ b/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, diff --git a/octavia/tests/functional/api/v2/test_quotas.py b/octavia/tests/functional/api/v2/test_quotas.py index 0d0de4a74c..5d037e6cc5 100644 --- a/octavia/tests/functional/api/v2/test_quotas.py +++ b/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 diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 7892e7b1b2..d1814fbe93 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/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_session = db_api.get_session(autocommit=False) self.assertRaises( @@ -761,7 +789,43 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 10, 'health_monitor': 10, - 'member': 0} + 'member': 0, + 'l7policy': 10, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb1')) + + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 0, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb1')) + + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 10, + 'l7rule': 0} self.repos.quotas.update(self.session, project_id, quota=quota) lock_session = db_api.get_session(autocommit=False) self.assertRaises( @@ -777,7 +841,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 1, '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( @@ -793,7 +859,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 10, 'health_monitor': 1, - '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( @@ -809,7 +877,45 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 10, 'health_monitor': 10, - 'member': 1} + 'member': 1, + 'l7policy': 10, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb1')) + + # Test quota for l7policy + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 1, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb1')) + + # Test quota for l7rule + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 10, + 'l7rule': 1} self.repos.quotas.update(self.session, project_id, quota=quota) lock_session = db_api.get_session(autocommit=False) self.assertRaises( @@ -826,7 +932,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.repos.create_load_balancer_tree(self.session, lock_session, @@ -851,7 +959,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 2, '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( @@ -869,7 +979,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 2, '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( @@ -887,7 +999,9 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 10, 'health_monitor': 1, - '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( @@ -905,7 +1019,49 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): 'listener': 10, 'pool': 10, 'health_monitor': 10, - 'member': 2} + 'member': 2, + 'l7policy': 10, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb2)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb2')) + + # ### Test l7policy quota + # Create with custom quotas and limit to two l7policy (lb has two), + # expect error of too many l7policy/over quota + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 2, + 'l7rule': 10} + self.repos.quotas.update(self.session, project_id, quota=quota) + lock_session = db_api.get_session(autocommit=False) + self.assertRaises( + exceptions.QuotaException, + self.repos.create_load_balancer_tree, + self.session, lock_session, copy.deepcopy(lb2)) + # Make sure we didn't create the load balancer anyway + self.assertIsNone(self.repos.load_balancer.get(self.session, + name='lb2')) + + # ### Test l7rule quota + # Create with custom quotas and limit to two l7rule (lb has two), + # expect error of too many l7rule/over quota + quota = {'load_balancer': 10, + 'listener': 10, + 'pool': 10, + 'health_monitor': 10, + 'member': 10, + 'l7policy': 10, + 'l7rule': 2} self.repos.quotas.update(self.session, project_id, quota=quota) lock_session = db_api.get_session(autocommit=False) self.assertRaises( @@ -1665,6 +1821,314 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): self.assertEqual(2, self.repos.quotas.get( self.session, project_id=project_id).in_use_member) + # ### Test l7policy quota + # Test with no pre-existing quota record default 0 + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=0) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertIsNone(self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test with no pre-existing quota record default 1 + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=1) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + # Test above project is now at quota + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test with no pre-existing quota record default unlimited + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', + default_l7policy_quota=constants.QUOTA_UNLIMITED) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + # Test above project adding another l7policy + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(2, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test upgrade case with pre-quota l7policy + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=1) + lb = self.repos.load_balancer.create( + self.session, id=uuidutils.generate_uuid(), + project_id=project_id, name="lb_name", + description="lb_description", + provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, + enabled=True) + listener = self.repos.listener.create( + self.session, protocol=constants.PROTOCOL_HTTP, protocol_port=80, + enabled=True, provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, project_id=project_id, + load_balancer_id=lb.id) + self.repos.l7policy.create( + self.session, name='l7policy', enabled=True, position=1, + action=constants.L7POLICY_ACTION_REJECT, + provisioning_status=constants.ACTIVE, listener_id=listener.id, + operating_status=constants.ONLINE, project_id=project_id, + id=uuidutils.generate_uuid()) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + + # Test upgrade case with pre-quota deleted l7policy + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=1) + lb = self.repos.load_balancer.create( + self.session, id=uuidutils.generate_uuid(), + project_id=project_id, name="lb_name", + description="lb_description", + provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, + enabled=True) + listener = self.repos.listener.create( + self.session, protocol=constants.PROTOCOL_HTTP, protocol_port=80, + enabled=True, provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, project_id=project_id, + load_balancer_id=lb.id) + self.repos.l7policy.create( + self.session, name='l7policy', enabled=True, position=1, + action=constants.L7POLICY_ACTION_REJECT, + provisioning_status=constants.DELETED, listener_id=listener.id, + operating_status=constants.ONLINE, project_id=project_id, + id=uuidutils.generate_uuid()) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test pre-existing quota with quota of zero + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=10) + quota = {'l7policy': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + + # Test pre-existing quota with quota of one + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=0) + quota = {'l7policy': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + # Test above project is now at quota + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test pre-existing quota with quota of unlimited + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=0) + quota = {'l7policy': constants.QUOTA_UNLIMITED} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + # Test above project adding another l7policy + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Policy, + project_id)) + self.assertEqual(2, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # ### Test l7rule quota + # Test with no pre-existing quota record default 0 + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=0) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertIsNone(self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test with no pre-existing quota record default 1 + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=1) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + # Test above project is now at quota + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test with no pre-existing quota record default unlimited + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', + default_l7rule_quota=constants.QUOTA_UNLIMITED) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + # Test above project adding another l7rule + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(2, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test upgrade case with pre-quota l7rule + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=1) + lb = self.repos.load_balancer.create( + self.session, id=uuidutils.generate_uuid(), + project_id=project_id, name="lb_name", + description="lb_description", + provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, + enabled=True) + listener = self.repos.listener.create( + self.session, protocol=constants.PROTOCOL_HTTP, protocol_port=80, + enabled=True, provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, project_id=project_id, + load_balancer_id=lb.id) + l7policy = self.repos.l7policy.create( + self.session, name='l7policy', enabled=True, position=1, + action=constants.L7POLICY_ACTION_REJECT, + provisioning_status=constants.ACTIVE, listener_id=listener.id, + operating_status=constants.ONLINE, project_id=project_id, + id=uuidutils.generate_uuid()) + self.repos.l7rule.create( + self.session, id=uuidutils.generate_uuid(), + l7policy_id=l7policy.id, type=constants.L7RULE_TYPE_HOST_NAME, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, enabled=True, + provisioning_status=constants.ACTIVE, value='hostname', + operating_status=constants.ONLINE, project_id=project_id) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + + # Test upgrade case with pre-quota deleted l7rule + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7policy_quota=1) + lb = self.repos.load_balancer.create( + self.session, id=uuidutils.generate_uuid(), + project_id=project_id, name="lb_name", + description="lb_description", + provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, + enabled=True) + listener = self.repos.listener.create( + self.session, protocol=constants.PROTOCOL_HTTP, protocol_port=80, + enabled=True, provisioning_status=constants.ACTIVE, + operating_status=constants.ONLINE, project_id=project_id, + load_balancer_id=lb.id) + l7policy = self.repos.l7policy.create( + self.session, name='l7policy', enabled=True, position=1, + action=constants.L7POLICY_ACTION_REJECT, + provisioning_status=constants.ACTIVE, listener_id=listener.id, + operating_status=constants.ONLINE, project_id=project_id, + id=uuidutils.generate_uuid()) + self.repos.l7rule.create( + self.session, id=uuidutils.generate_uuid(), + l7policy_id=l7policy.id, type=constants.L7RULE_TYPE_HOST_NAME, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, enabled=True, + provisioning_status=constants.DELETED, value='hostname', + operating_status=constants.ONLINE, project_id=project_id) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test pre-existing quota with quota of zero + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=10) + quota = {'l7rule': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + + # Test pre-existing quota with quota of one + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=0) + quota = {'l7rule': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + # Test above project is now at quota + self.assertTrue(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test pre-existing quota with quota of unlimited + project_id = uuidutils.generate_uuid() + conf.config(group='quotas', default_l7rule_quota=0) + quota = {'l7rule': constants.QUOTA_UNLIMITED} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(1, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + # Test above project adding another l7rule + self.assertFalse(self.repos.check_quota_met(self.session, + self.session, + models.L7Rule, + project_id)) + self.assertEqual(2, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + def test_decrement_quota(self): # Test decrement on non-existent quota with noauth project_id = uuidutils.generate_uuid() @@ -1929,6 +2393,99 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): self.assertEqual(0, self.repos.quotas.get( self.session, project_id=project_id).in_use_member) conf.config(group='api_settings', auth_strategy=constants.TESTING) + # ### Test l7policy quota + # Test decrement on zero in use quota + project_id = uuidutils.generate_uuid() + quota = {'in_use_l7policy': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Policy, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test decrement on zero in use quota with noauth + project_id = uuidutils.generate_uuid() + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + conf.config(group='api_settings', auth_strategy=constants.NOAUTH) + quota = {'in_use_l7policy': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Policy, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + conf.config(group='api_settings', auth_strategy=constants.TESTING) + + # Test decrement on in use quota + project_id = uuidutils.generate_uuid() + quota = {'in_use_l7policy': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Policy, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + + # Test decrement on in use quota with noauth + project_id = uuidutils.generate_uuid() + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + conf.config(group='api_settings', auth_strategy=constants.NOAUTH) + quota = {'in_use_l7policy': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Policy, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7policy) + conf.config(group='api_settings', auth_strategy=constants.TESTING) + + # ### Test l7rule quota + # Test decrement on zero in use quota + project_id = uuidutils.generate_uuid() + quota = {'in_use_l7rule': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Rule, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test decrement on zero in use quota with noauth + project_id = uuidutils.generate_uuid() + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + conf.config(group='api_settings', auth_strategy=constants.NOAUTH) + quota = {'in_use_l7rule': 0} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Rule, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + conf.config(group='api_settings', auth_strategy=constants.TESTING) + + # Test decrement on in use quota + project_id = uuidutils.generate_uuid() + quota = {'in_use_l7rule': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Rule, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + + # Test decrement on in use quota with noauth + project_id = uuidutils.generate_uuid() + conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) + conf.config(group='api_settings', auth_strategy=constants.NOAUTH) + quota = {'in_use_l7rule': 1} + self.repos.quotas.update(self.session, project_id, quota=quota) + self.repos.decrement_quota(self.session, + models.L7Rule, + project_id) + self.assertEqual(0, self.repos.quotas.get( + self.session, project_id=project_id).in_use_l7rule) + conf.config(group='api_settings', auth_strategy=constants.TESTING) def test_get_amphora_stats(self): listener2_id = uuidutils.generate_uuid() @@ -4337,12 +4894,14 @@ class TestQuotasRepository(BaseRepositoryTest): super(TestQuotasRepository, self).setUp() def update_quotas(self, project_id, load_balancer=20, listener=20, pool=20, - health_monitor=20, member=20): + health_monitor=20, member=20, l7policy=20, l7rule=20): quota = {'load_balancer': load_balancer, 'listener': listener, 'pool': pool, 'health_monitor': health_monitor, - 'member': member} + 'member': member, + 'l7policy': l7policy, + 'l7rule': l7rule} quotas = self.quota_repo.update(self.session, project_id, quota=quota) return quotas @@ -4358,6 +4917,10 @@ class TestQuotasRepository(BaseRepositoryTest): observed.health_monitor) self.assertEqual(expected.member, observed.member) + self.assertEqual(expected.l7policy, + observed.l7policy) + self.assertEqual(expected.l7rule, + observed.l7rule) def test_get(self): expected = self.update_quotas(self.FAKE_UUID_1) @@ -4394,6 +4957,8 @@ class TestQuotasRepository(BaseRepositoryTest): self.assertIsNone(observed.listener) self.assertIsNone(observed.member) self.assertIsNone(observed.pool) + self.assertIsNone(observed.l7policy) + self.assertIsNone(observed.l7rule) def test_delete_non_existent(self): self.assertRaises(exceptions.NotFound, diff --git a/octavia/tests/unit/api/v2/types/test_quotas.py b/octavia/tests/unit/api/v2/types/test_quotas.py new file mode 100644 index 0000000000..bdacfc2c97 --- /dev/null +++ b/octavia/tests/unit/api/v2/types/test_quotas.py @@ -0,0 +1,87 @@ +# 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. + +from wsme import exc +from wsme.rest import json as wsme_json +from wsme import types as wsme_types + +from octavia.api.v2.types import quotas as quota_type +from octavia.common import constants +from octavia.tests.unit.api.v2.types import base + + +class TestQuotaPut(base.BaseTypesTest): + + _type = quota_type.QuotaPUT + + def test_quota(self): + body = {'quota': {'loadbalancer': 5}} + quota = wsme_json.fromjson(self._type, body) + self.assertEqual(wsme_types.Unset, quota.quota.listener) + self.assertEqual(wsme_types.Unset, quota.quota.pool) + self.assertEqual(wsme_types.Unset, quota.quota.member) + self.assertEqual(wsme_types.Unset, quota.quota.healthmonitor) + self.assertEqual(wsme_types.Unset, quota.quota.l7policy) + self.assertEqual(wsme_types.Unset, quota.quota.l7rule) + + def test_invalid_quota(self): + body = {'quota': {'loadbalancer': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'loadbalancer': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'listener': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'listener': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'pool': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'pool': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'member': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'member': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'healthmonitor': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'healthmonitor': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'l7policy': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'l7policy': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + + body = {'quota': {'l7rule': constants.MAX_QUOTA + 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) + body = {'quota': {'l7rule': constants.MIN_QUOTA - 1}} + self.assertRaises(exc.InvalidInput, wsme_json.fromjson, + self._type, body) diff --git a/octavia/tests/unit/common/test_data_models.py b/octavia/tests/unit/common/test_data_models.py index b738269497..1700cd9982 100644 --- a/octavia/tests/unit/common/test_data_models.py +++ b/octavia/tests/unit/common/test_data_models.py @@ -118,6 +118,24 @@ class TestDataModels(base.TestCase): compute_flavor=self.COMPUTE_FLAVOR ) + self.QUOTA_obj = data_models.Quotas( + project_id=self.PROJECT_ID, + load_balancer=None, + listener=None, + 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_l7policy=None, + in_use_l7rule=None + ) + super(TestDataModels, self).setUp() def test_LoadBalancer_update(self): @@ -417,3 +435,47 @@ class TestDataModels(base.TestCase): test_Amp_obj.update(update_dict) self.assertEqual(reference_Amp_obj, test_Amp_obj) + + def test_Quota_update(self): + + new_loadbalancer_quota = 10 + new_listener_quota = 11 + new_pool_quota = 12 + new_healthmonitor_quota = 13 + new_member_quota = 14 + new_l7policy_quota = 15 + new_l7rule_quota = 16 + + update_dict = { + 'load_balancer': new_loadbalancer_quota, + 'listener': new_listener_quota, + 'pool': new_pool_quota, + 'health_monitor': new_healthmonitor_quota, + 'member': new_member_quota, + 'l7policy': new_l7policy_quota, + 'l7rule': new_l7rule_quota + } + + test_Quota_obj = copy.deepcopy(self.QUOTA_obj) + + reference_Quota_obj = data_models.Quotas( + project_id=self.PROJECT_ID, + load_balancer=new_loadbalancer_quota, + listener=new_listener_quota, + pool=new_pool_quota, + health_monitor=new_healthmonitor_quota, + member=new_member_quota, + l7policy=new_l7policy_quota, + l7rule=new_l7rule_quota, + in_use_health_monitor=None, + in_use_listener=None, + in_use_load_balancer=None, + in_use_member=None, + in_use_pool=None, + in_use_l7policy=None, + in_use_l7rule=None + ) + + test_Quota_obj.update(update_dict) + + self.assertEqual(reference_Quota_obj, test_Quota_obj) diff --git a/octavia/tests/unit/controller/worker/v1/tasks/test_database_tasks_quota.py b/octavia/tests/unit/controller/worker/v1/tasks/test_database_tasks_quota.py index 999c9389ee..a68917301e 100644 --- a/octavia/tests/unit/controller/worker/v1/tasks/test_database_tasks_quota.py +++ b/octavia/tests/unit/controller/worker/v1/tasks/test_database_tasks_quota.py @@ -56,6 +56,8 @@ class TestDatabaseTasksQuota(base.TestCase): if data_model == data_models.Pool: task.execute(test_object, self.zero_pool_child_count) else: + if data_model == data_models.L7Policy: + test_object.l7rules = [] task.execute(test_object) mock_decrement_quota.assert_called_once_with( @@ -95,6 +97,8 @@ class TestDatabaseTasksQuota(base.TestCase): self.zero_pool_child_count, self._tf_failure_mock) else: + if data_model == data_models.L7Policy: + test_object.l7rules = [] task.revert(test_object, self._tf_failure_mock) self.assertFalse(mock_get_session.called) self.assertFalse(mock_check_quota_met.called) @@ -320,3 +324,92 @@ class TestDatabaseTasksQuota(base.TestCase): result = task.execute(pool_hm_2_mem) self.assertEqual({'HM': 1, 'member': 2}, result) + + def test_decrement_l7policy_quota(self): + task = database_tasks.DecrementL7policyQuota() + data_model = data_models.L7Policy + self._test_decrement_quota(task, data_model) + + @mock.patch('octavia.db.repositories.Repositories.decrement_quota') + @mock.patch('octavia.db.repositories.Repositories.check_quota_met') + def test_decrement_l7policy_quota_with_children(self, + mock_check_quota_met, + mock_decrement_quota): + project_id = uuidutils.generate_uuid() + test_l7rule1 = mock.MagicMock() + test_l7rule1.project_id = project_id + test_l7rule2 = mock.MagicMock() + test_l7rule2.project_id = project_id + test_object = mock.MagicMock() + test_object.project_id = project_id + test_object.l7rules = [test_l7rule1, test_l7rule2] + task = database_tasks.DecrementL7policyQuota() + mock_session = mock.MagicMock() + + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_get_session_local.return_value = mock_session + + task.execute(test_object) + + calls = [mock.call(mock_session, data_models.L7Policy, project_id), + mock.call(mock_session, data_models.L7Rule, project_id, + quantity=2)] + + mock_decrement_quota.assert_has_calls(calls) + + mock_session.commit.assert_called_once_with() + + # revert + mock_session.reset_mock() + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_lock_session = mock.MagicMock() + mock_get_session_local.side_effect = [mock_session, + mock_lock_session, + mock_lock_session, + mock_lock_session] + + task.revert(test_object, None) + + calls = [mock.call(mock_session, mock_lock_session, + data_models.L7Policy, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id)] + + mock_check_quota_met.assert_has_calls(calls) + + self.assertEqual(3, mock_lock_session.commit.call_count) + + # revert with l7rule quota exception + mock_session.reset_mock() + mock_check_quota_met.side_effect = [None, None, + Exception('fail')] + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_lock_session = mock.MagicMock() + mock_get_session_local.side_effect = [mock_session, + mock_lock_session, + mock_lock_session, + mock_lock_session] + + task.revert(test_object, None) + + calls = [mock.call(mock_session, mock_lock_session, + data_models.L7Policy, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id)] + + mock_check_quota_met.assert_has_calls(calls) + + self.assertEqual(2, mock_lock_session.commit.call_count) + self.assertEqual(1, mock_lock_session.rollback.call_count) + + def test_decrement_l7rule_quota(self): + task = database_tasks.DecrementL7ruleQuota() + data_model = data_models.L7Rule + self._test_decrement_quota(task, data_model) diff --git a/octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py b/octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py index 6637120c7b..798c23fab7 100644 --- a/octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py +++ b/octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks_quota.py @@ -53,6 +53,9 @@ class TestDatabaseTasksQuota(base.TestCase): mock_session = mock.MagicMock() mock_get_session_local.return_value = mock_session + if data_model == data_models.L7Policy: + test_object.l7rules = [] + if data_model == data_models.Pool: task.execute(test_object, self.zero_pool_child_count) else: @@ -338,3 +341,92 @@ class TestDatabaseTasksQuota(base.TestCase): result = task.execute(pool_hm_2_mem.id) self.assertEqual({'HM': 1, 'member': 2}, result) + + def test_decrement_l7policy_quota(self): + task = database_tasks.DecrementL7policyQuota() + data_model = data_models.L7Policy + self._test_decrement_quota(task, data_model) + + @mock.patch('octavia.db.repositories.Repositories.decrement_quota') + @mock.patch('octavia.db.repositories.Repositories.check_quota_met') + def test_decrement_l7policy_quota_with_children(self, + mock_check_quota_met, + mock_decrement_quota): + project_id = uuidutils.generate_uuid() + test_l7rule1 = mock.MagicMock() + test_l7rule1.project_id = project_id + test_l7rule2 = mock.MagicMock() + test_l7rule2.project_id = project_id + test_object = mock.MagicMock() + test_object.project_id = project_id + test_object.l7rules = [test_l7rule1, test_l7rule2] + task = database_tasks.DecrementL7policyQuota() + mock_session = mock.MagicMock() + + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_get_session_local.return_value = mock_session + + task.execute(test_object) + + calls = [mock.call(mock_session, data_models.L7Policy, project_id), + mock.call(mock_session, data_models.L7Rule, project_id, + quantity=2)] + + mock_decrement_quota.assert_has_calls(calls) + + mock_session.commit.assert_called_once_with() + + # revert + mock_session.reset_mock() + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_lock_session = mock.MagicMock() + mock_get_session_local.side_effect = [mock_session, + mock_lock_session, + mock_lock_session, + mock_lock_session] + + task.revert(test_object, None) + + calls = [mock.call(mock_session, mock_lock_session, + data_models.L7Policy, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id)] + + mock_check_quota_met.assert_has_calls(calls) + + self.assertEqual(3, mock_lock_session.commit.call_count) + + # revert with l7rule quota exception + mock_session.reset_mock() + mock_check_quota_met.side_effect = [None, None, + Exception('fail')] + with mock.patch('octavia.db.api.' + 'get_session') as mock_get_session_local: + mock_lock_session = mock.MagicMock() + mock_get_session_local.side_effect = [mock_session, + mock_lock_session, + mock_lock_session, + mock_lock_session] + + task.revert(test_object, None) + + calls = [mock.call(mock_session, mock_lock_session, + data_models.L7Policy, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id), + mock.call(mock_session, mock_lock_session, + data_models.L7Rule, project_id)] + + mock_check_quota_met.assert_has_calls(calls) + + self.assertEqual(2, mock_lock_session.commit.call_count) + self.assertEqual(1, mock_lock_session.rollback.call_count) + + def test_decrement_l7rule_quota(self): + task = database_tasks.DecrementL7ruleQuota() + data_model = data_models.L7Rule + self._test_decrement_quota(task, data_model) diff --git a/releasenotes/notes/add-l7policy-and-l7rule-to-quota-4b873c77f1e608e6.yaml b/releasenotes/notes/add-l7policy-and-l7rule-to-quota-4b873c77f1e608e6.yaml new file mode 100644 index 0000000000..0988ef5520 --- /dev/null +++ b/releasenotes/notes/add-l7policy-and-l7rule-to-quota-4b873c77f1e608e6.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add l7policy and l7rule to octavia quota.