#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import logging
import os
import syslog
import pwd
from typing import Optional # NOQA
from clcommon.clproc import ProcLve
from clcommon import cpapi, ClPwd
from clcommon.cpapi.cpapiexceptions import NotSupported
from clveconfig import ve_config
from lve_utils.pylve_wrapper import PyLveError, PyLve # noqa: F401 — re-exported for callers
from websiteisolation import config as _ws_config
from websiteisolation import id_registry as _ws_id_registry
from websiteisolation import domain_lve_sentry_extra
# A child of the 'lvectl' logger, so these records land in
# /var/log/cloudlinux/lvectl.log when lvectl is the running program and go
# nowhere under the other entry points that share this module (see
# cllvectl.log.get_lvectl_child_logger). Per-item detail is DEBUG -- an
# `apply all` touches every domain on the box -- and is switched on with
# touch /var/log/cloudlinux/lvectl.log.debug.flag
# Summaries are INFO, and anything that silently drops a domain is WARNING.
log = logging.getLogger('lvectl.lveapi')
LVP_XML_TAG_NAME = "reseller"
LVE_NO_UBC = 1 << 1
LVE_NO_MAXENTER = 1 << 2
class NameMapError(Exception):
pass
class NameMapConfigError(NameMapError):
pass
class NameMapNotInitialized(NameMapError):
pass
class NameMap:
"""
Container for backend storing resellers_name<=>resellers_id map
As backend store use ve.cfg
Usage:
>>> name_map = NameMap()
>>> name_map.link_xml_node()
>>> name_map.id_list()
[1001]
"""
def __init__(self, xml_tag_name=LVP_XML_TAG_NAME):
self._xml_tag_name = xml_tag_name
self._xml_node = None
# Reseller name to id map (list of corteges)
self._reseller_id_name_map = None
def get_id(self, name):
for name_, id_ in self.load_from_node():
if name_ == name:
return id_
def get_name(self, id_):
for name_, _id in self.load_from_node():
if id_ == _id:
return name_
def id_list(self):
return [id_ for _, id_ in self.load_from_node()]
def link_xml_node(self, xml_node=None, use_cache=True):
"""
Initialize NameMap. If xml_node is none,
config will be loaded automatically
:param use_cache: Bool whether bypass ve.cfg xml cache
:param xml_node: !! DEPRECATED PARAM !!
this param is left only for
compatibility with our old code
"""
if xml_node is None:
# New mode, Load reseller_id, reseller_name pairs from ve.cfg to dictionary
self._xml_node = None
self._load_resellers_map_from_ve_cfg(use_cache)
else:
# For compatibility with our old code
self._xml_node = xml_node
self._reseller_id_name_map = None
def _load_resellers_map_from_ve_cfg(self, use_cache):
"""
Fills self._reseller_id_name_map from ve.cfg file
:return:
"""
self._reseller_id_name_map = []
ve_cfg, xml_node = self._try_get_xml_node(use_cache=use_cache)
for el_ in xml_node:
name = el_.getAttribute('user')
id_ = int(el_.getAttribute('id'))
if name and id_ and id_ not in self._reseller_id_name_map:
self._reseller_id_name_map.append((id_, name))
# Force delete XML object to avoid high memory load
del xml_node
del ve_cfg
def _try_get_xml_node(self, use_cache=True):
try:
ve_cfg, xml_node = ve_config.get_xml_config(use_cache=use_cache)
except ve_config.BadVeConfigException as e:
self._reseller_id_name_map = None
raise NameMapConfigError("Error happened while loading data from ve.cfg") from e
return ve_cfg, xml_node.getElementsByTagName(self._xml_tag_name)
def load_from_node(self):
"""
Obtain data from xml node as (name, id_) list
"""
if self._xml_node is None and self._reseller_id_name_map is None:
raise NameMapNotInitialized('Name map is not initialized. '
'Use obj.link_xml_node() to get data from config')
if self._xml_node:
# For compatibility with our old code
for el_ in self._xml_node.getElementsByTagName(self._xml_tag_name):
name = el_.getAttribute('user')
id_ = int(el_.getAttribute('id'))
if name and id_:
yield name, id_
if self._reseller_id_name_map:
# New mode, use resellers map
for id_, name in self._reseller_id_name_map:
yield name, id_
class LvpMap:
"""
Container for storing information about lve:lvp mapping
In which reseller container stored lve
"""
def __init__(self):
self.name_map = NameMap()
self._id_name_map = {}
self._name_id_map = {}
self._reseller_id_map_panel = None
self._pwd = ClPwd()
def _add_map(self, name, id_):
self._id_name_map[id_] = name
self._name_id_map[name] = id_
def pw_uid(self, name, default=None):
try:
return self._pwd.get_pw_by_name(name).pw_uid
except ClPwd.NoSuchUserException:
return default
def _get_panel_reseller_id(self, reseller):
# type: (str) -> Optional[int]
uid = self.pw_uid(reseller)
if uid is not None:
return uid
# in case when we cannot find reseller in passwd file
# let's ask control panel for reseller's id
if self._reseller_id_map_panel is None:
self._reseller_id_map_panel = cpapi.get_reseller_id_pairs()
return self._reseller_id_map_panel.get(reseller)
def get_reseller_id(self, name):
# type: (str) -> Optional[int]
"""
Convert reseller name to an LVE id.
It supports resellers without a system account (for Plesk compatibility).
"""
uid = self.name_map.get_id(name) or self._name_id_map.get(name)
if uid is not None:
return uid
try:
uid = self._get_panel_reseller_id(name)
except NotSupported:
uid = None
if uid is not None:
self._add_map(name, uid)
return uid
def get_reseller_name(self, id_):
"""
Convert reseller id to reseller name
It support resellers without system account (for Plesk compatibilyty)
"""
# add attribute fo in memory cache support
name = self.name_map.get_name(id_) or self._id_name_map.get(id_)
if name is not None:
return name
try:
name = pwd.getpwuid(id_).pw_name
if cpapi.is_reseller(name):
self._add_map(name, id_)
else:
name = None
except KeyError:
name = None
return name
def lve_lvp_pairs(self):
"""
This method loops over all user:reseller pairs in control panel
and returns appropriate lve_id:lvp_id pairs.
THIS METHOD WON'T CHECK IF 'RESELLER LIMITS' IS ENABLED IN ve.cfg
"""
resellers = set(cpapi.resellers())
reseller_uids = {}
for reseller in resellers:
try:
reseller_uids[reseller] = self.get_reseller_id(reseller)
except NotSupported:
syslog.syslog(
syslog.LOG_WARNING, f"Reseller {reseller} still exists in control panel, "
"but absent in /etc/passwd file")
for cplogin, reseller in cpapi.cpinfo(keyls=('cplogin', 'reseller')):
lve_id = self.pw_uid(cplogin)
# for some reasons (process of destroying user died
# or admin called 'pure' userdel), user may still exist in control panel
# but absent in /etc/passwd file; we can do nothing with that,
# so just skip and write a warning to syslog
if lve_id is None:
syslog.syslog(
syslog.LOG_WARNING, f"user {cplogin} still exists in control panel, "
"but absent in /etc/passwd file")
continue
lvp_id = reseller_uids.get(reseller, 0)
yield lve_id, lvp_id
@staticmethod
def resellers():
for reseller_name in cpapi.resellers():
yield reseller_name
@staticmethod
def reseller_uids(name):
"""
Obtain from control panel resellers uids
"""
uids = []
reseller_users = cpapi.reseller_users(name)
for user in reseller_users:
try:
id_ = pwd.getpwnam(user).pw_uid
uids.append(id_)
except KeyError:
syslog.syslog(
syslog.LOG_WARNING, f"user {user} still exists in control panel, "
"but absent in /etc/passwd file")
return uids
def lvp_lve_id_list(self, lvp_id):
reseller_name = self.get_reseller_name(lvp_id)
return self.reseller_uids(reseller_name)
class Lve:
def __init__(self, proc=None, py=None, map=None):
self.proc = proc or ProcLve()
self.py = py or PyLve()
self.map = map or LvpMap()
self._mapped_domain_users: set = set()
# CLOS-6868: memoised {lve_id: lvp_id}. Derived from ve.cfg plus the
# panel's user list, neither of which changes while a single lvectl
# command runs, so one build per process is enough. Before this,
# lve2lvp() rescanned the whole generator per account, which made
# `apply all` quadratic in account count — 7,738 rebuilds on a
# 3,868-account host, each re-deriving the list from cpinfo and passwd.
self._lve_lvp_map_cache: dict | None = None
def lve_lvp_map(self) -> dict:
"""{lve_id: lvp_id} from ve.cfg, built once per instance.
Callers that need a single lookup must use this rather than scanning
`lve_id_lvp_id_pairs()`; the scan is O(accounts) and doing it per
account is what made `apply all` quadratic.
"""
if self._lve_lvp_map_cache is None:
enabled_lvp_id = set(self.map.name_map.id_list())
# A user whose reseller is NOT enabled in ve.cfg still appears, with
# lvp_id 0 — dropping those rows would silently change which
# accounts `apply all` treats as host-level.
self._lve_lvp_map_cache = {
lve_id: (lvp_id if lvp_id in enabled_lvp_id else 0)
for lve_id, lvp_id in self.map.lve_lvp_pairs()
}
return self._lve_lvp_map_cache
def invalidate_lve_lvp_map(self) -> None:
"""Drop the memoised map after ve.cfg or the panel user list changes."""
self._lve_lvp_map_cache = None
def lve_id_lvp_id_pairs(self):
"""
Obtain {lve id}:{lvp id} pairs iterator based on ve.cfg config
(detect enabled resellers containers)
This method (unlike LvpMap.lve_lvp_pairs) will check
if reseller is enabled in ve.cfg and return lvp_id=0
for users of reseller with disabled reseller limits
"""
yield from self.lve_lvp_map().items()
def lve2lvp(self, lve_id):
"""
Obtain lvp id based on ve.cfg config (detect enabled resellers containers)
"""
return self.lve_lvp_map().get(lve_id, 0)
def nest_user_lve(self, lve_id, parent_lvp, kernel_map):
"""Make sure lve really sits inside its isolation LVP lvp.
*kernel_map* is the shared /proc/lve/map snapshot (CLOS-6868); this
writes its own effect back into it. *parent_lvp* is where lve
belongs when it is NOT isolated -- its reseller LVP, or 0. lvp
must already exist; callers create it first.
CLOS-7493. Two things this must not do, both of which reported success
while leaving the account outside its own aggregate cap:
* Trust the map. /proc/lve/map records where an LVE is *meant* to
live, not where it is; a row can name lvp while the LVE itself
is still at the host LVP. So when the map claims the move is done,
confirm it against lvp's own listing -- the kernel's answer.
That read is skipped in every other case, and skipped entirely for an
LVE that does not exist yet: ordinarily lve is created later by
lve_setup, already parented by the mapping this registers.
* Trust lve_lvp_move's return code. It returns 0 without touching the
LVE when the mapping already names the target (kmod
lve_lvp_map_move(): `old_lvp_id == lvp_id` -> return 0), which is
exactly the state above. Park the LVE on *parent_lvp* first, so the
move back has a mapping change to act on. Measured: the bare move
leaves it at /lve, the pair lands it at /lvp/lve.
A self-owned reseller that isolates a domain of its own is its own
parent (*parent_lvp* == *lve_id*), and parking there would be the
same no-op, so it is parked on the host LVP instead.
Whether lve_lvp_move should repair that divergence by itself is a
question for lve-kmod (CLOS-7468 left it open); until it does, the
re-seat is what re-homes such an LVE from here.
"""
mapped_to = kernel_map.get(lve_id, 0)
if mapped_to == lve_id and (
not self.py.lve_exists(lve_id)
or self.proc.check_inside_list(lve_id, lve_id)):
return False
log.info('nesting lve%s under its isolation LVP lvp%s (map said lvp%s)',
lve_id, lve_id, mapped_to)
if mapped_to == lve_id:
park_lvp = 0 if parent_lvp == lve_id else parent_lvp
log.info('lve%s is mapped to lvp%s but is not inside it; re-seating '
'via lvp%s', lve_id, lve_id, park_lvp)
self.py.lve_lvp_move(park_lvp, lve_id)
self.py.lve_lvp_move(
lve_id, lve_id,
err_msg=f'Can`t move lve_id={lve_id} to lvp_id={lve_id}; error code {{code}}'
)
kernel_map[lve_id] = lve_id
# Fail closed (CLOS-7468): a 0 from lve_lvp_move is not proof. Warn
# rather than raise -- one unappliable account must not cost every
# later account its limits (CLOS-7049).
if self.py.lve_exists(lve_id) and not self.proc.check_inside_list(lve_id, lve_id):
log.error('lve%s is STILL outside lvp%s after the move; the account '
'runs outside its own aggregate cap', lve_id, lve_id)
syslog.syslog(
syslog.LOG_WARNING,
f'lve{lve_id} could not be nested under its isolation LVP lvp{lve_id}; '
f'the account is not covered by its aggregate limit',
)
return True
def lve_destroy(self, lve_id, *args, **kwargs):
"""
safe destroy lve container with preserving lvp mapping
"""
if os.path.exists(self.proc.proc_lve_map()):
lvp_id = self.proc.map().get(lve_id, 0)
else:
lvp_id = 0
self.py.lve_destroy(lve_id, *args, **kwargs)
if lvp_id != 0:
try:
pwd.getpwuid(lve_id)
self.py.lve_lvp_map(lvp_id, lve_id)
except KeyError:
pass
def _build_domain_map(self, lve_id):
"""Resolve all domain→docroot→domain_id mappings once.
Returns a list of (domain_id, limits_dict) tuples.
Callers pass this to _map_domain_lves / apply_domain_lve_limits
to avoid redundant resolve_docroot + assign_domain_id calls.
Uses cpapi.userdomains() to fetch all domain→docroot pairs in a
single file read instead of calling resolve_docroot per domain,
which would re-parse the entire userdatadomains file each time.
"""
config = _ws_config.load_config(lve_id)
try:
username = pwd.getpwuid(lve_id).pw_name
docroot_by_domain = dict(cpapi.userdomains(username) or [])
except KeyError:
docroot_by_domain = {}
entries = []
# Skips are counted by reason, not just totalled: the summary below is
# the only per-domain record that reaches Sentry (clsentry hooks
# logging at ERROR), and the three reasons differ in what they are
# worth waking someone for. See the summary for the full account.
skipped_stale = skipped_not_enabled = skipped_tenant_data = 0
for d in config.domains:
if not d.name:
# A row with no name cannot be matched to a docroot at all.
# Warned, like every other skip below, so that the summary's
# skipped count always has a matching per-domain record.
skipped_tenant_data += 1
log.warning('uid %s: domains.json holds a domain entry with no name; '
'skipped, so no per-domain limits come from it', lve_id)
continue
try:
docroot = docroot_by_domain.get(d.name)
if docroot is None:
# domains.json names a domain the panel does not (any
# more): a rename or removal that left the tenant config
# stale. Its limits cannot be applied and nothing else
# reports that.
# %r, not %s: the name comes from tenant-writable
# domains.json unvalidated beyond its type, and a raw
# control character in a record root writes would forge
# further records.
skipped_stale += 1
log.warning(
'uid %s: domain %r from domains.json has no docroot in the panel; '
'its per-domain limits are NOT applied', lve_id, d.name)
continue
# Only map domains the administrator explicitly enabled (the
# registry entry is allocated via enable_domain_lve). Do NOT
# allocate here: domains.json is user-writable, so allocating on
# mere presence would let an account self-register domains and
# bypass the admin-only enable step.
domain_id = _ws_id_registry.get_domain_id(lve_id, docroot)
if domain_id is None:
# No registry entry means enable-domain-limits was never run
# for this domain (only allow-domain-limits), or it was
# disabled again. The domain still shows up in
# `lvectl list-domains` and in domains.json, so this skip is
# the whole difference between configured and enforced.
#
# DEBUG, not WARNING: an administrator who enabled a subset
# of an account's domains leaves every other domain on this
# path, so on a healthy host this is the common case, not a
# fault. At WARNING it fired on every partial rollout and
# promoted the summary below with it, which is what buried
# the skips that do mean something.
skipped_not_enabled += 1
log.debug(
'uid %s: domain %r (docroot %r) has no domain LVE id -- not enabled '
'via enable-domain-limits -- so its per-domain limits are NOT applied',
lve_id, d.name, docroot)
continue
entries.append((domain_id, d.limits.to_dict()))
except (ValueError, TypeError, OverflowError) as exc:
# Malformed tenant data (e.g. a non-str/unhashable domain name)
# must not abort the root apply-all batch — log and skip the
# offending domain so other domains and tenants still apply.
syslog.syslog(
syslog.LOG_WARNING,
f"Skipping malformed domain entry for user {lve_id} "
f"(name={d.name!r}): {exc}")
skipped_tenant_data += 1
log.warning('uid %s: malformed domain entry (name=%r) skipped, so its '
'per-domain limits are NOT applied: %s', lve_id, d.name, exc)
continue
skipped = len(config.domains) - len(entries)
# Which reason is present decides the level, because the level decides
# what leaves the host: the Sentry handler is attached at ERROR, so
# WARNING is written to lvectl.log and goes no further. Nothing this
# summary reports reaches ERROR, and each reason says why:
#
# - stale: domains.json names a domain the panel no longer resolves to
# a docroot. Real, but TENANT-REACHABLE: the iteration is over
# ~/.lve/domains.json, which the account owns, so an account that
# adds {"name": "nope.example"} moves this counter at will.
# - tenant_data: a nameless or malformed entry. ~/.lve/domains.json is
# tenant-writable, so promoting this would hand any account a switch
# that writes to our Sentry quota at will. WARNING, recorded in
# lvectl.log, never escalated.
# - not_enabled: the administrator simply has not run
# enable-domain-limits for that domain. The steady state of every
# partial rollout, so it does not raise the level at all.
# - orphaned registration: /etc/container/lvd_ids/ holds a docroot
# the panel no longer lists. Only root writes that file, via
# `lvectl enable-domain-limits`, which is why it once carried the
# ERROR -- but minting an entry is only one side of the comparison.
# The other side, the docroot leaving the panel, is a PANEL-USER
# action: on every supported panel the account owner deletes their
# own domain. So the level was end-user-triggerable after all, which
# is the same property that holds the two cases above at WARNING.
# It is also a STATE and not an event -- it re-reports on every apply
# of any account, for as long as the entry exists -- and the apply is
# not the place that can clear it. The clearing lives in the
# post_delete_domain hook (lvectllib._prune_domains_absent_from_panel),
# which runs once per deletion, as root, and reports its own failures
# (CLOS-7467). Here it stays a WARNING in lvectl.log.
orphaned_registrations = 0
if docroot_by_domain:
# Empty means the panel could not be asked at all (no passwd entry,
# or userdomains() returned nothing). Every registration would then
# look orphaned, so the question is unanswerable rather than
# answered no.
panel_docroots = set(docroot_by_domain.values())
orphaned_registrations = sum(
1 for docroot in (_ws_id_registry.get_all_entries(lve_id) or {})
if docroot not in panel_docroots
)
if orphaned_registrations or skipped_stale or skipped_tenant_data:
level = logging.WARNING
else:
level = logging.DEBUG
# Explicit fingerprint, because raven groups on the FORMATTED message:
# without it every distinct (uid, counts) combination opens its own
# Sentry issue, so one fleet-wide regression arrives as hundreds of
# one-event issues instead of one issue seen on hundreds of servers --
# and the per-server counts are what make them all distinct. The uid
# and the counts stay in the message for lvectl.log and for the event
# body; they just must not decide the grouping.
log.log(
level,
'uid %s: domain map built -- %s of %s configured domain(s) resolved to a domain '
'LVE id%s%s', lve_id, len(entries), len(config.domains),
f'; {skipped} skipped ({skipped_stale} stale, {skipped_tenant_data} malformed, '
f'{skipped_not_enabled} not enabled)' if skipped else '',
f'; {orphaned_registrations} registered docroot(s) the panel no longer lists'
if orphaned_registrations else '',
extra=domain_lve_sentry_extra('domain-map-skipped'))
return entries
def _map_domain_lves(self, lve_id, proc_map, domain_map):
"""Place every domain LVE that belongs to *lve_id* under its user LVP.
Uses lve_lvp_move for domain LVEs that already exist in the kernel,
and falls back to lve_lvp_map for those that do not yet exist (so
the kernel will place them under the correct LVP when lve_setup
creates them later).
*proc_map* is an lve_id→lvp_id dict (from /proc/lve/map) used to
skip mappings that are already correct. Updated in-place so callers
see the new state.
*domain_map* is a precomputed list from _build_domain_map().
"""
moved = pre_registered = 0
for domain_id, _limits in domain_map:
if proc_map.get(domain_id) == lve_id:
log.debug('uid %s: domain LVE %s already under lvp%s',
lve_id, domain_id, lve_id)
continue
if domain_id in proc_map:
# Domain LVE exists in the kernel — move it.
log.debug('uid %s: moving domain LVE %s from lvp%s to lvp%s',
lve_id, domain_id, proc_map.get(domain_id), lve_id)
self.py.lve_lvp_move(lve_id, domain_id)
moved += 1
else:
# Domain LVE does not exist yet — pre-register mapping.
log.debug('uid %s: pre-registering not-yet-created domain LVE %s under lvp%s',
lve_id, domain_id, lve_id)
self.py.lve_lvp_map(lve_id, domain_id)
pre_registered += 1
proc_map[domain_id] = lve_id
if moved or pre_registered:
log.info('uid %s: mapped domain LVEs under lvp%s -- %s moved, %s pre-registered, '
'%s domain(s) total', lve_id, lve_id, moved, pre_registered, len(domain_map))
self._mapped_domain_users.add(lve_id)
def apply_domain_lve_limits(self, lve_id, domain_map):
"""Apply per-domain limits from domains.json to each domain LVE.
Must be called after lve_set_default() resets all LVEs inside the
user LVP to the user's defaults. Each domain LVE is set to its
stored limits, or the user's current cpu when unconfigured (cpu=0).
CLOS-4024: the kernel rejects lve_lvp_setup when any child has cpu=0
inside a finite-cpu parent, so we never write cpu=0 to the kernel.
CLOS-4060: domains with no custom limits are skipped — lve_set_default
already reset their existing LVEs to user defaults, and non-existing
ones will be lazily created by lve_enter under the user's LVP.
*domain_map* is a precomputed list from _build_domain_map().
"""
custom_limits = [(d, l) for d, l in domain_map if l]
if not custom_limits:
log.debug('uid %s: no domain has custom limits; lve_set_default already reset '
'the %s domain LVE(s) to the user defaults', lve_id, len(domain_map))
return
try:
parent_ls_cpu = self.py.lve_info(lve_id).ls_cpu
except OSError:
# fallback to 100% of all available CPU
# fallback to 1 cpu core when we cannot identify
# number of cores and cpu_count returns None
parent_ls_cpu = (os.cpu_count() or 1) * 10000
for domain_id, limits in custom_limits:
settings = self.py.liblve_settings()
try:
# CLOS-4024: the kernel rejects lve_lvp_setup with EINVAL when
# any child LVE has cpu=0 inside a finite-cpu parent. Always
# set a non-zero cpu on domain LVEs: use the stored value if
# present, otherwise inherit the parent's current cpu limit.
if 'cpu' in limits:
settings.ls_cpu = int(limits['cpu']) or parent_ls_cpu
else:
settings.ls_cpu = parent_ls_cpu
if 'pmem' in limits:
pmem_bytes = int(limits['pmem'])
settings.ls_memory_phy = (
pmem_bytes // _ws_config.PAGE_SIZE if pmem_bytes else 0)
if 'io' in limits:
settings.ls_io = int(limits['io'])
if 'nproc' in limits:
settings.ls_nproc = int(limits['nproc'])
if 'iops' in limits:
settings.ls_iops = int(limits['iops'])
if 'ep' in limits:
settings.ls_enters = int(limits['ep'])
if 'vmem' in limits:
vmem_bytes = int(limits['vmem'])
settings.ls_memory = (
vmem_bytes // _ws_config.PAGE_SIZE if vmem_bytes else 0)
except (ValueError, TypeError, OverflowError) as exc:
# Malformed tenant data (e.g. a non-numeric or out-of-C-long
# limit value) must not abort the root apply-all batch — log
# and skip this domain so other domains and tenants still
# apply. Real kernel/lve errors from lve_setup below are NOT
# caught here and still propagate to the outer handler.
syslog.syslog(
syslog.LOG_WARNING,
f"Skipping malformed limits for domain LVE {domain_id} "
f"(user {lve_id}): {exc}")
continue
self.py.lve_setup(
domain_id, settings,
err_msg=f'Can`t setup domain LVE {domain_id}; error code {{code}}',
)
# Log the requested limits plus the resolved cpu -- ls_cpu is the only
# member this method always assigns (the rest are set only when the
# domain configures them), so reading the others back would make the
# log depend on more of the settings object than the code it reports on.
log.debug('uid %s: domain LVE %s set from %s, effective cpu=%s',
lve_id, domain_id, limits, settings.ls_cpu)
def _sync_map(self):
"""
Load lve_id:lvp_id map to kmod-lve.
For users with per-domain isolation configured, builds a nested LVP
hierarchy instead of a flat reseller mapping:
lvp<0>
[lvp] ← reseller LVP (if enabled)
lvp ← user-level LVP with isolation
lve ← user LVE
lve ... ← domain LVEs
lvp<0>
[lvp] ← reseller LVP (without isolation)
lve ← user LVE (flat, existing behaviour)
"""
# load mapping information from kernel (/proc/lve/map)
proc_map_dict = self.proc.map()
# loop over user_id:reseller_id pairs
# lve_id_lvp_id_pairs includes all control panel users
# and checks for enabled resellers in ve.cfg
# so user of reseller without reseller limits
# will be listed in response like 'tuple(user_id, 0)'
if self.py.domains_supported():
# CLOS-4542: only users with at least one registered domain are
# domain-isolated. A bare marker (allow-domain-limits with no
# enabled domain — the state cagefsctl --isolates-allow-all leaves
# every user in) must NOT get a per-user LVP here: lve_apply treats
# it as non-isolated (lvectllib._is_domain_isolated_user requires
# >= 1 domain) and never caps lvp, so nesting lve under a
# freshly-created uncapped lvp on a cold (post-reboot) rebuild
# would collapse the user to the kernel-default LVP. Mirror the
# _is_domain_isolated_user predicate so the two paths agree.
isolated_users = {
uid for uid in _ws_config.find_all_lve_ids_with_config()
if _ws_id_registry.get_all_entries(uid)
}
else:
isolated_users = set()
log.info('sync-map: %s domain-isolated user(s), per-domain support=%s',
len(isolated_users), self.py.domains_supported())
created_lvps = moved_lves = 0
for lve_id, lvp_id in self.lve_id_lvp_id_pairs():
if lve_id in isolated_users:
# User has per-domain isolation: build nested LVP hierarchy.
# Ensure the parent (reseller) LVP exists first.
if lvp_id != 0 and not self.proc.exist_lvp(lvp_id=lvp_id):
log.debug('sync-map: uid %s is isolated under reseller lvp%s; '
'creating the missing reseller LVP', lve_id, lvp_id)
self.py.lve_lvp_create(lvp_id)
created_lvps += 1
# Create user LVP nested under the reseller (or at root
# when there is no reseller). lve_apply_all() already
# creates the nested LVP for reseller users; this handles
# the non-reseller case and the standalone sync-map call.
if not self.proc.exist_lvp(lvp_id=lve_id):
log.info('sync-map: creating isolation LVP lvp%s for uid %s under lvp%s',
lve_id, lve_id, lvp_id)
if lvp_id:
self.py.lve_lvp_create2(lve_id, lvp_id)
else:
self.py.lve_lvp_create(lve_id)
created_lvps += 1
# Move the user LVE under the user LVP (both share lve_id).
# CLOS-7493: the map alone cannot say the move is done, and a
# map row that already names lvp makes lve_lvp_move a
# no-op -- nest_user_lve handles both.
if self.nest_user_lve(lve_id, lvp_id, proc_map_dict):
moved_lves += 1
if lve_id not in self._mapped_domain_users:
domain_map = self._build_domain_map(lve_id)
self._map_domain_lves(lve_id, proc_map=proc_map_dict,
domain_map=domain_map)
else:
log.debug('sync-map: uid %s domain LVEs already mapped this run', lve_id)
else:
# Standard behaviour: move user LVE under reseller/root LVP.
if proc_map_dict.get(lve_id, 0) != lvp_id: # change map if needed only
if not self.proc.exist_lvp(lvp_id=lvp_id):
log.debug('sync-map: creating lvp%s', lvp_id)
self.py.lve_lvp_create(lvp_id)
created_lvps += 1
log.debug('sync-map: moving lve%s from lvp%s to lvp%s',
lve_id, proc_map_dict.get(lve_id, 0), lvp_id)
self.py.lve_lvp_move(lvp_id, lve_id)
proc_map_dict[lve_id] = lvp_id
moved_lves += 1
log.info('sync-map: done -- %s LVP(s) created, %s LVE(s) moved',
created_lvps, moved_lves)
def sync_map(self):
"""
wrapped _sync_map function for prevent error if some cpapi not supported
"""
try:
self._sync_map()
except NotSupported:
# Panel does not expose the reseller/user pairs sync-map needs.
# Silently doing nothing here used to be indistinguishable from
# having synced successfully.
log.info('sync-map: skipped, the control panel does not support it')
def is_panel_supported(self):
"""
Check if current panel supported for reseller's limits;
:rtype: bool
"""
try:
return cpapi.is_reseller_limits_supported()
except NotSupported:
return False
def reseller_limit_supported(self):
"""
Check present all needed (kmod-lve, liblve, /proc/lve, panel) for manipulate resellers limits
"""
return all((self.py.resellers_supported(),
self.proc.resellers_supported(),
self.is_panel_supported()))
def is_lve10(self):
"""
Check present all needed (kmod-lve, liblve, /proc/lve) for manipulate resellers limits
"""
return all((self.py.resellers_supported(), self.proc.resellers_supported()))