コード例 #1
0
ファイル: cluster_restarter.py プロジェクト: zhan849/argo
class ClusterResumer(ClusterOperationBase):
    def __init__(self, cfg):
        assert isinstance(cfg, ClusterRestartConfig)
        self._cfg = cfg
        super(ClusterResumer,
              self).__init__(cluster_name=self._cfg.cluster_name,
                             cluster_id=self._cfg.cluster_id,
                             cloud_profile=self._cfg.cloud_profile,
                             dry_run=self._cfg.dry_run)

        # This will raise exception if name/id mapping cannot be found
        self._name_id = self._idobj.get_cluster_name_id()
        self._cluster_info = AXClusterInfo(cluster_name_id=self._name_id,
                                           aws_profile=self._cfg.cloud_profile)
        self._cluster_config = AXClusterConfig(
            cluster_name_id=self._name_id, aws_profile=self._cfg.cloud_profile)
        self._master_manager = AXMasterManager(
            cluster_name_id=self._name_id,
            region=self._cluster_config.get_region(),
            profile=self._cfg.cloud_profile)
        self._bootstrap_obj = AXBootstrap(
            cluster_name_id=self._name_id,
            aws_profile=self._cfg.cloud_profile,
            region=self._cluster_config.get_region())

        # Initialize node count to 1 as master is not in an auto scaling group
        self._total_nodes = 1
        self._cidr = str(get_public_ip()) + "/32"
        self._software_info = SoftwareInfo(info_dict=yaml.load(
            self._cluster_info.download_cluster_software_info()))

    def pre_run(self):
        if self._cluster_info.is_cluster_supported_by_portal():
            raise RuntimeError(
                "Cluster is currently supported by portal. Please login to portal to perform cluster management operations."
            )

        if self._csm.is_running():
            logger.info("Cluster is already running.")
            sys.exit(0)
        if not check_cluster_staging(cluster_info_obj=self._cluster_info,
                                     stage="stage2"):
            raise RuntimeError(
                "Cluster is not successfully installed: Stage2 information missing! Operation aborted."
            )
        self._csm.do_resume()
        self._persist_cluster_state_if_needed()

    def post_run(self):
        self._csm.done_resume()
        self._persist_cluster_state_if_needed()

    def run(self):
        if self._cfg.dry_run:
            logger.info("DRY RUN: Resuming cluster %s with software info %s",
                        self._name_id, self._software_info.to_dict())
            return

        logger.info("%s\n\nResuming cluster %s%s\n", COLOR_GREEN,
                    self._name_id, COLOR_NORM)
        # Main resume cluster routine
        try:
            self._master_manager.restart_master()
            self._recover_auto_scaling_groups()
            self._wait_for_master()
            self._ensure_restarter_access()
            self._wait_for_minions()
            ensure_manifest_temp_dir()
            self._start_platform()
            logger.info("\n\n%sSuccessfully resumed cluster %s%s\n",
                        COLOR_GREEN, self._name_id, COLOR_NORM)
        except Exception as e:
            logger.exception(e)
            raise RuntimeError(e)
        finally:
            self._disallow_restarter_access_if_needed()

    def _start_platform(self):
        """
        This step brings up Argo platform services
        :return:
        """
        logger.info("Bringing up Argo platform ...")

        self._cluster_info.download_platform_manifests_and_config(
            target_platform_manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
            target_platform_config_path=TEMP_PLATFORM_CONFIG_PATH)

        platform = AXPlatform(cluster_name_id=self._name_id,
                              aws_profile=self._cfg.cloud_profile,
                              manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
                              config_file=TEMP_PLATFORM_CONFIG_PATH,
                              software_info=self._software_info)
        platform.start()
        platform.stop_monitor()

    def _wait_for_master(self):
        """
        This step waits for master to be up and running
        :return:
        """
        count = 0
        running_master = None
        while count < WAIT_FOR_RUNNING_MASTER_RETRY:
            logger.info(
                "Waiting for master to be up and running. Trail %s / %s",
                count, WAIT_FOR_RUNNING_MASTER_RETRY)
            running_master = self._master_manager.discover_master(
                state=[EC2InstanceState.Running])
            if not running_master:
                time.sleep(5)
            else:
                logger.info("%sMaster %s is running%s", COLOR_GREEN,
                            running_master, COLOR_NORM)
                break
            count += 1
        if count == WAIT_FOR_RUNNING_MASTER_RETRY:
            raise RuntimeError(
                "Timeout waiting for master {} to come up. Please manually check cluster status"
                .format(running_master))

    def _wait_for_minions(self):
        """
        This step waits for all minions to come up and registered in Kubernetes master
        :return:
        """
        # Get kubernetes access token
        self._cluster_info.download_kube_config()
        kube_config = self._cluster_info.get_kube_config_file_path()

        # Wait for nodes to be ready.
        # Because we made sure during pause that kubernetes master already knows that all minions are gone,
        # we don't need to worry about cached minions here
        logger.info("Wait 120 seconds before Kubernetes master comes up ...")
        time.sleep(120)
        kubectl = KubernetesApiClient(config_file=kube_config)
        logger.info("Waiting for all Kubelets to be ready ...")

        trail = 0
        while True:
            try:
                all_kubelets_ready = True
                nodes = kubectl.api.list_node()
                logger.info("%s / %s nodes registered", len(nodes.items),
                            self._total_nodes)
                if len(nodes.items) < self._total_nodes:
                    all_kubelets_ready = False
                else:
                    for n in nodes.items:
                        kubelet_check = {
                            "KubeletHasSufficientDisk",
                            "KubeletHasSufficientMemory",
                            "KubeletHasNoDiskPressure", "KubeletReady",
                            "RouteCreated"
                        }
                        for cond in n.status.conditions:
                            if cond.reason in kubelet_check:
                                kubelet_check.remove(cond.reason)
                        if kubelet_check:
                            logger.info(
                                "Node %s not ready yet. Remaining Kubelet checkmarks: %s",
                                n.metadata.name, kubelet_check)
                            all_kubelets_ready = False
                            break
                        else:
                            logger.info("Node %s is ready.", n.metadata.name)
                if all_kubelets_ready:
                    logger.info("All Kubelets are ready")
                    break
            except Exception as e:
                if "Max retries exceeded" in str(e):
                    # If master API server is still not ready at this moment, we don't count as a trail
                    trail -= 1
                    logger.info("Kubernetes API server not ready yet")
                else:
                    logger.exception("Caught exception when listing nodes: %s",
                                     e)
            trail += 1
            if trail > WAIT_FOR_MINION_REG_RETRY:
                raise RuntimeError(
                    "Timeout waiting for minions to come up. Please manually check cluster status"
                )
            time.sleep(10)

    def _recover_auto_scaling_groups(self):
        """
        This steps does the following:
            - fetch the previously restored auto scaling group config. If this config cannot be found,
              we can assume that all autoscaling groups have correct configurations. This could happen
              when previous restart failed in the middle but passed this stage already, or the cluster is
              not even paused
            - Wait for all instances to be in service
        :return:
        """
        # Get previously persisted asg status
        logger.info("Fetching last cluster status ...")
        cluster_status_raw = self._cluster_info.download_cluster_status_before_pause(
        )

        asg_mgr = AXUserASGManager(cluster_name_id=self._name_id,
                                   aws_profile=self._cfg.cloud_profile,
                                   region=self._cluster_config.get_region())

        if cluster_status_raw:
            logger.info("Found last cluster status, restoring cluster ...")
            cluster_status = yaml.load(cluster_status_raw)
            all_asg_statuses = cluster_status["asg_status"]

            # Restore minions
            for asg_name in all_asg_statuses.keys():
                asg_status = all_asg_statuses[asg_name]
                min_size = asg_status["min_size"]
                max_size = asg_status["max_size"]
                desired = asg_status["desired_capacity"]
                self._total_nodes += desired
                logger.info(
                    "Recovering autoscaling group %s. Min: %s, Max: %s, Desired: %s",
                    asg_name, min_size, max_size, desired)
                asg_mgr.set_asg_spec(name=asg_name,
                                     minsize=min_size,
                                     maxsize=max_size,
                                     desired=desired)

            logger.info("Waiting for all auto scaling groups to scale up ...")
            asg_mgr.wait_for_desired_asg_state()
            logger.info("%sAll cluster instances are in service%s",
                        COLOR_GREEN, COLOR_NORM)

            # Delete previously stored cluster status
            self._cluster_info.delete_cluster_status_before_pause()
        else:
            all_asgs = asg_mgr.get_all_asgs()
            for asg in all_asgs:
                self._total_nodes += asg["DesiredCapacity"]

            logger.info(
                "Cannot find last cluster status, cluster already resumed with %s nodes",
                self._total_nodes)

    def _ensure_restarter_access(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Restarting cluster from a not trusted IP (%s). Temporarily allowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[],
                new_cidr=[self._cidr],
                action_name="allow-cluster-manager")

    def _disallow_restarter_access_if_needed(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Restarting cluster from a not trusted IP (%s). Disallowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[self._cidr],
                new_cidr=[],
                action_name="disallow-cluster-manager")
