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
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@

Check failure on line 1 in components/openstack-sync-operator/crds/neutron.understack.rackspace.net_neutronsegmentranges.yaml

View workflow job for this annotation

GitHub Actions / pre-commit

1:1 [empty-lines] too many blank lines (1 > 0)
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: neutronsegmentranges.neutron.understack.rackspace.net
spec:
group: neutron.understack.rackspace.net
names:
kind: NeutronSegmentRange
listKind: NeutronSegmentRangeList
plural: neutronsegmentranges
shortNames:
- nsr
singular: neutronsegmentrange
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
additionalPrinterColumns:
- name: Name
type: string
jsonPath: .spec.name
- name: Type
type: string
jsonPath: .spec.network_type
- name: Physical
type: string
jsonPath: .spec.physical_network
- name: Min
type: integer
jsonPath: .spec.minimum
- name: Max
type: integer
jsonPath: .spec.maximum
- name: Shared
type: boolean
jsonPath: .spec.shared
- name: SyncStatus
type: string
jsonPath: .status.syncStatus
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
schema:
openAPIV3Schema:
description: >-
NeutronSegmentRange defines one Neutron network segment range
(``openstack network segment range``). The operator finds, adopts, or
creates the range identified by ``spec.name`` and reconciles its
network type, physical network, and minimum/maximum segmentation IDs
toward the spec. Creating a CR claims ownership of the matching
OpenStack segment range. Neutron does not allow updating the
``network_type`` or ``physical_network`` of an existing range, so a
CR that disagrees with an existing range on either field fails loudly
rather than silently diverging.
type: object
required:
- spec
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
type: object
required:
- name
- network_type
- minimum
- maximum
- cloudCredentialsRef
properties:
cloudCredentialsRef:
description: >-
cloudCredentialsRef points to a Kubernetes Secret containing
an OpenStack clouds.yaml file. The operator reads this secret
directly at reconcile time; no volume mount is required.
type: object
required:
- secretName
- cloudName
properties:
secretName:
description: >-
Name of a Secret in the same namespace as this resource.
The Secret must contain a key named clouds.yaml holding
an OpenStack clouds.yaml file.
type: string
minLength: 1
maxLength: 253
cloudName:
description: >-
Name of the cloud entry within the clouds.yaml to
authenticate as.
type: string
minLength: 1
maxLength: 256
name:
description: >-
Neutron network segment range name. This is the identity the
operator uses to find, adopt, create, and prune the range, so
it must be unique per cloud.
type: string
minLength: 1
maxLength: 255
pattern: ^[A-Za-z0-9._/-]+$
network_type:
description: >-
The network type of the segment range. VLAN IDs apply to the
``vlan`` type; tunnel IDs apply to ``geneve``, ``gre`` and
``vxlan``.
type: string
enum:
- vlan
- vxlan
- gre
- geneve
- flat
minLength: 1
maxLength: 32
physical_network:
description: >-
The physical network the segment range is bound to. Required
for ``vlan`` and ``flat`` ranges and must be omitted for the
tunnelled types (``vxlan``, ``gre``, ``geneve``); the operator
enforces this at reconcile time.
type: string
minLength: 1
maxLength: 255
minimum:
description: >-
The minimum segmentation ID in the range (inclusive). Must be
less than or equal to ``maximum``.
type: integer
format: int64
minimum: 1
maximum:
description: >-
The maximum segmentation ID in the range (inclusive). Must be
greater than or equal to ``minimum``.
type: integer
format: int64
minimum: 1
shared:
description: >-
Whether the segment range is shared with all projects. When
false the range is scoped to ``project_id``, which then
becomes required.
type: boolean
default: true
project_id:
description: >-
The project the range is scoped to when ``shared`` is false.
Ignored for shared ranges.
type: string
minLength: 1
maxLength: 255
x-kubernetes-validations:
- rule: self.maximum >= self.minimum
message: maximum must be greater than or equal to minimum
- rule: "self.shared || has(self.project_id)"
message: project_id is required when shared is false
status:
description: NeutronSegmentRangeStatus defines the observed sync state.
type: object
properties:
syncStatus:
description: SyncStatus indicates the synchronization state with Neutron.
type: string
enum:
- Synced
- Failed
- Unknown
lastSyncTime:
description: LastSyncTime is the last time the operator attempted to sync the range.
type: string
format: date-time
observedGeneration:
description: ObservedGeneration is the metadata generation last processed by the operator.
type: integer
format: int64
message:
description: Message provides details about the last sync attempt.
type: string
maxLength: 2048
conditions:
description: Conditions describe current observed state.
type: array
items:
type: object
required:
- type
- status
properties:
type:
type: string
status:
type: string
enum:
- "True"
- "False"
- Unknown
reason:
type: string
message:
type: string
maxLength: 2048
lastTransitionTime:
type: string
format: date-time
subresources:
status: {}
17 changes: 17 additions & 0 deletions components/openstack-sync-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ rbac:
plugins:
openstackPlaceholder: false
neutronRouterFlavors: false
neutronSegmentRanges: false

pluginData:
openstackPlaceholder:
Expand All @@ -55,3 +56,19 @@ pluginData:
# When true, removing a NeutronRouterFlavor CR also deletes its unused
# operator-managed OpenStack flavor. Enable this before removing the CR.
PRUNE: false

