Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/storage/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pytest_testconfig import py_config
from pytest_testconfig import config as py_config

from utilities.constants import ArchImages, Images, StorageClassNames
from utilities.storage import HppCsiStorageClass
Expand Down
231 changes: 231 additions & 0 deletions tests/storage/test_predictible_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
"""
Test for predictable DataVolume and PersistentVolumeClaim names when restoring a VM.

Jira: https://redhat.atlassian.net/browse/CNV-80304
"""
Comment on lines +1 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

HIGH: PR description is missing required template sections.

Per the PR template guidelines, the following required sections must be present with meaningful content:

  • ##### What this PR does / why we need it: (must have meaningful content, not just the description field in the PR body)
  • ##### Which issue(s) this PR fixes:
  • ##### Special notes for reviewer:
  • ##### jira-ticket:

Please restore the missing PR template sections from .github/pull_request_template.md.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/test_predictible_names.py` around lines 1 - 5, The PR is
missing required template sections per .github/pull_request_template.md; update
the pull request description to include the sections "##### What this PR does /
why we need it:", "##### Which issue(s) this PR fixes:", "##### Special notes
for reviewer:", and "##### jira-ticket:" with meaningful content (not just
duplicating the brief description) so reviewers can see motivation, linked
issues (e.g., CNV-80304), any reviewer instructions, and the jira-ticket;
reference the test file name tests/storage/test_predictible_names.py in the
"What this PR does" section to explain the change and why it’s needed.


import pytest
from ocp_resources.datavolume import DataVolume
from ocp_resources.persistent_volume_claim import PersistentVolumeClaim
from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot
from pytest_testconfig import config as py_config

from tests.storage.utils import vm_restore_with_prefix_policy
from utilities.constants import OS_FLAVOR_FEDORA, TIMEOUT_10MIN
from utilities.storage import data_volume_template_with_source_ref_dict
from utilities.virt import VirtualMachineForTests, running_vm

VOLUME_RESTORE_POLICY = "PrefixTargetName"
SOURCE_VM_NAME = "source-fedora-vm"
RESTORED_VM_NAME = "restored-fedora-vm"


@pytest.fixture()
def vm_snapshot_restore_dicts_scope_function(
request,
unprivileged_client,
admin_client,
namespace,
fedora_data_source_scope_module,
):
"""Create VM, snapshot, and restore resources on cluster to verify actual restored names."""
params = getattr(request, "param", {})
source_vm_name = params.get("source_vm_name", SOURCE_VM_NAME)
restored_vm_name = params.get("restored_vm_name", RESTORED_VM_NAME)

with VirtualMachineForTests(
name=source_vm_name,
namespace=namespace.name,
client=unprivileged_client,
os_flavor=OS_FLAVOR_FEDORA,
data_volume_template=data_volume_template_with_source_ref_dict(
data_source=fedora_data_source_scope_module,
storage_class=py_config["default_storage_class"],
),
) as vm:
running_vm(vm=vm, wait_for_interfaces=False, check_ssh_connectivity=False)
source_dv_name = vm.instance.spec.dataVolumeTemplates[0].metadata.name

vm.stop(wait=True)

with VirtualMachineSnapshot(
name=f"{source_vm_name}-snapshot",
namespace=namespace.name,
vm_name=source_vm_name,
client=admin_client,
) as snapshot:
snapshot.wait_ready_to_use(timeout=TIMEOUT_10MIN)

with vm_restore_with_prefix_policy(
name=f"{restored_vm_name}-restore",
namespace=namespace.name,
vm_name=restored_vm_name,
snapshot_name=snapshot.name,
client=admin_client,
prefix_policy=VOLUME_RESTORE_POLICY,
dry_run=False,
) as restore:
restore.wait_restore_done(timeout=TIMEOUT_10MIN)

restored_dvs = list(
DataVolume.get(
dyn_client=admin_client,
namespace=namespace.name,
label_selector=f"restore.kubevirt.io/source-vm-name={source_vm_name}",
)
)
restored_pvcs = list(
PersistentVolumeClaim.get(
dyn_client=admin_client,
namespace=namespace.name,
label_selector=f"restore.kubevirt.io/source-vm-name={source_vm_name}",
)
)

assert restored_dvs, (
f"No DataVolumes found with label restore.kubevirt.io/source-vm-name={source_vm_name}"
)
assert restored_pvcs, f"No PVCs found with label restore.kubevirt.io/source-vm-name={source_vm_name}"

restored_dv_name = restored_dvs[0].name
restored_pvc_name = restored_pvcs[0].name

yield {
"source_dv_name": source_dv_name,
"source_pvc_name": source_dv_name,
"source_vm_name": vm.name,
"restored_vm_name": restored_vm_name,
"restored_dv_name": restored_dv_name,
"restored_pvc_name": restored_pvc_name,
"volumeRestorePolicy": restore.instance.spec.volumeRestorePolicy,
}
Comment on lines +59 to +101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

MAJOR: Restored VM and its DVs/PVCs are never cleaned up by the fixture.

When with vm_restore_with_prefix_policy(...) exits, only the VirtualMachineRestore object is deleted. The restore itself creates a new VM named restored_vm_name along with new DataVolumes and PVCs that are not owned by the restore object and are not deleted by any context manager in the fixture. If the namespace fixture is module- or session-scoped, these resources leak across tests and could cause quota exhaustion or name collisions.

The fixture should clean up the restored VM (which will cascade-delete its owned DVs/PVCs). For example:

with vm_restore_with_prefix_policy(...) as restore:
    restore.wait_restore_done(timeout=TIMEOUT_10MIN)
    # ... collect names ...
    yield {
        ...
    }
    # Teardown: delete the restored VM so its DVs/PVCs are removed
    restored_vm = VirtualMachineForTests(
        name=restored_vm_name,
        namespace=namespace.name,
        client=admin_client,
    )
    if restored_vm.exists:
        restored_vm.delete(wait=True)

Alternatively, wrap the restore in a VirtualMachineForTests context for restored_vm_name if the restore is guaranteed to create that VM.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/test_predictible_names.py` around lines 59 - 101, The fixture
using vm_restore_with_prefix_policy currently yields without deleting the
created restored VM (restored_vm_name), so DataVolumes/PVCs leak; after
yielding, locate the created VM (use
VirtualMachineForTests(name=restored_vm_name, namespace=namespace.name,
client=admin_client) or check via the API) and delete it (if exists, call
delete(wait=True)) so that its owned DVs/PVCs are cascade-removed; alternatively
wrap the restored VM in a VirtualMachineForTests context manager for teardown to
ensure cleanup.