コード例 #2
0
ファイル: cluster_upgrader.py プロジェクト: zhan849/argo
class ClusterUpgrader(ClusterOperationBase):
    def __init__(self, cfg):
        assert isinstance(cfg, ClusterUpgradeConfig)
        self._cfg = cfg
        super(ClusterUpgrader,
              self).__init__(cluster_name=self._cfg.cluster_name,
                             cluster_id=self._cfg.cluster_id,
                             cloud_profile=self._cfg.cloud_profile,
                             dry_run=self._cfg.dry_run)

        # This will raise exception if name/id mapping cannot be found
        self._name_id = self._idobj.get_cluster_name_id()
        self._cluster_info = AXClusterInfo(cluster_name_id=self._name_id,
                                           aws_profile=self._cfg.cloud_profile)
        self._cluster_config = AXClusterConfig(
            cluster_name_id=self._name_id, aws_profile=self._cfg.cloud_profile)
        self._bootstrap_obj = AXBootstrap(
            cluster_name_id=self._name_id,
            aws_profile=self._cfg.cloud_profile,
            region=self._cluster_config.get_region())
        self._current_software_info = SoftwareInfo(info_dict=yaml.load(
            self._cluster_info.download_cluster_software_info()))
        self._cidr = str(get_public_ip()) + "/32"
        self._upgrade_kube_needed = True
        self._upgrade_service_needed = True

    def pre_run(self):
        if self._cluster_info.is_cluster_supported_by_portal():
            raise RuntimeError(
                "Cluster is currently supported by portal. Please login to portal to perform cluster management operations."
            )

        if not check_cluster_staging(cluster_info_obj=self._cluster_info,
                                     stage="stage2"):
            raise RuntimeError(
                "Cluster is not successfully installed: Stage2 information missing! Operation aborted."
            )

        self._runtime_validation()

        self._csm.do_upgrade()
        self._persist_cluster_state_if_needed()

        if self._cfg.target_software_info.kube_installer_version == self._current_software_info.kube_installer_version \
            and self._cfg.target_software_info.kube_version == self._current_software_info.kube_version \
                and not self._cfg.force_upgrade:
            self._upgrade_kube_needed = False

        if self._cfg.target_software_info.image_namespace == self._current_software_info.image_namespace \
            and self._cfg.target_software_info.image_version == self._current_software_info.image_version \
            and self._cfg.target_software_info.image_version != "latest" \
            and not self._upgrade_kube_needed \
                and not self._cfg.force_upgrade:
            self._upgrade_service_needed = False

        if not self._upgrade_kube_needed and not self._upgrade_service_needed:
            logger.info(
                "%sCluster's software versions is not changed, not performing upgrade.%s",
                COLOR_GREEN, COLOR_NORM)
            logger.info(
                "%sIf you want to force upgrade cluster, please specify --force-upgrade flag.%s",
                COLOR_YELLOW, COLOR_NORM)
            sys.exit(0)

    def run(self):
        upgrade_info = "    Software Image: {}:{}  ->  {}:{}\n".format(
            self._current_software_info.image_namespace,
            self._current_software_info.image_version,
            self._cfg.target_software_info.image_namespace,
            self._cfg.target_software_info.image_version)
        upgrade_info += "    Kubernetes: {}  ->  {}\n".format(
            self._current_software_info.kube_version,
            self._cfg.target_software_info.kube_version)
        upgrade_info += "    Kubernetes Installer: {}  ->  {}".format(
            self._current_software_info.kube_installer_version,
            self._cfg.target_software_info.kube_installer_version)
        logger.info("\n\n%sUpgrading cluster %s:\n\n%s%s\n", COLOR_GREEN,
                    self._name_id, upgrade_info, COLOR_NORM)

        if self._cfg.dry_run:
            logger.info("DRY RUN: upgrading cluster %s", self._name_id)
            return

        # Main upgrade cluster routine
        try:
            self._ensure_credentials()

            self._ensure_upgrader_access()

            ensure_manifest_temp_dir()

            if self._upgrade_service_needed:
                self._shutdown_platform()

            if self._upgrade_kube_needed:
                self._upgrade_kube()

            if self._upgrade_service_needed:
                self._start_platform()
                self._cluster_info.upload_platform_manifests_and_config(
                    platform_manifest_root=self._cfg.manifest_root,
                    platform_config=self._cfg.bootstrap_config)
            logger.info("\n\n%sSuccessfully upgraded cluster %s:\n\n%s%s\n",
                        COLOR_GREEN, self._name_id, upgrade_info, COLOR_NORM)
        except Exception as e:
            logger.exception(e)
            raise RuntimeError(e)
        finally:
            self._disallow_upgrader_access_if_needed()

    def post_run(self):
        self._csm.done_upgrade()
        self._persist_cluster_state_if_needed()

    def _runtime_validation(self):
        all_errs = []
        # Abort operation if cluster is not successfully installed
        if not check_cluster_staging(cluster_info_obj=self._cluster_info,
                                     stage="stage2"):
            all_errs.append(
                "Cannot upgrade cluster that is not successfully installed: Stage2 information missing!"
            )

        cluster_status_raw = self._cluster_info.download_cluster_status_before_pause(
        )
        if cluster_status_raw:
            all_errs.append(
                "Upgrading a paused cluster is not currently supported. Please restart it first"
            )

        # Abort operation if registry information changed
        if self._cfg.target_software_info.registry != self._current_software_info.registry \
            or self._cfg.target_software_info.registry_secrets != self._current_software_info.registry_secrets:
            all_errs.append(
                "Changing registry information during upgrade is not supported currently!"
            )

        # Abort operation if ami information changed
        if self._cfg.target_software_info.ami_name != self._current_software_info.ami_name \
            or (self._cfg.target_software_info.ami_id and self._cfg.target_software_info.ami_id != self._current_software_info.ami_id):
            all_errs.append(
                "Upgrading AMI information is not currently supported.")

        if all_errs:
            raise RuntimeError(
                "Upgrade aborted. Error(s): {}".format(all_errs))

    def _ensure_credentials(self):
        self._cluster_info.download_kube_config()
        self._cluster_info.download_kube_key()

    def _shutdown_platform(self):
        """
        This step shuts down platform based on the config and manifest provided
        :return:
        """
        logger.info("Shutting Argo platform ...")
        self._cluster_info.download_platform_manifests_and_config(
            target_platform_manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
            target_platform_config_path=TEMP_PLATFORM_CONFIG_PATH)
        platform = AXPlatform(cluster_name_id=self._name_id,
                              aws_profile=self._cfg.cloud_profile,
                              manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
                              config_file=TEMP_PLATFORM_CONFIG_PATH)
        platform.stop()
        platform.stop_monitor()

    def _upgrade_kube(self):
        """
        This function calls our script to upgrade Kubernetes and cluster nodes
        :return:
        """
        env = {
            "CLUSTER_NAME_ID": self._name_id,
            "AX_CUSTOMER_ID": AXCustomerId().get_customer_id(),
            "OLD_KUBE_VERSION": self._current_software_info.kube_version,
            "NEW_KUBE_VERSION": self._cfg.target_software_info.kube_version,
            "NEW_CLUSTER_INSTALL_VERSION":
            self._cfg.target_software_info.kube_installer_version,
            "ARGO_AWS_REGION": self._cluster_config.get_region(),
            "AX_TARGET_CLOUD": Cloud().target_cloud()
        }

        if self._cfg.cloud_profile:
            env["ARGO_AWS_PROFILE"] = self._cfg.cloud_profile

        logger.info("Upgrading Kubernetes with environments %s", pformat(env))
        env.update(os.environ)
        subprocess.check_call(["upgrade-kubernetes"], env=env)

    def _start_platform(self):
        """
        This step brings up Argo platform services
        :return:
        """
        logger.info("Bringing up Argo platform ...")

        platform = AXPlatform(cluster_name_id=self._name_id,
                              aws_profile=self._cfg.cloud_profile,
                              manifest_root=self._cfg.manifest_root,
                              config_file=self._cfg.bootstrap_config,
                              software_info=self._cfg.target_software_info)
        platform.start()
        platform.stop_monitor()

    def _ensure_upgrader_access(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Upgrading cluster from a not trusted IP (%s). Temporarily allowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[],
                new_cidr=[self._cidr],
                action_name="allow-cluster-manager")

    def _disallow_upgrader_access_if_needed(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Upgrading cluster from a not trusted IP (%s). Disallowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[self._cidr],
                new_cidr=[],
                action_name="disallow-cluster-manager")
