Allow operators to tune VIP creation parameters

Change-Id: Iff46479d530e5e3b09f27fd9d335651521f77a11
This commit is contained in:
Adam Harwell 2017-06-15 17:14:41 -07:00
parent 3d4226b98a
commit 041d15a4b2
7 changed files with 171 additions and 10 deletions

View File

@ -120,6 +120,13 @@
# retry_interval = 1
# The maximum time to wait, in seconds, for a port to detach from an amphora
# port_detach_timeout = 300
# Allow/disallow specific network object types when creating VIPs.
# allow_vip_network_id = True
# allow_vip_subnet_id = True
# allow_vip_port_id = True
# List of network_ids that are valid for VIP creation.
# If this field empty, no validation is performed.
# valid_vip_networks =
[haproxy_amphora]
# base_path = /var/lib/octavia

View File

@ -110,11 +110,34 @@ class LoadBalancersController(base.BaseController):
))
def _validate_vip_request_object(self, load_balancer):
allowed_network_objects = []
if CONF.networking.allow_vip_port_id:
allowed_network_objects.append('vip_port_id')
if CONF.networking.allow_vip_network_id:
allowed_network_objects.append('vip_network_id')
if CONF.networking.allow_vip_subnet_id:
allowed_network_objects.append('vip_subnet_id')
msg = _("use of %(object)s is disallowed by this deployment's "
"configuration.")
if (load_balancer.vip_port_id and
not CONF.networking.allow_vip_port_id):
raise exceptions.ValidationException(
detail=msg % {'object': 'vip_port_id'})
if (load_balancer.vip_network_id and
not CONF.networking.allow_vip_network_id):
raise exceptions.ValidationException(
detail=msg % {'object': 'vip_network_id'})
if (load_balancer.vip_subnet_id and
not CONF.networking.allow_vip_subnet_id):
raise exceptions.ValidationException(
detail=msg % {'object': 'vip_subnet_id'})
if not (load_balancer.vip_port_id or
load_balancer.vip_network_id or
load_balancer.vip_subnet_id):
raise exceptions.ValidationException(detail=_(
"VIP must contain one of: port_id, network_id, subnet_id."))
raise exceptions.VIPValidationException(
objects=', '.join(allowed_network_objects))
# Validate the port id
if load_balancer.vip_port_id:
@ -129,6 +152,8 @@ class LoadBalancersController(base.BaseController):
subnet_id=load_balancer.vip_subnet_id)
load_balancer.vip_network_id = subnet.network_id
validate.network_allowed_by_config(load_balancer.vip_network_id)
@wsme_pecan.wsexpose(lb_types.LoadBalancerFullRootResponse,
body=lb_types.LoadBalancerRootPOST, status_code=201)
def post(self, load_balancer):

View File

