AlkantarClanX12

Your IP : 52.14.252.16


Current Path : /opt/cloudlinux/venv/lib64/python3.11/site-packages/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/lvectllib.py

# coding=utf-8
# Liblve functions lib

#
# 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 contextlib
import copy
import errno
import json
import math
import mmap
import os
import pwd
import re
import subprocess
import sys
import syslog
import warnings
import xml.dom.minidom as xml
from builtins import range
from functools import partial
from mmap import PAGESIZE
from typing import Dict, List, Optional, Text, Tuple, TypedDict  # NOQA

import unshare

import clcommon
import cldetectlib
import lveapi
from clcommon.const import Feature
from clcommon.cpapi import admins, get_main_username_by_uid, is_panel_feature_supported, reseller_users
from clcommon.cpapi.cpapiexceptions import EncodingError
from clcommon.lock import acquire_lock
from clcontrollib import detect_panelclass
from clevents import reseller_limits_disabled_post, reseller_limits_enabled_post
from cllimits.lib import exec_utility
from cllvectl.log import get_subprocess_logger
from clveconfig.ve_config import BadVeConfigException, get_xml_config, save_xml
from clveconfig.ve_lock import LockFailedException, setup_global_lock
from lveapi import LVP_XML_TAG_NAME, Lve, NameMap, PyLve, PyLveError
from secureio import create_dir_secure, write_file_via_tempfile

GET_CP_PACKAGE_SCRIPT = '/usr/bin/getcontrolpaneluserspackages'

CPUINFO = '/proc/cpuinfo'
CORE_WEIGHT = 10000
DEFAULT_PACKAGE = "VE_DEFAULT"

NOIOPS = False

UMOUNT = '/bin/umount'
EXCLUDE_MOUNTS_CONF = '/etc/container/exclude_mounts.conf'

MULTI_FORMAT = 'multi'
SINGLE_FORMAT = 'single'

IS_DEBUG = int(os.environ.get('PYLVE_DEBUG', 0))

XML_PLESK_ID = 'plesk_id'   # XML attribute name to bind package name in the ve.cfg with plesk DB id

if not is_panel_feature_supported(Feature.LVE):
    pylve = None
    lve = None
else:
    pylve = PyLve(debug=IS_DEBUG)
    lve = Lve(py=pylve)


class LiblveSettings(TypedDict):
    ls_cpu: int | str
    ls_cpus: int
    ls_io: int
    ls_enters: int
    ls_memory_phy: int
    ls_nproc: int
    ls_iops: int


def create_liblve_settings(**kwargs) -> LiblveSettings:
    defaults: LiblveSettings = {
        'ls_cpu': 0,
        'ls_cpus': 0,
        'ls_io': 0,
        'ls_enters': 0,
        'ls_memory_phy': 0,
        'ls_nproc': 0,
        'ls_iops': 0
    }
    defaults.update(kwargs)
    return defaults


def lvp_list():
    """Helper function for easy mocking in unittests"""
    if lve.reseller_limit_supported():
        return lve.proc.lvp_id_list()
    return []


def get_active_resellers():
    """
    Get list of resellers with activated reseller limits

    :return: list of pairs (name, uid)
    """
    name_map = NameMap()
    name_map.link_xml_node()
    return name_map.load_from_node()


def is_active_reseller_limits(reseller_name):
    """
    Check whether giver reseller has activated reseller limits or not
    :return: bool
    """
    return reseller_name in (name for name, uid in get_active_resellers())


# TODO: py3 move it to cllib/long script
def raise_cpanel_encoding_error(e: EncodingError):
    """
    Since cPanel user can corrupt config file for some user with wrong encodings,
    we want to notify him that he should fix encoding problems with the link to documentation.
    Print error message and exit with code 1 or raise given exception if it isn't cPanel.
    :return: None
    """
    if not cldetectlib.is_cpanel():
        raise e

    if JSON:
        json_format('multi',
                    ['ERROR', str(e)])
    else:
        print(e)
    sys.exit(1)


def get_global_lock(write=False):
    """
    ~~~~~~~~~~~~~~~~~~
    !!! DEPRECATED !!!
    ~~~~~~~~~~~~~~~~~~
    Please, use setup_global_lock instead if possible

    Wrapper over setup_global_lock. If lock cannot be set,
    it will write message and close app

    The only reason why it is here is legacy function
    check_result_and_exit that we use in TWO places
    :type write: bool
    :return: Nothing
    """
    try:
        setup_global_lock(write)
    except LockFailedException:
        check_result_and_exit(1, 'can`t get lock')


def check_result_and_exit(result, message):
    # on cl5 some func is unimplemented; so ENOSYS is not error;
    if result not in (0, -errno.ENOSYS):
        if JSON:
            json_format(MULTI_FORMAT, ['ERROR', f'lvectl: {message}'])
        else:
            print(f'lvectl: Error: {message}')
        sys.exit(result)


# Default parameters for lve
LVE_DEFAULT = {
    'cpu': 25,
    'ncpu': 1,
    'io': 25,
    'ep': 20,
    'mem': 0,
    'pmem': 262144,
    'nproc': 0,
    'iops': 1024

}
MEM_DEFAULT_CL5 = 262144
# Default parameters for lvp
LVP_DEFAULT = {
    'cpu': 100,
    'ncpu': 1,
    'io': 0,
    'ep': 0,
    'mem': 0,
    'pmem': 0,
    'nproc': 0,
    'iops': 0
}

LIMITS_LIST_NAME = ['ncpu', 'cpu', 'io', 'mem', 'pmem', 'nproc', 'iops', 'ep']

LVE_VERSION = 4
JSON = False
BYTES_FLAG = False


# defined structures for liblve and turples for functions
lve_settings = ''
setup_data = ''  # type: dict

# dict with user-packages relations
# keys = int UID or str package name
# data = string package name
packages_users = {}


# defined ve.cfg variables
ve_cfg = ''
ve_lveconfig = ''
ve_default = ''
ve_lve = ''
ve_lvp = ''  # for resellers limits
ve_defaults = ''  # type: dict
ve_package = ''
ubc = 'false'  # TODO: looks like not used anymore, check and remove it
ve_enter_by_name = ''
ve_binary = ''
ve_cfg_version = ''


# Set JSON if json output required
def set_json(json_flag):
    global JSON
    JSON = json_flag


def set_bytes(bytes_flag):
    global BYTES_FLAG
    BYTES_FLAG = bytes_flag


def get_fields():
    if NOIOPS and LVE_VERSION == 8:
        version = 'noiops_8'
    elif LVE_VERSION == 8:
        version = '8'
    elif LVE_VERSION == 6:
        version = '6'
    else:                       # LVE_VERSION == 4
        version = '4'
    fields = {
        'noiops_8': ['ID','SPEED','PMEM','VMEM','EP','NPROC','IO'],
        '8': ['ID','SPEED','PMEM','VMEM','EP','NPROC','IO','IOPS'],
        '6': ['ID','SPEED','PMEM','VMEM','EP','NPROC','IO'],
        '4': ['ID','SPEED','VMEM','EP','IO']
    }[version]
    if JSON:
        speed_idx = fields.index('SPEED') + 1
        return (fields[:speed_idx] + ["CPU"] + fields[speed_idx:])
    return fields


# Create structure
def init(lve_ver=None):
    global LVE_VERSION
    if lve_ver is None:
        lve_ver = clcommon.get_lve_version()
        if lve_ver[0] is None:
            raise RuntimeError('get_lve_version failed')
        LVE_VERSION = lve_ver[0]
    else:
        LVE_VERSION = lve_ver
    global lve_settings

    lve_status = pylve.initialize()
    if not lve_status:
        raise RuntimeError('init_lve() failed.')

    lve_settings = pylve.liblve_settings()


# we use /proc/cpuinfo to get cpu speed, but unfortunately
# it returns current CPU MHZ, which is different in lve environment
# and we cannot get right speed value there
# FIXME: LU-947
def _get_cpu_data_from_env():
    """Get cpu information from environment veriable"""
    packed_cpu_data = os.environ.get('CPU_DATA')
    if packed_cpu_data is None:
        return None

    try:
        return json.loads(packed_cpu_data)
    except (TypeError, ValueError) as e:
        print('Invalid environment variable \'CPU_DATA\' format', str(e))
        sys.exit(1)


def get_cpu_data():
    """
    Parse /proc/cpuinfo

    return [NumProc, frequency in MHZ]

    """

    cpuinfo = {}
    procinfo = {}
    nprocs = 0
    try:
        # f = open(CPUINFO, 'r')
        with open(CPUINFO, 'r', encoding='utf-8') as f:
            for line in f:
                if not line.strip():
                    # end of one processor
                    cpuinfo[f'proc{nprocs}'] = procinfo
                    nprocs = nprocs + 1
                    # Reset
                    procinfo = {}
                else:
                    if len(line.split(':')) == 2:
                        procinfo[line.split(':')[0].strip()] = line.split(':')[1].strip()
                    else:
                        procinfo[line.split(':')[0].strip()] = ''
    except IOError:
        print(f'lvectl: Error: Can`t open {CPUINFO}.')
        sys.exit(1)

    return [nprocs, cpuinfo['proc0']['cpu MHz']]


# It's extremely rare case when CPU changes at runtime so we will use cached
CPUINFO_DATA = _get_cpu_data_from_env() or get_cpu_data()


