Skip to content

Commit

Permalink
Allow listeners for different protocols on the same port
Browse files Browse the repository at this point in the history
Added 'protocol' name in the unique constraint list for listeners,
updated conflicting/duplicate entries detection in API.
Added alembic migration script.

Story: 2005070
Task: 29643

Change-Id: If85b59bddb8d6dc9916c3fef5155e838f1af63b6
  • Loading branch information
gthiemonge committed Aug 9, 2019
1 parent 6e4da85 commit b42a64a
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 4 deletions.
37 changes: 36 additions & 1 deletion octavia/api/v2/controllers/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,40 @@ def _validate_create_listener(self, lock_session, listener_dict):
listener_dict.get('client_ca_tls_certificate_id'),
listener_dict.get('client_crl_container_id', None))

# Validate that the L4 protocol (UDP or TCP) is not already used for
# the specified protocol_port in this load balancer
pcontext = pecan.request.context
query_filter = {
'project_id': listener_dict['project_id'],
'load_balancer_id': listener_dict['load_balancer_id'],
'protocol_port': listener_dict['protocol_port']
}

# Get listeners on the same load balancer that use the same
# protocol port
db_listeners = self.repositories.listener.get_all_API_list(
lock_session, show_deleted=False,
pagination_helper=pcontext.get(constants.PAGINATION_HELPER),
**query_filter)[0]

if db_listeners:
l4_protocol = constants.L4_PROTOCOL_MAP[listener_protocol]

# List supported protocols that share the same L4 protocol as our
# new listener
disallowed_protocols = [
p
for p in constants.L4_PROTOCOL_MAP
if constants.L4_PROTOCOL_MAP[p] == l4_protocol
]

for db_l in db_listeners:
# Check if l4 protocol ports conflict
if db_l.protocol in disallowed_protocols:
raise exceptions.DuplicateListenerEntry(
protocol=db_l.protocol,
port=listener_dict.get('protocol_port'))

try:
db_listener = self.repositories.listener.create(
lock_session, **listener_dict)
Expand All @@ -242,13 +276,14 @@ def _validate_create_listener(self, lock_session, listener_dict):
lock_session, id=db_listener.id)
return db_listener
except odb_exceptions.DBDuplicateEntry as de:
column_list = ['load_balancer_id', 'protocol_port']
column_list = ['load_balancer_id', 'protocol', 'protocol_port']
constraint_list = ['uq_listener_load_balancer_id_protocol_port']
if ['id'] == de.columns:
raise exceptions.IDAlreadyExists()
if (set(column_list) == set(de.columns) or
set(constraint_list) == set(de.columns)):
raise exceptions.DuplicateListenerEntry(
protocol=listener_dict.get('protocol'),
port=listener_dict.get('protocol_port'))
except odb_exceptions.DBError:
raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
Expand Down
10 changes: 10 additions & 0 deletions octavia/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,3 +671,13 @@

HAPROXY_HTTP_PROTOCOLS = [lib_consts.PROTOCOL_HTTP,
lib_consts.PROTOCOL_TERMINATED_HTTPS]

# Map each supported protocol to its L4 protocol
L4_PROTOCOL_MAP = {
PROTOCOL_TCP: PROTOCOL_TCP,
PROTOCOL_HTTP: PROTOCOL_TCP,
PROTOCOL_HTTPS: PROTOCOL_TCP,
PROTOCOL_TERMINATED_HTTPS: PROTOCOL_TCP,
PROTOCOL_PROXY: PROTOCOL_TCP,
PROTOCOL_UDP: PROTOCOL_UDP,
}
3 changes: 2 additions & 1 deletion octavia/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ class CertificateGenerationException(OctaviaException):

class DuplicateListenerEntry(APIException):
msg = _("Another Listener on this Load Balancer "
"is already using protocol_port %(port)d")
"is already using protocol %(protocol)s "
"and protocol_port %(port)d")
code = 409


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) 2019 Red Hat, Inc.
#
# 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 protocol in listener keys
Revision ID: a5762a99609a
Revises: 392fb85b4419
Create Date: 2019-06-28 14:02:11.415292
"""

from alembic import op

# revision identifiers, used by Alembic.
revision = 'a5762a99609a'
down_revision = '392fb85b4419'


def upgrade():
op.execute("ALTER TABLE `listener` "
"DROP INDEX `uq_listener_load_balancer_id_protocol_port`, "
"ADD UNIQUE KEY "
"`uq_listener_load_balancer_id_protocol_port` "
"(`load_balancer_id`, `protocol`, `protocol_port`)")
5 changes: 3 additions & 2 deletions octavia/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,9 @@ class Listener(base_models.BASE, base_models.IdMixin,
__v2_wsme__ = listener.ListenerResponse

__table_args__ = (
sa.UniqueConstraint('load_balancer_id', 'protocol_port',
name='uq_listener_load_balancer_id_protocol_port'),
sa.UniqueConstraint(
'load_balancer_id', 'protocol', 'protocol_port',
name='uq_listener_load_balancer_id_protocol_port'),
)

description = sa.Column(sa.String(255), nullable=True)
Expand Down
22 changes: 22 additions & 0 deletions octavia/tests/functional/api/v2/test_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,28 @@ def test_create_listeners_same_port(self):
body = self._build_body(listener2_post)
self.post(self.LISTENERS_PATH, body, status=409)

def test_create_listeners_tcp_https_same_port(self):
listener1 = self.create_listener(constants.PROTOCOL_TCP, 80,
self.lb_id)
self.set_lb_status(self.lb_id)
listener2_post = {'protocol': constants.PROTOCOL_HTTPS,
'protocol_port':
listener1['listener']['protocol_port'],
'loadbalancer_id': self.lb_id}
body = self._build_body(listener2_post)
self.post(self.LISTENERS_PATH, body, status=409)

def test_create_listeners_tcp_udp_same_port(self):
listener1 = self.create_listener(constants.PROTOCOL_TCP, 80,
self.lb_id)
self.set_lb_status(self.lb_id)
listener2_post = {'protocol': constants.PROTOCOL_UDP,
'protocol_port':
listener1['listener']['protocol_port'],
'loadbalancer_id': self.lb_id}
body = self._build_body(listener2_post)
self.post(self.LISTENERS_PATH, body, status=201)

def test_delete(self):
listener = self.create_listener(constants.PROTOCOL_HTTP, 80,
self.lb_id)
Expand Down
5 changes: 5 additions & 0 deletions releasenotes/notes/same-port-listeners-41198368d470e821.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
Fixed bug which prevented the creation of listeners for different protocols
on the same port (i.e: tcp port 53, and udp port 53).

0 comments on commit b42a64a

Please sign in to comment.