コード例 #3
0
class ClusterUninstaller(ClusterOperationBase):
    def __init__(self, cfg):
        assert isinstance(cfg, ClusterUninstallConfig)
        self._cfg = cfg
        super(ClusterUninstaller,
              self).__init__(cluster_name=self._cfg.cluster_name,
                             cluster_id=self._cfg.cluster_id,
                             cloud_profile=self._cfg.cloud_profile,
                             dry_run=self._cfg.dry_run)

        # This will raise exception if name/id mapping cannot be found
        self._name_id = self._idobj.get_cluster_name_id()
        self._cluster_info = AXClusterInfo(cluster_name_id=self._name_id,
                                           aws_profile=self._cfg.cloud_profile)
        self._cluster_config = AXClusterConfig(
            cluster_name_id=self._name_id, aws_profile=self._cfg.cloud_profile)

        # Initialize node count to 1 as master is not in an auto scaling group
        self._total_nodes = 1
        self._cidr = str(get_public_ip()) + "/32"

    def pre_run(self):
        if self._cluster_info.is_cluster_supported_by_portal():
            raise RuntimeError(
                "Cluster is currently supported by portal. Please login to portal to perform cluster management operations."
            )
        # Abort operation if cluster is not successfully installed
        if not check_cluster_staging(
                cluster_info_obj=self._cluster_info,
                stage="stage2") and not self._cfg.force_uninstall:
            raise RuntimeError(
                "Cluster is not successfully installed or has already been half deleted. If you really want to uninstall the cluster, please add '--force-uninstall' flag to finish uninstalling cluster. e.g. 'argocluster uninstall --force-uninstall --cluster-name xxx'"
            )
        if not self._csm.is_running() and not self._cfg.force_uninstall:
            raise RuntimeError(
                "Cluster is not in Running state. If you really want to uninstall the cluster, please add '--force-uninstall' flag to finish uninstalling cluster. e.g. 'argocluster uninstall --force-uninstall --cluster-name xxx'"
            )
        self._csm.do_uninstall()
        self._ensure_critical_information()
        self._persist_cluster_state_if_needed()

    def post_run(self):
        return

    def run(self):
        if self._cfg.dry_run:
            logger.info("DRY RUN: Uninstalling cluster %s", self._name_id)
            return

        logger.info("%s\n\nUninstalling cluster %s%s\n", COLOR_GREEN,
                    self._name_id, COLOR_NORM)

        # Main uninstall cluster routine
        try:
            self._check_cluster_before_uninstall()

            # We only need to keep stage0 information, which is an indication of we still need to
            # clean up the Kubernetes cluster
            self._cluster_info.delete_staging_info("stage2")
            self._cluster_info.delete_staging_info("stage1")
            self._clean_up_kubernetes_cluster()

            # As _clean_up_argo_specific_cloud_infrastructure() will clean everything inside bucket
            # that is related to this cluster, stage0 information is not explicitly deleted here
            self._clean_up_argo_specific_cloud_infrastructure()

            logger.info("\n\n%sSuccessfully uninstalled cluster %s%s\n",
                        COLOR_GREEN, self._name_id, COLOR_NORM)
        except Exception as e:
            logger.exception(e)
            raise RuntimeError(e)

    def _ensure_critical_information(self):
        """
        If not force uninstall, we don't require user to provide a cloud regions / placement and therefore
        these 2 fields in self._cfg are None. We need to load them from cluster config
        :return:
        """
        load_from_cluster_config = True
        if self._cfg.force_uninstall:
            if self._cfg.cloud_region and self._cfg.cloud_placement:
                load_from_cluster_config = False
            elif not check_cluster_staging(cluster_info_obj=self._cluster_info,
                                           stage="stage0"):
                # Fail uninstall when cluster_config does not exist and region/placement
                # information are not provided
                raise RuntimeError("""

        Cluster Stage 0 information is missing. Cluster is either not installed or it's management records in S3 are broken.
        If you believe there is still resource leftover, please provide cluster's region/placement information using
        "--cloud-placement" and "--cloud-region"
        
                    """)

        if load_from_cluster_config:
            self._cfg.cloud_region = self._cluster_config.get_region()
            self._cfg.cloud_placement = self._cluster_config.get_zone()

    def _clean_up_argo_specific_cloud_infrastructure(self):
        """
        This step cleans up components in cloud provider that are specifically needed by
        Argo cluster, including:
            - Buckets (everything under this cluster's directory)
            - Server certificates
        :return:
        """
        logger.info(
            "Cluster uninstall step: Clean Up Argo-specific Infrastructure")
        AXClusterBuckets(self._name_id, self._cfg.cloud_profile,
                         self._cfg.cloud_region).delete()

        # Delete server certificates: This code is deleting the default server certificates created
        # by public and private elb. Since server certs cannot be tagged, we need to delete them this way.
        certname = ManagedElb.get_elb_name(self._name_id, "ing-pub")
        delete_server_certificate(self._cfg.cloud_profile, certname)
        certname = ManagedElb.get_elb_name(self._name_id, "ing-pri")
        delete_server_certificate(self._cfg.cloud_profile, certname)

    def _clean_up_kubernetes_cluster(self):
        """
        This step cleans up Kubernetes if needed. It only touches components in cloud provider that
        Kubernetes needs, including:
            - Load Balancers
            - Instances
            - Auto scaling groups
            - launch configurations
            - Volumes
            - Security groups
            - Elastic IPs
            - VPCs (If this VPC is not shared)
        :return:
        """
        if not check_cluster_staging(
                cluster_info_obj=self._cluster_info,
                stage="stage0") and not self._cfg.force_uninstall:
            logger.info("Skip clean up Kubernetes cluster")
            return

        logger.info("Cluster uninstall step: Clean Up Kubernetes Cluster")

        if self._cfg.force_uninstall:
            msg = "{}\n\nIt is possible that cluster S3 bucket is accidentally deleted,\n".format(
                COLOR_YELLOW)
            msg += "or S3 bucket information has been altered unintentionally. In this\n"
            msg += "case, we still try to delete cluster since this is force uninstall.\n"
            msg += "NOTE: cluster deletion might NOT be successful and still requires\n"
            msg += "user to clean up left-over resources manually.{}\n".format(
                COLOR_NORM)
            logger.warning(msg)

        env = {
            "KUBERNETES_PROVIDER": self._cfg.cloud_provider,
            "KUBE_AWS_ZONE": self._cfg.cloud_placement,
            "KUBE_AWS_INSTANCE_PREFIX": self._name_id
        }

        if self._cfg.cloud_profile:
            env["AWS_DEFAULT_PROFILE"] = self._cfg.cloud_profile

        logger.info("\n\n%sCalling kube-down ...%s\n", COLOR_GREEN, COLOR_NORM)
        AXKubeUpDown(cluster_name_id=self._name_id,
                     env=env,
                     aws_profile=self._cfg.cloud_profile).down()

        # TODO (#111): revise volume teardown in GCP
        if Cloud().target_cloud_aws():
            delete_tagged_ebs(aws_profile=self._cfg.cloud_profile,
                              tag_key=COMMON_CLOUD_RESOURCE_TAG_KEY,
                              tag_value=self._name_id,
                              region=self._cfg.cloud_region)

    def _check_cluster_before_uninstall(self):
        """
        This step does sanity check before uninstalling the cluster.
        :return:
        """
        if not self._cfg.force_uninstall:
            logger.info("Cluster uninstall step: Sanity Checking")
            self._cluster_info.download_kube_config()
            self._ensure_uninstaller_access()
            self._check_cluster_fixture(kube_config_path=self._cluster_info.
                                        get_kube_config_file_path())
        else:
            msg = "{}\n\nForce uninstall: Skip checking cluster. Note that uninstall might fail if there is\n".format(
                COLOR_YELLOW)
            msg += "still managed fixture hooked up with cluster. In case cluster uninstall failed due to AWS\n"
            msg += "resource dependency, please manually clean up those resources and retry uninstall.\n{}".format(
                COLOR_NORM)
            logger.warning(msg)

    @staticmethod
    def _check_cluster_fixture(kube_config_path):
        """
        This step checks if the cluster has any fixture hooked up.
            - If there are fixtures hooked up, we abort uninstall, as we don't know how to tear down managed
               fixtures when we clean up cloud resources
            - If we don't know whether there is fixture or not, we print out a warning for now and continue
        :param kube_config_path: path to kube_config
        :return:
        """
        with open(kube_config_path, "r") as f:
            config_data = f.read()
        kube_config = yaml.load(config_data)
        username = None
        password = None

        # All kubeconfig we generate has only 1 cluster
        server = kube_config["clusters"][0]["cluster"]["server"]

        for user in kube_config.get("users", []):
            u = user["user"]
            if u.get("username", ""):
                username = u.get("username")
                password = u.get("password")
                break
        if not (username and password):
            logger.warning(
                "%sFailed to check managed fixture because Kubernetes credentials cannot be found to access cluster%s",
                COLOR_YELLOW, COLOR_NORM)
            return

        cmd = [
            "curl", "--insecure", "--silent", "-u",
            "{}:{}".format(username, password), "--max-time", "15",
            "{server}/api/v1/proxy/namespaces/axsys/services/fixturemanager/v1/fixture/instances?deleted=false"
            .format(server=server)
        ]

        try:
            ret = subprocess.check_output(cmd)
        except subprocess.CalledProcessError as cpe:
            msg = "{}\n\nFailed to check cluster fixture state due to {}. Cluster might\n".format(
                COLOR_YELLOW, cpe)
            msg += "not be healthy. We will proceed to uninstall cluster with best effort. Note if there are\n"
            msg += "fixtures that are not cleaned up, uninstall can fail. You can manually\n"
            msg += "clean them up and uninstall again.\n{}".format(COLOR_NORM)
            logger.warning(msg)
            return

        if ret:
            try:
                fixture = json.loads(ret).get("data", [])
                if fixture:
                    logger.error("Remaining fixtures:\n%s", fixture)
                    raise RuntimeError(
                        "Please cleanup all fixtures before doing uninstall. Or use '--force-uninstall' option to skip this check"
                    )
                else:
                    logger.info(
                        "Cluster has no fixture hooked up, proceed to uninstall."
                    )
            except ValueError as ve:
                # In case cluster is not healthy, command output will not be able to loaded
                # as json. Currently treat it same as "Cannot get fixture data" case
                logger.warning(
                    "Cannot parse fixture info: %s. Assume cluster has no fixture, proceed to uninstall. Fixture info: %s",
                    ve, ret)
        else:
            logger.warning(
                "Cannot get fixture data. Assume that cluster has no fixture hooked up, proceed to uninstall."
            )

    def _ensure_uninstaller_access(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Pausing cluster from a not trusted IP (%s). Temporarily allowing access.",
                self._cidr)
            bootstrap = AXBootstrap(cluster_name_id=self._name_id,
                                    aws_profile=self._cfg.cloud_profile,
                                    region=self._cfg.cloud_region)
            bootstrap.modify_node_security_groups(
                old_cidr=[],
                new_cidr=[self._cidr],
                action_name="allow-cluster-manager")
