Beispiel #1
0
def delete_ap_policy(custom_objects: CustomObjectsApi, name,
                     namespace) -> None:
    """
    Delete a AppProtect policy.
    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete a AP policy: {name}")
    delete_options = client.V1DeleteOptions()
    custom_objects.delete_namespaced_custom_object("appprotect.f5.com",
                                                   "v1beta1", namespace,
                                                   "appolicies", name,
                                                   delete_options)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "appprotect.f5.com",
        "v1beta1",
        namespace,
        "appolicies",
        name,
    )
    time.sleep(3)
    print(f"AP policy was removed with name: {name}")
Beispiel #2
0
class TestSparkOperator(TestCase):
    def setUp(self):
        self.namespace = "default"
        self.yml_file = "{}/files/spark.yml".format(os.path.abspath('.'))
        self.group = 'sparkoperator.k8s.io'
        self.version = 'v1beta1'
        self.plural = 'sparkapplications'
        self.job_name = "{}-{}".format("spark-test-job", int(time.time()))
        config = kube_config.load_kube_config()
        self.api_instance = CustomObjectsApi(ApiClient(config))

    def tearDown(self):
        self.api_instance.delete_namespaced_custom_object(
            group=self.group,
            version=self.version,
            namespace=self.namespace,
            plural=self.plural,
            name=self.job_name,
            body=V1DeleteOptions())

    def test_execute(self):
        dag = DAG(dag_id='test_spark_operator', start_date=datetime.now())
        task = SparkJobOperator(dag=dag,
                                task_id='test_k8s_spark_task',
                                namespace=self.namespace,
                                job_name=self.job_name,
                                yml_file=self.yml_file,
                                timeout=60)
        ti = TaskInstance(task=task, execution_date=datetime.now())
        task.execute(ti.get_template_context())
def _patch_and_delete_stubborn_custom_resources(  # type: ignore
    group: str,
    version: str,
    plural: str,
    namespace: str,
    status_element: str,
    logger: kopf.Logger,
    use_async=True,
    **_: Any,
):
    logger.info(f"_patch_and_delete_stubborn_custom_resources for {plural}.{group} in namespace {namespace}")
    co = CustomObjectsApi()
    resp = co.list_namespaced_custom_object(group=group, version=version, plural=plural, namespace=namespace)
    failed_res = [
        item.get("metadata").get("name")
        for item in resp["items"]
        if item.get("status", {}).get(status_element) in ["Failed", "Completed", "InProgress"]
    ]
    for item in failed_res:
        try:
            logger.info(f"Patching item {item} in {plural}.{group}")
            patch = json.loads("""{"metadata":{"finalizers":[]}}""")
            co.patch_namespaced_custom_object(
                group=group, version=version, plural=plural, namespace=namespace, name=item, body=patch
            )
            logger.info(f"Deleting item {item} in {plural}.{group}")
            co.delete_namespaced_custom_object(
                group=group,
                version=version,
                plural=plural,
                namespace=namespace,
                name=item,
            )
        except ApiException as e:
            logger.warn("Trying to patch and delete failed: %s\n" % e)
Beispiel #4
0
def test_cluster_node_count(admin_mc, remove_resource,
                            raw_remove_custom_resource):
    """Test that the cluster node count gets updated as nodes are added"""
    client = admin_mc.client
    cluster = client.create_cluster(
        name=random_str(), rancherKubernetesEngineConfig={"accessKey": "junk"})
    remove_resource(cluster)

    def _check_node_count(cluster, nodes):
        c = client.reload(cluster)
        return c.nodeCount == nodes

    def _node_count_fail(cluster, nodes):
        c = client.reload(cluster)
        s = "cluster {} failed to have proper node count, expected: {} has: {}"
        return s.format(c.id, nodes, c.nodeCount)

    node_count = 0
    wait_for(lambda: _check_node_count(cluster, node_count),
             fail_handler=lambda: _node_count_fail(cluster, node_count))

    # Nodes have to be created manually through k8s client to attach to a
    # pending cluster
    k8s_dynamic_client = CustomObjectsApi(admin_mc.k8s_client)
    body = {
        "metadata": {
            "name": random_str(),
            "namespace": cluster.id,
        },
        "kind": "Node",
        "apiVersion": "management.cattle.io/v3",
    }

    dynamic_nt = k8s_dynamic_client.create_namespaced_custom_object(
        "management.cattle.io", "v3", cluster.id, 'nodes', body)
    raw_remove_custom_resource(dynamic_nt)

    node_count = 1
    wait_for(lambda: _check_node_count(cluster, node_count),
             fail_handler=lambda: _node_count_fail(cluster, node_count))

    # Create node number 2
    body['metadata']['name'] = random_str()

    dynamic_nt1 = k8s_dynamic_client.create_namespaced_custom_object(
        "management.cattle.io", "v3", cluster.id, 'nodes', body)
    raw_remove_custom_resource(dynamic_nt1)

    node_count = 2
    wait_for(lambda: _check_node_count(cluster, node_count),
             fail_handler=lambda: _node_count_fail(cluster, node_count))

    # Delete a node
    k8s_dynamic_client.delete_namespaced_custom_object(
        "management.cattle.io", "v3", cluster.id, 'nodes',
        dynamic_nt1['metadata']['name'], {})

    node_count = 1
    wait_for(lambda: _check_node_count(cluster, node_count),
             fail_handler=lambda: _node_count_fail(cluster, node_count))
Beispiel #5
0
def delete_v_s_route(custom_objects: CustomObjectsApi, name,
                     namespace) -> None:
    """
    Delete a VirtualServerRoute.

    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete a VirtualServerRoute: {name}")
    delete_options = client.V1DeleteOptions()
    custom_objects.delete_namespaced_custom_object("k8s.nginx.org", "v1",
                                                   namespace,
                                                   "virtualserverroutes", name,
                                                   delete_options)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "k8s.nginx.org",
        "v1",
        namespace,
        "virtualserverroutes",
        name,
    )
    print(f"VirtualServerRoute was removed with the name '{name}'")
