Mini Shell
#!/opt/cloudlinux/venv/bin/python3 -bb
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import configparser as ConfigParser
import csv
import fcntl
import os
import pwd
import re
import tempfile
from collections import defaultdict
from stat import S_IRGRP, S_IROTH, S_IRUSR, S_IWUSR, ST_DEV
from typing import Dict, List, Optional, Tuple
import clcontrollib
import cldetectlib
from clcommon import FormattedException
# pylint: enable=E0611
from clcommon.clpwd import ClPwd
from clcommon.clquota import check_quota_enabled
from clcommon.cpapi import admin_packages, list_users, resellers_packages
from clcommon.cpapi.cpapiexceptions import CPAPIExternalProgramFailed, EncodingError
from clcommon.utils import (
ExternalProgramFailed,
get_file_lines,
get_filesystem_type,
run_command,
write_file_lines,
)
IS_DA = clcontrollib.detect.is_da()
DEFAULT_PACKAGE = 'default'
VE_DEFAULT_PACKAGE = 'VE_DEFAULT' # virtual package, alias for uid=0
class NoSuchPackageException(Exception):
def __init__(self, package):
Exception.__init__(self, "No such package (%s)" % (package,))
class NoSuchUserException(Exception):
def __init__(self, user):
Exception.__init__(self, "No such user (%s)" % (user,))
class InsufficientPrivilegesException(Exception):
def __init__(self):
Exception.__init__(self, "Insufficient privileges")
class IncorrectLimitFormatException(Exception):
def __init__(self, limit):
Exception.__init__(self, "Incorrect limit format (%s)" %(limit,))
class MalformedConfigException(FormattedException):
"""
Raised when config files is malformed and
cl-quota is not able to work with it
"""
def __init__(self, error: ConfigParser.ParsingError):
super(MalformedConfigException, self).__init__({
'message':
"cl-quota can't work because for malformed config. "
"Please, contact CloudLinux support if you "
"need help with resolving this issue. "
"Details: %(error_message)s",
'context': dict(
error_message=str(error)
)
})
class GeneralException(Exception):
def __init__(self, message):
Exception.__init__(self, message)
class QuotaDisabledException(Exception):
def __init__(self):
super(QuotaDisabledException, self).__init__('Quota disabled for all users on server')
class UserQuotaDisabledException(QuotaDisabledException):
"""
Raised when quota is disabled for one particular user
"""
def __init__(self, uid=None, homedir=None, message=None):
all_msg = 'Quota disabled'
if uid:
all_msg += ' for user id %s' % uid
if homedir:
all_msg += ' (home directory %s)' % homedir
if message:
all_msg += '; %s' % message
Exception.__init__(self, all_msg)
def _is_sys_path(path):
"""
>>> _is_sys_path('/home/username')
False
>>> _is_sys_path('/var/davecot')
True
"""
if path[-1] != '/':
path += '/'
sys_path_ = ('/root/', '/usr/', '/var/', '/sbin/', '/dev/', '/bin/', '/srv/', '/sys/', '/etc/ntp/')
if path == '/':
return True
for path_ in sys_path_:
if path.startswith(path_):
return True
def _get_users_list():
"""
Return no system users uid list
"""
cl_pwd = ClPwd()
pw_dict = cl_pwd.get_user_dict()
users_uid = [pw_dict[usr].pw_uid for usr in pw_dict if not _is_sys_path(pw_dict[usr].pw_dir)]
return users_uid
def is_quota_inheritance_enabled() -> bool:
"""
Check `cl_quota_inodes_inheritance` parameter in the config file
"""
res = cldetectlib.get_boolean_param(cldetectlib.CL_CONFIG_FILE, 'cl_quota_inodes_inheritance', default_val=False)
return res
class QuotaWrapper(object):
"""
Base quota class for inode quotas handling
* Update system quotas via setquota
* Retrieves system quotas via repquota
* Stores current quotas in /etc/container/cl-quotas.dat file
* Maintaines /etc/container/cl-quotas.cache file with resolved quotas. That file can be read by non-privileged users
"""
PROC_MOUNTS = '/proc/mounts'
QUOTASYNC = '/usr/bin/quotasync'
SETQUOTA = '/usr/sbin/setquota'
REPQUOTA = '/usr/sbin/repquota'
GETPACKS = '/usr/bin/getcontrolpaneluserspackages'
DATAFILE = '/etc/container/cl-quotas.dat'
CACHEFILE = '/etc/container/cl-quotas.cache'
# File lock variables
LOCK_FD = None
LOCK_FILE = DATAFILE + '.lock'
LOCK_WRITE = False
def __init__(self):
self._assert_file_exists(QuotaWrapper.PROC_MOUNTS)
self._assert_file_exists(QuotaWrapper.REPQUOTA)
self._assert_file_exists(QuotaWrapper.SETQUOTA)
self._quota_enabled_list = list()
self._panel_present = None
self._grace = {}
self._quota = {}
self._device_quota = {}
self._package_to_uids_map = {} # type: Dict[str, List[str]]
self._uid_to_packages_map = {} # type: Dict[str, List[str]]
self._uid_to_homedir_map = {} # type: Dict[str, str]
self._dh = self._get_saved_data_handler()
self._fields = ['bytes_used', 'bytes_soft', 'bytes_hard', 'inodes_used', 'inodes_soft', 'inodes_hard']
self._euid = os.geteuid()
self._devices = self._load_quota_devices()
self._mountpoint_device_mapped = self._get_mountpoint_device_map(self._devices)
self._device_user_map = None
# List of all packages (all admin's packages + all reseller packages)
self._all_package_list = None
@staticmethod
def _assert_file_exists(path):
"""
Checks if command is present and exits if no
"""
if not os.path.exists(path):
raise RuntimeError('No such command (%s)' % (path,))
def __enter__(self):
return self
def __exit__(self, _type, _value, _traceback):
if self.LOCK_FD is not None:
self.LOCK_FD.close()
def get_user_limits(self, uid):
'''
Returns user limits converted to tuples
'''
return self._convert_data_to_tuples(self._get_current_quotas(uid))
def get_all_users_limits(self):
'''
Returns all user limits converted to tuples
'''
return self._convert_data_to_tuples(self._get_current_quotas())
def get_package_limits(self, package):
"""
:param packname: Package name for get limits. If None, returns all packages,
else - only supplied package
Returns package limits converted to tuples (called only from main)
"""
return self._convert_data_to_tuples(self._get_package_quotas(packname=package))
def get_all_packages_limits(self, package=None):
"""
Returns all packages limits converted to tuples (called only from main)
"""
return self._convert_data_to_tuples(self._get_package_quotas(packname=package, all_packages=True))
def _preprocess_limit(self, limit):
"""
Preprocessed passed limit: 'default' --> '0', 'unlimited' --> -1, else calls _check_limit
:param limit:
:return:
"""
if limit == 'default':
return '0'
if limit in ('unlimited', '-1'):
return '-1'
return self._check_limit(limit)
def _get_package_from_dh(self, package):
return self._dh.get('packages', package).split(':')
def _get_all_packages_with_limits(self, clean_dead_packages=False):
"""
Retrive all available packages with their limits
:param clean_dead_packages: if True - remove all nonexistent packages from cl-quotas.dat
:return: Dictionary: { 'package_name': (soft_limit, hard_limit) }
"""
# result dictionary
package_limits_dict = {}
# Load packages limits from cl-quota.dat
db_packages = {}
if self._dh.has_section('packages') and len(self._dh.items('packages')) > 0:
list_of_packages = self._get_all_package_list()
for package in self._dh.options('packages'):
if clean_dead_packages and package not in list_of_packages:
self._dh.remove_option('packages', package)
continue
package_limits = self._get_package_from_dh(package)
# Pass package, if limits not well-formed
if len(package_limits) != 2:
continue
db_packages[package] = package_limits[0], package_limits[1]
if clean_dead_packages:
self._write_data()
# Put all panel packages to result dictionary
self._get_package_to_users_map()
for package in self._package_to_uids_map.keys():
if package in db_packages:
# if package present in cl-quota.dat, take limits
package_limits_dict[package] = db_packages[package]
else:
package_limits_dict[package] = ('0', '0')
return package_limits_dict
def set_user_limit(
self,
uid: str,
soft: Optional[str] = None,
hard: Optional[str] = None,
save: bool = True,
force_save: bool = False,
only_store: bool = False,
):
"""
Set limits for users
* Resolve limits according to those saved in cl-quota.dat
Limits are resolved in the following order:
user limits --> package limits --> root user (uid=0) limits
* Apply new limits when resolved limits differ from those in cache or when `force_save` is True
* Write updated values to cl-quota.dat if `save` is True
Always update cl-quota.dat in case both zeroes or both unlimited are provided
:param uid: user id
:param soft: soft limit value
:param hard: hard limit value
:param save: save limits to cl-quota.dat
:param force_save: save limits to cl-quota.dat even if they are not changed
:param only_store: store limits in memory, but do not apply them
:return: None
"""
self._check_admin()
# Validate limits value, convert 'unlimited' --> '-1', 'default' --> '0'
soft_validated, hard_validated = self._preprocess_limit(soft), self._preprocess_limit(hard)
# Derive limits from cl-quota.dat according to the limits inheritance rules
soft_resolved, hard_resolved = self._combine_user_limits(uid=uid, soft=soft_validated, hard=hard_validated)
# Convert limit to format acceptable by setquota utility
soft_converted, hard_converted = self._convert_for_sys_utility(soft=soft_resolved, hard=hard_resolved)
# Get data from repquota utility
cached = self._get_current_quotas(uid)[uid]
# Run cmd only if quota changed or force_save is True
# If force_save is True it equals to --save-all-paramters in cloudlinux-limits
if (soft_converted, hard_converted) != (cached["inodes_soft"], cached["inodes_hard"]) or force_save:
# Don't apply limits to root user
if uid != '0':
device = self._get_home_device(self._fetch_homedir(uid))
if only_store:
stdin = f'{uid} {cached["bytes_soft"]} {cached["bytes_hard"]} {soft_converted} {hard_converted}\n'
self._device_quota[device] = self._device_quota.get(device, '') + stdin
else:
cmd = [
QuotaWrapper.SETQUOTA,
'-u', uid, cached['bytes_soft'], cached['bytes_hard'], soft_converted, hard_converted, device,
]
run_command(cmd)
self._sync_quota_files(device)
if save:
soft_user_dat, hard_user_dat = self._get_user_limits_to_save(
uid, soft_validated, hard_validated, force_save=force_save)
self._save_user_limits(uid, soft_user_dat, hard_user_dat)
# Always update cl-quota.dat in case both zeroes or both unlimited are provided
if (soft_validated == '0' and hard_validated == '0') or (soft_validated == '-1' and hard_validated == '-1'):
self._save_user_limits(uid, soft_validated, hard_validated)
if uid == '0':
self._apply_all_limits()
def set_package_limit(
self,
package: str,
soft: Optional[str] = None,
hard: Optional[str] = None,
save: bool = True,
only_store: bool = False
) -> None:
"""
Sets limits for package
:param package: package name
:param soft: soft limit value
:param hard: hard limit value
:param save: save limits to cl-quota.dat
:param only_store: store limits in memory, but do not apply them
:return: None
"""
self._check_admin()
# Validate limits value, convert 'unlimited' --> '-1', 'default' --> '0'
soft_validated, hard_validated = self._preprocess_limit(soft), self._preprocess_limit(hard)
# Set limits for empty reseller package
if save is True \
and package in self._get_package_quotas(all_packages=True) \
and package not in self._get_package_to_users_map():
# Drive limits from cl-quota.dat according to the limits inheritance rules
soft_resolved, hard_resolved = self._get_saved_package_limits_if_none(
package, soft_validated, hard_validated)
self._save_package_limits(package, soft_resolved, hard_resolved)
return
if not self._check_package_exists(package):
return
# Example: {'/dev/sda1': ['502', '504', '515', '521', '501']}
device_user_map = self._get_device_user_map()
cached_quotas = self._get_current_quotas()
for device in device_user_map.keys():
std_in = []
for uid in self._get_package_to_users_map(package):
if uid not in device_user_map[device]:
continue
soft_resolved, hard_resolved = self._combine_package_limits(
uid=uid, package=package, soft=soft_validated, hard=hard_validated)
soft_converted, hard_converted = self._convert_for_sys_utility(soft=soft_resolved, hard=hard_resolved)
try:
soft_cached, hard_cached = cached_quotas[uid]['inodes_soft'], cached_quotas[uid]['inodes_hard']
if (soft_converted, hard_converted) != (soft_cached, hard_cached):
std_in.append(
f'{uid} '
f'{cached_quotas[uid]["bytes_soft"]} {cached_quotas[uid]["bytes_hard"]} '
f'{soft_converted} {hard_converted}'
)
except KeyError:
pass # skip error when quota is on but not configured
if len(std_in) != 0:
std_in = ('\n'.join(std_in) + '\n')
self._device_quota[device] = self._device_quota.get(device, '') + std_in
if save:
soft_data, hard_data = self._get_package_limits_to_save(package, soft_validated, hard_validated)
self._save_package_limits(package, soft_data, hard_data)
if not only_store:
self._flush_device_quota()
def synchronize(self):
"""
Read limits from file and applies them to packages and users
"""
self._check_admin()
package_limits = self._get_all_packages_with_limits(clean_dead_packages=True)
for package, (soft, hard) in package_limits.items():
self.set_package_limit(package, soft, hard, save=False, only_store=True)
self._remove_unexisting_users()
self._flush_device_quota()
def save_user_cache(self):
"""
Caches the limits to non-privileged user to see them
"""
self._check_admin()
# get data from repquota utility
current_quotas = self._get_current_quotas()
# form 2d array for writing to file
cache_content = [
[k] + [current_quotas[k][field] for field in self._fields]
for k in sorted(current_quotas.keys(), key=int)
]
self._get_global_lock(True)
file_handler = self._prepare_writer(QuotaWrapper.CACHEFILE)
csv_out = csv.writer(file_handler, quoting=csv.QUOTE_MINIMAL)
csv_out.writerows(cache_content)
self._end_writer(QuotaWrapper.CACHEFILE)
self._release_lock()
def _apply_all_limits(self, skip_root: bool = True):
"""Set limits for all users. Skip root user if skip_root is True"""
for uid in self._get_uid_to_packages_map().keys():
if uid == '0' and skip_root:
continue
self.set_user_limit(uid, soft=None, hard=None, save=False, only_store=True)
self._flush_device_quota()
def _flush_device_quota(self):
"""Write all device quotas to disk"""
quotas_written: bool = False
for device in self._device_quota.keys():
cmd = [QuotaWrapper.SETQUOTA, '-bu', device]
run_command(cmd, std_in=self._device_quota[device])
quotas_written = True
if quotas_written:
self._sync_quota_files()
self._device_quota = {}
def _sync_quota_files(self, device: str | None = None):
"""
In order to sync inodes limits in kernel with limits in user.quota file run `quotasync` command.
Otherwise `repquota` called right after `setquota` may return old limits existed before `setquota` call.
Skipped on the XFS filesystem because XFS does not require a separate quota synchronization step
as it handles these operations in real-time. Additionally, the specific functionality required by `quotasync`
to sync the disk quota information is not implemented, resulting in an error.
"""
if device is not None:
fs_type = get_filesystem_type(device)
if fs_type.lower() == 'xfs':
return
cmd = [QuotaWrapper.QUOTASYNC, device]
else:
cmd = [QuotaWrapper.QUOTASYNC, '-a']
run_command(cmd)
def _remove_unexisting_users(self):
"""Remove all records from cl-quota.dat for users which do not exist"""
if self._dh.has_section('users'):
for uid in self._dh.options('users'):
try:
# Check user presence
self._fetch_homedir(uid)
except NoSuchUserException:
self._dh.remove_option('users', uid)
self._write_data()
def _check_package_exists(self, package):
"""Check whether package exists"""
try:
self._get_package_to_users_map(package)
except NoSuchPackageException:
return False
else:
return True
def _get_user_limits_to_save(
self,
uid: str,
soft_validated: Optional[str],
hard_validated: Optional[str],
force_save: bool = False,
) -> Tuple[Optional[str], Optional[str]]:
"""
Derive package limit values to save to cl-quota.dat
If None passed as limit to method, then replace it by the user's value from cl-quota.dat
Update cl-quota.dat only if the derivation result changes
"""
soft_user_dat, hard_user_dat = self._get_user_limits(uid=uid)
soft_resolved, hard_resolved = self._combine_user_limits(uid=uid, soft=soft_validated, hard=hard_validated)
soft_none_resolved, hard_none_resolved = self._combine_user_limits(uid=uid, soft=None, hard=None)
if soft_resolved != soft_none_resolved or (force_save and soft_validated is not None):
soft_user_dat = soft_validated
if hard_resolved != hard_none_resolved or (force_save and soft_validated is not None):
hard_user_dat = hard_validated
return soft_user_dat, hard_user_dat
def _get_package_limits_to_save(
self, package: str, soft_validated: Optional[str], hard_validated: Optional[str]) -> Tuple[str, str]:
"""Derive package limit values to save to cl-quota.dat"""
p_soft, p_hard = self._get_package_limits(package=package)
# If new value is provided, then update package limit
p_soft = soft_validated if soft_validated is not None else p_soft
p_hard = hard_validated if hard_validated is not None else p_hard
return p_soft, p_hard
def _check_present_panel(self):
"""
Return True if control panel present
"""
if self._panel_present is None:
self._panel_present = 'Unknown' != run_command(['/usr/bin/cldetect', '--detect-cp-nameonly']).rstrip()
return self._panel_present
def _check_admin(self):
'''
Raise exception if no admin user
'''
if self._euid != 0:
raise InsufficientPrivilegesException()
def _get_saved_data_handler(self) -> ConfigParser.ConfigParser:
'''
Gets ConfigParser handler for future use
Loads saved quotas from /etc/container/cl-quotas.dat file
'''
self._get_global_lock(True)
dh = ConfigParser.ConfigParser(interpolation=None, strict=False)
dh.optionxform = str
try:
dh.read(QuotaWrapper.DATAFILE)
except ConfigParser.ParsingError as e:
raise MalformedConfigException(e)
finally:
self._release_lock()
return dh
def _get_device_user_map(self):
"""
Returns dictionary mapping devices to lists of users
"""
if self._device_user_map is not None:
return self._device_user_map
devices_map = {}
device_user_pairs = []
for uid in self._get_list_of_uids():
try:
device_user_pairs.append((self._get_home_device(self._fetch_homedir(uid)), uid))
except KeyError:
continue
for pair in device_user_pairs:
if pair[0] not in devices_map:
devices_map[pair[0]] = []
devices_map[pair[0]].append(pair[1])
self._device_user_map = devices_map
return self._device_user_map
def _check_limit(self, limit: Optional[str]) -> Optional[str]:
if limit is None or limit == '-1':
return limit
limit_pattern = re.compile(r'(\d+)')
pattern_match = limit_pattern.search(limit)
if not pattern_match:
raise IncorrectLimitFormatException(limit)
return pattern_match.group(1)
def _combine_user_limits(self, uid: str, soft: Optional[str] = None, hard: Optional[str] = None) -> Tuple[str, str]:
"""
Determines user limits by resolving them according to the limits inheritance rule:
user limits ---(overridden by provided as method param if provided not None)--->
---> package limits ---> root user (uid=0) limits
uid: user id
soft: Optional[str] = None: limit value, can be:
* None -- value not passed
* "-1" -- unlimited
* "0" -- default (next value from hierarchy should be considered)
* "1", "2", ... -- precice limit values
hard: soft: Optional[str] = None: limit value, values range the same as for soft
return: Tuple[str, str]: (soft_limit, hard_limit), they can be:
* "-1" -- unlimited
* "1", "2", ... -- precice limit values
"""
if uid == '0':
soft, hard = self._get_user_limits_override_none(uid=uid, soft=soft, hard=hard)
soft, hard = (soft if soft != '0' else '-1', hard if hard != '0' else '-1')
return soft, hard
soft, hard = self._get_user_limits_override_none(uid=uid, soft=soft, hard=hard)
for package in self._get_uid_to_packages_map(uid):
soft, hard = self._get_package_limits_override_default(package=package, soft=soft, hard=hard)
soft, hard = self._get_user_limits_override_default(uid='0', soft=soft, hard=hard)
soft, hard = (soft if soft != '0' else '-1', hard if hard != '0' else '-1')
return soft, hard
def _get_user_limits_override_none(self, uid: str, soft: Optional[str] = None, hard: Optional[str] = None) -> Tuple[str, str]:
"""Get user limits from cl-quota.dat. If limit is None, then override it by user's limit
:param str uid: user id
:param Optional[str] soft: limit value, can be:
* None -- value not passed, should be overridden by user's limit
* "-1" -- unlimited
* "0" -- default (next value from hierarchy should be taken)
* "1", "2", ... -- values
:param Optional[str] hard: limit values same as for soft
:return Tuple[str, str]: derived limits
"""
user_soft, user_hard = self._get_user_limits(uid=uid)
# Override by passed limits values
soft = self._check_limit(limit=soft if soft is not None else user_soft)
hard = self._check_limit(limit=hard if hard is not None else user_hard)
return soft, hard
def _get_user_limits_override_default(self, uid: str, soft: str, hard: str) -> Tuple[str, str]:
"""Get user limits from cl-quota.dat. If limit is default, then override it by user's limit
:param str uid: user id
:param Optional[str] soft: limit value, can be:
* "-1" -- unlimited
* "0" -- default (next value from hierarchy should be taken)
* "1", "2", ... -- values
:param Optional[str] hard: limit values same as for soft
:return Tuple[str, str]: derived limits
"""
user_soft, user_hard = self._get_user_limits(uid=uid)
# Override by passed limits values
soft = self._check_limit(limit=soft if soft != '0' else user_soft)
hard = self._check_limit(limit=hard if hard != '0' else user_hard)
return soft, hard
def _get_user_limits(self, uid: str) -> Tuple[str, str]:
"""Try to get user's limits from cl-quota.dat"""
try:
user_soft, user_hard = self._dh.get('users', uid).split(':')
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
user_soft, user_hard = '0', '0'
return user_soft, user_hard
def _get_package_limits_override_default(self, package: str, soft: str, hard: str) -> Tuple[str, str]:
"""Get package limits from cl-quota.dat. If passed limit is default, then override it by package's limit"""
pack_soft, pack_hard = self._get_package_limits(package=package)
soft = self._check_limit(limit=pack_soft if soft == '0' else soft)
hard = self._check_limit(limit=pack_hard if hard == '0' else hard)
return soft, hard
def _get_package_limits(self, package: str) -> Tuple[str, str]:
"""Try to get package's limits from cl-quota.dat"""
try:
soft, hard = self._get_package_from_dh(package)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
soft, hard = '0', '0'
return soft, hard
@staticmethod
def _convert_for_sys_utility(soft: Optional[str], hard: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
"""Converts limits for setquota utility which threats 0 as unlimited"""
if soft == '-1':
soft = '0'
if hard == '-1':
hard = '0'
return soft, hard
def _combine_package_limits(
self, package: str, uid: str, soft: Optional[str], hard: Optional[str]) -> Tuple[str, str]:
"""
Determines package limits taking into account saved user and default ones
"""
u_soft, u_hard = self._get_user_limits(uid=uid)
if IS_DA and is_quota_inheritance_enabled():
# Check the real user's package and save his quotas (instead of setting `DEFAULT` package ones)
# This is only DA's specificall
da_real_package = self._get_da_real_package(uid=uid)
if da_real_package != package:
da_real_quotas = self._get_package_quotas(packname=da_real_package, all_packages=True)
soft = p_soft = da_real_quotas[da_real_package]['inodes_soft']
hard = p_hard = da_real_quotas[da_real_package]['inodes_hard']
else:
p_soft, p_hard = self._get_package_limits(package=package)
else:
p_soft, p_hard = self._get_package_limits(package=package)
# Combine package limits with new package limits
# If user limit is non-default, take it, otherwise use package limit
# Override package limit by the new one if it's possible
soft = u_soft if u_soft != '0' else (soft if soft is not None else p_soft)
hard = u_hard if u_hard != '0' else (hard if hard is not None else p_hard)
# If package limits absent, use default limits
soft, hard = self._get_user_limits_override_default(uid='0', soft=soft, hard=hard)
soft, hard = (soft if soft != '0' else '-1', hard if hard != '0' else '-1')
return soft, hard
def _get_saved_package_limits_if_none(self, package, soft=None, hard=None):
"""
Applies saved package limits if none has been passed
"""
try:
pack_soft, pack_hard = self._get_package_from_dh(package)
if soft is None and pack_soft != '0':
soft = pack_soft
if hard is None and pack_hard != '0':
hard = pack_hard
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
pass
soft = self._check_limit(soft)
hard = self._check_limit(hard)
return soft, hard
def _get_da_real_package(self, uid: str) -> str:
"""
Get real package name for DA user
:param str uid: user id
:return str: retrieved package name
"""
username = ClPwd().get_names(int(uid))[0]
return clcontrollib.DirectAdmin()._get_user_package(username)
def _get_current_quotas(self, uid=None):
"""
Retrieves current quotas.
If euid == 0, use data from repquota utility, else from /etc/container/cl-quotas.cache file
"""
if self._euid != 0:
return self._load_user_cache()
if not self._quota:
# Retrieves quotas from repquota utility
self._quota = self._load_current_quotas()
if uid:
try:
return {uid: self._quota[uid]}
except KeyError:
self._check_if_quota_enabled(uid)
raise NoSuchUserException(uid)
return self._quota
def _get_package_quotas(self, packname=None, all_packages=False):
"""
Prepares package limits data for outputting
(call only from get_package_limits/get_all_packages_limits - main)
:param packname: Package name for get limits. If present, function returns
limits only for this package, else - all packages
:param all_packages: If False reads only used and admin's packages, True - all packages
(including reseller packages without users)
:return Dictionary of package limits:
{package_name: {'inodes_used': 'xxx', 'inodes_soft': 'yyy', 'inodes_hard': 'zzz'}
"""
q = {}
if all_packages:
# Get list of all packages
list_of_packages = self._get_all_package_list()
else:
# Get list of used packages + all admin's packages
list_of_packages = self._get_list_of_packages()
for package in list_of_packages:
values = ['-']
try:
if package == VE_DEFAULT_PACKAGE:
# Because "VE_DEFAULT" package is not a real package and just
# uses limits from LVE == 0 we should read it's limits
# from there
soft, hard = self._dh.get('users', '0').split(':')
else:
soft, hard = self._dh.get('packages', package).split(':')
soft = self._check_limit(soft)
hard = self._check_limit(hard)
if soft == '-1':
soft = '-'
if hard == '-1':
hard = '-'
values.extend([soft, hard])
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
values.extend(['0', '0'])
q.update(self._populate(package, values))
if packname:
try:
return {packname: q[packname]}
except KeyError:
raise NoSuchPackageException(packname)
return q
def _populate(self, item, data):
return {item: dict(list(map((lambda x: (x[1], data[x[0]])), enumerate(self._fields[3:]))))}
def _get_list_of_packages(self):
return list(self._get_package_to_users_map().keys())
def _get_list_of_uids(self):
return list(self._get_uid_to_packages_map().keys())
def _get_package_to_users_map(self, package=None):
if not self._package_to_uids_map:
self._package_to_uids_map = self._load_package_uids_data()
if package:
try:
return self._package_to_uids_map[package]
except KeyError:
raise NoSuchPackageException(package)
return self._package_to_uids_map
def _check_if_quota_enabled(self, uid):
if uid in self._quota_enabled_list:
return
home_dir = self._fetch_homedir(uid)
quota_disabled_message = check_quota_enabled(path=home_dir)
if quota_disabled_message:
raise UserQuotaDisabledException(uid=uid, homedir=home_dir, message=quota_disabled_message)
else:
self._quota_enabled_list.append(uid)
def _get_uid_to_packages_map(self, uid=None):
if not self._uid_to_packages_map:
self._package_to_uids_map = self._load_package_uids_data()
if uid:
try:
return self._uid_to_packages_map[uid]
except KeyError:
raise NoSuchUserException(uid)
return self._uid_to_packages_map
def _get_packages_uids_from_cpapi(self) -> Dict[str, List[str]]:
"""
Retrieve package-uids map from cpapi. Only for custom panels. See LU-610 for details.
Null packages coming from cpapi are considered to be 'default' package.
:return: Dictionary with data.
Example response:
{'default': ['1038', '1043', '1046'], 'res1_pack1': ['1044'], 'pack1': ['1042']}
Coorresponding self._uid_to_packages_map value:
{'1038': ['default'], '1042': ['pack1'], '1043': ['default'], '1044': ['res1_pack1'], '1046': ['default']}
"""
try:
users_packages = list_users()
except (OSError, CPAPIExternalProgramFailed, EncodingError) as e:
raise ExternalProgramFailed('%s. Can not get users' % (str(e)))
# Example of users_packages:
# {1000: {'reseller': 'root', 'package': 'Package1'},
# 1001: {'reseller': 'res1', 'package': 'BusinessPackage'},
# 1002: {'reseller': 'root', 'package': None}}
packages_users = defaultdict(list) # type: Dict[str, List[str]]
self._uid_to_packages_map = defaultdict(list) # type: Dict[str, List[str]]
for uid, uid_data in users_packages.items():
s_uid = str(uid)
package = uid_data['package'] if uid_data['package'] is not None else VE_DEFAULT_PACKAGE
packages_users[package].append(s_uid)
self._uid_to_packages_map[s_uid].append(package)
try:
admin_pkgs = admin_packages(raise_exc=True)
except (OSError, CPAPIExternalProgramFailed) as e:
raise ExternalProgramFailed('%s. Can not get admin packages' % (str(e)))
for package in admin_pkgs:
packages_users.setdefault(package if package is not None else VE_DEFAULT_PACKAGE, [])
packages_users.setdefault(VE_DEFAULT_PACKAGE, [])
return packages_users
def _load_package_uids_data(self) -> Dict[str, List[str]]:
"""
Gets map of packages and users
:rtype dict
:return Dictionary with data. Example:
{'default': ['1038', '1043', '1046'], 'res1_pack1': ['1044'], 'pack1': ['1042']}
"""
packages = {}
if self._euid != 0:
return packages
# if packages not supported all user has 'VE_DEFAULT' package
if not self._check_present_panel():
packages[VE_DEFAULT_PACKAGE] = list(map(str, _get_users_list()))
self._uid_to_packages_map = {i: [VE_DEFAULT_PACKAGE] for i in packages[VE_DEFAULT_PACKAGE]}
return packages
return self._get_packages_uids_from_cpapi()
def _get_all_package_list(self):
"""
Retrives all (root and resellers) panel package list
:return: List of package names
"""
# If list already loaded - do nothing
if self._all_package_list:
return self._all_package_list
try:
self._all_package_list = []
list_admin_packages = admin_packages(raise_exc=True)
for package in list_admin_packages:
self._all_package_list.append(package)
except (OSError, CPAPIExternalProgramFailed) as e:
raise ExternalProgramFailed('%s. Can not get admin packages' % (str(e)))
try:
dict_resellers_packages = resellers_packages(raise_exc=True)
for packages_list in dict_resellers_packages.values():
for package in packages_list:
self._all_package_list.append(package)
except (OSError, CPAPIExternalProgramFailed) as e:
raise ExternalProgramFailed('%s. Can not get reseller packages' % (str(e)))
# Add 'VE_DEFAULT' package to list
if VE_DEFAULT_PACKAGE not in self._all_package_list:
self._all_package_list.append(VE_DEFAULT_PACKAGE)
return self._all_package_list
def _convert_data_to_tuples(self, data):
'''
Convert dict to tuples for passing to printing routines
'''
for key in data.keys():
try:
entry = tuple(map((lambda x: (x, data[key][x])), self._fields[3:]))
data[key] = entry
except KeyError:
continue
return data
def _load_current_quotas(self):
"""
Gets current quota settings from repqouta utility for further processing
"""
q = {}
device = None
devices = self._devices
cmd = [QuotaWrapper.REPQUOTA, '-una']
data = run_command(cmd)
grace_regex_pattern = re.compile(r'(block|inode)\sgrace\stime:?\s(\d[\w:]+)(?:;|$|\s)', re.IGNORECASE)
for line in data.splitlines():
if line.startswith('#'):
if not device:
continue
parts = line.split()
if len(parts) != 8:
parts = self._remove_redundant_fields_from_input(parts)
uid = parts[0][1:]
if uid == '0': # We do not want to limit root :)
continue
try:
if device not in devices:
device = self._find_unknown_device(device)
if device in devices and self._is_home_device(self._fetch_homedir(uid), device):
q[uid] = dict(list(map((lambda x: (self._fields[x[0]], x[1])), enumerate(parts[2:]))))
except (KeyError, IndexError, NoSuchUserException):
continue
elif line.startswith('***'):
device = line[line.find('/dev'):].strip()
elif 'grace' in line:
found = grace_regex_pattern.findall(line)
if found:
self._grace.update(dict(list(map((lambda x: (x[0].lower(), x[1])), found))))
q.update(self._add_default())
return q
def _remove_redundant_fields_from_input(self, parts):
stripped_parts = parts[:2]
is_digit_pattern = re.compile(r'^\d+$')
stripped_parts.extend(
[field for field in parts[2:] if is_digit_pattern.search(field)])
return stripped_parts
def _fetch_homedir(self, uid):
if len(self._uid_to_homedir_map) == 0:
self._uid_to_homedir_map.update({str(entry.pw_uid): entry.pw_dir for entry in pwd.getpwall()})
try:
return self._uid_to_homedir_map[uid]
except KeyError:
raise NoSuchUserException(uid)
def _load_quota_devices(self):
"""
Gets mounted filesystems list and picks ones with quota on
Example of returned data structure:
{'/dev/mapper/VolGroup-lv_root': [
{'mountpoint': '/', 'quota_file': 'quota.user', 'quota_type': 'vfsv0'},
{'mountpoint': '/var', 'quota_file': 'quota.user', 'quota_type': 'vfsv0'}
],
'/dev/mapper/VolGroup-lv_root2': [
{'mountpoint': '/', 'quota_file': 'quota.user', 'quota_type': 'vfsv0'},
{'mountpoint': '/var', 'quota_file': 'quota.user', 'quota_type': 'vfsv0'}
]
}
"""
devices = {} # type: Dict[str, List[Dict[str, str]]]
proc_mounts_stream = open(QuotaWrapper.PROC_MOUNTS)
split_patt = re.compile(r' |,')
for line in proc_mounts_stream:
if line.startswith('rootfs /'):
continue
line_splited = split_patt.split(line)
device = line_splited[0]
mountpoint_data = {'mountpoint': line_splited[1]}
for line_splited_element in line_splited:
if line_splited_element.startswith('usrquota=') or line_splited_element.startswith('usruota='):
mountpoint_data['quota_file'] = line_splited_element.split('=')[1]
elif line_splited_element.startswith('jqfmt='):
mountpoint_data['quota_type'] = line_splited_element.split('=')[1]
if device in devices:
devices[device].append(mountpoint_data)
else:
devices[device] = [mountpoint_data]
proc_mounts_stream.close()
if len(devices) == 0:
# TODO: this only can happen when system HAS NO MOUNTS AT ALL
raise QuotaDisabledException()
return devices
def _load_user_cache(self):
'''
For non-privileged user we outputting data from the file
'''
q = {}
try:
self._get_global_lock()
fo = open(QuotaWrapper.CACHEFILE)
cvs_in = csv.reader(fo, delimiter=',')
except (OSError, IOError):
# We don't want to confuse a panel with error messages.
# Let the data be zeroes until they arrive
return {str(self._euid): dict.fromkeys(self._fields, '0')}
finally:
self._release_lock()
uid = str(self._euid)
for row in cvs_in:
if row[0] == uid:
q.update({row[0]: dict(list(map(
(lambda x: (self._fields[x],
row[x+1])),
range(len(self._fields)))))}) # pylint: disable=range-builtin-not-iterating
break
# We want to prevent crazy cases like misedited cache file
if not q:
return {str(self._euid): dict.fromkeys(self._fields, '0')}
return q
def _get_mountpoint_device_map(self, devices) -> List[Tuple[str, str]]:
"""
return list tuple ('mountpoin tpath', 'device') reverse sorted by deep mountpoint path
[('/mountpoint_path/path', '/device'), ('/mountpoint_path', '/device')]
"""
def sort_by_deep_path(device_mountpoint):
if device_mountpoint[0] == '/':
deep_path = 0
else:
deep_path = device_mountpoint[0].count('/')
return deep_path
mountpoint_device_map = []
for device, mountpoint_data_list in devices.items():
for mountpoint_data in mountpoint_data_list:
mountpoint_path = mountpoint_data['mountpoint']
mountpoint_device_map.append((mountpoint_path, device))
mountpoint_device_map.sort(key=sort_by_deep_path, reverse=True)
return mountpoint_device_map
def _get_home_device(self, home):
"""
Returns device user homedir is on
"""
def _add_slash(path):
if path and path[-1] != '/':
path += '/'
return path
dirname = _add_slash(os.path.dirname(home))
for mounpoint_path, device in self._mountpoint_device_mapped:
if dirname.startswith(_add_slash(mounpoint_path)):
return device
def _is_home_device(self, home, device):
"""
Checks if a device is user homedir device
"""
return self._get_home_device(home) == device
def _find_unknown_device(self, device):
try:
dev = os.stat(device)[ST_DEV]
dev_to_find = (os.major(dev), os.minor(dev))
for current_device in self._devices.keys():
dev = os.stat(current_device)[ST_DEV]
if dev_to_find == (os.major(dev), os.minor(dev)):
return current_device
except OSError:
return device
def _add_default(self):
"""
Insert 'default' quota.
Calls only from _load_current_quotas, after parsing repquota's output
"""
values = ['-', '0', '0', '-']
try:
user_soft, user_hard = self._dh.get('users', '0').split(':')
# Replace -1 to 0 for set unlimited limit
if user_soft == '-1':
user_soft = '0'
if user_hard == '-1':
user_hard = '0'
values.extend([user_soft, user_hard])
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
values.extend(['0', '0'])
return {'0': dict(list(map((lambda x: (x[1], values[x[0]])), enumerate(self._fields))))}
def _save_user_limits(self, uid, soft, hard):
"""
Saves user limits
"""
if soft is None:
soft = '0'
if hard is None:
hard = '0'
# remove user limits if they are equal to default
if soft == '0' and hard == '0' and self._dh.has_section('users'):
self._dh.remove_option('users', uid)
else:
if not self._dh.has_section('users'):
self._dh.add_section('users')
self._dh.set('users', uid, '%s:%s' % (soft, hard))
self._write_data()
def _save_package_limits(self, package, soft, hard):
"""
Saves package limits
"""
if soft is None:
soft = '0'
if hard is None:
hard = '0'
if soft == '0' and hard == '0' and self._dh.has_section('packages'):
self._dh.remove_option('packages', package)
else:
if not self._dh.has_section('packages'):
self._dh.add_section('packages')
self._dh.set('packages', package, '%s:%s' % (soft, hard))
self._write_data()
self._copy_package_limits_to_cpanel(package)
def _copy_package_limits_to_cpanel(self, package):
"""
Copy package quota limits from cl-quotas.dat to cpanel packages data
"""
if not cldetectlib.is_cpanel():
return # skip func if panel not cPanel
package_path = f'/var/cpanel/packages/{package}'
cpanel_package_lines = get_file_lines(package_path)
if len(cpanel_package_lines) == 0:
return # skip func if no cPanel package found
old_cpanel_data, modified_cpanel_lines = self._parse_cpanel_package_data(cpanel_package_lines)
if old_cpanel_data is None and modified_cpanel_lines is None:
return # skip func if no lve extension in package
# don't rewrite cpanel package file if new quotas for package are the same
quotas_data = self._get_package_quotas(package, all_packages=True)[package]
# unlimited quotas for package are indicated as '-',
# but in package we want to write '-1'
for key, value in quotas_data.items():
if value == '-':
quotas_data[key] = '-1'
old_cpanel_limits = (old_cpanel_data.get('inodes_soft', '0'), old_cpanel_data.get('inodes_hard', '0'))
current_quota_limits = (quotas_data['inodes_soft'], quotas_data['inodes_hard'])
if old_cpanel_limits == current_quota_limits:
return
for limit_type in ('inodes_soft', 'inodes_hard'):
limit_string = 'lve_' + str(limit_type) + '=' + str(quotas_data[limit_type]) + '\n'
modified_cpanel_lines.append(limit_string)
write_file_lines(package_path, modified_cpanel_lines, 'w')
@staticmethod
def _parse_cpanel_package_data(cpanel_package_lines):
"""
Process cpanel_package_lines - get values of all old lve_ limits
and remove lines with limits that would be changed
"""
cpanel_package_lines_modified = cpanel_package_lines[:]
old_cpanel_data = {}
for line in cpanel_package_lines:
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]
if limit_name in ('inodes_soft', 'inodes_hard'):
cpanel_package_lines_modified.remove(line)
if line.startswith('_PACKAGE_EXTENSIONS') and 'lve' not in line:
return None, None
return old_cpanel_data, cpanel_package_lines_modified
def _save_data(self, soft, hard, item, item_type):
'''
Saves data to a file
'''
if soft == '0' and hard == '0':
try:
self._dh.remove_option(item_type, item)
except ConfigParser.NoSectionError:
pass
else:
if not self._dh.has_section(item_type):
self._dh.add_section(item_type)
self._dh.set(item_type, item, '%s:%s' % (soft, hard))
self._write_data()
def _prepare_writer(self, filepath):
"""
Open temporary file for writing and return file object
"""
path = os.path.dirname(filepath)
try:
fd, temp_path = tempfile.mkstemp(prefix='lvetmp_', dir=path)
file_handler = os.fdopen(fd, 'w')
self._tmp = temp_path
return file_handler
except (IOError, OSError):
if os.path.exists(temp_path):
os.unlink(temp_path)
raise GeneralException("Could not save data")
def _end_writer(self, path):
'''
Routines after writing to file
'''
try:
mask = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
os.rename(self._tmp, path)
os.chmod(path, mask)
except OSError:
pass
def _write_data(self):
'''
Actual place of saving data to a file
'''
self._get_global_lock(True)
file_handler = self._prepare_writer(QuotaWrapper.DATAFILE)
self._dh.write(file_handler)
self._end_writer(QuotaWrapper.DATAFILE)
self._release_lock()
##########################
# File lock functions
def _get_global_lock(self, write=False):
if write:
QuotaWrapper.LOCK_WRITE = True
if QuotaWrapper.LOCK_FD is None:
try:
QuotaWrapper.LOCK_FD = open(QuotaWrapper.LOCK_FILE, 'r')
except (IOError, OSError):
raise GeneralException("Can't open lock file for reading")
try:
fcntl.flock(QuotaWrapper.LOCK_FD.fileno(), fcntl.LOCK_EX)
except IOError:
raise GeneralException("Can't get lock")
def _release_lock(self):
if (not QuotaWrapper.LOCK_WRITE) and (QuotaWrapper.LOCK_FD is not None):
QuotaWrapper.LOCK_FD.close()
QuotaWrapper.LOCK_FD = None
Zerion Mini Shell 1.0