class TestPredictableNamesOnRestore:
"""
Tests for predictable DV and PVC names when restoring VMs with volumeRestorePolicy: PrefixTargetName.

Preconditions:
- Fedora DataSource available in golden images namespace
- Default storage class configured
"""

@pytest.mark.polarion("CNV-80304")
@pytest.mark.usefixtures("vm_snapshot_restore_dicts_scope_function")
def test_restored_dv_and_pvc_names_have_vm_prefix_default_names(self, vm_snapshot_restore_dicts_scope_function):
Comment on lines +113 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

HIGH: Remove redundant @pytest.mark.usefixtures — fixture return value is actively used.

Per the guideline: "Always use @pytest.mark.usefixtures when fixture return value is not used by the test." The return value here is used extensively (lines 139–144). Having both the decorator and the parameter is a violation. Remove the decorator.

🔧 Proposed fix
     `@pytest.mark.polarion`("CNV-80304")
-    `@pytest.mark.usefixtures`("vm_snapshot_restore_dicts_scope_function")
     def test_restored_dv_and_pvc_names_have_vm_prefix_default_names(self, vm_snapshot_restore_dicts_scope_function):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.mark.polarion("CNV-80304")
@pytest.mark.usefixtures("vm_snapshot_restore_dicts_scope_function")
def test_restored_dv_and_pvc_names_have_vm_prefix_default_names(self, vm_snapshot_restore_dicts_scope_function):
`@pytest.mark.polarion`("CNV-80304")
def test_restored_dv_and_pvc_names_have_vm_prefix_default_names(self, vm_snapshot_restore_dicts_scope_function):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/test_predictible_names.py` around lines 113 - 115, The test
function test_restored_dv_and_pvc_names_have_vm_prefix_default_names currently
both declares the fixture vm_snapshot_restore_dicts_scope_function as a function
parameter and also decorates the test with
`@pytest.mark.usefixtures`("vm_snapshot_restore_dicts_scope_function"); remove the
redundant decorator
`@pytest.mark.usefixtures`("vm_snapshot_restore_dicts_scope_function") so the
fixture's return value (accessed inside the test) is provided via the function
parameter only and the test continues to use
vm_snapshot_restore_dicts_scope_function within lines ~139–144.

"""
Verify restored DataVolume and PVC names are prefixed with target VM name.

Preconditions:
- VM created and running from DataSource
- Snapshot created from stopped VM
- Restore created with PrefixTargetName policy

Steps:
1. Extract original DataVolume name from source VM
2. Extract original PVC name from source VM
3. Extract restored DataVolume name from cluster
4. Extract restored PVC name from cluster
5. Verify restored DV name follows pattern "{restored_vm_name}-{source_dv_name}"
6. Verify restored PVC name follows pattern "{restored_vm_name}-{source_pvc_name}"

Expected:
- Restored DataVolume name is "{restored_vm_name}-{source_dv_name}" truncated to 63 characters
- Restored PVC name is "{restored_vm_name}-{source_pvc_name}" truncated to 63 characters

Markers:
- polarion: CNV-80304
"""
source_dv_name = vm_snapshot_restore_dicts_scope_function["source_dv_name"]
source_pvc_name = vm_snapshot_restore_dicts_scope_function["source_pvc_name"]
restored_vm_name = vm_snapshot_restore_dicts_scope_function["restored_vm_name"]
restored_dv_name = vm_snapshot_restore_dicts_scope_function["restored_dv_name"]
restored_pvc_name = vm_snapshot_restore_dicts_scope_function["restored_pvc_name"]
volume_restore_policy = vm_snapshot_restore_dicts_scope_function["volumeRestorePolicy"]

expected_restored_dv_name = f"{restored_vm_name}-{source_dv_name}"[:63]
expected_restored_pvc_name = f"{restored_vm_name}-{source_pvc_name}"[:63]

assert volume_restore_policy == VOLUME_RESTORE_POLICY, (
f"volumeRestorePolicy is '{volume_restore_policy}', expected '{VOLUME_RESTORE_POLICY}'"
)

assert restored_dv_name == expected_restored_dv_name, (
f"Restored DV name is '{restored_dv_name}', expected '{expected_restored_dv_name}'"
)

assert restored_pvc_name == expected_restored_pvc_name, (
f"Restored PVC name is '{restored_pvc_name}', expected '{expected_restored_pvc_name}'"
)
Comment on lines +139 to +159
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Duplicate assertion logic — extract a shared helper.

Lines 139–159 and 211–231 are identical. If the truncation logic or policy check ever changes, both blocks must be updated in sync. Extract a private helper:

♻️ Suggested refactor
`@staticmethod`
def _assert_predictable_names(restore_dicts: dict) -> None:
    source_dv_name = restore_dicts["source_dv_name"]
    source_pvc_name = restore_dicts["source_pvc_name"]
    restored_vm_name = restore_dicts["restored_vm_name"]
    restored_dv_name = restore_dicts["restored_dv_name"]
    restored_pvc_name = restore_dicts["restored_pvc_name"]
    volume_restore_policy = restore_dicts["volumeRestorePolicy"]

    expected_restored_dv_name = f"{restored_vm_name}-{source_dv_name}"[:63]
    expected_restored_pvc_name = f"{restored_vm_name}-{source_pvc_name}"[:63]

    assert volume_restore_policy == VOLUME_RESTORE_POLICY, (
        f"volumeRestorePolicy is '{volume_restore_policy}', expected '{VOLUME_RESTORE_POLICY}'"
    )
    assert restored_dv_name == expected_restored_dv_name, (
        f"Restored DV name is '{restored_dv_name}', expected '{expected_restored_dv_name}'"
    )
    assert restored_pvc_name == expected_restored_pvc_name, (
        f"Restored PVC name is '{restored_pvc_name}', expected '{expected_restored_pvc_name}'"
    )

Then each test becomes a single call:

def test_restored_dv_and_pvc_names_have_vm_prefix_default_names(self, vm_snapshot_restore_dicts_scope_function):
    self._assert_predictable_names(restore_dicts=vm_snapshot_restore_dicts_scope_function)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/test_predictible_names.py` around lines 139 - 159, Extract the
