Path : /proc/thread-self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/clsummary/
File Upload :
Current File : //proc/thread-self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/clsummary/net_acct.py

# coding=utf-8
#
# 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
"""
Telemetry collector for LVE Traffic Accounting (CLOS-4341).

Parses /proc/lve/list and produces a small set of metrics that let us answer:
- does this kernel expose net accounting at all (NETO/NETI columns present)?
- how many user LVEs exist and how many actually accumulated traffic?
- what's the cumulative NETO/NETI volume across user LVEs on this host?

/proc/lve/list contains two non-user LVE rows we exclude from all aggregates:
- *default* (user_id = UINT_MAX) — kernel catch-all for processes not
  attached to any specific LVE (sshd, systemd, dnf, ...). Has non-zero
  traffic on essentially every running CL host.
- *root* (user_id = 0) — LVE container for root (uid 0). Not a hosting user.

After exclusion the metrics describe user hosting activity, and lves_total
matches the count seen by `lvectl list` and the panel's user list.
"""

import os
from typing import Dict, Iterable, Optional

PROC_LVE_LIST = "/proc/lve/list"

# user-id slots that don't represent hosting customers and are excluded from
# all per-LVE aggregates: default catch-all bucket and root.
NON_USER_LVE_IDS = frozenset({0, 0xFFFFFFFF})  # 0 = root, UINT_MAX = default

METRIC_NAMES = (
    "net_acct_kernel_supported",
    "net_acct_lves_total",
    "net_acct_lves_with_traffic",
    "net_acct_total_neto_bytes",
    "net_acct_total_neti_bytes",
)


def _user_id_excluded(lve_id_field: str, excluded: Iterable[int]) -> bool:
    """True if the LVE row's user_id is in the excluded set.

    LVE ids in /proc/lve/list use the form "<lvp_id>,<user_id>" (e.g. "0,1002")
    or just "<user_id>" on older kernels. Malformed rows are excluded.
    """
    user_id = lve_id_field.rsplit(",", 1)[-1]
    try:
        return int(user_id) in excluded
    except ValueError:
        return True


def _empty_result(supported: int = 0) -> Dict[str, int]:
    return {
        "net_acct_kernel_supported": supported,
        "net_acct_lves_total": 0,
        "net_acct_lves_with_traffic": 0,
        "net_acct_total_neto_bytes": 0,
        "net_acct_total_neti_bytes": 0,
    }


def parse_proc_lve_list(
    content: str,
    skip_user_ids: Optional[Iterable[int]] = None,
) -> Dict[str, int]:
    """Parse /proc/lve/list text and return the 5 net_acct metrics.

    Format of /proc/lve/list:
        <version>:<TAB>LVE<TAB>...<TAB>NETO<TAB>NETI    # header
        <lve_id><TAB>...<TAB><neto><TAB><neti>          # one row per LVE

    The leading "<version>:" prefix is optional/version-dependent; we tolerate
    its presence and absence. NETO/NETI are cumulative byte counters; their
    lowercase counterparts lNETO/lNETI are limit columns and ignored here.

    skip_user_ids — additional user-id slots to exclude beyond the standard
    root/default. Used by cloudlinux-summary to skip its own transient
    self-LVE (created via _run_self_in_lve) which would otherwise inflate
    counts by 1 on every collection run.

    On any structural problem (no header, missing NETO/NETI columns, malformed
    rows) we treat the kernel as not supporting net accounting and return a
    zeroed result.
    """
    if not content:
        return _empty_result()

    lines = content.splitlines()
    if not lines:
        return _empty_result()

    header = lines[0]
    # Drop optional "<version>:" prefix that lvectl-format headers carry.
    if ":" in header:
        header = header.split(":", 1)[1]

    columns = header.split("\t")
    try:
        neto_idx = columns.index("NETO")
        neti_idx = columns.index("NETI")
    except ValueError:
        return _empty_result()

    excluded_ids = set(NON_USER_LVE_IDS)
    if skip_user_ids:
        excluded_ids.update(skip_user_ids)

    total = 0
    with_traffic = 0
    sum_neto = 0
    sum_neti = 0

    for raw in lines[1:]:
        if not raw.strip():
            continue
        fields = raw.split("\t")
        if len(fields) <= max(neto_idx, neti_idx):
            continue
        if _user_id_excluded(fields[0], excluded_ids):
            continue
        try:
            neto = int(fields[neto_idx])
            neti = int(fields[neti_idx])
        except ValueError:
            continue

        total += 1
        if neto > 0 or neti > 0:
            with_traffic += 1
        sum_neto += neto
        sum_neti += neti

    return {
        "net_acct_kernel_supported": 1,
        "net_acct_lves_total": total,
        "net_acct_lves_with_traffic": with_traffic,
        "net_acct_total_neto_bytes": sum_neto,
        "net_acct_total_neti_bytes": sum_neti,
    }


def collect_net_acct_metrics(
    path: str = PROC_LVE_LIST,
    skip_user_ids: Optional[Iterable[int]] = None,
) -> Dict[str, int]:
    """Read /proc/lve/list and return parsed metrics.

    Missing file (older kernels, non-CL kernels, Ubuntu without lve) yields
    kernel_supported=0 and zeros for the rest.
    """
    if not os.path.exists(path):
        return _empty_result()
    try:
        with open(path, "r", encoding="utf-8", errors="replace") as fh:
            content = fh.read()
    except OSError:
        return _empty_result()
    return parse_proc_lve_list(content, skip_user_ids=skip_user_ids)