Repository: red-hat-storage/ocs-ci
Pull request #11523: add-hyperconverged-into-dependencies
Author: login: DanielOsypenko
Labels: name: Verified
description: Mark when PR was verified and log provided
name: size/XL
name: lgtm
Created: 2025-02-27T20:06:00Z | Merged: 2025-03-10T12:21:42Z

In past we had a problem with deploying ODF on Hosted Client clusters that were deployed with latest unreleased CVN and latest unreleased ACM.
The nature of the problem was not determined, but installing upstream hyperconverged along with upstream mce proved to be sustainable. 
This PR is also giving us a backdoor option, when CNV ureleased version is still not available and upstream HCO is already been populated via unstable index-tag of quay.io/kubevirt/hyperconverged-cluster-index

Link to the issue: https://issues.redhat.com/browse/OCPBUGS-32196  
Link to verification run: https://privatebin.corp.redhat.com/?08e39cfadafad0c6#E5JtjD7BpRWhLi22Jobta6U3FX6hJ3RfgUJZcAvqjsUT

Added int parameter in configuration `ENV_DATA.clusters.<cluster_name>.storage_quota`. Default value - no storage_quota, meaning storage for this client is unlimited



Diff:
diff --git a/conf/README.md b/conf/README.md
index 1fee8f9c28b..d082e2edcea 100644
--- a/conf/README.md
+++ b/conf/README.md
@@ -336,7 +336,9 @@ higher priority).
             * `node_network_configuration_policy_destination_route` - The destination route of NodeNetworkConfigurationPolicy CR
 * `hcp_version` - version of HCP client to be deployed on machine running the tests
 * `metallb_version` - MetalLB operator version to install
-* `install_hypershift_upstream` - Install hypershift from upstream or not (Default: false). Necessary for unreleased OCP/CNV versions
+* `deploy_acm_hub_cluster` - Deploy ACM hub cluster or not (Default: false)
+* `cnv_deployment` - Deploy CNV or not (Default: false) necessary for Converged clusters with hosted clients
+* `deploy_hyperconverged` - Deploy hyperconverged operator or not (Default: false).  Necessary for Converged clusters with hosted clients with unreleased OCP version
 * `clusters` - section for hosted clusters
     * `<cluster name>` - name of the cluster
       * `hosted_cluster_path` - path to the cluster directory to store auth_path, credentials files or cluster related files
@@ -347,6 +349,7 @@ higher priority).
       * `hosted_odf_registry` - registry for hosted ODF (default: quay.io/rhceph-dev/ocs-registry)
       * `hosted_odf_version` - version of ODF to be deployed on hosted clusters
       * `cp_availability_policy` - "HighlyAvailable" or "SingleReplica"; if not provided the default value is "SingleReplica"
+      * `storage_quota` - storage quota for the hosted cluster
 * `wait_timeout_for_healthy_osd_in_minutes` - timeout waiting for healthy OSDs before continuing upgrade (see https://bugzilla.redhat.com/show_bug.cgi?id=2276694 for more details)
 * `osd_maintenance_timeout` - is a duration in minutes that determines how long an entire failureDomain like region/zone/host will be held in noout
 * `odf_provider_mode_deployment` - True if you would like to enable provider mode deployment.
diff --git a/conf/examples/provider_mode_ibm_cloud_baremetal_kubevirt_clients.yaml b/conf/examples/provider_mode_ibm_cloud_baremetal_kubevirt_clients.yaml
index 251a016015b..5464a3fadd2 100644
--- a/conf/examples/provider_mode_ibm_cloud_baremetal_kubevirt_clients.yaml
+++ b/conf/examples/provider_mode_ibm_cloud_baremetal_kubevirt_clients.yaml
@@ -11,6 +11,7 @@ DEPLOYMENT:
   metallb_operator: true
   cnv_latest_stable: true
   local_storage: true
+  deploy_hyperconverged: false # use when unreleased version on Clients is needed
 ENV_DATA:
   platform: "hci_baremetal"
   cluster_type: "provider" # it is necessary to run the Hosted clusters deployment on the Provider cluster
@@ -19,6 +20,7 @@ ENV_DATA:
   acm_hub_channel: "release-2.10" # this is an example, please provide the desired version
   hcp_version: "4.16" # this is an example, please provide the desired version
   metallb_version: "4.16" # this is an example, please provide the desired version
+  deploy_mce: false # second option to enable multicluster setup; used instead of ACM
   clusters: # the list of the Hosted clusters and their configuration. If field does not exist HostedClsuter installation will be skipped
     hcp415-bm3-e: # the field name is the name of the Hosted cluster
       hosted_cluster_path: "~/clusters/hcp416-bm3-e/openshift-cluster-dir" # path to store auth_path dir or cluster related files
@@ -30,6 +32,7 @@ ENV_DATA:
       setup_storage_client: true # if true, the Storage Client will be created and verification will be performed
       nodepool_replicas: 3 # number of worker nodes created for Hosted cluster
       cp_availability_policy: "HighlyAvailable" # this field is optional, if not provided the default value is "SingleReplica"
+      storage_quota: 100  # int. 100 means "100Gi". this field is optional, if not provided client will have unlimited storage quota
     hcp415-bm3-f:
       hosted_cluster_path: "~/clusters/hcp416-bm3-f/openshift-cluster-dir" # path to store auth_path dir or cluster related files
       ocp_version:  "4.15.10" # this is an example, please provide the desired OCP version
diff --git a/ocs_ci/deployment/deployment.py b/ocs_ci/deployment/deployment.py
index d6e9bdc4f0d..14824454ef3 100644
--- a/ocs_ci/deployment/deployment.py
+++ b/ocs_ci/deployment/deployment.py
@@ -459,18 +459,14 @@ def do_deploy_ocs(self):
     def do_deploy_mce(self):
         """
         Deploy Multicluster Engine
+        Shall run on OCP deployment phase
 
         """
-        if config.DEPLOYMENT.get("deploy_mce"):
-            if config.ENV_DATA.get("skip_mce_check_if_present"):
-                check_mce_deployed = False
-                check_mce_ready = False
-            else:
-                check_mce_deployed = True
-                check_mce_ready = True
-            mce_installer = MCEInstaller()
-            mce_installer.deploy_mce(check_mce_deployed, check_mce_ready)
-            mce_installer.validate_mce_deployment()
+        if config.ENV_DATA["skip_ocs_deployment"]:
+
+            if config.ENV_DATA.get("deploy_mce"):
+                mce_installer = MCEInstaller()
+                mce_installer.deploy_mce()
 
     def do_deploy_oadp(self):
         """
@@ -648,6 +644,21 @@ def do_deploy_cnv(self):
                 check_cnv_ready = True
             CNVInstaller().deploy_cnv(check_cnv_deployed, check_cnv_ready)
 
+    def do_deploy_hyperconverged(self):
+        """
+        Deploy HyperConverged Operator and resources that works instead of CNV operator.
+        Should run on OCP deployment phase
+        """
+        if config.ENV_DATA["skip_ocs_deployment"]:
+
+            if config.ENV_DATA.get(
+                "deploy_hyperconverged"
+            ) and not config.DEPLOYMENT.get("cnv_deployment"):
+                from ocs_ci.deployment.hyperconverged import HyperConverged
+
+                hyperconverged = HyperConverged()
+                hyperconverged.deploy_hyperconverged()
+
     def do_deploy_metallb(self):
         """
         Deploy MetalLB
@@ -746,6 +757,7 @@ def deploy_cluster(self, log_cli_level="DEBUG"):
         self.do_deploy_odf_provider_mode()
         self.do_deploy_mce()
         self.do_deploy_cnv()
+        self.do_deploy_hyperconverged()
         self.do_deploy_metallb()
         self.do_deploy_hosted_clusters()
 
diff --git a/ocs_ci/deployment/helpers/hypershift_base.py b/ocs_ci/deployment/helpers/hypershift_base.py
index 301379f3754..2bb929035fe 100644
--- a/ocs_ci/deployment/helpers/hypershift_base.py
+++ b/ocs_ci/deployment/helpers/hypershift_base.py
@@ -260,9 +260,12 @@ def install_hcp_and_hypershift_from_git(self):
                 f"hypershift binary download failed to path:{self.hypershift_binary_path}"
             )
 
-    def download_hcp_binary_with_podman(self):
+    def _download_hcp_binary_with_podman(self):
         """
         Download hcp binary to bin_dir
+
+        !!! This method is not used in the code, but it is kept for reference !!!
+        Use install_hcp_and_hypershift_from_git instead
         """
         if self.hcp_binary_exists():
             logger.info(
diff --git a/ocs_ci/deployment/hosted_cluster.py b/ocs_ci/deployment/hosted_cluster.py
index 2c84e5c1d71..82e9e89edcd 100644
--- a/ocs_ci/deployment/hosted_cluster.py
+++ b/ocs_ci/deployment/hosted_cluster.py
@@ -5,6 +5,7 @@
 import time
 from concurrent.futures import ThreadPoolExecutor
 from ocs_ci.deployment.cnv import CNVInstaller
+from ocs_ci.deployment.hyperconverged import HyperConverged
 from ocs_ci.deployment.mce import MCEInstaller
 from ocs_ci.deployment.deployment import Deployment
 from ocs_ci.deployment.helpers.hypershift_base import (
@@ -22,6 +23,7 @@
     CommandFailed,
     TimeoutExpiredError,
     ResourceWrongStatusException,
+    UnexpectedDeploymentConfiguration,
 )
 from ocs_ci.ocs.ocp import OCP
 from ocs_ci.ocs.resources.catalog_source import CatalogSource
@@ -36,8 +38,6 @@
 from ocs_ci.utility.utils import (
     exec_cmd,
     TimeoutSampler,
-    get_ocp_version,
-    get_latest_release_version,
 )
 from ocs_ci.utility.version import get_semantic_version
 from ocs_ci.ocs.resources.storage_client import StorageClient
@@ -260,12 +260,47 @@ def deploy_hosted_ocp_clusters(self, cluster_names_list=None):
             # operators and finish the rest preparation steps. For the rest of the clusters we will only deploy OCP
             # with hcp command.
             first_ocp_deployment = index == 0
-            cluster_name = hosted_ocp_cluster.deploy_ocp(
-                deploy_cnv=first_ocp_deployment,
-                deploy_acm_hub=first_ocp_deployment,
+
+            # Put logic on checking and deploying dependencies here
+            if first_ocp_deployment:
+                # ACM installation is a default for Provider/Converged deployments
+                deploy_acm_hub = config.ENV_DATA.get("deploy_acm_hub_cluster", False)
+                # CNV installation is a default for Provider/Converged deployments
+                deploy_cnv = config.DEPLOYMENT.get("cnv_deployment", False)
+                deploy_mce = config.DEPLOYMENT.get("deploy_mce", False)
+                deploy_hyperconverged = config.ENV_DATA.get(
+                    "deploy_hyperconverged", False
+                )
+
+                # Validate conflicting deployments
+                if deploy_acm_hub and deploy_mce:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'deploy_acm_hub_cluster' and 'deploy_mce' are enabled. Choose one."
+                    )
+                if deploy_cnv and deploy_hyperconverged:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'cnv_deployment' and 'deploy_hyperconverged' are enabled. Choose one."
+                    )
+
+            else:
+                deploy_acm_hub = False
+                deploy_cnv = False
+                deploy_hyperconverged = False
+                deploy_mce = False
+
+            if not config.ENV_DATA["platform"].lower() in HCI_PROVIDER_CLIENT_PLATFORMS:
+                raise ProviderModeNotFoundException()
+
+            hosted_ocp_cluster.deploy_dependencies(
+                deploy_acm_hub=deploy_acm_hub,
+                deploy_cnv=deploy_cnv,
                 deploy_metallb=first_ocp_deployment,
                 download_hcp_binary=first_ocp_deployment,
+                deploy_hyperconverged=deploy_hyperconverged,
+                deploy_mce=deploy_mce,
             )