def convert_from_old_cpu(data, lncpu=0):
    """
    Try converting to kernel format from old CPU format (percentage of whole
    cpu) and optionally the NCPU format. Return whichever is less.

    :param data: string presumably in old CPU format
    :param lncpu: integer number of cores limit
    """
    data = str(data)
    lncpu = lncpu or 0
    cpu_data = CPUINFO_DATA
    ncpu = int(cpu_data[0])
    cpu_percent = re.match(r'\d{1,2}0?$', data)  # 0-100
    if cpu_percent is not None:
        data = int(data)
        if 0 < data <= 100:
            from_cpu_limit = int(round(CORE_WEIGHT // 100 * ncpu * data))
            if lncpu == 0:
                return from_cpu_limit
            return min(lncpu * CORE_WEIGHT, from_cpu_limit)
    return None

def convert_from_speed_percent(data):
    """
    Try converting cpu limit from SPEED in percentage of one CORE format to
    kernel format.
    """
    data = str(data)
    cpu_data = CPUINFO_DATA
    ncpu = int(cpu_data[0])
    percent = re.match(r'\d+(?:\.\d+)?%$', data)  # *%
    if percent is not None:
        percent = float(data.replace('%', ''))
        if percent > ncpu * 100:
            percent = ncpu * 100
        if percent > 0:
            return int(round(CORE_WEIGHT // 100 * percent))
        return None
    return None

def convert_from_speed_hz(data):
    """
    Try converting cpu limit from SPEED in mhz/gzh format to kernel format.
    """
    data = str(data)
    cpu_data = CPUINFO_DATA
    ncpu = int(cpu_data[0])
    cpu_freq = float(cpu_data[1])
    pattern = re.compile(r'(?P<freq>\d+(?:\.\d+)?)(?P<suffix>mhz|ghz)+$', re.IGNORECASE)
    match = pattern.match(data)  # *mhz\ghz
    if match is not None:
        suffix = match.group('suffix')
        freq = float(match.group('freq'))
        if suffix.upper() == 'GHZ':
            freq = freq * 1000
        if freq > cpu_freq * ncpu:
            freq = cpu_freq * ncpu
        if freq > 0:
            return int(round(freq * CORE_WEIGHT / cpu_freq))
    return None

def convert_from_speed(data):
    """
    Try converting cpu limit value from either SPEED limit format
    (percentage of CORE or mhz/ghz) to kernel format.
    """
    return (
        convert_from_speed_percent(data)
        or convert_from_speed_hz(data)
    )

def convert_to_kernel_format(data, lncpu=0):
    """
    Convert different variants of cpu limit to kmod ver 8 variant

    :param data: Value in old CPU format or SPEED with % or mhz/ghz.
    :param lncpu: Limit in old NCPU format.
    :return: CPU limit in kmod ver 8+ format or None for bad format
    """
    from_cpu = convert_from_old_cpu(data, lncpu)
    if from_cpu is not None:
        return from_cpu
    from_speed_percent = convert_from_speed_percent(data)
    if from_speed_percent is not None:
        return from_speed_percent
    from_speed_hz = convert_from_speed_hz(data)
    if from_speed_hz is not None:
        return from_speed_hz

    return None


def speed_to_old_cpu(speed):
    """
    convert speed to old cpu format
    args: cpu limit in speed value
    return: old cpu limit format
    """
    cpu_data = CPUINFO_DATA
    nproc = int(cpu_data[0])
    speed = str(speed)
    if '*' in speed:
        return '*' + str(int(round(int(speed.lstrip('*')) // nproc)))
    return str(int(round(int(speed) // nproc)))


@contextlib.contextmanager
def temporary_lve(settings):
    # type: (pylve.liblve_settings) -> contextlib.GeneratorContextManager
    """
    Run subprocess in lve with pseudo-random id and given limits
    """
    pylve.initialize()
    lve_id = pylve.get_available_lve_id()
    try:
        pylve.lve_setup(lve_id, settings)
    except PyLveError:
        syslog.syslog(syslog.LOG_ALERT, f"Unable to setup lve with id {lve_id}, "
                                        "something is wrong, check dmesg for details")
        raise

    try:
        yield lve_id
    finally:
        pylve.lve_destroy(lve_id)


def make_liblve_settings(settings: LiblveSettings):
    # type: (LiblveSettings) -> pylve.liblve_settings
    """
    Just a nice user-friendly constructor of liblve_settings object
    You can pass the following ls_cpu and ls_cpus values:
    - in percents of one core (just ls_cpu='75%', ls_cpus will be ignored)
    - in old 'CPU' format (two arguments, ls_cpu and ls_cpus required, both int)
    """
    s = pylve.liblve_settings()
    s.ls_cpu = convert_to_kernel_format(settings['ls_cpu'], lncpu=settings['ls_cpus'])
    s.ls_io = settings['ls_io']
    s.ls_enters = settings['ls_enters']
    s.ls_nproc = settings['ls_nproc']
    s.ls_iops = settings['ls_iops']
    # convert memory from bytes to mempages
    s.ls_memory_phy = int(math.ceil(1. * settings['ls_memory_phy'] / PAGESIZE))
    return s


def get_ve_lve_user_uid(ve_lve_element):
    user_uid = str(ve_lve_element.getAttribute('id'))
    if not user_uid:
        user_name = ve_lve_element.getAttribute('user')
        user_uid = pwd.getpwnam(user_name).pw_uid
    return int(user_uid)


def json_format(error_type, data, extensions=None):
    """
    Print output in json as:
    {"status": "ERROR/OK", "msg": "Some Message", "ext1": "foo", "ext2": "bar"}
    where "status" and "msg" field are mandatory

    :param str error_type: Either MULTI_ERROR or SINGLE_ERROR
    :param list data: List with a status string and a message string
    :param dict extensions: Some additional fields for the final json object
    :return: None
    """
    result = {'status': str(data[0])}
    if error_type == MULTI_FORMAT:
        result['msg'] = str(data[1])
    if extensions is not None:
        result.update(extensions)
    print(json.dumps(result))


def check_def_value(xml, ve_defaults, ve_cfg, val, default):
    try:
        ve_defaults[val] = int(ve_default.getElementsByTagName(val)[0].getAttribute('limit'))
    except (ValueError, IndexError, TypeError):
        ve_defaults[val] = default[val]
        node = ve_cfg.createElement(val)
        node.setAttribute('limit',str(default[val]))
        try:
            xml.appendChild(node)
        except Exception:
            pass


def xml_filter_tag(node, tag):
    return [_ for _ in node.childNodes if isinstance(_, xml.Element) and _.tagName == tag]


def xml_filter_first(node, tag, attr=None, attr_val=None):
    for child_node in xml_filter_tag(node, tag):
        if attr is not None and not child_node.hasAttribute(attr):
            continue
        if attr_val is not None and child_node.getAttribute(attr) != attr_val:
            continue
        return child_node


def get_child_tag_atrr(node, tag, attr):
    filtered_child_node = xml_filter_first(node=node, tag=tag, attr=attr)
    if filtered_child_node is None:
        raise IndexError()
    return filtered_child_node.getAttribute(attr)


def set_child_tag_atrr(node, tag, attr, val):
    """
    Find in children nodes node with tag and setup attribute
    insted el.getElementsByTagName not search recursiveli in tree
    """
    first_child_node = xml_filter_tag(node, tag)[0]
    first_child_node.setAttribute(attr, str(val))


def _load_config_wrapper():
    """Load config from ve.cfg"""
    global ve_cfg
    global ve_lveconfig

    try:
        ve_cfg, ve_lveconfig = get_xml_config()
    except BadVeConfigException as e:
        if JSON:
            json_format(MULTI_FORMAT, ['ERROR', str(e)])
        else:
            print(str(e))
        sys.exit(1)


def _load_default_limits(lvp_id: int, lvp_defaults: bool):
    """Load default limits

    :param int lvp_id: lvp id
    :param bool lvp_defaults: load reseller's default limits instead of global
    :return: dict with default limits
    """
    global ve_default
    global ubc

    ubc = 'true'

    try:
        if not lvp_id:
            ve_default = xml_filter_tag(ve_lveconfig, 'defaults')[0]
            return LVE_DEFAULT

        if lvp_defaults:
            ve_default = ve_cfg.createElement('defaults')
            return LVP_DEFAULT

        defaults_root_node = xml_filter_first(ve_lveconfig, LVP_XML_TAG_NAME, 'id', str(lvp_id))
        if defaults_root_node:  # if no such reseller with lvp in config
            ve_default = defaults_root_node.getElementsByTagName('defaults')[0]
        else:
            ve_default = ve_default.cloneNode(ve_default)
        return LVE_DEFAULT

    except IndexError:
        if JSON:
            json_format('multi', ['WARNING', 'default section error in ve.cfg'])
            sys.exit(1)
        else:
            print('warning: default section error in ve.cfg')

    return LVE_DEFAULT


def _all_config_elements_loaded():
    return all(x != '' for x in (ve_lve, ve_lvp, ve_package, ve_binary, ve_enter_by_name, ve_cfg_version))

def _load_config_elements():
    """Load all config elements from ve.cfg"""
    global ve_lve
    global ve_lvp
    global ve_package
    global ve_binary
    global ve_enter_by_name
    global ve_cfg_version

    ve_lve = ve_lveconfig.getElementsByTagName("lve")
    ve_package = ve_lveconfig.getElementsByTagName("package")
    ve_lvp = ve_lveconfig.getElementsByTagName(LVP_XML_TAG_NAME)
    lve.map.name_map.link_xml_node(ve_lveconfig)

    enter_by_name_elems = ve_lveconfig.getElementsByTagName('enter-by-name')
    if len(enter_by_name_elems) > 0:
        ve_enter_by_name = enter_by_name_elems[0]
    else:
        ve_enter_by_name = ve_cfg.createElement('enter-by-name')
        ve_lveconfig.appendChild(ve_enter_by_name)

    ve_binary = ve_enter_by_name.getElementsByTagName('binary')

    # expected version tag in next format
    #
    # <lveconfig>
    #   <version>2</version>
    #   <system>
    #   ....
    #   </system>
    #   ....
    # </lveconfig>

    cfg_version_elems = ve_lveconfig.getElementsByTagName('version')
    ve_cfg_version = int(cfg_version_elems[0].firstChild.nodeValue) if len(cfg_version_elems) > 0 else 1


def _load_ve_defaults(default_limits: Dict[str, int]):
    """Create ve_defaults dict with default values for all limits

    :param dict default_limits: default limits for lve or lvp
    """
    global ve_cfg
    global ve_defaults
    global ve_default

    ve_defaults = {}
    check_def_val = partial(
        check_def_value,
        xml=ve_default,
        ve_defaults=ve_defaults,
        ve_cfg=ve_cfg,
        default=default_limits,
    )
    check_def_val(val='ncpu')

    try:
        speed = ve_default.getElementsByTagName('cpu')[0].getAttribute('limit')
        ve_defaults['cpu'] = convert_to_kernel_format(speed, lncpu=ve_defaults['ncpu'])
    except (ValueError, IndexError, TypeError):
        ve_defaults['cpu'] = convert_to_kernel_format(default_limits['cpu'], lncpu=ve_defaults['ncpu'])
        cpu = ve_cfg.createElement('cpu')
        cpu.setAttribute('limit', str(default_limits['cpu']))
        try:
            ve_default.appendChild(cpu)
        except Exception:
            pass

    try:
        ve_defaults['ep'] = int(ve_default.getElementsByTagName('other')[0].getAttribute('maxentryprocs'))
    except (ValueError, IndexError, TypeError):
        ve_defaults['ep'] = default_limits['ep']
        ep = ve_cfg.createElement('other')
        ep.setAttribute('maxentryprocs', str(default_limits['ep']))
        try:
            ve_default.appendChild(ep)
        except Exception:
            pass

    check_def_val(val='io')
    if LVE_VERSION > 5:
        check_def_val(val='mem')
    else:
        check_def_val(val='mem', default={'mem': MEM_DEFAULT_CL5})  # pylint: disable=redundant-keyword-arg
    check_def_val(val='pmem')
    check_def_val(val='nproc')
    check_def_val(val='iops')

    _check_defaults_for_nones()


def _check_defaults_for_nones():
    """Check that all default values are not None"""
    global ve_defaults

    for key, value in ve_defaults.items():
        if value is not None:
            continue

        err_msg = f'ERROR: Incorrect {key} default value'
        if JSON:
            json_format('multi', ['ERROR', err_msg])
        else:
            sys.stderr.write(f'{err_msg}\n')
        sys.exit(1)


def get_XML_cfg(lvp_id=0, lvp_defaults=False, load_config_elements=True):
    """
    :param bool lvp_defaults: load reseller's default limits instead of global
    :param int lvp_id: lvp id to load customise defaults
    """

    _load_config_wrapper()
    default_limits = _load_default_limits(lvp_id, lvp_defaults)

    # load config elements if load_config_elements=True or they were not loaded before
    if load_config_elements or not _all_config_elements_loaded():
        _load_config_elements()

    _load_ve_defaults(default_limits)


def check_value(val, el, ve_defaults, setup_data):
    try:
        value = int(get_child_tag_atrr(el, tag=val, attr='limit'))
        setup_data[val] = value
        return value
    except (ValueError, IndexError, TypeError):
        return int(ve_defaults[val])


def _load_resellers_xml_data(reseller, xml_config_load_elements=True):
    """
    This function is a pure workaround for our ugly globals-based API which
    should be fixed partially with LU-496, because there is no clean way
    to retrieve reseller's data from ve.cfg without touching globals
    :param reseller: reseller name
    :return: Nothing. It just updates some globals
    """
    # TODO after LU-496 we should read and cache all reseller's settings
    name_map = lveapi.NameMap()
    name_map.link_xml_node()
    reseller_id = name_map.get_id(reseller)
    get_XML_cfg(reseller_id, load_config_elements=xml_config_load_elements)


def prepare_setup_data(plan_id=None, reseller=None, lve_id=None):
    # type: (Optional[Text], Optional[Text]) -> None
    """
    Put limit values that will be applied later in a global variable `setup_ve`.

    :param plan_id: package
    :param reseller:
        If reseller is None we only inherit from admin packages.
        In that case we ignore all tags in ve.cfg with a "reseller" attribute.
    """

    global setup_data
    setup_data = copy.copy(ve_defaults)
    if plan_id is not None:
        res_pkg_dict = get_reseller_packages_map()
        # LU-510: Investigate the problem with reseller's list, part 2.
        #   Fix applying limit for reseller user with admin package
        if reseller is not None and reseller in res_pkg_dict and plan_id in res_pkg_dict[reseller]:

            def is_needed_plan(el):
                return el.getAttribute('id') == plan_id and el.getAttribute('reseller') == reseller

        else:
            if cldetectlib.is_da() and lve_id is not None:
                try:
                    user_pwd = pwd.getpwuid(lve_id)
                    filename = f'/usr/local/directadmin/data/users/{user_pwd.pw_name}/user.conf'
                    with open(filename, encoding='utf-8') as f:
                        text = f.read()
                except Exception:
                    text = ''
                # LU-1663 --> LU-3410:
                #   it is normal situation for DA to set DA(not LVE) user package to "custom"
                #   in this case the package is still correct and this warning should not be logged
                if 'package=custom' not in text:
                    syslog.syslog(
                        syslog.LOG_ALERT,
                        f"Package for user with id {lve_id} is incorrect, please recover it using Note from "
                        "https://docs.cloudlinux.com/cloudlinux_os_components/#installation-enabling-and-disabling",
                    )

            # Ignore all tags in ve.cfg with a "reseller" attribute.
            def is_needed_plan(el):
                return el.getAttribute('id') == plan_id and not el.getAttribute('reseller')

        # Example command when `ve_package` is not empty:
        #   cloudlinux-packages set --json --for-reseller root --package kekage --pmem 994 --nproc 77
        # `ve_package` is set by reading ve.cfg in `get_XML_cfg`.
        # <package> tag is added to ve.cfg in `package_set_ext`.
        for el in ve_package:
            if is_needed_plan(el):
                lncpu = check_value('ncpu', el, ve_defaults, setup_data)
                try:
                    cpu = int(convert_to_kernel_format(get_child_tag_atrr(el, tag='cpu', attr='limit'), lncpu=lncpu))
                    setup_data['cpu'] = cpu
                except (ValueError, IndexError, TypeError):
                    pass
                check_value('io', el, ve_defaults, setup_data)
                if (ubc == 'true'):
                    check_value('mem', el, ve_defaults, setup_data)
                else:
                    setup_data['mem'] = 0
                try:
                    ep = int(get_child_tag_atrr(el, tag='other', attr='maxentryprocs'))
                    setup_data['ep'] = ep
                except (ValueError, IndexError, TypeError):
                    pass
                check_value('nproc', el, ve_defaults, setup_data)
                check_value('pmem', el, ve_defaults, setup_data)
                check_value('iops', el, ve_defaults, setup_data)


def umount_dir(path):
    try:
        # run the "umount" command and suppress it's output, return True when child exit code is not zero
        with subprocess.Popen(
            [UMOUNT, "-l", path],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        ) as proc:
            proc.communicate()
            return proc.returncode != 0
    except OSError:
        check_result_and_exit(-1, f'failed to run "{UMOUNT} -l {path}"')


def prepare_mounts():
    """
    Unmount all paths from /proc/mounts that match regular expressions from /etc/container/exclude_mounts.conf file
    """
    if not os.path.isfile(EXCLUDE_MOUNTS_CONF):
        return
    reg_exp_list = []
    try:
        with open(EXCLUDE_MOUNTS_CONF, 'r', encoding='utf-8') as conf:
            for r in conf:
                pattern = r.strip()
                if pattern:
                    reg_exp_list.append(re.compile(pattern))
    except IOError:
        check_result_and_exit(-1, f'failed to read {EXCLUDE_MOUNTS_CONF}')
    if not reg_exp_list:
        return
    unshare.unshare(unshare.CLONE_NEWNS)
    try:
        with open('/proc/mounts', 'r', encoding='utf-8') as f:
            mounts = [m.split()[1] for m in f.readlines()]
    except (IndexError, IOError):
        check_result_and_exit(-1, 'failed to parse /proc/mounts')
    ATTEMPTS = 10
    for _ in range(ATTEMPTS):
        error = False
        for mount in mounts:
            for reg_exp in reg_exp_list:
                m = reg_exp.search(mount)
                if m:
                    error = umount_dir(mount) or error
                    break
        if not error:
            break


def lve_start():
    """
    Start LVE engine and initialize default mount namespace for LVE
    """
    MOUNT_CMD = '/bin/mount --make-rprivate / >/dev/null 2>&1'
    try:
        subprocess.call(MOUNT_CMD, shell=True, executable='/bin/bash')
    except OSError:
        print('Error: failed to execute', MOUNT_CMD)
    prepare_mounts()
    pylve.lve_start(err_msg='Can`t init lve default settings')


def lve_create(lve_id, ignore_error=False):
    """
    Create LVE container for given ID
    :type lve_id: int
    :type ignore_error: bool
    :return: Nothing
    """
    pylve.lve_create(lve_id,
                     err_msg=f'lvectl: Can`t create lve with id {lve_id}; error code {{code}}',
                     ignore_error=ignore_error)


def lvp_create(lvp_id, ignore_error=False):
    """
    Create LVP container for given ID
    :type lvp_id: int
    :type ignore_error: bool
    :return: Nothing
    """
    pylve.lve_lvp_create(lvp_id,
                         err_msg=f'lvectl: Can`t create lvp with id {lvp_id}; error code {{code}}',
                         ignore_error=ignore_error)


def destroy_lvp_all():
    logger = get_subprocess_logger('lvectllib')

    destroyed_list = []
    for lvp_id in lvp_list():
        logger.debug('destroy_lvp_all: destroying LVP with id %s', lvp_id)
        pylve.lve_lvp_destroy(lvp_id,
                              err_msg=f'lvectl: Can`t destroy lvp with id {lvp_id}; error code {{code}}')
        destroyed_list.append(lvp_id)
    return destroyed_list


def lvp_destroy(lvp_id):
    logger = get_subprocess_logger('lvectllib')

    if lvp_id == 'all':
        destroy_lvp_all()
        return

    logger.debug('lvp_destroy: destroying LVP with id %s', lvp_id)
    pylve.lve_lvp_destroy(lvp_id,
                          err_msg=f'lvectl: Can`t destroy lvp with id {lvp_id}; error code {{code}}')


# Destroy LVE container for ID
def lve_destroy(lve_id):
    logger = get_subprocess_logger('lvectllib')

    cant_remove_msg = f'Can\'t remove lve {lve_id} from kernel - error code -3'
    cant_destroy_msg = f'Can`t destroy lve with id {lve_id}; error code {{code}}'
    destroyed = False

    if lve_id == 'all':
        if lve.reseller_limit_supported():
            destroyed = bool(destroy_lvp_all())  # destroy all top level containers
        if len(list(lve.proc.lve_id_list())) > 0:
            for id_ in lve.proc.lve_id_list():
                logger.debug('lve_destroy all: destroying LVE with id %s', id_)
                lve.lve_destroy(id_, err_msg=cant_destroy_msg)
                destroyed = True
        else:
            destroyed = True  # empty list - all lve already destroyed
    else:
        if lve.proc.check_inside_list(lve_id) or \
                (lve.proc.resellers_supported() and lve.proc.detect_inside_lvp(lve_id) is not None):
            logger.debug('lve_destroy: destroying LVE with id %s', lve_id)
            lve.lve_destroy(lve_id, err_msg=cant_destroy_msg)
            destroyed = True
        elif not lve.proc.check_inside_list(lve_id):
            destroyed = True  # lve_id doesn`t exist so it`s already destroy!

    if destroyed:
        if JSON:
            json_format(SINGLE_FORMAT, ['OK'])
    else:
        if JSON:
            json_format(MULTI_FORMAT, ['WARN', cant_remove_msg])
        else:
            print(f'warning: {cant_remove_msg}')


# Setup LVE for ID
def lve_setup(lve_id, lvp_id=0):
    if lvp_id and not lve.proc.exist_lvp(lvp_id):
        # create lve top container if not exist
        lvp_create(lvp_id)

    with warnings.catch_warnings():  # convert all warning to exceptions
        warnings.filterwarnings('error')
        try:
            lve_settings.ls_io = int(setup_data['io'])
            lve_settings.ls_cpu = int(setup_data['cpu'])
            lve_settings.ls_cpus = int(setup_data['ncpu'])
            lve_settings.ls_memory = int(setup_data['mem'])
            lve_settings.ls_enters = int(setup_data['ep'])

            if LVE_VERSION > 5:
                lve_settings.ls_memory_phy = int(setup_data['pmem'])
                lve_settings.ls_nproc = int(setup_data['nproc'])
            if LVE_VERSION > 6:
                lve_settings.ls_iops = int(setup_data['iops'])
            if lvp_id:
                if lve_id == 0:
                    pylve.lve_set_default(
                        lvp_id, lve_settings, err_msg=f'Can`t setup default settings for LVP {lvp_id}')
                else:
                    pylve.lve_lvp_setup(
                        lvp_id, lve_settings, err_msg=f'Can`t setup lvp with id {lvp_id}; error code {{code}}')
            elif lve_id == 0:
                pylve.lve_set_default(lve_settings, err_msg='Can`t setup default settings')
            else:
                pylve.lve_setup(
                    lve_id, lve_settings, err_msg=f'Can`t setup lve with id {lve_id}; error code {{code}}')

        except RuntimeWarning as rw:
            # exit if caught a warning - can`t set limits to lve
            check_result_and_exit(1,'Can`t setup lve ' + str(lve_id) + '. RuntimeWarning excepted: ' + str(rw))


def get_package_and_reseller_by_lve_id(lve_id):
    """
    Get pair of package, reseller for lve_id
    :param lve_id: lve_id, UID with package, reseller
    :return: tuple of (package, reseller); Both can be None
    """
    global packages_users
    package = None
    reseller = None
    # hate this! backup global ref to packages_users
    old_packages_users = packages_users
    GetControlPanelUsers('list-users')
    # now packages_users should be dict of dicts:
    # {lve_id : {'package' : , 'reseller':}}
    temp_package = packages_users
    # restore global ref to packages_users
    packages_users = old_packages_users
    if isinstance(temp_package, dict):
        try:
            reseller = temp_package[lve_id]['reseller']
            # if reseller is empty then reseller is root/admin. return to None
            if not reseller:
                reseller = None
        except KeyError:
            reseller = None
        try:
            package = temp_package[lve_id]['package']
            # if package is empty  - return to undefined state
            if not package:
                package = None
        except KeyError:
            package = None

    return package, reseller


# (rprilipskii): Honestly, relying on global variables for storage like this leaves a bad
# taste in my mouth, but I'd have to rewrite a more lvectl code than I'd prefer if I
# wanted to do it in a more pythonic way. Technical debt is a problem.
CACHED_EFFECTIVE_LIMITS = {}
EFFECTIVE_CACHE_FILE = "/var/run/cloudlinux/effective-normal-limits"
BURSTABLE_LIMITS_FLAG_FILE = "/opt/cloudlinux/flags/enabled-flags.d/burstable-limits.flag"

def write_effective_cache(reset=False):
    """
    Save calculated effective normal limits for an LVE to a cache file.

    The cache file is stored in /var/run (tmpfs) for faster operations.
    It disappears on reboot, but lvectl apply all runs in the lvectl systemd service anyway,
    so the file will always reappear if the service functions normally.

    If additional speed is desired, consider replacing the standard JSON lib
    with a faster one like rapidjson or orjson.

    :param reset: If True, recreate the cache file from scratch with data available in the dict,
    instead of updating it, defaults to False
    :type reset: bool, optional
    """
    # NOTE: When running `lvectl apply all`, this write will happen at the very end
    # of the operation. Applying the limits, however, happens before this - right after
    # they're calculated.
    # This means a substantial time gap and a potential race condition if someone calls
    # `lvectl set` while `apply all` is not yet finished (e.g. cache and kernel having different limits).
    # Could be solved by makin limit application happen after all effective limits are
    # done calculating, and wrapping limit application and cache writing in the same filelock.

    # Do nothing if the corresponding feature flag is not set
    if not os.path.exists(BURSTABLE_LIMITS_FLAG_FILE):
        return

    # If there's no /var/run/cloudlinux directory, create it.
    try:
        effective_dir = os.path.dirname(EFFECTIVE_CACHE_FILE)
        if not os.path.isdir(effective_dir):
            var_run_dir = os.path.dirname(effective_dir)
            create_dir_secure(effective_dir, 0o600, 0, 0, var_run_dir)
    except OSError as e:
        print(f"Error: failed to create the folder {effective_dir}: {e}")
        raise

    # Load already cached effective limits and merge them with
    # the limits calculated during the current lvectl run.
    # Unless we want to discard them and start over, that is.
    effective_cache_lock = acquire_lock(f"{EFFECTIVE_CACHE_FILE}.lock")
    with effective_cache_lock:
        if reset:
            effective_limits = {}
        else:
            try:
                if os.path.isfile(EFFECTIVE_CACHE_FILE):
                    with open(EFFECTIVE_CACHE_FILE, "r", encoding="utf8") as readfile:
                        effective_limits = json.load(readfile)
                else:
                    effective_limits = {}
            except json.JSONDecodeError as e:
                print(f"Error: failed to parse the effective normal limit cache file {EFFECTIVE_CACHE_FILE}: {e}")
                raise
            except OSError as e:
                print(f"Error: failed to read the effective normal limit cache file {EFFECTIVE_CACHE_FILE}: {e}")
                raise
        effective_limits.update(CACHED_EFFECTIVE_LIMITS)

        try:
            write_file_via_tempfile(json.dumps(effective_limits), EFFECTIVE_CACHE_FILE, 0o600)
        except OSError as e:
            print(f"Error: failed to save the effective normal limit cache file {EFFECTIVE_CACHE_FILE}: {e}")


def cache_effective_limits(lve_id):
    """
    Cache the calculated effective normal limits for an LVE to a dictionary.

    setup_data is a global dict that contains the LVE configuration last applied.
    Holds only one entry - during operations like lvectl apply all, we enter this
    function repeatedly with setup_data containing info for one processed LVE each time.

    These limits can be used by the Burstable Limits components
    to set/reset burst or normal limits without having to go
    through effective limit calculation or invoking lvectl as a middleman.
    Comparing the cached limits to current active limits (in kernel)
    also shows whether or not the LVE has burst limits active at the moment.

    This function gets called in lve_apply, which means it also runs within:
    * lvectl apply all
    * lvectl apply-many
    * lvectl set
    * and others, that also call lve_apply

    :param lve_id: LVE ID for which the limits are being saved.
    :type lve_id: int
    """
    global CACHED_EFFECTIVE_LIMITS
    CACHED_EFFECTIVE_LIMITS[str(lve_id)] = setup_data


# pylint: disable-msg=too-many-arguments
# Apply user settings
def lve_apply(lve_id, plan_id=None, result=False, reseller=None, out_node=None, lvp_id=0):
    """
    Aplly limits to LVE lve_id
    :param lve_id: lve id
    :type lve_id: int
    :param plan_id: package for user with lve_id. deprecated
    :type plan_id: string
    :param result: if True = don't apply limits. only create setup_data with actual limits
    :type result: boolean
    :param reseller: if True = plan_id is resellers plan. deprecated
    :type reseller: boolean
    :param out_node: node with limits for lve_id
    :type out_node: xml_node
    :param lvp_id: reseller container id; host container if 0
    """
    global setup_data
    global packages_users

    old_packages_users = packages_users
    if lve_id != 0:
        GetControlPanelUsers('userid', lve_id)
        new_packages_users, packages_users = packages_users, old_packages_users

    if ve_cfg == '':
        get_XML_cfg(lvp_id=lvp_id)

    el = None
    if out_node is not None:
        el = out_node
    else:
        node_list = ve_lvp if lvp_id else ve_lve
        el = next(
            filter(lambda node: get_ve_lve_user_uid(ve_lve_element=node) == lve_id, node_list),
            None
        )

    # reseller (lvp_id!=0) should not use package and default limits
    if lvp_id == 0:
        try:
            plan_id = new_packages_users[lve_id]['package']
        except (NameError, KeyError):
            plan_id = None

        try:
            reseller = new_packages_users[lve_id]['reseller']
        except (NameError, KeyError):
            reseller = None

    if el is not None:
        # get limits from package
        prepare_setup_data(plan_id, reseller=reseller)

        # prepare custom limits for lve
        lncpu = check_value('ncpu', el, ve_defaults, setup_data)
        try:
            setup_data['cpu'] = convert_to_kernel_format(get_child_tag_atrr(el, tag='cpu', attr='limit'), lncpu=lncpu)
        except (ValueError, IndexError, TypeError):
            pass
        if setup_data['cpu'] is None:
            setup_data['cpu'] = ve_defaults['cpu']

        check_value('io', el, ve_defaults, setup_data)

        if ubc == 'true':
            check_value('mem', el, ve_defaults, setup_data)
        else:
            setup_data['mem'] = 0

        try:
            setup_data['ep'] = int(get_child_tag_atrr(el, tag='other', attr='maxentryprocs'))
        except (ValueError, IndexError, TypeError):
            pass

        check_value('nproc', el, ve_defaults, setup_data)
        check_value('pmem', el, ve_defaults, setup_data)
        check_value('iops', el, ve_defaults, setup_data)

    else:
        # apply default limits
        prepare_setup_data(plan_id, reseller=reseller, lve_id=lve_id)
        if ubc == 'false':
            setup_data['mem'] = 0

    if not result:
        cache_effective_limits(lve_id)
        # apply limits
        lve_setup(lve_id, lvp_id=lvp_id)


def _pprint(*fields):
    """
    Print data with the last column 30 symbols wide.
    Useful for printing data that contains package names.
    """
    formatted_string = _format_fields(fields, wide_indices=[len(fields) - 1])
    print(formatted_string)


def _pprint_f(*fields):
    """
    Print data with the two last columns 30 symbols wide.
    Useful for printing full data of every user with package name
    and reseller name.
    """
    formatted_string = _format_fields(
        fields,
        wide_indices=[len(fields) - 2, len(fields) - 1],
    )
    print(formatted_string)


def _pprint_p(*fields):
    """
    Print data with the first column 30 symbols wide.
    Useful for printing packages data.
    """
    formatted_string = _format_fields(fields, wide_indices=[0])
    print(formatted_string)


def _pprint_r(*fields):
    """
    Print data with the first and last columns 30 symbols wide.
    Useful for printing data with user names and package names.
    """
    formatted_string = _format_fields(fields, wide_indices=[0, len(fields) - 1])
    print(formatted_string)


def _format_fields(fields: tuple, wide_indices: list, width: int = 30) -> str:
    """
    Helper function to format fields
    based on specified indices for wide columns.

    Args:
        fields: The fields to format.
        wide_indices: List of indices in the fields that should be wide.
        width: The width of the wide columns.

    Returns:
        A formatted string with specified fields widened.
    """
    formatted_fields = []

    for index, field in enumerate(fields):
        # Convert field to string to ensure compatibility with formatting
        field_str = str(field)

        if index in wide_indices:
            formatted_fields.append(f"{field_str:>{width}}")
        else:
            formatted_fields.append(f"{field_str:>8}")

    return ''.join(formatted_fields)


def _pmem_vmem_to_bytes_value(value):
    """
    Convert pmem or vmem limits to bytes value

    :param value: pmem or vmem limits in kbytes value
    :return: bytes value of limit

    """
    # if value was changed we remove asterisk from value for counted this
    value = str(value)
    was_changed = isinstance(value, str) and value.startswith('*')
    value = value.replace('*', '')
    if value:
        value = int(value)
        value *= 4096
    else:
        value = 0
    # return asterisk in value if this was changed
    value = f'*{value}' if was_changed else value
    return value


def _mb_mem(value):
    """
    Convert amount of RAM to M format

    :param string value: amount of memory in KB
    :rtype: string
    :return: amount of memory in MB like "1234M"
    """
    result = ''
    was_changed = False
    if isinstance(value, str) and value.startswith('*'):
        value = value[1:]
        was_changed = True
    try:
        v = int(value)
    except ValueError:
        return ""
    if was_changed:
        result = '*'
    value = v * 4 // 1024
    if value > 0:
        result = f'{result}{value}M'
    else:
        result = f'{result}{v * 4}K'
    return result


def _formatter(printer, default_id="0", default_package="default", more_fields=None):
    """
    Generate header and default package data either as print to stdout or as json string
    """
    defaults = ve_defaults.copy()
    #  convert from kernel format for output
    defaults['cpu'] = defaults['cpu'] // 100

    def get_data(key):
        return defaults.get(key, '')

    _cpu = speed_to_old_cpu(get_data("cpu")) if get_data("cpu") != '' else ''

    def convert_mem_limits(value):
        return _pmem_vmem_to_bytes_value(value) if BYTES_FLAG else _mb_mem(value)

    fields_map = {
        'ID': default_id, 'SPEED': str(get_data('cpu')),
        'CPU': str(_cpu), 'NCPU': str(get_data('ncpu')),
        'PMEM': str(convert_mem_limits(get_data('pmem'))), 'VMEM': str(convert_mem_limits(get_data('mem'))),
        'EP': str(get_data('ep')), 'NPROC': str(get_data('nproc')), 'IO': str(get_data('io')),
        'IOPS': str(get_data('iops')), 'PACKAGE': default_package
    }
    res = []
    fields = get_fields()
    if more_fields is not None:
        fields += more_fields
    if JSON:
        line = ','.join(f'"{f}":"{fields_map.get(f, "")}"' for f in fields)
        res = [f'{{{line}}}']
    else:
        printer(*fields)
        printer(*[fields_map.get(f, "") for f in fields])
    return res


def _user_formatter(fields, printer=_pprint):
    """
    Generate inner function with closured fields names and printer function

    :param list fields: List of strings that represent names of fields in final output
    :param callable printer: Function to format and print data for every entry
    :rtype: callable
    :return: function to format data for every user
    """
    def wrapper(user):
        """
        :param string user: Find and format data for this User ID
        :rtype: list
        :return: List of given user's statistics data line or empty list
        """
        data = ''
        package = packages_users[user]["package"]
        reseller = packages_users[user]["reseller"]
        if reseller == '':
            reseller = None
        if ve_cfg_version <= 1:
            # Reseller's default limits will not be inherited by its end-users.
            # Backward compatibility - show some reseller packages in
            # paneluserslimits.
            prepare_setup_data(package, reseller=None)
        else:
            if reseller is not None:
                # We can set xml_config_load_elements=False
                # because get_XML_cfg was called with True before _user_formatter called
                _load_resellers_xml_data(reseller, xml_config_load_elements=False)
            else:
                # It's important to re-read admin's limits here or we will use
                # limits from previous reseller for next users in list
                # We can set xml_config_load_elements=False
                # because get_XML_cfg was called with True before _user_formatter called
                get_XML_cfg(load_config_elements=False)
            prepare_setup_data(package, reseller=reseller)
        data = copy.copy(setup_data)        # only after reading reseller's xml
        lve_apply(user, plan_id=package, reseller=reseller, result=True)

        def check_changed(key):
            return '*' + str(setup_data[key]) if str(data[key]) != str(setup_data[key]) else str(data[key])

        def convert_mem_limits(value):
            return _pmem_vmem_to_bytes_value(value) if BYTES_FLAG else _mb_mem(value)

        data['id'] = str(user)
        data['cpu'] = str(check_changed('cpu'))
        data['ncpu'] = str(check_changed('ncpu'))
        data['pmem'] = str(convert_mem_limits(check_changed('pmem')))
        data['vmem'] = str(convert_mem_limits(check_changed('mem')))
        data['ep'] = str(check_changed('ep'))
        data['io'] = str(check_changed('io'))
        data['nproc'] = str(check_changed('nproc'))
        data['iops'] = str(check_changed('iops'))
        if JSON:
            data['package'] = _normalize_str(package)
        else:
            data['package'] = package
        if reseller is None:
            data['reseller'] = 'N/A' if JSON else ''
        else:
            data['reseller'] = reseller

        if '*' in data['cpu']:
            data['cpu'] =   '*' + str(int(data['cpu'].lstrip('*')) // 100)
        else:
            data['cpu'] = int(data['cpu']) // 100
        data['speed'] = data['cpu']
        data['cpu'] = str(speed_to_old_cpu(data['speed']))

        res = []
        if JSON:
            line = ','.join(f'"{f}":"{data[f.lower()]}"' for f in fields)
            res = [f'{{{line}}}']
        else:
            printer(*[data[f.lower()] for f in fields])
        return res
    return wrapper


# Show current user's limits for control panel
# 'lvectl paneluserslimits' or 'lvectl paneluserlimits lve_id'
def paneluserslimits(userid=None, reseller=None):
    get_XML_cfg()
    try:
        # create cache for userid_calls
        GetControlPanelUsers('list-users')
        # use explicit compare, because userid may be zero!
        # if userid == 0, then show only default limits
        # LU-374
        if userid is not None and userid:
            GetControlPanelUsers('userid', userid)
        # LU-530
        elif reseller is not None:
            GetControlPanelUsers('list-reseller-users', reseller=reseller)
        else:
            GetControlPanelUsers('list-users')
    except Exception:
        pass
    more_fields = ["PACKAGE"]
    result = _formatter(_pprint, more_fields=more_fields)
    fields = get_fields() + more_fields
    formatter = _user_formatter(fields)
    for user in packages_users:
        result += formatter(user)
    if JSON:
        print('{"data":[' + ','.join(result) + ']}')


def paneluserslist():
    # type: () -> List[Tuple[int, str, str]]
    """Get list of tuples[lve_id, reseller, package] from control panel"""
    GetControlPanelUsers('list-users')
    result = []
    for str_uid, payload in packages_users.items():
        result.append((int(str_uid), payload['reseller'], payload['package']))
    return result


def panelpackagesdict():
    # type: () -> Dict[str, List[str]]
    """Get dict of pairs[provider, list[package_name]] from control panel"""
    from clveconfig import DEFAULT_PROVIDER  # NOQA
    packages = {}

    GetControlPanelUsers('list-packages')
    # admin's packages are already in bytes...
    packages[DEFAULT_PROVIDER] = list(packages_users.keys())
    GetControlPanelUsers('list-resellers-packages')
    # ..but we must convert reseller's package to bytes, because cl-summary
    # expects bytes and print warnings about unicode comparison
    packages.update(packages_users)

    return packages


# lvectl all-user-list
def all_users_limits():
    """
    Implements lvectl all-user-list command

    :return: None, prints result to stdout
    """
    get_XML_cfg()
    GetControlPanelUsers('list-users')

    result = _formatter(_pprint_f, more_fields=["PACKAGE", "RESELLER"])
    fields = get_fields() + ["PACKAGE", "RESELLER"]

    formatter = _user_formatter(fields, printer=_pprint_f)
    for user in packages_users:
        result += formatter(user)

    if JSON:
        print('{"data":[' + ','.join(result) + ']}')


def _filtering_da_admins(ve_dict):
    """
    Filtering DirectAdmin's admins for `lvectl apply all` command

    :param ve_dict: dict with LVE
    :return: filtering dict

    """

    if cldetectlib.getCPName() == 'DirectAdmin':
        # get list of uids DirectAdmin's admins
        uids_da_admins = [pwd.getpwnam(user).pw_uid for user in admins()]
        ve_dict = {key: value for key, value in ve_dict.items() if key not in uids_da_admins}

    return ve_dict


def prepare_apply_data(lvp_id=0):
    try:
        # update packages_users global dict
        GetControlPanelUsers()
        packages_users_ = dict(packages_users)
        if lvp_id:  # filter for apply lve top containers
            cfg_lvp_id_list = lve.map.name_map.id_list()
            packages_users_ = {k: v for k, v in packages_users.items() if k in cfg_lvp_id_list}
    except Exception:
        packages_users_ = {}

    if lvp_id is True:
        node_list = ve_lvp
        id_list = lvp_list()
    else:
        node_list = ve_lve
        id_list = lve.proc.lve_id_list(lvp_id=lvp_id)

    # get xml node for each lve_id
    # ve_dict is a local dict with lve_id and node with limit for lve_id
    # keys - int lve_id
    # data - xml node or None
    ve_dict = {}
    for node in node_list:
        ve_dict[get_ve_lve_user_uid(ve_lve_element=node)] = {'node' : node, 'reseller' : None}
    for lve_id in id_list:
        if lve_id not in ve_dict:
            # add lve_id for LVE that are not in ve.cfg
            ve_dict[lve_id] = {'node': None, 'reseller': None}

    if (len(packages_users_) != 0):
        #  filtering addon admins DA.
        #  package_users contain only users, not addon admins DA
        ve_dict = _filtering_da_admins(ve_dict)
        for uid in packages_users_:
            if uid not in ve_dict:
                node = None
            else:
                node = ve_dict[uid]['node']
            # add lve_id for users that have package assigned
            pkg = packages_users_[uid]
            resellers = guess_reseller_by_package(pkg)
            if len(resellers) > 0:
                ve_dict[uid] = {'node' : node, 'reseller' : resellers[0]}
            else:
                ve_dict[uid] = {'node' : node, 'reseller' : None}
    return ve_dict


def lve_destroy_and_recreate_all():
    lve_ve_dict, lve_lvp_map = _get_lve_ve_dict_and_lvp_map()

    remaning_alive_lves = set(lve.proc.lve_id_list())
    # NOTE: First we handle all LVEs not belonging to any LVP.
    for lve_id in lve_ve_dict.keys():
        if lve_lvp_map.get(lve_id, 0) != 0:
            # This code path will never be triggered if reseller limits are disabled
            remaning_alive_lves.discard(lve_id)
            continue
        if lve_id in remaning_alive_lves:
            lve.lve_destroy(lve_id)
            remaning_alive_lves.discard(lve_id)
        # TODO(vlebedev): Is this context manager really necessary here?
        with lve.py.context_ignore_error(ignore_error=True):
            lve_apply(
                lve_id,
                out_node=lve_ve_dict[lve_id]['node'],
                reseller=lve_ve_dict[lve_id]['reseller'],
            )
    # NOTE: Reset the default LVE settings.
    lve_apply(lve_id=0)

    if lve.reseller_limit_supported():
        lvp_ve_dict = prepare_apply_data(True)
        remaining_alive_lvps = set(lve.proc.lvp_id_list())

        # NOTE: Now we handle LVPs and LVEs inside those LVPs.
        for lvp_id in lvp_ve_dict.keys():
            if lvp_id in remaining_alive_lvps:
                pylve.lve_lvp_destroy(lvp_id)
                remaining_alive_lvps.discard(lvp_id)
            _create_if_necessary_and_configure_lvp(lvp_ve_dict, lvp_id)

            # apply reseller's users
            reseller_name = lve.map.get_reseller_name(lvp_id)
            kernel_mapping = lve.proc.map()
            for user in clcommon.cpapi.reseller_users(reseller_name):
                lve_id = pwd.getpwnam(user).pw_uid
                if lve_id in remaning_alive_lves:
                    lve.lve_destroy(lve_id)
                    remaning_alive_lves.discard(lve_id)

                if kernel_mapping.get(lve_id, 0) != lvp_id:
                    lve.py.lve_lvp_move(
                        lvp_id,
                        lve_id,
                        err_msg=f'Can`t move lve_id={lve_id} to lvp_id={lvp_id}; error code {{code}}'
                    )
                lve_apply(lve_id=lve_id, reseller=reseller_name)

        # NOTE: Destroy any remaining live LVP that was not handled above.
        for lvp_id in remaining_alive_lvps:
            pylve.lve_lvp_destroy(lvp_id)

    # TODO(vlebedev): Not sure if this cleanup (and the one for LVPs above) is
    #                 really necesary but at least it faithfully replicates the
    #                 behaviour of `lvectl destroy all` part.
    # NOTE: Destroy any remaining live LVE that was not handled above.
    for lve_id in remaning_alive_lves:
        lve.lve_destroy(lve_id)


# Apply all users and resellers settings
def lve_apply_all():
    ve_dict, lve_lvp_map = _get_lve_ve_dict_and_lvp_map()

    with lve.py.context_ignore_error(ignore_error=True):
        for lve_id in ve_dict.keys():
            if lve_lvp_map.get(lve_id, 0) == 0:
                lve_apply(lve_id, out_node=ve_dict[lve_id]['node'], reseller=ve_dict[lve_id]['reseller'])

    lve_apply(lve_id=0)

    # apply limits for all LVP and LVEs inside LVP
    if not lve.reseller_limit_supported():
        return

    ve_dict = prepare_apply_data(True)
    kernel_mapping = lve.proc.map()

    for lvp_id_ in ve_dict.keys():
        _create_if_necessary_and_configure_lvp(ve_dict, lvp_id_)

        # apply reseller's users
        reseller_name = lve.map.get_reseller_name(lvp_id_)
        for user in clcommon.cpapi.reseller_users(reseller_name):
            lve_id_ = pwd.getpwnam(user).pw_uid
            # LU-511: create mapping before lve if needed
            if kernel_mapping.get(lve_id_, 0) != lvp_id_:
                lve.py.lve_lvp_move(
                    lvp_id_,
                    lve_id_,
                    err_msg=f'Can`t move lve_id={lve_id_} to lvp_id={lvp_id_}; error code {{code}}'
                )
            lve_apply(lve_id=lve_id_, reseller=reseller_name)


def _get_lve_ve_dict_and_lvp_map() -> tuple[dict, dict]:
    get_XML_cfg()
    GetControlPanelUsers('list-users')
    lve_ve_dict = prepare_apply_data()
    lve_lvp_map = dict(lve.lve_id_lvp_id_pairs()) if lve.reseller_limit_supported() else {}
    return lve_ve_dict, lve_lvp_map


def _create_if_necessary_and_configure_lvp(ve_dict, lvp_id) -> None:
    # apply lvp limits using defaults for lvp
    # load reseller defaults instead of gloabal
    get_XML_cfg(lvp_id=lvp_id, lvp_defaults=True, load_config_elements=False)
    lve_apply(lve_id=lvp_id,
              out_node=ve_dict[lvp_id]['node'],
              reseller=ve_dict[lvp_id]['reseller'],
              lvp_id=lvp_id)

    get_XML_cfg(lvp_id=lvp_id, load_config_elements=False)
    lve_apply(lve_id=0, lvp_id=lvp_id)


def _remove_reseller(lvp_id):
    """Remove reseller from ve.cfg and from procfs."""
    get_global_lock(True)
    get_XML_cfg(lvp_id=lvp_id)
    for el in ve_lvp:
        if get_ve_lve_user_uid(ve_lve_element=el) == lvp_id:
            users = lve.proc.map_lve_id_list(lvp_id)
            # move containers to host
            for lve_id in users[:]:
                pylve.lve_lvp_move(0, lve_id)
                try:
                    pwd.getpwuid(lve_id)
                except KeyError:
                    if lve.py.lve_exists(lve_id):
                        lve.lve_destroy(lve_id)
                    users.remove(lve_id)
            lvp_destroy(lvp_id)  # destroy container
            el.parentNode.removeChild(el)  # remove record
            save_xml(ve_cfg)
            # load defaults host settings for end users
            get_XML_cfg(lvp_id=0)
            # apply limits from config (including package limits)
            for lve_id in users:
                lve_apply(lve_id)
            get_XML_cfg(lvp_id=lvp_id)
            return True
    return False


def disable_reseller_limits(reseller_name, lvp_id):
    """Disable reseller limits and call hooks"""
    if _remove_reseller(lvp_id):
        reseller_limits_disabled_post.throw_event(reseller=reseller_name)
    else:
        if JSON:
            json_format('multi', ['WARNING', f'no configuration found for LVP {lvp_id}'])
            sys.exit(-1)
        else:
            print(f'warning: no configuration found for LVP {lvp_id}')


# Delete User from ve.cfg and set default lve settings
def lve_delete(lve_id):
    get_global_lock(True)
    get_XML_cfg()
    Deleted = False
    for el in ve_lve:
        if get_ve_lve_user_uid(ve_lve_element=el) == lve_id:
            Deleted = True
            lve_destroy(lve_id)
            lve_create(lve_id)
            el.parentNode.removeChild(el)
            save_xml(ve_cfg)
            get_XML_cfg()
            lve_apply(lve_id)
    if not Deleted:
        if JSON:
            json_format('multi', ['WARNING', f'no configuration found for VE {lve_id}'])
            sys.exit(-1)
        else:
            print(f'warning: no configuration found for VE {lve_id}')


def lve_enter_check():
    if not os.path.exists('/proc/lve/enter'):
        if JSON:
            json_format('multi', ['WARNING', 'enter by name not supported'])
        else:
            print('warning: enter by name not supported')
        sys.exit(-1)


def enter_apply(sign, binary):
    lve_enter_check()
    try:
        msg = sign + binary.strip()
        with open('/proc/lve/enter', 'w', encoding='utf-8') as f:
            f.write(msg)
    except Exception:
        pass


def list_binaries():
    get_XML_cfg()
    if JSON:
        result = '{"data":['
        first = True
        for el in ve_binary:
            path = el.getAttribute('path')
            if first:
                result += '"' + path + '"'
                first = False
            else:
                result += ',"' + path + '"'
        result += ']}'
        print(result)
    else:
        print("Binaries")
        for el in ve_binary:
            print(el.getAttribute('path'))


def load_binaries():
    get_XML_cfg()
    for el in ve_binary:
        enter_apply('+', el.getAttribute('path'))


def reload_binaries():
    lve_enter_check()
    with open('/proc/lve/enter', 'r', encoding='utf-8') as f:
        for line in f:
            enter_apply('-', line)
    load_binaries()


def del_binary(binary):
    global ve_binary
    get_global_lock(True)
    lve_enter_check()
    get_XML_cfg()
    deleted = False
    for el in ve_binary:
        if el.getAttribute('path') == binary:
            deleted = True
            enter_apply('-', binary)
            el.parentNode.removeChild(el)
            save_xml(ve_cfg)
            get_XML_cfg()
    if not deleted:
        if JSON:
            json_format('multi', ['WARNING', f'no configuration found for {binary}'])
        else:
            print(f'warning: no configuration found for {binary}')
        sys.exit(-1)


def set_binary(binary):
    global ve_binary
    global ve_enter_by_name
    get_global_lock(True)
    get_XML_cfg()
    for el in ve_binary:
        if el.getAttribute('path') == binary:
            return  # nothing to do, it is already there
    enter_apply('+', binary)
    bin_xml = ve_cfg.createElement('binary')
    bin_xml.setAttribute('path', binary)
    ve_enter_by_name.appendChild(bin_xml)
    save_xml(ve_cfg)
    get_XML_cfg()


def lve_set_default(set_data, package_flag, is_needed, lvp_id=0):
    """
    Set given lve or package to default values for given parameters

    :param dict set_data: Arguments of lvectl call
    :param bool package_flag: Should we delete package or lve with given id
    :param callable is_needed: Function that takes xml element and set_data dict and returns
                               whether current xml element contains info about needed ID from set_data
    """
    try:
        if package_flag:
            data = ve_package
        elif lvp_id:
            data = ve_lvp
        else:
            data = ve_lve
        el = [e for e in data if is_needed(e, set_data)][0]
    except IndexError:
        return
    if lvp_id:
        # for lvectl set-reseller {id} --default=A,B,C; remove limit record in ve.cfg
        for tag_ in set_data['set-default']:
            if tag_ == 'ep':
                n = xml_filter_first(el, 'other', 'maxentryprocs')
            else:
                n = xml_filter_first(el, tag_, 'limit')
            if n:
                n.parentNode.removeChild(n)
        return
    to_keep = set(LIMITS_LIST_NAME) - set_data['set-default']
    for limit in to_keep:
        if limit == 'ep' and len(el.getElementsByTagName('other')) > 0:
            # dict.setdefault isn't lazy evaluated
            if limit not in set_data:
                set_data[limit] = el.getElementsByTagName('other')[0].getAttribute('maxentryprocs')
        elif len(el.getElementsByTagName(limit)) > 0:
            # dict.setdefault isn't lazy evaluated
            if limit not in set_data:
                set_data[limit] = el.getElementsByTagName(limit)[0].getAttribute('limit')
    # delete this lve or package
    if package_flag:
        plan_delete(set_data['ve_id'])
    else:
        lve_delete(set_data['ve_id'])


def _check_reseller_user_pair(uid, reseller_name):
    """
    Checks is uid owned by reseller
    :param uid: uid for check
    :param reseller_name: Reseller name, None treats as root
    :return: True - valid reseller/user pair, False - else
        Special case:
          if reseller_name is None (root) - always valid
    """
    if reseller_name in (None, 'root'):
        return True
    # reseller is not root
    # determine username
    username = get_main_username_by_uid(uid)
    if username in ('root', 'N/A'):
        # user is root or no such user -- error
        return False
    # determine users of supplied reseller's container.
    try:
        # Get reseller's users list
        reseller_users_list = reseller_users(reseller_name)
    except Exception:
        # any error - ignore, reseller is root
        reseller_users_list = []
    if username in reseller_users_list:
        return True
    return False


# Set limits for user
# TODO: split this method into several independent:
# - enable_reseller_limits
# - set_reseller_default_limits
# - set_reseller_limits
# - set_lve_limits
def lve_set(set_data, lvp_id=0):
    # set_data example:
    #  {'iops': 2222, 'reseller_name': 'res', 'save': False, 've_id': 1023, 'pmem': 524288}
    # 524288 * 4096 = 2G -- pmem=2G
    if lvp_id == 0:
        # Set limits for user's LVE, check reseller/user match
        reseller_name = set_data.get('reseller_name', None)
        lve_id = set_data['ve_id']
        if not _check_reseller_user_pair(lve_id, reseller_name):
            return False
    global setup_data
    get_global_lock(True)
    if lvp_id and lvp_id == set_data['ve_id']:
        # reseller's container limits
        if lve.proc.exist_lvp(lvp_id):
            # reseller's container exists... load info about his container
            get_XML_cfg(lvp_id=lvp_id, lvp_defaults=True)
        else:
            # reseller's container does not exists... create new one with default limits
            get_XML_cfg(lvp_id=lvp_id)
    elif lvp_id == 0 and lve.reseller_limit_supported():
        # user's limits (reseller & not)
        get_XML_cfg(lvp_id=lve.proc.detect_inside_lvp(set_data['ve_id']))
    else:
        # default limits and limits for user when reseller's does not supported
        get_XML_cfg(lvp_id=lvp_id)

    try:
        GetControlPanelUsers()
    except Exception:
        pass
    try:
        # LU-366. Fix reset user's limits to unlimited if user in reseller package
        # and reseller is not root/admin
        package = packages_users[set_data['ve_id']]
        resellers = guess_reseller_by_package(package)
        reseller = resellers[0] if resellers else ''
        prepare_setup_data(package, reseller=reseller)
    except Exception:
        setup_data = ve_defaults
    if set_data['ve_id'] != 0:
        has_ve = False
        # set default

        def is_needed_user(el, set_data):
            return get_ve_lve_user_uid(ve_lve_element=el) == set_data['ve_id']

        if 'set-default' in set_data:
            lve_set_default(set_data, package_flag=False, is_needed=is_needed_user, lvp_id=lvp_id)
        if 'ncpu' in set_data:
            lncpu = int(set_data['ncpu'])
        else:
            lncpu = ve_defaults['ncpu']

        # check that cpu value in any format (cpu, speed=% or speed=[m|g]hz) is equal or not
        cpu_is_different = True
        if 'cpu' in set_data:
            setted_cpu = convert_to_kernel_format(set_data['cpu'], lncpu = lncpu)
            if setted_cpu == setup_data['cpu']:
                cpu_is_different = False

        if lvp_id:
            el_list = ve_lvp
        else:
            el_list = ve_lve  # choose top level container for modifications
        for el in el_list:
            if is_needed_user(el, set_data):
                for key in LIMITS_LIST_NAME:
                    if key in set_data:
                        try:
                            if key == 'ep':
                                set_child_tag_atrr(el, 'other', 'maxentryprocs', set_data[key])
                            else:
                                set_child_tag_atrr(el, key, 'limit', set_data[key])
                        except (ValueError, IndexError, TypeError):
                            # we already checked cpu value, so use cpu_is_different result
                            if key == "cpu":
                                is_different = cpu_is_different
                            # otherwise compare with default in usual way
                            else:
                                is_different = setup_data[key] != set_data[key]
                            if is_different or set_data['save']:
                                if key == 'ep':
                                    node = ve_cfg.createElement('other')
                                    node.setAttribute('maxentryprocs',str(set_data[key]))
                                else:
                                    node = ve_cfg.createElement(key)
                                    node.setAttribute('limit',str(set_data[key]))
                                el.appendChild(node)

                if not set_data.get('skip-update-cfg', False):
                    save_xml(ve_cfg)

                has_ve = True
                if lvp_id and lvp_id == set_data['ve_id']:
                    # reseller's container limits
                    if lve.proc.exist_lvp(lvp_id):
                        # reseller's container does not exists... create new one with default limits
                        get_XML_cfg(lvp_id=lvp_id, lvp_defaults=True)
                    else:
                        # reseller's container exists... load info about his container
                        get_XML_cfg(lvp_id=lvp_id)
                elif lvp_id == 0 and lve.reseller_limit_supported():
                    # user's limits (reseller & not)
                    get_XML_cfg(lvp_id=lve.proc.detect_inside_lvp(set_data['ve_id']))
                else:
                    # default limits and limits for user when reseller's does not supported
                    get_XML_cfg(lvp_id=lvp_id)
                lve_apply(set_data['ve_id'], lvp_id=lvp_id)
            else:
                pass

        if not has_ve and set_data['ve_id']:
            el_name = LVP_XML_TAG_NAME if lvp_id else 'lve'
            el = ve_cfg.createElement(el_name)
            if lvp_id:  # for create resellers limit config
                # set reseller_name reseller_id map
                el.setAttribute('id', str(lvp_id))
                el.setAttribute('user', set_data['user'])
                el.appendChild(ve_default)  # copy default limits to reseller
            else:
                if set_data.get('save-username'):
                    el.setAttribute('user', pwd.getpwuid(set_data['ve_id']).pw_name)
                else:
                    el.setAttribute('id', str(set_data['ve_id']))

            for key in LIMITS_LIST_NAME:
                if key in set_data:
                    # we already checked cpu value, so use cpu_is_different result
                    if key == "cpu":
                        is_different = cpu_is_different
                    # otherwise compare with default in usual way
                    else:
                        is_different = setup_data[key] != set_data[key]
                    if is_different or set_data['save']:
                        if key == 'ep':
                            node = ve_cfg.createElement('other')
                            node.setAttribute('maxentryprocs',str(set_data[key]))
                        else:
                            node = ve_cfg.createElement(key)
                            node.setAttribute('limit',str(set_data[key]))
                        el.appendChild(node)

            added = False
            for el2 in ve_package:
                el2.parentNode.insertBefore(el,el2)
                added = True
                break
            if not added:
                ve_cfg.lastChild.appendChild(el)

            if not set_data.get('skip-update-cfg', False):
                save_xml(ve_cfg)

            if lvp_id:
                enables_reseller_limits = not lve.proc.exist_lvp(lvp_id)
                # load lvp defaults and lvp tag, set limits for reseller
                get_XML_cfg(lvp_id=lvp_id, lvp_defaults=True)
                lve_apply(set_data['ve_id'], lvp_id=lvp_id)
                # copy default limits from host container
                pylve.lve_set_default(lvp_id, pylve.lve_info(0))
                # load lvp tag and reseller's end user defaults
                get_XML_cfg(lvp_id=lvp_id)
                reseller_name = lve.map.get_reseller_name(lvp_id)
                for lve_id_ in lve.map.lvp_lve_id_list(lvp_id=lvp_id):
                    lve.py.lve_lvp_move(lvp_id, lve_id_)
                    lve_apply(lve_id_, reseller=reseller_name)
                # call hook if we enabled reseller limits
                if enables_reseller_limits:
                    reseller_limits_enabled_post.throw_event(reseller=reseller_name)
            else:
                if lve.reseller_limit_supported():
                    get_XML_cfg(lvp_id=lve.proc.detect_inside_lvp(set_data['ve_id']))
                else:
                    get_XML_cfg(lvp_id=lvp_id)
                lve_apply(set_data['ve_id'])
    else:
        for key in LIMITS_LIST_NAME:
            if key in set_data:
                if key == 'ep':
                    ve_default.getElementsByTagName('other')[0].setAttribute('maxentryprocs',str(set_data[key]))
                else:
                    ve_default.getElementsByTagName(key)[0].setAttribute('limit',str(set_data[key]))

        if not set_data.get('skip-update-cfg', False):
            save_xml(ve_cfg)

        get_XML_cfg(lvp_id=lvp_id)
        lve_apply(set_data['ve_id'], lvp_id=lvp_id)
    return True


def package_set(set_data, is_reseller=False):
    """
    Set package with some heuristic algorithm to simulate old package set behavior
    """
    get_global_lock(True)
    get_XML_cfg()
    # Removed in LU-351
    # set_data['ve_id'] = unicode(set_data['ve_id'].decode('utf-8'))
    reseller_list = guess_reseller_by_package(set_data['ve_id'])
    if len(reseller_list) == 0:
        reseller = None
    elif len(reseller_list) >= 1:
        # if dublicated packages found - use first as a reseller
        reseller = reseller_list[0]
    #if ve.cfg has tag <version>2</version> - trying to guess reseller name by package
    #if reseller is undef or ve.cfg has not tag version - work with ver 1
    if reseller is not None and ve_cfg_version > 1:
        set_data['reseller_name'] = reseller
        package_set_ext(set_data, is_reseller=True)
    else:
        package_set_ext(set_data, is_reseller=False)


# Set new package or modify exist
# package-set-ext
def package_set_ext(set_data, is_reseller=False):
    get_global_lock(True)
    get_XML_cfg()
    has_package = False

    if is_reseller:
        def is_needed_plan(el, set_data):
            return (
                el.getAttribute('id') == set_data['ve_id'] and el.getAttribute('reseller') == set_data['reseller_name']
            )
    else:
        def is_needed_plan(el, set_data):
            return el.getAttribute('id') == set_data['ve_id'] and not el.getAttribute('reseller')

    if 'set-default' in set_data:
        lve_set_default(set_data, package_flag=True, is_needed=is_needed_plan)

    for el in ve_package:
        if is_needed_plan(el, set_data):
            for key in LIMITS_LIST_NAME:
                if key in set_data:
                    try:
                        if key == 'ep':
                            el.getElementsByTagName('other')[0].setAttribute('maxentryprocs',str(set_data[key]))
                        else:
                            el.getElementsByTagName(key)[0].setAttribute('limit',str(set_data[key]))
                    except (ValueError, IndexError, TypeError):
                        if key == 'ep':
                            node = ve_cfg.createElement('other')
                            node.setAttribute('maxentryprocs',str(set_data[key]))
                        else:
                            node = ve_cfg.createElement(key)
                            node.setAttribute('limit',str(set_data[key]))
                        el.appendChild(node)
            if cldetectlib.is_plesk():
                plesk_id = _plesk_get_package_id(set_data.get('reseller_name', ''), set_data['ve_id'])
                el.setAttribute(XML_PLESK_ID, str(plesk_id))
            has_package = True
    if not has_package:
        package_reseller = ''
        el = ve_cfg.createElement('package')
        el.setAttribute('id', set_data['ve_id'])
        if is_reseller:
            el.setAttribute('reseller', set_data['reseller_name'])
            package_reseller = set_data['reseller_name']
        if cldetectlib.is_plesk():
            plesk_id = _plesk_get_package_id(package_reseller, set_data['ve_id'])
            el.setAttribute(XML_PLESK_ID, str(plesk_id))
        for key in LIMITS_LIST_NAME:
            if key in set_data:
                if key == 'ep':
                    node = ve_cfg.createElement('other')
                    node.setAttribute('maxentryprocs',str(set_data[key]))
                else:
                    node = ve_cfg.createElement(key)
                    node.setAttribute('limit',str(set_data[key]))
                el.appendChild(node)
        ve_cfg.lastChild.appendChild(el)

    save_xml(ve_cfg)
    get_XML_cfg()
    copy_package_settings_to_cpanel(set_data)

    if 'ncpu' in set_data:
        lncpu = int(set_data['ncpu'])
    else:
        lncpu = ve_defaults['ncpu']

    if 'cpu' in set_data:
        set_data['cpu'] = convert_to_kernel_format(set_data['cpu'], lncpu=lncpu)

    reseller = set_data['reseller_name'] if is_reseller else None
    plan_apply(set_data['ve_id'], reseller=reseller)


def _plesk_get_package_id(reseller: str, package: str) -> Optional[int]:
    """
    Find the right package id from plesk DB query
    """
    panel = detect_panelclass()
    packages = panel.list_domain_packages_with_id()
    try:
        pack = next(filter(
            lambda x: x[0] in {reseller, 'root'} and x[1] == package,   # no reseller == reseller is root (admin)
            packages
        ))
        return pack[2]
    except StopIteration:
        return None


def get_reseller_packages_map():
    """
    Retrives resellers to packages map from panel using /usr/bin/getcontrolpaneluserspackages
    :return: Dictionary:
    { 'reseller1' -> ['pack1', 'pack2'], 'reseller2' -> ['pack'] }
    """
    global packages_users
    packages_users_copy = packages_users.copy()
    GetControlPanelUsers('list-resellers-packages')
    reseller_packages_map = packages_users
    packages_users = packages_users_copy
    return reseller_packages_map


def reseller_package_set(set_data):
    """
    Set reseller package limits
    :param set_data: input data dictionary
    :return: True - limits was set succesfully
            False - supplied provider has no supplied package
    """
    # set limits to package that belongs to given reseller
    reseller_name = set_data['reseller_name']
    package_name = set_data['ve_id']
    # Retrive resellers packages from panel
    reseller_packages_map = get_reseller_packages_map()
    # If reseller has supplied package -- set limit
    if reseller_name in reseller_packages_map and package_name in reseller_packages_map[reseller_name]:
        # Reseller/package pair valid - set limit
        package_set_ext(set_data, is_reseller=True)
        return True
    # ERROR: Supplied reseller has no supplied package
    return False


def copy_package_settings_to_cpanel(set_data):
    """
    Copy package limits from ve.cfg to cpanel packages data
    """
    package = set_data['ve_id']
    if not cldetectlib.is_cpanel():
        return  # skip func if panel not cPanel
    package_path = f'/var/cpanel/packages/{package}'
    if not os.path.isfile(package_path):
        return  # skip func if no cPanel packages found
    with open(package_path, 'r', encoding='utf-8') as f:
        cpanel_package_data = f.readlines()
    new_cpanel_package_data = cpanel_package_data[:]
    old_cpanel_data = {}

    # proces old_cpanel_package_data - get old limits and remove stings from it
    # result of processing - cpanel_package_data_modify and old_cpanel_data
    for line in cpanel_package_data:
        if line.startswith('lve_'):
            line_parts = line.strip().split('=')
            limit_name = line_parts[0].replace('lve_', '').strip()
            if line_parts[1] != 'DEFAULT':
                old_cpanel_data[limit_name] = line_parts[1]  # get old_limits
            if limit_name in LIMITS_LIST_NAME:
                new_cpanel_package_data.remove(line)
        if line.startswith('_PACKAGE_EXTENSIONS') and 'lve' not in line:
            return      # skip func if no lve extention install to the package

    for limit_name in ('pmem', 'mem', 'vmem'):
        if limit_name in old_cpanel_data:
            memory_page_value = clcommon.memory_to_page(old_cpanel_data[limit_name])
            old_cpanel_data[limit_name] = memory_page_value or ve_defaults[limit_name]

    if is_limits_equals(old_cpanel_data, set_data):
        return              # skip writeting to file - limits are equals

    # create and add to new cpanel_data_file limits lines like:
    # lve_ + limit_name + = + limit_value
    cpanel_data = create_cpanel_limits(package, ve_package)
    for limit_name in LIMITS_LIST_NAME:
        limit_value = cpanel_data[limit_name]
        limit_line = f'lve_{limit_name}={limit_value}\n'
        new_cpanel_package_data.append(limit_line)
    write_file_via_tempfile(''.join(new_cpanel_package_data), package_path, 0o644)


def is_limits_equals(old_limits, new_limits):
    """
    check if new set of limits for package are equals to used
    """
    for key in new_limits.keys():
        if key in ('ve_id', 'save'):
            continue    # ve_id == package name. skip this key
        try:
            if old_limits[key] != new_limits[key]:
                return False
        except KeyError:
            return False
    return True


def create_cpanel_limits(package_id, xml_packages):
    """
    create limits for cpanel package file
    use data from ve.cfg:
    limit = limit if found in ve.cfg or DEFAULT
    return dict
    """
    result_data = {}
    for el in xml_packages:
        if el.getAttribute('id') == package_id:
            for limit in LIMITS_LIST_NAME:
                try:
                    if limit == 'ep':
                        result_data[limit] = str(
                            el.getElementsByTagName('other')[0].getAttribute('maxentryprocs')
                        ).strip()
                    elif limit in ("mem", "vmem", "pmem"):
                        result_data[limit] = str(
                            clcommon.page_to_memory(
                                int(el.getElementsByTagName(limit)[0].getAttribute("limit"))
                            )
                        ).strip()
                    else:
                        result_data[limit] = str(el.getElementsByTagName(limit)[0].getAttribute('limit')).strip()
                except (ValueError, IndexError, TypeError):
                    result_data[limit] = 'DEFAULT'
    return result_data


# Delete plan from ve.cfg
def plan_delete(plan_id, reseller_name=None):
    get_global_lock(True)
    get_XML_cfg()
    Deleted = False
    if reseller_name is None:
        def is_needed_package(el):
            return el.getAttribute('id') == plan_id and not el.getAttribute('reseller')
    else:
        def is_needed_package(el):
            return el.getAttribute('id') == plan_id and el.getAttribute('reseller') == reseller_name

    for el in ve_package:
        if is_needed_package(el):
            Deleted = True
            el.parentNode.removeChild(el)
            save_xml(ve_cfg)
            break
    if not Deleted:
        # try to guess reseller name only if no reseller name
        if reseller_name is None:
            resellers_list = guess_reseller_by_package(plan_id)
            if len(resellers_list) == 0:
                reseller = None
            else:
                # if some resellers found - use first
                reseller = resellers_list[0]
            # try to delete package only if we guess reseller
            if reseller is not None:
                plan_delete(plan_id, reseller)
                return
        if JSON:
            json_format(
                'multi',
                ['WARNING', f'no configuration found for plan {plan_id}']
            )
            sys.exit(-1)
        else:
            print(f'warning: no configuration found for plan {plan_id}')
    lve_apply_all()


def reseller_plan_delete(plan_id, reseller_name):
    plan_delete(plan_id, reseller_name=reseller_name)


def get_xml_limit(el, key):
    try:
        return str(el.getElementsByTagName(key)[0].getAttribute('limit'))
    except (ValueError, IndexError):
        #  convert from kernel format to output
        return ve_defaults[key] if key != 'cpu' else f'{ve_defaults[key] // 100}%'


def _normalize_str(data_str):
    """
    Normalize string for JSON output.
    Example:
     - Input string: -_&[{}]'"`te\\s/t\a
     - Output string: -_&[{}]'\"`te\\\\s/t\\a
    :param data_str: String for normalize
    :return: Normalied string
    """
    def _get_char_index(input_string, char_to_search, ordinal):
        """
        Get the index of the specified occurrence of character in string
        :param input_string: String
        :param char_to_search: Character to search
        :param ordinal: Required occurence number
        :return: Char index
        """
        count = 0
        for idx, ch in enumerate(input_string):
            if ch == char_to_search:
                # Char found
                count += 1
                if count == ordinal:
                    return idx
        # Char not found
        return -1

    if data_str is None:
        return None
    json_str = json.dumps({'str': data_str})
    # json_str example: {"str": "-_&[{}]'\"`te\\\\s/t\\a"},
    # get '-_&[{}]'\"`te\\\\s/t\\a' from it
    # Get third " index
    trd_idx = _get_char_index(json_str, '"', 3)
    # Get Last " index
    last_idx = json_str.rfind('"')
    return json_str[trd_idx + 1:last_idx]


def _package_formatter(fields, is_reseller=False, printer=None):
    """
    Generate inner function with closured fields names, is_reseller flag and printer function

    :param list fields: List of strings that represent names of fields in final output
    :param boolean is_reseller: Format output with info about reseller or not
    :param callable printer: Function to format and print data for every entry
    :rtype: callable
    :return: function to format data for every user
    """

    printer = printer if printer is not None else _pprint_r if is_reseller else _pprint_p

    def wrapper(package_name, reseller_name=None):
        """
        :param string package_name: Find and format data for this package name
        :param string reseller_name: reseller name, owner of supplied package
        :rtype: list
        :return: List of giver package's statistics data line or empty list
        """
        # in order to avoid unicode warnings here
        if is_reseller:
            # Reseller package
            _load_resellers_xml_data(reseller_name)
            data = copy.copy(ve_defaults)   # only after reading reseller's xml
            data['reseller'] = reseller_name

            def is_needed_package(el):
                return package_name == el.getAttribute('id') and el.getAttribute('reseller') == reseller_name
        else:
            # Admin's package
            def is_needed_package(el):
                return package_name == el.getAttribute('id') and not el.getAttribute('reseller')

            data = copy.copy(ve_defaults)

        data['id'] = _normalize_str(package_name) if JSON else package_name

        def convert_mem_limits(value):
            return _pmem_vmem_to_bytes_value(value) if BYTES_FLAG else _mb_mem(value)

        data['vmem'] = convert_mem_limits(data['mem'])
        if LVE_VERSION > 4:
            data['pmem'] = convert_mem_limits(data['pmem'])
        for el in ve_package:
            if is_needed_package(el):
                lncpu = get_xml_limit(el, 'ncpu')
                data['ncpu'] = lncpu if lncpu != '' else str(ve_defaults['ncpu'])
                data['speed'] = str(convert_to_kernel_format(
                    get_xml_limit(el, 'cpu'), lncpu=int(data['ncpu'])) // 100
                )
                try:
                    data['ep'] = str(int(
                        el.getElementsByTagName('other')[0].getAttribute('maxentryprocs')
                    ))
                except (IndexError, ValueError):
                    pass
                data['pmem'] = convert_mem_limits(get_xml_limit(el, 'pmem'))
                data['vmem'] = convert_mem_limits(get_xml_limit(el, 'mem'))
                data['io'] = get_xml_limit(el, 'io')
                data['nproc'] = get_xml_limit(el, 'nproc')
                data['iops'] = get_xml_limit(el, 'iops')
        if data.get('speed') is None:
            #  convert from kernel format for output
            data['speed'] = data['cpu'] // 100
        data['cpu'] = str(speed_to_old_cpu(data['speed']))

        res = []
        if JSON:
            line = ','.join(f'"{f}":"{data.get(f.lower(), "N/A")}"' for f in fields)
            res = [f'{{{line}}}']
        else:
            printer(*[data.get(f.lower(), '') for f in fields])
        return res
    return wrapper


# lvectl package-list
def get_packages_list():
    get_XML_cfg()
    GetControlPanelUsers('list-packages')
    packages = packages_users.copy()
    GetControlPanelUsers('list-resellers-packages')
    reseller_packages = packages_users.copy()

    result = _formatter(_pprint_p, default_id=DEFAULT_PACKAGE)
    formatter = _package_formatter(get_fields(), is_reseller=False, printer=_pprint_p)
    for package in packages:
        result += formatter(package)
    if ve_cfg_version > 1:
        formatter = _package_formatter(get_fields(), is_reseller=True, printer=_pprint_p)
    # reseller_packages: {'reseller_name': ['pack1', 'pack2']}
    for reseller_name, packages_list in reseller_packages.items():
        # On DA skip all non-admin's packages
        if cldetectlib.is_da() and reseller_name != 'admin':
            continue
        for reseller_package in packages_list:
            if ve_cfg_version > 1:
                result += formatter(reseller_package, reseller_name)
            else:
                result += formatter(reseller_package)

    if JSON:
        print('{"data":[' + ','.join(result) + ']}')


# lvectl reseller-package-list
def get_resellers_packages_list():
    get_XML_cfg()
    GetControlPanelUsers('list-resellers-packages')

    more_fields = ["RESELLER"]
    fields = get_fields() + more_fields
    result = _formatter(_pprint_p, default_id=DEFAULT_PACKAGE)

    formatter = _package_formatter(fields, is_reseller=True)
    # packages_users: {'reseller_name': ['pack1', 'pack2']}
    for reseller_name, packages_list in packages_users.items():
        for reseller_package in packages_list:
            result += formatter(reseller_package, reseller_name)

    if JSON:
        print('{"data":[' + ','.join(result) + ']}')


# lvectl all-package-list
def get_all_packages_list():
    get_XML_cfg()
    GetControlPanelUsers('list-packages')
    packages = packages_users.copy()
    GetControlPanelUsers('list-resellers-packages')
    reseller_packages = packages_users.copy()

    more_fields = ["RESELLER"]
    fields = get_fields() + more_fields
    # make header with default package
    result = _formatter(_pprint_r, default_id=DEFAULT_PACKAGE, more_fields=more_fields)

    formatter = _package_formatter(fields, is_reseller=False, printer=_pprint_r)
    for package in packages:
        result += formatter(package)
    # Print resellers packages
    formatter = _package_formatter(fields, is_reseller=True)
    # reseller_packages: {'reseller_name': ['pack1', 'pack2']}
    for reseller_name, packages_list in reseller_packages.items():
        for reseller_package in packages_list:
            result += formatter(reseller_package, reseller_name)

    if JSON:
        print('{"data": [' + ','.join(result) + ']}')


cached_resellers_packages = None
cached_list_packages      = None
cached_users              = None
cached_reseller_users     = None
cached_default            = None


def _convert_packages_list(package_list):
    """
    Converts package list to internal format
    :param package_list: Package list. Example: ['BusinessPackage', 'Package2']
    :return: Package list as dictionary. Example: {'BusinessPackage': 'BusinessPackage', 'Package2': 'Package2'}
    """
    packages_users_dict = {}
    for package in package_list:
        packages_users_dict[package] = package
    return packages_users_dict


# Get users from control panel with plans
def GetControlPanelUsers(option='list-all', lve_package_id='', reseller=None):
    """
    Parse output from GET_CP_PACKAGE_SCRIPT and get package and lve relations

    :param option: option for GET_CP_PACKAGE_SCRIPT.
     Option is one from the following possible values:  'userid', 'package', 'list-packages', 'list-resellers-packages'
    :type option: string
    :param lve_package_id: lve_id or package_name
    :type lve_package_id: string or int
    :param reseller:
    :type reseller: string
    """
    global cached_list_packages
    global cached_resellers_packages
    global cached_users
    global cached_reseller_users
    global cached_default
    global packages_users
    # Check arguments
    if option not in ('list-all', 'userid', 'package', 'list-packages',
                      'list-resellers-packages', 'list-users', 'list-reseller-users'):
        return False
    if option in ('userid', 'package') and lve_package_id == '':
        return False

    from clcommon.cpapi import (  # pylint: disable=import-outside-toplevel
        admin_packages,
        get_reseller_users,
        get_uids_list_by_package,
        list_all,
        list_users,
        reseller_package_by_uid,
        resellers_packages,
    )

    try:
        if option == 'userid':
            if cached_users is not None:
                try:
                    packages_users = {
                        lve_package_id: {
                            'package': cached_users[lve_package_id]['package'],
                            'reseller': cached_users[lve_package_id]['reseller']
                        }
                    }
                except KeyError:
                    packages_users = {lve_package_id: {'package': '', 'reseller': ''}}
            else:
                try:
                    reseller_name, package = reseller_package_by_uid(lve_package_id)
                except ValueError:
                    # this is possible on vm without control panel
                    reseller_name = package = ''
                packages_users = {lve_package_id: {'package': package, 'reseller': reseller_name}}
                return True
        elif option == 'package':
            # reseller - optional argument
            packages_users = {lve_package_id: get_uids_list_by_package(lve_package_id, reseller)}
            return True
        elif option == 'list-packages':
            # Result format:
            # {'BusinessPackage': 'BusinessPackage', 'Package2': 'Package2'}
            if cached_list_packages is None:
                package_list = admin_packages()
                # Convert to output format
                packages_users = _convert_packages_list(package_list)
                cached_list_packages = packages_users
            else:
                # list-packages data already present
                packages_users = cached_list_packages
            return True
        elif option == 'list-resellers-packages':
            # Result format:
            #  {'res2 SimplePackage': 'res2 Package',
            #   'res1 BusinessPackage': 'res1 UltraPackage'}
            if cached_resellers_packages is None:
                # in order to produce same results as code that works with Popen
                packages_users = resellers_packages()
                cached_resellers_packages = packages_users
            else:
                # list-resellers-packages data already present
                packages_users = cached_resellers_packages
            return True
        elif option == 'list-users':
            # Result format:
            # {1000: {'reseller': '', 'package': 'Package1'},
            #  1001: {'reseller': '', 'package': 'BusinessPackage'},
            # }
            if cached_users is None:
                cached_users = list_users()
            packages_users = cached_users
            return True
        elif option == 'list-reseller-users':
            # {1001: {'reseller': 'res1', 'package': 'BusinessPackage'},
            #  1004: {'reseller': 'res1', 'package': 'BusinessPackage'}}
            if cached_reseller_users is None:
                reseller_users_dict = get_reseller_users(reseller)
                # for uid, user_data in reseller_users_dict.iteritems():
                #     packages_users[uid] = user_data
                cached_reseller_users = reseller_users_dict
                packages_users = reseller_users_dict
            else:
                # list-reseller-users data already present
                packages_users = cached_reseller_users
            return True
        elif option == 'list-all':  # deprecated. TODO: Remove this option
            # Result format:
            # {1000: 'Package1', 1001: 'BusinessPackage'}
            if cached_default is None:
                cached_default = list_all()
            packages_users = cached_default
            return True
    except EncodingError as e:
        raise_cpanel_encoding_error(e)
    except OSError:
        pass
    return False


def get_panel_users_count():
    """
    Retrieves panel users count
    :return:
    """
    GetControlPanelUsers()
    return len(packages_users)


# Apply plan settings for users
def plan_apply(plan_id, reseller=None):
    # fill the cache to speedup `lvectl package-set-ext` with many users in one package
    GetControlPanelUsers("list-users")

    if GetControlPanelUsers("package", plan_id, reseller=reseller):
        for uid in packages_users[plan_id]:
            lve_apply(int(uid), plan_id, reseller=reseller)


# Destroy many LVEs from stdin
def destroy_many(users_list):
    for line in users_list:
        line = line.replace('\n','')
        users = line.strip().split()
        for user in users:
            if (len(user) != 0):
                try:
                    user = int(user)
                    lve_destroy(user)
                except Exception:
                    pass


# Apply many LVEs from stdin
def apply_many(users_list):
    get_XML_cfg()
    try:
        GetControlPanelUsers()
    except Exception:
        pass

    for line in users_list:
        line = line.replace('\n','')
        users = line.strip().split()
        for user in users:
            if (len(user) != 0):
                try:
                    user = int(user)
                    lve_apply(user)
                except Exception:
                    pass


# Put pid into LVE
def limit_pid(lve_id, pid, flags):
    pylve.lve_enter_pid_flags(
        int(lve_id), int(pid), flags,
        err_msg=f'Can`t put proccess with pid {pid} in lve {lve_id}; error code {{code}}'
    )


# Get pid from LVE
def release_pid(pid):
    pylve.lve_leave_pid(int(pid), err_msg=f'Can`t release process with pid {pid}')


def get_globals():
    global ve_cfg
    global ve_lveconfig
    global ve_default
    global ve_lve
    global ve_defaults
    global ve_package
    global ve_binary
    global ve_enter_by_name
    global ubc

    return {'ve_cfg': ve_cfg, 've_lveconfig': ve_lveconfig,
            've_default': ve_default,
            've_lve': ve_lve, 've_defaults': ve_defaults,
            've_package': ve_package, 'ubc': ubc,
            've_enter_by_name': ve_enter_by_name}


def guess_reseller_by_package(package):
    reseller = []
    global packages_users
    pkg_users_old = packages_users.copy()
    GetControlPanelUsers('list-resellers-packages')
    reseller_packages = packages_users.copy()
    packages_users = pkg_users_old.copy()

    # reseller_packages: {'reseller_name': ['pack1', 'pack2']}
    for reseller_name, packages_list in reseller_packages.items():
        for package_name_in_key in packages_list:
            if package == package_name_in_key:
                reseller.extend([reseller_name])
    return reseller


# LU-400
def call_endurance_custom_script(args):
    """
    Call Endurance's custom script

    :param args: list of arguments for pass to Endurance's custom script
    :return: None

    """

    endurance_custom_script = cldetectlib.get_param_from_file(cldetectlib.CL_CONFIG_FILE,
                                                              'ENDURANCE_CUSTOM_SCRIPT',
                                                              separator='=')
    if endurance_custom_script and os.path.isfile(endurance_custom_script):

        ret_code, std_out = exec_utility(endurance_custom_script, args)
        if ret_code != 0:
            message = f'Error while executing Endurance\'s custom script\n{std_out}'
            if JSON:
                json_format('multi', ['ERROR', message])
            else:
                err_message = f"error: {message}"
                sys.stderr.write(f"{err_message}\n")
                sys.exit(ret_code)


def _page_to_memory_or_bytes(value):
    """
    Convert page value to human-readable value or bytes, depending on BYTES_FLAG;
    E.g.
    >>> _page_to_memory_or_bytes(1233254)  # BYTES_FLAG=False
    '100M'
    >>> _page_to_memory_or_bytes(1233254)  # BYTES_FLAG=True
    654321

    :type value: int
    :rtype: str | int
    """
    if BYTES_FLAG:
        return int(round(value * mmap.PAGESIZE))
    return _mb_mem(value)


def remove_absent_resellers():
    """
    Remove from LVE all resellers, which are absent from panel
    :return: None
    """
    # Build resellers ids list for removal
    reseller_id_list_for_delete = []
    # Get resellers list from cpapi
    # Create a list from a generator for repeated membership testing
    cpapi_resellers_list = list(lve.map.resellers())
    get_XML_cfg()  # for loading reseller_name<=>reseller_id map form ve.cfg
    # If reseller present in LVE, but absent in panel - add it to list for removal
    for lve_reseller_id in lvp_list():
        try:
            lve_reseller_name = lve.map.get_reseller_name(lve_reseller_id)
            if lve_reseller_name not in cpapi_resellers_list:
                # Reseller does not exist in panel, remove it from LVE
                reseller_id_list_for_delete.append(lve_reseller_id)
        except (KeyError, OSError, IOError):
            # No such user, remove it from LVE
            reseller_id_list_for_delete.append(lve_reseller_id)
    # Remove all selected resellers ignoring all errors (for example no such reseller)
    for reseller_id_for_delete in reseller_id_list_for_delete:
        _remove_reseller(reseller_id_for_delete)


def remove_absent_users():
    """
    Remove from LVE all users, which absent in system
    :return: None
    """

    for lve_id in lve.proc.map():
        try:
            pwd.getpwuid(lve_id)  # Check the existence of the user
        except KeyError:
            try:
                lve.lve_destroy(lve_id)  # Destroy lve, if user not exist
            except PyLveError:  # If lve not exist
                try:
                    lve.py.lve_create(lve_id)  # we create lve
                    lve.lve_destroy(lve_id)  # and destroy them again
                    # After destroing lve, mapping will be cleansed of absent lve_id
                except PyLveError:
                    pass