Mini Shell
"""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