+
+            cluster_name = hosted_ocp_cluster.deploy_ocp()
             if cluster_name:
                 cluster_names.append(cluster_name)
 
@@ -318,7 +353,7 @@ def download_hosted_clusters_kubeconfig_files(self):
         """
 
         if not (self.hcp_binary_exists() and self.hypershift_binary_exists()):
-            self.download_hcp_binary_with_podman()
+            self.update_hcp_binary()
 
         for name in config.ENV_DATA.get("clusters").keys():
             path = config.ENV_DATA.get("clusters").get(name).get("hosted_cluster_path")
@@ -361,7 +396,12 @@ def deploy_multiple_odf_clients(self):
 
 
 class HypershiftHostedOCP(
-    HyperShiftBase, MetalLBInstaller, CNVInstaller, Deployment, MCEInstaller
+    HyperShiftBase,
+    MetalLBInstaller,
+    CNVInstaller,
+    Deployment,
+    MCEInstaller,
+    HyperConverged,
 ):
     def __init__(self, name):
         Deployment.__init__(self)
@@ -369,6 +409,7 @@ def __init__(self, name):
         MetalLBInstaller.__init__(self)
         CNVInstaller.__init__(self)
         MCEInstaller.__init__(self)
+        HyperConverged.__init__(self)
         self.name = name
         if config.ENV_DATA.get("clusters", {}).get(self.name):
             cluster_path = (
@@ -384,36 +425,16 @@ def __init__(self, name):
                 f"Cluster path for desired cluster with name '{self.name}' was not found in ENV_DATA.clusters"
             )
 
-    def deploy_ocp(
-        self,
-        deploy_cnv=True,
-        deploy_acm_hub=True,
-        deploy_metallb=True,
-        download_hcp_binary=True,
-        deploy_mce=True,
-    ):
+    def deploy_ocp(self, **kwargs) -> str:
         """
         Deploy hosted OCP cluster on provisioned Provider platform
 
         Args:
-            deploy_cnv: (bool) Deploy CNV
-            deploy_acm_hub: (bool) Deploy ACM Hub
-            deploy_metallb: (bool) Deploy MetalLB
-            download_hcp_binary: (bool) Download HCP binary
+            **kwargs: Additional arguments for create_kubevirt_ocp_cluster (currently not in use)
 
         Returns:
             str: Name of the hosted cluster
         """
-        if not config.ENV_DATA["platform"].lower() in HCI_PROVIDER_CLIENT_PLATFORMS:
-            raise ProviderModeNotFoundException()
-
-        if not config.ENV_DATA.get("deploy_acm_hub_cluster", True):
-            deploy_acm_hub = False
-
-        self.deploy_dependencies(
-            deploy_acm_hub, deploy_cnv, deploy_metallb, download_hcp_binary, deploy_mce
-        )
-
         ocp_version = str(config.ENV_DATA["clusters"][self.name].get("ocp_version"))
         if ocp_version and len(ocp_version.split(".")) == 2:
             # if ocp_version is provided in form x.y, we need to get the full form x.y.z
@@ -461,6 +482,7 @@ def deploy_dependencies(
         deploy_metallb,
         download_hcp_binary,
         deploy_mce,
+        deploy_hyperconverged,
     ):
         """
         Deploy dependencies for hosted OCP cluster
