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.
Files changed (97) hide show
  1. octavia/amphorae/backends/agent/api_server/keepalivedlvs.py +9 -0
  2. octavia/amphorae/backends/agent/api_server/loadbalancer.py +6 -6
  3. octavia/amphorae/backends/agent/api_server/plug.py +1 -1
  4. octavia/amphorae/backends/agent/api_server/util.py +35 -2
  5. octavia/amphorae/backends/health_daemon/status_message.py +1 -2
  6. octavia/amphorae/drivers/haproxy/rest_api_driver.py +12 -7
  7. octavia/api/drivers/amphora_driver/flavor_schema.py +5 -0
  8. octavia/api/drivers/noop_driver/driver.py +2 -1
  9. octavia/api/drivers/utils.py +12 -0
  10. octavia/api/root_controller.py +8 -2
  11. octavia/api/v2/controllers/base.py +8 -4
  12. octavia/api/v2/controllers/listener.py +12 -2
  13. octavia/api/v2/controllers/load_balancer.py +33 -1
  14. octavia/api/v2/controllers/member.py +58 -4
  15. octavia/api/v2/types/load_balancer.py +7 -1
  16. octavia/api/v2/types/member.py +3 -0
  17. octavia/common/base_taskflow.py +19 -10
  18. octavia/common/clients.py +8 -2
  19. octavia/common/config.py +17 -2
  20. octavia/common/constants.py +6 -0
  21. octavia/common/data_models.py +32 -2
  22. octavia/common/exceptions.py +5 -0
  23. octavia/common/utils.py +4 -1
  24. octavia/common/validate.py +16 -0
  25. octavia/compute/drivers/noop_driver/driver.py +30 -1
  26. octavia/controller/healthmanager/health_manager.py +7 -0
  27. octavia/controller/worker/v2/flows/amphora_flows.py +3 -5
  28. octavia/controller/worker/v2/flows/listener_flows.py +2 -1
  29. octavia/controller/worker/v2/flows/load_balancer_flows.py +38 -0
  30. octavia/controller/worker/v2/taskflow_jobboard_driver.py +34 -6
  31. octavia/controller/worker/v2/tasks/compute_tasks.py +9 -5
  32. octavia/controller/worker/v2/tasks/database_tasks.py +26 -6
  33. octavia/controller/worker/v2/tasks/network_tasks.py +118 -70
  34. octavia/db/base_models.py +29 -5
  35. octavia/db/migration/alembic_migrations/versions/3097e55493ae_add_sg_id_to_vip_table.py +39 -0
  36. octavia/db/migration/alembic_migrations/versions/8db7a6443785_add_member_vnic_type.py +36 -0
  37. octavia/db/migration/alembic_migrations/versions/fabf4983846b_add_member_port_table.py +40 -0
  38. octavia/db/models.py +43 -1
  39. octavia/db/repositories.py +88 -9
  40. octavia/network/base.py +29 -12
  41. octavia/network/data_models.py +2 -1
  42. octavia/network/drivers/neutron/allowed_address_pairs.py +55 -46
  43. octavia/network/drivers/neutron/base.py +28 -16
  44. octavia/network/drivers/neutron/utils.py +2 -2
  45. octavia/network/drivers/noop_driver/driver.py +150 -29
  46. octavia/policies/__init__.py +4 -0
  47. octavia/policies/advanced_rbac.py +95 -0
  48. octavia/policies/base.py +5 -101
  49. octavia/policies/keystone_default_roles.py +81 -0
  50. octavia/policies/loadbalancer.py +13 -0
  51. octavia/tests/common/constants.py +2 -1
  52. octavia/tests/common/sample_data_models.py +27 -14
  53. octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +5 -4
  54. octavia/tests/functional/api/drivers/driver_agent/test_driver_agent.py +2 -1
  55. octavia/tests/functional/api/v2/test_health_monitor.py +1 -1
  56. octavia/tests/functional/api/v2/test_l7policy.py +1 -1
  57. octavia/tests/functional/api/v2/test_listener.py +1 -1
  58. octavia/tests/functional/api/v2/test_load_balancer.py +150 -4
  59. octavia/tests/functional/api/v2/test_member.py +50 -0
  60. octavia/tests/functional/api/v2/test_pool.py +1 -1
  61. octavia/tests/functional/api/v2/test_quotas.py +5 -8
  62. octavia/tests/functional/db/base.py +6 -6
  63. octavia/tests/functional/db/test_models.py +124 -1
  64. octavia/tests/functional/db/test_repositories.py +237 -19
  65. octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py +89 -1
  66. octavia/tests/unit/amphorae/drivers/haproxy/test_rest_api_driver_1_0.py +10 -7
  67. octavia/tests/unit/api/drivers/test_utils.py +6 -1
  68. octavia/tests/unit/certificates/generator/test_local.py +1 -1
  69. octavia/tests/unit/common/test_base_taskflow.py +4 -3
  70. octavia/tests/unit/compute/drivers/noop_driver/test_driver.py +28 -2
  71. octavia/tests/unit/controller/worker/v2/flows/test_load_balancer_flows.py +27 -1
  72. octavia/tests/unit/controller/worker/v2/tasks/test_database_tasks.py +28 -6
  73. octavia/tests/unit/controller/worker/v2/tasks/test_network_tasks.py +100 -79
  74. octavia/tests/unit/controller/worker/v2/test_taskflow_jobboard_driver.py +8 -0
  75. octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +62 -45
  76. octavia/tests/unit/network/drivers/neutron/test_base.py +7 -7
  77. octavia/tests/unit/network/drivers/noop_driver/test_driver.py +55 -42
  78. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/tox.ini +0 -1
  79. {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/AUTHORS +3 -0
  80. octavia-16.0.0.dist-info/METADATA +156 -0
  81. {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/RECORD +95 -90
  82. {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/WHEEL +1 -1
  83. {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/entry_points.txt +1 -1
  84. octavia-16.0.0.dist-info/pbr.json +1 -0
  85. octavia-15.0.0.0rc1.dist-info/METADATA +0 -156
  86. octavia-15.0.0.0rc1.dist-info/pbr.json +0 -1
  87. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/LICENSE +0 -0
  88. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/README.rst +0 -0
  89. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/README.rst +0 -0
  90. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/diskimage-create.sh +0 -0
  91. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/image-tests.sh +0 -0
  92. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/requirements.txt +0 -0
  93. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/test-requirements.txt +0 -0
  94. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/data/share/octavia/diskimage-create/version.txt +0 -0
  95. {octavia-15.0.0.0rc1.data → octavia-16.0.0.data}/scripts/octavia-wsgi +0 -0
  96. {octavia-15.0.0.0rc1.dist-info → octavia-16.0.0.dist-info}/LICENSE +0 -0
  97. {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
- usedforsecurity=False).hexdigest()) # nosec
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
- usedforsecurity=False).hexdigest() # nosec
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 send_vip_advertisements(lb_id):
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
- vips = get_haproxy_vip_addresses(lb_id)
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 secretutils.constant_time_compare(expected_hmc, calculated_hmc):
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(pem, usedforsecurity=False).hexdigest() # nosec
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(crt_list,
465
- usedforsecurity=False).hexdigest() # nosec
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(secret, usedforsecurity=False).hexdigest() # nosec
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(pem, usedforsecurity=False).hexdigest() # nosec
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 []
@@ -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
@@ -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', 'CURRENT',
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, vips, allowed_cidrs):
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(vip_addresses, allowed_cidrs)
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
- member_dict = db_prepare.create_member(member.to_dict(
176
- render_unsets=True), self.pool_id, bool(pool.health_monitor))
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
 
@@ -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):