Mini Shell

Direktori : /opt/saltstack/salt/extras-3.10/rads/
Upload File :
Current File : //opt/saltstack/salt/extras-3.10/rads/vz.py

"""VZ / HA functions"""

import enum
import shlex
import subprocess
import os
from typing import Iterable, Union, Optional
import distro


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('Error running {!r}. stderr={!r}'.format(cmd, ret.stderr))
    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