Skip to content

Commit 0061664

Browse files
committed
border: dedicated VM gateway via type: core
Give Border a type discriminator like the LB (core_agent default | core), calque of net_lb.type. type: core provisions a dedicated small VM gateway (BorderIaasBuilder -> target_node_set from [gservice] border_image); the paas builder then schedules the border_node capability onto that VM's agent. An explicit node pin still wins over type. Adds migration 0068 (type jsonb + ipsv4 varchar[]), ipsv4 read-only in the API, and tests. For small installations that want a produced gateway VM instead of turning the core node into the NAT gateway (core_agent) or pinning to a compute node (border_node).
1 parent 8112bc3 commit 0061664

9 files changed

Lines changed: 379 additions & 11 deletions

File tree

exordos_core/cmd/gservice.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
default="https://repo.exordos.com/exordos-lbaas/0.1.0/exordos-lbaas.raw.zst",
3535
help="URL to get image for LB dataplane VM",
3636
),
37+
cfg.StrOpt(
38+
"border-image",
39+
default=(
40+
"https://repo.exordos.com/exordos-border/0.1.0/exordos-border.raw.zst"
41+
),
42+
help="URL to get the image for a `core` (VM-based) border gateway. "
43+
"The image must ship the universal agent with "
44+
"BorderCapabilityDriver (border_node capability).",
45+
),
3746
]
3847

3948

exordos_core/gservice/service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
from exordos_core.elements.services import builders as em_builders
4949
from exordos_core.janitor import service as janitor_service
5050
from exordos_core.network import service as n_network_service
51+
from exordos_core.network.border.builders import iaas as net_border_iaas
5152
from exordos_core.network.border.builders import paas as net_border_paas
53+
from exordos_core.network.border.dm import models as border_models
5254
from exordos_core.network.lb.builders import iaas as net_lb_iaas
5355
from exordos_core.network.lb.builders import paas as net_lb_paas
5456
from exordos_core.network.lb.dm import models as lb_models
@@ -123,6 +125,10 @@ def __init__(self, iter_min_period=3, iter_pause=0.1):
123125
project_id=nc.NODE_SET_PROJECT,
124126
)
125127
net_lb_paas_builder = net_lb_paas.LBBuilder()
128+
net_border_iaas_builder = net_border_iaas.BorderIaasBuilder(
129+
instance_model=border_models.IaasBorder,
130+
project_id=nc.NODE_SET_PROJECT,
131+
)
126132
net_border_paas_builder = net_border_paas.BorderBuilder()
127133