@@ -470,8 +492,17 @@ def deploy_dependencies(
             deploy_metallb: bool Deploy MetalLB
             download_hcp_binary: bool Download HCP binary
             deploy_mce: bool Deploy mce
+            deploy_hyperconverged: bool Deploy Hyperconverged
 
         """
+
+        # log out all args in one log.info
+        logger.info(
+            f"Deploying dependencies for hosted OCP cluster '{self.name}': "
+            f"deploy_acm_hub={deploy_acm_hub}, deploy_cnv={deploy_cnv}, "
+            f"deploy_metallb={deploy_metallb}, download_hcp_binary={download_hcp_binary}, "
+            f"deploy_mce={deploy_mce}, deploy_hyperconverged={deploy_hyperconverged}"
+        )
         initial_default_sc = helpers.get_default_storage_class()
         logger.info(f"Initial default StorageClass: {initial_default_sc}")
         if not initial_default_sc == constants.CEPHBLOCKPOOL_SC:
@@ -483,41 +514,30 @@ def deploy_dependencies(
             except CommandFailed as e:
                 logger.error(f"Failed to change default StorageClass: {e}")
 
-        if deploy_cnv:
+        if deploy_cnv and not deploy_hyperconverged:
             self.deploy_cnv(check_cnv_ready=True)
-        if deploy_acm_hub:
+        elif deploy_hyperconverged and not deploy_cnv:
+            self.deploy_hyperconverged()
+        elif deploy_cnv and deploy_hyperconverged:
+            raise UnexpectedDeploymentConfiguration(
+                "Both deploy_cnv and deploy_hyperconverged are set to True. "
+                "Please choose only one of them."
+            )
+
+        if deploy_acm_hub and not deploy_mce:
             self.deploy_acm_hub()
+        elif deploy_mce and not deploy_acm_hub:
+            self.deploy_mce()
+        elif deploy_acm_hub and deploy_mce:
+            raise UnexpectedDeploymentConfiguration(
+                "Both deploy_acm_hub and deploy_mce are set to True. "
+                "Please choose only one of them."
+            )
+
         if deploy_metallb:
             self.deploy_lb()
         if download_hcp_binary:
             self.update_hcp_binary()
-        if deploy_mce and not deploy_acm_hub:
-            self.deploy_mce()
-            self.validate_mce_deployment()
-
-        provider_ocp_version = str(
-            get_semantic_version(get_ocp_version(), only_major_minor=True)
-        )
-        latest_released_ocp_version = str(
-            get_semantic_version(get_latest_release_version(), only_major_minor=True)
-        )
-
-        if provider_ocp_version > latest_released_ocp_version:
-            logger.info("running on unreleased OCP version")
-            if config.ENV_DATA.get("install_hypershift_upstream"):
-                try:
-                    self.disable_multicluster_engine()
-                    # avoid timelapse error "
-                    # Error: [serviceaccounts "operator" is forbidden: unable to create new content"
-                    logger.info(
-                        "Sleeping for 5 minutes after disable_multicluster_engine()"
-                    )
-                    time.sleep(5 * 60)
-                    self.install_hypershift_upstream_on_cluster()
-                except CommandFailed as e:
-                    raise AssertionError(
-                        f"Failed to install Hypershift on the cluster: {e}"
-                    )
 
         # Enable central infrastructure management service for agent
         if config.DEPLOYMENT.get("hosted_cluster_platform") == "agent":
@@ -612,6 +632,11 @@ def __init__(self, name: str):
             constants.PROVIDER_MODE_STORAGE_CLIENT
         )
         self.storage_client_name = storage_client_data["metadata"]["name"]
+        self.storage_quota = (
+            config.ENV_DATA.get("clusters", {})
+            .get(self.name, {})
+            .get("storage_quota", None)
+        )
 
     @kubeconfig_exists_decorator
     def exec_oc_cmd(self, cmd, timeout=300, ignore_error=False, **kwargs):
@@ -956,7 +981,11 @@ def get_onboarding_key(self):
         )
 
         try:
-            token = generate_onboarding_token(private_key=decoded_key)
+            token = generate_onboarding_token(
+                private_key=decoded_key,
+                use_ticketgen_with_quota=True,
+                storage_quota=self.storage_quota,
+            )
         except Exception as e:
             logger.error(f"Error during onboarding token generation: {e}")
             token = ""
diff --git a/ocs_ci/deployment/hyperconverged.py b/ocs_ci/deployment/hyperconverged.py
new file mode 100644
index 00000000000..2b62a12f363
--- /dev/null
+++ b/ocs_ci/deployment/hyperconverged.py
@@ -0,0 +1,233 @@
+import logging
+import semantic_version
+
+from ocs_ci.ocs.exceptions import HyperConvergedNotDeployedException, CommandFailed
+from ocs_ci.ocs.ocp import OCP
+from ocs_ci.ocs import constants
+from ocs_ci.ocs.resources.deployment import Deployment
+from ocs_ci.ocs.resources.ocs import OCS
+from ocs_ci.ocs.resources.pod import wait_for_pods_to_be_running
+from ocs_ci.ocs.utils import get_pod_name_by_pattern
+from ocs_ci.ocs.version import get_ocp_version
+from ocs_ci.utility import templating
+from ocs_ci.ocs.resources.catalog_source import CatalogSource
+from ocs_ci.utility.retry import retry
+from ocs_ci.utility.utils import wait_custom_resource_defenition_available
+
+logger = logging.getLogger(__name__)
+
+
+class HyperConverged:
+    """
+    This class represent HyperConverged and contains all related methods we need to do with it.
+    Hyperconverged Operator is used instead of unreleased CNV, to overcome catalogsource limitations on Client clusters
+    """
+
+    def __init__(self):
+        self.namespace = constants.HYPERCONVERGED_NAMESPACE
+        self.ns_obj = OCP(kind=constants.NAMESPACES)
+        self.operator_group = OCP(
+            kind=constants.OPERATOR_GROUP, namespace=self.namespace
+        )
+        self.catsrc = OCP(
+            kind=constants.CATSRC, namespace=constants.MARKETPLACE_NAMESPACE
+        )
+        self.subs = OCP(kind=constants.PROVIDER_SUBSCRIPTION, namespace=self.namespace)
+        # type of hyperconverged becomes available after the Hyperconverged operator is deployed
+        self.hyperconverged = None
+        self.ocp_version = get_ocp_version()
+
+    def create_hyperconverged_namespace(self):
+        """
+        Creates the namespace for hyperconverged resources
+
+        """
+        if not self.ns_obj.is_exist(
+            resource_name=self.namespace,
+        ):
+            logger.info(
+                f"Creating namespace {self.namespace} for hyperconverged resources"
+            )
+            namespace_yaml_file = templating.load_yaml(
+                constants.HYPERCONVERGED_NAMESPACE_YAML
+            )
+            namespace_yaml = OCS(**namespace_yaml_file)
+            namespace_yaml.create()
+        else:
+            logger.info(f"{self.namespace} already exists")
+        return self.ns_obj.check_resource_existence(
+            should_exist=True, resource_name=self.namespace
+        )
+
+    def create_operator_group(self):
+        """
+        Creates operator group for hyperconverged resources
+
+        """
+        logger.info("Creating operator group for hyperconverged resources")
+
+        if not self.operator_group.is_exist(
+            resource_name=constants.HYPERCONVERGED_OPERATOR_GROUP_NAME
+        ):
+            operator_group_yaml_file = templating.load_yaml(
+                constants.HYPERCONVERGED_OPERATOR_GROUP_YAML
+            )
+            operator_group_yaml = OCS(**operator_group_yaml_file)
+            operator_group_yaml.create()
+        return self.operator_group.check_resource_existence(
+            should_exist=True,
+            resource_name=constants.HYPERCONVERGED_OPERATOR_GROUP_NAME,
+        )
+
+    def create_catalog_source(self):
+        """
+        Creates catalog source for hyperconverged resources
+        ! No customization by purpose. Will always align with branch default image that is set in the default config.
+        """
+        logger.info("Check if catalog source already exist")
+        if not self.catsrc.is_exist(
+            resource_name=constants.HYPERCONVERGED_CATALOGSOURCE
+        ):
+            catalog_source_yaml_file = templating.load_yaml(
+                constants.HYPERVERGED_CATALOGSOURCE_YAML
+            )
+            hyperconverged_version = get_hyperconverged_corresponding_version(
+                self.ocp_version
+            )
+            catalog_source_yaml_file["spec"]["image"] = catalog_source_yaml_file[
+                "spec"
+            ]["image"].format(hyperconverged_version=hyperconverged_version)
+            catalog_source_yaml = OCS(**catalog_source_yaml_file)
+            catalog_source_yaml.create()
+        self.catsrc.check_resource_existence(
+            should_exist=True, resource_name=constants.HYPERCONVERGED_CATALOGSOURCE
+        )
+        catalog_source_yaml = CatalogSource(
+            constants.HYPERCONVERGED_CATALOGSOURCE, constants.MARKETPLACE_NAMESPACE
+        )
+        catalog_source_yaml.wait_for_state("READY")
+
+    def create_subscription(self):
+        """
+        Creates subscription for hyperconverged operator
+
+        """
+        logger.info("Check if subscription already exist")
+        if not self.subs.is_exist(resource_name=constants.HYPERCONVERGED_SUBSCRIPTION):
+            subscription_yaml_data = templating.load_yaml(
+                constants.HYPERCONVERGED_SUBSCRIPTION_YAML
+            )
+            hyperconverged_version = get_hyperconverged_corresponding_version(
+                self.ocp_version
+            )
+            subscription_yaml_data["spec"]["channel"] = subscription_yaml_data["spec"][
+                "channel"
+            ].format(hyperconverged_version=hyperconverged_version)
+            subscription_obj = OCS(**subscription_yaml_data)
+            subscription_obj.create()
+        self.subs.check_resource_existence(
+            should_exist=True, resource_name=constants.HYPERCONVERGED_SUBSCRIPTION
+        )
+
+        pod_names = get_pod_name_by_pattern(
+            "hco-operator", self.namespace
+        ) + get_pod_name_by_pattern("virt-operator", self.namespace)
+        wait_for_pods_to_be_running(namespace=self.namespace, pod_names=pod_names)
+
+    def create_hyperconverged_instance(self):
+        """
+        Create Hyperconverged instance
+        """
+        self.hyperconverged = OCP(
+            kind=constants.HYPERCONVERGED_KIND, namespace=self.namespace
+        )
+        if not self.hyperconverged.is_exist(
+            resource_name=constants.HYPERCONVERGED_NAME
+        ):
+            hyperconverged_instance_yaml_file = templating.load_yaml(
+                constants.HYPERCONVERGED_YAML
+            )
+            hyperconverged_instance_yaml = OCS(**hyperconverged_instance_yaml_file)
+            retry(CommandFailed, tries=10, delay=60)(
+                hyperconverged_instance_yaml.create
+            )()
+
+        self.hyperconverged.check_resource_existence(
+            should_exist=True, resource_name=constants.HYPERCONVERGED_NAME
+        )
+        # wait for pods to be up and running
+        deployments = ["virt-operator", "virt-api", "virt-controller"]
+
+        for resource_name in deployments:
+            depl_ocp_obj = OCP(
+                kind=constants.DEPLOYMENT,
+                namespace=self.namespace,
+                resource_name=resource_name,
+            )
+            deployment_obj = Deployment(
+                **depl_ocp_obj.get(retry=60, wait=10, dont_raise=True)
+            )
+            deployment_obj.wait_for_available_replicas(timeout=600)
+
+    def deploy_hyperconverged(self):
+        """
+        Deploy Hyperconverged Operator and resources
+        """
+        # avoid mix in MRO calling explicitly the method of own class
+        HyperConverged.create_hyperconverged_namespace(self)
+        HyperConverged.create_operator_group(self)
+        HyperConverged.create_catalog_source(self)
+        HyperConverged.create_subscription(self)
+        if not wait_custom_resource_defenition_available(constants.HYPERCONVERGED_CRD):
+            raise HyperConvergedNotDeployedException(
+                f"crd {constants.HYPERCONVERGED_CRD} is unavailable"
+            )
+        HyperConverged.create_hyperconverged_instance(self)
+
+
+def get_hyperconverged_corresponding_version(ocp_version: str) -> str:
+    """
+    Given an OCP version, return the corresponding Hyperconverged version.
+
+    Rule:
+    - Hyperconverged Major = OCP Major - 3
+    - Hyperconverged Minor = OCP Minor - 4
+
+    Args:
+        ocp_version: OCP version as a string (e.g., "4.18" or "4.18.3")
+    Returns:
+        Corresponding Hyperconverged version as a string (e.g., "1.14")
+    """
+    if not semantic_version.validate(ocp_version):
+        ocp_version += ".0"  # Ensure valid semantic versioning if patch is missing
+
+    ocp_semver = semantic_version.Version(ocp_version)
+    hyperconverged_major = ocp_semver.major - 3
+    hyperconverged_minor = ocp_semver.minor - 4
+
+    return f"{hyperconverged_major}.{hyperconverged_minor}"
+
+
+def get_ocp_corresponding_version(hyperconverged_version: str) -> str:
+    """
+    Given a Hyperconverged version, return the corresponding OCP version.
+
+    Rule:
+    - OCP Major = Hyperconverged Major + 3
+    - OCP Minor = Hyperconverged Minor + 4
+
+    Args:
+        hyperconverged_version: Hyperconverged version as a string (e.g., "1.14")
+    Returns:
+        Corresponding OCP version as a string (e.g., "4.18")
+    """
+    if not semantic_version.validate(hyperconverged_version):
+        hyperconverged_version += (
+            ".0"  # Ensure valid semantic versioning if patch is missing
+        )
+
+    hyperconverged_semver = semantic_version.Version(hyperconverged_version)
+    ocp_major = hyperconverged_semver.major + 3
+    ocp_minor = hyperconverged_semver.minor + 4
+
+    return f"{ocp_major}.{ocp_minor}"
diff --git a/ocs_ci/deployment/mce.py b/ocs_ci/deployment/mce.py
index 7cbc6913264..356f30cccdc 100644
--- a/ocs_ci/deployment/mce.py
+++ b/ocs_ci/deployment/mce.py
@@ -5,19 +5,26 @@
 import logging
 import tempfile
 
-from ocs_ci.framework import config
-from ocs_ci.ocs.resources.ocs import OCS
+from ocs_ci.deployment.qe_app_registry import QeAppRegistry
+from ocs_ci.ocs import ocp
 from ocs_ci.ocs.ocp import OCP
+from ocs_ci.ocs.resources.ocs import OCS
+from ocs_ci.framework import config
 from ocs_ci.utility import templating
 from ocs_ci.ocs import constants
 from ocs_ci.utility.utils import (
     run_cmd,
     exec_cmd,
+    wait_custom_resource_defenition_available,
 )
 from ocs_ci.ocs.resources.catalog_source import CatalogSource
-from ocs_ci.ocs import ocp
+from ocs_ci.ocs.resources.deployment import Deployment
 from ocs_ci.utility.utils import get_running_ocp_version
-from ocs_ci.ocs.exceptions import CommandFailed, UnavailableResourceException
+from ocs_ci.ocs.exceptions import (
+    CommandFailed,
+    UnavailableResourceException,
+    MultiClusterEngineNotDeployedException,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -28,22 +35,30 @@ class MCEInstaller(object):
     """
 
     def __init__(self):
-        self.namespace = constants.MCE_NAMESPACE
+        self.mce_namespace = constants.MCE_NAMESPACE
         self.ns_obj = ocp.OCP(kind=constants.NAMESPACES)
         self.hypershift_override_image_cm = "hypershift-override-images-new"
         self.multicluster_engine = ocp.OCP(
             kind="MultiClusterEngine",
             resource_name=constants.MULTICLUSTER_ENGINE,
+            namespace=self.mce_namespace,
         )
         self.catsrc = ocp.OCP(
             kind=constants.CATSRC, namespace=constants.MARKETPLACE_NAMESPACE
         )
         self.subs = ocp.OCP(kind=constants.PROVIDER_SUBSCRIPTION)
 
-    def create_mce_catalog_source(self):
+    def _create_mce_catalog_source(self):
         """
         Creates a catalogsource for mce operator.
 
+        We use qe-app-registry catalog source to install latest version
+        In future if we want to install particular image we can use subscription.spec.startingCSV:<image> like
+        quay.io:443/acm-d/mce-custom-registry:2.13.0-DOWNSTREAM-2025-03-02-02-49-35
+        In this case we don't need another channel in subscription but may reuse stable-2.8 channel
+
+        ! Important. This catalog source does not work without ICSP, this catsrc is not used in a moment.
+        ! This method was left for reference only.
         """
         if not self.catsrc.is_exist(
             resource_name=constants.MCE_CATSRC_NAME,
@@ -79,15 +94,13 @@ def create_mce_namespace(self):
             CommandFailed: If the 'oc create' command fails.
         """
         if not self.ns_obj.is_exist(
-            resource_name=self.namespace,
+            resource_name=self.mce_namespace,
         ):
-            logger.info(f"Creating namespace {self.namespace} for mce resources")
+            logger.info(f"Creating namespace {self.mce_namespace} for mce resources")
             namespace_yaml_file = templating.load_yaml(constants.MCE_NAMESPACE_YAML)
             namespace_yaml = OCS(**namespace_yaml_file)
             namespace_yaml.create()
-            logger.info(f"MCE namespace {self.namespace} was created successfully")
-        else:
-            logger.info(f"{self.namespace} already exists")
+            logger.info(f"MCE namespace {self.mce_namespace} was created successfully")
 
     def create_multiclusterengine_operatorgroup(self):
         """
@@ -98,14 +111,12 @@ def create_multiclusterengine_operatorgroup(self):
         if not self.multicluster_engine.is_exist(
             resource_name=constants.MULTICLUSTER_ENGINE
         ):
-
             operatorgroup_yaml_file = templating.load_yaml(
                 constants.MCE_OPERATOR_GROUP_YAML
             )
             operatorgroup_yaml = OCS(**operatorgroup_yaml_file)
             operatorgroup_yaml.create()
             logger.info("mce OperatorGroup created successfully")
-        self.multicluster_engine.wait_for_phase("Available")
 
     def create_multiclusterengine_resource(self):
         """
@@ -128,26 +139,23 @@ def create_mce_subscription(self):
                 constants.MCE_SUBSCRIPTION_YAML
             )
 
-            if config.DEPLOYMENT.get("mce_latest_stable"):
-                mce_subscription_yaml_data["spec"][
-                    "source"
-                ] = constants.OPERATOR_CATALOG_SOURCE_NAME
-                mce_channel = "stable"
-            else:
-                mce_channel = config.DEPLOYMENT.get("mce_channel")
+            # ! Important, channel-2.8 becomes available after OCP 4.18 release
+            if config.DEPLOYMENT.get("mce_channel"):
+                mce_subscription_yaml_data["spec"]["channel"] = config.DEPLOYMENT.get(
+                    "mce_channel"
+                )
 
-            mce_subscription_yaml_data["spec"]["channel"] = mce_channel
             mce_subscription_manifest = tempfile.NamedTemporaryFile(
                 mode="w+", prefix="mce_subscription_manifest", delete=False
             )
             templating.dump_data_to_temp_yaml(
                 mce_subscription_yaml_data, mce_subscription_manifest.name
             )
-            logger.info("Creating subscription for mce operator")
-            run_cmd(f"oc create -f {mce_subscription_manifest.name}")
+            logger.info("Creating subscription for the mce operator")
+            exec_cmd(f"oc create -f {mce_subscription_manifest.name}")
             OCP(
                 kind=constants.SUBSCRIPTION_COREOS,
-                namespace=self.namespace,
+                namespace=self.mce_namespace,
                 resource_name=constants.MCE_OPERATOR,
             ).check_resource_existence(
                 should_exist=True, resource_name=constants.MCE_OPERATOR
@@ -160,7 +168,9 @@ def check_hypershift_namespace(self):
         Check hypershift namespace created
 
         """
-        logger.info(f"hypershift namespace {self.namespace} was created successfully")
+        logger.info(
+            f"hypershift namespace {self.mce_namespace} was created successfully"
+        )
         is_hypershift_ns_available = self.ns_obj.is_exist(
             resource_name=constants.HYPERSHIFT_NAMESPACE,
         )
@@ -176,15 +186,20 @@ def check_supported_versions(self):
             namespace=constants.HYPERSHIFT_NAMESPACE,
         )
 
-        if not configmaps_obj.is_exist(
-            resource_name=constants.SUPPORTED_VERSIONS_CONFIGMAP
+        if not configmaps_obj.check_resource_existence(
+            should_exist=True,
+            timeout=60,
+            resource_name=constants.SUPPORTED_VERSIONS_CONFIGMAP,
         ):
             raise UnavailableResourceException(
-                f"Configmap {constants.SUPPORTED_VERSIONS_CONFIGMAP} does not exist in hypershift namespace"
+                f"Configmap {constants.SUPPORTED_VERSIONS_CONFIGMAP} does not exist "
+                f"in {constants.HYPERSHIFT_NAMESPACE} namespace"
             )
 
-        cmd = "oc get cm -n hypershift supported-versions -o jsonpath='{.data.supported-versions}'"
+        cmd = f"oc get cm -n {constants.HYPERSHIFT_NAMESPACE} supported-versions "
+        cmd += "-o jsonpath='{.data.supported-versions}'"
         cmd_res = exec_cmd(cmd, shell=True)
+        supported_versions = ""
         if cmd_res.returncode == 0:
             supported_versions = cmd_res.stdout.decode("utf-8")
             logger.info(f"Supported versions: {supported_versions}")
@@ -199,7 +214,7 @@ def create_image_override(self):
         # Create image override configmap using the image override json
         cmd = (
             f"oc create cm {self.hypershift_override_image_cm} --from-file={constants.IMAGE_OVERRIDE_JSON}"
-            "-n {self.namespace}"
+            f"-n {self.mce_namespace}"
         )
         cmd_res = exec_cmd(cmd, shell=True)
         if cmd_res.returncode:
@@ -211,43 +226,42 @@ def create_image_override(self):
         )
         self.multicluster_engine.wait_until_running()
 
-    def deploy_mce(self, check_mce_deployed=False, check_mce_ready=False):
+    def deploy_mce(self):
         """
         Installs mce enabling software emulation.
 
-        Args:
-            check_mce_deployed (bool): If True, check if mce is already deployed. If so, skip the deployment.
-            check_mce_ready (bool): If True, check if mce is ready. If so, skip the deployment.
         """
-        if check_mce_deployed:
-            if self.mce_installed():
-                logger.info("mce operator is already deployed, skipping the deployment")
-                return
-
-        if check_mce_ready:
-            if self.post_install_verification(raise_exception=False):
-                logger.info("mce operator ready, skipping the deployment")
-                return
-
-        logger.info("Installing mce")
-        # we create catsrc with nightly builds only if config.DEPLOYMENT does not have mce_latest_stable
-        if not config.DEPLOYMENT.get("mce_latest_stable"):
-            # Create mce catalog source
-            self.create_mce_catalog_source()
-        # Create multicluster-engine namespace
-        self.create_mce_namespace()
-        # create mce subscription
-        self.create_mce_subscription()
-        # Deploy the multiclusterengine operatorgroup
-        self.create_multiclusterengine_operatorgroup()
-        # Create mce resource
-        self.create_multiclusterengine_resource()
-        # Check hypershift ns created
-        if not self.check_hypershift_namespace():
-            cmd = f"oc create namespace {constants.HYPERSHIFT_NAMESPACE}"
-            cmd_res = exec_cmd(cmd, shell=True)
-            if cmd_res.returncode:
-                raise CommandFailed("Failed to create hypershift namespace")
+
+        if not self.mce_installed():
+            logger.info("Installing mce")
+            # we create catsrc with nightly builds only if config.DEPLOYMENT does not have mce_latest_stable
+            qe_app_registry = QeAppRegistry()
+            qe_app_registry.icsp()
+            qe_app_registry.catalog_source()
+            self.create_mce_namespace()
+            self.create_multiclusterengine_operatorgroup()
+            self.create_mce_subscription()
+            if not wait_custom_resource_defenition_available(
+                constants.MULTICLUSTER_ENGINE_CRD
+            ):
+                raise MultiClusterEngineNotDeployedException(
+                    f"crd {constants.MULTICLUSTER_ENGINE_CRD} is unavailable"
+                )
+
+        # check whether mce instance is created, if it is installed but mce don't pass validation we can not heal it in
+        # script here, hence no sense for full validation of mce
+        if not self.mce_exists():
+
+            # Create mce resource
+            self.create_multiclusterengine_resource()
+            # Check hypershift ns created
+            if not self.check_hypershift_namespace():
+                cmd = f"oc create namespace {constants.HYPERSHIFT_NAMESPACE}"
+                cmd_res = exec_cmd(cmd, shell=True)
+                if cmd_res.returncode:
+                    raise CommandFailed("Failed to create hypershift namespace")
+
+        self.wait_mce_resources()
         # Check supported versions in supported-versions configmap
         self.check_supported_versions()
 
@@ -258,36 +272,48 @@ def mce_installed(self):
         Returns:
              bool: True if MCE is installed, False otherwise
         """
-        ocp = OCP(kind=constants.ROOK_OPERATOR, namespace=self.namespace)
-        return ocp.check_resource_existence(
-            timeout=12, should_exist=True, resource_name=constants.MCE_OPERATOR
+        ocp_obj = OCP(kind=constants.ROOK_OPERATOR)
+        # unlike other k8s resources, operators are OLM manager resources that identified by merged name.namespace
+        return ocp_obj.check_resource_existence(
+            timeout=12,
+            should_exist=True,
+            resource_name=constants.MCE_OPERATOR_OPERATOR_NAME_WITH_NS,
         )
 
-    def post_install_verification(self, raise_exception=False):
+    def mce_exists(self):
         """
-        Performs MCE post-installation verification, with raise_exception = False may be used safely to run on
-        clusters with MCE installed or not installed.
-
-        Args:
-            raise_exception: If True, allow function to fail the job and raise an exception. If false, return False
-        instead of raising an exception.
+        Check if MCE exists
 
         Returns:
-            bool: True if the verification conditions are met, False otherwise
-        Raises:
-            TimeoutExpiredError: If the verification conditions are not met within the timeout
-            and raise_exception is True.
-            ResourceNotFoundError if the namespace does not exist and raise_exception is True.
-            ResourceWrongStatusException if the nodes are not ready, verification fails and raise_exception
-            is True.
+            bool: True if MCE exists, False otherwise
         """
-        # TODO: implement
-        pass
+        return self.multicluster_engine.is_exist(
+            resource_name=constants.MULTICLUSTER_ENGINE
+        )
 
-    def validate_mce_deployment(self):
+    def wait_mce_resources(self):
         """
-        Validate mce operator installation
+        Wait for mce Available state and deployments Ready state
+
+        Raises:
+            TimeoutExpiredError: If the deployment is not in the 'Available' state within the timeout
         """
-        if self.mce_hyperconverged_installed():
-            logger.info("mce operator is already deployed")
-            return
+        if not self.mce_exists():
+            raise UnavailableResourceException("MCE resource is not created")
+        deployments = [
+            "multicluster-engine-operator",
+            "ocm-controller",
+            "cluster-manager",
+            "ocm-webhook",
+        ]
+
+        for resource_name in deployments:
+            depl_ocp_obj = OCP(
+                kind=constants.DEPLOYMENT,
+                namespace=self.mce_namespace,
+                resource_name=resource_name,
+            )
+            deployment_obj = Deployment(
+                **depl_ocp_obj.get(retry=60, wait=10, dont_raise=True)
+            )
+            deployment_obj.wait_for_available_replicas(timeout=600)
diff --git a/ocs_ci/framework/conf/default_config.yaml b/ocs_ci/framework/conf/default_config.yaml
index 81f9e25c56a..b88bf1458e5 100644
--- a/ocs_ci/framework/conf/default_config.yaml
+++ b/ocs_ci/framework/conf/default_config.yaml
@@ -123,6 +123,10 @@ DEPLOYMENT:
   rosa_cli_version: '1.2.49'
   # Multicluster engine
   deploy_mce: false
+  # ! Pay attention. stable-2.8 is not available until OCP 4.18 becomes available
+  mce_channel: "stable-2.8"
+  # catalogsource format of image with tag; latest unreleased, that is supported by OCP 4.18
+  mce_image: ""
 
 
 
@@ -232,15 +236,14 @@ ENV_DATA:
   rhel7.9_worker_ami: 'ami-058a93d58c5797698'
   rhel8.4_worker_ami: 'ami-074bab065c112399f'
   cluster_type: ""
-  # Provider mode dependencies checks during deployment for presence
-  skip_mce_check_if_present: false
-  skip_cnv_check_if_present: false
 
   # RHEL template which are used for vSphere platform
   rhel7_template: 'rhel79_ocs4qe'
   rhel8_template: 'rhel87_ocs4qe'
   # ACM HUB deployment
   deploy_acm_hub_cluster: false
+  # HyperConverged deployment
+  deploy_hyperconverged: false
 
   # Managed service git stat  default configurations
   # Managed StorageCluster size in TiB
diff --git a/ocs_ci/ocs/constants.py b/ocs_ci/ocs/constants.py
index 9db9907ab61..0f5ad0f1aa5 100644
--- a/ocs_ci/ocs/constants.py
+++ b/ocs_ci/ocs/constants.py
@@ -249,6 +249,7 @@
 ENCRYPTIONKEYROTATIONCRONJOB = "encryptionkeyrotationcronjobs.csiaddons.openshift.io"
 ENCRYPTIONKEYROTATIONJOB = "encryptionkeyrotationjobs.csiaddons.openshift.io"
 DEFAULT_CEPH_DEVICECLASS = "defaultCephDeviceClass"
+CRD_KIND = "CustomResourceDefinition"
 
 # Provisioners
 AWS_EFS_PROVISIONER = "openshift.org/aws-efs"
@@ -506,6 +507,35 @@
 DEFAULT_VOLUMESNAPSHOTCLASS_CEPHFS_MS_PC = f"{DEFAULT_CLUSTERNAME}-cephfs"
 DEFAULT_VOLUMESNAPSHOTCLASS_RBD_MS_PC = f"{DEFAULT_CLUSTERNAME}-ceph-rbd"
 
+# hyperconverged defaults
+HYPERCONVERGED_NAMESPACE = "kubevirt-hyperconverged"
+# MCE_NAMESPACE_YAML = os.path.join(TEMPLATE_DEPLOYMENT_DIR_MCE, "mce_namespace.yaml")
+TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED = os.path.join(
+    TEMPLATE_DIR, "hyperconverged-deployment"
+)
+HYPERCONVERGED_NAMESPACE_YAML = os.path.join(
+    TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED, "hyperconverged_namespace.yaml"
+)
+HYPERCONVERGED_OPERATOR_GROUP_YAML = os.path.join(
+    TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED, "hyperconverged_operator_group.yaml"
+)
+HYPERCONVERGED_OPERATOR_GROUP_NAME = "hyperconverged-operator-group"
+HYPERCONVERGED_CATALOGSOURCE = "hyperconverged-catalogsource"
+HYPERVERGED_CATALOGSOURCE_YAML = os.path.join(
+    TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED, "hyperconverged-catsrc.yaml"
+)
+HYPERCONVERGED_SUBSCRIPTION = "community-kubevirt-hyperconverged"
+HYPERCONVERGED_SUBSCRIPTION_YAML = os.path.join(
+    TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED, "hyperconverged_subscription.yaml"
+)
+HYPERCONVERGED_KIND = "HyperConverged"
+HYPERCONVERGED_NAME = "kubevirt-hyperconverged"
+HYPERCONVERGED_YAML = os.path.join(
+    TEMPLATE_DEPLOYMENT_DIR_HYPERCONVERGED, "hyperconverged.yaml"
+)
+HYPERCONVERGED_CRD = "hyperconvergeds.hco.kubevirt.io"
+
+
 # CNV deployment constants
 CNV_NAMESPACE = "openshift-cnv"
 CNV_QUAY_NIGHTLY_IMAGE = "quay.io/openshift-cnv/nightly-catalog"
@@ -2778,10 +2808,12 @@
     TEMPLATE_DEPLOYMENT_DIR_MCE, "mce_subscription.yaml"
 )
 MCE_OPERATOR = "multicluster-engine"
+MCE_OPERATOR_OPERATOR_NAME_WITH_NS = f"{MCE_OPERATOR}.{MCE_NAMESPACE}"
 MCE_RESOURCE_YAML = os.path.join(TEMPLATE_DEPLOYMENT_DIR_MCE, "mce_resource.yaml")
 MCE_OPERATOR_GROUP_YAML = os.path.join(
     TEMPLATE_DEPLOYMENT_DIR_MCE, "mce_operatorgroup.yaml"
 )
+MULTICLUSTER_ENGINE_CRD = "multiclusterengines.multicluster.openshift.io"
 HYPERSHIFT_NAMESPACE = "hypershift"
 SUPPORTED_VERSIONS_CONFIGMAP = "supported-versions"
 IMAGE_OVERRIDE_JSON = os.path.join(TEMPLATE_DEPLOYMENT_DIR_MCE, "image-override.json")
diff --git a/ocs_ci/ocs/exceptions.py b/ocs_ci/ocs/exceptions.py
index 8dd3aecbe5c..09600ac46af 100644
--- a/ocs_ci/ocs/exceptions.py
+++ b/ocs_ci/ocs/exceptions.py
@@ -687,6 +687,14 @@ class HyperConvergedHealthException(Exception):
     pass
 
 
+class HyperConvergedNotDeployedException(Exception):
+    pass
+
+
+class MultiClusterEngineNotDeployedException(Exception):
+    pass
+
+
 class OpenShiftAPIResponseException(Exception):
     def __init__(self, response):
         self.response = response
diff --git a/ocs_ci/ocs/resources/deployment.py b/ocs_ci/ocs/resources/deployment.py
index 370513a8b72..3c501f22db3 100644
--- a/ocs_ci/ocs/resources/deployment.py
+++ b/ocs_ci/ocs/resources/deployment.py
@@ -105,7 +105,7 @@ def set_revision(self, revision, resource_name=None):
         cmd = f"rollout undo {self.kind}/{resource_name} --to-revision={revision}"
         self.ocp.exec_oc_cmd(cmd)
 
-    def wait_for_available_replicas(self, timeout=15, sleep=3):
+    def wait_for_available_replicas(self, timeout=360, sleep=3):
         """
         Wait for number of available replicas reach number of desired replicas.
 
@@ -133,7 +133,9 @@ def _get_available_replicas():
             )
             return available_replicas
 
