Skip to content

Commit 7607c0c

Browse files
committed
fix(elements): correct element status actualization after upgrade
Element statuses remained ACTIVE too long after an upgrade because the em_incorrect_resource_statuses_view only compared resource statuses without checking target/actual resource hashes. Now the view considers a resource ACTIVE only when both the actual resource status is ACTIVE and target/actual hashes match, otherwise the resource is IN_PROGRESS. Additionally: - Set element status to IN_PROGRESS explicitly in Manifest.upgrade for better responsiveness - Pass session to ElementIncorrectStatusesView.actualize_status for transactional consistency with resource status updates - Propagate IN_PROGRESS status to Node when machine config changes in NodeBuilderService._update_machine - Check machine images before transitioning to ACTIVE in PoolBuilderService._actualize_machine_status - Add migration 0068 with the updated view definition - Add unit tests for status actualization and Manifest.upgrade Closes #515 Signed-off-by: Anton Kremenetsky <anton.kremenetsky@gmail.com>
1 parent 3b85318 commit 7607c0c

7 files changed

Lines changed: 271 additions & 18 deletions

File tree

exordos_core/compute/builders/node.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ def _update_machine(
120120
machine.status = nc.MachineStatus.IN_PROGRESS.value
121121
machine.update(force=force)
122122

123+
if target_node.status != nc.NodeStatus.IN_PROGRESS.value:
124+
target_node.status = nc.NodeStatus.IN_PROGRESS.value
125+
target_node.save()
126+
123127
def _is_root_volume(self, volume: models.Volume) -> bool:
124128
return volume.index == 0
125129

exordos_core/compute/builders/pool.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,17 @@ def _actualize_machine_status(
421421
pool_machine.status == nc.MachineStatus.ACTIVE
422422
and guest_machine.status == nc.MachineStatus.ACTIVE
423423
):
424-
machine.status = nc.MachineStatus.ACTIVE.value
425-
return
424+
# Transition to ACTIVE only when the machine images are actual.
425+
# Without this check the machine could be marked ACTIVE while
426+
# pool_machine or guest_machine still use stale images from a
427+
# previous version.
428+
resolved_image = self._resolve_image(machine.image)
429+
if (
430+
machine.image == pool_machine.image
431+
and resolved_image == guest_machine.image
432+
):
433+
machine.status = nc.MachineStatus.ACTIVE.value
434+
return
426435

427436
# TODO(akremenetsky): Support more statuses
428437
machine.status = nc.MachineStatus.IN_PROGRESS.value

exordos_core/elements/dm/models.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from restalchemy.dm import relationships
3232
from restalchemy.dm import types as ra_types
3333
from restalchemy.dm import types_dynamic as ra_types_dyn
34-
from restalchemy.storage.sql import engines
3534
from restalchemy.storage.sql import orm
3635

3736
from exordos_core.common import exceptions
@@ -209,6 +208,7 @@ def upgrade(self, session: tp.Any | None = None) -> tuple["Manifest", "Element"]
209208
element.api_version = self.api_version
210209
element.description = self.description
211210
element.manifest = self
211+
element.status = Status.IN_PROGRESS.value
212212
element.save(session=session)
213213
return self.apply_element(element)
214214

@@ -533,20 +533,18 @@ class ElementIncorrectStatusesView(
533533
read_only=True,
534534
)
535535

536-
def actualize_status(self):
537-
engine = engines.engine_factory.get_engine()
538-
with engine.session_manager() as s:
539-
s.execute(
540-
f"""
541-
UPDATE {Element.__tablename__}
542-
SET status = %s
543-
WHERE uuid = %s;
544-
""",
545-
(
546-
self.actual_status,
547-
self.uuid,
548-
),
549-
)
536+
def actualize_status(self, session):
537+
session.execute(
538+
f"""
539+
UPDATE {Element.__tablename__}
540+
SET status = %s
541+
WHERE uuid = %s;
542+
""",
543+
(
544+
self.actual_status,
545+
self.uuid,
546+
),
547+
)
550548

551549

552550
class Requirement(

exordos_core/elements/services/builders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _actualize_statuses(self, session):
6767
em_status_model.api_status,
6868
em_status_model.actual_status,
6969
)
70-
em_status_model.actualize_status()
70+
em_status_model.actualize_status(session)
7171

7272
def _iteration(self):
7373
with contexts.Context().session_manager() as session:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright 2025 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+
from unittest import mock
18+
import uuid as sys_uuid
19+
20+
from restalchemy.storage.sql import orm
21+
22+
from exordos_core.compute import constants as nc
23+
from exordos_core.compute.builders import node as node_builder
24+
from exordos_core.compute.dm import models as compute_models
25+
26+
27+
class TestNodeBuilderService:
28+
def test_update_machine_sets_node_status_to_in_progress(
29+
self,
30+
monkeypatch,
31+
):
32+
service = node_builder.NodeBuilderService()
33+
node_uuid = sys_uuid.uuid4()
34+
35+
target_node = mock.MagicMock()
36+
target_node.uuid = node_uuid
37+
target_node.cores = 2
38+
target_node.ram = 1024
39+
target_node.name = "target-name"
40+
target_node.description = "target-desc"
41+
target_node.hostname = "host-a"
42+
target_node.status = nc.NodeStatus.ACTIVE.value
43+
44+
actual_node = mock.MagicMock()
45+
actual_node.uuid = node_uuid
46+
actual_node.cores = 4
47+
actual_node.ram = 2048
48+
actual_node.name = "actual-name"
49+
actual_node.description = "actual-desc"
50+
actual_node.hostname = "host-b"
51+
52+
machine = mock.MagicMock()
53+
machine.cores = 1
54+
machine.ram = 512
55+
machine.name = "old-name"
56+
machine.description = "old-desc"
57+
58+
def fake_get_one_or_none(self, *args, **kwargs):
59+
if self.model_cls is compute_models.Machine:
60+
return machine
61+
raise NotImplementedError
62+
63+
monkeypatch.setattr(
64+
orm.ObjectCollection, "get_one_or_none", fake_get_one_or_none
65+
)
66+
67+
service._update_machine(target_node, actual_node)
68+
69+
assert machine.cores == target_node.cores
70+
assert machine.ram == target_node.ram
71+
assert machine.name == target_node.name
72+
assert machine.description == target_node.description
73+
assert machine.status == nc.MachineStatus.IN_PROGRESS.value
74+
machine.update.assert_called_once_with(force=True)
75+
assert target_node.status == nc.NodeStatus.IN_PROGRESS.value
76+
target_node.save.assert_called_once_with()

exordos_core/tests/unit/elements/dm/test_element_engine.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@
1414
# License for the specific language governing permissions and limitations
1515
# under the License.
1616

17+
from unittest import mock
1718
import uuid as sys_uuid
1819

1920
import pytest
2021

2122
from exordos_core.common import exceptions
2223
from exordos_core.elements.dm.models import Element
2324
from exordos_core.elements.dm.models import ElementEngine
25+
from exordos_core.elements.dm.models import ElementIncorrectStatusesView
2426
from exordos_core.elements.dm.models import Export
2527
from exordos_core.elements.dm.models import Manifest
2628
from exordos_core.elements.dm.models import Resource
29+
from exordos_core.elements.dm.models import Status
2730
from exordos_core.elements.dm.models import element_engine
2831

2932

@@ -768,3 +771,58 @@ def test_same_element_export_with_different_paths_returns_correct_resource(
768771
assert result is compute_resource
769772
assert result.value["type"] == "compute"
770773
assert result is not storage_resource
774+
775+
776+
class TestElementIncorrectStatusesView:
777+
def test_actualize_status_uses_passed_session(self):
778+
session = mock.MagicMock()
779+
view = ElementIncorrectStatusesView(
780+
uuid=sys_uuid.uuid4(),
781+
actual_status=Status.IN_PROGRESS.value,
782+
)
783+
784+
view.actualize_status(session)
785+
786+
session.execute.assert_called_once()
787+
args, _ = session.execute.call_args
788+
assert "UPDATE em_elements" in args[0]
789+
assert args[1] == (view.actual_status, view.uuid)
790+
791+
792+
class TestManifestUpgrade:
793+
def test_upgrade_sets_element_status_to_in_progress(self, monkeypatch):
794+
manifest = Manifest(
795+
uuid=sys_uuid.uuid4(),
796+
name="test-element",
797+
version="2.0.0",
798+
api_version="v1",
799+
description="test",
800+
)
801+
element = Element(
802+
uuid=sys_uuid.uuid4(),
803+
name="test-element",
804+
version="1.0.0",
805+
api_version="v1",
806+
description="test",
807+
status=Status.ACTIVE.value,
808+
)
809+
810+
def fake_get_one_or_none(self, *args, **kwargs):
811+
if self.model_cls is Element:
812+
return element
813+
raise NotImplementedError
814+
815+
from restalchemy.storage.sql import orm
816+
817+
monkeypatch.setattr(
818+
orm.ObjectCollection, "get_one_or_none", fake_get_one_or_none
819+
)
820+
monkeypatch.setattr(element_engine, "load_from_database", lambda: None)
821+
monkeypatch.setattr(Element, "save", lambda *args, **kwargs: None)
822+
monkeypatch.setattr(
823+
Manifest, "apply_element", lambda self, element: (self, element)
824+
)
825+
826+
manifest.upgrade()
827+
828+
assert element.status == Status.IN_PROGRESS.value
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
from restalchemy.storage.sql import migrations
18+
19+
20+
class MigrationStep(migrations.AbstractMigrationStep):
21+
def __init__(self):
22+
self._depends = [
23+
"0067-init-border-ec37b4.py",
24+
]
25+
26+
@property
27+
def migration_id(self):
28+
return "437c8950-3580-406a-aaae-48f9aeac42c2"
29+
30+
@property
31+
def is_manual(self):
32+
return False
33+
34+
def upgrade(self, session):
35+
session.execute(
36+
"""
37+
CREATE OR REPLACE VIEW "em_incorrect_resource_statuses_view" AS
38+
SELECT
39+
"er"."uuid" AS "uuid",
40+
"er"."status" AS "current_status",
41+
(
42+
CASE
43+
WHEN "utr"."hash" IS NULL THEN "uar"."status"
44+
WHEN "uar"."status" = 'ACTIVE'
45+
AND "utr"."hash" = "uar"."hash" THEN 'ACTIVE'
46+
WHEN "uar"."status" IS NULL THEN NULL
47+
ELSE 'IN_PROGRESS'
48+
END
49+
)::varchar(32) AS "actual_status"
50+
FROM
51+
"em_resources" "er"
52+
LEFT JOIN (
53+
SELECT
54+
"uuid",
55+
"hash"
56+
FROM "ua_target_resources"
57+
WHERE "kind" LIKE 'em_%'
58+
) AS "utr"
59+
ON "er"."uuid" = "utr"."uuid"
60+
LEFT JOIN (
61+
SELECT
62+
"uuid",
63+
"status",
64+
"hash"
65+
FROM "ua_actual_resources"
66+
WHERE "kind" LIKE 'em_%'
67+
) AS "uar"
68+
ON "er"."uuid" = "uar"."uuid"
69+
WHERE
70+
"er"."status" IS DISTINCT FROM (
71+
CASE
72+
WHEN "utr"."hash" IS NULL THEN "uar"."status"
73+
WHEN "uar"."status" = 'ACTIVE'
74+
AND "utr"."hash" = "uar"."hash" THEN 'ACTIVE'
75+
WHEN "uar"."status" IS NULL THEN NULL
76+
ELSE 'IN_PROGRESS'
77+
END
78+
)::varchar(32);
79+
""",
80+
None,
81+
)
82+
83+
def downgrade(self, session):
84+
session.execute(
85+
"""
86+
CREATE OR REPLACE VIEW "em_incorrect_resource_statuses_view" AS
87+
SELECT
88+
"er"."uuid" AS "uuid",
89+
"er"."status" AS "current_status",
90+
"uar"."status" AS "actual_status"
91+
FROM
92+
"em_resources" "er"
93+
LEFT JOIN (
94+
SELECT
95+
"uuid",
96+
"status"
97+
FROM "ua_actual_resources"
98+
WHERE "kind" LIKE 'em_%'
99+
) AS "uar"
100+
ON "er"."uuid" = "uar"."uuid"
101+
WHERE
102+
"er"."status" IS DISTINCT FROM "uar"."status";
103+
""",
104+
None,
105+
)
106+
107+
108+
migration_step = MigrationStep()

0 commit comments

Comments
 (0)