diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 631c6f882a..3b971e5b5b 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -78,6 +78,34 @@ SUPPORTED_OPERATING_STATUSES = (ONLINE, OFFLINE, DEGRADED, ERROR, NO_MONITOR) AMPHORA_VM = 'VM' SUPPORTED_AMPHORA_TYPES = (AMPHORA_VM,) +# L7 constants +L7RULE_TYPE_HOST_NAME = 'HOST_NAME' +L7RULE_TYPE_PATH = 'PATH' +L7RULE_TYPE_FILE_TYPE = 'FILE_TYPE' +L7RULE_TYPE_HEADER = 'HEADER' +L7RULE_TYPE_COOKIE = 'COOKIE' +SUPPORTED_L7RULE_TYPES = (L7RULE_TYPE_HOST_NAME, L7RULE_TYPE_PATH, + L7RULE_TYPE_FILE_TYPE, L7RULE_TYPE_HEADER, + L7RULE_TYPE_COOKIE) + +L7RULE_COMPARE_TYPE_REGEX = 'REGEX' +L7RULE_COMPARE_TYPE_STARTS_WITH = 'STARTS_WITH' +L7RULE_COMPARE_TYPE_ENDS_WITH = 'ENDS_WITH' +L7RULE_COMPARE_TYPE_CONTAINS = 'CONTAINS' +L7RULE_COMPARE_TYPE_EQUAL_TO = 'EQUAL_TO' +SUPPORTED_L7RULE_COMPARE_TYPES = (L7RULE_COMPARE_TYPE_REGEX, + L7RULE_COMPARE_TYPE_STARTS_WITH, + L7RULE_COMPARE_TYPE_ENDS_WITH, + L7RULE_COMPARE_TYPE_CONTAINS, + L7RULE_COMPARE_TYPE_EQUAL_TO) + +L7POLICY_ACTION_REJECT = 'REJECT' +L7POLICY_ACTION_REDIRECT_TO_URL = 'REDIRECT_TO_URL' +L7POLICY_ACTION_REDIRECT_TO_POOL = 'REDIRECT_TO_POOL' +SUPPORTED_L7POLICY_ACTIONS = (L7POLICY_ACTION_REJECT, + L7POLICY_ACTION_REDIRECT_TO_URL, + L7POLICY_ACTION_REDIRECT_TO_POOL) + # Task/Flow constants AMPHORA = 'amphora' FAILED_AMPHORA = 'failed_amphora' diff --git a/octavia/common/data_models.py b/octavia/common/data_models.py index 828448223c..2a1fdb4543 100644 --- a/octavia/common/data_models.py +++ b/octavia/common/data_models.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 Rackspace +# Copyright (c) 2016 Blue Box, an IBM Company # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,6 +18,8 @@ import re from sqlalchemy.orm import collections +from octavia.common import constants + class BaseDataModel(object): @@ -55,7 +58,8 @@ class BaseDataModel(object): # First handle all objects with their own ID, then handle subordinate # objects. if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer', - 'Listener', 'Amphora']: + 'Listener', 'Amphora', 'L7Policy', + 'L7Rule']: return obj.__class__.__name__ + obj.id elif obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']: return obj.__class__.__name__ + obj.pool_id @@ -169,7 +173,7 @@ class Pool(BaseDataModel): protocol=None, lb_algorithm=None, enabled=None, operating_status=None, members=None, health_monitor=None, session_persistence=None, load_balancer_id=None, - load_balancer=None, listeners=None): + load_balancer=None, listeners=None, l7policies=None): self.id = id self.project_id = project_id self.name = name @@ -184,6 +188,7 @@ class Pool(BaseDataModel): self.health_monitor = health_monitor self.session_persistence = session_persistence self.listeners = listeners or [] + self.l7policies = l7policies or [] def update(self, update_dict): for key, value in update_dict.items(): @@ -197,7 +202,6 @@ class Pool(BaseDataModel): setattr(self, key, value) def delete(self): - # TODO(sbalukoff): Clean up L7Policies that reference this pool for listener in self.listeners: if listener.default_pool_id == self.id: listener.default_pool = None @@ -210,6 +214,11 @@ class Pool(BaseDataModel): if pool.id == self.id: self.load_balancer.pools.remove(pool) break + for l7policy in self.l7policies: + if l7policy.redirect_pool_id == self.id: + l7policy.action = constants.L7POLICY_ACTION_REJECT + l7policy.redirect_pool = None + l7policy.redirect_pool_id = None class Member(BaseDataModel): @@ -243,7 +252,7 @@ class Listener(BaseDataModel): enabled=None, provisioning_status=None, operating_status=None, tls_certificate_id=None, stats=None, default_pool=None, load_balancer=None, sni_containers=None, peer_port=None, - pools=None): + l7policies=None, pools=None): self.id = id self.project_id = project_id self.name = name @@ -262,12 +271,21 @@ class Listener(BaseDataModel): self.load_balancer = load_balancer self.sni_containers = sni_containers or [] self.peer_port = peer_port + self.l7policies = l7policies or [] self.pools = pools or [] def update(self, update_dict): for key, value in update_dict.items(): setattr(self, key, value) if key == 'default_pool_id': + if self.default_pool is not None: + l7_pool_ids = [p.redirect_pool_id for p in self.l7policies + if p.redirect_pool_id is not None and + len(p.l7rules) > 0 and p.enabled is True] + old_pool = self.default_pool + if old_pool.id not in l7_pool_ids: + self.pools.remove(old_pool) + old_pool.listeners.remove(self) if value is not None: pool = self._find_in_graph('Pool' + value) if pool not in self.pools: @@ -393,3 +411,126 @@ class AmphoraHealth(BaseDataModel): self.amphora_id = amphora_id self.last_update = last_update self.busy = busy + + +class L7Rule(BaseDataModel): + + def __init__(self, id=None, l7policy_id=None, type=None, + compare_type=None, key=None, value=None, l7policy=None, + invert=False): + self.id = id + self.l7policy_id = l7policy_id + self.type = type + self.compare_type = compare_type + self.key = key + self.value = value + self.l7policy = l7policy + self.invert = invert + + def delete(self): + if len(self.l7policy.l7rules) == 1: + # l7policy should disappear from pool and listener lists. Since + # we are operating only on the data model, we can fake this by + # calling the policy's delete method. + self.l7policy.delete() + for r in self.l7policy.l7rules: + if r.id == self.id: + self.l7policy.l7rules.remove(r) + break + + +class L7Policy(BaseDataModel): + + def __init__(self, id=None, name=None, description=None, listener_id=None, + action=None, redirect_pool_id=None, redirect_url=None, + position=None, listener=None, redirect_pool=None, + enabled=None, l7rules=None): + self.id = id + self.name = name + self.description = description + self.listener_id = listener_id + self.action = action + self.redirect_pool_id = redirect_pool_id + self.redirect_url = redirect_url + self.position = position + self.listener = listener + self.redirect_pool = redirect_pool + self.enabled = enabled + self.l7rules = l7rules or [] + + def _conditionally_remove_pool_links(self, pool): + """Removes links to the given pool from parent objects. + + Note this only happens if our listener isn't referencing the pool + via its default_pool or another active l7policy's redirect_pool_id. + """ + if (self.listener.default_pool is not None and + pool is not None and + pool.id != self.listener.default_pool.id and + pool in self.listener.pools): + listener_l7pools = [ + p.redirect_pool for p in self.listener.l7policies + if p.redirect_pool is not None and + len(p.l7rules) > 0 and p.enabled is True and + p.id != self.id] + if pool not in listener_l7pools: + self.listener.pools.remove(pool) + pool.listeners.remove(self.listener) + + def update(self, update_dict): + for key, value in update_dict.items(): + if key == 'redirect_pool_id': + self._conditionally_remove_pool_links(self.redirect_pool) + self.action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL + self.redirect_url = None + pool = self._find_in_graph('Pool' + value) + self.redirect_pool = pool + if len(self.l7rules) > 0 and (self.enabled is True or + ('enabled' in update_dict.keys() + and update_dict['enabled'] + is True)): + if pool not in self.listener.pools: + self.listener.pools.append(pool) + if self.listener not in pool.listeners: + pool.listeners.append(self.listener) + elif key == 'redirect_url': + self.action = constants.L7POLICY_ACTION_REDIRECT_TO_URL + self._conditionally_remove_pool_links(self.redirect_pool) + self.redirect_pool = None + self.redirect_pool_id = None + elif key == 'action' and value == constants.L7POLICY_ACTION_REJECT: + self.redirect_url = None + self._conditionally_remove_pool_links(self.redirect_pool) + self.redirect_pool = None + self.redirect_pool_id = None + elif key == 'position': + self.listener.l7policies.remove(self) + self.listener.l7policies.insert(value - 1, self) + elif (key == 'enabled' + and (self.action == + constants.L7POLICY_ACTION_REDIRECT_TO_POOL + or ('action' in update_dict.keys() + and update_dict['action'] == + constants.L7POLICY_ACTION_REDIRECT_TO_POOL)) + and (self.redirect_pool is not None + or ('redirect_pool_id' in update_dict.keys() and + self._find_in_graph( + 'Pool' + update_dict['redirect_pool_id']) + is not None))): + if self.redirect_pool is None: + self.redirect_pool = self._find_in_graph( + 'Pool' + update_dict['redirect_pool_id']) + self.listener.pools.append(self.redirect_pool) + self.redirect_pool.listeners.append(self.listener) + setattr(self, key, value) + + def delete(self): + self._conditionally_remove_pool_links(self.redirect_pool) + if self.redirect_pool: + for p in self.redirect_pool.l7policies: + if p.id == self.id: + self.redirect_pool.l7policies.remove(p) + for p in self.listener.l7policies: + if p.id == self.id: + self.listener.l7policies.remove(p) + break diff --git a/octavia/db/base_models.py b/octavia/db/base_models.py index e532cdbc2d..f15bd00ed6 100644 --- a/octavia/db/base_models.py +++ b/octavia/db/base_models.py @@ -29,7 +29,8 @@ class OctaviaBase(models.ModelBase): # First handle all objects with their own ID, then handle subordinate # objects. if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer', - 'Listener', 'Amphora']: + 'Listener', 'Amphora', 'L7Policy', + 'L7Rule']: return obj.__class__.__name__ + obj.id elif obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']: return obj.__class__.__name__ + obj.pool_id diff --git a/octavia/db/migration/alembic_migrations/versions/458c9ee2a011_l7_policies_and_rules.py b/octavia/db/migration/alembic_migrations/versions/458c9ee2a011_l7_policies_and_rules.py new file mode 100644 index 0000000000..5d07cf5320 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/458c9ee2a011_l7_policies_and_rules.py @@ -0,0 +1,149 @@ +# Copyright 2015 Blue Box, an IBM Company +# +# 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. + +"""L7 Policies and Rules + +Revision ID: 458c9ee2a011 +Revises: 29ff921a6eb +Create Date: 2016-01-07 11:45:45.391851 + +""" + +# revision identifiers, used by Alembic. +revision = '458c9ee2a011' +down_revision = '29ff921a6eb' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import sql + + +def upgrade(): + # L7 Rule Types + op.create_table( + u'l7rule_type', + sa.Column(u'name', sa.String(36), primary_key=True), + sa.Column(u'description', sa.String(255), nullable=True) + ) + + # Create temporary table for table data seeding + insert_table = sql.table( + u'l7rule_type', + sql.column(u'name', sa.String), + sql.column(u'description', sa.String) + ) + + op.bulk_insert( + insert_table, + [ + {'name': 'HOST_NAME'}, + {'name': 'PATH'}, + {'name': 'FILE_TYPE'}, + {'name': 'HEADER'}, + {'name': 'COOKIE'} + ] + ) + + # L7 Rule Compare Types + op.create_table( + u'l7rule_compare_type', + sa.Column(u'name', sa.String(36), primary_key=True), + sa.Column(u'description', sa.String(255), nullable=True) + ) + + insert_table = sql.table( + u'l7rule_compare_type', + sql.column(u'name', sa.String), + sql.column(u'description', sa.String) + ) + + op.bulk_insert( + insert_table, + [ + {'name': 'REGEX'}, + {'name': 'STARTS_WITH'}, + {'name': 'ENDS_WITH'}, + {'name': 'CONTAINS'}, + {'name': 'EQUAL_TO'} + ] + ) + + # L7 Policy Actions + op.create_table( + u'l7policy_action', + sa.Column(u'name', sa.String(36), primary_key=True), + sa.Column(u'description', sa.String(255), nullable=True) + ) + + insert_table = sql.table( + u'l7policy_action', + sql.column(u'name', sa.String), + sql.column(u'description', sa.String) + ) + + op.bulk_insert( + insert_table, + [ + {'name': 'REJECT'}, + {'name': 'REDIRECT_TO_URL'}, + {'name': 'REDIRECT_TO_POOL'} + ] + ) + + # L7 Policies + op.create_table( + u'l7policy', + sa.Column(u'id', sa.String(36), nullable=False), + sa.Column(u'name', sa.String(255), nullable=True), + sa.Column(u'description', sa.String(255), nullable=True), + sa.Column(u'listener_id', sa.String(36), nullable=False), + sa.Column(u'action', sa.String(36), nullable=False), + sa.Column(u'redirect_pool_id', sa.String(36), nullable=True), + sa.Column(u'redirect_url', sa.String(255), nullable=True), + sa.Column(u'position', sa.Integer, nullable=False), + sa.Column(u'enabled', sa.Boolean(), default=True, nullable=False), + + sa.PrimaryKeyConstraint(u'id'), + sa.ForeignKeyConstraint([u'listener_id'], + [u'listener.id'], + name=u'fk_l7policy_listener_id'), + sa.ForeignKeyConstraint([u'redirect_pool_id'], + [u'pool.id'], + name=u'fk_l7policy_pool_id'), + sa.ForeignKeyConstraint([u'action'], + [u'l7policy_action.name'], + name=u'fk_l7policy_l7policy_action_name') + ) + + # L7 Rules + op.create_table( + u'l7rule', + sa.Column(u'id', sa.String(36), nullable=False), + sa.Column(u'l7policy_id', sa.String(36), nullable=False), + sa.Column(u'type', sa.String(36), nullable=False), + sa.Column(u'compare_type', sa.String(36), nullable=False), + sa.Column(u'key', sa.String(255), nullable=True), + sa.Column(u'value', sa.String(255), nullable=False), + sa.Column(u'invert', sa.Boolean(), default=False, nullable=False), + sa.PrimaryKeyConstraint(u'id'), + sa.ForeignKeyConstraint([u'l7policy_id'], + [u'l7policy.id'], + name=u'fk_l7rule_l7policy_id'), + sa.ForeignKeyConstraint([u'type'], + [u'l7rule_type.name'], + name=u'fk_l7rule_l7rule_type_name'), + sa.ForeignKeyConstraint([u'compare_type'], + [u'l7rule_compare_type.name'], + name=u'fk_l7rule_l7rule_compare_type_name') + ) diff --git a/octavia/db/models.py b/octavia/db/models.py index 07492ddc1f..920dd83fcf 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -1,4 +1,5 @@ # Copyright 2014 Rackspace +# Copyright 2016 Blue Box, an IBM Company # # 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 @@ -14,6 +15,7 @@ import sqlalchemy as sa +from sqlalchemy.ext import orderinglist from sqlalchemy import orm from sqlalchemy.orm import validates from sqlalchemy.sql import func @@ -67,6 +69,21 @@ class VRRPAuthMethod(base_models.BASE, base_models.LookupTableMixin): __tablename__ = "vrrp_auth_method" +class L7RuleType(base_models.BASE, base_models.LookupTableMixin): + + __tablename__ = "l7rule_type" + + +class L7RuleCompareType(base_models.BASE, base_models.LookupTableMixin): + + __tablename__ = "l7rule_compare_type" + + +class L7PolicyAction(base_models.BASE, base_models.LookupTableMixin): + + __tablename__ = "l7policy_action" + + class SessionPersistence(base_models.BASE): __data_model__ = data_models.SessionPersistence @@ -215,12 +232,22 @@ class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin): uselist=True, cascade="delete")) - # Defining this as a custom method instead of an SQLAlchemy relationship - # for now. When L7 gets added, this list will also include any listeners - # referenced by enabled L7policies + # This property should be a unique list of any listeners that reference + # this pool as its default_pool and any listeners referenced by enabled + # L7Policies with at least one l7rule which also reference this pool. The + # intent is that pool.listeners should be a unique list of listeners + # *actually* using the pool. @property def listeners(self): - return self._default_listeners + _listeners = self._default_listeners[:] + _l_ids = [l.id for l in _listeners] + l7_listeners = [p.listener for p in self.l7policies + if len(p.l7rules) > 0 and p.enabled is True] + for l in l7_listeners: + if l.id not in _l_ids: + _listeners.append(l) + _l_ids.append(l.id) + return _listeners class LoadBalancer(base_models.BASE, base_models.IdMixin, @@ -338,22 +365,29 @@ class Listener(base_models.BASE, base_models.IdMixin, backref=orm.backref("listeners", uselist=True, cascade="delete")) - # _default_listeners backref is used to generate part of pool.listeners - # list. default_pool = orm.relationship("Pool", uselist=False, backref=orm.backref("_default_listeners", uselist=True)) peer_port = sa.Column(sa.Integer(), nullable=True) - # Defining this as a custom method instead of an SQLAlchemy relationship - # for now. When L7 gets added, this list will also include any pools - # referenced by enabled L7policies + # This property should be a unique list of the default_pool and anything + # referenced by enabled L7Policies with at least one rule that also + # reference this listener. The intent is that listener.pools should be a + # unique list of pools this listener is *actually* using. @property def pools(self): _pools = [] - _p_ids = [p.id for p in _pools] - if self.default_pool and self.default_pool.id not in _p_ids: + _p_ids = [] + if self.default_pool: _pools.append(self.default_pool) + _p_ids.append(self.default_pool.id) + l7_pools = [p.redirect_pool for p in self.l7policies + if p.redirect_pool is not None and len(p.l7rules) > 0 and + p.enabled is True] + for p in l7_pools: + if p.id not in _p_ids: + _pools.append(p) + _p_ids.append(p.id) return _pools @@ -421,3 +455,75 @@ class AmphoraHealth(base_models.BASE): nullable=False) busy = sa.Column(sa.Boolean(), default=False, nullable=False) + + +class L7Rule(base_models.BASE, base_models.IdMixin): + + __data_model__ = data_models.L7Rule + + __tablename__ = "l7rule" + + l7policy_id = sa.Column( + sa.String(36), + sa.ForeignKey("l7policy.id", name="fk_l7rule_l7policy_id"), + nullable=False) + type = sa.Column( + sa.String(36), + sa.ForeignKey( + "l7rule_type.name", + name="fk_l7rule_l7rule_type_name"), + nullable=False) + compare_type = sa.Column( + sa.String(36), + sa.ForeignKey( + "l7rule_compare_type.name", + name="fk_l7rule_l7rule_compare_type_name"), + nullable=False) + key = sa.Column(sa.String(255), nullable=True) + value = sa.Column(sa.String(255), nullable=False) + invert = sa.Column(sa.Boolean(), default=False, nullable=False) + l7policy = orm.relationship("L7Policy", uselist=False, + backref=orm.backref("l7rules", + uselist=True, + cascade="delete")) + + +class L7Policy(base_models.BASE, base_models.IdMixin): + + __data_model__ = data_models.L7Policy + + __tablename__ = "l7policy" + + name = sa.Column(sa.String(255), nullable=True) + description = sa.Column(sa.String(255), nullable=True) + listener_id = sa.Column( + sa.String(36), + sa.ForeignKey("listener.id", name="fk_l7policy_listener_id"), + nullable=False) + action = sa.Column( + sa.String(36), + sa.ForeignKey( + "l7policy_action.name", + name="fk_l7policy_l7policy_action_name"), + nullable=False) + redirect_pool_id = sa.Column( + sa.String(36), + sa.ForeignKey("pool.id", name="fk_l7policy_pool_id"), + nullable=True) + redirect_url = sa.Column( + sa.String(255), + nullable=True) + position = sa.Column(sa.Integer, nullable=False) + enabled = sa.Column(sa.Boolean(), nullable=False) + listener = orm.relationship( + "Listener", uselist=False, + backref=orm.backref( + "l7policies", + uselist=True, + order_by="L7Policy.position", + collection_class=orderinglist.ordering_list('position', + count_from=1), + cascade="delete")) + redirect_pool = orm.relationship("Pool", uselist=False, + backref=orm.backref("l7policies", + uselist=True)) diff --git a/octavia/tests/functional/db/base.py b/octavia/tests/functional/db/base.py index 8db32a429d..bf3bc41fe2 100644 --- a/octavia/tests/functional/db/base.py +++ b/octavia/tests/functional/db/base.py @@ -71,6 +71,13 @@ class OctaviaDBTestBase(test_base.DbTestCase): models.LBTopology) self._seed_lookup_table(session, constants.SUPPORTED_VRRP_AUTH, models.VRRPAuthMethod) + self._seed_lookup_table(session, constants.SUPPORTED_L7RULE_TYPES, + models.L7RuleType) + self._seed_lookup_table(session, + constants.SUPPORTED_L7RULE_COMPARE_TYPES, + models.L7RuleCompareType) + self._seed_lookup_table(session, constants.SUPPORTED_L7POLICY_ACTIONS, + models.L7PolicyAction) def _seed_lookup_table(self, session, name_list, model_cls): for name in name_list: diff --git a/octavia/tests/functional/db/test_models.py b/octavia/tests/functional/db/test_models.py index 6baf690bdb..0da8a55c39 100644 --- a/octavia/tests/functional/db/test_models.py +++ b/octavia/tests/functional/db/test_models.py @@ -139,6 +139,24 @@ class ModelTestMixin(object): kwargs.update(overrides) return self._insert(session, models.AmphoraHealth, kwargs) + def create_l7policy(self, session, listener_id, **overrides): + kwargs = {'id': self.FAKE_UUID_1, + 'listener_id': listener_id, + 'action': constants.L7POLICY_ACTION_REJECT, + 'position': 0, + 'enabled': True} + kwargs.update(overrides) + return self._insert(session, models.L7Policy, kwargs) + + def create_l7rule(self, session, l7policy_id, **overrides): + kwargs = {'id': self.FAKE_UUID_1, + 'l7policy_id': l7policy_id, + 'type': constants.L7RULE_TYPE_PATH, + 'compare_type': constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + 'value': '/api'} + kwargs.update(overrides) + return self._insert(session, models.L7Rule, kwargs) + class PoolModelTest(base.OctaviaDBTestBase, ModelTestMixin): @@ -195,12 +213,13 @@ class PoolModelTest(base.OctaviaDBTestBase, ModelTestMixin): def test_listener_relationship(self): pool = self.create_pool(self.session) - self.create_listener(self.session, default_pool_id=pool.id) + listener = self.create_listener(self.session, default_pool_id=pool.id) new_pool = self.session.query(models.Pool).filter_by( id=pool.id).first() self.assertIsNotNone(new_pool.listeners) self.assertIsInstance(new_pool.listeners, list) self.assertIsInstance(new_pool.listeners[0], models.Listener) + self.assertIn(listener.id, [l.id for l in new_pool.listeners]) class MemberModelTest(base.OctaviaDBTestBase, ModelTestMixin): @@ -316,13 +335,15 @@ class ListenerModelTest(base.OctaviaDBTestBase, ModelTestMixin): self.assertIsNotNone(new_listener.stats) self.assertIsInstance(new_listener.stats, models.ListenerStatistics) - def test_pool_relationship(self): + def test_default_pool_relationship(self): pool = self.create_pool(self.session) listener = self.create_listener(self.session, default_pool_id=pool.id) new_listener = self.session.query(models.Listener).filter_by( id=listener.id).first() self.assertIsNotNone(new_listener.default_pool) self.assertIsInstance(new_listener.default_pool, models.Pool) + self.assertIsInstance(new_listener.pools, list) + self.assertIn(pool.id, [p.id for p in new_listener.pools]) def test_sni_relationship(self): listener = self.create_listener(self.session) @@ -335,6 +356,15 @@ class ListenerModelTest(base.OctaviaDBTestBase, ModelTestMixin): self.assertIsNotNone(new_listener.sni_containers) self.assertEqual(2, len(new_listener.sni_containers)) + def test_pools_list(self): + pool = self.create_pool(self.session) + listener = self.create_listener(self.session, default_pool_id=pool.id) + new_listener = self.session.query(models.Listener).filter_by( + id=listener.id).first() + self.assertIsNotNone(new_listener.pools) + self.assertIsInstance(new_listener.pools, list) + self.assertIsInstance(new_listener.pools[0], models.Pool) + class ListenerStatisticsModelTest(base.OctaviaDBTestBase, ModelTestMixin): @@ -586,6 +616,169 @@ class AmphoraHealthModelTest(base.OctaviaDBTestBase, ModelTestMixin): self.assertIsNone(new_amphora_health) +class L7PolicyModelTest(base.OctaviaDBTestBase, ModelTestMixin): + def setUp(self): + super(L7PolicyModelTest, self).setUp() + self.listener = self.create_listener(self.session) + + def test_create(self): + l7policy = self.create_l7policy(self.session, self.listener.id) + self.assertIsInstance(l7policy, models.L7Policy) + + def test_update(self): + l7policy = self.create_l7policy(self.session, self.listener.id) + pool = self.create_pool(self.session) + l7policy.action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL + l7policy.redirect_pool_id = pool.id + new_l7policy = self.session.query( + models.L7Policy).filter_by(id=l7policy.id).first() + self.assertEqual(pool.id, new_l7policy.redirect_pool_id) + self.assertEqual(constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + new_l7policy.action) + + def test_delete(self): + l7policy = self.create_l7policy(self.session, self.listener.id) + l7policy_id = l7policy.id + with self.session.begin(): + self.session.delete(l7policy) + self.session.flush() + new_l7policy = self.session.query( + models.L7Policy).filter_by(id=l7policy_id).first() + self.assertIsNone(new_l7policy) + + def test_l7rule_relationship(self): + l7policy = self.create_l7policy(self.session, self.listener.id) + self.create_l7rule( + self.session, l7policy.id, id=self.FAKE_UUID_1, + type=constants.L7RULE_TYPE_HOST_NAME, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + value='www.example.com') + self.create_l7rule( + self.session, l7policy.id, id=self.FAKE_UUID_2, + type=constants.L7RULE_TYPE_PATH, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + value='/api') + new_l7policy = self.session.query( + models.L7Policy).filter_by(id=l7policy.id).first() + self.assertIsNotNone(new_l7policy.l7rules) + self.assertEqual(2, len(new_l7policy.l7rules)) + self.assertIsInstance(new_l7policy.l7rules[0], models.L7Rule) + self.assertIsInstance(new_l7policy.l7rules[1], models.L7Rule) + + def test_pool_relationship(self): + l7policy = self.create_l7policy(self.session, self.listener.id) + self.create_pool(self.session, id=self.FAKE_UUID_2) + l7policy.action = constants.L7POLICY_ACTION_REDIRECT_TO_POOL + l7policy.redirect_pool_id = self.FAKE_UUID_2 + new_l7policy = self.session.query( + models.L7Policy).filter_by(id=l7policy.id).first() + self.assertIsNotNone(new_l7policy.redirect_pool) + self.assertIsInstance(new_l7policy.redirect_pool, models.Pool) + + def test_listener_relationship(self): + l7policy = self.create_l7policy(self.session, self.listener.id, + id=self.FAKE_UUID_1) + self.create_l7policy(self.session, self.listener.id, + id=self.FAKE_UUID_2, position=1) + new_l7policy = self.session.query(models.L7Policy).filter_by( + id=l7policy.id).first() + self.assertIsNotNone(new_l7policy.listener) + self.assertIsInstance(new_l7policy.listener, models.Listener) + + def test_listeners_pools_refs_with_l7policy_with_l7rule(self): + pool = self.create_pool(self.session, id=self.FAKE_UUID_2) + l7policy = self.create_l7policy( + self.session, self.listener.id, + action=constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + redirect_pool_id=pool.id) + self.create_l7rule(self.session, l7policy.id, id=self.FAKE_UUID_1) + new_pool = self.session.query(models.Pool).filter_by( + id=pool.id).first() + new_listener = self.session.query(models.Listener).filter_by( + id=self.listener.id).first() + self.assertIsInstance(new_pool.listeners, list) + self.assertIn(new_listener.id, [l.id for l in new_pool.listeners]) + self.assertIsInstance(new_listener.pools, list) + self.assertIn(new_pool.id, [p.id for p in new_listener.pools]) + + def test_listeners_pools_refs_with_l7policy_without_l7rule(self): + pool = self.create_pool(self.session, id=self.FAKE_UUID_2) + self.create_l7policy( + self.session, self.listener.id, + action=constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + redirect_pool_id=pool.id) + new_pool = self.session.query(models.Pool).filter_by( + id=pool.id).first() + new_listener = self.session.query(models.Listener).filter_by( + id=self.listener.id).first() + self.assertIsInstance(new_pool.listeners, list) + self.assertNotIn(new_listener.id, [l.id for l in new_pool.listeners]) + self.assertIsInstance(new_listener.pools, list) + self.assertNotIn(new_pool.id, [p.id for p in new_listener.pools]) + + def test_listeners_pools_refs_with_disabled_l7policy(self): + pool = self.create_pool(self.session, id=self.FAKE_UUID_2) + l7policy = self.create_l7policy( + self.session, self.listener.id, + action=constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + redirect_pool_id=pool.id, enabled=False) + self.create_l7rule(self.session, l7policy.id, id=self.FAKE_UUID_1) + new_pool = self.session.query(models.Pool).filter_by( + id=pool.id).first() + new_listener = self.session.query(models.Listener).filter_by( + id=self.listener.id).first() + self.assertIsInstance(new_pool.listeners, list) + self.assertNotIn(new_listener.id, [l.id for l in new_pool.listeners]) + self.assertIsInstance(new_listener.pools, list) + self.assertNotIn(new_pool.id, [p.id for p in new_listener.pools]) + + +class L7RuleModelTest(base.OctaviaDBTestBase, ModelTestMixin): + + def setUp(self): + super(L7RuleModelTest, self).setUp() + self.listener = self.create_listener(self.session) + self.l7policy = self.create_l7policy(self.session, self.listener.id) + + def test_create(self): + l7rule = self.create_l7rule(self.session, self.l7policy.id) + self.assertIsInstance(l7rule, models.L7Rule) + + def test_update(self): + l7rule = self.create_l7rule(self.session, self.l7policy.id) + l7rule_id = l7rule.id + l7rule.value = '/images' + new_l7rule = self.session.query( + models.L7Rule).filter_by(id=l7rule_id).first() + self.assertEqual('/images', new_l7rule.value) + + def test_delete(self): + l7rule = self.create_l7rule(self.session, self.l7policy.id) + l7rule_id = l7rule.id + with self.session.begin(): + self.session.delete(l7rule) + self.session.flush() + new_l7rule = self.session.query( + models.L7Rule).filter_by(id=l7rule_id).first() + self.assertIsNone(new_l7rule) + + def test_l7policy_relationship(self): + l7rule = self.create_l7rule( + self.session, self.l7policy.id, id=self.FAKE_UUID_1, + type=constants.L7RULE_TYPE_HOST_NAME, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + value='www.example.com') + self.create_l7rule( + self.session, self.l7policy.id, id=self.FAKE_UUID_2, + type=constants.L7RULE_TYPE_PATH, + compare_type=constants.L7RULE_COMPARE_TYPE_EQUAL_TO, + value='/api') + new_l7rule = self.session.query(models.L7Rule).filter_by( + id=l7rule.id).first() + self.assertIsNotNone(new_l7rule.l7policy) + self.assertIsInstance(new_l7rule.l7policy, models.L7Policy) + + class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): def setUp(self): @@ -604,6 +797,12 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): self.stats = self.create_listener_statistics(self.session, self.listener.id) self.sni = self.create_sni(self.session, listener_id=self.listener.id) + self.l7policy = self.create_l7policy( + self.session, listener_id=self.listener.id, + action=constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + redirect_pool_id=self.pool.id) + self.l7rule = self.create_l7rule(self.session, + l7policy_id=self.l7policy.id) @staticmethod def _get_unique_key(obj): @@ -611,7 +810,8 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): # First handle all objects with their own ID, then handle subordinate # objects. if obj.__class__.__name__ in ['Member', 'Pool', 'LoadBalancer', - 'Listener', 'Amphora']: + 'Listener', 'Amphora', 'L7Policy', + 'L7Rule']: return obj.__class__.__name__ + obj.id elif obj.__class__.__name__ in ['SessionPersistence', 'HealthMonitor']: return obj.__class__.__name__ + obj.pool_id @@ -758,6 +958,16 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): id=self.member.id).first() self.check_member(member_db.to_data_model()) + def test_l7policy_tree(self): + l7policy_db = self.session.query(models.L7Policy).filter_by( + id=self.l7policy.id).first() + self.check_l7policy(l7policy_db.to_data_model()) + + def test_l7rule_tree(self): + l7rule_db = self.session.query(models.L7Rule).filter_by( + id=self.l7rule.id).first() + self.check_l7rule(l7rule_db.to_data_model()) + def check_load_balancer(self, lb, check_listeners=True, check_amphorae=True, check_vip=True, check_pools=True): @@ -804,7 +1014,8 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): self.check_load_balancer(amphora.load_balancer) def check_listener(self, listener, check_sni=True, check_pools=True, - check_lb=True, check_statistics=True): + check_lb=True, check_statistics=True, + check_l7policies=True): self.assertIsInstance(listener, data_models.Listener) self.check_listener_data_model(listener) if check_lb: @@ -822,6 +1033,12 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): if check_statistics: self.check_listener_statistics(listener.stats, check_listener=False) + if check_l7policies: + c_l7policies = listener.l7policies + self.assertIsInstance(c_l7policies, list) + for policy in c_l7policies: + self.check_l7policy(policy, check_listener=False, + check_pool=check_pools, check_lb=check_lb) def check_session_persistence(self, session_persistence, check_pool=True): self.assertIsInstance(session_persistence, @@ -836,6 +1053,31 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): if check_pool: self.check_pool(member.pool, check_members=False) + def check_l7policy(self, l7policy, check_listener=True, check_pool=True, + check_l7rules=True, check_lb=True): + self.assertIsInstance(l7policy, data_models.L7Policy) + self.check_l7policy_data_model(l7policy) + if check_listener: + self.check_listener(l7policy.listener, check_l7policies=False, + check_pools=check_pool, check_lb=check_lb) + if check_l7rules: + c_l7rules = l7policy.l7rules + self.assertIsInstance(c_l7rules, list) + for rule in c_l7rules: + self.check_l7rule(rule, check_l7policy=False) + if check_pool and l7policy.redirect_pool is not None: + self.assertEqual(l7policy.action, + constants.L7POLICY_ACTION_REDIRECT_TO_POOL) + self.check_pool(l7policy.redirect_pool, + check_listeners=check_listener, + check_l7policies=False, check_lb=check_lb) + + def check_l7rule(self, l7rule, check_l7policy=True): + self.assertIsInstance(l7rule, data_models.L7Rule) + self.check_l7rule_data_model(l7rule) + if check_l7policy: + self.check_l7policy(l7rule.l7policy) + def check_health_monitor(self, health_monitor, check_pool=True): self.assertIsInstance(health_monitor, data_models.HealthMonitor) self.check_health_monitor_data_model(health_monitor) @@ -843,7 +1085,8 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): self.check_pool(health_monitor.pool, check_hm=False) def check_pool(self, pool, check_listeners=True, check_sp=True, - check_hm=True, check_members=True, check_lb=True): + check_hm=True, check_members=True, check_l7policies=True, + check_lb=True): self.assertIsInstance(pool, data_models.Pool) self.check_pool_data_model(pool) if check_listeners: @@ -864,6 +1107,13 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): if check_lb: self.check_load_balancer(pool.load_balancer, check_pools=False, check_listeners=check_listeners) + if check_l7policies: + c_l7policies = pool.l7policies + self.assertIsInstance(c_l7policies, list) + for policy in c_l7policies: + self.check_l7policy(policy, check_pool=False, + check_listener=check_listeners, + check_lb=check_lb) def check_load_balancer_data_model(self, lb): self.assertEqual(self.FAKE_UUID_1, lb.project_id) @@ -924,6 +1174,23 @@ class DataModelConversionTest(base.OctaviaDBTestBase, ModelTestMixin): self.assertEqual(constants.ONLINE, member.operating_status) self.assertTrue(member.enabled) + def check_l7policy_data_model(self, l7policy): + self.assertEqual(self.FAKE_UUID_1, l7policy.id) + self.assertEqual(self.listener.id, l7policy.listener_id) + self.assertEqual(constants.L7POLICY_ACTION_REDIRECT_TO_POOL, + l7policy.action) + self.assertEqual(self.pool.id, l7policy.redirect_pool_id) + self.assertEqual(0, l7policy.position) + + def check_l7rule_data_model(self, l7rule): + self.assertEqual(self.FAKE_UUID_1, l7rule.id) + self.assertEqual(self.l7policy.id, l7rule.l7policy_id) + self.assertEqual(constants.L7RULE_TYPE_PATH, l7rule.type) + self.assertEqual(constants.L7RULE_COMPARE_TYPE_STARTS_WITH, + l7rule.compare_type) + self.assertEqual('/api', l7rule.value) + self.assertFalse(l7rule.invert) + def check_amphora_data_model(self, amphora): self.assertEqual(self.FAKE_UUID_1, amphora.id) self.assertEqual(self.FAKE_UUID_1, amphora.compute_id) diff --git a/octavia/tests/functional/db/test_repositories.py b/octavia/tests/functional/db/test_repositories.py index 14f2010a4b..9767e797f6 100644 --- a/octavia/tests/functional/db/test_repositories.py +++ b/octavia/tests/functional/db/test_repositories.py @@ -151,6 +151,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): del pool_dm_dict['listeners'] del pool_dm_dict['load_balancer'] del pool_dm_dict['load_balancer_id'] + del pool_dm_dict['l7policies'] self.assertEqual(pool, pool_dm_dict) new_listener = self.repos.listener.get(self.session, id=self.listener.id) @@ -173,6 +174,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): del pool_dm_dict['listeners'] del pool_dm_dict['load_balancer'] del pool_dm_dict['load_balancer_id'] + del pool_dm_dict['l7policies'] self.assertEqual(pool, pool_dm_dict) sp_dm_dict = pool_dm.session_persistence.to_dict() del sp_dm_dict['pool'] @@ -203,6 +205,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): del pool_dm_dict['listeners'] del pool_dm_dict['load_balancer'] del pool_dm_dict['load_balancer_id'] + del pool_dm_dict['l7policies'] pool.update(update_pool) self.assertEqual(pool, pool_dm_dict) self.assertIsNone(new_pool_dm.session_persistence) @@ -228,6 +231,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase): del pool_dm_dict['listeners'] del pool_dm_dict['load_balancer'] del pool_dm_dict['load_balancer_id'] + del pool_dm_dict['l7policies'] pool.update(update_pool) self.assertEqual(pool, pool_dm_dict) sp_dm_dict = new_pool_dm.session_persistence.to_dict()