octavia 15.0.0.0rc1__py3-none-any.whl → 16.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- octavia/amphorae/backends/agent/api_server/keepalivedlvs.py +9 -0
- octavia/amphorae/backends/agent/api_server/loadbalancer.py +6 -6
- octavia/amphorae/backends/agent/api_server/plug.py +1 -1
- octavia/amphorae/backends/agent/api_server/util.py +35 -2
- octavia/amphorae/backends/health_daemon/status_message.py +1 -2
- octavia/amphorae/drivers/haproxy/rest_api_driver.py +12 -7
- octavia/api/drivers/amphora_driver/flavor_schema.py +5 -0
- octavia/api/drivers/noop_driver/driver.py +2 -1
- octavia/api/drivers/utils.py +12 -0
- octavia/api/root_controller.py +8 -2
- octavia/api/v2/controllers/base.py +8 -4
- octavia/api/v2/controllers/listener.py +12 -2
- octavia/api/v2/controllers/load_balancer.py +33 -1
- octavia/api/v2/controllers/member.py +58 -4
- octavia/api/v2/types/load_balancer.py +7 -1
- octavia/api/v2/types/member.py +3 -0
- octavia/common/base_taskflow.py +19 -10
- octavia/common/clients.py +8 -2
- octavia/common/config.py +17 -2
- octavia/common/constants.py +6 -0
- octavia/common/data_models.py +32 -2
- octavia/common/exceptions.py +5 -0
- octavia/common/utils.py +4 -1
- octavia/common/validate.py +16 -0
- octavia/compute/drivers/noop_driver/driver.py +30 -1
- octavia/controller/healthmanager/health_manager.py +7 -0
- octavia/controller/worker/v2/flows/amphora_flows.py +3 -5
- octavia/controller/worker/v2/flows/listener_flows.py +2 -1
- octavia/controller/worker/v2/flows/load_balancer_flows.py +38 -0
- octavia/controller/worker/v2/taskflow_jobboard_driver.py +34 -6
- octavia/controller/worker/v2/tasks/compute_tasks.py +9 -5
- octavia/controller/worker/v2/tasks/database_tasks.py +26 -6
- octavia/controller/worker/v2/tasks/network_tasks.py +118 -70
- octavia/db/base_models.py +29 -5
- octavia/db/migration/alembic_migrations/versions/3097e55493ae_add_sg_id_to_vip_table.py +39 -0
- octavia/db/migration/alembic_migrations/versions/8db7a6443785_add_member_vnic_type.py +36 -0
- octavia/db/migration/alembic_migrations/versions/fabf4983846b_add_member_port_table.py +40 -0
- octavia/db/models.py +43 -1
- octavia/db/repositories.py +88 -9
- octavia/network/base.py +29 -12
- octavia/network/data_models.py +2 -1
- octavia/network/drivers/neutron/allowed_address_pairs.py +55 -46
- octavia/network/drivers/neutron/base.py +28 -16
- octavia/network/drivers/neutron/utils.py +2 -2
- octavia/network/drivers/noop_driver/driver.py +150 -29
- octavia/policies/__init__.py +4 -0
- octavia/policies/advanced_rbac.py +95 -0
- octavia/policies/base.py +5 -101
- octavia/policies/keystone_default_roles.py +81 -0
- octavia/policies/loadbalancer.py +13 -0
- octavia/tests/common/constants.py +2 -1
- octavia/tests/common/sample_data_models.py +27 -14
- octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +5 -4
- octavia/tests/functional/api/drivers/driver_agent/test_driver_agent.py +2 -1
- octavia/tests/functional/api/v2/test_health_monitor.py +1 -1
- octavia/tests/functional/api/v2/test_l7policy.py +1 -1
- octavia/tests/functional/api/v2/test_listener.py +1 -1
- octavia/tests/functional/api/v2/test_load_balancer.py +150 -4
- octavia/tests/functional/api/v2/test_member.py +50 -0
- octavia/tests/functional/api/v2/test_pool.py +1 -1
- octavia/tests/functional/api/v2/test_quotas.py +5 -8
- octavia/tests/functional/db/base.py +6 -6
- octavia/tests/functional/db/test_models.py +124 -1
- octavia/tests/functional/db/test_repositories.py +237 -19
- octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py +89 -1
- octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py +10 -7
- octavia/tests/unit/api/drivers/test_utils.py +6 -1
- octavia/tests/unit/certificates/generator/test_local.py +1 -1
- octavia/tests/unit/common/test_base_taskflow.py +4 -3
- octavia/tests/unit/compute/drivers/noop_driver/test_driver.py +28 -2
- octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +27 -1
- octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks.py +28 -6
- octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +100 -79
- octavia/tests/unit/controller/worker/v2/test_taskflow_jobboard_driver.py +8 -0
- octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +62 -45
- octavia/tests/unit/network/drivers/neutron/test_base.py +7 -7
- octavia/tests/unit/network/drivers/noop_driver/test_driver.py +55 -42
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/tox.ini +0 -1
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/AUTHORS +3 -0
- octavia-16.0.0.dist-info/METADATA +156 -0
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/RECORD +95 -90
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/WHEEL +1 -1
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/entry_points.txt +1 -1
- octavia-16.0.0.dist-info/pbr.json +1 -0
- octavia-15.0.0.0rc1.dist-info/METADATA +0 -156
- octavia-15.0.0.0rc1.dist-info/pbr.json +0 -1
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/LICENSE +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/README.rst +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/README.rst +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/diskimage-create.sh +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/image-tests.sh +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/requirements.txt +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/test-requirements.txt +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/version.txt +0 -0
- {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/scripts/octavia-wsgi +0 -0
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/LICENSE +0 -0
- {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/top_level.txt +0 -0
@@ -201,6 +201,15 @@ class KeepalivedLvs(lvs_listener_base.LvsListenerApiServerBase):
|
|
201
201
|
f"{listener_id}"),
|
202
202
|
'details': e.output}, status=500)
|
203
203
|
|
204
|
+
is_vrrp = (CONF.controller_worker.loadbalancer_topology ==
|
205
|
+
consts.TOPOLOGY_ACTIVE_STANDBY)
|
206
|
+
# TODO(gthiemonge) remove RESTART from the list (same as previous todo
|
207
|
+
# in this function)
|
208
|
+
if not is_vrrp and action in [consts.AMP_ACTION_START,
|
209
|
+
consts.AMP_ACTION_RESTART,
|
210
|
+
consts.AMP_ACTION_RELOAD]:
|
211
|
+
util.send_vip_advertisements(listener_id=listener_id)
|
212
|
+
|
204
213
|
return webob.Response(
|
205
214
|
json={'message': 'OK',
|
206
215
|
'details': (f'keepalivedlvs listener {listener_id} '
|
@@ -12,6 +12,7 @@
|
|
12
12
|
# License for the specific language governing permissions and limitations
|
13
13
|
# under the License.
|
14
14
|
|
15
|
+
import hashlib
|
15
16
|
import io
|
16
17
|
import os
|
17
18
|
import re
|
@@ -24,7 +25,6 @@ import flask
|
|
24
25
|
import jinja2
|
25
26
|
from oslo_config import cfg
|
26
27
|
from oslo_log import log as logging
|
27
|
-
from oslo_utils.secretutils import md5
|
28
28
|
import webob
|
29
29
|
from werkzeug import exceptions
|
30
30
|
|
@@ -55,7 +55,7 @@ SYSTEMD_TEMPLATE = JINJA_ENV.get_template(SYSTEMD_CONF)
|
|
55
55
|
class Wrapped:
|
56
56
|
def __init__(self, stream_):
|
57
57
|
self.stream = stream_
|
58
|
-
self.hash = md5(usedforsecurity=False) # nosec
|
58
|
+
self.hash = hashlib.md5(usedforsecurity=False) # nosec
|
59
59
|
|
60
60
|
def read(self, line):
|
61
61
|
block = self.stream.read(line)
|
@@ -82,8 +82,8 @@ class Loadbalancer:
|
|
82
82
|
cfg = file.read()
|
83
83
|
resp = webob.Response(cfg, content_type='text/plain')
|
84
84
|
resp.headers['ETag'] = (
|
85
|
-
md5(octavia_utils.b(cfg),
|
86
|
-
|
85
|
+
hashlib.md5(octavia_utils.b(cfg),
|
86
|
+
usedforsecurity=False).hexdigest()) # nosec
|
87
87
|
return resp
|
88
88
|
|
89
89
|
def upload_haproxy_config(self, amphora_id, lb_id):
|
@@ -408,8 +408,8 @@ class Loadbalancer:
|
|
408
408
|
|
409
409
|
with open(cert_path, encoding='utf-8') as crt_file:
|
410
410
|
cert = crt_file.read()
|
411
|
-
md5sum = md5(octavia_utils.b(cert),
|
412
|
-
|
411
|
+
md5sum = hashlib.md5(octavia_utils.b(cert),
|
412
|
+
usedforsecurity=False).hexdigest() # nosec
|
413
413
|
resp = webob.Response(json={'md5sum': md5sum})
|
414
414
|
resp.headers['ETag'] = md5sum
|
415
415
|
return resp
|
@@ -247,7 +247,7 @@ class Plug:
|
|
247
247
|
with pyroute2.IPRoute() as ipr:
|
248
248
|
idx = ipr.link_lookup(address=mac)[0]
|
249
249
|
# Workaround for https://github.com/PyCQA/pylint/issues/8497
|
250
|
-
# pylint: disable=E1136, E1121
|
250
|
+
# pylint: disable=E1136, E1121, E1133
|
251
251
|
addr = ipr.get_links(idx)[0]
|
252
252
|
for attr in addr['attrs']:
|
253
253
|
if attr[0] == consts.IFLA_IFNAME:
|
@@ -347,7 +347,37 @@ def get_haproxy_vip_addresses(lb_id):
|
|
347
347
|
return vips
|
348
348
|
|
349
349
|
|
350
|
-
def
|
350
|
+
def get_lvs_vip_addresses(listener_id: str) -> list[str]:
|
351
|
+
"""Get the VIP addresses for a LVS load balancer.
|
352
|
+
|
353
|
+
:param listener_id: The listener ID to get VIP addresses from.
|
354
|
+
:returns: List of VIP addresses (IPv4 and IPv6)
|
355
|
+
"""
|
356
|
+
vips = []
|
357
|
+
# Extract the VIP addresses from keepalived configuration
|
358
|
+
# Format is
|
359
|
+
# virtual_server_group ipv<n>-group {
|
360
|
+
# vip_address1 port1
|
361
|
+
# vip_address2 port2
|
362
|
+
# }
|
363
|
+
# it can be repeated in case of dual-stack LBs
|
364
|
+
with open(keepalived_lvs_cfg_path(listener_id), encoding='utf-8') as file:
|
365
|
+
vsg_section = False
|
366
|
+
for line in file:
|
367
|
+
current_line = line.strip()
|
368
|
+
if vsg_section:
|
369
|
+
if current_line.startswith('}'):
|
370
|
+
vsg_section = False
|
371
|
+
else:
|
372
|
+
vip_address = current_line.split(' ')[0]
|
373
|
+
vips.append(vip_address)
|
374
|
+
elif line.startswith('virtual_server_group '):
|
375
|
+
vsg_section = True
|
376
|
+
return vips
|
377
|
+
|
378
|
+
|
379
|
+
def send_vip_advertisements(lb_id: tp.Optional[str] = None,
|
380
|
+
listener_id: tp.Optional[str] = None):
|
351
381
|
"""Sends address advertisements for each load balancer VIP.
|
352
382
|
|
353
383
|
This method will send either GARP (IPv4) or neighbor advertisements (IPv6)
|
@@ -357,7 +387,10 @@ def send_vip_advertisements(lb_id):
|
|
357
387
|
:returns: None
|
358
388
|
"""
|
359
389
|
try:
|
360
|
-
|
390
|
+
if lb_id:
|
391
|
+
vips = get_haproxy_vip_addresses(lb_id)
|
392
|
+
else:
|
393
|
+
vips = get_lvs_vip_addresses(listener_id)
|
361
394
|
|
362
395
|
for vip in vips:
|
363
396
|
interface = network_utils.get_interface_name(
|
@@ -19,7 +19,6 @@ import zlib
|
|
19
19
|
|
20
20
|
from oslo_log import log as logging
|
21
21
|
from oslo_serialization import jsonutils
|
22
|
-
from oslo_utils import secretutils
|
23
22
|
|
24
23
|
from octavia.common import exceptions
|
25
24
|
|
@@ -70,7 +69,7 @@ def get_payload(envelope, key, hex=True):
|
|
70
69
|
payload = envelope[:-len]
|
71
70
|
expected_hmc = envelope[-len:]
|
72
71
|
calculated_hmc = get_hmac(payload, key, hex=hex)
|
73
|
-
if not
|
72
|
+
if not hmac.compare_digest(expected_hmc, calculated_hmc):
|
74
73
|
LOG.warning(
|
75
74
|
'calculated hmac(hex=%(hex)s): %(s1)s not equal to msg hmac: '
|
76
75
|
'%(s2)s dropping packet',
|
@@ -23,7 +23,6 @@ import warnings
|
|
23
23
|
|
24
24
|
from oslo_context import context as oslo_context
|
25
25
|
from oslo_log import log as logging
|
26
|
-
from oslo_utils.secretutils import md5
|
27
26
|
import requests
|
28
27
|
from stevedore import driver as stevedore_driver
|
29
28
|
|
@@ -407,7 +406,10 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|
407
406
|
fixed_ips.append(ip)
|
408
407
|
port_info = {'mac_address': port.mac_address,
|
409
408
|
'fixed_ips': fixed_ips,
|
410
|
-
'mtu': port.network.mtu
|
409
|
+
'mtu': port.network.mtu,
|
410
|
+
'is_sriov': False}
|
411
|
+
if port.vnic_type == consts.VNIC_TYPE_DIRECT:
|
412
|
+
port_info['is_sriov'] = True
|
411
413
|
if port.id == amphora.vrrp_port_id:
|
412
414
|
# We have to special-case sharing the vrrp port and pass through
|
413
415
|
# enough extra information to populate the whole VIP port
|
@@ -450,7 +452,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|
450
452
|
if amphora and obj_id:
|
451
453
|
for cert in certs:
|
452
454
|
pem = cert_parser.build_pem(cert)
|
453
|
-
md5sum = md5(
|
455
|
+
md5sum = hashlib.md5(
|
456
|
+
pem, usedforsecurity=False).hexdigest() # nosec
|
454
457
|
name = f'{cert.id}.pem'
|
455
458
|
cert_filename_list.append(
|
456
459
|
os.path.join(
|
@@ -461,8 +464,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|
461
464
|
# Build and upload the crt-list file for haproxy
|
462
465
|
crt_list = "\n".join(cert_filename_list)
|
463
466
|
crt_list = f'{crt_list}\n'.encode()
|
464
|
-
md5sum = md5(
|
465
|
-
|
467
|
+
md5sum = hashlib.md5(
|
468
|
+
crt_list, usedforsecurity=False).hexdigest() # nosec
|
466
469
|
name = f'{listener.id}.pem'
|
467
470
|
self._upload_cert(amphora, obj_id, crt_list, md5sum, name)
|
468
471
|
return {'tls_cert': tls_cert, 'sni_certs': sni_certs}
|
@@ -480,7 +483,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|
480
483
|
secret = secret.encode('utf-8')
|
481
484
|
except AttributeError:
|
482
485
|
pass
|
483
|
-
md5sum = md5(
|
486
|
+
md5sum = hashlib.md5(
|
487
|
+
secret, usedforsecurity=False).hexdigest() # nosec
|
484
488
|
id = hashlib.sha1(secret).hexdigest() # nosec
|
485
489
|
name = f'{id}.pem'
|
486
490
|
|
@@ -519,7 +523,8 @@ class HaproxyAmphoraLoadBalancerDriver(
|
|
519
523
|
pem = pem.encode('utf-8')
|
520
524
|
except AttributeError:
|
521
525
|
pass
|
522
|
-
md5sum = md5(
|
526
|
+
md5sum = hashlib.md5(
|
527
|
+
pem, usedforsecurity=False).hexdigest() # nosec
|
523
528
|
name = f'{tls_cert.id}.pem'
|
524
529
|
if amphora and obj_id:
|
525
530
|
self._upload_cert(amphora, obj_id, pem=pem,
|
@@ -53,5 +53,10 @@ SUPPORTED_FLAVOR_SCHEMA = {
|
|
53
53
|
"description": "When true, the VIP port will be created using an "
|
54
54
|
"SR-IOV VF port."
|
55
55
|
},
|
56
|
+
consts.ALLOW_MEMBER_SRIOV: {
|
57
|
+
"type": "boolean",
|
58
|
+
"description": "When true, users can request a member port be "
|
59
|
+
"SR-IOV enabled at member creation time."
|
60
|
+
}
|
56
61
|
}
|
57
62
|
}
|
@@ -50,7 +50,8 @@ class NoopManager:
|
|
50
50
|
vip = data_models.VIP(vip_address=vip_address,
|
51
51
|
vip_network_id=vip_network_id,
|
52
52
|
vip_port_id=vip_port_id,
|
53
|
-
vip_subnet_id=vip_subnet_id
|
53
|
+
vip_subnet_id=vip_subnet_id,
|
54
|
+
vip_sg_ids=vip_dictionary.get('vip_sg_ids', []))
|
54
55
|
|
55
56
|
vip_return_dict = vip.to_dict()
|
56
57
|
additional_vip_dicts = additional_vip_dicts or []
|
octavia/api/drivers/utils.py
CHANGED
@@ -16,6 +16,7 @@ import copy
|
|
16
16
|
|
17
17
|
from octavia_lib.api.drivers import data_models as driver_dm
|
18
18
|
from octavia_lib.api.drivers import exceptions as lib_exceptions
|
19
|
+
from octavia_lib.common import constants as lib_consts
|
19
20
|
from oslo_config import cfg
|
20
21
|
from oslo_context import context as oslo_context
|
21
22
|
from oslo_log import log as logging
|
@@ -130,6 +131,7 @@ def lb_dict_to_provider_dict(lb_dict, vip=None, add_vips=None, db_pools=None,
|
|
130
131
|
new_lb_dict['vip_port_id'] = vip.port_id
|
131
132
|
new_lb_dict['vip_subnet_id'] = vip.subnet_id
|
132
133
|
new_lb_dict['vip_qos_policy_id'] = vip.qos_policy_id
|
134
|
+
new_lb_dict[lib_consts.VIP_SG_IDS] = vip.sg_ids
|
133
135
|
if 'flavor_id' in lb_dict and lb_dict['flavor_id']:
|
134
136
|
flavor_repo = repositories.FlavorRepository()
|
135
137
|
session = db_api.get_session()
|
@@ -460,6 +462,12 @@ def db_members_to_provider_members(db_members):
|
|
460
462
|
|
461
463
|
def db_member_to_provider_member(db_member):
|
462
464
|
new_member_dict = member_dict_to_provider_dict(db_member.to_dict())
|
465
|
+
if constants.REQUEST_SRIOV in new_member_dict:
|
466
|
+
request_sriov = new_member_dict.pop(constants.REQUEST_SRIOV)
|
467
|
+
if request_sriov:
|
468
|
+
new_member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_DIRECT
|
469
|
+
else:
|
470
|
+
new_member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_NORMAL
|
463
471
|
return driver_dm.Member.from_dict(new_member_dict)
|
464
472
|
|
465
473
|
|
@@ -565,6 +573,8 @@ def vip_dict_to_provider_dict(vip_dict):
|
|
565
573
|
new_vip_dict['vip_subnet_id'] = vip_dict['subnet_id']
|
566
574
|
if 'qos_policy_id' in vip_dict:
|
567
575
|
new_vip_dict['vip_qos_policy_id'] = vip_dict['qos_policy_id']
|
576
|
+
if constants.SG_IDS in vip_dict:
|
577
|
+
new_vip_dict[lib_consts.VIP_SG_IDS] = vip_dict[constants.SG_IDS]
|
568
578
|
if constants.OCTAVIA_OWNED in vip_dict:
|
569
579
|
new_vip_dict[constants.OCTAVIA_OWNED] = vip_dict[
|
570
580
|
constants.OCTAVIA_OWNED]
|
@@ -596,6 +606,8 @@ def provider_vip_dict_to_vip_obj(vip_dictionary):
|
|
596
606
|
vip_obj.subnet_id = vip_dictionary['vip_subnet_id']
|
597
607
|
if 'vip_qos_policy_id' in vip_dictionary:
|
598
608
|
vip_obj.qos_policy_id = vip_dictionary['vip_qos_policy_id']
|
609
|
+
if lib_consts.VIP_SG_IDS in vip_dictionary:
|
610
|
+
vip_obj.sg_ids = vip_dictionary[lib_consts.VIP_SG_IDS]
|
599
611
|
if constants.OCTAVIA_OWNED in vip_dictionary:
|
600
612
|
vip_obj.octavia_owned = vip_dictionary[constants.OCTAVIA_OWNED]
|
601
613
|
return vip_obj
|
octavia/api/root_controller.py
CHANGED
@@ -148,7 +148,13 @@ class RootController:
|
|
148
148
|
# HTTP Strict Transport Security (HSTS)
|
149
149
|
self._add_a_version(versions, 'v2.27', 'v2', 'SUPPORTED',
|
150
150
|
'2023-05-05T00:00:00Z', host_url)
|
151
|
-
# Add port vnic_type for SR-IOV
|
152
|
-
self._add_a_version(versions, 'v2.28', 'v2', '
|
151
|
+
# Add VIP port vnic_type for SR-IOV
|
152
|
+
self._add_a_version(versions, 'v2.28', 'v2', 'SUPPORTED',
|
153
153
|
'2023-11-08T00:00:00Z', host_url)
|
154
|
+
# Add VIP SGs
|
155
|
+
self._add_a_version(versions, 'v2.29', 'v2', 'SUPPORTED',
|
156
|
+
'2024-10-15T00:00:00Z', host_url)
|
157
|
+
# Add member port SR-IOV support
|
158
|
+
self._add_a_version(versions, 'v2.30', 'v2', 'CURRENT',
|
159
|
+
'2025-02-26T00:00:00Z', host_url)
|
154
160
|
return {'versions': versions}
|
@@ -64,9 +64,11 @@ class BaseController(pecan_rest.RestController):
|
|
64
64
|
return converted
|
65
65
|
|
66
66
|
@staticmethod
|
67
|
-
def _get_db_obj(session, repo, data_model, id, show_deleted=True
|
67
|
+
def _get_db_obj(session, repo, data_model, id, show_deleted=True,
|
68
|
+
limited_graph=False):
|
68
69
|
"""Gets an object from the database and returns it."""
|
69
|
-
db_obj = repo.get(session, id=id, show_deleted=show_deleted
|
70
|
+
db_obj = repo.get(session, id=id, show_deleted=show_deleted,
|
71
|
+
limited_graph=limited_graph)
|
70
72
|
if not db_obj:
|
71
73
|
LOG.debug('%(name)s %(id)s not found',
|
72
74
|
{'name': data_model._name(), 'id': id})
|
@@ -92,11 +94,13 @@ class BaseController(pecan_rest.RestController):
|
|
92
94
|
listener_id = db_l7policy.listener_id
|
93
95
|
return load_balancer_id, listener_id
|
94
96
|
|
95
|
-
def _get_db_pool(self, session, id, show_deleted=True
|
97
|
+
def _get_db_pool(self, session, id, show_deleted=True,
|
98
|
+
limited_graph=False):
|
96
99
|
"""Get a pool from the database."""
|
97
100
|
return self._get_db_obj(session, self.repositories.pool,
|
98
101
|
data_models.Pool, id,
|
99
|
-
show_deleted=show_deleted
|
102
|
+
show_deleted=show_deleted,
|
103
|
+
limited_graph=limited_graph)
|
100
104
|
|
101
105
|
def _get_db_member(self, session, id, show_deleted=True):
|
102
106
|
"""Get a member from the database."""
|
@@ -157,7 +157,15 @@ class ListenersController(base.BaseController):
|
|
157
157
|
value=headers,
|
158
158
|
option=f'{listener_protocol} protocol listener.')
|
159
159
|
|
160
|
-
def _validate_cidr_compatible_with_vip(self,
|
160
|
+
def _validate_cidr_compatible_with_vip(self, db_vip: data_models.Vip,
|
161
|
+
vips: list[str],
|
162
|
+
allowed_cidrs: list[str]):
|
163
|
+
if allowed_cidrs and db_vip.sg_ids:
|
164
|
+
msg = _("Allowed CIDRs are not allowed when using custom VIP "
|
165
|
+
"Security Groups.")
|
166
|
+
raise exceptions.ValidationException(
|
167
|
+
detail=msg)
|
168
|
+
|
161
169
|
for cidr in allowed_cidrs:
|
162
170
|
for vip in vips:
|
163
171
|
# Check if CIDR IP version matches VIP IP version
|
@@ -315,7 +323,8 @@ class ListenersController(base.BaseController):
|
|
315
323
|
lock_session, id=lb_id)
|
316
324
|
vip_addresses = [lb_db.vip.ip_address]
|
317
325
|
vip_addresses.extend([vip.ip_address for vip in lb_db.additional_vips])
|
318
|
-
self._validate_cidr_compatible_with_vip(
|
326
|
+
self._validate_cidr_compatible_with_vip(lb_db.vip,
|
327
|
+
vip_addresses, allowed_cidrs)
|
319
328
|
|
320
329
|
if _can_tls_offload:
|
321
330
|
# Validate TLS version list
|
@@ -542,6 +551,7 @@ class ListenersController(base.BaseController):
|
|
542
551
|
for vip in db_listener.load_balancer.additional_vips]
|
543
552
|
)
|
544
553
|
self._validate_cidr_compatible_with_vip(
|
554
|
+
db_listener.load_balancer.vip,
|
545
555
|
vip_addresses, listener.allowed_cidrs)
|
546
556
|
|
547
557
|
# Check TLS cipher prohibit list
|
@@ -307,6 +307,19 @@ class LoadBalancersController(base.BaseController):
|
|
307
307
|
# Multi-vip validation for ensuring subnets are "sane"
|
308
308
|
self._validate_subnets_share_network_but_no_duplicates(load_balancer)
|
309
309
|
|
310
|
+
# Validate optional security groups
|
311
|
+
if load_balancer.vip_sg_ids:
|
312
|
+
for sg_id in load_balancer.vip_sg_ids:
|
313
|
+
validate.security_group_exists(sg_id, context=context)
|
314
|
+
|
315
|
+
def _validate_vnic_type(self, vnic_type: str,
|
316
|
+
load_balancer: lb_types.LoadBalancerPOST):
|
317
|
+
if (vnic_type == constants.VNIC_TYPE_DIRECT and
|
318
|
+
load_balancer.vip_sg_ids):
|
319
|
+
msg = _("VIP Security Groups are not allowed with VNIC direct "
|
320
|
+
"type")
|
321
|
+
raise exceptions.ValidationException(detail=msg)
|
322
|
+
|
310
323
|
@staticmethod
|
311
324
|
def _create_vip_port_if_not_exist(load_balancer_db):
|
312
325
|
"""Create vip port."""
|
@@ -433,7 +446,7 @@ class LoadBalancersController(base.BaseController):
|
|
433
446
|
|
434
447
|
@wsme_pecan.wsexpose(lb_types.LoadBalancerFullRootResponse,
|
435
448
|
body=lb_types.LoadBalancerRootPOST, status_code=201)
|
436
|
-
def post(self, load_balancer):
|
449
|
+
def post(self, load_balancer: lb_types.LoadBalancerRootPOST):
|
437
450
|
"""Creates a load balancer."""
|
438
451
|
load_balancer = load_balancer.loadbalancer
|
439
452
|
context = pecan_request.context.get('octavia_context')
|
@@ -449,6 +462,10 @@ class LoadBalancersController(base.BaseController):
|
|
449
462
|
|
450
463
|
self._auth_validate_action(context, load_balancer.project_id,
|
451
464
|
constants.RBAC_POST)
|
465
|
+
if not isinstance(load_balancer.vip_sg_ids, wtypes.UnsetType):
|
466
|
+
self._auth_validate_action(
|
467
|
+
context, load_balancer.project_id,
|
468
|
+
f"{constants.RBAC_POST}:vip_sg_ids")
|
452
469
|
|
453
470
|
self._validate_vip_request_object(load_balancer, context=context)
|
454
471
|
|
@@ -503,6 +520,9 @@ class LoadBalancersController(base.BaseController):
|
|
503
520
|
else:
|
504
521
|
vip_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_NORMAL
|
505
522
|
|
523
|
+
self._validate_vnic_type(vip_dict[constants.VNIC_TYPE],
|
524
|
+
load_balancer)
|
525
|
+
|
506
526
|
db_lb = self.repositories.create_load_balancer_and_vip(
|
507
527
|
lock_session, lb_dict, vip_dict, additional_vip_dicts)
|
508
528
|
|
@@ -716,6 +736,9 @@ class LoadBalancersController(base.BaseController):
|
|
716
736
|
|
717
737
|
self._auth_validate_action(context, db_lb.project_id,
|
718
738
|
constants.RBAC_PUT)
|
739
|
+
if not isinstance(load_balancer.vip_sg_ids, wtypes.UnsetType):
|
740
|
+
self._auth_validate_action(context, db_lb.project_id,
|
741
|
+
f"{constants.RBAC_PUT}:vip_sg_ids")
|
719
742
|
|
720
743
|
if not isinstance(load_balancer.vip_qos_policy_id, wtypes.UnsetType):
|
721
744
|
network_driver = utils.get_network_driver()
|
@@ -724,6 +747,15 @@ class LoadBalancersController(base.BaseController):
|
|
724
747
|
if db_lb.vip.qos_policy_id != load_balancer.vip_qos_policy_id:
|
725
748
|
validate.qos_policy_exists(load_balancer.vip_qos_policy_id)
|
726
749
|
|
750
|
+
if not isinstance(load_balancer.vip_sg_ids, wtypes.UnsetType):
|
751
|
+
if load_balancer.vip_sg_ids is None:
|
752
|
+
load_balancer.vip_sg_ids = []
|
753
|
+
else:
|
754
|
+
for sg_id in load_balancer.vip_sg_ids:
|
755
|
+
validate.security_group_exists(sg_id, context=context)
|
756
|
+
|
757
|
+
self._validate_vnic_type(db_lb.vip.vnic_type, load_balancer)
|
758
|
+
|
727
759
|
# Load the driver early as it also provides validation
|
728
760
|
driver = driver_factory.get_driver(db_lb.provider)
|
729
761
|
|
@@ -19,6 +19,7 @@ from oslo_log import log as logging
|
|
19
19
|
from oslo_utils import excutils
|
20
20
|
from oslo_utils import strutils
|
21
21
|
from pecan import request as pecan_request
|
22
|
+
from sqlalchemy.orm import exc as sa_exception
|
22
23
|
from wsme import types as wtypes
|
23
24
|
from wsmeext import pecan as wsme_pecan
|
24
25
|
|
@@ -73,7 +74,7 @@ class MemberController(base.BaseController):
|
|
73
74
|
|
74
75
|
with context.session.begin():
|
75
76
|
pool = self._get_db_pool(context.session, self.pool_id,
|
76
|
-
show_deleted=False)
|
77
|
+
show_deleted=False, limited_graph=True)
|
77
78
|
|
78
79
|
self._auth_validate_action(context, pool.project_id,
|
79
80
|
constants.RBAC_GET_ALL)
|
@@ -81,7 +82,8 @@ class MemberController(base.BaseController):
|
|
81
82
|
db_members, links = self.repositories.member.get_all_API_list(
|
82
83
|
context.session, show_deleted=False,
|
83
84
|
pool_id=self.pool_id,
|
84
|
-
pagination_helper=pcontext.get(constants.PAGINATION_HELPER)
|
85
|
+
pagination_helper=pcontext.get(constants.PAGINATION_HELPER),
|
86
|
+
limited_graph=True)
|
85
87
|
result = self._convert_db_to_type(
|
86
88
|
db_members, [member_types.MemberResponse])
|
87
89
|
if fields is not None:
|
@@ -145,10 +147,20 @@ class MemberController(base.BaseController):
|
|
145
147
|
member = member_.member
|
146
148
|
context = pecan_request.context.get('octavia_context')
|
147
149
|
|
150
|
+
flavor_dict = {}
|
148
151
|
with context.session.begin():
|
149
152
|
pool = self.repositories.pool.get(context.session, id=self.pool_id)
|
150
153
|
member.project_id, provider = self._get_lb_project_id_provider(
|
151
154
|
context.session, pool.load_balancer_id)
|
155
|
+
if pool.load_balancer.flavor_id:
|
156
|
+
try:
|
157
|
+
flavor_dict = (
|
158
|
+
self.repositories.flavor.get_flavor_metadata_dict(
|
159
|
+
context.session, pool.load_balancer.flavor_id))
|
160
|
+
except sa_exception.NoResultFound:
|
161
|
+
LOG.error("load balancer has a flavor ID: %s that was not "
|
162
|
+
"found in the database. Assuming no flavor.",
|
163
|
+
pool.load_balancer.flavor_id)
|
152
164
|
|
153
165
|
self._auth_validate_action(context, member.project_id,
|
154
166
|
constants.RBAC_POST)
|
@@ -172,8 +184,23 @@ class MemberController(base.BaseController):
|
|
172
184
|
raise exceptions.QuotaException(
|
173
185
|
resource=data_models.Member._name())
|
174
186
|
|
175
|
-
|
176
|
-
|
187
|
+
db_member_dict = member.to_dict(render_unsets=True)
|
188
|
+
|
189
|
+
# Validate and store port SR-IOV vnic_type
|
190
|
+
request_sriov = db_member_dict.pop('request_sriov')
|
191
|
+
if (request_sriov and not
|
192
|
+
flavor_dict.get(constants.ALLOW_MEMBER_SRIOV, False)):
|
193
|
+
raise exceptions.MemberSRIOVDisabled
|
194
|
+
if request_sriov:
|
195
|
+
db_member_dict[constants.VNIC_TYPE] = (
|
196
|
+
constants.VNIC_TYPE_DIRECT)
|
197
|
+
else:
|
198
|
+
db_member_dict[constants.VNIC_TYPE] = (
|
199
|
+
constants.VNIC_TYPE_NORMAL)
|
200
|
+
|
201
|
+
member_dict = db_prepare.create_member(db_member_dict,
|
202
|
+
self.pool_id,
|
203
|
+
bool(pool.health_monitor))
|
177
204
|
|
178
205
|
self._test_lb_and_listener_and_pool_statuses(context.session)
|
179
206
|
|
@@ -203,6 +230,28 @@ class MemberController(base.BaseController):
|
|
203
230
|
|
204
231
|
def _graph_create(self, lock_session, member_dict):
|
205
232
|
pool = self.repositories.pool.get(lock_session, id=self.pool_id)
|
233
|
+
|
234
|
+
# Validate and store port SR-IOV vnic_type
|
235
|
+
request_sriov = member_dict.pop('request_sriov')
|
236
|
+
flavor_dict = {}
|
237
|
+
if pool.load_balancer.flavor_id:
|
238
|
+
try:
|
239
|
+
flavor_dict = (
|
240
|
+
self.repositories.flavor.get_flavor_metadata_dict(
|
241
|
+
lock_session, pool.load_balancer.flavor_id))
|
242
|
+
except sa_exception.NoResultFound:
|
243
|
+
LOG.error("load balancer has a flavor ID: %s that was not "
|
244
|
+
"found in the database. Assuming no flavor.",
|
245
|
+
pool.load_balancer.flavor_id)
|
246
|
+
if (request_sriov and not
|
247
|
+
flavor_dict.get(constants.ALLOW_MEMBER_SRIOV, False)):
|
248
|
+
raise exceptions.MemberSRIOVDisabled
|
249
|
+
|
250
|
+
if request_sriov:
|
251
|
+
member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_DIRECT
|
252
|
+
else:
|
253
|
+
member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_NORMAL
|
254
|
+
|
206
255
|
member_dict = db_prepare.create_member(
|
207
256
|
member_dict, self.pool_id, bool(pool.health_monitor))
|
208
257
|
db_member = self._validate_create_member(lock_session, member_dict)
|
@@ -439,6 +488,11 @@ class MembersController(MemberController):
|
|
439
488
|
m.project_id = db_pool.project_id
|
440
489
|
db_member_dict = m.to_dict(render_unsets=False)
|
441
490
|
db_member_dict.pop('id')
|
491
|
+
# We don't allow updating the vnic_type
|
492
|
+
# TODO(johnsom) Give the user an error once we change the
|
493
|
+
# wsme type for batch member update to not use
|
494
|
+
# the MemberPOST type
|
495
|
+
db_member_dict.pop(constants.REQUEST_SRIOV)
|
442
496
|
self.repositories.member.update(
|
443
497
|
context.session, m.id, **db_member_dict)
|
444
498
|
|
@@ -26,6 +26,7 @@ class BaseLoadBalancerType(types.BaseType):
|
|
26
26
|
'vip_network_id': 'vip.network_id',
|
27
27
|
'vip_qos_policy_id': 'vip.qos_policy_id',
|
28
28
|
'vip_vnic_type': 'vip.vnic_type',
|
29
|
+
'vip_sg_ids': 'vip.sg_ids',
|
29
30
|
'admin_state_up': 'enabled'}
|
30
31
|
_child_map = {'vip': {
|
31
32
|
'ip_address': 'vip_address',
|
@@ -33,7 +34,8 @@ class BaseLoadBalancerType(types.BaseType):
|
|
33
34
|
'port_id': 'vip_port_id',
|
34
35
|
'network_id': 'vip_network_id',
|
35
36
|
'qos_policy_id': 'vip_qos_policy_id',
|
36
|
-
'vnic_type': 'vip_vnic_type'
|
37
|
+
'vnic_type': 'vip_vnic_type',
|
38
|
+
'sg_ids': 'vip_sg_ids'}}
|
37
39
|
|
38
40
|
|
39
41
|
class AdditionalVipsType(types.BaseType):
|
@@ -57,6 +59,7 @@ class LoadBalancerResponse(BaseLoadBalancerType):
|
|
57
59
|
vip_port_id = wtypes.wsattr(wtypes.UuidType())
|
58
60
|
vip_subnet_id = wtypes.wsattr(wtypes.UuidType())
|
59
61
|
vip_network_id = wtypes.wsattr(wtypes.UuidType())
|
62
|
+
vip_sg_ids = wtypes.wsattr([wtypes.UuidType()])
|
60
63
|
additional_vips = wtypes.wsattr([AdditionalVipsType])
|
61
64
|
listeners = wtypes.wsattr([types.IdOnlyType])
|
62
65
|
pools = wtypes.wsattr([types.IdOnlyType])
|
@@ -78,6 +81,7 @@ class LoadBalancerResponse(BaseLoadBalancerType):
|
|
78
81
|
result.vip_network_id = data_model.vip.network_id
|
79
82
|
result.vip_qos_policy_id = data_model.vip.qos_policy_id
|
80
83
|
result.vip_vnic_type = data_model.vip.vnic_type
|
84
|
+
result.vip_sg_ids = data_model.vip.sg_ids
|
81
85
|
result.additional_vips = [
|
82
86
|
AdditionalVipsType.from_data_model(i)
|
83
87
|
for i in data_model.additional_vips]
|
@@ -131,6 +135,7 @@ class LoadBalancerPOST(BaseLoadBalancerType):
|
|
131
135
|
vip_subnet_id = wtypes.wsattr(wtypes.UuidType())
|
132
136
|
vip_network_id = wtypes.wsattr(wtypes.UuidType())
|
133
137
|
vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType())
|
138
|
+
vip_sg_ids = wtypes.wsattr([wtypes.UuidType()])
|
134
139
|
additional_vips = wtypes.wsattr([AdditionalVipsType], default=[])
|
135
140
|
project_id = wtypes.wsattr(wtypes.StringType(max_length=36))
|
136
141
|
listeners = wtypes.wsattr([listener.ListenerSingleCreate], default=[])
|
@@ -152,6 +157,7 @@ class LoadBalancerPUT(BaseLoadBalancerType):
|
|
152
157
|
description = wtypes.wsattr(wtypes.StringType(max_length=255))
|
153
158
|
vip_qos_policy_id = wtypes.wsattr(wtypes.UuidType())
|
154
159
|
admin_state_up = wtypes.wsattr(bool)
|
160
|
+
vip_sg_ids = wtypes.wsattr([wtypes.UuidType()])
|
155
161
|
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
|
156
162
|
|
157
163
|
|
octavia/api/v2/types/member.py
CHANGED
@@ -42,6 +42,7 @@ class MemberResponse(BaseMemberType):
|
|
42
42
|
monitor_address = wtypes.wsattr(types.IPAddressType())
|
43
43
|
monitor_port = wtypes.wsattr(wtypes.IntegerType())
|
44
44
|
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
45
|
+
vnic_type = wtypes.wsattr(wtypes.StringType())
|
45
46
|
|
46
47
|
@classmethod
|
47
48
|
def from_data_model(cls, data_model, children=False):
|
@@ -85,6 +86,7 @@ class MemberPOST(BaseMemberType):
|
|
85
86
|
default=None)
|
86
87
|
monitor_address = wtypes.wsattr(types.IPAddressType(), default=None)
|
87
88
|
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
|
89
|
+
request_sriov = wtypes.wsattr(bool, default=False)
|
88
90
|
|
89
91
|
|
90
92
|
class MemberRootPOST(types.BaseType):
|
@@ -129,6 +131,7 @@ class MemberSingleCreate(BaseMemberType):
|
|
129
131
|
minimum=constants.MIN_PORT_NUMBER, maximum=constants.MAX_PORT_NUMBER))
|
130
132
|
monitor_address = wtypes.wsattr(types.IPAddressType())
|
131
133
|
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
|
134
|
+
request_sriov = wtypes.wsattr(bool, default=False)
|
132
135
|
|
133
136
|
|
134
137
|
class MemberStatusResponse(BaseMemberType):
|