Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ retreive = "retreive"
metadat = "metadat"
# operators/rook/values-cluster.yaml mentions "ceph-blockpool-ecoded"
ecoded = "ecoded"
# lazy option is not a typo
selectin = "selectin"
4 changes: 4 additions & 0 deletions python/neutron-understack/neutron_understack/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__version__ = "0.1"

# Neutron's extension loader looks for ``<service_plugin_root>.extensions`` as
# an attribute on the imported top-level package.
from neutron_understack import extensions # noqa: F401
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from neutron_lib import constants
from neutron_lib.api import converters
from neutron_lib.api.definitions import l3

ALIAS = "understack_vni"
IS_SHIM_EXTENSION = False
IS_STANDARD_ATTR_EXTENSION = False
NAME = "Understack Router VNI"
API_PREFIX = ""
DESCRIPTION = "Router extension for Understack hardware VRF VNI allocation"
UPDATED_TIMESTAMP = "2026-07-01T00:00:00-00:00"
RESOURCE_NAME = l3.ROUTER
COLLECTION_NAME = l3.ROUTERS

EVPN_VNI = "evpn_vni"

RESOURCE_ATTRIBUTE_MAP = {
COLLECTION_NAME: {
EVPN_VNI: {
"allow_post": True,
"allow_put": False,
"convert_to": converters.convert_to_int_if_not_none,
"default": 0,
"is_visible": True,
"is_filter": True,
"is_sort_key": True,
"enforce_policy": True,
"validate": {"type:range_or_none": [0, constants.MAX_VXLAN_VNI]},
},
},
}

SUB_RESOURCE_ATTRIBUTE_MAP = {}
ACTION_MAP = {}
REQUIRED_EXTENSIONS = [l3.ALIAS]
OPTIONAL_EXTENSIONS = []
ACTION_STATUS = {}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import itertools

from neutron_understack.conf.policies import evpn


def list_rules():
return itertools.chain(
evpn.list_rules(),
)
34 changes: 34 additions & 0 deletions python/neutron-understack/neutron_understack/conf/policies/evpn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from neutron.conf.policies import base
from oslo_policy import policy

COLLECTION_PATH = "/routers"
RESOURCE_PATH = "/routers/{id}"

ACTION_POST = [
{"method": "POST", "path": COLLECTION_PATH},
]
ACTION_GET = [
{"method": "GET", "path": COLLECTION_PATH},
{"method": "GET", "path": RESOURCE_PATH},
]

rules = [
policy.DocumentedRuleDefault(
name="create_router:evpn_vni",
check_str=base.ADMIN,
scope_types=["project"],
description="Specify ``evpn_vni`` attribute when creating a router",
operations=ACTION_POST,
),
policy.DocumentedRuleDefault(
name="get_router:evpn_vni",
check_str=base.ADMIN_OR_PROJECT_READER,
scope_types=["project"],
description="Get ``evpn_vni`` attribute of a router",
operations=ACTION_GET,
),
]


def list_rules():
return rules
27 changes: 27 additions & 0 deletions python/neutron-understack/neutron_understack/config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import contextlib

from keystoneauth1 import loading as ks_loading
from keystoneauth1 import session as ks_session
from oslo_config import cfg

_OPT_GRP_ML2_UNDERSTACK = "ml2_understack"
_OPT_GRP_IRONIC = "ironic"
_OPT_GRP_L3_SVC_CISCO_ASA = "l3_service_cisco_asa"
_OPT_GRP_UNDERSTACK_VNI = "understack_vni"

_mech_understack_opts = [
cfg.StrOpt(
Expand Down Expand Up @@ -99,6 +102,19 @@
),
]

_understack_vni_opts = [
cfg.ListOpt(
"vni_ranges",
default=["1:16777215"],
item_type=cfg.types.String(),
help=(
"Comma-separated list of VNI ranges available for automatic "
"Understack VRF router VNI allocation. Each entry is either a "
"single VNI or an inclusive start:end range."
),
),
]


def list_understack_opts():
return [
Expand All @@ -125,6 +141,12 @@ def list_cisco_asa_opts():
]


def list_understack_vni_opts():
return [
(_OPT_GRP_UNDERSTACK_VNI, _understack_vni_opts),
]


def register_ml2_understack_opts(config):
config.register_opts(_mech_understack_opts, _OPT_GRP_ML2_UNDERSTACK)

Expand All @@ -139,6 +161,11 @@ def register_l3_svc_cisco_asa_opts(config):
config.register_opts(_l3_svc_cisco_asa_opts, _OPT_GRP_L3_SVC_CISCO_ASA)


def register_understack_vni_opts(config):
with contextlib.suppress(cfg.DuplicateOptError):
config.register_opts(_understack_vni_opts, _OPT_GRP_UNDERSTACK_VNI)