@ -91,7 +91,17 @@ networking_opts = [
'networking service.')),
cfg.IntOpt('port_detach_timeout', default=300,
help=_('Seconds to wait for a port to detach from an '
'amphora.'))
'amphora.')),
cfg.BoolOpt('allow_vip_network_id', default=True,
help=_('Can users supply a network_id for their VIP?')),
cfg.BoolOpt('allow_vip_subnet_id', default=True,
help=_('Can users supply a subnet_id for their VIP?')),
cfg.BoolOpt('allow_vip_port_id', default=True,
help=_('Can users supply a port_id for their VIP?')),
cfg.ListOpt('valid_vip_networks',
help=_('List of network_ids that are valid for VIP '
'creation. If this field is empty, no validation '
'is performed.')),
]
healthmanager_opts = [

View File

@ -286,6 +286,11 @@ class ValidationException(APIException):
code = 400
class VIPValidationException(APIException):
msg = _('Validation failure: VIP must contain one of: %(objects)s.')
code = 400
class InvalidSortKey(APIException):
msg = _("Supplied sort key '%(key)s' is not valid.")
code = 400

View File

@ -21,6 +21,7 @@ Defined here so these can also be used at deeper levels than the API.
import re
from oslo_config import cfg
import rfc3986
from octavia.common import constants
@ -28,6 +29,9 @@ from octavia.common import exceptions
from octavia.common import utils
CONF = cfg.CONF
def url(url, require_scheme=True):
"""Raises an error if the url doesn't look like a URL."""
try:
@ -238,3 +242,12 @@ def network_exists_optionally_contains_subnet(network_id, subnet_id=None):
raise exceptions.InvalidSubresource(resource='Subnet',
id=subnet_id)
return network
def network_allowed_by_config(network_id):
if CONF.networking.valid_vip_networks:
valid_networks = map(str.lower, CONF.networking.valid_vip_networks)
if network_id not in valid_networks:
raise exceptions.ValidationException(detail=_(
'Supplied VIP network_id is not allowed by the configuration '
'of this deployment.'))

View File

@ -76,7 +76,7 @@ class TestLoadBalancer(base.BaseAPITest):
body = self._build_body(lb_json)
response = self.post(self.LBS_PATH, body, status=400)
err_msg = ('Validation failure: VIP must contain one of: '
'port_id, network_id, subnet_id.')
'vip_port_id, vip_network_id, vip_subnet_id.')
self.assertEqual(err_msg, response.json.get('faultstring'))
def test_create_with_empty_vip(self):
@ -224,17 +224,100 @@ class TestLoadBalancer(base.BaseAPITest):
def test_create_with_long_name(self):
lb_json = {'name': 'n' * 256,
'vip_subnet_id': uuidutils.generate_uuid()}
self.post(self.LBS_PATH, lb_json, status=400)
'vip_subnet_id': uuidutils.generate_uuid(),
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('Invalid input for field/attribute name',
response.json.get('faultstring'))
def test_create_with_long_description(self):
lb_json = {'description': 'n' * 256,
'vip_subnet_id': uuidutils.generate_uuid()}
self.post(self.LBS_PATH, lb_json, status=400)
'vip_subnet_id': uuidutils.generate_uuid(),
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('Invalid input for field/attribute description',
response.json.get('faultstring'))
def test_create_with_nonuuid_vip_attributes(self):
lb_json = {'vip_subnet_id': 'HI'}
self.post(self.LBS_PATH, lb_json, status=400)
lb_json = {'vip_subnet_id': 'HI',
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('Invalid input for field/attribute vip_subnet_id',
response.json.get('faultstring'))
def test_create_with_allowed_network_id(self):
network_id = uuidutils.generate_uuid()
self.conf.config(group="networking", valid_vip_networks=network_id)
subnet = network_models.Subnet(id=uuidutils.generate_uuid(),
network_id=network_id,
ip_version=4)
network = network_models.Network(id=network_id, subnets=[subnet.id])
lb_json = {'vip_network_id': network.id,
'project_id': self.project_id}
body = self._build_body(lb_json)
with mock.patch(
"octavia.network.drivers.noop_driver.driver.NoopManager"
".get_network") as mock_get_network, mock.patch(
"octavia.network.drivers.noop_driver.driver.NoopManager"
".get_subnet") as mock_get_subnet:
mock_get_network.return_value = network
mock_get_subnet.return_value = subnet
response = self.post(self.LBS_PATH, body)
api_lb = response.json.get(self.root_tag)
self._assert_request_matches_response(lb_json, api_lb)
self.assertEqual(subnet.id, api_lb.get('vip_subnet_id'))
self.assertEqual(network_id, api_lb.get('vip_network_id'))
def test_create_with_disallowed_network_id(self):
network_id1 = uuidutils.generate_uuid()
network_id2 = uuidutils.generate_uuid()
self.conf.config(group="networking", valid_vip_networks=network_id1)
subnet = network_models.Subnet(id=uuidutils.generate_uuid(),
network_id=network_id2,
ip_version=4)
network = network_models.Network(id=network_id2, subnets=[subnet.id])
lb_json = {'vip_network_id': network.id,
'project_id': self.project_id}
body = self._build_body(lb_json)
with mock.patch(
"octavia.network.drivers.noop_driver.driver.NoopManager"
".get_network") as mock_get_network, mock.patch(
"octavia.network.drivers.noop_driver.driver.NoopManager"
".get_subnet") as mock_get_subnet:
mock_get_network.return_value = network
mock_get_subnet.return_value = subnet
response = self.post(self.LBS_PATH, body, status=400)
self.assertIn("Supplied VIP network_id is not allowed",
response.json.get('faultstring'))
def test_create_with_disallowed_vip_objects(self):
self.conf.config(group="networking", allow_vip_network_id=False)
self.conf.config(group="networking", allow_vip_subnet_id=False)
self.conf.config(group="networking", allow_vip_port_id=False)
lb_json = {'vip_network_id': uuidutils.generate_uuid(),
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('use of vip_network_id is disallowed',
response.json.get('faultstring'))
lb_json = {'vip_subnet_id': uuidutils.generate_uuid(),
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('use of vip_subnet_id is disallowed',
response.json.get('faultstring'))
lb_json = {'vip_port_id': uuidutils.generate_uuid(),
'project_id': self.project_id}
response = self.post(self.LBS_PATH, self._build_body(lb_json),
status=400)
self.assertIn('use of vip_port_id is disallowed',
response.json.get('faultstring'))
def test_create_with_project_id(self):
project_id = uuidutils.generate_uuid()

View File

@ -13,6 +13,8 @@
# under the License.
import mock
from oslo_config import cfg
from oslo_config import fixture as oslo_fixture
from oslo_utils import uuidutils
from octavia.api.v1.types import load_balancer as lb_types
@ -28,6 +30,10 @@ class TestValidations(base.TestCase):
# Note that particularly complex validation testing is handled via
# functional tests elsewhere (ex. repository tests)
def setUp(self):
super(TestValidations, self).setUp()
self.conf = oslo_fixture.Config(cfg.CONF)
def test_validate_url(self):
ret = validate.url('http://example.com')
self.assertTrue(ret)
@ -329,3 +335,15 @@ class TestValidations(base.TestCase):
exceptions.InvalidSubresource,
validate.network_exists_optionally_contains_subnet,
vip.network_id, vip.subnet_id)
def test_network_allowed_by_config(self):
net_id1 = uuidutils.generate_uuid()
net_id2 = uuidutils.generate_uuid()
net_id3 = uuidutils.generate_uuid()
valid_net_ids = ",".join((net_id1, net_id2))
self.conf.config(group="networking", valid_vip_networks=valid_net_ids)
validate.network_allowed_by_config(net_id1)
validate.network_allowed_by_config(net_id2)
self.assertRaises(
exceptions.ValidationException,
validate.network_allowed_by_config, net_id3)