Skip to content

Commit f2d1bb2

Browse files
author
Geetika Batra
committed
feat(openstack-sync): add NeutronSegmentRange CRD and sync plugin
Add a NeutronSegmentRange CRD to the openstack-sync-operator and a segment_ranges hook plus plugin package that reconciles Neutron network segment ranges from custom resources, built on the new SyncPlugin framework. - CRD: neutron.understack.rackspace.net/v1alpha1 NeutronSegmentRange - plugin: reconcile/create/adopt by owner-prefixed name, prune on removal - immutable network_type/physical_network mismatches fail loudly - wired into operator values.yaml (plugins.neutronSegmentRanges, disabled) - prune driven by framework CleanupPolicy (no manual config.prune gate)
1 parent 61c03e0 commit f2d1bb2

10 files changed

Lines changed: 758 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
---
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
name: neutronsegmentranges.neutron.understack.rackspace.net
6+
spec:
7+
group: neutron.understack.rackspace.net
8+
names:
9+
kind: NeutronSegmentRange
10+
listKind: NeutronSegmentRangeList
11+
plural: neutronsegmentranges
12+
shortNames:
13+
- nsr
14+
singular: neutronsegmentrange
15+
scope: Namespaced
16+
versions:
17+
- name: v1alpha1
18+
served: true
19+
storage: true
20+
additionalPrinterColumns:
21+
- name: Name
22+
type: string
23+
jsonPath: .spec.name
24+
- name: Type
25+
type: string
26+
jsonPath: .spec.network_type
27+
- name: Physical
28+
type: string
29+
jsonPath: .spec.physical_network
30+
- name: Min
31+
type: integer
32+
jsonPath: .spec.minimum
33+
- name: Max
34+
type: integer
35+
jsonPath: .spec.maximum
36+
- name: Shared
37+
type: boolean
38+
jsonPath: .spec.shared
39+
- name: SyncStatus
40+
type: string
41+
jsonPath: .status.syncStatus
42+
- name: Age
43+
type: date
44+
jsonPath: .metadata.creationTimestamp
45+
schema:
46+
openAPIV3Schema:
47+
description: >-
48+
NeutronSegmentRange defines one Neutron network segment range
49+
(``openstack network segment range``). The operator finds, adopts, or
50+
creates the range identified by ``spec.name`` and reconciles its
51+
network type, physical network, and minimum/maximum segmentation IDs
52+
toward the spec. Creating a CR claims ownership of the matching
53+
OpenStack segment range. Neutron does not allow updating the
54+
``network_type`` or ``physical_network`` of an existing range, so a
55+
CR that disagrees with an existing range on either field fails loudly
56+
rather than silently diverging.
57+
type: object
58+
required:
59+
- spec
60+
properties:
61+
apiVersion:
62+
type: string
63+
kind:
64+
type: string
65+
metadata:
66+
type: object
67+
spec:
68+
type: object
69+
required:
70+
- name
71+
- network_type
72+
- minimum
73+
- maximum
74+
- cloudCredentialsRef
75+
properties:
76+
cloudCredentialsRef:
77+
description: >-
78+
cloudCredentialsRef points to a Kubernetes Secret containing
79+
an OpenStack clouds.yaml file. The operator reads this secret
80+
directly at reconcile time; no volume mount is required.
81+
type: object
82+
required:
83+
- secretName
84+
- cloudName
85+
properties:
86+
secretName:
87+
description: >-
88+
Name of a Secret in the same namespace as this resource.
89+
The Secret must contain a key named clouds.yaml holding
90+
an OpenStack clouds.yaml file.
91+
type: string
92+
minLength: 1
93+
maxLength: 253
94+
cloudName:
95+
description: >-
96+
Name of the cloud entry within the clouds.yaml to
97+
authenticate as.
98+
type: string
99+
minLength: 1
100+
maxLength: 256
101+
name:
102+
description: >-
103+
Neutron network segment range name. This is the identity the
104+
operator uses to find, adopt, create, and prune the range, so
105+
it must be unique per cloud.
106+
type: string
107+
minLength: 1
108+
maxLength: 255
109+
pattern: ^[A-Za-z0-9._/-]+$
110+
network_type:
111+
description: >-
112+
The network type of the segment range. VLAN IDs apply to the
113+
``vlan`` type; tunnel IDs apply to ``geneve``, ``gre`` and
114+
``vxlan``.
115+
type: string
116+
enum:
117+
- vlan
118+
- vxlan
119+
- gre
120+
- geneve
121+
minLength: 1
122+
maxLength: 32
123+
physical_network:
124+
description: >-
125+
The physical network the segment range is bound to. Required
126+
for ``vlan`` ranges and must be omitted for the tunnelled
127+
types (``vxlan``, ``gre``, ``geneve``); the operator enforces
128+
this at reconcile time.
129+
type: string
130+
minLength: 1
131+
maxLength: 255
132+
minimum:
133+
description: >-
134+
The minimum segmentation ID in the range (inclusive). Must be
135+
less than or equal to ``maximum``.
136+
type: integer
137+
format: int64
138+
minimum: 1
139+
maximum:
140+
description: >-
141+
The maximum segmentation ID in the range (inclusive). Must be
142+
greater than or equal to ``minimum``.
143+
type: integer
144+
format: int64
145+
minimum: 1
146+
shared:
147+
description: >-
148+
Whether the segment range is shared with all projects. When
149+
false the range is scoped to ``project_id``, which then
150+
becomes required.
151+
type: boolean
152+
default: true
153+
project_id:
154+
description: >-
155+
The project the range is scoped to when ``shared`` is false.
156+
Ignored for shared ranges.
157+
type: string
158+
minLength: 1
159+
maxLength: 255
160+
x-kubernetes-validations:
161+
- rule: self.maximum >= self.minimum
162+
message: maximum must be greater than or equal to minimum
163+
- rule: "self.shared || has(self.project_id)"
164+
message: project_id is required when shared is false
165+
status:
166+
description: NeutronSegmentRangeStatus defines the observed sync state.
167+
type: object
168+
properties:
169+
syncStatus:
170+
description: SyncStatus indicates the synchronization state with Neutron.
171+
type: string
172+
enum:
173+
- Synced
174+
- Failed
175+
- Unknown
176+
lastSyncTime:
177+
description: LastSyncTime is the last time the operator attempted to sync the range.
178+
type: string
179+
format: date-time
180+
observedGeneration:
181+
description: ObservedGeneration is the metadata generation last processed by the operator.
182+
type: integer
183+
format: int64
184+
message:
185+
description: Message provides details about the last sync attempt.
186+
type: string
187+
maxLength: 2048
188+
conditions:
189+
description: Conditions describe current observed state.
190+
type: array
191+
items:
192+
type: object
193+
required:
194+
- type
195+
- status
196+
properties:
197+
type:
198+
type: string
199+
status:
200+
type: string
201+
enum:
202+
- "True"
203+
- "False"
204+
- Unknown
205+
reason:
206+
type: string
207+
message:
208+
type: string
209+
maxLength: 2048
210+
lastTransitionTime:
211+
type: string
212+
format: date-time
213+
subresources:
214+
status: {}