コード例 #4
0
class ClusterPauser(ClusterOperationBase):
    def __init__(self, cfg):
        assert isinstance(cfg, ClusterPauseConfig)
        self._cfg = cfg
        super(ClusterPauser,
              self).__init__(cluster_name=self._cfg.cluster_name,
                             cluster_id=self._cfg.cluster_id,
                             cloud_profile=self._cfg.cloud_profile,
                             dry_run=self._cfg.dry_run)

        # This will raise exception if name/id mapping cannot be found
        self._name_id = self._idobj.get_cluster_name_id()
        self._cluster_info = AXClusterInfo(cluster_name_id=self._name_id,
                                           aws_profile=self._cfg.cloud_profile)
        self._cluster_config = AXClusterConfig(
            cluster_name_id=self._name_id, aws_profile=self._cfg.cloud_profile)
        self._master_manager = AXMasterManager(
            cluster_name_id=self._name_id,
            region=self._cluster_config.get_region(),
            profile=self._cfg.cloud_profile)
        self._bootstrap_obj = AXBootstrap(
            cluster_name_id=self._name_id,
            aws_profile=self._cfg.cloud_profile,
            region=self._cluster_config.get_region())
        self._cidr = str(get_public_ip()) + "/32"

    def pre_run(self):
        if self._cluster_info.is_cluster_supported_by_portal():
            raise RuntimeError(
                "Cluster is currently supported by portal. Please login to portal to perform cluster management operations."
            )
        if self._csm.is_paused():
            logger.info("Cluster is already paused.")
            sys.exit(0)

        # This is for backward compatibility
        if not check_cluster_staging(cluster_info_obj=self._cluster_info,
                                     stage="stage2"):
            raise RuntimeError(
                "Cluster is not successfully installed: Stage2 information missing! Operation aborted."
            )
        self._csm.do_pause()
        self._persist_cluster_state_if_needed()

    def run(self):
        if self._cfg.dry_run:
            logger.info("DRY RUN: pausing cluster %s", self._name_id)
            return

        # Check if cluster's master is paused already. Since terminating master is the very last thing
        # of pausing cluster, if master is already stopped, cluster has already been successfully paused
        stopped_master = self._master_manager.discover_master(
            state=[EC2InstanceState.Stopped])
        if stopped_master:
            logger.info(
                "\n\n%sMaster %s already stopped. Cluster %s already paused%s\n",
                COLOR_GREEN, stopped_master, self._name_id, COLOR_NORM)
            return
        else:
            logger.info("\n\n%sPausing cluster %s%s\n", COLOR_GREEN,
                        self._name_id, COLOR_NORM)

        # Main pause cluster routine
        try:
            self._ensure_pauser_access()
            ensure_manifest_temp_dir()
            self._shutdown_platform()
            self._scale_down_auto_scaling_groups()
            self._wait_for_deregistering_minions()
            logger.info("Stopping master ...")
            self._master_manager.stop_master()
            logger.info("\n\n%sSuccessfully paused cluster %s%s\n",
                        COLOR_GREEN, self._name_id, COLOR_NORM)
        except Exception as e:
            logger.exception(e)
            raise RuntimeError(e)
        finally:
            self._disallow_pauser_access_if_needed()

    def post_run(self):
        self._csm.done_pause()
        self._persist_cluster_state_if_needed()

    def _wait_for_deregistering_minions(self):
        """
        This step waits for all minions to be de-registered from Kubernetes master,
        e.g. `kubectl get nodes` returns no minions besides master
        :return:
        """
        # Wait for kubernetes master de-register all minions
        logger.info(
            "Waiting for Kubernetes master to de-register all existing minions"
        )
        self._cluster_info.download_kube_config()
        kube_config = self._cluster_info.get_kube_config_file_path()
        kubectl = KubernetesApiClient(config_file=kube_config)
        while True:
            try:
                nodes = kubectl.api.list_node()
                node_names = []

                # list nodes should only show master now
                if len(nodes.items) > 1:
                    for n in nodes.items:
                        node_names.append(n.metadata.name)
                    logger.info("Remaining Kubernetes minions: %s", node_names)
                else:
                    # I don't see it necessary to check if the remaining node is master or not
                    logger.info("%sAll minions de-registered from master%s",
                                COLOR_GREEN, COLOR_NORM)
                    break
            except Exception as e:
                logger.warning("Caught exception when listing nodes: %s", e)
            time.sleep(15)

    def _scale_down_auto_scaling_groups(self):
        """
        This step:
            - Persist autoscaling group states to S3,
            - Scale down all autoscaling groups to zero,
            - Wait for all minion to be terminated
        :return:
        """
        logger.info("Discovering autoscaling groups")
        asg_mgr = AXUserASGManager(cluster_name_id=self._name_id,
                                   aws_profile=self._cfg.cloud_profile,
                                   region=self._cluster_config.get_region())
        all_asgs = asg_mgr.get_all_asgs()

        # Generate cluster status before pause. This is used to recover same amount of nodes
        # when we want to restart cluster
        cluster_status = {"asg_status": {}}
        for asg in all_asgs:
            cluster_status["asg_status"][asg["AutoScalingGroupName"]] = {
                "min_size": asg["MinSize"],
                "max_size": asg["MaxSize"],
                "desired_capacity": asg["DesiredCapacity"]
            }
        self._cluster_info.upload_cluster_status_before_pause(
            status=yaml.dump(cluster_status))

        # Scale down asg
        logger.info("Scaling down autoscaling groups ...")
        for asg in all_asgs:
            asg_name = asg["AutoScalingGroupName"]
            asg_mgr.set_asg_spec(name=asg_name, minsize=0, maxsize=0)

        # Waiting for nodes to be terminated
        logger.info("Waiting for all auto scaling groups to scale down ...")
        asg_mgr.wait_for_desired_asg_state()
        logger.info("%sAll cluster nodes are terminated%s", COLOR_GREEN,
                    COLOR_NORM)

    def _shutdown_platform(self):
        """
        This step shuts down platform based on the config and manifest provided
        :return:
        """
        logger.info("Shutting platform for pausing the cluster ...")
        self._cluster_info.download_platform_manifests_and_config(
            target_platform_manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
            target_platform_config_path=TEMP_PLATFORM_CONFIG_PATH)
        platform = AXPlatform(cluster_name_id=self._name_id,
                              aws_profile=self._cfg.cloud_profile,
                              manifest_root=TEMP_PLATFORM_MANIFEST_ROOT,
                              config_file=TEMP_PLATFORM_CONFIG_PATH)
        platform.stop()
        platform.stop_monitor()

    def _ensure_pauser_access(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Pausing cluster from a not trusted IP (%s). Temporarily allowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[],
                new_cidr=[self._cidr],
                action_name="allow-cluster-manager")

    def _disallow_pauser_access_if_needed(self):
        if self._cidr not in self._cluster_config.get_trusted_cidr():
            logger.info(
                "Pausing cluster from a not trusted IP (%s). Disallowing access.",
                self._cidr)
            self._bootstrap_obj.modify_node_security_groups(
                old_cidr=[self._cidr],
                new_cidr=[],
                action_name="disallow-cluster-manager")