neutronSegmentRanges:
hook:
path: /hooks/segment_ranges.py
crd: crds/neutron.understack.rackspace.net_neutronsegmentranges.yaml
envPrefix: NEUTRON_SEGMENT_RANGE
env:
SYNC_CRONTAB: "0 * * * *"
# Neutron readiness wait before a segment range reconcile fails.
# Total wait is READY_RETRIES * READY_DELAY seconds.
READY_RETRIES: 30
READY_DELAY: 10
# When true, removing a NeutronSegmentRange CR also deletes its
# operator-managed OpenStack segment range. Enable this before
# removing the CR.
PRUNE: false
71 changes: 71 additions & 0 deletions python/openstack-sync/openstack_sync/hooks/segment_ranges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Shell-operator hook for Neutron network segment range reconciliation."""

from __future__ import annotations

import sys
from typing import Any

from openstack_sync.hooks.framework import HookConfig
from openstack_sync.hooks.framework import SyncPlugin
from openstack_sync.hooks.framework import build_crd_hook_config
from openstack_sync.hooks.framework import hook_enabled
from openstack_sync.hooks.framework import hook_inputs
from openstack_sync.hooks.framework import run_hook
from openstack_sync.hooks.framework import run_sync
from openstack_sync.plugins.common import wait_for_openstack_network
from openstack_sync.plugins.neutron.segment_ranges import prune as prune_module
from openstack_sync.plugins.neutron.segment_ranges import reconcile as reconcile_module
from openstack_sync.plugins.neutron.segment_ranges.config import BINDING_NAME
from openstack_sync.plugins.neutron.segment_ranges.config import ENV_PREFIX


class SegmentRangePlugin(SyncPlugin):
"""Sync NeutronSegmentRange CRs into Neutron network segment ranges."""

noun = "segment range"

def wait_for_api(self, conn: Any) -> None:
wait_for_openstack_network(
conn,
retries=self.config.ready_retries,
delay=self.config.ready_delay,
)

def new_cache(self) -> reconcile_module.RangeCache:
# Keyed by managed range name and shared across every CR in one
# credential group, so the managed-range listing is fetched once and
# reused by each reconcile and the prune.
return {}

def reconcile(
self, conn: Any, spec: dict[str, Any], cache: reconcile_module.RangeCache
) -> list[str]:
return reconcile_module.sync_segment_range(conn, spec, cache)

def prune(
self,
conn: Any,
desired_specs: list[dict[str, Any]],
*,
authoritative_empty: bool,
) -> None:
if not self.config.prune:
return
prune_module.prune_removed_ranges(
conn, desired_specs, authoritative_empty=authoritative_empty
)


def main() -> int:
def run(contexts: list[dict[str, Any]]) -> int:
if not hook_enabled(ENV_PREFIX):
return 0
config = HookConfig.from_env(ENV_PREFIX, binding_name=BINDING_NAME)
return run_sync(SegmentRangePlugin(config), hook_inputs(contexts, config))

return run_hook(lambda: build_crd_hook_config(ENV_PREFIX, BINDING_NAME), run)


if __name__ == "__main__":
sys.exit(main())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Neutron network segment range sync plugin."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Segment-range plugin constants.

Runtime configuration comes from
:class:`openstack_sync.hooks.framework.HookConfig`, built from the
``NEUTRON_SEGMENT_RANGE`` env prefix the Helm chart injects. The values here are
not configurable at runtime.
"""

from __future__ import annotations

#: Env prefix the Helm chart uses for this plugin's variables.
ENV_PREFIX = "NEUTRON_SEGMENT_RANGE"

#: shell-operator binding label for the CRD watch.
BINDING_NAME = "neutron-segment-ranges"

#: Network types Neutron binds to a physical network. VLAN and flat ranges
#: require ``physical_network``; the tunnelled types must omit it.
PHYSICAL_NETWORK_TYPES = frozenset({"vlan", "flat"})

#: Network types carried over a tunnel, which must not set ``physical_network``.
TUNNEL_NETWORK_TYPES = frozenset({"vxlan", "gre", "geneve"})
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Ownership tracking for operator-managed network segment ranges.

A NeutronSegmentRange CR is an ownership claim for the matching OpenStack
segment range. Unlike Neutron flavors and service profiles, a segment range has
no ``description`` or ``meta_info`` field the operator can stamp -- Neutron's
NetworkSegmentRange resource exposes only ``name``, ``network_type``,
``physical_network``, ``minimum``, ``maximum``, ``shared`` and ``project_id``.

Ownership therefore rides on the range's ``name``. Every range the operator
creates or adopts carries an owner-prefixed name, and prune only ever deletes
ranges whose name carries that prefix. A range created out-of-band with a plain
name is never in the managed set, so it is never pruned.

The prefix is transparent to CR authors: ``spec.name`` is the logical name, and
:func:`managed_name` / :func:`logical_name` translate between the logical name
and the name stored in Neutron.
"""

from __future__ import annotations

from typing import Any

from openstack_sync.plugins.common import get_value

#: Prepended to every operator-managed segment range name in Neutron. Chosen to
#: be unambiguous and to survive Neutron's name length limit (255) with room to
#: spare for a logical name.
NAME_PREFIX = "understack-sr:"


def managed_name(logical_name: str) -> str:
"""Return the Neutron range name for a CR's logical *logical_name*."""
if logical_name.startswith(NAME_PREFIX):
return logical_name
return f"{NAME_PREFIX}{logical_name}"


def logical_name(neutron_name: str) -> str:
"""Return the CR-facing logical name for a Neutron range *neutron_name*."""
if neutron_name.startswith(NAME_PREFIX):
return neutron_name[len(NAME_PREFIX) :]
return neutron_name


def is_managed_range(segment_range: Any) -> bool:
"""Return True when *segment_range*'s name carries the operator prefix."""
name = str(get_value(segment_range, "name", default=""))
return name.startswith(NAME_PREFIX)
Loading
Loading