Mini Shell

Direktori : /opt/imh-python/lib/python3.9/site-packages/rads/
Upload File :
Current File : //opt/imh-python/lib/python3.9/site-packages/rads/vz.py

"""VZ / HA functions"""

import enum
import json
import os
import shlex
import subprocess

from pathlib import Path
from typing import Iterable, Optional, Union

import distro


class CT:
    """Create a CT object representing a VZ Container"""

    def __init__(self, ctid: str):
        try:
            self.ctid: str = str(ctid)
        except ValueError as exc:
            raise VZError(f"Provided ctid '{ctid}' is not of type str()") from exc

        # List of options for vzlist
        opts = [
            "cpulimit",
            "cpus",
            "cpuunits",
            "ctid",
            "description",
            "device",
            "disabled",
            "diskinodes",
            "diskspace",
            "hostname",
            "ip",
            "laverage",
            "name",
            "netfilter",
            "numiptent",
            "numproc",
            "onboot",
            "ostemplate",
            "physpages",
            "private",
            "root",
            "status",
            "swappages",
            "uptime",
            "uuid",
            "veid",
        ]

        # Build vzlist command
        command = ["vzlist", "-jo", ",".join(opts), self.ctid]

        # Execute vzlist command
        result = subprocess.run(command, capture_output=True, text=True, check=False)

        # Parse vzlist command result
        if result.returncode:
            raise VZError(f"{result.stderr}")

        data = json.loads(result.stdout)[0]

        self.cpulimit: int = data.get("cpulimit", 0)
        self.cpus: int = data.get("cpus", 0)
        self.cpuunits: int = data.get("cpuunits", 0)
        self.ctid: Union[int, str] = data.get("ctid", "")
        self.description: str = data.get("description", "")
        self.device: str = data.get("device", "")
        self.disabled: bool = data.get("disabled", False)
        self.diskinodes: dict = data.get("diskinodes", {})
        self.diskspace: dict = data.get("diskspace", {})
        self.hostname: str = data.get("hostname", "")
        self.ip: list[str] = data.get("ip", [])
        self.laverage: list[float] = data.get("laverage", [])
        self.name: str = data.get("name", "")
        self.netfilter: str = data.get("netfilter", "")
        self.numiptent: dict = data.get("numiptent", {})
        self.numproc: dict = data.get("numproc", {})
        self.onboot: bool = data.get("onboot", False)
        self.ostemplate: str = data.get("ostemplate", "")
        self.physpages: dict = data.get("physpages", {})
        self.private: str = data.get("private", "")
        self.root: str = data.get("root", "")
        self.status: str = data.get("status", "")
        self.swappages: dict = data.get("swappages", {})
        self.uptime: float = data.get("uptime", 0.0)
        self.uuid: str = data.get("uuid", "")
        self.veid: str = data.get("veid", "")

    def __repr__(self):
        attrs = ", ".join(f"{key}={repr(getattr(self, key))}" for key in self.__dict__)
        return f"CT({attrs})"

    def start(self) -> subprocess.CompletedProcess:
        """
        Starts the container identified by self.ctid using the 'vzctl start' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl start' command execution.
        """
        command = ["vzctl", "start", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def stop(self) -> subprocess.CompletedProcess:
        """
        Stops the container identified by self.ctid using the 'vzctl stop' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl stop' command execution.
        """
        command = ["vzctl", "stop", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def restart(self) -> subprocess.CompletedProcess:
        """
        Restarts the container identified by self.ctid using the 'vzctl restart' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl restart' command execution.
        """
        command = ["vzctl", "restart", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def mount(self) -> subprocess.CompletedProcess:
        """
        Mounts the container's filesystem identified by self.ctid using the 'vzctl mount' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl mount' command execution.
        """
        command = ["vzctl", "mount", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def umount(self) -> subprocess.CompletedProcess:
        """
        Unmounts the container's filesystem identified by self.ctid using the 'vzctl umount' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl umount' command execution.
        """
        command = ["vzctl", "umount", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def destroy(self) -> subprocess.CompletedProcess:
        """
        Destroys the container identified by self.ctid using the 'vzctl destroy' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl destroy' command execution.
        """
        command = ["vzctl", "destroy", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def register(self, dst) -> subprocess.CompletedProcess:
        """
        Registers the container at the specified destination using the 'vzctl register' command.

        Args:
            dst (str): The destination path where the container is to be registered.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl register' command execution.
        """
        command = ["vzctl", "register", dst, self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def unregister(self) -> subprocess.CompletedProcess:
        """
        Unregisters the container identified by self.ctid using the 'vzctl unregister' command.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl unregister' command execution.
        """
        command = ["vzctl", "unregister", self.ctid]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def clone(self, name: str) -> subprocess.CompletedProcess:
        """
        Clones the container identified by self.ctid with the specified name using the 'prlctl clone' command.

        Args:
            name (str): The name for the new cloned container.

        Returns:
            subprocess.CompletedProcess: The result of the 'prlctl clone' command execution.
        """
        command = ["prlctl", "clone", self.ctid, "--name", str(name)]
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def exec2(self, cmd: str) -> subprocess.CompletedProcess:
        """
        Executes the specified command within the container identified by self.ctid using the 'vzctl exec2' command.

        Args:
            cmd (str): The command to execute within the container.

        Returns:
            subprocess.CompletedProcess: The result of the 'vzctl exec2' command execution.
        """
        command = ["vzctl", "exec2", self.ctid] + str(cmd).split()
        return subprocess.run(command, capture_output=True, text=True, check=False)

    def set(self, **kwargs) -> subprocess.CompletedProcess:
        """
        Sets various configuration options for the container identified by self.ctid.

        This method constructs a command to modify the container's configuration using
        the `vzctl set` command. It accepts keyword arguments where each key-value pair
        represents an option and its corresponding value to be set. The method appends
        "--save" to the command to save the changes permanently. After running the
        command, the method reinitializes the object to reflect the updated configuration.

        Args:
            **kwargs: Arbitrary keyword arguments representing configuration options and values.

        Returns:
            subprocess.CompletedProcess: The result of the `subprocess.run` call,
            containing information about the execution of the command.
        """
        arg_list = []

        for key, value in kwargs.items():
            arg_list.append(f"--{key}")
            arg_list.append(f"{value}")
        arg_list.append("--save")

        command = ["vzctl", "set", self.ctid] + arg_list
        result = subprocess.run(command, capture_output=True, text=True, check=False)

        self.__init__(self.ctid)

        return result

    def fix_ctid(self, start=True) -> None:
        """
        A tool for offline fixing CTID fields of VZ7 containers. Re-registers the container while offline.

        This method stops the container, unregisters it, renames the container directory from the current CTID
        to the container name, updates the instance attributes accordingly, and re-registers the container with
        the new details. Optionally, it can restart the container if the `start` parameter is set to True.

        Args:
            start (bool): If True, the container will be restarted after the CTID fix is applied. Defaults to True.

        Returns:
            None
        """
        self.stop()
        self.unregister()

        src = Path(f"/vz/private/{self.ctid}")
        dst = Path(f"/vz/private/{self.name}")
        src.rename(dst)

        self.private = str(dst)
        self.root = f"/vz/root/{self.name}"
        self.ctid = self.veid = self.name

        self.register(self.private)

        if start:
            self.start()


class ListCmd(enum.Enum):
    """vz list base commands"""

    VZLIST = ["/usr/sbin/vzlist", "-H"]
    PRLCTL = ["/usr/bin/prlctl", "list", "-H"]


class VZError(Exception):
    """Raised for errors with VZ and OpenVZ"""


def is_vz() -> bool:
    """Checks if host is a Virtuozzo node"""
    return bool("Virtuozzo" in distro.name())


def is_openvz() -> bool:
    """Check if host is an OpenVZ node"""
    return os.path.isfile("/etc/virtuozzo-release") and not is_vz()


def is_vz7() -> bool:
    """Check if host is a Virtuozzo 7 node"""
    return bool(is_vz() and distro.major_version() == "7")


def is_vps() -> bool:
    """Check if host is a Virtuozzo container"""
    try:
        with open("/proc/vz/veinfo", encoding="ascii") as handle:
            ve_data = handle.read().strip()
    except IOError:
        return False  # if veinfo doesn't exist this can't be a vps
    if ve_data.count("\n") != 0:
        return False
    try:
        veid = int(
            ve_data.split()[0]
        )  # if veinfo contains >1 line, this is a CL or VZ node
    except ValueError:
        return True  # veinfo contains a UUID
    return veid != 0


def _exec(cmd: Iterable):
    """For executing prlctl or vzlist"""
    try:
        ret = subprocess.run(cmd, capture_output=True, encoding="utf-8", check=False)
    except FileNotFoundError as exc:
        raise VZError(exc) from exc
    if ret.returncode:  # nonzero
        raise VZError(f"Error running {cmd!r}. stderr={ret.stderr!r}")
    return ret


def is_ct_running(ctid: Union[str, int]) -> bool:
    """Checks if a container is running

    Args:
        ctid: container ID to check
    Returns:
        True if the container is running on this node, False if it
        isn't or if some other error occurs
    """
    try:
        ret = subprocess.run(
            ["/usr/bin/prlctl", "list", "-H", "-o", "status", str(ctid)],
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            encoding="utf-8",
            check=True,
        )
    except FileNotFoundError:
        pass  # retry with vzlist
    except subprocess.CalledProcessError:
        return False
    else:
        return ret.stdout.split()[0] == "running"
    try:
        ret = _exec(["/usr/sbin/vzlist", "-H", "-o", "status", str(ctid)])
    except VZError:  # CTID probably doesn't exist
        return False
    return ret.stdout.split()[0] == "running"


def uuid2ctid(uuid: str) -> str:
    """get the legacy CTID of a container

    Args:
        uuid: VZ UUID to find the legacy CTID for
    Raises:
        VZError: if the prlctl command fails
    """
    ret = _exec(["/usr/bin/prlctl", "list", "-H", "-o", "name", uuid])
    return ret.stdout.split()[0]


def ctid2uuid(ctid: Union[int, str]) -> str:
    """Obtain the UUID of a container from its legacy CTID

    Warning:
        This does not work on VZ4
    Args:
        ctid: Legacy CTID to get the UUID for
    Raises:
        VZError: if the prlctl command fails
    """
    ret = _exec(["/usr/bin/prlctl", "list", "-H", "-o", "uuid", str(ctid)])
    return ret.stdout.split()[0].strip(r"{}")


def get_envid(ctid: Union[int, str]) -> str:
    """Obtain the EnvID of a container

    Note:
        This determines what the subdirectory of /vz/root and /vz/private will
        be. This also has to run on VZ4 which lacks the envid field or prlctl,
        so we just return the CTID
    Args:
        ctid: legacy CTID to find the envid for
    Raises:
        VZError: if the prlctl command fails or
            /etc/virtuozzo-release is missing
    """
    try:
        with open("/etc/virtuozzo-release", "r", encoding="utf-8") as handle:
            if "Virtuozzo release 4" in handle.read():
                return str(ctid)
    except FileNotFoundError as exc:
        raise VZError(exc) from exc
    ret = _exec(["/usr/bin/prlctl", "list", "-H", "-o", "envid", str(ctid)])
    return ret.stdout.split()[0]


def _list_cmd(
    opts: list,
    args: Optional[list] = None,
    list_cmd: Optional[ListCmd] = None,
) -> tuple[ListCmd, list[str]]:
    """Deterines the cmd to run based on VZ version for get_cts()

    Args:
        opts: items to send into ``-o/--output``
        args: optional params to send such as ``--all``
        list_cmd (ListCmd): set this to ListCmd.VZLIST or ListCmd.PRLCTL to
            skip auto-detecting which command to use
    """
    if list_cmd is None:
        if is_vz():
            if not is_vz7() and "ostemplate" in opts:
                # prlctl's ostemplate is broken and reports distro on vz6
                # switch to vzlist; fix envid to veid if it was requested
                list_cmd = ListCmd.VZLIST
            else:
                list_cmd = ListCmd.PRLCTL
        else:  # OpenVZ
            list_cmd = ListCmd.VZLIST
    if list_cmd == ListCmd.VZLIST:
        conv_opts = {x: ("veid" if x == "envid" else x) for x in opts}
    else:
        # prctl refers to 'ctid' as 'name'
        conv_opts = {x: ("name" if x == "ctid" else x) for x in opts}
    cmd = list_cmd.value.copy()
    if args is not None:
        cmd.extend(args)
    # forces opts's vals to be in the same order as args
    cmd_opts = ",".join([conv_opts[x] for x in opts])
    cmd.extend(["-o", cmd_opts])
    return list_cmd, cmd


def _read_row(
    list_cmd: ListCmd, cmd: list[str], row: list[str], opts: list[str]
) -> dict[str, str]:
    # if number of rows matches requested options, return normally
    if len(row) == len(opts):
        return {x: row[i] for i, x in enumerate(opts)}
    # handle an edge case: prlctl can print missing ostemplates as '' while
    # vzlist prints it as '-', making the prlctl one harder to parse
    if (
        list_cmd == ListCmd.PRLCTL
        and len(row) == len(opts) - 1
        and "ostemplate" in opts
    ):
        opts = opts.copy()
        opts.remove("ostemplate")
        ret = {x: row[i] for i, x in enumerate(opts)}
        ret["ostemplate"] = "-"
        return ret
    raise VZError(
        f"{shlex.join(cmd)} expected {len(opts)} columns," f" but got {len(row)}: {row}"
    )


def get_cts(
    opts: Optional[list] = None,
    args: Optional[list] = None,
    list_cmd: Optional[ListCmd] = None,
) -> list[dict[str, str]]:
    """Returns containers according to platform as a list of dicts

    Args:
        opts: items to send into -o/--output (will default to ['ctid'] if None)
        args: optional params to send such as --all
        list_cmd (ListCmd): set this to ListCmd.VZLIST or ListCmd.PRLCTL to
            skip auto-detecting which command to use
    Raises:
        VZError: if the prlctl or vzlist command fails
    """
    if not opts:
        opts = ["ctid"]
    ret = []
    # process each line as a dict where keys are the arg and vals are the result
    list_cmd, cmd = _list_cmd(opts, args, list_cmd)
    for row in _exec(cmd).stdout.splitlines():
        row = row.strip()
        if not row:
            continue  # blank line
        ret.append(_read_row(list_cmd, cmd, row.split(), opts))
    return ret

Zerion Mini Shell 1.0