Skip to content

Commit 693c889

Browse files
committed
refactor(compute): migrate MachinePool driver_spec to KindModelSelectorType
- Replace free-form dict driver_spec with discriminated union of LibvirtPoolDriverSpec, ExordosLocalHyperDriverSpec and DummyPoolDriverSpec kind models - Add ExordosLocalHyperDriverSpec extending LibvirtPoolDriverSpec with required node field for local hypervisor support - Register LocalPoolAgentDriver with local_pool capability - Update LibvirtPoolDriver to accept LibvirtPoolDriverSpec subclasses - Update DummyPoolDriver and MetaPool in agent driver to use typed spec - Fix bootstrap code to use attribute access for connection_uri - Remove obsolete driver_spec filter in scheduler (now always required) - Add pool scheduling logic: exordos_local_hyper pools scheduled on agents with matching node; regular pools exclude local agents - Add migration to rename "driver" key to "kind" in existing data and update updated_at to propagate changes to data plane - Update test fixtures and tests to use new "kind" format - Add unit tests for local/regular pool scheduling logic Signed-off-by: Anton Kremenetsky <anton.kremenetsky@gmail.com>
1 parent 1f396f6 commit 693c889

15 files changed

Lines changed: 519 additions & 101 deletions

File tree

exordos_core/bootstrap/defaults.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,7 @@ def apply_startup_db(spec: dict[str, tp.Any]) -> None:
402402

403403
# Skip if the pool already exists
404404
for _pool in machine_pools:
405-
if (
406-
_pool.driver_spec["connection_uri"]
407-
== pool.driver_spec["connection_uri"]
408-
):
405+
if _pool.driver_spec.connection_uri == pool.driver_spec.connection_uri:
409406
break
410407
else:
411408
# Pool does not exist, create it

exordos_core/cmd/bootstrap.py

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from oslo_config import cfg
3131
from restalchemy.common import config_opts as ra_config_opts
3232
from restalchemy.dm import filters as dm_filters
33-
from restalchemy.storage import exceptions as ra_exceptions
3433
from restalchemy.storage.sql import engines
3534
import yaml
3635

@@ -177,50 +176,6 @@ def _apply_flat_network(stand: dict[str, tp.Any]) -> None:
177176
LOG.info("Created subnet %s", subnet.uuid)
178177

179178

180-
def _apply_startup_db(spec: dict[str, tp.Any]) -> None:
181-
"""Idempotent startup database configuration."""
182-
stand = spec.get("stand", {})
183-
if not stand:
184-
LOG.info("No `stand` section found in %s", spec)
185-
return
186-
187-
# Apply flat network configuration
188-
_apply_flat_network(stand)
189-
190-
# Apply machine pools
191-
# NOTE(akremenetsky): It maybe a problem for large installations
192-
# with many machine pools, but it's fine for now.
193-
machine_pools = models.MachinePool.objects.get_all()
194-
195-
for hypervisor in stand.get("hypervisors", []):
196-
hypervisor["iface_mtu"] = 1500
197-
pool_data = {
198-
"name": "hypervisor",
199-
"machine_type": "VM",
200-
"driver_spec": hypervisor,
201-
}
202-
pool = models.MachinePool.restore_from_simple_view(**pool_data)
203-
204-
# Skip if the pool already exists
205-
for _pool in machine_pools:
206-
if (
207-
_pool.driver_spec["connection_uri"]
208-
== pool.driver_spec["connection_uri"]
209-
):
210-
break
211-
else:
212-
# Pool does not exist, create it
213-
try:
214-
pool.insert()
215-
except ra_exceptions.ConflictRecords:
216-
LOG.info("Machine pool %s already exists", pool.uuid)
217-
else:
218-
LOG.info("Created machine pool %s", pool.uuid)
219-
continue
220-
221-
LOG.info("Machine pool %s already exists, skipping", pool.uuid)
222-
223-
224179
def _ensure_exordos_config(spec: dict[str, tp.Any]):
225180
"""Ensure gctl configuration file exists."""
226181
if "admin_password" not in spec:

exordos_core/compute/agents/universal/drivers/pool.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ class MetaPool(meta.MetaCoordinatorDataPlaneModel):
4646

4747
__driver_map__ = {}
4848

