From 0dff1ed08398a248eaa15eff7aceae3ccf8743fa Mon Sep 17 00:00:00 2001 From: Carlo Lobrano Date: Tue, 1 Sep 2026 09:34:25 +0200 Subject: [PATCH] OSAC-4575: set explicit storageTier on every ComputeInstance in tests After OSAC-3632, AAP will resolve a StorageClass per disk from the disk's storageTier field. When a disk has no tier, provisioning will fail. The OSAC-3633 PR (#367) added the storage tier pattern but only in its new tests; pre-existing VM-provisioning tests still create tier-less instances. Changes: - Add boot_disk_storage_tier parameter to GRPCClient.create_compute_instance and GRPCClient.create_compute_instance_with_disk_image helpers - Move default_storage_tier fixture from tests/vmaas/conftest.py to root tests/conftest.py so all suites (catalog, references) can use it - Update all gRPC-based ComputeInstance creation calls across vmaas, catalog, and references suites to set boot_disk_storage_tier - Update _ci_create_data helper and inline gRPC data in references tests to include boot_disk with storage_tier Assisted-by: Claude Code Signed-off-by: Carlo Lobrano --- tests/conftest.py | 10 +++++++ tests/core/grpc_client.py | 17 ++++++++++-- ...ompute_instance_catalog_item_disk_image.py | 11 +++++--- ...compute_instance_catalog_item_lifecycle.py | 17 ++++++++---- .../e2e/references/test_compute_references.py | 17 +++++++++++- tests/e2e/vmaas/conftest.py | 10 ------- .../vmaas/test_compute_instance_disk_image.py | 27 ++++++++++++++++--- 7 files changed, 84 insertions(+), 25 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 479e848239..f9d692a732 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,6 +25,16 @@ from tests.core.runner import env, run +@pytest.fixture(scope="session") +def default_storage_tier() -> str: + """Reference installer-provided storage tier. + + Defaults to "local" tier created by osac-installer when lvms.enabled=true. + Available to all suites that create ComputeInstances (vmaas, catalog, references). + """ + return env("OSAC_STORAGE_TIER", "local") + + def pytest_configure(config: pytest.Config) -> None: """Give each xdist worker its own log_file so records stay chronologically ordered. diff --git a/tests/core/grpc_client.py b/tests/core/grpc_client.py index 46000ed3e7..d45e909156 100644 --- a/tests/core/grpc_client.py +++ b/tests/core/grpc_client.py @@ -43,9 +43,19 @@ def _build_args(self, *, service: str, data: dict[str, Any] | None = None) -> li def call(self, *, service: str, data: dict[str, Any] | None = None) -> dict[str, Any]: return json.loads(run(*self._build_args(service=service, data=data))) - def create_compute_instance(self, *, catalog_item: str, subnet_ids: list[str], name: str | None = None) -> str: + def create_compute_instance( + self, + *, + catalog_item: str, + subnet_ids: list[str], + name: str | None = None, + boot_disk_storage_tier: str | None = None, + ) -> str: attachments = [{"subnet": {"id": sid}} for sid in subnet_ids] - obj: dict[str, Any] = {"spec": {"catalog_item": {"id": catalog_item}, "network_attachments": attachments}} + spec: dict[str, Any] = {"catalog_item": {"id": catalog_item}, "network_attachments": attachments} + if boot_disk_storage_tier is not None: + spec["boot_disk"] = {"storage_tier": boot_disk_storage_tier} + obj: dict[str, Any] = {"spec": spec} if name is not None: obj["metadata"] = {"name": name} response: dict[str, Any] = self.call(service=f"{PUBLIC_API}.ComputeInstances/Create", data={"object": obj}) @@ -598,6 +608,7 @@ def create_compute_instance_with_disk_image( subnet_ids: list[str], instance_type: str | None = None, name: str | None = None, + boot_disk_storage_tier: str | None = None, ) -> dict[str, Any]: attachments = [{"subnet": {"id": sid}} for sid in subnet_ids] spec: dict[str, Any] = { @@ -607,6 +618,8 @@ def create_compute_instance_with_disk_image( } if instance_type is not None: spec["instance_type"] = {"name": instance_type} + if boot_disk_storage_tier is not None: + spec["boot_disk"] = {"storage_tier": boot_disk_storage_tier} obj: dict[str, Any] = {"spec": spec} if name is not None: obj["metadata"] = {"name": name} diff --git a/tests/e2e/catalog/test_compute_instance_catalog_item_disk_image.py b/tests/e2e/catalog/test_compute_instance_catalog_item_disk_image.py index bb6c3eb1d5..6d3a3347ac 100644 --- a/tests/e2e/catalog/test_compute_instance_catalog_item_disk_image.py +++ b/tests/e2e/catalog/test_compute_instance_catalog_item_disk_image.py @@ -12,7 +12,7 @@ def test_catalog_item_disk_image_default_applied( - grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str + grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str, default_storage_tier: str ) -> None: """AC-2 / TC-FR9-01: CatalogItem disk_image field_definition default is applied to the ComputeInstance.""" di_id: str | None = None @@ -28,10 +28,12 @@ def test_catalog_item_disk_image_default_applied( # network_attachments must also be declared: with field_definitions present, the server # (applyFieldDefinitions) allowlists only catalog_item, template, and the declared fd # paths, then rejects any other spec leaf. The CI-create below sends network_attachments - # (the subnet), so it must be a declared field or the create is rejected InvalidArgument. + # (the subnet) and boot_disk.storage_tier, so both must be declared or the create is + # rejected InvalidArgument. field_defs = [ {"path": "disk_image", "display_name": "Disk Image", "editable": True, "default": di_name}, {"path": "network_attachments", "display_name": "Network", "editable": True}, + {"path": "boot_disk.storage_tier", "display_name": "Boot Disk Storage Tier", "editable": True}, ] catalog_item_id = grpc.create_compute_instance_catalog_item( name=unique_name("e2e-cidi-cat"), @@ -42,7 +44,10 @@ def test_catalog_item_disk_image_default_applied( # disk_image is deliberately omitted — it must be inherited from the catalog default. ci_id = grpc.create_compute_instance( - name=unique_name("e2e-cidi-ci"), catalog_item=catalog_item_id, subnet_ids=[default_subnet_id] + name=unique_name("e2e-cidi-ci"), + catalog_item=catalog_item_id, + subnet_ids=[default_subnet_id], + boot_disk_storage_tier=default_storage_tier, ) ci = grpc.get_compute_instance(ci_id=ci_id) diff --git a/tests/e2e/catalog/test_compute_instance_catalog_item_lifecycle.py b/tests/e2e/catalog/test_compute_instance_catalog_item_lifecycle.py index af7d850143..19746283ec 100644 --- a/tests/e2e/catalog/test_compute_instance_catalog_item_lifecycle.py +++ b/tests/e2e/catalog/test_compute_instance_catalog_item_lifecycle.py @@ -142,7 +142,7 @@ def test_compute_instance_catalog_item_field_definitions( def test_create_compute_instance_with_catalog_item( - grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str + grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str, default_storage_tier: str ) -> None: name = unique_name("e2e-ci-cat") catalog_item_id = grpc.create_compute_instance_catalog_item( @@ -152,7 +152,10 @@ def test_create_compute_instance_with_catalog_item( try: ci_name = unique_name("e2e-ci") ci_id = grpc.create_compute_instance( - name=ci_name, catalog_item=catalog_item_id, subnet_ids=[default_subnet_id] + name=ci_name, + catalog_item=catalog_item_id, + subnet_ids=[default_subnet_id], + boot_disk_storage_tier=default_storage_tier, ) assert ci_id in grpc.list_compute_instance_ids() @@ -173,7 +176,7 @@ def test_create_compute_instance_with_catalog_item( def test_create_compute_instance_with_unpublished_catalog_item_fails( - grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str + grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str, default_storage_tier: str ) -> None: name = unique_name("e2e-ci-unpub") catalog_item_id = grpc.create_compute_instance_catalog_item( @@ -186,6 +189,7 @@ def test_create_compute_instance_with_unpublished_catalog_item_fails( "object": { "spec": { "catalog_item": {"id": catalog_item_id}, + "boot_disk": {"storage_tier": default_storage_tier}, "network_attachments": [{"subnet": {"id": default_subnet_id}}], } } @@ -198,7 +202,7 @@ def test_create_compute_instance_with_unpublished_catalog_item_fails( def test_delete_compute_instance_catalog_item_blocked_when_referenced( - grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str + grpc: GRPCClient, compute_instance_template: str, default_subnet_id: str, default_storage_tier: str ) -> None: name = unique_name("e2e-ci-ref") catalog_item_id = grpc.create_compute_instance_catalog_item( @@ -208,7 +212,10 @@ def test_delete_compute_instance_catalog_item_blocked_when_referenced( try: ci_name = unique_name("e2e-ci") ci_id = grpc.create_compute_instance( - name=ci_name, catalog_item=catalog_item_id, subnet_ids=[default_subnet_id] + name=ci_name, + catalog_item=catalog_item_id, + subnet_ids=[default_subnet_id], + boot_disk_storage_tier=default_storage_tier, ) output, rc = grpc.call_unchecked( diff --git a/tests/e2e/references/test_compute_references.py b/tests/e2e/references/test_compute_references.py index 17815d18eb..e95757c78e 100644 --- a/tests/e2e/references/test_compute_references.py +++ b/tests/e2e/references/test_compute_references.py @@ -66,7 +66,13 @@ def ref_disk_image(grpc: GRPCClient) -> Generator[str, None, None]: def _ci_create_data( - name: str, cat_item_name: str, subnet_name: str, sg_name: str, instance_type: str, disk_image: str + name: str, + cat_item_name: str, + subnet_name: str, + sg_name: str, + instance_type: str, + disk_image: str, + storage_tier: str, ) -> dict[str, Any]: return { "object": { @@ -75,6 +81,7 @@ def _ci_create_data( "catalog_item": {"name": cat_item_name}, "instance_type": {"name": instance_type}, "disk_image": {"name": disk_image}, + "boot_disk": {"storage_tier": storage_tier}, "network_attachments": [ {"subnet": {"name": subnet_name}, "security_groups": [{"name": sg_name}]} ], @@ -95,6 +102,7 @@ def test_compute_instance_full_chain_by_name( ref_ci_catalog_item: str, ref_instance_type: str, ref_disk_image: str, + default_storage_tier: str, ): tag = uuid4().hex[:8] ci_name = f"ref-ci-chain-{tag}" @@ -110,6 +118,7 @@ def test_compute_instance_full_chain_by_name( ref_security_group["name"], ref_instance_type, ref_disk_image, + default_storage_tier, ), ) ci_id = response["object"]["id"] @@ -144,6 +153,7 @@ def test_compute_instance_reaches_running_with_name_refs( ref_ci_catalog_item: str, ref_instance_type: str, ref_disk_image: str, + default_storage_tier: str, ): tag = uuid4().hex[:8] ci_name = f"ref-ci-run-{tag}" @@ -159,6 +169,7 @@ def test_compute_instance_reaches_running_with_name_refs( ref_security_group["name"], ref_instance_type, ref_disk_image, + default_storage_tier, ), ) ci_id = response["object"]["id"] @@ -185,6 +196,7 @@ def test_invalid_subnet_name_returns_array_indexed_field_path( ref_ci_catalog_item: str, ref_instance_type: str, ref_disk_image: str, + default_storage_tier: str, ): tag = uuid4().hex[:8] cat_item = grpc.get_compute_instance_catalog_item(catalog_item_id=ref_ci_catalog_item) @@ -200,6 +212,7 @@ def test_invalid_subnet_name_returns_array_indexed_field_path( "catalog_item": {"name": cat_item_name}, "instance_type": {"name": ref_instance_type}, "disk_image": {"name": ref_disk_image}, + "boot_disk": {"storage_tier": default_storage_tier}, "network_attachments": [ { "subnet": {"name": ref_subnet["name"]}, @@ -221,6 +234,7 @@ def test_cross_tenant_template_reference( ref_security_group: dict[str, str], ref_instance_type: str, ref_disk_image: str, + default_storage_tier: str, ): tag = uuid4().hex[:8] cat_name = f"ref-xt-cat-{tag}" @@ -236,6 +250,7 @@ def test_cross_tenant_template_reference( ref_security_group["name"], ref_instance_type, ref_disk_image, + default_storage_tier, ), ) ci_id = response["object"]["id"] diff --git a/tests/e2e/vmaas/conftest.py b/tests/e2e/vmaas/conftest.py index 19bf24a15c..aa1b32f0b9 100644 --- a/tests/e2e/vmaas/conftest.py +++ b/tests/e2e/vmaas/conftest.py @@ -183,16 +183,6 @@ def default_disk_image(grpc: GRPCClient, test_run_id: str) -> Iterator[str]: raise -@pytest.fixture(scope="session") -def default_storage_tier() -> str: - """ - Reference installer-provided storage tier (matches network_class pattern). - - Defaults to "local" tier created by osac-installer when lvms.enabled=true. - """ - return env("OSAC_STORAGE_TIER", "local") - - @pytest.fixture(scope="session") def additional_storage_tiers(private_grpc: GRPCClient, test_run_id: str) -> Iterator[dict[str, dict[str, str]]]: """ diff --git a/tests/e2e/vmaas/test_compute_instance_disk_image.py b/tests/e2e/vmaas/test_compute_instance_disk_image.py index 7676197a03..46f8a5eb3d 100644 --- a/tests/e2e/vmaas/test_compute_instance_disk_image.py +++ b/tests/e2e/vmaas/test_compute_instance_disk_image.py @@ -24,7 +24,12 @@ def _unique_name(prefix: str = "e2e-cidi") -> str: def test_compute_instance_with_disk_image( - grpc: GRPCClient, vm_template: str, default_subnet: str, default_instance_type: str, k8s_hub_client: K8sClient + grpc: GRPCClient, + vm_template: str, + default_subnet: str, + default_instance_type: str, + k8s_hub_client: K8sClient, + default_storage_tier: str, ) -> None: """AC-1 / TC-FR7-01: Create CI with DiskImage reference, verify VM runs with correct image.""" di_name = _unique_name("e2e-di") @@ -47,6 +52,7 @@ def test_compute_instance_with_disk_image( subnet_ids=[default_subnet], instance_type=default_instance_type, name=_unique_name("e2e-ci"), + boot_disk_storage_tier=default_storage_tier, ) ci_id = response["object"]["id"] assert ci_id, "create_compute_instance should return a non-empty ID" @@ -78,7 +84,7 @@ def test_compute_instance_with_disk_image( def test_obsolete_disk_image_blocks_creation( - grpc: GRPCClient, vm_template: str, default_subnet: str, default_instance_type: str + grpc: GRPCClient, vm_template: str, default_subnet: str, default_instance_type: str, default_storage_tier: str ) -> None: """AC-2 / TC-FR7-04: OBSOLETE DiskImage blocks ComputeInstance creation.""" di_name = _unique_name("e2e-di") @@ -96,6 +102,7 @@ def test_obsolete_disk_image_blocks_creation( disk_image_name=di_name, subnet_ids=[default_subnet], instance_type=default_instance_type, + boot_disk_storage_tier=default_storage_tier, ) assert_grpc_rejected(exc_info, "FailedPrecondition") finally: @@ -104,7 +111,12 @@ def test_obsolete_disk_image_blocks_creation( def test_deprecated_disk_image_allows_creation_with_warning( - grpc: GRPCClient, vm_template: str, default_subnet: str, default_instance_type: str, k8s_hub_client: K8sClient + grpc: GRPCClient, + vm_template: str, + default_subnet: str, + default_instance_type: str, + k8s_hub_client: K8sClient, + default_storage_tier: str, ) -> None: """AC-3 / TC-FR7-05: DEPRECATED DiskImage allows creation with warning.""" di_name = _unique_name("e2e-di") @@ -123,6 +135,7 @@ def test_deprecated_disk_image_allows_creation_with_warning( subnet_ids=[default_subnet], instance_type=default_instance_type, name=_unique_name("e2e-ci"), + boot_disk_storage_tier=default_storage_tier, ) ci_id = response["object"]["id"] assert ci_id, "CI creation with DEPRECATED DiskImage should succeed" @@ -187,7 +200,12 @@ def test_template_disk_image_default(grpc: GRPCClient, private_grpc: GRPCClient) def test_disk_image_deletion_protection( - grpc: GRPCClient, vm_template: str, default_subnet: str, default_instance_type: str, k8s_hub_client: K8sClient + grpc: GRPCClient, + vm_template: str, + default_subnet: str, + default_instance_type: str, + k8s_hub_client: K8sClient, + default_storage_tier: str, ) -> None: """AC-5 / TC-FR12-01 + TC-FR12-04: Deletion protection lifecycle.""" di_name = _unique_name("e2e-di") @@ -204,6 +222,7 @@ def test_disk_image_deletion_protection( subnet_ids=[default_subnet], instance_type=default_instance_type, name=_unique_name("e2e-ci"), + boot_disk_storage_tier=default_storage_tier, ) ci_id = response["object"]["id"] ci_name = wait_for_cr(k8s=k8s_hub_client, uuid=ci_id)