Skip to content

Commit e131e4b

Browse files
authored
Merge pull request #33 from stackhpc/upstream/master-2025-02-24
Synchronise master with upstream
2 parents 1fb8caa + 63be8fd commit e131e4b

27 files changed

+603
-123
lines changed

devstack/plugin.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,10 @@ function octavia_configure {
431431
# Controller side symmetric encryption, not used for PKI
432432
iniset $OCTAVIA_CONF certificates server_certs_key_passphrase insecure-key-do-not-use-this-key
433433

434+
if [[ "$OCTAVIA_USE_ADVANCED_RBAC" == "True" ]]; then
435+
cp $OCTAVIA_DIR/etc/policy/octavia-advanced-rbac-policy.yaml $OCTAVIA_CONF_DIR/policy.yaml
436+
iniset $OCTAVIA_CONF oslo_policy policy_file $OCTAVIA_CONF_DIR/policy.yaml
437+
fi
434438
if [[ "$OCTAVIA_USE_LEGACY_RBAC" == "True" ]]; then
435439
cp $OCTAVIA_DIR/etc/policy/admin_or_owner-policy.yaml $OCTAVIA_CONF_DIR/policy.yaml
436440
iniset $OCTAVIA_CONF oslo_policy policy_file $OCTAVIA_CONF_DIR/policy.yaml
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# This policy YAML file implements the "Advanced RBAC" rules for Octavia that
2+
# were introduced in the Pike release of the Octavia API.
3+
#
4+
# These rules require users to have a load-balancer_* role to be able to access
5+
# the Octavia v2 API.
6+
#
7+
# This is stricter than the "Keystone Default Roles" implemented in the code
8+
# as part of the "Consistent and Secure Default RBAC" OpenStack community goal.
9+
10+
# The default is to not allow access unless the auth_strategy is 'noauth'.
11+
# Users must be a member of one of the following roles to have access to
12+
# the load-balancer API:
13+
#
14+
# role:load-balancer_observer
15+
# User has access to load-balancer read-only APIs
16+
# role:load-balancer_global_observer
17+
# User has access to load-balancer read-only APIs including resources
18+
# owned by others.
19+
# role:load-balancer_member
20+
# User has access to load-balancer read and write APIs
21+
# role:load-balancer_admin
22+
# User is considered an admin for all load-balnacer APIs including
23+
# resources owned by others.
24+
# role:admin
25+
# User is admin to all APIs
26+
27+
"context_is_admin": "role:admin or
28+
role:load-balancer_admin"
29+
30+
# API access roles
31+
32+
"load-balancer:owner": "project_id:%(project_id)s"
33+
34+
# Note: 'is_admin:True' is a policy rule that takes into account the
35+
# auth_strategy == noauth configuration setting.
36+
# It is equivalent to 'rule:context_is_admin or {auth_strategy == noauth}'
37+
38+
"load-balancer:admin": "is_admin:True or
39+
role:admin or
40+
role:load-balancer_admin"
41+
42+
"load-balancer:observer_and_owner": "role:load-balancer_observer and
43+
rule:load-balancer:owner"
44+
45+
"load-balancer:global_observer": "role:load-balancer_global_observer"
46+
47+
"load-balancer:member_and_owner": "role:load-balancer_member and
48+
rule:load-balancer:owner"
49+
50+
# API access methods
51+
52+
"load-balancer:read": "rule:load-balancer:observer_and_owner or
53+
rule:load-balancer:global_observer or
54+
rule:load-balancer:member_and_owner or
55+
rule:load-balancer:admin"
56+
57+
"load-balancer:read-global": "rule:load-balancer:global_observer or
58+
rule:load-balancer:admin"
59+
60+
"load-balancer:write": "rule:load-balancer:member_and_owner or
61+
rule:load-balancer:admin"
62+
63+
"load-balancer:read-quota": "rule:load-balancer:observer_and_owner or
64+
rule:load-balancer:global_observer or
65+
rule:load-balancer:member_and_owner or
66+
role:load-balancer_quota_admin or
67+
rule:load-balancer:admin"
68+
69+
"load-balancer:read-quota-global": "rule:load-balancer:global_observer or
70+
role:load-balancer_quota_admin or
71+
rule:load-balancer:admin"
72+
73+
"load-balancer:write-quota": "role:load-balancer_quota_admin or
74+
rule:load-balancer:admin"

octavia/api/drivers/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from octavia_lib.api.drivers import data_models as driver_dm
1818
from octavia_lib.api.drivers import exceptions as lib_exceptions
19+
from octavia_lib.common import constants as lib_consts
1920
from oslo_config import cfg
2021
from oslo_context import context as oslo_context
2122
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,
130131
new_lb_dict['vip_port_id'] = vip.port_id
131132
new_lb_dict['vip_subnet_id'] = vip.subnet_id
132133
new_lb_dict['vip_qos_policy_id'] = vip.qos_policy_id
134+
new_lb_dict[lib_consts.VIP_SG_IDS] = vip.sg_ids
133135
if 'flavor_id' in lb_dict and lb_dict['flavor_id']:
134136
flavor_repo = repositories.FlavorRepository()
135137
session = db_api.get_session()
@@ -565,6 +567,8 @@ def vip_dict_to_provider_dict(vip_dict):
565567
new_vip_dict['vip_subnet_id'] = vip_dict['subnet_id']
566568
if 'qos_policy_id' in vip_dict:
567569
new_vip_dict['vip_qos_policy_id'] = vip_dict['qos_policy_id']
570+
if constants.SG_IDS in vip_dict:
571+
new_vip_dict[lib_consts.VIP_SG_IDS] = vip_dict[constants.SG_IDS]
568572
if constants.OCTAVIA_OWNED in vip_dict:
569573
new_vip_dict[constants.OCTAVIA_OWNED] = vip_dict[
570574
constants.OCTAVIA_OWNED]
@@ -596,6 +600,8 @@ def provider_vip_dict_to_vip_obj(vip_dictionary):
596600
vip_obj.subnet_id = vip_dictionary['vip_subnet_id']
597601
if 'vip_qos_policy_id' in vip_dictionary:
598602
vip_obj.qos_policy_id = vip_dictionary['vip_qos_policy_id']
603+
if lib_consts.VIP_SG_IDS in vip_dictionary:
604+
vip_obj.sg_ids = vip_dictionary[lib_consts.VIP_SG_IDS]
599605
if constants.OCTAVIA_OWNED in vip_dictionary:
600606
vip_obj.octavia_owned = vip_dictionary[constants.OCTAVIA_OWNED]
601607
return vip_obj

octavia/common/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@
428428
SECURITY_GROUP_RULES = 'security_group_rules'
429429
SERVER_GROUP_ID = 'server_group_id'
430430
SERVER_PEM = 'server_pem'
431+
SG_IDS = 'sg_ids'
431432
SNI_CONTAINER_DATA = 'sni_container_data'
432433
SNI_CONTAINERS = 'sni_containers'
433434
SOFT_ANTI_AFFINITY = 'soft-anti-affinity'

octavia/common/data_models.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ class Vip(BaseDataModel):
558558
def __init__(self, load_balancer_id=None, ip_address=None,
559559
subnet_id=None, network_id=None, port_id=None,
560560
load_balancer=None, qos_policy_id=None, octavia_owned=None,
561-
vnic_type=None):
561+
vnic_type=None, sg_ids=None):
562562
self.load_balancer_id = load_balancer_id
563563
self.ip_address = ip_address
564564
self.subnet_id = subnet_id
@@ -568,6 +568,18 @@ def __init__(self, load_balancer_id=None, ip_address=None,
568568
self.qos_policy_id = qos_policy_id
569569
self.octavia_owned = octavia_owned
570570
self.vnic_type = vnic_type
571+
self.sg_ids = sg_ids or []
572+
573+
def to_dict(self, **kwargs):
574+
ret = super().to_dict(**kwargs)
575+
if kwargs.get('recurse') is not True:
576+
# NOTE(gthiemonge) we need to return the associated SG IDs but as
577+
# sg_ids is a list, they are only added with recurse=True, which
578+
# does a full recursion on the Vip, adding the associated
579+
# LoadBalancer, its Listeners, etc...
580+
# Adding it directly here avoids unnecessary recursion
581+
ret[constants.SG_IDS] = self.sg_ids
582+
return ret
571583

572584

573585
class AdditionalVip(BaseDataModel):
@@ -886,3 +898,9 @@ def __init__(self, listener_id=None, cidr=None):
886898
# object. Otherwise we recurse down the "ghost" listener object.
887899
def to_dict(self, **kwargs):
888900
return {'cidr': self.cidr, 'listener_id': self.listener_id}
901+
902+
903+
class VipSecurityGroup(BaseDataModel):
904+
def __init__(self, load_balancer_id: str = None, sg_id: str = None):
905+
self.load_balancer_id = load_balancer_id
906+
self.sg_id = sg_id

octavia/db/base_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def _get_unique_key(obj):
5959
if obj.__class__.__name__ in ['AdditionalVip']:
6060
return (obj.__class__.__name__ +
6161
obj.load_balancer_id + obj.subnet_id)
62+
if obj.__class__.__name__ in ['VipSecurityGroup']:
63+
return obj.__class__.__name__ + obj.load_balancer_id + obj.sg_id
6264
raise NotImplementedError
6365

6466
def to_data_model(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""add sg_id to vip table
14+
15+
Revision ID: 3097e55493ae
16+
Revises: db2a73e82626
17+
Create Date: 2024-04-05 10:04:32.015445
18+
19+
"""
20+
21+
from alembic import op
22+
import sqlalchemy as sa
23+
24+
25+
# revision identifiers, used by Alembic.
26+
revision = '3097e55493ae'
27+
down_revision = 'db2a73e82626'
28+
29+
30+
def upgrade():
31+
op.create_table(
32+
"vip_security_group",
33+
sa.Column("load_balancer_id", sa.String(36), nullable=False),
34+
sa.Column("sg_id", sa.String(36), nullable=False),
35+
sa.ForeignKeyConstraint(["load_balancer_id"],
36+
["vip.load_balancer_id"],
37+
name="fk_vip_sg_vip_lb_id"),
38+
sa.PrimaryKeyConstraint("load_balancer_id", "sg_id")
39+
)

octavia/db/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,14 @@ class Vip(base_models.BASE):
508508
octavia_owned = sa.Column(sa.Boolean(), nullable=True)
509509
vnic_type = sa.Column(sa.String(64), nullable=True)
510510

511+
sgs = orm.relationship(
512+
"VipSecurityGroup", cascade="all,delete-orphan",
513+
uselist=True, backref=orm.backref("vip", uselist=False))
514+
515+
@property
516+
def sg_ids(self) -> list[str]:
517+
return [sg.sg_id for sg in self.sgs]
518+
511519

512520
class AdditionalVip(base_models.BASE):
513521

@@ -976,3 +984,19 @@ class ListenerCidr(base_models.BASE):
976984
sa.ForeignKey("listener.id", name="fk_listener_cidr_listener_id"),
977985
nullable=False)
978986
cidr = sa.Column(sa.String(64), nullable=False)
987+
988+
989+
class VipSecurityGroup(base_models.BASE):
990+
991+
__data_model__ = data_models.VipSecurityGroup
992+
993+
__tablename__ = "vip_security_group"
994+
__table_args__ = (
995+
sa.PrimaryKeyConstraint('load_balancer_id', 'sg_id'),
996+
)
997+
998+
load_balancer_id = sa.Column(
999+
sa.String(36),
1000+
sa.ForeignKey("vip.load_balancer_id", name="fk_vip_sg_vip_lb_id"),
1001+
nullable=False)
1002+
sg_id = sa.Column(sa.String(64), nullable=False)

octavia/db/repositories.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,17 @@ def create_load_balancer_and_vip(self, session, lb_dict, vip_dict,
275275
lb_dict['id'] = uuidutils.generate_uuid()
276276
lb = models.LoadBalancer(**lb_dict)
277277
session.add(lb)
278+
vip_sg_ids = vip_dict.pop(consts.SG_IDS, [])
278279
vip_dict['load_balancer_id'] = lb_dict['id']
279280
vip = models.Vip(**vip_dict)
280281
session.add(vip)
282+
if vip_sg_ids:
283+
vip_dict[consts.SG_IDS] = vip_sg_ids
284+
for vip_sg_id in vip_sg_ids:
285+
vip_sg = models.VipSecurityGroup(
286+
load_balancer_id=lb_dict['id'],
287+
sg_id=vip_sg_id)
288+
session.add(vip_sg)
281289
for add_vip_dict in additional_vip_dicts:
282290
add_vip_dict['load_balancer_id'] = lb_dict['id']
283291
add_vip_dict['network_id'] = vip_dict.get('network_id')
@@ -734,6 +742,8 @@ def get_all_API_list(self, session, pagination_helper=None, **filters):
734742
query_options = (
735743
subqueryload(models.LoadBalancer.vip),
736744
subqueryload(models.LoadBalancer.additional_vips),
745+
(subqueryload(models.LoadBalancer.vip).
746+
subqueryload(models.Vip.sgs)),
737747
subqueryload(models.LoadBalancer.amphorae),
738748
subqueryload(models.LoadBalancer.pools),
739749
subqueryload(models.LoadBalancer.listeners),
@@ -811,8 +821,24 @@ class VipRepository(BaseRepository):
811821

812822
def update(self, session, load_balancer_id, **model_kwargs):
813823
"""Updates a vip entity in the database by load_balancer_id."""
814-
session.query(self.model_class).filter_by(
815-
load_balancer_id=load_balancer_id).update(model_kwargs)
824+
sg_ids = model_kwargs.pop(consts.SG_IDS, None)
825+
826+
vip = session.query(self.model_class).filter_by(
827+
load_balancer_id=load_balancer_id)
828+
if model_kwargs:
829+
vip.update(model_kwargs)
830+
831+
# NOTE(gthiemonge) the vip must be updated when sg_ids is []
832+
# (removal of current sg_ids)
833+
if sg_ids is not None:
834+
vip = vip.first()
835+
vip.sgs = [
836+
models.VipSecurityGroup(
837+
load_balancer_id=load_balancer_id,
838+
sg_id=sg_id)
839+
for sg_id in sg_ids]
840+
841+
session.flush()
816842

817843

818844
class AdditionalVipRepository(BaseRepository):

octavia/network/base.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@
1313
# under the License.
1414

1515
import abc
16+
import typing
1617

1718
from octavia.common import constants
1819
from octavia.common import exceptions
1920

21+
if typing.TYPE_CHECKING:
22+
from octavia.common import context
23+
import octavia.network.data_models as n_data_models
24+
2025

2126
class NetworkException(exceptions.OctaviaException):
2227
pass
@@ -294,13 +299,25 @@ def get_port_by_net_id_device_id(self, network_id, device_id):
294299

295300
@abc.abstractmethod
296301
def get_security_group(self, sg_name):
297-
"""Retrieves the security group by it's name.
302+
"""Retrieves the security group by its name.
298303
299304
:param sg_name: The security group name.
300305
:return: octavia.network.data_models.SecurityGroup, None if not enabled
301306
:raises: NetworkException, SecurityGroupNotFound
302307
"""
303308

309+
@abc.abstractmethod
310+
def get_security_group_by_id(self, sg_id: str,
311+
context: 'context.RequestContext' = None) -> (
312+
'n_data_models.SecurityGroup'):
313+
"""Retrieves the security group by its id.
314+
315+
:param sg_id: The security group ID.
316+
:param context: A request context
317+
:return: octavia.network.data_models.SecurityGroup, None if not enabled
318+
:raises: NetworkException, SecurityGroupNotFound
319+
"""
320+
304321
@abc.abstractmethod
305322
def failover_preparation(self, amphora):
306323
"""Prepare an amphora for failover.

0 commit comments

Comments
 (0)