-        for available_replicas in TimeoutSampler(360, 2, func=_get_available_replicas):
+        for available_replicas in TimeoutSampler(
+            timeout, sleep, func=_get_available_replicas
+        ):
             if available_replicas == desired_replicas:
                 logger.info(
                     f"Deployment {self.name} reached "
diff --git a/ocs_ci/templates/hyperconverged-deployment/hyperconverged-catsrc.yaml b/ocs_ci/templates/hyperconverged-deployment/hyperconverged-catsrc.yaml
new file mode 100644
index 00000000000..a35cec24b33
--- /dev/null
+++ b/ocs_ci/templates/hyperconverged-deployment/hyperconverged-catsrc.yaml
@@ -0,0 +1,8 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: CatalogSource
+metadata:
+  name: hyperconverged-catalogsource
+  namespace: openshift-marketplace
+spec:
+  image: 'quay.io/kubevirt/hyperconverged-cluster-index:{hyperconverged_version}.0-unstable'
+  sourceType: grpc
diff --git a/ocs_ci/templates/hyperconverged-deployment/hyperconverged.yaml b/ocs_ci/templates/hyperconverged-deployment/hyperconverged.yaml
new file mode 100644
index 00000000000..345d4849c47
--- /dev/null
+++ b/ocs_ci/templates/hyperconverged-deployment/hyperconverged.yaml
@@ -0,0 +1,5 @@
+kind: HyperConverged
+apiVersion: hco.kubevirt.io/v1beta1
+metadata:
+  name: kubevirt-hyperconverged
+  namespace: kubevirt-hyperconverged
diff --git a/ocs_ci/templates/hyperconverged-deployment/hyperconverged_namespace.yaml b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_namespace.yaml
new file mode 100644
index 00000000000..7702ddcb468
--- /dev/null
+++ b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: kubevirt-hyperconverged
+  labels:
+    openshift.io/cluster-monitoring: "true"
diff --git a/ocs_ci/templates/hyperconverged-deployment/hyperconverged_operator_group.yaml b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_operator_group.yaml
new file mode 100644
index 00000000000..44d764776f7
--- /dev/null
+++ b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_operator_group.yaml
@@ -0,0 +1,5 @@
+apiVersion: operators.coreos.com/v1
+kind: OperatorGroup
+metadata:
+  name: hyperconverged-operator-group
+  namespace: kubevirt-hyperconverged
diff --git a/ocs_ci/templates/hyperconverged-deployment/hyperconverged_subscription.yaml b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_subscription.yaml
new file mode 100644
index 00000000000..5220299d948
--- /dev/null
+++ b/ocs_ci/templates/hyperconverged-deployment/hyperconverged_subscription.yaml
@@ -0,0 +1,14 @@
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+  name: community-kubevirt-hyperconverged
+  namespace: kubevirt-hyperconverged
+spec:
+  # candidate-v1.14 is the latest version of the kubevirt-hyperconverged operator that supports OCP 4.18,
+  # 1.15 is the latest version that supports OCP 4.19
+  # rule is OCP version - 3.4
+  channel: candidate-v{hyperconverged_version}
+  installPlanApproval: Automatic
+  name: community-kubevirt-hyperconverged
+  source: hyperconverged-catalogsource
+  sourceNamespace: openshift-marketplace
diff --git a/ocs_ci/templates/mce-deployment/mce_catsrc.yaml b/ocs_ci/templates/mce-deployment/mce_catsrc.yaml
index 3cebd937c55..9769b528a51 100644
--- a/ocs_ci/templates/mce-deployment/mce_catsrc.yaml
+++ b/ocs_ci/templates/mce-deployment/mce_catsrc.yaml
@@ -4,5 +4,6 @@ metadata:
   name: mce-catalogsource
   namespace: openshift-marketplace
 spec:
-  image: quay.io:443/acm-d/mce-custom-registry:2.13.0-DOWNSTREAM-2025-02-25-05-34-41
+  # latest version of the catalog image for OCP 4.18
+  image: quay.io:443/acm-d/mce-custom-registry:2.8-latest
   sourceType: grpc
diff --git a/ocs_ci/templates/mce-deployment/mce_subscription.yaml b/ocs_ci/templates/mce-deployment/mce_subscription.yaml
index 32b15714637..fd1d7b5f1c1 100644
--- a/ocs_ci/templates/mce-deployment/mce_subscription.yaml
+++ b/ocs_ci/templates/mce-deployment/mce_subscription.yaml
@@ -3,9 +3,12 @@ kind: Subscription
 metadata:
   name: multicluster-engine
   namespace: multicluster-engine
+  labels:
+    operators.coreos.com/multicluster-engine.multicluster-engine: ""
 spec:
-  channel: stable
+  channel: stable-2.8
   installPlanApproval: Automatic
   name: multicluster-engine
-  source: mce-catalogsource
+  source: qe-app-registry
   sourceNamespace: openshift-marketplace
+  startingCSV: multicluster-engine.v2.8.0
diff --git a/ocs_ci/utility/managedservice.py b/ocs_ci/utility/managedservice.py
index 3d33dce2c2e..2f3c08ad7d4 100644
--- a/ocs_ci/utility/managedservice.py
+++ b/ocs_ci/utility/managedservice.py
@@ -1,6 +1,7 @@
 import logging
 import os
 import stat
+from pathlib import Path
 
 from tempfile import NamedTemporaryFile
 
@@ -12,7 +13,12 @@
 logger = logging.getLogger(__name__)
 
 
-def generate_onboarding_token(private_key: str = None):
+def generate_onboarding_token(
+    private_key: str = None,
+    use_ticketgen_with_quota: bool = False,
+    subject_role: str = None,
+    storage_quota: int = None,
+):
     """
     Generate Onboarding token for consumer cluster via following steps:
 
@@ -27,6 +33,9 @@ def generate_onboarding_token(private_key: str = None):
 
     Args:
         private_key (str): private key for Managed Service
+        use_ticketgen_with_quota (bool): If True, the ticketgen.sh script will be run with -q flag
+        subject_role (str): Role of the subject, this role has a default in ticketgen-with-quota.sh - ocs-client
+        storage_quota (int): Quota for the storage cluster
 
     Raises:
         CommandFailed: In case the script ticketgen.sh fails.
@@ -38,12 +47,31 @@ def generate_onboarding_token(private_key: str = None):
     """
     logger.debug("Generate onboarding token for ODF to ODF deployment")
     ticketgen_script_path = os.path.join(constants.DATA_DIR, "ticketgen.sh")
-    # download ticketgen.sh script
-    logger.debug("Download and prepare ticketgen.sh script")
-    download_file(
-        "https://raw.githubusercontent.com/red-hat-storage/ocs-operator/main/hack/ticketgen/ticketgen.sh",
-        ticketgen_script_path,
-    )
+
+    if not use_ticketgen_with_quota:
+        logger.debug("Download and prepare ticketgen.sh script")
+        download_file(
+            "https://raw.githubusercontent.com/red-hat-storage/ocs-operator/main/hack/ticketgen/ticketgen.sh",
+            ticketgen_script_path,
+        )
+        if not Path(ticketgen_script_path).exists():
+            raise FileNotFoundError(
+                f"Failed to download ticketgen.sh script to {ticketgen_script_path}"
+            )
+    else:
+        logger.info("using ticketgen.sh script with --quota flag")
+        script_path = (
+            Path(__file__).resolve().parents[2]
+            / "scripts"
+            / "bash"
+            / "ticketgen-with-quota.sh"
+        )
+        if not script_path.exists():
+            raise FileNotFoundError(
+                f"Failed to find ticketgen-with-quota.sh script in {script_path}"
+            )
+        ticketgen_script_path = str(script_path)
+
     # add execute permission to the ticketgen.sh script
     current_file_permissions = os.stat(ticketgen_script_path)
     os.chmod(
@@ -67,13 +95,34 @@ def generate_onboarding_token(private_key: str = None):
             '  private_key: "..."\n'
             '  public_key: "..."'
         )
+
+    script_cmd = f"{ticketgen_script_path}"
+
+    if use_ticketgen_with_quota:
+
+        from ocs_ci.ocs.ocp import OCP
+
+        sc_obj = OCP(
+            kind=constants.STORAGECLUSTER,
+            namespace=config.ENV_DATA["cluster_namespace"],
+        )
+        uid = sc_obj.get()["items"][0]["metadata"]["uid"]
+        script_cmd += f" -c {uid} "
+
+        if subject_role:
+            script_cmd += f" -r {subject_role}"
+        if storage_quota:
+            script_cmd += f" -q {storage_quota}"
+
     with NamedTemporaryFile(
         mode="w", prefix="private", suffix=".pem", delete=True
     ) as key_file:
         key_file.write(private_key)
         key_file.flush()
         logger.debug("Generate Onboarding token")
-        ticketgen_result = exec_cmd(f"{ticketgen_script_path} {key_file.name}")
+        script_cmd += f" {key_file.name}"
+        ticketgen_result = exec_cmd(script_cmd)
+
     ticketgen_output = ticketgen_result.stdout.decode()
     if ticketgen_result.stderr:
         raise CommandFailed(
diff --git a/ocs_ci/utility/utils.py b/ocs_ci/utility/utils.py
index caaa8309716..7b50e76ef0e 100644
--- a/ocs_ci/utility/utils.py
+++ b/ocs_ci/utility/utils.py
@@ -5389,3 +5389,30 @@ def create_config_ini_file(params):
     log.info(f"config.ini file for cluster is located at {config_ini_file.name}")
 
     return config_ini_file.name
+
+
+def wait_custom_resource_defenition_available(crd_name, timeout=600):
+    """
+    Wait for the custom resource definition to be available
+
+    Args:
+        crd_name (str): Name of the custom resource definition
+        timeout (int): Time in seconds to wait for the CRD to be available
+    Returns:
+        bool: True if CRD is available, False otherwise
+
+    """
+    from ocs_ci.ocs.ocp import OCP
+
+    wait = 10
+    retry_num = timeout // wait
+    crd_obj = OCP(kind=constants.CRD_KIND)
+    return bool(
+        crd_obj.get(
+            resource_name=crd_name,
+            out_yaml_format=False,
+            retry=retry_num,
+            wait=wait,
+            dont_raise=True,
+        )
+    )
diff --git a/tests/libtest/test_provider_create_hosted_cluster.py b/tests/libtest/test_provider_create_hosted_cluster.py
index 14dd367db78..b11111545b3 100644
--- a/tests/libtest/test_provider_create_hosted_cluster.py
+++ b/tests/libtest/test_provider_create_hosted_cluster.py
@@ -56,7 +56,16 @@ def test_provider_deploy_OCP_hosted(self):
         logger.info("Test deploy hosted OCP on provider platform")
         cluster_name = list(config.ENV_DATA["clusters"].keys())[-1]
 
-        HypershiftHostedOCP(cluster_name).deploy_ocp()
+        hypershift_hosted = HypershiftHostedOCP(cluster_name)
+        hypershift_hosted.deploy_dependencies(
+            deploy_acm_hub=True,
+            deploy_cnv=True,
+            deploy_metallb=True,
+            download_hcp_binary=True,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
+        )
+        hypershift_hosted.deploy_ocp()
 
     @hci_provider_required
     def test_provider_deploy_OCP_hosted_skip_cnv_and_lb(self):
@@ -67,10 +76,16 @@ def test_provider_deploy_OCP_hosted_skip_cnv_and_lb(self):
             "Test deploy hosted OCP on provider platform with metallb and cnv ready"
         )
         cluster_name = list(config.ENV_DATA["clusters"].keys())[-1]
-
-        HypershiftHostedOCP(cluster_name).deploy_ocp(
-            deploy_cnv=False, deploy_metallb=False, download_hcp_binary=True
+        hypershift_hosted = HypershiftHostedOCP(cluster_name)
+        hypershift_hosted.deploy_dependencies(
+            deploy_acm_hub=True,
+            deploy_cnv=False,
+            deploy_metallb=False,
+            download_hcp_binary=True,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
         )
+        hypershift_hosted.deploy_ocp()
 
     @hci_provider_required
     def test_provider_deploy_OCP_hosted_skip_cnv(self):
@@ -80,7 +95,16 @@ def test_provider_deploy_OCP_hosted_skip_cnv(self):
         logger.info("Test deploy hosted OCP on provider platform with cnv ready")
         cluster_name = list(config.ENV_DATA["clusters"].keys())[-1]
 
-        HypershiftHostedOCP(cluster_name).deploy_ocp(deploy_cnv=False)
+        hypershift_hosted = HypershiftHostedOCP(cluster_name)
+        hypershift_hosted.deploy_dependencies(
+            deploy_acm_hub=True,
+            deploy_cnv=False,
+            deploy_metallb=True,
+            download_hcp_binary=True,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
+        )
+        HypershiftHostedOCP(cluster_name).deploy_ocp()
 
     @hci_provider_required
     def test_provider_deploy_OCP_hosted_multiple(self):
@@ -113,6 +137,7 @@ def test_deploy_OCP_and_setup_ODF_client_on_hosted_clusters(self):
         Test install ODF on hosted cluster
         """
         logger.info("Deploy hosted OCP on provider platform multiple times")
+
         HostedClients().do_deploy()
 
     @runs_on_provider
@@ -224,6 +249,8 @@ def test_deploy_acm(self):
             deploy_cnv=False,
             deploy_metallb=False,
             download_hcp_binary=False,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
         )
         assert validate_acm_hub_install(), "ACM not installed or MCE not configured"
 
@@ -240,6 +267,8 @@ def test_deploy_cnv(self):
             deploy_cnv=True,
             deploy_metallb=False,
             download_hcp_binary=False,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
         )
         assert hypershift_hosted.cnv_hyperconverged_installed(), "CNV not installed"
 
@@ -256,6 +285,8 @@ def test_deploy_metallb(self):
             deploy_cnv=False,
             deploy_metallb=True,
             download_hcp_binary=False,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
         )
         assert hypershift_hosted.metallb_instance_created(), "Metallb not installed"
 
@@ -272,6 +303,8 @@ def test_download_hcp(self):
             deploy_cnv=False,
             deploy_metallb=False,
             download_hcp_binary=True,
+            deploy_hyperconverged=False,
+            deploy_mce=False,
         )
         assert hypershift_hosted.hcp_binary_exists(), "HCP binary not downloaded"
 
@@ -313,8 +346,6 @@ def test_verify_native_storage(self):
                 storage_class in storage_class_classes
             ), "Storage classes ae not created as expected"
 
-    @runs_on_provider
-    @hci_provider_required
     def test_deploy_mce(self):
         """
         Test deploy mce without installting acm
@@ -327,14 +358,42 @@ def test_deploy_mce(self):
             deploy_metallb=False,
             download_hcp_binary=False,
             deploy_mce=True,
+            deploy_hyperconverged=False,
         )
 
     @hci_provider_required
-    def test_provider_deploy_OCP_hosted_skip_acm(self):
+    def test_provider_deploy_ocp_hosted_skip_acm(self):
         """
         Test deploy hosted OCP on provider platform with cnv ready beforehand
+        ! Suitable for released version of OCP only (no mce and hyperconverged)
         """
         logger.info("Test deploy hosted OCP on provider platform with cnv ready")
         cluster_name = list(config.ENV_DATA["clusters"].keys())[-1]
+        hypershift_hosted = HypershiftHostedOCP(cluster_name)
+
+        hypershift_hosted.deploy_dependencies(
+            deploy_acm_hub=False,
+            deploy_cnv=True,
+            deploy_metallb=True,
+            download_hcp_binary=True,
+            deploy_mce=False,
+            deploy_hyperconverged=False,
+        )
+
+        HypershiftHostedOCP(cluster_name).deploy_ocp()
 
-        HypershiftHostedOCP(cluster_name).deploy_ocp(deploy_acm_hub=False)
+    def test_deploy_hyperconverged(self):
+        """
+        Test deploy hyperconverged operator
+        ! Hyperconverged is used instead of unreleased CNV, to overcome catalogsource limitations on client clusters
+        """
+        logger.info("Test deploy hyperconverged operator")
+        hypershift_hosted = HypershiftHostedOCP("dummy")
+        hypershift_hosted.deploy_dependencies(
+            deploy_acm_hub=False,
+            deploy_cnv=False,
+            deploy_metallb=False,
+            download_hcp_binary=False,
+            deploy_mce=False,
+            deploy_hyperconverged=True,
+        )


Reviews:
user: login: fbalak
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T10:12:43Z
user: login: DanielOsypenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:24:15Z
user: login: DanielOsypenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:25:56Z
user: login: DanielOsypenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:27:19Z
user: login: DanielOsypenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:30:03Z
user: login: fbalak
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T11:44:47Z
user: login: dahorak
body: Overall LGTM, just small question about one condition.

About the Provider - Client jenkins job, should I add following two parameters also to the job UI (similarly as we have `DEPLOY_ACM_HUB` and `DEPLOY_CNV`?
```
* `mce_deployment` - Deploy MCE or not (Default: false)
* `deploy_hyperconverged` - Deploy hyperconverged operator or not (Default: false).  Necessary for Converged clusters with hosted clients with unreleased OCP version
```


![obrazek](https://github.com/user-attachments/assets/8dd82c9e-bcd2-49d9-8164-1d512b2a209e)

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:41:48Z
user: login: dahorak
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T12:42:08Z
user: login: DanielOsypenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:58:17Z
user: login: dahorak
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T18:56:34Z
user: login: fbalak
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:04:04Z

Review comments:
diff_hunk: @@ -384,36 +428,16 @@ def __init__(self, name):
                 f"Cluster path for desired cluster with name '{self.name}' was not found in ENV_DATA.clusters"
             )
 
-    def deploy_ocp(
-        self,
-        deploy_cnv=True,
-        deploy_acm_hub=True,
-        deploy_metallb=True,
-        download_hcp_binary=True,
-        deploy_mce=True,
-    ):
+    def deploy_ocp(self, **kwargs) -> str:
         """
         Deploy hosted OCP cluster on provisioned Provider platform
 
         Args:
-            deploy_cnv: (bool) Deploy CNV
path: ocs_ci/deployment/hosted_cluster.py
user: login: fbalak
body: It would be good to keep some arguments documented and have **kwargs after they are listed.
created_at: 2025-03-03T10:06:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -493,31 +534,6 @@ def deploy_dependencies(
             self.update_hcp_binary()
         if deploy_mce and not deploy_acm_hub:
             self.deploy_mce()
-            self.validate_mce_deployment()
-
-        provider_ocp_version = str(
-            get_semantic_version(get_ocp_version(), only_major_minor=True)
-        )
-        latest_released_ocp_version = str(
-            get_semantic_version(get_latest_release_version(), only_major_minor=True)
-        )
-
-        if provider_ocp_version > latest_released_ocp_version:
path: ocs_ci/deployment/hosted_cluster.py
user: login: fbalak
body: This looks ok to me. If the workaround is needed for older version then we can use release-4.17 branch.
created_at: 2025-03-03T10:08:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +260,50 @@ def deploy_hosted_ocp_clusters(self, cluster_names_list=None):
             # operators and finish the rest preparation steps. For the rest of the clusters we will only deploy OCP
             # with hcp command.
             first_ocp_deployment = index == 0
-            cluster_name = hosted_ocp_cluster.deploy_ocp(
-                deploy_cnv=first_ocp_deployment,
-                deploy_acm_hub=first_ocp_deployment,
+
+            # Put logic on checking and deploying dependencies here
+            if first_ocp_deployment:
+                # ACM installation is a default for Provider/Converged deployments
+                deploy_acm_hub = config.ENV_DATA.get("deploy_acm_hub_cluster", True)
+                # CNV installation is a default for Provider/Converged deployments
+                deploy_cnv = config.DEPLOYMENT.get("cnv_deployment", True)
+                deploy_mce = config.DEPLOYMENT.get("mce_deployment", False)
+                deploy_hyperconverged = config.ENV_DATA.get(
+                    "deploy_hyperconverged", False
+                )
+
+                # Validate conflicting deployments
+                if deploy_acm_hub and deploy_mce:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'deploy_acm_hub_cluster' and 'mce_deployment' are enabled. Choose one."
+                    )
+                if deploy_cnv and deploy_hyperconverged:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'cnv_deployment' and 'deploy_hyperconverged' are enabled. Choose one."
+                    )
+
+            else:
+                deploy_acm_hub = False
path: ocs_ci/deployment/hosted_cluster.py
user: login: fbalak
body: Shouldn't the values be collected from config even after first deployment?
created_at: 2025-03-03T10:12:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +260,50 @@ def deploy_hosted_ocp_clusters(self, cluster_names_list=None):
             # operators and finish the rest preparation steps. For the rest of the clusters we will only deploy OCP
             # with hcp command.
             first_ocp_deployment = index == 0
-            cluster_name = hosted_ocp_cluster.deploy_ocp(
-                deploy_cnv=first_ocp_deployment,
-                deploy_acm_hub=first_ocp_deployment,
+
+            # Put logic on checking and deploying dependencies here
+            if first_ocp_deployment:
+                # ACM installation is a default for Provider/Converged deployments
+                deploy_acm_hub = config.ENV_DATA.get("deploy_acm_hub_cluster", True)
+                # CNV installation is a default for Provider/Converged deployments
+                deploy_cnv = config.DEPLOYMENT.get("cnv_deployment", True)
+                deploy_mce = config.DEPLOYMENT.get("mce_deployment", False)
+                deploy_hyperconverged = config.ENV_DATA.get(
+                    "deploy_hyperconverged", False
+                )
+
+                # Validate conflicting deployments
+                if deploy_acm_hub and deploy_mce:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'deploy_acm_hub_cluster' and 'mce_deployment' are enabled. Choose one."
+                    )
+                if deploy_cnv and deploy_hyperconverged:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'cnv_deployment' and 'deploy_hyperconverged' are enabled. Choose one."
+                    )
+
+            else:
+                deploy_acm_hub = False
path: ocs_ci/deployment/hosted_cluster.py
user: login: DanielOsypenko
body: this function is driven by `ocs_ci/deployment/deployment.py:deploy_cluster` which has its own `deploy_cnv` call.
We have it here in case we don't want to deploy Provider and use fixture, or try to deploy clients directly from other place.

So in this case it passes the calls where we do install cnv - mandatory, when config saying this and now only check that with first client cnv and rest dependencies are installed.
Hope that answers on q. 
created_at: 2025-03-03T11:24:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -384,36 +428,16 @@ def __init__(self, name):
                 f"Cluster path for desired cluster with name '{self.name}' was not found in ENV_DATA.clusters"
             )
 
-    def deploy_ocp(
-        self,
-        deploy_cnv=True,
-        deploy_acm_hub=True,
-        deploy_metallb=True,
-        download_hcp_binary=True,
-        deploy_mce=True,
-    ):
+    def deploy_ocp(self, **kwargs) -> str:
         """
         Deploy hosted OCP cluster on provisioned Provider platform
 
         Args:
-            deploy_cnv: (bool) Deploy CNV
path: ocs_ci/deployment/hosted_cluster.py
user: login: DanielOsypenko
body: we don't use kwargs in any call, this was removed entirely, because it was bad design. Now before calling `deploy_ocp` we need to call `deploy_dependencies`, that reduces shared dependencies and makes code more flexible
created_at: 2025-03-03T11:25:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -384,36 +428,16 @@ def __init__(self, name):
                 f"Cluster path for desired cluster with name '{self.name}' was not found in ENV_DATA.clusters"
             )
 
-    def deploy_ocp(
-        self,
-        deploy_cnv=True,
-        deploy_acm_hub=True,
-        deploy_metallb=True,
-        download_hcp_binary=True,
-        deploy_mce=True,
-    ):
+    def deploy_ocp(self, **kwargs) -> str:
         """
         Deploy hosted OCP cluster on provisioned Provider platform
 
         Args:
-            deploy_cnv: (bool) Deploy CNV
path: ocs_ci/deployment/hosted_cluster.py
user: login: DanielOsypenko
body: kwargs were used because deploy_ocp is overloaded method and pylint urges on missing arguments, and kinda for future use
created_at: 2025-03-03T11:27:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -493,31 +534,6 @@ def deploy_dependencies(
             self.update_hcp_binary()
         if deploy_mce and not deploy_acm_hub:
             self.deploy_mce()
-            self.validate_mce_deployment()
-
-        provider_ocp_version = str(
-            get_semantic_version(get_ocp_version(), only_major_minor=True)
-        )
-        latest_released_ocp_version = str(
-            get_semantic_version(get_latest_release_version(), only_major_minor=True)
-        )
-
-        if provider_ocp_version > latest_released_ocp_version:
path: ocs_ci/deployment/hosted_cluster.py
user: login: DanielOsypenko
body: we don't need this workaround anymore, because it was not stable at all.
Now we have option to deploy Hyperconverged from the start, in aregular way we do it will all other dependencies, instead of installing ACM -> turning off imbedded Hyperconverged -> installing Hyperconverged upstream.
So this code is redundant and not stable, hence removed 
created_at: 2025-03-03T11:30:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -384,36 +428,16 @@ def __init__(self, name):
                 f"Cluster path for desired cluster with name '{self.name}' was not found in ENV_DATA.clusters"
             )
 
-    def deploy_ocp(
-        self,
-        deploy_cnv=True,
-        deploy_acm_hub=True,
-        deploy_metallb=True,
-        download_hcp_binary=True,
-        deploy_mce=True,
-    ):
+    def deploy_ocp(self, **kwargs) -> str:
         """
         Deploy hosted OCP cluster on provisioned Provider platform
 
         Args:
-            deploy_cnv: (bool) Deploy CNV
path: ocs_ci/deployment/hosted_cluster.py
user: login: fbalak
body: Ok, sounds reasonable.
created_at: 2025-03-03T11:43:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +260,50 @@ def deploy_hosted_ocp_clusters(self, cluster_names_list=None):
             # operators and finish the rest preparation steps. For the rest of the clusters we will only deploy OCP
             # with hcp command.
             first_ocp_deployment = index == 0
-            cluster_name = hosted_ocp_cluster.deploy_ocp(
-                deploy_cnv=first_ocp_deployment,
-                deploy_acm_hub=first_ocp_deployment,
+
+            # Put logic on checking and deploying dependencies here
+            if first_ocp_deployment:
+                # ACM installation is a default for Provider/Converged deployments
+                deploy_acm_hub = config.ENV_DATA.get("deploy_acm_hub_cluster", True)
+                # CNV installation is a default for Provider/Converged deployments
+                deploy_cnv = config.DEPLOYMENT.get("cnv_deployment", True)
+                deploy_mce = config.DEPLOYMENT.get("mce_deployment", False)
+                deploy_hyperconverged = config.ENV_DATA.get(
+                    "deploy_hyperconverged", False
+                )
+
+                # Validate conflicting deployments
+                if deploy_acm_hub and deploy_mce:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'deploy_acm_hub_cluster' and 'mce_deployment' are enabled. Choose one."
+                    )
+                if deploy_cnv and deploy_hyperconverged:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'cnv_deployment' and 'deploy_hyperconverged' are enabled. Choose one."
+                    )
+
+            else:
+                deploy_acm_hub = False
+                deploy_cnv = False
+                deploy_hyperconverged = False
+                deploy_mce = False
+
+            if not config.ENV_DATA["platform"].lower() in HCI_PROVIDER_CLIENT_PLATFORMS:
+                raise ProviderModeNotFoundException()
+
+            if not config.ENV_DATA.get("deploy_acm_hub_cluster", True):
+                deploy_acm_hub = False
path: ocs_ci/deployment/hosted_cluster.py
user: login: dahorak
body: Is this part necessary?
created_at: 2025-03-03T12:30:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,12 +260,50 @@ def deploy_hosted_ocp_clusters(self, cluster_names_list=None):
             # operators and finish the rest preparation steps. For the rest of the clusters we will only deploy OCP
             # with hcp command.
             first_ocp_deployment = index == 0