duplicated assertion block into a private helper (e.g., a `@staticmethod` named
_assert_predictable_names) that accepts the restore dict
(vm_snapshot_restore_dicts_scope_function), reads
source_dv_name/source_pvc_name/restored_vm_name/restored_dv_name/restored_pvc_name/volumeRestorePolicy,
computes expected_restored_dv_name and expected_restored_pvc_name using
f"{restored_vm_name}-{source_*}"[:63], and asserts volume_restore_policy ==
VOLUME_RESTORE_POLICY and the two restored name equality checks; then replace
the two identical inline blocks (the ones using source_dv_name, source_pvc_name,
restored_vm_name, restored_dv_name, restored_pvc_name, volumeRestorePolicy) with
calls to
self._assert_predictable_names(vm_snapshot_restore_dicts_scope_function).


@pytest.mark.polarion("CNV-80304b")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

HIGH: CNV-80304b is not a valid Polarion ID.

Polarion test IDs follow the CNV-NNNNN format (all-digit suffix). CNV-80304b with a letter suffix won't map to a real Jira/Polarion ticket and will break result reporting. Either reuse the existing CNV-80304 ID (if this is a sub-test of the same scenario) or obtain a new dedicated Polarion ID for this parametrized test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/test_predictible_names.py` at line 161, The Polarion ID used in
the pytest marker is invalid ("CNV-80304b"); update the pytest.mark.polarion
decorator in tests/storage/test_predictible_names.py (the pytest-marked test
using `@pytest.mark.polarion`) to use a valid CNV-NNNNN format—either change
"CNV-80304b" to the numeric "CNV-80304" if appropriate or replace it with a
newly issued valid Polarion ID so test reporting will map correctly.

@pytest.mark.parametrize(
"vm_snapshot_restore_dicts_scope_function",
[
pytest.param(
{"source_vm_name": "short-vm", "restored_vm_name": "restored-short"},
id="short_names",
),
pytest.param(
{
"source_vm_name": "very-long-source-vm-name-that-might-exceed-limits",
"restored_vm_name": "very-long-restored-vm-name-that-might-exceed-limits",
},
id="long_names_truncated",
),
pytest.param(
{"source_vm_name": "vm-with-numbers-123", "restored_vm_name": "restored-456"},
id="names_with_numbers",
),
],
indirect=True,
)
def test_restored_dv_and_pvc_names_have_vm_prefix_parametrized(self, vm_snapshot_restore_dicts_scope_function):
"""
Verify restored DataVolume and PVC names are prefixed with target VM name for various name combinations.

Preconditions:
- VM created and running from DataSource
- Snapshot created from stopped VM
- Restore created with PrefixTargetName policy
- Test parametrized with different source and restored VM name combinations

Steps:
1. Extract original DataVolume name from source VM
2. Extract original PVC name from source VM
3. Extract restored DataVolume name from cluster
4. Extract restored PVC name from cluster
5. Construct expected DV name as "{restored_vm_name}-{source_dv_name}" truncated to 63 chars
6. Construct expected PVC name as "{restored_vm_name}-{source_pvc_name}" truncated to 63 chars
7. Verify restored DV name matches expected pattern
8. Verify restored PVC name matches expected pattern

Expected:
- Restored DataVolume name is "{restored_vm_name}-{source_dv_name}" truncated to 63 characters
- Restored PVC name is "{restored_vm_name}-{source_pvc_name}" truncated to 63 characters
- Pattern holds for short names, long names, and names with numbers

Markers:
- polarion: CNV-80304b
"""
source_dv_name = vm_snapshot_restore_dicts_scope_function["source_dv_name"]
source_pvc_name = vm_snapshot_restore_dicts_scope_function["source_pvc_name"]
restored_vm_name = vm_snapshot_restore_dicts_scope_function["restored_vm_name"]
restored_dv_name = vm_snapshot_restore_dicts_scope_function["restored_dv_name"]
restored_pvc_name = vm_snapshot_restore_dicts_scope_function["restored_pvc_name"]
volume_restore_policy = vm_snapshot_restore_dicts_scope_function["volumeRestorePolicy"]

expected_restored_dv_name = f"{restored_vm_name}-{source_dv_name}"[:63]
expected_restored_pvc_name = f"{restored_vm_name}-{source_pvc_name}"[:63]

assert volume_restore_policy == VOLUME_RESTORE_POLICY, (
f"volumeRestorePolicy is '{volume_restore_policy}', expected '{VOLUME_RESTORE_POLICY}'"
)

assert restored_dv_name == expected_restored_dv_name, (
f"Restored DV name is '{restored_dv_name}', expected '{expected_restored_dv_name}'"
)

assert restored_pvc_name == expected_restored_pvc_name, (
f"Restored PVC name is '{restored_pvc_name}', expected '{expected_restored_pvc_name}'"
)
48 changes: 47 additions & 1 deletion tests/storage/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import ast
import logging
import shlex
from collections.abc import Generator
from contextlib import contextmanager
from typing import Generator

import requests
from kubernetes.dynamic import DynamicClient
Expand All @@ -21,6 +21,7 @@
from ocp_resources.storage_profile import StorageProfile
from ocp_resources.template import Template
from ocp_resources.upload_token_request import UploadTokenRequest
from ocp_resources.virtual_machine_restore import VirtualMachineRestore
from pyhelper_utils.shell import run_ssh_commands
from pytest_testconfig import config as py_config
from timeout_sampler import TimeoutExpiredError, TimeoutSampler
Expand Down Expand Up @@ -536,3 +537,48 @@ def check_file_in_vm(
vm_console.expect(pattern=file_name, timeout=TIMEOUT_20SEC)
vm_console.sendline(f"cat {file_name}")
vm_console.expect(pattern=file_content, timeout=TIMEOUT_20SEC)


@contextmanager
def vm_restore_with_prefix_policy(
name: str,
namespace: str,
vm_name: str,
snapshot_name: str,
client: DynamicClient,
prefix_policy: str,
dry_run: bool,
**kwargs,
) -> Generator[VirtualMachineRestore]:
Comment on lines +548 to +552
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

HIGH: Fix FBT001 (boolean positional arg) and ANN003 (untyped **kwargs) — the linter must be satisfied per repo guidelines.

The coding guidelines prohibit # noqa suppressions and require fixing the code directly. Both dry_run: bool as a positional arg and the untyped **kwargs violate active Ruff rules. Additionally, Generator[VirtualMachineRestore] provides only one of the three required type parameters for collections.abc.Generator.

🔧 Proposed fix
+from typing import Any
 ...

 `@contextmanager`
 def vm_restore_with_prefix_policy(
     name: str,
     namespace: str,
     vm_name: str,
     snapshot_name: str,
     client: DynamicClient,
     prefix_policy: str,
-    dry_run: bool,
-    **kwargs,
-) -> Generator[VirtualMachineRestore]:
+    *,
+    dry_run: bool = False,
+    **kwargs: Any,
+) -> Generator[VirtualMachineRestore, None, None]:

The * separator makes dry_run keyword-only (FBT001), adding = False makes it optional since False is the only value callers pass. **kwargs: Any addresses ANN003. Generator[VirtualMachineRestore, None, None] provides the required YieldType, SendType, ReturnType params.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
client: DynamicClient,
prefix_policy: str,
dry_run: bool,
**kwargs,
) -> Generator[VirtualMachineRestore]:
client: DynamicClient,
prefix_policy: str,
*,
dry_run: bool = False,
**kwargs: Any,
) -> Generator[VirtualMachineRestore, None, None]:
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 550-550: Boolean-typed positional argument in function definition

(FBT001)


[warning] 551-551: Missing type annotation for **kwargs

(ANN003)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storage/utils.py` around lines 548 - 552, The function signature
currently exposes dry_run as a positional boolean, leaves **kwargs untyped, and
uses an incomplete Generator type; update the signature to make dry_run
keyword-only and optional by adding a '*' before it and setting dry_run: bool =
False, change **kwargs to **kwargs: Any, and update the return annotation to
Generator[VirtualMachineRestore, None, None]; also ensure Any is imported from
typing (or typing_extensions) if not already.

"""
Creates VirtualMachineRestore with volumeRestorePolicy: PrefixTargetName.

This allows restoring snapshots to new VMs without overwriting original PVCs.
The restored PVCs will be prefixed with the target VM name.

Args:
name: Restore object name
namespace: Kubernetes namespace
client: Kubernetes client (must be admin)
vm_name: Target VM name (will be created/updated)
snapshot_name: VirtualMachineSnapshot name to restore from
prefix_policy: VolumeRestorePolicy to use
**kwargs: Additional arguments for VirtualMachineRestore

Yields:
VirtualMachineRestore: Configured restore object
"""
restore = VirtualMachineRestore(
name=name,
namespace=namespace,
vm_name=vm_name,
snapshot_name=snapshot_name,
client=client,
dry_run=dry_run,
**kwargs,
)
restore.to_dict()
restore.res["spec"]["volumeRestorePolicy"] = prefix_policy

with restore:
yield restore