Beispiel #6
0
def delete_resource(custom_objects: CustomObjectsApi, resource, namespace,
                    plural) -> None:
    """
    Delete a Resource.

    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param resource: a dictionary representation of the resource yaml
    :param namespace:
    :param plural: the plural of the resource
    :return:
    """

    name = resource['metadata']['name']
    kind = resource['kind']
    group, version = resource["apiVersion"].split("/")

    print(f"Delete a '{kind}' with name '{name}'")

    custom_objects.delete_namespaced_custom_object(group, version, namespace,
                                                   plural, name)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        group,
        version,
        namespace,
        plural,
        name,
    )
    print(f"Resource '{kind}' was removed with name '{name}'")
Beispiel #7
0
def destroy_custom_object(api: client.CustomObjectsApi, group, plural, version,
                          namespace, name):
    if len(
            api.list_namespaced_custom_object(
                namespace=namespace,
                field_selector=f'metadata.name={name}',
                group=group,
                plural=plural,
                version=version)['items']) == 1:
        logger.info(f'destroying custom object: {namespace}/{name}')
        api.delete_namespaced_custom_object(namespace=namespace,
                                            group=group,
                                            plural=plural,
                                            version=version,
                                            name=name,
                                            body=V1DeleteOptions())
    else:
        logger.info(
            f'cannot find custom object to destroy: {namespace}/{name}')
def delete_ap_logconf(custom_objects: CustomObjectsApi, name,
                      namespace) -> None:
    """
    Delete a AppProtect logconf.
    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete AP logconf: {name}")
    delete_options = client.V1DeleteOptions()
    custom_objects.delete_namespaced_custom_object("appprotect.f5.com",
                                                   "v1beta1", namespace,
                                                   "aplogconfs", name,
                                                   delete_options)
    ensure_item_removal(custom_objects.get_namespaced_custom_object,
                        "appprotect.f5.com", "v1beta1", namespace,
                        "aplogconfs", name)
    print(f"AP logconf was removed with name: {name}")
Beispiel #9
0
def delete_ap_usersig(custom_objects: CustomObjectsApi, name, namespace) -> None:
    """
    Delete a AppProtect usersig.
    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete AP UserSig: {name}")
    custom_objects.delete_namespaced_custom_object(
        "appprotect.f5.com", "v1beta1", namespace, "apusersigs", name
    )
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "appprotect.f5.com",
        "v1beta1",
        namespace,
        "apusersigs",
        name,
    )
    print(f"AP UserSig was removed with name: {name}")
def delete_policy(custom_objects: CustomObjectsApi, name, namespace) -> None:
    """
    Delete a Policy.

    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete a Policy: {name}")

    custom_objects.delete_namespaced_custom_object("k8s.nginx.org", "v1",
                                                   namespace, "policies", name)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "k8s.nginx.org",
        "v1",
        namespace,
        "policies",
        name,
    )
    print(f"Policy was removed with name '{name}'")
Beispiel #11
0
def delete_dos_logconf(custom_objects: CustomObjectsApi, name,
                       namespace) -> None:
    """
    Delete a Dos logconf.
    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete DOS logconf: {name}")
    custom_objects.delete_namespaced_custom_object("appprotectdos.f5.com",
                                                   "v1beta1", namespace,
                                                   "apdoslogconfs", name)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "appprotectdos.f5.com",
        "v1beta1",
        namespace,
        "apdoslogconfs",
        name,
    )
    print(f"DOS logconf was removed with name: {name}")