128134
# Infra scheduler
@@ -226,6 +232,7 @@ def __init__(self, iter_min_period=3, iter_pause=0.1):
226232
service_builder,
227233
net_lb_iaas_builder,
228234
net_lb_paas_builder,
235+
net_border_iaas_builder,
229236
net_border_paas_builder,
230237
secret_svc,
231238
em_builder,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Copyright 2026 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import logging
18+
import typing as tp
19+
import uuid as sys_uuid
20+
21+
from gcl_sdk.agents.universal.dm import models as ua_models
22+
from gcl_sdk.infra import constants as sdk_c
23+
from gcl_sdk.infra.dm import models as sdk_models
24+
from gcl_sdk.infra.services import builder
25+
from oslo_config import cfg
26+
27+
from exordos_core.network.border.dm import models
28+
from exordos_core.network.lb.dm import models as lb_models
29+
30+
LOG = logging.getLogger(__name__)
31+
CONF = cfg.CONF
32+
33+
NODE_SET_KIND = lb_models.TargetNodeSet.get_resource_kind()
34+
35+
36+
class BorderIaasBuilder(builder.CoreInfraBuilder):
37+
"""Provisions the dedicated VM of a `core` (VM-based) border.
38+
39+
Calque of the LB iaas builder: one single-replica target_node_set from
40+
``[gservice] border_image`` (an image whose universal agent ships
41+
BorderCapabilityDriver). The paas builder schedules the ``border_node``
42+
capability onto that VM's agent, same as it does for a pinned node.
43+
"""
44+
45+
_name_prefix = "border"
46+
47+
def __init__(
48+
self,
49+
instance_model: tp.Type[models.IaasBorder],
50+
project_id: sys_uuid.UUID,
51+
):
52+
super().__init__(instance_model)
53+
self._project_id = project_id
54+
55+
def create_infra(
56+
self, instance: models.IaasBorder
57+
) -> tp.Collection[ua_models.TargetResourceKindAwareMixin]:
58+
# A pinned border (node set) never provisions its own VM,
59+
# whatever the type says.
60+
if instance.node or instance.type.kind != "core":
61+
return []
62+
63+
node_set = lb_models.TargetNodeSet(
64+
uuid=instance.uuid,
65+
name=f"{self._name_prefix}-{instance.name}",
66+
cores=instance.type.cpu,
67+
ram=instance.type.ram,
68+
replicas=1,
69+
project_id=self._project_id,
70+
status=sdk_c.NodeStatus.NEW.value,
71+
disk_spec=sdk_models.SetRootDiskSpec(
72+
size=instance.type.disk_size,
73+
image=CONF.gservice.border_image,
74+
),
75+
)
76+
return [node_set]
77+
78+
def actualize_infra(
79+
self,
80+
instance: models.IaasBorder,
81+
infra: builder.InfraCollection,
82+
) -> tp.Collection[ua_models.TargetResourceKindAwareMixin]:
83+
if instance.node or instance.type.kind != "core":
84+
return tuple()
85+
86+
nodeset = None
87+
tgt_nodeset = None
88+
for target, actual in infra.infra_objects:
89+
if target.get_resource_kind() == NODE_SET_KIND:
90+
nodeset = actual
91+
target.cores = instance.type.cpu
92+
target.ram = instance.type.ram
93+
target.replicas = 1
94+
tgt_nodeset = target
95+
break
96+
else:
97+
raise ValueError("A core border must have its target_node_set!")
98+
99+
if nodeset and nodeset.nodes:
100+
instance.ipsv4 = [node["ipv4"] for node in nodeset.nodes.values()]
101+
102+
# The paas builder owns the final ACTIVE/ERROR flip (it tracks the
103+
# border_node capability); here only reflect VM provisioning.
104+
try:
105+
if sdk_c.InstanceStatus(nodeset.status) == sdk_c.InstanceStatus.ERROR:
106+
instance.status = sdk_c.InstanceStatus.ERROR.value
107+
except (ValueError, AttributeError):
108+
pass
109+
110+
return (tgt_nodeset,)

exordos_core/network/border/builders/paas.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@
1616

1717
import logging
1818
import typing as tp
19+
import uuid as sys_uuid
1920

2021
from gcl_sdk.agents.universal.dm import models as ua_models
22+
from gcl_sdk.infra.dm import models as sdk_models
2123
from gcl_sdk.paas.services import builder
24+
from restalchemy.dm import filters as ra_filters
25+
from restalchemy.storage import exceptions as ra_storage_exc
2226

2327
from exordos_core.elements import constants as cc
2428
from exordos_core.network.border.dm import models
29+
from exordos_core.network.lb.dm import models as lb_models
2530

2631
LOG = logging.getLogger(__name__)
2732

33+
NODE_SET_KIND = lb_models.TargetNodeSet.get_resource_kind()
34+
2835

