diff --git a/etc/octavia.conf b/etc/octavia.conf index f2daf1801a..df57867661 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -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 diff --git a/octavia/api/v2/controllers/load_balancer.py b/octavia/api/v2/controllers/load_balancer.py index bcd061be18..656204e4f0 100644 --- a/octavia/api/v2/controllers/load_balancer.py +++ b/octavia/api/v2/controllers/load_balancer.py @@ -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): diff --git a/octavia/common/config.py b/octavia/common/config.py index 8ccbae9a25..55805642cd 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -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 = [ diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index f7ffe79521..b984e37cb6 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -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 diff --git a/octavia/common/validate.py b/octavia/common/validate.py index 527bd2febd..9d9bfd7c58 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -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.')) diff --git a/octavia/tests/functional/api/v2/test_load_balancer.py b/octavia/tests/functional/api/v2/test_load_balancer.py index 80403b755f..059c20a69e 100644 --- a/octavia/tests/functional/api/v2/test_load_balancer.py +++ b/octavia/tests/functional/api/v2/test_load_balancer.py @@ -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() diff --git a/octavia/tests/unit/common/test_validations.py b/octavia/tests/unit/common/test_validations.py index 88db0e111d..5be02928cb 100644 --- a/octavia/tests/unit/common/test_validations.py +++ b/octavia/tests/unit/common/test_validations.py @@ -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)