Beispiel #12
0
def delete_dos_policy(custom_objects: CustomObjectsApi, name,
                      namespace) -> None:
    """
    Delete a Dos policy.
    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete a DOS policy: {name}")
    custom_objects.delete_namespaced_custom_object("appprotectdos.f5.com",
                                                   "v1beta1", namespace,
                                                   "apdospolicies", name)
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "appprotectdos.f5.com",
        "v1beta1",
        namespace,
        "apdospolicies",
        name,
    )
    time.sleep(3)
    print(f"DOS policy was removed with name: {name}")
Beispiel #13
0
def delete_virtual_server(custom_objects: CustomObjectsApi, name, namespace) -> None:
    """
    Delete a VirtualServer.

    :param custom_objects: CustomObjectsApi
    :param namespace: namespace
    :param name:
    :return:
    """
    print(f"Delete a VirtualServer: {name}")

    custom_objects.delete_namespaced_custom_object(
        "k8s.nginx.org", "v1", namespace, "virtualservers", name
    )
    ensure_item_removal(
        custom_objects.get_namespaced_custom_object,
        "k8s.nginx.org",
        "v1",
        namespace,
        "virtualservers",
        name,
    )
    print(f"VirtualServer was removed with name '{name}'")
class NMStateConfig(BaseCustomResource):
    """Configure nmstate (static IP) related settings for agents."""

    _plural = "nmstateconfigs"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created nmstate config %s: %s", self.ref,
                    pformat(yaml_data))

    def create(
        self,
        config: dict,
        interfaces: list,
        label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion": f"{CRD_API_GROUP}/{CRD_API_VERSION}",
            "kind": "NMStateConfig",
            "metadata": {
                "labels": {
                    f"{CRD_API_GROUP}/selector-nmstate-config-name": label,
                },
                **self.ref.as_dict(),
            },
            "spec": {
                "config": config,
                "interfaces": interfaces,
            },
        }
        body["spec"].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created nmstate config %s: %s", self.ref, pformat(body))

    def patch(
        self,
        config: dict,
        interfaces: list,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}

        spec = body["spec"]
        if config:
            spec["config"] = config

        if interfaces:
            spec["interfaces"] = interfaces

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching nmstate config %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted nmstate config %s", self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of NMStateConfig.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"nmstate config {self.ref} status",
            expected_exceptions=KeyError,
        )
Beispiel #15
0
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
                                  ):
    """Kubernetes Traefik Middleware Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    def _crd_exists(self) -> bool:
        """Check if the traefik middleware exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def reconcile(self, current: TraefikMiddleware,
                  reference: TraefikMiddleware):
        super().reconcile(current, reference)
        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
            raise NeedsUpdate()

    def get_reference_object(self) -> TraefikMiddleware:
        """Get deployment object for outpost"""
        if not ProxyProvider.objects.filter(
                outpost__in=[self.controller.outpost],
                forward_auth_mode=True,
        ).exists():
            self.logger.debug("No providers with forward auth enabled.")
            raise Disabled()
        if not self._crd_exists():
            self.logger.debug("CRD doesn't exist")
            raise Disabled()
        return TraefikMiddleware(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="Middleware",
            metadata=TraefikMiddlewareMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=
            TraefikMiddlewareSpec(forwardAuth=TraefikMiddlewareSpecForwardAuth(
                address=
                f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik",
                authResponseHeaders=[
                    "Set-Cookie",
                    "X-Auth-Username",
                    "X-Forwarded-Email",
                    "X-Forwarded-Preferred-Username",
                    "X-Forwarded-User",
                ],
                trustForwardHeader=True,
            )),
        )

    def create(self, reference: TraefikMiddleware):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: TraefikMiddleware):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> TraefikMiddleware:
        return from_dict(
            TraefikMiddleware,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
Beispiel #16
0
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """

    _plural = "clusterdeployments"
    _platform_field = {"platform": {"agentBareMetal": {"agentSelector": {}}}}

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables["namespace"],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created cluster deployment %s: %s", self.ref, pformat(yaml_data))

    def create(
            self,
            secret: Secret,
            base_domain: str = env_variables["base_domain"],
            agent_cluster_install_ref: Optional[ObjectReference] = None,
            **kwargs,
    ):
        body = {
            "apiVersion": f"{HIVE_API_GROUP}/{HIVE_API_VERSION}",
            "kind": "ClusterDeployment",
            "metadata": self.ref.as_dict(),
            "spec": {
                "clusterName": self.ref.name,
                "baseDomain": base_domain,
                "pullSecretRef": secret.ref.as_dict(),
            }
        }
        body["spec"].update(self._platform_field)

        if agent_cluster_install_ref:
            body["spec"]["clusterInstallRef"] = agent_cluster_install_ref.as_dict()

        body["spec"].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created cluster deployment %s: %s", self.ref, pformat(body))

    def patch(
        self,
        secret: Optional[Secret] = None,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}
        body["spec"]["platform"] = {"agentBareMetal": {}}

        spec = body["spec"]
        body["spec"].update(self._platform_field)

        if secret:
            spec["pullSecretRef"] = secret.ref.as_dict()

        if "agent_cluster_install_ref" in kwargs:
            spec["clusterInstallRef"] = kwargs["agent_cluster_install_ref"].as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching cluster deployment %s: %s", self.ref, pformat(body))

    def annotate_install_config(self, install_config: str) -> None:
        body = {"metadata": {"annotations": {f"{CRD_API_GROUP}/install-config-overrides": install_config}}}

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching cluster install config %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted cluster deployment %s", self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """

        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} status",
            expected_exceptions=KeyError,
        )

    def condition(
            self,
            cond_type,
            timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[Optional[str], Optional[str]]:
        for condition in self.status(timeout).get("conditions", []):
            if cond_type == condition.get("type"):
                return condition.get("status"), condition.get("reason")
        return None, None

    def wait_for_condition(
        self,
        cond_type: str,
        required_status: str,
        required_reason: Optional[str] = None,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        def _has_required_condition() -> Optional[bool]:
            status, reason = self.condition(cond_type=cond_type, timeout=0.5)
            if status == required_status:
                if required_reason:
                    return required_reason == reason
                return True
            return False

        logger.info(
            "Waiting till cluster will be in condition %s with status: %s "
            "reason: %s", cond_type, required_status, required_reason
        )

        waiting.wait(
            _has_required_condition,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} condition {cond_type} to be in {required_status}",
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def list_agents(self) -> List[Agent]:
        return Agent.list(self.crd_api, self)

    def wait_for_agents(
        self,
        num_agents: int = 1,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_AGENTS_TIMEOUT,
    ) -> List[Agent]:
        def _wait_for_sufficient_agents_number() -> List[Agent]:
            agents = self.list_agents()
            return agents if len(agents) == num_agents else []

        return waiting.wait(
            _wait_for_sufficient_agents_number,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} to have {num_agents} agents",
        )
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """

    _plural = "agents"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment: "ClusterDeployment",
    ) -> List["Agent"]:
        resources = crd_api.list_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=cls._plural,
            namespace=cluster_deployment.ref.namespace,
        )
        assigned_agents = []
        for item in resources.get("items", []):
            assigned_cluster_ref = ObjectReference(
                name=item["spec"]["clusterDeploymentName"]["name"],
                namespace=item["spec"]["clusterDeploymentName"]["namespace"],
            )
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(
                        kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item["metadata"]["name"],
                        namespace=item["metadata"]["namespace"],
                    ))

        return assigned_agents

    def create(self):
        raise RuntimeError(
            "agent resource must be created by the assisted-installer operator"
        )

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def patch(self, **kwargs) -> None:
        body = {"spec": kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching agent %s: %s", self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted agent %s", self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"agent {self.ref} status",
            expected_exceptions=KeyError,
        )

    def approve(self) -> None:
        self.patch(approved=True)
        logger.info("approved agent %s", self.ref)
Beispiel #18
0
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """
    _plural = 'clusterdeployments'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(yaml_data))

    def create(
        self,
        platform: Platform,
        install_strategy: InstallStrategy,
        secret: Secret,
        base_domain: str = env_variables['base_domain'],
        **kwargs,
    ):
        body = {
            'apiVersion': f'{HIVE_API_GROUP}/{HIVE_API_VERSION}',
            'kind': 'ClusterDeployment',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterName': self.ref.name,
                'baseDomain': base_domain,
                'platform': platform.as_dict(),
                'provisioning': {
                    'installStrategy': install_strategy.as_dict()
                },
                'pullSecretRef': secret.ref.as_dict(),
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(body))

    def patch(
        self,
        platform: Optional[Platform] = None,
        install_strategy: Optional[InstallStrategy] = None,
        secret: Optional[Secret] = None,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if platform:
            spec['platform'] = platform.as_dict()

        if install_strategy:
            spec['provisioning'] = {
                'installStrategy': install_strategy.as_dict()
            }

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching cluster deployment %s: %s', self.ref,
                    pformat(body))

    def annotate_install_config(self, install_config: str) -> None:
        body = {
            'metadata': {
                'annotations': {
                    f'{CRD_API_GROUP}/install-config-overrides': install_config
                }
            }
        }

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching cluster install config %s: %s', self.ref,
                    pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted cluster deployment %s', self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} status',
            expected_exceptions=KeyError,
        )

    def state(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[str, str]:
        state, state_info = None, None
        for condition in self.status(timeout).get('conditions', []):
            reason = condition.get('reason')

            if reason == 'AgentPlatformState':
                state = condition.get('message')
            elif reason == 'AgentPlatformStateInfo':
                state_info = condition.get('message')

            if state and state_info:
                break

        return state, state_info

    def wait_for_state(
        self,
        required_state: str,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
        *,
        raise_on_states: Iterable[str] = FAILURE_STATES,
    ) -> None:
        required_state = required_state.lower()
        raise_on_states = [x.lower() for x in raise_on_states]

        def _has_required_state() -> Optional[bool]:
            state, state_info = self.state(timeout=0.5)
            state = state.lower() if state else state
            if state == required_state:
                return True
            elif state in raise_on_states:
                raise UnexpectedStateError(
                    f'while waiting for state `{required_state}`, cluster '
                    f'{self.ref} state changed unexpectedly to `{state}`: '
                    f'{state_info}')

        logger.info("Waiting till cluster will be in %s state", required_state)
        waiting.wait(
            _has_required_state,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state to be {required_state}',
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def list_agents(self) -> List[Agent]:
        return Agent.list(self.crd_api, self)

    def wait_for_agents(
        self,
        num_agents: int = 1,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_AGENTS_TIMEOUT,
    ) -> List[Agent]:
        def _wait_for_sufficient_agents_number() -> List[Agent]:
            agents = self.list_agents()
            return agents if len(agents) == num_agents else []

        return waiting.wait(
            _wait_for_sufficient_agents_number,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} to have {num_agents} agents',
        )

    def wait_to_be_installed(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_INSTALLATION_COMPLETE_TIMEOUT,
    ) -> None:

        waiting.wait(
            lambda: self.get()['spec'].get('installed') is True,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state installed',
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def download_kubeconfig(self, kubeconfig_path):
        def _get_kubeconfig_secret() -> dict:
            return self.get(
            )['spec']['clusterMetadata']['adminKubeconfigSecretRef']

        secret_ref = waiting.wait(
            _get_kubeconfig_secret,
            sleep_seconds=1,
            timeout_seconds=240,
            expected_exceptions=KeyError,
            waiting_for=f'kubeconfig secret creation for cluster {self.ref}',
        )

        kubeconfig_data = Secret(
            kube_api_client=self.crd_api.api_client,
            **secret_ref,
        ).get().data['kubeconfig']

        with open(kubeconfig_path, 'wt') as kubeconfig_file:
            kubeconfig_file.write(b64decode(kubeconfig_data).decode())
Beispiel #19
0
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
                                  ):
    """Kubernetes Traefik Middleware Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    @property
    def noop(self) -> bool:
        if not ProxyProvider.objects.filter(
                outpost__in=[self.controller.outpost],
                mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
        ).exists():
            self.logger.debug("No providers with forward auth enabled.")
            return True
        if not self._crd_exists():
            self.logger.debug("CRD doesn't exist")
            return True
        return False

    def _crd_exists(self) -> bool:
        """Check if the traefik middleware exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def reconcile(self, current: TraefikMiddleware,
                  reference: TraefikMiddleware):
        super().reconcile(current, reference)
        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
            raise NeedsUpdate()
        if (current.spec.forwardAuth.authResponseHeadersRegex !=
                reference.spec.forwardAuth.authResponseHeadersRegex):
            raise NeedsUpdate()
        # Ensure all of our headers are set, others can be added by the user.
        if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
                reference.spec.forwardAuth.authResponseHeaders):
            raise NeedsUpdate()

    def get_reference_object(self) -> TraefikMiddleware:
        """Get deployment object for outpost"""
        return TraefikMiddleware(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="Middleware",
            metadata=TraefikMiddlewareMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=
            TraefikMiddlewareSpec(forwardAuth=TraefikMiddlewareSpecForwardAuth(
                address=
                f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
                authResponseHeaders=[
                    "X-authentik-username",
                    "X-authentik-groups",
                    "X-authentik-email",
                    "X-authentik-name",
                    "X-authentik-uid",
                    "X-authentik-jwt",
                    "X-authentik-meta-jwks",
                    "X-authentik-meta-outpost",
                    "X-authentik-meta-provider",
                    "X-authentik-meta-app",
                    "X-authentik-meta-version",
                ],
                authResponseHeadersRegex="",
                trustForwardHeader=True,
            )),
        )

    def create(self, reference: TraefikMiddleware):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: TraefikMiddleware):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> TraefikMiddleware:
        return from_dict(
            TraefikMiddleware,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
class PrometheusServiceMonitorReconciler(
        KubernetesObjectReconciler[PrometheusServiceMonitor]):
    """Kubernetes Prometheus ServiceMonitor Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    @property
    def noop(self) -> bool:
        return (not self._crd_exists()) or (self.is_embedded)

    def _crd_exists(self) -> bool:
        """Check if the Prometheus ServiceMonitor exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def get_reference_object(self) -> PrometheusServiceMonitor:
        """Get service monitor object for outpost"""
        return PrometheusServiceMonitor(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="ServiceMonitor",
            metadata=PrometheusServiceMonitorMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=PrometheusServiceMonitorSpec(
                endpoints=[
                    PrometheusServiceMonitorSpecEndpoint(port="http-metrics", )
                ],
                selector=PrometheusServiceMonitorSpecSelector(
                    matchLabels=self.get_object_meta(name=self.name).labels, ),
            ),
        )

    def create(self, reference: PrometheusServiceMonitor):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: PrometheusServiceMonitor):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> PrometheusServiceMonitor:
        return from_dict(
            PrometheusServiceMonitor,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: PrometheusServiceMonitor,
               reference: PrometheusServiceMonitor):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
Beispiel #21
0
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """

    _plural = "agents"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment,
        agents_namespace=None,
    ) -> List["Agent"]:
        agents_namespace = agents_namespace or cluster_deployment.ref.namespace
        resources = crd_api.list_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=cls._plural,
            namespace=agents_namespace,
        )
        assigned_agents = []
        for item in resources.get("items", []):
            if item["spec"].get("clusterDeploymentName") is None:
                # Unbound late-binding agent, not part of the given cluster_deployment
                continue

            assigned_cluster_ref = ObjectReference(
                name=item["spec"]["clusterDeploymentName"]["name"],
                namespace=item["spec"]["clusterDeploymentName"]["namespace"],
            )
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(
                        kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item["metadata"]["name"],
                        namespace=item["metadata"]["namespace"],
                    )
                )

        return assigned_agents

    def create(self):
        raise RuntimeError("agent resource must be created by the assisted-installer operator")

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def patch(self, **kwargs) -> None:
        body = {"spec": kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        log.info("patching agent %s: %s", self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        log.info("deleted agent %s", self.ref)

    def status(self, timeout: Union[int, float] = consts.DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"agent {self.ref} status",
            expected_exceptions=KeyError,
        )

    @property
    def role(self) -> Optional[str]:
        return self.get()["spec"].get("role")

    def set_role(self, role: str) -> None:
        self.patch(role=role)
        log.info(f"set agent {self.ref} role to {role}")

    def approve(self) -> None:
        self.patch(approved=True)
        log.info("approved agent %s", self.ref)

    def bind(self, cluster_deployment) -> None:
        """
        Bind an unbound agent to a cluster deployment
        """
        self.patch(
            clusterDeploymentName={
                "name": cluster_deployment.ref.name,
                "namespace": cluster_deployment.ref.namespace,
            }
        )
        log.info(f"Bound agent {self.ref} to cluster_deployment {cluster_deployment.ref}")

    @classmethod
    def wait_for_agents_to_be_bound(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_READY_FOR_INSTALL_TIMEOUT
    ) -> None:
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.BOUND,
            timeout=timeout,
        )

    @classmethod
    def wait_for_agents_to_be_ready_for_install(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_READY_FOR_INSTALL_TIMEOUT
    ) -> None:
        for status_type in (
            consts.AgentStatus.SPEC_SYNCED,
            consts.AgentStatus.CONNECTED,
            consts.AgentStatus.REQUIREMENTS_MET,
            consts.AgentStatus.VALIDATED,
        ):
            cls.wait_till_all_agents_are_in_status(
                agents=agents,
                status_type=status_type,
                timeout=timeout,
            )

    @classmethod
    def wait_for_agents_to_unbound(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT
    ) -> None:
        cls.wait_for_agents_to_be_ready_for_install(agents=agents, timeout=timeout)
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.BOUND,
            status="False",
            timeout=timeout,
        )

    @classmethod
    def wait_for_agents_to_install(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_INSTALLATION_TIMEOUT
    ) -> None:
        cls.wait_for_agents_to_be_ready_for_install(agents=agents, timeout=timeout)
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.INSTALLED,
            timeout=timeout,
        )

    @staticmethod
    def are_agents_in_status(
        agents: List["Agent"],
        status_type: str,
        status: str,
    ) -> bool:
        agents_conditions = {
            agent.ref.name: {condition["type"]: condition["status"] for condition in agent.status()["conditions"]}
            for agent in agents
        }

        log.info(
            f"Waiting for agents to have the condition '{status_type}' ="
            f" '{status}' and currently agent conditions are {agents_conditions}"
        )

        return all(agent_conditions.get(status_type, None) == status for agent_conditions in agents_conditions.values())

    @staticmethod
    def wait_till_all_agents_are_in_status(
        agents: List["Agent"],
        status_type: str,
        timeout,
        status="True",
        interval=10,
    ) -> None:
        log.info(f"Now Wait till agents have status as {status_type}")

        waiting.wait(
            lambda: Agent.are_agents_in_status(
                agents,
                status_type,
                status=status,
            ),
            timeout_seconds=timeout,
            sleep_seconds=interval,
            waiting_for=f"Agents to have {status_type} status",
        )
Beispiel #22
0
class NMStateConfig(BaseCustomResource):
    """Configure nmstate (static IP) related settings for agents."""
    _plural = 'nmstateconfigs'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created nmstate config %s: %s', self.ref,
                    pformat(yaml_data))

    def create(
        self,
        config: dict,
        interfaces: list,
        label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            'apiVersion': f'{CRD_API_GROUP}/{CRD_API_VERSION}',
            'kind': 'NMStateConfig',
            'metadata': {
                'labels': {
                    f'{CRD_API_GROUP}/selector-nmstate-config-name': label,
                },
                **self.ref.as_dict()
            },
            'spec': {
                'config': config,
                'interfaces': interfaces,
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created nmstate config %s: %s', self.ref, pformat(body))

    def patch(
        self,
        config: dict,
        interfaces: list,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if config:
            spec['config'] = config

        if interfaces:
            spec['interfaces'] = interfaces

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching nmstate config %s: %s', self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted nmstate config %s', self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of NMStateConfig.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(_attempt_to_get_status,
                            sleep_seconds=0.5,
                            timeout_seconds=timeout,
                            waiting_for=f'nmstate config {self.ref} status',
                            expected_exceptions=KeyError)
Beispiel #23
0
class InfraEnv(BaseCustomResource):
    """
    InfraEnv is used to generate cluster iso.
    Image is automatically generated on CRD deployment, after InfraEnv is
    reconciled. Image download url will be exposed in the status.
    """
    _plural = 'infraenvs'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created infraEnv %s: %s', self.ref, pformat(yaml_data))

    def create(
        self,
        cluster_deployment: ClusterDeployment,
        secret: Secret,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            'apiVersion': f'{CRD_API_GROUP}/{CRD_API_VERSION}',
            'kind': 'InfraEnv',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterRef': cluster_deployment.ref.as_dict(),
                'pullSecretRef': secret.ref.as_dict(),
                'nmStateConfigLabelSelector': {
                    'matchLabels': {
                        f'{CRD_API_GROUP}/selector-nmstate-config-name':
                        nmstate_label or ''
                    }
                },
                'agentLabelSelector': {
                    'matchLabels': label_selector or {}
                },
                'ignitionConfigOverride': ignition_config_override or '',
            }
        }
        spec = body['spec']
        if proxy:
            spec['proxy'] = proxy.as_dict()
        spec.update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created infraEnv %s: %s', self.ref, pformat(body))

    def patch(
        self,
        cluster_deployment: Optional[ClusterDeployment],
        secret: Optional[Secret],
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if cluster_deployment:
            spec['clusterRef'] = cluster_deployment.ref.as_dict()

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        if proxy:
            spec['proxy'] = proxy.as_dict()

        if label_selector:
            spec['agentLabelSelector'] = {'matchLabels': label_selector}

        if nmstate_label:
            spec['nmStateConfigLabelSelector'] = {
                'matchLabels': {
                    f'{CRD_API_GROUP}/selector-nmstate-config-name':
                    nmstate_label,
                }
            }

        if ignition_config_override:
            spec['ignitionConfigOverride'] = ignition_config_override

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching infraEnv %s: %s', self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted infraEnv %s', self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of InfraEnv.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'infraEnv {self.ref} status',
            expected_exceptions=KeyError,
        )

    def get_iso_download_url(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_ISO_URL_TIMEOUT,
    ):
        def _attempt_to_get_image_url() -> str:
            return self.get()['status']['isoDownloadURL']

        return waiting.wait(
            _attempt_to_get_image_url,
            sleep_seconds=3,
            timeout_seconds=timeout,
            waiting_for='image to be created',
            expected_exceptions=KeyError,
        )

    def get_cluster_id(self):
        iso_download_url = self.get_iso_download_url()
        return ISO_URL_PATTERN.match(iso_download_url).group('cluster_id')
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """

    _hive_api_group = 'hive.openshift.io'
    _plural = 'clusterdeployments'

    def __init__(self,
                 kube_api_client: ApiClient,
                 name: str,
                 namespace: str = env_variables['namespace']):
        BaseCustomResource.__init__(self, name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)
        self._assigned_secret = None

    @property
    def secret(self) -> Secret:
        return self._assigned_secret

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace)
        secret_ref = yaml_data['spec']['pullSecretRef']
        self._assigned_secret = Secret(
            kube_api_client=self.crd_api.api_client,
            name=secret_ref['name'],
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(yaml_data))

    def create(self,
               platform: Platform,
               install_strategy: InstallStrategy,
               secret: Secret,
               base_domain: str = env_variables['base_domain'],
               **kwargs) -> None:
        body = {
            'apiVersion': f'{self._hive_api_group}/v1',
            'kind': 'ClusterDeployment',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterName': self.ref.name,
                'baseDomain': base_domain,
                'platform': platform.as_dict(),
                'provisioning': {
                    'installStrategy': install_strategy.as_dict()
                },
                'pullSecretRef': secret.ref.as_dict(),
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace)
        self._assigned_secret = secret

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(body))

    def patch(self,
              platform: Optional[Platform] = None,
              install_strategy: Optional[InstallStrategy] = None,
              secret: Optional[Secret] = None,
              **kwargs) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if platform:
            spec['platform'] = platform.as_dict()

        if install_strategy:
            spec['provisioning'] = {
                'installStrategy': install_strategy.as_dict()
            }

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body)

        logger.info('patching cluster deployment %s: %s', self.ref,
                    pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

        logger.info('deleted cluster deployment %s', self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(_attempt_to_get_status,
                            sleep_seconds=0.5,
                            timeout_seconds=timeout,
                            waiting_for=f'cluster {self.ref} status',
                            expected_exceptions=KeyError)

    def state(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT
    ) -> Tuple[str, str]:
        state, state_info = None, None
        for condition in self.status(timeout).get('conditions', []):
            reason = condition.get('reason')

            if reason == 'AgentPlatformState':
                state = condition.get('message')
            elif reason == 'AgentPlatformStateInfo':
                state_info = condition.get('message')

            if state and state_info:
                break

        return state, state_info

    def wait_for_state(
        self,
        required_state: str,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT) -> None:
        required_state = required_state.lower()

        def _has_required_state() -> bool:
            state, _ = self.state(timeout=0.5)
            return state.lower() == required_state

        waiting.wait(
            _has_required_state,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state to be {required_state}',
            expected_exceptions=waiting.exceptions.TimeoutExpired)
class AgentClusterInstall(BaseCustomResource):
    """ This CRD represents a request to provision an agent based cluster.
        In the AgentClusterInstall, the user can specify requirements like
        networking, number of control plane and workers nodes and more.
        The installation will start automatically if the required number of
        hosts is available, the hosts are ready to be installed and the Agents
        are approved.
        The AgentClusterInstall reflects the ClusterDeployment/Installation
        status through Conditions."""

    _api_group = "extensions.hive.openshift.io"
    _api_version = "v1beta1"
    _plural = "agentclusterinstalls"
    _kind = "AgentClusterInstall"
    _requirements_met_condition_name = "RequirementsMet"
    _completed_condition_name = "Completed"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)
        self.ref.kind = self._kind
        self.ref.group = self._api_group
        self.ref.version = self._api_version

    def create(
        self,
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion":
            f"{self._api_group}/{self._api_version}",
            "kind":
            self._kind,
            "metadata":
            self.ref.as_dict(),
            "spec":
            self._get_spec_dict(
                cluster_deployment_ref=cluster_deployment_ref,
                cluster_cidr=cluster_cidr,
                host_prefix=host_prefix,
                service_network=service_network,
                control_plane_agents=control_plane_agents,
                **kwargs,
            ),
        }

        self.crd_api.create_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created agentclusterinstall %s: %s", self.ref,
                    pformat(body))

    def patch(
        self,
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> None:
        body = {
            "spec":
            self._get_spec_dict(
                cluster_deployment_ref=cluster_deployment_ref,
                cluster_cidr=cluster_cidr,
                host_prefix=host_prefix,
                service_network=service_network,
                control_plane_agents=control_plane_agents,
                **kwargs,
            )
        }

        self.crd_api.patch_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching agentclusterinstall %s: %s", self.ref,
                    pformat(body))

    @staticmethod
    def _get_spec_dict(
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> dict:
        spec = {
            "clusterDeploymentRef":
            cluster_deployment_ref.as_dict(),
            "imageSetRef":
            kwargs.pop("image_set_ref", ClusterImageSetReference()).as_dict(),
            "networking": {
                "clusterNetwork": [{
                    "cidr": cluster_cidr,
                    "hostPrefix": host_prefix,
                }],
                "serviceNetwork": [service_network],
            },
            "provisionRequirements": {
                "controlPlaneAgents": control_plane_agents,
                "workerAgents": kwargs.pop("worker_agents", 0),
            }
        }

        if "api_vip" in kwargs:
            spec["apiVIP"] = kwargs.pop("api_vip")

        if "ingress_vip" in kwargs:
            spec["ingressVIP"] = kwargs.pop("ingress_vip")

        if "ssh_pub_key" in kwargs:
            spec["sshPublicKey"] = kwargs.pop("ssh_pub_key")

        if "machine_cidr" in kwargs:
            spec["networking"]["machineNetwork"] = [{
                "cidr":
                kwargs.pop("machine_cidr")
            }]

        spec.update(kwargs)
        return spec

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted agentclusterinstall %s", self.ref)

    def status(
            self,
            timeout: Union[int] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} status",
            expected_exceptions=KeyError,
        )

    def wait_to_be_ready(
        self,
        ready: bool,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._requirements_met_condition_name,
            required_status=str(ready),
            timeout=timeout,
        )

    def wait_to_be_installing(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._requirements_met_condition_name,
            required_status="True",
            required_reason="ClusterAlreadyInstalling",
            timeout=timeout,
        )

    def wait_to_be_installed(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_INSTALLATION_COMPLETE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._completed_condition_name,
            required_status="True",
            required_reason="InstallationCompleted",
            timeout=timeout,
        )

    def wait_for_condition(
        self,
        cond_type: str,
        required_status: str,
        required_reason: Optional[str] = None,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:

        logger.info(
            "waiting for agentclusterinstall %s condition %s to be in status "
            "%s", self.ref, cond_type, required_status)

        def _has_required_condition() -> Optional[bool]:
            status, reason = self.condition(cond_type=cond_type, timeout=0.5)
            logger.info(
                f"waiting for condition <{cond_type}> to be in status <{required_status}>. actual status is: {status} {reason}"
            )
            if status == required_status:
                if required_reason:
                    return required_reason == reason
                return True

        waiting.wait(
            _has_required_condition,
            timeout_seconds=timeout,
            waiting_for=f"agentclusterinstall {self.ref} condition "
            f"{cond_type} to be {required_status}",
            sleep_seconds=10,
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def condition(
        self,
        cond_type,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[Optional[str], Optional[str]]:
        for condition in self.status(timeout).get("conditions", []):
            if cond_type == condition.get("type"):
                return condition.get("status"), condition.get("reason")

        return None, None

    def download_kubeconfig(self, kubeconfig_path):
        def _get_kubeconfig_secret() -> dict:
            return self.get(
            )["spec"]["clusterMetadata"]["adminKubeconfigSecretRef"]

        secret_ref = waiting.wait(
            _get_kubeconfig_secret,
            sleep_seconds=1,
            timeout_seconds=DEFAULT_WAIT_FOR_KUBECONFIG_TIMEOUT,
            expected_exceptions=KeyError,
            waiting_for=f"kubeconfig secret creation for cluster {self.ref}",
        )

        kubeconfig_data = (Secret(
            kube_api_client=self.crd_api.api_client,
            namespace=self._reference.namespace,
            **secret_ref,
        ).get().data["kubeconfig"])

        with open(kubeconfig_path, "wt") as kubeconfig_file:
            kubeconfig_file.write(b64decode(kubeconfig_data).decode())
class InfraEnv(BaseCustomResource):
    """
    InfraEnv is used to generate cluster iso.
    Image is automatically generated on CRD deployment, after InfraEnv is
    reconciled. Image download url will be exposed in the status.
    """

    _plural = "infraenvs"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables["namespace"],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created infraEnv %s: %s", self.ref, pformat(yaml_data))

    def create(
        self,
        cluster_deployment: ClusterDeployment,
        secret: Secret,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        ssh_pub_key: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion": f"{CRD_API_GROUP}/{CRD_API_VERSION}",
            "kind": "InfraEnv",
            "metadata": self.ref.as_dict(),
            "spec": {
                "clusterRef": cluster_deployment.ref.as_dict(),
                "pullSecretRef": secret.ref.as_dict(),
                "nmStateConfigLabelSelector": {
                    "matchLabels": {
                        f"{CRD_API_GROUP}/selector-nmstate-config-name":
                        nmstate_label or ""
                    }
                },
                "agentLabelSelector": {
                    "matchLabels": label_selector or {}
                },
                "ignitionConfigOverride": ignition_config_override or "",
            },
        }
        spec = body["spec"]
        if proxy:
            spec["proxy"] = proxy.as_dict()
        if ssh_pub_key:
            spec["sshAuthorizedKey"] = ssh_pub_key

        spec.update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created infraEnv %s: %s", self.ref, pformat(body))

    def patch(
        self,
        cluster_deployment: Optional[ClusterDeployment],
        secret: Optional[Secret],
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        ssh_pub_key: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}

        spec = body["spec"]
        if cluster_deployment:
            spec["clusterRef"] = cluster_deployment.ref.as_dict()

        if secret:
            spec["pullSecretRef"] = secret.ref.as_dict()

        if proxy:
            spec["proxy"] = proxy.as_dict()

        if label_selector:
            spec["agentLabelSelector"] = {"matchLabels": label_selector}

        if nmstate_label:
            spec["nmStateConfigLabelSelector"] = {
                "matchLabels": {
                    f"{CRD_API_GROUP}/selector-nmstate-config-name":
                    nmstate_label,
                }
            }

        if ignition_config_override:
            spec["ignitionConfigOverride"] = ignition_config_override

        if ssh_pub_key:
            spec["sshAuthorizedKey"] = ssh_pub_key

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching infraEnv %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted infraEnv %s", self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of InfraEnv.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"infraEnv {self.ref} status",
            expected_exceptions=KeyError,
        )

    def get_iso_download_url(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_ISO_URL_TIMEOUT,
    ):
        def _attempt_to_get_image_url() -> str:
            return self.get()["status"]["isoDownloadURL"]

        return waiting.wait(
            _attempt_to_get_image_url,
            sleep_seconds=3,
            timeout_seconds=timeout,
            waiting_for="image to be created",
            expected_exceptions=KeyError,
        )

    def get_cluster_id(self):
        iso_download_url = self.get_iso_download_url()
        return ISO_URL_PATTERN.match(iso_download_url).group("cluster_id")

    @classmethod
    def deploy_default_infraenv(
        cls,
        kube_api_client: ApiClient,
        name: str,
        ignore_conflict: bool = True,
        cluster_deployment: Optional[ClusterDeployment] = None,
        secret: Optional[Secret] = None,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        **kwargs,
    ) -> "InfraEnv":

        infra_env = InfraEnv(kube_api_client, name)
        try:
            if "filepath" in kwargs:
                infra_env._create_infraenv_from_yaml_file(
                    filepath=kwargs["filepath"], )
            else:
                infra_env._create_infraenv_from_attrs(
                    kube_api_client=kube_api_client,
                    name=name,
                    ignore_conflict=ignore_conflict,
                    cluster_deployment=cluster_deployment,
                    secret=secret,
                    proxy=proxy,
                    label_selector=label_selector,
                    ignition_config_override=ignition_config_override,
                    **kwargs,
                )
        except ApiException as e:
            if not (e.reason == "Conflict" and ignore_conflict):
                raise

        # wait until install-env will have status (i.e until resource will be
        # processed in assisted-service).
        infra_env.status()

        return infra_env

    def _create_infraenv_from_yaml_file(
        self,
        filepath: str,
    ) -> None:
        with open(filepath) as fp:
            yaml_data = yaml.safe_load(fp)

        self.create_from_yaml(yaml_data)

    def _create_infraenv_from_attrs(
        self,
        kube_api_client: ApiClient,
        cluster_deployment: ClusterDeployment,
        secret: Optional[Secret] = None,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        **kwargs,
    ) -> None:
        if not secret:
            secret = deploy_default_secret(
                kube_api_client=kube_api_client,
                name=cluster_deployment.ref.name,
            )
        self.create(
            cluster_deployment=cluster_deployment,
            secret=secret,
            proxy=proxy,
            label_selector=label_selector,
            ignition_config_override=ignition_config_override,
            **kwargs,
        )
Beispiel #27
0
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """
    _api_group = 'adi.io.my.domain'
    _version = 'v1alpha1'
    _plural = 'agents'

    def __init__(self,
                 kube_api_client: ApiClient,
                 name: str,
                 namespace: str = env_variables['namespace']):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment: 'ClusterDeployment',
    ) -> List['Agent']:
        resources = crd_api.list_namespaced_custom_object(
            group=cls._api_group,
            version=cls._version,
            plural=cls._plural,
            namespace=cluster_deployment.ref.namespace,
        )
        assigned_agents = []
        for item in resources.get('items', []):
            assigned_cluster_ref = ObjectReference(
                name=item['spec']['clusterDeploymentName']['name'],
                namespace=item['spec']['clusterDeploymentName']['namespace'])
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item['metadata']['name'],
                        namespace=item['metadata']['namespace']))

        return assigned_agents

    def create(self):
        raise RuntimeError(
            'agent resource must be created by the assisted-installer operator'
        )

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

    def patch(self, **kwargs) -> None:
        body = {'spec': kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body)

        logger.info('patching agent %s: %s', self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

        logger.info('deleted agent %s', self.ref)

    def status(self) -> dict:
        return self.get()['status']

    def approve(self) -> None:
        self.patch(approved=True)

        logger.info('approved agent %s', self.ref)