Source code for ironic.console.container.kubernetes

#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""
Kubernetes pod console container provider.
"""
import json
import re
import time
import yaml

from oslo_concurrency import processutils
from oslo_log import log as logging

from ironic.common import exception
from ironic.common import utils
from ironic.conf import CONF
from ironic.console.container import base

LOG = logging.getLogger(__name__)

# How often to check pod status
POD_READY_POLL_INTERVAL = 2


[docs] class KubernetesConsoleContainer(base.BaseConsoleContainer): """Console container provider which uses kubernetes pods.""" def __init__(self): # confirm kubectl is available try: utils.execute("kubectl", "version") except processutils.ProcessExecutionError as e: LOG.exception( "kubectl not available, " "this provider cannot be used." ) raise exception.ConsoleContainerError( provider="kubernetes", reason=e ) if not CONF.vnc.console_image: raise exception.ConsoleContainerError( provider="kubernetes", reason="[vnc]console_image must be set.", ) try: self._render_template() except Exception as e: raise exception.ConsoleContainerError( provider="kubernetes", reason=f"Parsing {CONF.vnc.kubernetes_container_template} " f"failed: {e}", ) def _render_template(self, uuid="", app_name=None, app_info=None): """Render the Kubernetes manifest template. :param uuid: Unique identifier for the node. :param app_name: Name of the application to run in the container. :param app_info: Dictionary of application-specific information. :returns: A string containing the rendered Kubernetes YAML manifest. """ # TODO(stevebaker) Support bind-mounting certificate files to # handle verified BMC certificates if not uuid: uuid = "" if not app_name: app_name = "fake" if not app_info: app_info = {} params = { "uuid": uuid, "image": CONF.vnc.console_image, "app": app_name, "app_info": json.dumps(app_info).strip(), "read_only": CONF.vnc.read_only, "conductor": CONF.host, } return utils.render_template( CONF.vnc.kubernetes_container_template, params=params ) def _apply(self, manifest): try: utils.execute( "kubectl", "apply", "-f", "-", process_input=manifest ) except processutils.ProcessExecutionError as e: LOG.exception("Problem calling kubectl apply") raise exception.ConsoleContainerError( provider="kubernetes", reason=e ) def _delete( self, resource_type, namespace, resource_name=None, selector=None ): args = [ "kubectl", "delete", "-n", namespace, resource_type, "--ignore-not-found=true", ] if resource_name: args.append(resource_name) elif selector: args.append("-l") args.append(selector) else: raise exception.ConsoleContainerError( provider="kubernetes", reason="Delete must be called with either a resource name " "or selector.", ) try: utils.execute(*args) except processutils.ProcessExecutionError as e: LOG.exception("Problem calling kubectl delete") raise exception.ConsoleContainerError( provider="kubernetes", reason=e ) def _get_pod_node_ip(self, pod_name, namespace): try: out, _ = utils.execute( "kubectl", "get", "pod", pod_name, "-n", namespace, "-o", "jsonpath={.status.podIP}", ) return out.strip() except processutils.ProcessExecutionError as e: LOG.exception("Problem getting pod host IP for %s", pod_name) raise exception.ConsoleContainerError( provider="kubernetes", reason=e ) def _wait_for_pod_ready(self, pod_name, namespace): end_time = time.time() + CONF.vnc.kubernetes_pod_timeout while time.time() < end_time: try: out, _ = utils.execute( "kubectl", "get", "pod", pod_name, "-n", namespace, "-o", "json", ) pod_status = json.loads(out) if ( "status" in pod_status and "conditions" in pod_status["status"] ): for condition in pod_status["status"]["conditions"]: if ( condition["type"] == "Ready" and condition["status"] == "True" ): LOG.debug("Pod %s is ready.", pod_name) return except ( processutils.ProcessExecutionError, json.JSONDecodeError, ) as e: LOG.warning( "Could not get pod status for %s: %s", pod_name, e ) time.sleep(POD_READY_POLL_INTERVAL) msg = ( f"Pod {pod_name} did not become ready in " f"{CONF.vnc.kubernetes_pod_timeout}s" ) raise exception.ConsoleContainerError( provider="kubernetes", reason=msg ) def _get_resources_from_yaml(self, rendered, kind=None): """Extracts Kubernetes resources from a YAML manifest. This method parses a multi-document YAML string and yields each Kubernetes resource (dictionary) found. If `kind` is specified, only resources of that specific kind are yielded. :param rendered: A string containing the rendered Kubernetes YAML manifest. :param kind: Optional string, the 'kind' of Kubernetes resource to filter by (e.g., 'Pod', 'Service'). If None, all resources are yielded. :returns: A generator yielding Kubernetes resource dictionaries. """ # Split the YAML into individual documents documents = re.split(r"^---\s*$", rendered, flags=re.MULTILINE) for doc in documents: if not doc.strip(): continue data = yaml.safe_load(doc) if not data: continue if not kind or data.get("kind") == kind: yield data
[docs] def start_container(self, task, app_name, app_info): """Start a console container for a node. Any existing running container for this node will be stopped. :param task: A TaskManager instance. :raises: ConsoleContainerError """ node = task.node uuid = node.uuid LOG.debug("Starting console container for node %s", uuid) rendered = self._render_template(uuid, app_name, app_info) self._apply(rendered) pod = list(self._get_resources_from_yaml(rendered, kind="Pod"))[0] pod_name = pod["metadata"]["name"] namespace = pod["metadata"]["namespace"] try: self._wait_for_pod_ready(pod_name, namespace) host_ip = self._get_pod_node_ip(pod_name, namespace) except Exception as e: LOG.error( "Failed to start container for node %s, cleaning up.", uuid ) try: self._stop_container(uuid) except Exception: LOG.exception( "Could not clean up resources for node %s", uuid ) raise e return host_ip, 5900
def _stop_container(self, uuid): rendered = self._render_template(uuid) resources = list(self._get_resources_from_yaml(rendered)) resources.reverse() for resource in resources: kind = resource["kind"] name = resource["metadata"]["name"] namespace = resource["metadata"]["namespace"] self._delete(kind, namespace, resource_name=name)
[docs] def stop_container(self, task): """Stop a console container for a node. Any existing running container for this node will be stopped. :param task: A TaskManager instance. :raises: ConsoleContainerError """ node = task.node uuid = node.uuid self._stop_container(uuid)
def _labels_to_selector(self, labels): selector = [] for key, value in labels.items(): selector.append(f"{key}={value}") return ",".join(selector)
[docs] def stop_all_containers(self): """Stops all running console containers This is run on conductor startup and graceful shutdown to ensure no console containers are running. :raises: ConsoleContainerError """ LOG.debug("Stopping all console containers") rendered = self._render_template() resources = list(self._get_resources_from_yaml(rendered)) resources.reverse() for resource in resources: kind = resource["kind"] namespace = resource["metadata"]["namespace"] labels = resource["metadata"]["labels"] selector = self._labels_to_selector(labels) self._delete(kind, namespace, selector=selector)