49-
driver_spec = properties.property(types.Dict(), required=True)
49+
driver_spec = properties.property(
50+
types_dynamic.KindModelSelectorType(
51+
types_dynamic.KindModelType(models.LibvirtPoolDriverSpec),
52+
types_dynamic.KindModelType(models.ExordosLocalHyperDriverSpec),
53+
types_dynamic.KindModelType(models.DummyPoolDriverSpec),
54+
),
55+
required=True,
56+
)
5057
machine_type = properties.property(
5158
types.Enum([t.value for t in nc.NodeType]),
5259
default=nc.NodeType.VM.value,
@@ -88,8 +95,7 @@ def load_driver(self) -> driver_base.AbstractPoolDriver:
8895
if driver_key in self.__driver_map__:
8996
return self.__driver_map__[driver_key]
9097

91-
# TODO(akremenetsky): Use dynamic typing for this field
92-
driver_kind = self.driver_spec["driver"]
98+
driver_kind = self.driver_spec.KIND
9399

94100
class_ = utils.load_from_entry_point(nc.EP_MACHINE_POOL_DRIVERS, driver_kind)
95101

@@ -816,3 +822,9 @@ class PoolAgentDriver(meta.MetaCoordinatorAgentDriver):
816822
},
817823
},
818824
}
825+
826+
827+
class LocalPoolAgentDriver(PoolAgentDriver):
828+
def get_capabilities(self) -> list[str]:
829+
"""Returns a list of capabilities supported by the driver."""
830+
return super().get_capabilities() + ["local_pool"]

exordos_core/compute/dm/models.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,60 @@ def has_capacity(self, size: int) -> bool:
102102
return self.available >= size
103103

104104

105+
class AbstractPoolDriverSpec(
106+
types_dynamic.AbstractKindModel,
107+
models.SimpleViewMixin,
108+
):
109+
"""Base class for all pool driver specs."""
110+
111+
112+
class LibvirtPoolDriverSpec(AbstractPoolDriverSpec):
113+
KIND = "libvirt"
114+
115+
connection_uri = properties.property(
116+
types.String(max_length=2048),
117+
required=True,
118+
)
119+
network = properties.property(
120+
types.AllowNone(types.String(max_length=255)),
121+
default=None,
122+
)
123+
storage_pool = properties.property(
124+
types.AllowNone(types.String(max_length=255)),
125+
default=None,
126+
)
127+
machine_prefix = properties.property(
128+
types.AllowNone(types.String(max_length=255)),
129+
default=None,
130+
)
131+
network_type = properties.property(
132+
types.Enum(["network", "bridge"]),
133+
default="network",
134+
)
135+
iface_rom_file = properties.property(
136+
types.AllowNone(types.String(max_length=255)),
137+
default=None,
138+
)
139+
iface_mtu = properties.property(
140+
types.Integer(min_value=0, max_value=65536),
141+
default=1500,
142+
)
143+
iface_source = properties.property(
144+
types.AllowNone(types.String(max_length=255)),
145+
default=None,
146+
)
147+
148+
149+
class ExordosLocalHyperDriverSpec(LibvirtPoolDriverSpec):
150+
KIND = "exordos_local_hyper"
151+
152+
node = properties.property(types.UUID(), required=True)
153+
154+
155+
class DummyPoolDriverSpec(AbstractPoolDriverSpec):
156+
KIND = "dummy"
157+
158+
105159
class ThinStoragePool(
106160
AbstractStoragePool,
107161
models.ModelWithNameDesc,
@@ -146,7 +200,14 @@ class MachinePool(
146200
__tablename__ = "machine_pools"
147201
__driver_map__ = {}
148202

149-
driver_spec = properties.property(types.Dict(), default=dict)
203+
driver_spec = properties.property(
204+
types_dynamic.KindModelSelectorType(
205+
types_dynamic.KindModelType(LibvirtPoolDriverSpec),
206+
types_dynamic.KindModelType(ExordosLocalHyperDriverSpec),
207+
types_dynamic.KindModelType(DummyPoolDriverSpec),
208+
),
209+
required=True,
210+
)
150211
agent = properties.property(types.AllowNone(types.UUID()), default=None)
151212
builder = properties.property(types.AllowNone(types.UUID()), default=None)
152213
machine_type = properties.property(
@@ -174,26 +235,6 @@ class MachinePool(
174235
default=list,
175236
)
176237

177-
@property
178-
def has_driver(self) -> bool:
179-
return bool(self.driver_spec)
180-
181-
@classmethod
182-
def default_hw_pool(cls) -> tp.Optional["MachinePool"]:
183-
"""Get the default pool for HW machines if exists.
184-
185-
The method returns the default pool if only a pool
186-
with required parameters exists and there are not
187-
other pools with similar parameters.
188-
"""
189-
return cls.objects.get_one_or_none(
190-
filters={
191-
"machine_type": dm_filters.EQ(nc.NodeType.HW.value),
192-
"driver_spec": dm_filters.EQ("{}"),
193-
"status": dm_filters.EQ(nc.MachinePoolStatus.ACTIVE.value),
194-
},
195-
)
196-
197238
def load_driver(self) -> tp.Type["AbstractPoolDriver"]:
198239
"""
199240
Load the driver for the machine pool.

exordos_core/compute/pool/drivers/base.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,19 @@ def list_storage_pools(self) -> tp.Collection[models.AbstractStoragePool]:
143143

144144

145145
class DummyPoolDriver(AbstractPoolDriver):
146-
SPEC = {"driver": "dummy"}
147-
148146
def __init__(self, pool: models.MachinePool, dry_run: bool = False):
149-
if pool.driver_spec != self.SPEC:
150-
raise ValueError(f"Invalid driver spec: {pool.driver_spec}")
147+
if pool.driver_spec is None or pool.driver_spec.KIND != "dummy":
148+
raise ValueError(
149+
f"Unsupported driver spec kind: "
150+
f"{pool.driver_spec.KIND if pool.driver_spec else None!r}"
151+
)
151152
super().__init__(dry_run=dry_run)
152153

153154
def get_pool_info(self) -> models.MachinePool:
154155
"""Get pool info."""
155-
return models.MachinePool()
156+
return models.MachinePool(
157+
driver_spec=models.DummyPoolDriverSpec(),
158+
)
156159

157160
def list_pool_resources(
158161
self,
@@ -162,7 +165,13 @@ def list_pool_resources(
162165
tp.Collection[models.MachineVolume],
163166
]:
164167
"""List pool resources."""
165-
return models.MachinePool(), [], []
168+
return (
169+
models.MachinePool(
170+
driver_spec=models.DummyPoolDriverSpec(),
171+
),
172+
[],
173+
[],
174+
)
166175

167176
def list_machines(
168177
self,

exordos_core/compute/pool/drivers/libvirt.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -529,20 +529,16 @@ def add_interface(
529529
)
530530

531531

532-
class LibvirtPoolDriverSpec(tp.NamedTuple):
533-
driver: tp.Literal["libvirt"]
534-
network: str
535-
storage_pool: str
536-
connection_uri: str
537-
machine_prefix: tp.Optional[str] = None
538-
network_type: NetworkType = "network"
539-
iface_rom_file: tp.Optional[str] = None
540-
iface_mtu: int = 1450
541-
542-
543532
class LibvirtPoolDriver(base.AbstractPoolDriver):
544533
def __init__(self, pool: models.MachinePool, dry_run: bool = False):
545-
self._spec = LibvirtPoolDriverSpec(**pool.driver_spec)
534+
if pool.driver_spec is None or not isinstance(
535+
pool.driver_spec, models.LibvirtPoolDriverSpec
536+
):
537+
raise ValueError(
538+
f"Unsupported driver spec kind: "
539+
f"{pool.driver_spec.KIND if pool.driver_spec else None!r}"
540+
)
541+
self._spec = pool.driver_spec
546542
self._pool = pool
547543
# Check if connection string is valid and we can connect
548544
_ = self._client
@@ -860,6 +856,7 @@ def get_pool_info(self) -> models.MachinePool:
860856
"""Get pool info."""
861857
info = self._client.getInfo()
862858
return models.MachinePool(
859+
driver_spec=self._spec,
863860
all_cores=info[2],
864861
all_ram=info[1],
865862
)

exordos_core/compute/scheduler/service.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ def _get_pools(
159159
"status": dm_filters.EQ(nc.MachinePoolStatus.ACTIVE.value),
160160
"machine_type": dm_filters.EQ(nc.NodeType.VM.value),
161161
"builder": dm_filters.IsNot(None),
162-
"driver_spec": dm_filters.NE("{}"),
163162
},
164163
limit=limit,
165164
)
@@ -486,7 +485,33 @@ def _schedule_pools(self, pool_builders: tp.List[ua_models.UniversalAgent]) -> N
486485

487486
for pool in unsheduled:
488487
builder = random.choice(pool_builders)
489-
agent = random.choice(machine_agents[MACHINE_POOL_CAP])
488+
489+
# TODO(akremenetsky): In the target architecture, pool scheduling
490+
# should be done through filters and weighters, similar to node
491+
# scheduling. For now, use a simple condition for specific kinds.
492+
# For exordos_local_hyper pools, only schedule on local agents
493+
# whose node matches the pool's driver_spec.node.
494+
# For other pools, exclude local agents (with local_pool capability)
495+
# since they are dedicated to local hypervisors.
496+
all_agents = machine_agents[MACHINE_POOL_CAP]
497+
if pool.driver_spec.KIND == "exordos_local_hyper":
498+
available_agents = [
499+
a for a in all_agents if a.node == pool.driver_spec.node
500+
]
501+
else:
502+
available_agents = [
503+
a for a in all_agents if "local_pool" not in a.list_capabilities
504+
]
505+
506+
if not available_agents:
507+
LOG.warning(
508+
"No suitable agents found to schedule pool %s (kind=%s)",
509+
pool.uuid,
510+
pool.driver_spec.KIND,
511+
)
512+
continue
513+
514+
agent = random.choice(available_agents)
490515

491516
try:
492517
pool.builder = builder.uuid

exordos_core/tests/functional/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,11 @@ def factory(
559559
**kwargs,
560560
) -> tp.Dict[str, tp.Any]:
561561
uuid = uuid or _make_uuid()
562-
driver_spec = {"driver": "libvirt"} if driver_spec is None else driver_spec
562+
driver_spec = (
563+
{"kind": "libvirt", "connection_uri": "qemu+tcp://127.0.0.1/system"}
564+
if driver_spec is None
565+
else driver_spec
566+
)
563567
status_value = nc.MachinePoolStatus.ACTIVE.value if status is None else status
564568
storage_pool = node_models.ThinStoragePool(
565569
pool_type="dummy",

exordos_core/tests/functional/restapi/compute/test_hypervisor_api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def test_hypervisors_add_several(
115115
hypervisor = pool_factory(
116116
name=f"hypervisor_{i}",
117117
driver_spec={
118-
"driver": "libvirt",
118+
"kind": "libvirt",
119119
"connection_uri": f"qemu+tcp://10.20.0.{str(i + 1)}/system",
120120
},
121121
)
@@ -252,7 +252,7 @@ def test_hypervisors_add_different_connection_uris(
252252

253253
hypervisor1 = pool_factory(
254254
driver_spec={
255-
"driver": "libvirt",
255+
"kind": "libvirt",
256256
"connection_uri": "qemu+tcp://10.20.0.10/system",
257257
},
258258
)
@@ -262,7 +262,7 @@ def test_hypervisors_add_different_connection_uris(
262262

263263
hypervisor2 = pool_factory(
264264
driver_spec={
265-
"driver": "libvirt",
265+
"kind": "libvirt",
266266
"connection_uri": "qemu+tcp://10.20.0.20/system",
267267
},
268268
)
@@ -286,7 +286,7 @@ def test_hypervisors_add_same_connection_uri(
286286

287287
hypervisor1 = pool_factory(
288288
driver_spec={
289-
"driver": "libvirt",
289+
"kind": "libvirt",
290290
"connection_uri": "qemu+tcp://10.20.0.10/system",
291291
},
292292
)
@@ -296,7 +296,7 @@ def test_hypervisors_add_same_connection_uri(
296296

297297
hypervisor2 = pool_factory(
298298
driver_spec={
299-
"driver": "libvirt",
299+
"kind": "libvirt",
300300
"connection_uri": "qemu+tcp://10.20.0.10/system",
301301
},
302302
)

0 commit comments

Comments
 (0)