-            cluster_name = hosted_ocp_cluster.deploy_ocp(
-                deploy_cnv=first_ocp_deployment,
-                deploy_acm_hub=first_ocp_deployment,
+
+            # Put logic on checking and deploying dependencies here
+            if first_ocp_deployment:
+                # ACM installation is a default for Provider/Converged deployments
+                deploy_acm_hub = config.ENV_DATA.get("deploy_acm_hub_cluster", True)
+                # CNV installation is a default for Provider/Converged deployments
+                deploy_cnv = config.DEPLOYMENT.get("cnv_deployment", True)
+                deploy_mce = config.DEPLOYMENT.get("mce_deployment", False)
+                deploy_hyperconverged = config.ENV_DATA.get(
+                    "deploy_hyperconverged", False
+                )
+
+                # Validate conflicting deployments
+                if deploy_acm_hub and deploy_mce:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'deploy_acm_hub_cluster' and 'mce_deployment' are enabled. Choose one."
+                    )
+                if deploy_cnv and deploy_hyperconverged:
+                    raise UnexpectedDeploymentConfiguration(
+                        "Conflict: Both 'cnv_deployment' and 'deploy_hyperconverged' are enabled. Choose one."
+                    )
+
+            else:
+                deploy_acm_hub = False
+                deploy_cnv = False
+                deploy_hyperconverged = False
+                deploy_mce = False
+
+            if not config.ENV_DATA["platform"].lower() in HCI_PROVIDER_CLIENT_PLATFORMS:
+                raise ProviderModeNotFoundException()
+
+            if not config.ENV_DATA.get("deploy_acm_hub_cluster", True):
+                deploy_acm_hub = False
path: ocs_ci/deployment/hosted_cluster.py
user: login: DanielOsypenko
body: removed
created_at: 2025-03-05T09:58:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: DanielOsypenko
created_at: 2025-03-02T18:28:25Z
body: added mce nstallation rework into 2nd commit
verified on both clean cluster and on cluster with mce: https://privatebin.corp.redhat.com/?76769169232a1435#6tbg8gXQpJFF2n9ewSpVpFuo94zNAHTpK4jsD7Rfdoob 

author_association: CONTRIBUTOR
user: login: DanielOsypenko
created_at: 2025-03-07T13:28:28Z
body: verified with https://jenkins-csb-odf-qe-ocs4.dno.corp.redhat.com/view/all/job/qe-odf-provider-client-multicluster/139/
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-03-10T08:04:09Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/red-hat-storage/ocs-ci/pull/11523#pullrequestreview-2668084547" title="Approved">dahorak</a>*, *<a href="https://github.com/red-hat-storage/ocs-ci/pull/11523#" title="Author self-approved">DanielOsypenko</a>*, *<a href="https://github.com/red-hat-storage/ocs-ci/pull/11523#pullrequestreview-2670077290" title="Approved">fbalak</a>*


The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=red-hat-storage%2Focs-ci).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/red-hat-storage/ocs-ci/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE