Skip to content

Commit 465d263

Browse files
fix(compute): address PR #494 review comments
- MachinePool.insert(): when an explicit agent_private_key is given and an existing NodeEncryptionKey for the node already differs from it, update the stored key to match instead of silently keeping the stale one. A caller that already generated and deployed a specific key (the bootstrap flow) must end up in sync with the DB - an existing key from unrelated prior provisioning (e.g. this node's own compute Node) would otherwise silently win, leaving the agent unable to authenticate with the key it was actually given. - get_agent_private_key(): raise a clear error for non-local hypervisors (driver_spec without a `node` field) instead of an AttributeError. This is reachable directly through the get_agent_private_key hypervisor action on any hypervisor UUID, regardless of kind.
1 parent daeb2ab commit 465d263

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

exordos_core/compute/dm/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,25 @@ def insert(self, session=None, agent_private_key: str | None = None):
136136
private_key=agent_private_key,
137137
)
138138
private_key.insert(session=session)
139+
elif (
140+
agent_private_key is not None
141+
and existing_keys[0].private_key != agent_private_key
142+
):
143+
# A caller that generated and already deployed a specific
144+
# key (e.g. bootstrap) must end up in sync with the DB -
145+
# an existing key from unrelated prior provisioning (e.g.
146+
# this node's own compute Node) would otherwise silently
147+
# win, leaving the agent unable to authenticate.
148+
existing_keys[0].private_key = agent_private_key
149+
existing_keys[0].save(session=session)
139150

140151
def get_agent_private_key(self):
152+
if not isinstance(self.driver_spec, ExordosLocalHyperDriverSpec):
153+
raise ValueError(
154+
"Agent private key is only available for local hypervisors "
155+
f"(kind={self.driver_spec.KIND})"
156+
)
157+
141158
enc_key = ua_models.NodeEncryptionKey.objects.get_one(
142159
filters={"uuid": dm_filters.EQ(self.driver_spec.node)}
143160
)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
import uuid as sys_uuid
17+
from unittest.mock import MagicMock
18+
from unittest.mock import patch
19+
20+
from gcl_sdk.agents.universal.dm import models as ua_models
21+
import pytest
22+
23+
from exordos_core.compute.dm import models
24+
25+
26+
def _local_hyper_pool(node_uuid: sys_uuid.UUID) -> models.MachinePool:
27+
return models.MachinePool(
28+
driver_spec=models.ExordosLocalHyperDriverSpec(
29+
connection_uri="qemu:///system",
30+
node=node_uuid,
31+
),
32+
)
33+
34+
35+
def _patched_objects(**methods):
36+
# NodeEncryptionKey.objects is a descriptor that builds a fresh
37+
# ObjectCollection on every access, so patching one already-fetched
38+
# instance doesn't affect the production code's own separate access.
39+
# Replace the class-level "objects" attribute itself instead.
40+
return patch.object(ua_models.NodeEncryptionKey, "objects", MagicMock(**methods))
41+
42+
43+
class TestMachinePoolInsert:
44+
def test_creates_a_key_when_none_exists(self):
45+
node_uuid = sys_uuid.uuid4()
46+
pool = _local_hyper_pool(node_uuid)
47+
48+
with (
49+
patch.object(models.orm.SQLStorableMixin, "insert"),
50+
_patched_objects(get_all=MagicMock(return_value=[])),
51+
patch.object(ua_models.NodeEncryptionKey, "insert") as key_insert,
52+
):
53+
pool.insert(agent_private_key="a-generated-key")
54+
55+
key_insert.assert_called_once()
56+
57+
def test_reuses_an_existing_key_when_none_explicitly_given(self):
58+
node_uuid = sys_uuid.uuid4()
59+
pool = _local_hyper_pool(node_uuid)
60+
existing = MagicMock(private_key="already-there")
61+
62+
with (
63+
patch.object(models.orm.SQLStorableMixin, "insert"),
64+
_patched_objects(get_all=MagicMock(return_value=[existing])),
65+
patch.object(ua_models.NodeEncryptionKey, "insert") as key_insert,
66+
):
67+
pool.insert()
68+
69+
key_insert.assert_not_called()
70+
existing.save.assert_not_called()
71+
72+
def test_updates_an_existing_key_that_differs_from_the_explicit_one(self):
73+
# A caller that already generated and deployed a specific key (e.g.
74+
# bootstrap) must end up in sync with the DB - an existing key from
75+
# unrelated prior provisioning (e.g. this node's own compute Node)
76+
# would otherwise silently win, leaving the agent unable to
77+
# authenticate with the key it was actually given.
78+
node_uuid = sys_uuid.uuid4()
79+
pool = _local_hyper_pool(node_uuid)
80+
existing = MagicMock(private_key="stale-key")
81+
82+
with (
83+
patch.object(models.orm.SQLStorableMixin, "insert"),
84+
_patched_objects(get_all=MagicMock(return_value=[existing])),
85+
):
86+
pool.insert(agent_private_key="fresh-key")
87+
88+
assert existing.private_key == "fresh-key"
89+
existing.save.assert_called_once()
90+
91+
def test_does_not_touch_the_db_for_non_local_hypervisors(self):
92+
pool = models.MachinePool(
93+
driver_spec=models.LibvirtPoolDriverSpec(
94+
connection_uri="qemu+tcp://127.0.0.1/system",
95+
),
96+
)
97+
98+
with (
99+
patch.object(models.orm.SQLStorableMixin, "insert"),
100+
_patched_objects() as mocked_objects,
101+
):
102+
pool.insert()
103+
104+
mocked_objects.get_all.assert_not_called()
105+
106+
107+
class TestGetAgentPrivateKey:
108+
def test_returns_the_stored_key_for_a_local_hypervisor(self):
109+
node_uuid = sys_uuid.uuid4()
110+
pool = _local_hyper_pool(node_uuid)
111+
stored = MagicMock(private_key="the-key")
112+
113+
with _patched_objects(get_one=MagicMock(return_value=stored)) as mocked_objects:
114+
assert pool.get_agent_private_key() == "the-key"
115+
116+
mocked_objects.get_one.assert_called_once()
117+
118+
def test_raises_for_non_local_hypervisors(self):
119+
pool = models.MachinePool(
120+
driver_spec=models.LibvirtPoolDriverSpec(
121+
connection_uri="qemu+tcp://127.0.0.1/system",
122+
),
123+
)
124+
125+
with pytest.raises(ValueError, match="only available for local hypervisors"):
126+
pool.get_agent_private_key()

0 commit comments

Comments
 (0)