From 2dd59d11b5081444fee6909c1c4a15725eb6eaeb Mon Sep 17 00:00:00 2001 From: shylesh Date: Tue, 12 Apr 2022 12:20:55 -0700 Subject: [PATCH] Ocp deployment using acm UI (#5562) Implement OCP DR clusters deployment using ACM UI Signed-off-by: Shylesh Kumar Mohan --- .../multicluster_acm_ocp_deployment.yaml | 4 + conf/ocsci/multicluster_mode_rdr.yaml | 3 + ocs_ci/deployment/deployment.py | 102 ++-- ocs_ci/deployment/factory.py | 12 +- ocs_ci/deployment/multicluster_deployment.py | 108 ++++ ocs_ci/ocs/constants.py | 22 + ocs_ci/ocs/exceptions.py | 4 + ocs_ci/ocs/ui/acm_ui.py | 525 +++++++++++++++++- ocs_ci/ocs/ui/base_ui.py | 6 +- ocs_ci/ocs/ui/views.py | 41 +- 10 files changed, 783 insertions(+), 44 deletions(-) create mode 100644 conf/ocsci/multicluster_acm_ocp_deployment.yaml create mode 100644 conf/ocsci/multicluster_mode_rdr.yaml create mode 100644 ocs_ci/deployment/multicluster_deployment.py diff --git a/conf/ocsci/multicluster_acm_ocp_deployment.yaml b/conf/ocsci/multicluster_acm_ocp_deployment.yaml new file mode 100644 index 00000000000..e8401fa9765 --- /dev/null +++ b/conf/ocsci/multicluster_acm_ocp_deployment.yaml @@ -0,0 +1,4 @@ +ENV_DATA: + acm_ocp_deployment: True +UI_SELENIUM: + headless: False diff --git a/conf/ocsci/multicluster_mode_rdr.yaml b/conf/ocsci/multicluster_mode_rdr.yaml new file mode 100644 index 00000000000..91d4cef6862 --- /dev/null +++ b/conf/ocsci/multicluster_mode_rdr.yaml @@ -0,0 +1,3 @@ +MULTICLUSTER: + # Other modes could be managed services + multicluster_mode: "regional-dr" diff --git a/ocs_ci/deployment/deployment.py b/ocs_ci/deployment/deployment.py index b6be80851f2..d98e95faf0f 100644 --- a/ocs_ci/deployment/deployment.py +++ b/ocs_ci/deployment/deployment.py @@ -154,12 +154,12 @@ class OCPDeployment(BaseOCPDeployment): pass - def deploy_cluster(self, log_cli_level="DEBUG"): + def do_deploy_ocp(self, log_cli_level): """ - We are handling both OCP and OCS deployment here based on flags - + Deploy OCP Args: - log_cli_level (str): log level for installer (default: DEBUG) + log_cli_level (str): log level for the installer + """ if not config.ENV_DATA["skip_ocp_deployment"]: if is_cluster_running(self.cluster_path): @@ -175,46 +175,22 @@ def deploy_cluster(self, log_cli_level="DEBUG"): collect_ocs_logs("deployment", ocs=False) raise - # Deployment of network split scripts via machineconfig API happens - # before OCS deployment. - if config.DEPLOYMENT.get("network_split_setup"): - master_zones = config.ENV_DATA.get("master_availability_zones") - worker_zones = config.ENV_DATA.get("worker_availability_zones") - # special external zone, which is directly defined by ip addr list, - # such zone could represent external services, which we could block - # access to via ax-bx-cx network split - if config.DEPLOYMENT.get("network_split_zonex_addrs") is not None: - x_addr_list = config.DEPLOYMENT["network_split_zonex_addrs"].split(",") - else: - x_addr_list = None - if config.DEPLOYMENT.get("arbiter_deployment"): - arbiter_zone = self.get_arbiter_location() - logger.debug("detected arbiter zone: %s", arbiter_zone) - else: - arbiter_zone = None - # TODO: use temporary directory for all temporary files of - # ocs-deployment, not just here in this particular case - tmp_path = Path(tempfile.mkdtemp(prefix="ocs-ci-deployment-")) - logger.debug("created temporary directory %s", tmp_path) - setup_netsplit( - tmp_path, master_zones, worker_zones, x_addr_list, arbiter_zone - ) - ocp_version = version.get_semantic_ocp_version_from_config() - if ( - config.ENV_DATA.get("deploy_acm_hub_cluster") - and ocp_version >= version.VERSION_4_9 - ): - try: - self.deploy_acm_hub() - except Exception as e: - logger.error(e) + def do_deploy_submariner(self): + """ + Deploy Submariner operator + """ # Multicluster operations if config.multicluster: # Configure submariner only on non-ACM clusters submariner = Submariner() submariner.deploy() + def do_deploy_ocs(self): + """ + Deploy OCS/ODF and run verification as well + + """ if not config.ENV_DATA["skip_ocs_deployment"]: for i in range(config.nclusters): if config.multicluster and config.get_acm_index() == i: @@ -234,7 +210,6 @@ def deploy_cluster(self, log_cli_level="DEBUG"): collect_ocs_logs("deployment", ocp=False) raise config.reset_ctx() - # Run ocs_install_verification here only in case of multicluster. # For single cluster, test_deployment will take care. if config.multicluster: @@ -251,12 +226,63 @@ def deploy_cluster(self, log_cli_level="DEBUG"): else: logger.warning("OCS deployment will be skipped") + def do_deploy_rdr(self): + """ + Call Regional DR deploy + + """ # Multicluster: Handle all ODF multicluster DR ops if config.multicluster: dr_conf = self.get_rdr_conf() deploy_dr = MultiClusterDROperatorsDeploy(dr_conf) deploy_dr.deploy() + def deploy_cluster(self, log_cli_level="DEBUG"): + """ + We are handling both OCP and OCS deployment here based on flags + + Args: + log_cli_level (str): log level for installer (default: DEBUG) + """ + self.do_deploy_ocp(log_cli_level) + # Deployment of network split scripts via machineconfig API happens + # before OCS deployment. + if config.DEPLOYMENT.get("network_split_setup"): + master_zones = config.ENV_DATA.get("master_availability_zones") + worker_zones = config.ENV_DATA.get("worker_availability_zones") + # special external zone, which is directly defined by ip addr list, + # such zone could represent external services, which we could block + # access to via ax-bx-cx network split + if config.DEPLOYMENT.get("network_split_zonex_addrs") is not None: + x_addr_list = config.DEPLOYMENT["network_split_zonex_addrs"].split(",") + else: + x_addr_list = None + if config.DEPLOYMENT.get("arbiter_deployment"): + arbiter_zone = self.get_arbiter_location() + logger.debug("detected arbiter zone: %s", arbiter_zone) + else: + arbiter_zone = None + # TODO: use temporary directory for all temporary files of + # ocs-deployment, not just here in this particular case + tmp_path = Path(tempfile.mkdtemp(prefix="ocs-ci-deployment-")) + logger.debug("created temporary directory %s", tmp_path) + setup_netsplit( + tmp_path, master_zones, worker_zones, x_addr_list, arbiter_zone + ) + ocp_version = version.get_semantic_ocp_version_from_config() + if ( + config.ENV_DATA.get("deploy_acm_hub_cluster") + and ocp_version >= version.VERSION_4_9 + ): + try: + self.deploy_acm_hub() + except Exception as e: + logger.error(e) + + self.do_deploy_submariner() + self.do_deploy_ocs() + self.do_deploy_rdr() + def get_rdr_conf(self): """ Aggregate important Regional DR parameters in the dictionary diff --git a/ocs_ci/deployment/factory.py b/ocs_ci/deployment/factory.py index a3e178bdc41..4bdd0254873 100644 --- a/ocs_ci/deployment/factory.py +++ b/ocs_ci/deployment/factory.py @@ -13,7 +13,10 @@ class DeploymentFactory(object): """ def __init__(self): - self.deployment_platform = config.ENV_DATA["platform"].lower() + if config.ENV_DATA.get("acm_ocp_deployment"): + self.deployment_platform = constants.ACM_OCP_DEPLOYMENT + else: + self.deployment_platform = config.ENV_DATA["platform"].lower() self.cls_map = {} # A map of all existing deployments and respective classes # should be put here, but only in the condition if that platform is used. @@ -76,6 +79,10 @@ def __init__(self): from .rhv import RHVIPI self.cls_map["rhv_ipi"] = RHVIPI + elif self.deployment_platform == constants.ACM_OCP_DEPLOYMENT: + from .multicluster_deployment import OCPDeployWithACM + + self.cls_map["acm_ocp_deployment"] = OCPDeployWithACM def get_deployment(self): """ @@ -84,6 +91,9 @@ def get_deployment(self): deployment_platform may look like 'aws', 'vmware', 'baremetal' deployment_type may be like 'ipi' or 'upi' """ + if config.ENV_DATA.get("acm_ocp_deployment"): + logger.info("Deployment will be done through ACM platform") + return self.cls_map[constants.ACM_OCP_DEPLOYMENT]() deployment_type = config.ENV_DATA["deployment_type"] flexy_deployment = config.ENV_DATA["flexy_deployment"] deployment_cls_key = ( diff --git a/ocs_ci/deployment/multicluster_deployment.py b/ocs_ci/deployment/multicluster_deployment.py new file mode 100644 index 00000000000..6edfb8ddca1 --- /dev/null +++ b/ocs_ci/deployment/multicluster_deployment.py @@ -0,0 +1,108 @@ +import logging + +from ocs_ci.deployment.deployment import Deployment +from ocs_ci.framework import config +from ocs_ci.ocs.exceptions import ACMClusterDeployException +from ocs_ci.ocs.ui import acm_ui +from ocs_ci.ocs.utils import get_non_acm_cluster_config +from ocs_ci.ocs.acm import acm +from ocs_ci.ocs import constants + + +logger = logging.getLogger(__name__) + + +class OCPDeployWithACM(Deployment): + """ + When we instantiate this class, the assumption is we already have + an OCP cluster with ACM installed and current context is ACM + + """ + + def __init__(self): + """ + When we init Deployment class it will have all the + ACM cluster's context + """ + super().__init__() + self.multicluster_mode = config.MULTICLUSTER.get("multicluster_mode", None) + # Used for housekeeping during multiple OCP cluster deployments + self.deployment_cluster_list = list() + # Whether to start deployment in asynchronous mode or synchronous mode + # In async deploy mode, we will have a single wait method waiting for + # all the cluster deployments to finish + self.deploy_sync_mode = config.MULTICLUSTER.get("deploy_sync_mode", "async") + self.ui_driver = None + + def do_deploy_ocp(self, log_cli_level="INFO"): + """ + This function overrides the parent's function in order accomodate + ACM based OCP cluster deployments + + """ + if self.multicluster_mode == constants.RDR_MODE: + self.do_rdr_acm_ocp_deploy() + + def do_rdr_acm_ocp_deploy(self): + """ + Specific to regional DR OCP cluster deployments + + """ + factory = acm_ui.ACMOCPDeploymentFactory() + self.ui_driver = acm.login_to_acm() + + if self.deploy_sync_mode == "async": + rdr_clusters = get_non_acm_cluster_config() + for c in rdr_clusters: + logger.info(f"{c.ENV_DATA['cluster_name']}") + logger.info(f"{c.ENV_DATA['platform']}") + logger.info(f"{c.ENV_DATA['deployment_type']}") + for cluster_conf in rdr_clusters: + deployer = factory.get_platform_instance(self.ui_driver, cluster_conf) + deployer.create_cluster_prereq() + deployer.create_cluster() + self.deployment_cluster_list.append(deployer) + # At this point deployment of all non-acm ocp clusters have been + # triggered, we need to wait for all of them to succeed + self.wait_for_all_clusters_async() + # Download kubeconfig to the respective directories + for cluster in self.deployment_cluster_list: + cluster.download_cluster_conf_files() + + def wait_for_all_clusters_async(self): + # We will say done only when none of the clusters are in + # 'Creating' state. Either they have to be in 'Ready' state + # OR 'Failed' state + done_waiting = False + while not done_waiting: + done_waiting = True + for cluster in self.deployment_cluster_list: + if cluster.deployment_status not in ["ready", "failed"]: + cluster.get_deployment_status() + done_waiting = False + # We will fail even if one of the clusters failed to deploy + failed_list = list() + success_list = list() + for cluster in self.deployment_cluster_list: + if cluster.deployment_status == "failed": + failed_list.append(cluster) + else: + success_list.append(cluster) + + if success_list: + logger.info("Deployment for following clusters Passed") + logger.info(f"{[c.cluster_name for c in success_list]}") + if failed_list: + logger.error("Deployment failed for following clusters") + logger.error(f"{[c.cluster_name for c in failed_list]}") + raise ACMClusterDeployException("one or more Cluster Deployment failed ") + + def deploy_cluster(self, log_cli_level="INFO"): + """ + We deploy new OCP clusters using ACM + Note: Importing cluster through ACM has been implemented as part + of Jenkins pipeline + + """ + + super().deploy_cluster(log_cli_level=log_cli_level) diff --git a/ocs_ci/ocs/constants.py b/ocs_ci/ocs/constants.py index ffe81e7cd90..fe652074aaa 100644 --- a/ocs_ci/ocs/constants.py +++ b/ocs_ci/ocs/constants.py @@ -11,6 +11,7 @@ import os + # Logging LOG_FORMAT = "%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s" @@ -654,6 +655,7 @@ DR_RAMEN_HUB_OPERATOR_CONFIG = "ramen-hub-operator-config" DR_RAMEN_CLUSTER_OPERATOR_CONFIG = "ramen-dr-cluster-operator-config" ODF_MULTICLUSTER_ORCHESTRATOR_CONTROLLER_MANAGER = "odfmo-controller-manager" +RDR_MODE = "regional-dr" # DR constants SUBMARINER_DOWNLOAD_URL = "https://get.submariner.io" @@ -772,6 +774,7 @@ OPENSHIFT_DEDICATED_PLATFORM = "openshiftdedicated" RHV_PLATFORM = "rhv" ROSA_PLATFORM = "rosa" +ACM_OCP_DEPLOYMENT = "acm_ocp_deployment" ON_PREM_PLATFORMS = [ VSPHERE_PLATFORM, BAREMETAL_PLATFORM, @@ -1665,3 +1668,22 @@ VAULT_TOKEN = "vaulttokens" VAULT_TENANT_SA = "vaulttenantsa" RBD_CSI_VAULT_TOKEN_REVIEWER_NAME = "rbd-csi-vault-token-review" +# ACM UI related constants +PLATFORM_XPATH_MAP = { + "vsphere": "cc_provider_vmware_vsphere", + "AWS": None, + "baremetal": None, + "azure": None, +} +ACM_PLATOFRM_VSPHERE_CRED_PREFIX = "vsphereacmocp-" +# example release image url : quay.io/openshift-release-dev/ocp-release:4.9.23-x86_64 +ACM_OCP_RELEASE_IMG_URL_PREFIX = "registry.ci.openshift.org/ocp/release" +ACM_VSPHERE_NETWORK = "VM Network" +ACM_CLUSTER_DEPLOY_TIMEOUT = 2700 # 45 minutes +ACM_CLUSTER_DEPLOYMENT_LABEL_KEY = "hive.openshift.io/cluster-deployment-name" +ACM_CLUSTER_DEPLOYMENT_SECRET_TYPE_LABEL_KEY = "hive.openshift.io/secret-type" +# Concatenated CA file for vcenter +VSPHERE_CA_FILE_PATH = os.path.join(DATA_DIR, "vsphere_ca.crt") +SSH_PRIV_KEY = os.path.expanduser(os.path.join(".ssh", "openshift-dev.pem")) +SSH_PUB_KEY = os.path.expanduser(os.path.join(".ssh", "openshift-dev.pub")) +SPACE = " " diff --git a/ocs_ci/ocs/exceptions.py b/ocs_ci/ocs/exceptions.py index ce99606b01f..2175d8d0fed 100644 --- a/ocs_ci/ocs/exceptions.py +++ b/ocs_ci/ocs/exceptions.py @@ -472,6 +472,10 @@ class BenchmarkTestFailed(Exception): pass +class ACMClusterDeployException(Exception): + pass + + class WrongVersionExpression(ValueError): pass diff --git a/ocs_ci/ocs/ui/acm_ui.py b/ocs_ci/ocs/ui/acm_ui.py index f675e0bfd68..f721eada49d 100644 --- a/ocs_ci/ocs/ui/acm_ui.py +++ b/ocs_ci/ocs/ui/acm_ui.py @@ -1,8 +1,34 @@ +import os import logging +import time +from selenium.webdriver.common.by import By +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.common.keys import Keys +from ocs_ci.ocs import constants +from ocs_ci.ocs.exceptions import ACMClusterDeployException from ocs_ci.ocs.ui.base_ui import BaseUI +from ocs_ci.ocs.ui.helpers_ui import format_locator from ocs_ci.ocs.ui.views import locators -from ocs_ci.utility.utils import get_ocp_version +from ocs_ci.utility.utils import ( + get_ocp_version, + expose_ocp_version, + run_cmd, +) +from ocs_ci.ocs.constants import ( + PLATFORM_XPATH_MAP, + ACM_PLATOFRM_VSPHERE_CRED_PREFIX, + VSPHERE_CA_FILE_PATH, + DATA_DIR, + ACM_OCP_RELEASE_IMG_URL_PREFIX, + ACM_VSPHERE_NETWORK, + ACM_CLUSTER_DEPLOY_TIMEOUT, + ACM_CLUSTER_DEPLOYMENT_LABEL_KEY, + ACM_CLUSTER_DEPLOYMENT_SECRET_TYPE_LABEL_KEY, +) +from ocs_ci.framework import config +from ocs_ci.utility.retry import retry + log = logging.getLogger(__name__) @@ -103,3 +129,500 @@ def navigate_credentials_page(self): """ log.info("Navigate into Governance Page") self.do_click(locator=self.acm_page_nav["Credentials"]) + + +class ACMOCPClusterDeployment(AcmPageNavigator): + """ + Everything related to cluster creation through ACM goes here + + """ + + def __init__(self, driver, platform, cluster_conf): + super().__init__(driver) + self.platform = platform + self.cluster_conf = cluster_conf + self.cluster_name = self.cluster_conf.ENV_DATA["cluster_name"] + self.cluster_path = self.cluster_conf.ENV_DATA["cluster_path"] + self.deploy_sync_mode = config.MULTICLUSTER.get("deploy_sync_mode", "async") + self.deployment_status = None + self.cluster_deploy_timeout = self.cluster_conf.ENV_DATA.get( + "cluster_deploy_timeout", ACM_CLUSTER_DEPLOY_TIMEOUT + ) + self.deployment_failed_reason = None + self.deployment_start_time = 0 + + def create_cluster_prereq(self): + raise NotImplementedError("Child class has to implement this method") + + def navigate_create_clusters_page(self): + # Navigate to Clusters page which has 'Create Cluster'/ + # 'Import Cluster' buttons + # Here we click on "Create Cluster" and we will be in create cluster page + while True: + self.navigate_clusters_page() + log.info("Clicking on 'CreateCluster'") + # Because of weird selenium behaviour we are checking + # for CreateCluster button in 3 different ways + # 1. CreateCluster button + # 2. CreateCluster button with index xpath + # 3. Checking url, which should end with 'create-cluster' + if not self.check_element_presence( + (By.XPATH, self.acm_page_nav["cc_create_cluster"][0]), timeout=60 + ): + log.error("Create cluster button not found") + raise ACMClusterDeployException("Can't continue with deployment") + log.info("check 1:Found create cluster button") + if not self.check_element_presence( + (By.XPATH, self.acm_page_nav["cc_create_cluster_index_xpath"][0]), + timeout=300, + ): + log.error("Create cluster button not found") + raise ACMClusterDeployException("Can't continue with deployment") + log.info("check 2:Found create cluster by index path") + self.do_click(locator=self.acm_page_nav["cc_create_cluster"], timeout=100) + time.sleep(20) + if self.driver.current_url.endswith("create-cluster"): + break + + def click_next_button(self): + self.do_click(self.acm_page_nav["cc_next_page_button"]) + + def fill_multiple_textbox(self, key_val): + """ + In a page if we want to fill multiple text boxes we can use + this function which iteratively fills in values from the dictionary parameter + + key_val (dict): keys corresponds to the xpath of text box, value corresponds + to the value to be filled in + + """ + for xpath, value in key_val.items(): + self.do_send_keys(locator=xpath, text=value) + + def click_platform_and_credentials(self): + self.navigate_create_clusters_page() + self.do_click( + locator=self.acm_page_nav[PLATFORM_XPATH_MAP[self.platform]], timeout=100 + ) + self.do_click( + locator=self.acm_page_nav["cc_infrastructure_provider_creds_dropdown"] + ) + credential = format_locator( + self.acm_page_nav["cc_infrastructure_provider_creds_select_creds"], + self.platform_credential_name, + ) + self.do_click(locator=credential) + + @retry(ACMClusterDeployException, tries=3, delay=10, backoff=1) + def goto_cluster_details_page(self): + self.navigate_clusters_page() + locator = format_locator(self.acm_page_nav["cc_table_entry"], self.cluster_name) + self.do_click(locator=locator) + self.do_click(locator=self.acm_page_nav["cc_cluster_details_page"], timeout=100) + self.choose_expanded_mode(True, self.acm_page_nav["cc_details_toggle_icon"]) + + def get_deployment_status(self): + self.goto_cluster_details_page() + if self.acm_cluster_status_failed(timeout=2): + self.deployment_status = "failed" + elif self.acm_cluster_status_ready(timeout=2): + self.deployment_status = "ready" + elif self.acm_cluster_status_creating(timeout=2): + self.deployment_status = "creating" + else: + self.deployment_status = "unknown" + + elapsed_time = int(time.time() - self.deployment_start_time) + if elapsed_time > self.cluster_deploy_timeout: + if self.deployment_status == "creating": + self.deployment_status = "failed" + self.deployment_failed_reason = "deploy_timeout" + + def wait_for_cluster_create(self): + + # Wait for status creating + staus_check_timeout = 300 + while ( + not self.acm_cluster_status_ready(staus_check_timeout) + and self.cluster_deploy_timeout >= 1 + ): + self.cluster_deploy_timeout -= staus_check_timeout + if self.acm_cluster_status_creating(): + log.info(f"Cluster {self.cluster_name} is in 'Creating' phase") + else: + self.acm_bailout_if_failed() + if self.acm_cluster_status_ready(): + log.info( + f"Cluster create successful, Cluster {self.cluster_name} is in 'Ready' state" + ) + + def acm_bailout_if_failed(self): + if self.acm_cluster_status_failed(): + raise ACMClusterDeployException("Deployment is in 'FAILED' state") + + def acm_cluster_status_failed(self, timeout=5): + return self.check_element_presence( + ( + self.acm_page_nav["cc_cluster_status_page_status_failed"][1], + self.acm_page_nav["cc_cluster_status_page_status_failed"][0], + ), + timeout=timeout, + ) + + def acm_cluster_status_ready(self, timeout=120): + return self.check_element_presence( + ( + self.acm_page_nav["cc_cluster_status_page_status_ready"][1], + self.acm_page_nav["cc_cluster_status_page_status_ready"][0], + ), + timeout=timeout, + ) + + def acm_cluster_status_creating(self, timeout=120): + return self.check_element_presence( + ( + self.acm_page_nav["cc_cluster_status_page_status_creating"][1], + self.acm_page_nav["cc_cluster_status_page_status_creating"][0], + ), + timeout=timeout, + ) + + def download_cluster_conf_files(self): + """ + Download install-config and kubeconfig to cluster dir + + """ + if not os.path.exists(os.path.expanduser(f"{self.cluster_path}")): + os.mkdir(os.path.expanduser(f"{self.cluster_path}")) + + # create auth dir inside cluster dir + auth_dir = os.path.join(os.path.expanduser(f"{self.cluster_path}"), "auth") + if not os.path.exists(auth_dir): + os.mkdir(auth_dir) + + self.download_kubeconfig(auth_dir) + + def download_kubeconfig(self, authdir): + get_kubeconf_secret_cmd = ( + f"$(oc get secret -o name -n {self.cluster_name} " + f"-l {ACM_CLUSTER_DEPLOYMENT_LABEL_KEY}={self.cluster_name} " + f"-l {ACM_CLUSTER_DEPLOYMENT_SECRET_TYPE_LABEL_KEY}=kubeconfig)" + ) + extract_cmd = ( + f"oc extract -n {self.cluster_name} " + f"{get_kubeconf_secret_cmd} " + f"--to={authdir} --confirm" + ) + run_cmd(extract_cmd) + if not os.path.exists(os.path.join(authdir, "kubeconfig")): + raise ACMClusterDeployException("Could not find the kubeconfig") + + def create_cluster(self, cluster_config=None): + """ + Create cluster using ACM UI + + Args: + cluster_config (Config): framework.Config object of complete configuration required + for deployment + + """ + raise NotImplementedError("Child class should implement this function") + + +class ACMOCPPlatformVsphereIPI(ACMOCPClusterDeployment): + """ + This class handles all behind the scene activities + for cluster creation through ACM for vsphere platform + + """ + + def __init__(self, driver, cluster_conf=None): + super().__init__(driver=driver, platform="vsphere", cluster_conf=cluster_conf) + self.platform_credential_name = cluster_conf.ENV_DATA.get( + "platform_credential_name", + f"{ACM_PLATOFRM_VSPHERE_CRED_PREFIX}{self.cluster_name}", + ) + # API VIP & Ingress IP + self.ips = None + self.vsphere_network = None + + def create_cluster_prereq(self, timeout=600): + """ + Perform all prereqs before vsphere cluster creation from ACM + + Args: + timeout (int): Timeout for any UI operations + + """ + # Create vsphre credentials + # Click on 'Add credential' in 'Infrastructure provider' page + self.navigate_create_clusters_page() + self.refresh_page() + hard_timeout = config.ENV_DATA.get("acm_ui_hard_deadline", 1200) + remaining = hard_timeout + while True: + ret = self.check_element_presence( + (By.XPATH, self.acm_page_nav[PLATFORM_XPATH_MAP[self.platform]][0]), + timeout=300, + ) + if ret: + log.info("Found platform icon") + break + else: + if remaining < 0: + raise TimeoutException("Timedout while waiting for platform icon") + else: + remaining -= timeout + self.navigate_create_clusters_page() + self.refresh_page() + + self.do_click( + locator=self.acm_page_nav[PLATFORM_XPATH_MAP[self.platform]], timeout=100 + ) + + # "Basic vsphere credential info" + # 1. credential name + # 2. Namespace + # 3. Base DNS domain + self.do_click(locator=self.acm_page_nav["cc_provider_credentials"], timeout=100) + parent_tab = self.driver.current_window_handle + tabs = self.driver.window_handles + self.driver.switch_to.window(tabs[1]) + self.do_click(locator=self.acm_page_nav["cc_provider_creds_vsphere"]) + + basic_cred_dict = { + self.acm_page_nav[ + "cc_provider_creds_vsphere_cred_name" + ]: self.platform_credential_name, + self.acm_page_nav[ + "cc_provider_creds_vsphere_base_dns" + ]: f"{self.cluster_conf.ENV_DATA['base_domain']}", + } + self.fill_multiple_textbox(basic_cred_dict) + # Credential Namespace is not a text box but a dropdown + self.do_click(self.acm_page_nav["cc_provider_creds_vsphere_cred_namespace"]) + self.do_click(self.acm_page_nav["cc_provider_creds_default_namespace"]) + + # click on 'Next' button at the bottom + self.click_next_button() + + # Detailed VMWare credentials section + # 1. vCenter server + # 2. vCenter username + # 3. vCenter password + # 4. cVenter root CA certificate + # 5. vSphere cluster name + # 6. vSphere datacenter + # 7. vSphere default Datastore + with open(VSPHERE_CA_FILE_PATH, "r") as fp: + vsphere_ca = fp.read() + vsphere_creds_dict = { + self.acm_page_nav[ + "cc_provider_creds_vsphere_vcenter_server" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_server']}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_username" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_user']}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_password" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_password']}", + self.acm_page_nav["cc_provider_creds_vsphere_rootca"]: f"{vsphere_ca}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_clustername" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_cluster']}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_dc" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_datacenter']}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_datastore" + ]: f"{self.cluster_conf.ENV_DATA['vsphere_datastore']}", + } + self.fill_multiple_textbox(vsphere_creds_dict) + self.click_next_button() + + # Pull Secret and SSH + # 1. Pull secret + # 2. SSH Private key + # 3. SSH Public key + with open(os.path.join(DATA_DIR, "pull-secret"), "r") as fp: + pull_secret = fp.read() + ssh_pub_key_path = os.path.expanduser(self.cluster_conf.DEPLOYMENT["ssh_key"]) + ssh_priv_key_path = os.path.expanduser( + self.cluster_conf.DEPLOYMENT["ssh_key_private"] + ) + + with open(ssh_pub_key_path, "r") as fp: + ssh_pub_key = fp.read() + + with open(ssh_priv_key_path, "r") as fp: + ssh_priv_key = fp.read() + + pull_secret_and_ssh = { + self.acm_page_nav["cc_provider_creds_vsphere_pullsecret"]: f"{pull_secret}", + self.acm_page_nav[ + "cc_provider_creds_vsphere_ssh_privkey" + ]: f"{ssh_priv_key}", + self.acm_page_nav["cc_provider_creds_vsphere_ssh_pubkey"]: f"{ssh_pub_key}", + } + self.fill_multiple_textbox(pull_secret_and_ssh) + self.click_next_button() + self.do_click(locator=self.acm_page_nav["cc_provider_creds_vsphere_add_button"]) + # Go to credentials tab + self.do_click(locator=self.acm_page_nav["Credentials"]) + credential_table_entry = format_locator( + self.acm_page_nav["cc_table_entry"], self.platform_credential_name + ) + if not self.check_element_presence( + (By.XPATH, credential_table_entry[0]), timeout=20 + ): + raise ACMClusterDeployException("Could not create credentials for vsphere") + else: + log.info( + f"vsphere credential successfully created {self.platform_credential_name}" + ) + # Get the ips in prereq itself + from ocs_ci.deployment import vmware + + # Switch context to cluster which we are about to create + prev_ctx = config.cur_index + config.switch_ctx(self.cluster_conf.MULTICLUSTER["multicluster_index"]) + self.ips = vmware.assign_ips(2) + vmware.create_dns_records(self.ips) + config.switch_ctx(prev_ctx) + self.driver.close() + self.driver.switch_to.window(parent_tab) + self.driver.switch_to.default_content() + + def create_cluster(self): + """ + This function navigates through following pages in the UI + 1. Cluster details + 2. Node poools + 3. Networks + 4. Proxy + 5. Automation + 6. Review + + Raises: + ACMClusterDeployException: If deployment failed for the cluster + + """ + self.navigate_create_clusters_page() + self.click_platform_and_credentials() + self.click_next_button() + self.fill_cluster_details_page() + self.click_next_button() + # For now we don't do anything in 'Node Pools' page + self.click_next_button() + self.fill_network_info() + self.click_next_button() + # Skip proxy for now + self.click_next_button() + # Skip Automation for now + self.click_next_button() + # We are at Review page + # Click on create + self.do_click(locator=self.acm_page_nav["cc_create_button"]) + self.deployment_start_time = time.time() + # We will be redirect to 'Details' page which has cluster deployment progress + if self.deploy_sync_mode == "sync": + try: + self.wait_for_cluster_create() + except ACMClusterDeployException: + log.error( + f"Failed to create OCP cluster {self.cluster_conf.ENV_DATA['cluster_name']}" + ) + raise + # Download kubeconfig and install-config file + self.download_cluster_conf_files() + else: + # Async mode of deployment, so just return to caller + # we will just wait for status 'Creating' and then return + if not self.acm_cluster_status_creating(timeout=600): + raise ACMClusterDeployException( + f"Cluster {self.cluster_name} didn't reach 'Creating' phase" + ) + self.deployment_status = "Creating" + return + + def fill_network_info(self): + """ + We need to fill following network info + 1. vSphere network name + 2. API VIP + 3. Ingress VIP + """ + self.vsphere_network = self.cluster_conf.ENV_DATA.get( + "vm_network", ACM_VSPHERE_NETWORK + ) + self.do_click(self.acm_page_nav["cc_vsphere_network_name"]) + self.do_send_keys( + self.acm_page_nav["cc_vsphere_network_name"], self.vsphere_network + ) + # Chrome has a weird problem of trimming the whitespace + # Suppose if network name is 'VM Network', when we put this text + # in text box it automatically becomes 'VMNetwork', hence we need to take + # care + ele = self.driver.find_element( + By.XPATH, self.acm_page_nav["cc_vsphere_network_name"][0] + ) + remote_text = ele.get_property("value") + if remote_text != self.vsphere_network: + # Check if we have white space char + # in network name + try: + index = self.vsphere_network.index(constants.SPACE) + left_shift_offset = len(remote_text) - index + self.do_send_keys( + self.acm_page_nav["cc_vsphere_network_name"], + f"{left_shift_offset*Keys.ARROW_LEFT}{constants.SPACE}", + ) + except ValueError: + raise ACMClusterDeployException( + "Weird browser behaviour, Not able to provide vsphere network info" + ) + + vsphere_network = { + self.acm_page_nav["cc_api_vip"]: f"{self.ips[0]}", + self.acm_page_nav["cc_ingress_vip"]: f"{self.ips[1]}", + } + self.fill_multiple_textbox(vsphere_network) + + def fill_cluster_details_page(self): + """ + Fill in following details in "Cluster details" page + 1. Cluster name + 2. Base DNS domain + 3. Release image + + """ + release_img = self.get_ocp_release_img() + cluster_details = { + self.acm_page_nav[ + "cc_cluster_name" + ]: f"{self.cluster_conf.ENV_DATA['cluster_name']}", + self.acm_page_nav["cc_openshift_release_image"]: f"{release_img}", + } + self.fill_multiple_textbox(cluster_details) + + def get_ocp_release_img(self): + vers = expose_ocp_version(self.cluster_conf.DEPLOYMENT["installer_version"]) + return f"{ACM_OCP_RELEASE_IMG_URL_PREFIX}:{vers}" + + +class ACMOCPDeploymentFactory(object): + def __init__(self): + # All platform specific classes should have map here + self.platform_map = {"vsphereipi": ACMOCPPlatformVsphereIPI} + + def get_platform_instance(self, driver, cluster_config): + """ + Args: + driver: selenium UI driver object + cluster_config (dict): Cluster Config object + """ + platform_deployment = ( + f"{cluster_config.ENV_DATA['platform']}" + f"{cluster_config.ENV_DATA['deployment_type']}" + ) + return self.platform_map[platform_deployment](driver, cluster_config) diff --git a/ocs_ci/ocs/ui/base_ui.py b/ocs_ci/ocs/ui/base_ui.py index 147ead6b614..f42455d0c22 100644 --- a/ocs_ci/ocs/ui/base_ui.py +++ b/ocs_ci/ocs/ui/base_ui.py @@ -323,8 +323,12 @@ def check_element_presence(self, locator, timeout=5): wait.until(ec.presence_of_element_located(locator)) return True except NoSuchElementException: - self.take_screenshot() logger.error("Expected element not found on UI") + self.take_screenshot() + return False + except TimeoutException: + logger.error("Timedout while waiting for element") + self.take_screenshot() return False diff --git a/ocs_ci/ocs/ui/views.py b/ocs_ci/ocs/ui/views.py index fd1dedba8b6..8c96b7dc8fd 100644 --- a/ocs_ci/ocs/ui/views.py +++ b/ocs_ci/ocs/ui/views.py @@ -455,8 +455,15 @@ By.XPATH, ), "cluster-set-selection": ("//a[normalize-space()='{}']", By.XPATH), - "cc_create_cluster": ("createCluster", By.ID), - "cc_provider_vmware_vsphere": ("//*[@id='vmware-vsphere']", By.XPATH), + "cc_create_cluster": ("//button[@id='createCluster']", By.XPATH), + "cc_create_cluster_index_xpath": ( + "(//button[normalize-space()='Create cluster'])[1]", + By.XPATH, + ), + "cc_provider_vmware_vsphere": ( + "//div[contains(text(),'VMware vSphere')]", + By.XPATH, + ), "cc_cluster_name": ("//input[@id='eman']", By.XPATH), "cc_base_dns_domain": ("//input[@id='baseDomain']", By.XPATH), "cc_openshift_release_image": ("//input[@id='imageSet']", By.XPATH), @@ -488,6 +495,10 @@ "//input[@id='namespaceName-input-toggle-select-typeahead']", By.XPATH, ), + "cc_provider_creds_default_namespace": ( + "//button[normalize-space()='default']", + By.XPATH, + ), "cc_provider_creds_vsphere_base_dns": ("//input[@id='baseDomain']", By.XPATH), "cc_provider_creds_vsphere_vcenter_server": ("//input[@id='vCenter']", By.XPATH), "cc_provider_creds_vsphere_username": ("//input[@id='username']", By.XPATH), @@ -512,7 +523,7 @@ "//button[normalize-space()='Add']", By.XPATH, ), - "cc_cluster_status_page_download_config": ( + "cc_cluster_status_page_download_config_dropdown": ( "//button[@id='download-configuration']", By.XPATH, ), @@ -520,10 +531,33 @@ "//a[normalize-space()='install-config']", By.XPATH, ), + "cc_cluster_status_page_status": ( + "//button[normalize-space()={}]", + By.XPATH, + ), "cc_cluster_status_page_status_failed": ( "//button[normalize-space()='Failed']", By.XPATH, ), + "cc_cluster_status_page_status_creating": ( + "//button[normalize-space()='Creating']", + By.XPATH, + ), + "cc_cluster_status_page_status_ready": ( + "//button[normalize-space()='Ready']", + By.XPATH, + ), + "cc_cluster_status_page_download_config_kubeconfig": ( + "//a[normalize-space()='kubeconfig']", + By.XPATH, + ), + "cc_table_entry": ("//a[normalize-space()='{}']", By.XPATH), + "cc_cluster_details_page": ("//div[text()='Details']", By.XPATH), + "cc_cluster_status_text": ("//span[text()='Status']", By.XPATH), + "cc_details_toggle_icon": ( + "//span[@class='pf-c-card__header-toggle-icon']", + By.XPATH, + ), } add_capacity = { @@ -753,6 +787,7 @@ "add_capacity": add_capacity, "validation": {**validation, **validation_4_8, **validation_4_9}, "pvc": {**pvc, **pvc_4_7, **pvc_4_8, **pvc_4_9}, + "acm_page": {**acm_page_nav, **acm_configuration}, }, "4.9": { "login": login,