From 04a275f2d35a3f59d37f8e722c692a507e014ef6 Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sun, 2 Nov 2025 16:42:28 +0200 Subject: [PATCH 01/12] release: New Release 4.20.0 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 87def38108..f3389b3d10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "11.0.101" +version = "4.20.0" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 05c85a19b7..ff718374c6 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "11.0.101" +version = "4.20.0" source = { editable = "." } dependencies = [ { name = "cloup" }, From b85e5420762683872b30379b66daa1e10875b73f Mon Sep 17 00:00:00 2001 From: rnetser Date: Mon, 3 Nov 2025 11:29:37 +0200 Subject: [PATCH 02/12] release: New Release 4.20.1 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3389b3d10..b2b8c12a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "4.20.0" +version = "4.20.1" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index ff718374c6..09f21187c7 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "4.20.0" +version = "4.20.1" source = { editable = "." } dependencies = [ { name = "cloup" }, From d51e194d9f947c50fd6e013466f941e3a76287e1 Mon Sep 17 00:00:00 2001 From: "Redhat-QE[bot]" <108792105+redhat-qe-bot@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:21:16 +0200 Subject: [PATCH 03/12] Add retry on resource creation (#2572) (#2579) Co-authored-by: Ruth Netser --- ocp_resources/resource.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ocp_resources/resource.py b/ocp_resources/resource.py index 45d400ca8d..32fb75d68d 100644 --- a/ocp_resources/resource.py +++ b/ocp_resources/resource.py @@ -1000,15 +1000,18 @@ def wait_for_status( self.logger.error(f"Status of {self.kind} {self.name} is {current_status}") raise - def create(self, wait: bool = False) -> ResourceInstance | None: + def create( + self, wait: bool = False, exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS + ) -> ResourceInstance | None: """ Create resource. Args: wait (bool) : True to wait for resource status. + exceptions_dict (dict[type[Exception], list[str]]): Dictionary of exceptions to retry on. Returns: - bool: True if create succeeded, False otherwise. + ResourceInstance | None: Created resource instance or None if create failed. """ self.to_dict() @@ -1020,10 +1023,13 @@ def create(self, wait: bool = False) -> ResourceInstance | None: self.logger.info(f"Create {self.kind} {self.name}") self.logger.info(f"Posting {hashed_res}") self.logger.debug(f"\n{yaml.dump(hashed_res)}") - resource_kwargs = {"body": self.res, "namespace": self.namespace} + resource_kwargs: dict[str, Any] = {"body": self.res, "namespace": self.namespace} if self.dry_run: resource_kwargs["dry_run"] = "All" - resource_ = self.api.create(**resource_kwargs) + + resource_ = Resource.retry_cluster_exceptions( + func=self.api.create, exceptions_dict=exceptions_dict, **resource_kwargs + ) with contextlib.suppress(ForbiddenError, AttributeError, NotFoundError): # some resources do not support get() (no instance) or the client do not have permissions self.initial_resource_version = self.instance.metadata.resourceVersion From c3d2e8323e3acda348403c7a3ff0e9c09f586173 Mon Sep 17 00:00:00 2001 From: "Redhat-QE[bot]" <108792105+redhat-qe-bot@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:39:29 +0200 Subject: [PATCH 04/12] Fix: Project - allow retry (#2591) (#2594) Co-authored-by: Ruth Netser --- .github/workflows/code-check.yml.old | 25 ------------------------- ocp_resources/project_request.py | 12 +++++++++++- ocp_resources/resource.py | 14 +++++++++----- 3 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 .github/workflows/code-check.yml.old diff --git a/.github/workflows/code-check.yml.old b/.github/workflows/code-check.yml.old deleted file mode 100644 index 6fbbb2bdb9..0000000000 --- a/.github/workflows/code-check.yml.old +++ /dev/null @@ -1,25 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: code-check -on: - pull_request: -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Run tox - run: | - tox -e code-check,unittests,validate-resources diff --git a/ocp_resources/project_request.py b/ocp_resources/project_request.py index 0b53ad3feb..6ba0e39858 100644 --- a/ocp_resources/project_request.py +++ b/ocp_resources/project_request.py @@ -2,8 +2,11 @@ from typing import Any +from kubernetes.dynamic.exceptions import ForbiddenError + from ocp_resources.project_project_openshift_io import Project from ocp_resources.resource import Resource +from ocp_resources.utils.constants import DEFAULT_CLUSTER_RETRY_EXCEPTIONS, PROTOCOL_ERROR_EXCEPTION_DICT class ProjectRequest(Resource): @@ -54,7 +57,14 @@ def deploy(self, wait: bool = False) -> Project: # type: ignore[override] teardown=self.teardown, delete_timeout=self.delete_timeout, ) - project.wait_for_status(status=project.Status.ACTIVE) + + # When a ProjectRequest is created, the project is created and the user is added to the project. + # RBAC binding may not have propagated yet, causing transient 403 Forbidden errors, so we need to retry + # on ForbiddenError as well. + project.wait_for_status( + status=project.Status.ACTIVE, + exceptions_dict=PROTOCOL_ERROR_EXCEPTION_DICT | DEFAULT_CLUSTER_RETRY_EXCEPTIONS | {ForbiddenError: []}, + ) return project diff --git a/ocp_resources/resource.py b/ocp_resources/resource.py index 32fb75d68d..bee3f70c07 100644 --- a/ocp_resources/resource.py +++ b/ocp_resources/resource.py @@ -953,7 +953,13 @@ def _kube_v1_api(self) -> kubernetes.client.CoreV1Api: return kubernetes.client.CoreV1Api(api_client=self.client.client) def wait_for_status( - self, status: str, timeout: int = TIMEOUT_4MINUTES, stop_status: str | None = None, sleep: int = 1 + self, + status: str, + timeout: int = TIMEOUT_4MINUTES, + stop_status: str | None = None, + sleep: int = 1, + exceptions_dict: dict[type[Exception], list[str]] = PROTOCOL_ERROR_EXCEPTION_DICT + | DEFAULT_CLUSTER_RETRY_EXCEPTIONS, ) -> None: """ Wait for resource to be in status @@ -962,6 +968,7 @@ def wait_for_status( status (str): Expected status. timeout (int): Time to wait for the resource. stop_status (str): Status which should stop the wait and failed. + exceptions_dict (dict[type[Exception], list[str]]): Dictionary of exceptions to retry on. Raises: TimeoutExpiredError: If resource in not in desire status. @@ -971,10 +978,7 @@ def wait_for_status( samples = TimeoutSampler( wait_timeout=timeout, sleep=sleep, - exceptions_dict={ - **PROTOCOL_ERROR_EXCEPTION_DICT, - **DEFAULT_CLUSTER_RETRY_EXCEPTIONS, - }, + exceptions_dict=exceptions_dict, func=lambda: self.exists, ) current_status = None From 49845641038b1c87d24c3aa8f994ca678d61a58b Mon Sep 17 00:00:00 2001 From: rnetser Date: Tue, 25 Nov 2025 10:27:52 +0200 Subject: [PATCH 05/12] release: New Release 4.20.2 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b2b8c12a6f..9a204e841d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "4.20.1" +version = "4.20.2" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 09f21187c7..b1a4fc2200 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "4.20.1" +version = "4.20.2" source = { editable = "." } dependencies = [ { name = "cloup" }, From c9b2640633f94669aa1d6e0e0d77e2a05c3cef9f Mon Sep 17 00:00:00 2001 From: "redhat-qe[bot2]" <159281658+redhat-qe-bot2@users.noreply.github.com> Date: Wed, 26 Nov 2025 08:07:19 +0200 Subject: [PATCH 06/12] Add PROTOCOL_ERROR_EXCEPTION_DICT to the default exceptions_dict in create() (#2581) (#2595) Co-authored-by: Ahmad Hafe --- ocp_resources/resource.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ocp_resources/resource.py b/ocp_resources/resource.py index bee3f70c07..894fe9d865 100644 --- a/ocp_resources/resource.py +++ b/ocp_resources/resource.py @@ -1005,7 +1005,10 @@ def wait_for_status( raise def create( - self, wait: bool = False, exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS + self, + wait: bool = False, + exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS + | PROTOCOL_ERROR_EXCEPTION_DICT, ) -> ResourceInstance | None: """ Create resource. From 0d56f62d4912e23c0fe0a2e0d7cae2ff5f4b29a0 Mon Sep 17 00:00:00 2001 From: rnetser Date: Wed, 26 Nov 2025 08:31:53 +0200 Subject: [PATCH 07/12] release: New Release 4.20.3 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9a204e841d..9853822d37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "4.20.2" +version = "4.20.3" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index b1a4fc2200..4001372cd9 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "4.20.2" +version = "4.20.3" source = { editable = "." } dependencies = [ { name = "cloup" }, From 5fbaac8e8f38f94334b44e3e0a0198f3e625c6c0 Mon Sep 17 00:00:00 2001 From: rh-bot-1 <164334716+rh-bot-1@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:29:40 +0200 Subject: [PATCH 08/12] Update VMI pause/unpause (#2596) (#2602) removed wait_for_domstate_pause_status from pause/unpause (when called with wait=True): 1. check for domstate was an old w/a with is irrelevant now 2. executing domstate command on virt-launhcer pod can be done only with admin client (even though pause/unpause can be executed by unpriv client) Co-authored-by: vsibirsk <57763370+vsibirsk@users.noreply.github.com> --- ocp_resources/virtual_machine_instance.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ocp_resources/virtual_machine_instance.py b/ocp_resources/virtual_machine_instance.py index ee124c4096..7353d1dba8 100644 --- a/ocp_resources/virtual_machine_instance.py +++ b/ocp_resources/virtual_machine_instance.py @@ -1,5 +1,6 @@ import shlex from typing import Any +from warnings import warn import xmltodict from kubernetes.dynamic.exceptions import ResourceNotFoundError @@ -151,10 +152,14 @@ def wait_for_pause_status(self, pause, timeout=TIMEOUT_4MINUTES): TimeoutExpiredError: If resource not exists. """ self.logger.info(f"Wait until {self.kind} {self.name} is {'Paused' if pause else 'Unpuased'}") - self.wait_for_domstate_pause_status(pause=pause, timeout=timeout) self.wait_for_vmi_condition_pause_status(pause=pause, timeout=timeout) def wait_for_domstate_pause_status(self, pause, timeout=TIMEOUT_4MINUTES): + warn( + message="wait_for_domstate_pause_status is deprecated and will be removed the next version.", + category=DeprecationWarning, + stacklevel=2, + ) pause_status = "paused" if pause else "running" samples = TimeoutSampler( wait_timeout=timeout, @@ -269,6 +274,11 @@ def get_domstate(self): Returns: String: VMI Status as string """ + warn( + message="get_domstate is deprecated and will be removed the next version.", + category=DeprecationWarning, + stacklevel=2, + ) return self.execute_virsh_command(command="domstate") def get_dommemstat(self): From 5f0c1e94ae0370a45e256af2036de7c7e46081de Mon Sep 17 00:00:00 2001 From: rnetser Date: Wed, 10 Dec 2025 14:53:34 +0200 Subject: [PATCH 09/12] release: New Release 4.20.4 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9853822d37..348eab2935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "4.20.3" +version = "4.20.4" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 4001372cd9..3d8688ac66 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "4.20.3" +version = "4.20.4" source = { editable = "." } dependencies = [ { name = "cloup" }, From 38463f1c4a4390303cb6c8042f5c1d16332215d9 Mon Sep 17 00:00:00 2001 From: rh-bot-1 <164334716+rh-bot-1@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:43:36 +0200 Subject: [PATCH 10/12] Add UpToDate status (#2616) (#2618) The openshift-virtualization-tests repo using it multiple times, To verify DataImportCron condition is UpToDate=true Signed-off-by: Harel Meir Co-authored-by: Harel Meir --- ocp_resources/utils/resource_constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ocp_resources/utils/resource_constants.py b/ocp_resources/utils/resource_constants.py index 09b3c580f6..0981f6a953 100644 --- a/ocp_resources/utils/resource_constants.py +++ b/ocp_resources/utils/resource_constants.py @@ -31,6 +31,7 @@ class Condition: NETWORK_READY: str = "NetworkReady" ARCHIVED: str = "Archived" CANCELED: str = "Canceled" + UP_TO_DATE: str = "UpToDate" class Status: TRUE: str = "True" From da344ee3b4a643748ecea78b84f48729bac5d023 Mon Sep 17 00:00:00 2001 From: "Redhat-QE[bot]" <108792105+redhat-qe-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:22:08 +0200 Subject: [PATCH 11/12] Plan: Add a few missing parameters (#2545) (#2651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pvcNameTemplate - volumeNameTemplate - networkNameTemplate - preserveStaticIPs - targetNodeSelector - targetLabels - targetAffinity Co-authored-by: Marián Krčmárik Co-authored-by: Meni Yakove <441263+myakove@users.noreply.github.com> --- ocp_resources/plan.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ocp_resources/plan.py b/ocp_resources/plan.py index d49ad14965..5e1d328f7a 100644 --- a/ocp_resources/plan.py +++ b/ocp_resources/plan.py @@ -23,6 +23,8 @@ class Plan(NamespacedResource): type (str, optional): Migration type. Valid values: "cold", "warm", "live", "conversion". pvc_name_template_use_generate_name (bool, optional): Whether to use generateName for PVC name templates. pvc_name_template (str, optional): Template for generating PVC names. + volume_name_template (str, optional): Template for generating volume interface names in the target VM. + network_name_template (str, optional): Template for generating network interface names in the target VM. skip_guest_conversion (bool, optional): Whether to skip guest conversion. target_power_state (str, optional): Specifies the desired power state of the target VM after migration. - "on": Target VM will be powered on after migration @@ -30,6 +32,13 @@ class Plan(NamespacedResource): - "auto" or None (default): Target VM will match the source VM's power state use_compatibility_mode (bool, optional): Whether to use compatibility mode. migrate_shared_disks (bool, optional): Whether to migrate shared disks. + preserve_static_ips (bool, optional): Whether to preserve static IPs during migration. + target_node_selector (dict, optional): Node selector for the target VM. Specifies which node labels + should be used for NodeSelector parameter of VMI resource. + target_labels (dict, optional): Labels to be applied to the target VM. Specifies which labels + should be added to the target VM resource. + target_affinity (dict, optional): Affinity rules for the target VM. Specifies which affinity + rules should be applied to the target VM resource. """ api_group = NamespacedResource.ApiGroup.FORKLIFT_KONVEYOR_IO @@ -54,10 +63,16 @@ def __init__( type: str | None = None, pvc_name_template_use_generate_name: bool | None = None, pvc_name_template: str | None = None, + volume_name_template: str | None = None, + network_name_template: str | None = None, skip_guest_conversion: bool | None = None, target_power_state: str | None = None, use_compatibility_mode: bool | None = None, migrate_shared_disks: bool | None = None, + preserve_static_ips: bool | None = None, + target_node_selector: dict[str, str] | None = None, + target_labels: dict[str, str] | None = None, + target_affinity: dict[str, Any] | None = None, **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -80,10 +95,16 @@ def __init__( self.type = type self.pvc_name_template_use_generate_name = pvc_name_template_use_generate_name self.pvc_name_template = pvc_name_template + self.volume_name_template = volume_name_template + self.network_name_template = network_name_template self.skip_guest_conversion = skip_guest_conversion self.target_power_state = target_power_state self.use_compatibility_mode = use_compatibility_mode self.migrate_shared_disks = migrate_shared_disks + self.preserve_static_ips = preserve_static_ips + self.target_node_selector = target_node_selector + self.target_labels = target_labels + self.target_affinity = target_affinity if self.pre_hook_name and self.pre_hook_namespace: self.hooks_array.append( @@ -150,6 +171,12 @@ def to_dict(self) -> None: if self.pvc_name_template is not None: spec["pvcNameTemplate"] = self.pvc_name_template + if self.volume_name_template is not None: + spec["volumeNameTemplate"] = self.volume_name_template + + if self.network_name_template is not None: + spec["networkNameTemplate"] = self.network_name_template + if self.skip_guest_conversion is not None: spec["skipGuestConversion"] = self.skip_guest_conversion @@ -162,6 +189,18 @@ def to_dict(self) -> None: if self.migrate_shared_disks is not None: spec["migrateSharedDisks"] = self.migrate_shared_disks + if self.preserve_static_ips is not None: + spec["preserveStaticIPs"] = self.preserve_static_ips + + if self.target_node_selector is not None: + spec["targetNodeSelector"] = self.target_node_selector + + if self.target_labels is not None: + spec["targetLabels"] = self.target_labels + + if self.target_affinity is not None: + spec["targetAffinity"] = self.target_affinity + def _generate_hook_spec(self, hook_name: str, hook_namespace: str, hook_type: str) -> dict[str, Any]: return { "hook": { From 8a36ac59ef44ae651a17b37da72866378e1c3d92 Mon Sep 17 00:00:00 2001 From: rnetser Date: Wed, 18 Feb 2026 16:55:20 +0200 Subject: [PATCH 12/12] release: New Release 4.20.5 is out --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 348eab2935..910b756086 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ include = [ [project] requires-python = ">=3.10" name = "openshift-python-wrapper" -version = "4.20.4" +version = "4.20.5" description = "Wrapper around https://github.com/kubernetes-client/python" readme = "README.md" license = "Apache-2.0" diff --git a/uv.lock b/uv.lock index 3d8688ac66..a0821af4f8 100644 --- a/uv.lock +++ b/uv.lock @@ -1202,7 +1202,7 @@ wheels = [ [[package]] name = "openshift-python-wrapper" -version = "4.20.4" +version = "4.20.5" source = { editable = "." } dependencies = [ { name = "cloup" },