‎components/openstack-sync-operator/values.yaml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ plugins:
3232
neutronRouterFlavors: false
3333
neutronSubnetPools: false
3434
ironicRunbooks: false
35+
neutronSegmentRanges: false
3536

3637
pluginData:
3738
openstackPlaceholder:
@@ -83,3 +84,19 @@ pluginData:
8384
# When true, removing an IronicRunbook CR also deletes its
8485
# operator-owned Ironic runbook. Enable this before removing the CR.
8586
PRUNE: false
87+
88+
neutronSegmentRanges:
89+
hook:
90+
path: /hooks/segment_ranges.py
91+
crd: crds/neutron.understack.rackspace.net_neutronsegmentranges.yaml
92+
envPrefix: NEUTRON_SEGMENT_RANGE
93+
env:
94+
SYNC_CRONTAB: "0 * * * *"
95+
# Neutron readiness wait before a segment range reconcile fails.
96+
# Total wait is READY_RETRIES * READY_DELAY seconds.
97+
READY_RETRIES: 30
98+
READY_DELAY: 10
99+
# When true, removing a NeutronSegmentRange CR also deletes its
100+
# operator-managed OpenStack segment range. Enable this before
101+
# removing the CR.
102+
PRUNE: false

‎containers/openstack-sync-operator/Dockerfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ COPY --chmod=755 python/openstack-sync/openstack_sync/hooks/placeholder.py /hook
2020
COPY --chmod=755 python/openstack-sync/openstack_sync/hooks/router_flavors.py /hooks/router_flavors.py
2121
COPY --chmod=755 python/openstack-sync/openstack_sync/hooks/subnet_pools.py /hooks/subnet_pools.py
2222
COPY --chmod=755 python/openstack-sync/openstack_sync/hooks/ironic_runbooks.py /hooks/ironic_runbooks.py
23+
COPY --chmod=755 python/openstack-sync/openstack_sync/hooks/segment_ranges.py /hooks/segment_ranges.py
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
"""Shell-operator hook for Neutron network segment range reconciliation."""
3+
4+
from __future__ import annotations
5+
6+
import sys
7+
from typing import Any
8+
9+
from openstack_sync.hooks.framework import HookConfig
10+
from openstack_sync.hooks.framework import SyncPlugin
11+
from openstack_sync.hooks.framework import build_crd_hook_config
12+
from openstack_sync.hooks.framework import hook_enabled
13+
from openstack_sync.hooks.framework import hook_inputs
14+
from openstack_sync.hooks.framework import run_hook
15+
from openstack_sync.hooks.framework import run_sync
16+
from openstack_sync.plugins.common import wait_for_openstack_network
17+
from openstack_sync.plugins.neutron.segment_ranges import prune as prune_module
18+
from openstack_sync.plugins.neutron.segment_ranges import reconcile as reconcile_module
19+
from openstack_sync.plugins.neutron.segment_ranges.config import BINDING_NAME
20+
from openstack_sync.plugins.neutron.segment_ranges.config import ENV_PREFIX
21+
22+
23+
class SegmentRangePlugin(SyncPlugin):
24+
"""Sync NeutronSegmentRange CRs into Neutron network segment ranges."""
25+
26+
noun = "segment range"
27+
28+
def wait_for_api(self, conn: Any) -> None:
29+
wait_for_openstack_network(
30+
conn,
31+
retries=self.config.ready_retries,
32+
delay=self.config.ready_delay,
33+
)
34+
35+
def new_cache(self) -> reconcile_module.RangeCache:
36+
# Keyed by managed range name and shared across every CR in one
37+
# credential group, so the managed-range listing is fetched once and
38+
# reused by each reconcile and the prune.
39+
return {}
40+
41+
def reconcile(
42+
self, conn: Any, spec: dict[str, Any], cache: reconcile_module.RangeCache
43+
) -> list[str]:
44+
return reconcile_module.sync_segment_range(conn, spec, cache)
45+
46+
def prune(
47+
self,
48+
conn: Any,
49+
desired_specs: list[dict[str, Any]],
50+
*,
51+
authoritative_empty: bool,
52+
) -> None:
53+
# The framework only calls prune under a cleanup policy that runs it,
54+
# which for this plugin means the chart's PRUNE flag is enabled (see
55+
# SyncPlugin.cleanup_policy: config.prune + a real prune step ->
56+
# FINALIZED_PRUNE). No manual gate on config.prune is needed here.
57+
prune_module.prune_removed_ranges(
58+
conn, desired_specs, authoritative_empty=authoritative_empty
59+
)
60+
61+
62+
def main() -> int:
63+
def run(contexts: list[dict[str, Any]]) -> int:
64+
if not hook_enabled(ENV_PREFIX):
65+
return 0
66+
config = HookConfig.from_env(ENV_PREFIX, binding_name=BINDING_NAME)
67+
return run_sync(SegmentRangePlugin(config), hook_inputs(contexts, config))
68+
69+
return run_hook(lambda: build_crd_hook_config(ENV_PREFIX, BINDING_NAME), run)
70+
71+
72+
if __name__ == "__main__":
73+
sys.exit(main())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Neutron network segment range sync plugin."""
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Segment-range plugin constants.
2+
3+
Runtime configuration comes from
4+
:class:`openstack_sync.hooks.framework.HookConfig`, built from the
5+
``NEUTRON_SEGMENT_RANGE`` env prefix the Helm chart injects. The values here are
6+
not configurable at runtime.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
#: Env prefix the Helm chart uses for this plugin's variables.
12+
ENV_PREFIX = "NEUTRON_SEGMENT_RANGE"
13+
14+
#: shell-operator binding label for the CRD watch.
15+
BINDING_NAME = "neutron-segment-ranges"
16+
17+
#: Network types Neutron binds to a physical network. VLAN ranges require
18+
#: ``physical_network``; the tunnelled types must omit it.
19+
PHYSICAL_NETWORK_TYPES = frozenset({"vlan"})
20+
21+
#: Network types carried over a tunnel, which must not set ``physical_network``.
22+
TUNNEL_NETWORK_TYPES = frozenset({"vxlan", "gre", "geneve"})
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Ownership tracking for operator-managed network segment ranges.
2+
3+
A NeutronSegmentRange CR is an ownership claim for the matching OpenStack
4+
segment range. Unlike Neutron flavors and service profiles, a segment range has
5+
no ``description`` or ``meta_info`` field the operator can stamp -- Neutron's
6+
NetworkSegmentRange resource exposes only ``name``, ``network_type``,
7+
``physical_network``, ``minimum``, ``maximum``, ``shared`` and ``project_id``.
8+
9+
Ownership therefore rides on the range's ``name``. Every range the operator
10+
creates or adopts carries an owner-prefixed name, and prune only ever deletes
11+
ranges whose name carries that prefix. A range created out-of-band with a plain
12+
name is never in the managed set, so it is never pruned.
13+
14+
The prefix is transparent to CR authors: ``spec.name`` is the logical name, and
15+
:func:`managed_name` / :func:`logical_name` translate between the logical name
16+
and the name stored in Neutron.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from typing import Any
22+
23+
from openstack_sync.plugins.common import get_value
24+
25+
#: Prepended to every operator-managed segment range name in Neutron. Chosen to
26+
#: be unambiguous and to survive Neutron's name length limit (255) with room to
27+
#: spare for a logical name.
28+
NAME_PREFIX = "understack-sr:"
29+
30+
31+
def managed_name(logical_name: str) -> str:
32+
"""Return the Neutron range name for a CR's logical *logical_name*."""
33+
if logical_name.startswith(NAME_PREFIX):
34+
return logical_name
35+
return f"{NAME_PREFIX}{logical_name}"
36+
37+
38+
def logical_name(neutron_name: str) -> str:
39+
"""Return the CR-facing logical name for a Neutron range *neutron_name*."""
40+
if neutron_name.startswith(NAME_PREFIX):
41+
return neutron_name[len(NAME_PREFIX) :]
42+
return neutron_name
43+
44+
45+
def is_managed_range(segment_range: Any) -> bool:
46+
"""Return True when *segment_range*'s name carries the operator prefix."""
47+
name = str(get_value(segment_range, "name", default=""))
48+
return name.startswith(NAME_PREFIX)

0 commit comments

Comments
 (0)