def get_session(group: str) -> ks_session.Session:
auth = ks_loading.load_auth_from_conf_options(cfg.CONF, group)
session = ks_loading.load_session_from_conf_options(cfg.CONF, group, auth=auth)
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from logging.config import fileConfig

import sqlalchemy as sa
from alembic import context
from neutron_lib.db import model_base
from oslo_config import cfg
from oslo_db.sqlalchemy import session
from sqlalchemy import event

VERSION_TABLE = "neutron_understack_alembic_version"
MYSQL_ENGINE = None
config = context.config
neutron_config = config.neutron_config
fileConfig(config.config_file_name)
target_metadata = model_base.BASEV2.metadata


def set_mysql_engine():
try:
mysql_engine = neutron_config.command.mysql_engine
except cfg.NoSuchOptError:
mysql_engine = None

global MYSQL_ENGINE # noqa: PLW0603
MYSQL_ENGINE = mysql_engine or model_base.BASEV2.__table_args__["mysql_engine"]


def run_migrations_offline():
set_mysql_engine()
kwargs = {"version_table": VERSION_TABLE}
if neutron_config.database.connection:
kwargs["url"] = neutron_config.database.connection
else:
kwargs["dialect_name"] = neutron_config.database.engine
context.configure(**kwargs)

with context.begin_transaction():
context.run_migrations()


@event.listens_for(sa.Table, "after_parent_attach")
def set_storage_engine(target, parent):
if MYSQL_ENGINE:
target.kwargs["mysql_engine"] = MYSQL_ENGINE


def run_migrations_online():
set_mysql_engine()
engine = session.create_engine(neutron_config.database.connection)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata,
version_table=VERSION_TABLE,
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
engine.dispose()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright ${create_date.year} Understack Developers
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#

"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}

"""

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}


# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
% if branch_labels:
branch_labels = ${repr(branch_labels)}
% endif


def upgrade():
${upgrades if upgrades else "pass"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Start neutron-understack contract branch.

Revision ID: 8f5b7d0f4b2b
Revises: start_neutron_understack
Create Date: 2026-07-01 00:00:00.000000

"""

from neutron.db.migration import cli

revision = "8f5b7d0f4b2b"
down_revision = "start_neutron_understack"
branch_labels = (cli.CONTRACT_BRANCH,)


def upgrade():
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Add Understack router VNI allocation table.

Revision ID: 5b7f7d0f4b2a
Revises: start_neutron_understack
Create Date: 2026-07-01 00:00:00.000000

"""

import sqlalchemy as sa
from neutron.db import migration
from neutron.db.migration import cli

revision = "5b7f7d0f4b2a"
down_revision = "start_neutron_understack"
branch_labels = (cli.EXPAND_BRANCH,)


def upgrade():
migration.create_table_if_not_exists(
"understack_router_vni_allocations",
sa.Column("vni", sa.Integer(), autoincrement=False, nullable=False),
sa.Column("router_id", sa.String(length=36), nullable=True),
sa.Column(
"allocated_at",
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
),
sa.Column("released_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["router_id"], ["routers.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("vni"),
sa.UniqueConstraint(
"router_id",
name="uniq_understack_router_vni_allocations0router_id",
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
8f5b7d0f4b2b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5b7f7d0f4b2a
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Start neutron-understack migration chain.

Revision ID: start_neutron_understack
Revises: None
Create Date: 2026-07-01 00:00:00.000000

"""

revision = "start_neutron_understack"
down_revision = None


def upgrade():
pass
43 changes: 43 additions & 0 deletions python/neutron-understack/neutron_understack/db/understack_vni.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sqlalchemy as sa
from neutron_lib.db import model_base
from sqlalchemy import orm


class UnderstackRouterVNIAllocation(model_base.BASEV2):
__tablename__ = "understack_router_vni_allocations"

vni = sa.Column(sa.Integer(), primary_key=True, autoincrement=False)
router_id = sa.Column(
sa.String(36),
sa.ForeignKey("routers.id", ondelete="SET NULL"),
nullable=True,
)
allocated_at = sa.Column(
sa.DateTime(),
nullable=False,
server_default=sa.func.now(),
)
released_at = sa.Column(sa.DateTime(), nullable=True)

__table_args__ = (
sa.UniqueConstraint(
"router_id",
name="uniq_understack_router_vni_allocations0router_id",
),
model_base.BASEV2.__table_args__,
)

router = orm.relationship(
"Router",
lazy="noload",
load_on_pending=True,
viewonly=True,
backref=orm.backref(
"understack_vni_allocation",
lazy="selectin",
uselist=False,
viewonly=True,
),
)

revises_on_change = ("router",)
Empty file.
Loading
Loading