2936
class BorderBuilder(builder.PaaSBuilder):
3037
def __init__(
@@ -45,12 +52,14 @@ def actualize_paas_objects(
4552
instance: models.PaasBorder,
4653
paas_collection: builder.PaaSCollection,
4754
) -> tp.Collection[ua_models.TargetResourceKindAwareMixin]:
48-
"""Produce a single border capability resource.
55+
"""Produce the border capability resource(s).
4956
50-
With ``instance.node`` set it is a ``border_node`` scheduled to that
51-
node's agent (distributed egress, e.g. a managed realm node);
52-
otherwise a ``border_agent`` scheduled by capability to the core
53-
node's agent (step 0).
57+
Deployment shape precedence (see the user_api Border model):
58+
* ``instance.node`` -> ``border_node`` pinned to that node's agent
59+
(distributed egress, e.g. a managed realm node);
60+
* ``type.kind: core`` -> ``border_node`` on the dedicated VM the iaas
61+
builder provisions (net_border_iaas -> target_node_set);
62+
* otherwise -> ``border_agent`` on the core node (step 0).
5463
"""
5564

5665
if instance.node:
@@ -63,6 +72,19 @@ def actualize_paas_objects(
6372
routes=instance.get_routes(),
6473
)
6574
]
75+
elif instance.type.kind == "core":
76+
# Until the iaas builder has the VM up there is nothing to
77+
# schedule; the instance stays IN_PROGRESS and is retried.
78+
actual_resources = [
79+
models.BorderNode(
80+
uuid=sys_uuid.UUID(node_uuid),
81+
agent_uuid=sys_uuid.UUID(node_uuid),
82+
snat_rules=instance.get_snat_rules(),
83+
forwards=instance.get_forwards(),
84+
routes=instance.get_routes(),
85+
)
86+
for node_uuid in self._get_iaas_nodes(instance)
87+
]
6688
else:
6789
actual_resources = [
6890
models.BorderAgent(
@@ -87,3 +109,17 @@ def actualize_paas_objects(
87109
instance.status = cc.ServiceStatus.IN_PROGRESS.value
88110

89111
return actual_resources
112+
113+
def _get_iaas_nodes(self, instance: models.PaasBorder) -> tp.List[str]:
114+
"""Node uuids of the border VM node set (empty until provisioned)."""
115+
try:
116+
res = ua_models.Resource.objects.get_one(
117+
filters={
118+
"uuid": ra_filters.EQ(instance.uuid),
119+
"kind": ra_filters.EQ(NODE_SET_KIND),
120+
}
121+
)
122+
except ra_storage_exc.RecordNotFound:
123+
return []
124+
nodeset = sdk_models.NodeSet.from_ua_resource(res)
125+
return list(nodeset.nodes or {})

exordos_core/network/border/dm/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from restalchemy.dm import properties
2020
from restalchemy.dm import types
2121

22+
from exordos_core.network.lb.dm import models as lb_models
2223
from exordos_core.user_api.network.dm import models
2324

2425

@@ -65,6 +66,33 @@ def get_resource_kind(cls) -> str:
6566
return "border_node"
6667

6768

69+
class IaasBorder(models.Border, ua_models.InstanceWithDerivativesMixin):
70+
"""IaaS view of a `core` (dedicated VM) border.
71+
72+
Mirrors IaasLB: for type.kind == "core" the iaas builder materializes a
73+
single-replica target_node_set (the gateway VM); the paas builder then
74+
schedules the border_node capability to that VM's agent.
75+
"""
76+
77+
__derivative_model_map__ = {
78+
"target_node_set": lb_models.TargetNodeSet,
79+
}
80+
81+
@classmethod
82+
def get_resource_kind(cls) -> str:
83+
return "net_border_iaas"
84+
85+
def get_resource_target_fields(self):
86+
return frozenset(
87+
(
88+
"uuid",
89+
"name",
90+
"type",
91+
"project_id",
92+
)
93+
)
94+
95+
6896
class PaasBorder(models.Border, ua_models.InstanceWithDerivativesMixin):
6997
__derivative_model_map__ = {
7098
"border_agent": BorderAgent,
@@ -82,6 +110,7 @@ def get_resource_target_fields(self):
82110
"name",
83111
"project_id",
84112
"node",
113+
"type",
85114
)
86115
)
87116

exordos_core/tests/unit/network/test_border_builder.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ def test_paasborder_getters_read_inline_rules():
4747
assert border.get_routes() == []
4848

4949

50-
def _instance(snat=None, forwards=None, routes=None, node=None):
50+
def _instance(snat=None, forwards=None, routes=None, node=None, kind="core_agent"):
5151
inst = mock.MagicMock()
5252
inst.uuid = sys_uuid.uuid4()
5353
inst.node = node
54+
inst.type.kind = kind
5455
inst.get_snat_rules.return_value = snat or []
5556
inst.get_forwards.return_value = forwards or []
5657
inst.get_routes.return_value = routes or []
@@ -120,6 +121,81 @@ def test_produces_border_node_when_target_node_set():
120121
assert node_res.forwards == forwards
121122

122123

124+
def test_core_kind_schedules_border_node_per_vm_node():
125+
vm_node = sys_uuid.uuid4()
126+
snat = [{"source_cidr": "10.77.0.0/24", "mode": "masquerade", "snat_to": None}]
127+
inst = _instance(snat=snat, kind="core")
128+
129+
b = border_paas.BorderBuilder()
130+
with mock.patch.object(b, "_get_iaas_nodes", return_value=[str(vm_node)]):
131+
resources = b.create_paas_objects(inst)
132+
133+
assert len(resources) == 1
134+
node_res = resources[0]
135+
assert node_res.get_resource_kind() == "border_node"
136+
# The capability lands on the dedicated VM's agent.
137+
assert node_res.uuid == vm_node
138+
assert node_res.agent_uuid == vm_node
139+
assert node_res.snat_rules == snat
140+
141+
142+
def test_core_kind_defers_until_the_vm_exists():
143+
inst = _instance(kind="core")
144+
145+
b = border_paas.BorderBuilder()
146+
with mock.patch.object(b, "_get_iaas_nodes", return_value=[]):
147+
resources = b.create_paas_objects(inst)
148+
149+
assert resources == []
150+
assert inst.status == cc.ServiceStatus.IN_PROGRESS.value
151+
152+
153+
def test_pinned_node_wins_over_core_kind():
154+
node = sys_uuid.uuid4()
155+
inst = _instance(node=node, kind="core")
156+
157+
resources = border_paas.BorderBuilder().create_paas_objects(inst)
158+
159+
assert len(resources) == 1
160+
assert resources[0].get_resource_kind() == "border_node"
161+
assert resources[0].agent_uuid == node
162+
163+
164+
def test_iaas_builder_provisions_single_vm_for_core_kind():
165+
from oslo_config import cfg
166+
167+
from exordos_core.network.border.builders import iaas as border_iaas
168+
169+
try:
170+
cfg.CONF.register_opts(
171+
[cfg.StrOpt("border_image", default="http://img/border.raw.zst")],
172+
"gservice",
173+
)
174+
except cfg.DuplicateOptError:
175+
pass
176+
177+
inst = _instance(kind="core")
178+
inst.name = "gw"
179+
inst.type.cpu = 2
180+
inst.type.ram = 1024
181+
inst.type.disk_size = 12
182+
183+
b = border_iaas.BorderIaasBuilder.__new__(border_iaas.BorderIaasBuilder)
184+
b._project_id = sys_uuid.uuid4()
185+
186+
objs = b.create_infra(inst)
187+
assert len(objs) == 1
188+
ns = objs[0]
189+
assert ns.get_resource_kind() == "target_node_set"
190+
assert ns.replicas == 1
191+
assert ns.cores == 2 and ns.ram == 1024
192+
assert ns.disk_spec.image.endswith("border.raw.zst")
193+
194+
# core_agent and pinned borders never provision a VM
195+
assert b.create_infra(_instance(kind="core_agent")) == []
196+
assert b.create_infra(_instance(kind="core", node=sys_uuid.uuid4())) == []
197+
198+
123199
def test_status_in_progress_when_no_actual_objects_yet():
124200
inst = _instance()
125201
border_paas.BorderBuilder().actualize_paas_objects(inst, _collection())

exordos_core/user_api/network/api/controllers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class BorderController(
127127
default=field_p.Permissions.RW,
128128
fields={
129129
"status": {constants.ALL: field_p.Permissions.RO},
130+
"ipsv4": {constants.ALL: field_p.Permissions.RO},
130131
},
131132
),
132133
)

0 commit comments

Comments
 (0)