Sindbad~EG File Manager
#!/opt/alt/python37/bin/python3 -bb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
#
# Author: Igor Seletskiy <iseletsk@cloudlinux.com>
# Alexander Grynchuk <agrynchuk@cloudlinux.com>
# Illarion Kovalchuk <ikovalchuk@cloudlinux.com>
#
# pylint: disable=too-many-lines
from __future__ import absolute_import
from __future__ import division
import itertools
import locale
import logging
import os
import pwd
import json
from datetime import datetime, timedelta
from email import message_from_string
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from itertools import groupby
from clcommon import cpapi, clproc, clemail
from clcommon.clproc import LIMIT_LVP_ID
from clcommon.cpapi import get_user_login_url, get_admin_locale, cpinfo, admins
from clcommon.cpapi.plugins.universal import get_admin_email as system_admin_email
from clconfig.lve_stats2_lib import get_notification as get_admin_notification
from prettytable import PrettyTable, ALL
from lveapi import NameMap
from typing import List, Optional, Tuple, Dict, Iterable, Iterator, NamedTuple # pylint: disable=unused-import
from lvestats.core.plugin import LveStatsPlugin
from lvestats.lib.commons import dateutil
from lvestats.lib.commons.func import merge_dicts, deserialize_lve_id, get_hostname, serialize_lve_id, gcd
from clcommon.mail_helper import MailHelper
from lvestats.lib.commons.sizeutil import mempages_to_bytes
from lvestats.lib.lveinfolib import get_lve_version, HistoryShowUnion, FIELD_FAULT, FIELD_AVERAGE, FIELD_LIMIT
from lvestats.lib.notifications_helper import NotificationsHelper
from lvestats.lib.commons.func import user_should_be_notified
from builtins import map
from functools import reduce
try:
# Just for backward compatibility with not released lve-utils
from clconfig.lve_stats2_reseller_lib import get_notification
except ImportError:
# Will results in a fallback to admin's settings:
def get_notification(reseller):
return None
DEFAULT_LOCALE = 'en_US'
ADMIN_TEMPL = 'admin_notify.txt'
ADMIN_TEMPL_HTML = 'admin_notify.html'
USER_TEMPL = 'user_notify.txt'
USER_TEMPL_HTML = 'user_notify.html'
RESELLER_FAULTS_TEMPL = 'reseller_faults_notify.txt'
RESELLER_FAULTS_TEMPL_HTML = 'reseller_faults_notify.html'
RESELLER_TEMPL = 'reseller_notify.txt'
RESELLER_TEMPL_HTML = 'reseller_notify.html'
DEFAULT_ADMIN_MAIL = system_admin_email()
DEFAULT_SUBJECT = 'Hosting account resources exceeded'
FIELDS_ALL = ['ID'] + FIELD_FAULT + ['anyF'] + FIELD_AVERAGE + FIELD_LIMIT
NotifySettings = NamedTuple('NotifySettings', [
('NOTIFY_MIN_FAULTS', int),
('NOTIFY_INTERVAL', int),
])
NotifyFaultsOptions = NamedTuple('NotifyFaultsOptions', [
('NOTIFY_CPU', bool),
('NOTIFY_IO', bool),
('NOTIFY_IOPS', bool),
('NOTIFY_MEMORY', bool),
('NOTIFY_EP', bool),
('NOTIFY_NPROC', bool),
])
AdminSettings = NamedTuple('AdminSettings', [
('NOTIFY_ADMIN', Optional[bool]),
('NOTIFY_CUSTOMERS_ON_FAULTS', Optional[bool]),
('NOTIFY_RESELLER_CUSTOMERS', Optional[bool]),
('NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS', Optional[bool]),
('NOTIFY_FAULTS_TYPES', NotifyFaultsOptions),
('NOTIFY_OPTIONS_SELF', NotifySettings),
('NOTIFY_OPTIONS_CUSTOMER', NotifySettings),
('NOTIFY_FROM_EMAIL', Optional[str]),
('REPORT_ADMIN_EMAIL', Optional[str]),
('NOTIFY_SUBJECT', str),
('NOTIFY_CHARSET_EMAIL', Optional[str]),
])
ResellerSettings = NamedTuple('ResellerSettings', [
('NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS', bool),
('NOTIFY_CUSTOMERS_ON_FAULTS', bool),
('NOTIFY_RESELLER_ON_TOTAL_FAULTS', bool),
('NOTIFY_FAULTS_TYPES', NotifyFaultsOptions),
('NOTIFY_OPTIONS_SELF', NotifySettings),
('NOTIFY_OPTIONS_CUSTOMER', NotifySettings),
])
class StatsNotifierTemplateError(clemail.jinja2.exceptions.TemplateError):
pass
class StatsNotifier(LveStatsPlugin):
DEFAULT_PERIOD = 12 * 60 * 60 # 12 hours
MIN_PERIOD = 1 * 60 * 60 # 1 hour
TEMPLATE_DIR = '/usr/share/lve/emails/'
TEMPLATE_CUSTOM_DIR = '/etc/cl.emails.d/'
ADMIN_LVP_ID = 0
LOCALE_DEFINES_FILE = 'locale_defines.json'
def __init__(self):
self.server_id = None
self.db_engine = None
self.lve_version = 6
self.log = logging.getLogger('statsnotifier')
self._load_plugin_settings()
self.hostname = get_hostname()
self._mail_helper = MailHelper()
self._reseller_settings_cache = {}
self._proc_lve = clproc.ProcLve()
self._name_map = NameMap()
try:
self.resellers = set(cpapi.resellers())
except (cpapi.NotSupported, AttributeError):
self.resellers = set()
self._notifications = NotificationsHelper()
def _load_plugin_settings(self):
"""[Re]load all plugin settings"""
self._admin_settings = self._get_admin_settings()
self._default_reseller_settings = self._get_default_reseller_settings()
# let's wake up at least every hour (minimal period for resellers)
# then we will iter over all resellers and do stuff
notify_interval_admin = self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL
notify_interval_users = self._admin_settings.NOTIFY_OPTIONS_CUSTOMER.NOTIFY_INTERVAL
self.period = reduce(
gcd, [notify_interval_admin, notify_interval_users, StatsNotifier.MIN_PERIOD])
# Set default email headers
self._prepare_default_mail_headers()
# Log plugin settings
self.log.info("Config: NOTIFY_ADMIN=%s", self._admin_settings.NOTIFY_ADMIN)
self.log.info("Config: NOTIFY_CUSTOMERS_ON_FAULTS=%s", self._admin_settings.NOTIFY_CUSTOMERS_ON_FAULTS)
self.log.info("Config: NOTIFY_RESELLER_CUSTOMERS=%s", self._admin_settings.NOTIFY_RESELLER_CUSTOMERS)
self.log.info("Config: NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS=%s", self._admin_settings.NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS)
self.log.info("Config: NOTIFY_CPU=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_CPU)
self.log.info("Config: NOTIFY_IO=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_IO)
self.log.info("Config: NOTIFY_IOPS=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_IOPS)
self.log.info("Config: NOTIFY_MEMORY=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_MEMORY)
self.log.info("Config: NOTIFY_EP=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_EP)
self.log.info("Config: NOTIFY_NPROC=%s", self._admin_settings.NOTIFY_FAULTS_TYPES.NOTIFY_NPROC)
self.log.info("Config: NOTIFY_INTERVAL_ADMIN=%s seconds", self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL)
self.log.info("Config: NOTIFY_MIN_FAULTS_ADMIN=%s", self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_MIN_FAULTS)
self.log.info("Config: NOTIFY_INTERVAL_USER=%s seconds", self._admin_settings.NOTIFY_OPTIONS_CUSTOMER.NOTIFY_INTERVAL)
self.log.info("Config: NOTIFY_MIN_FAULTS_USER=%s", self._admin_settings.NOTIFY_OPTIONS_CUSTOMER.NOTIFY_MIN_FAULTS)
self.log.info("Config: NOTIFY_FROM_EMAIL=%s", self._admin_settings.NOTIFY_FROM_EMAIL)
self.log.info("Config: REPORT_ADMIN_EMAIL=%s", self._admin_settings.REPORT_ADMIN_EMAIL)
self.log.info("Config: NOTIFY_SUBJECT=%s", self._admin_settings.NOTIFY_SUBJECT)
self.log.info("Config: NOTIFY_CHARSET_EMAIL=%s", self._admin_settings.NOTIFY_CHARSET_EMAIL)
self.log.info("Config: Email subject used if locale_defines.json absent: %s", self.mail_headers['Subject'])
def _get_resellers_settings(self, with_admin=False):
# type: (bool) -> Tuple[int, ResellerSettings]
# get notifications settings for each reseller that is present in proc/lve/list
if self._proc_lve.resellers_supported():
for reseller_id in self._proc_lve.lvp_id_list():
reseller_name = self._name_map.get_name(reseller_id)
if reseller_name is None:
self.log.warning("Unable to obtain notify settings: reseller %s exists "
"in /proc/lve/resellers, but is absent in ve.cfg.", reseller_id)
continue
reseller_settings = self.get_reseller_settings(reseller_name)
yield reseller_id, reseller_settings or self._default_reseller_settings
if with_admin:
yield self.ADMIN_LVP_ID, self._admin_settings
def _get_customers_options_filtered(self):
# type: () -> filter
"""
We do not need resellers with disabled
NOTIFY_CUSTOMERS_ON_FAULTS notifications,
also we do not need those resellers,
whose customers were notified recently.
Let's remove such resellers from check-list.
"""
resellers_settings = self._get_resellers_settings(with_admin=True)
# we do not need resellers that disables their customers notification
# also we do not need resellers that were notified recently
def _filter_by_enabled_notifications(reseller_notify_settings):
# type: (Tuple[int, ResellerSettings]) -> bool
(reseller_id, notify_options) = reseller_notify_settings
return notify_options.NOTIFY_CUSTOMERS_ON_FAULTS and \
self._notifications.users_need_notification(reseller_id,
notify_options.NOTIFY_OPTIONS_CUSTOMER.NOTIFY_INTERVAL)
return filter(_filter_by_enabled_notifications, # pylint: disable=filter-builtin-not-iterating
resellers_settings)
def _get_customers_options_grouped(self):
# type: () -> groupby
"""
If some resellers have same settings
for notify we can send notifications to
their users using only one database request.
For that, we must group them by customers settings
"""
def _group_by_customers_options(customers_notify_settings):
# type: (Tuple[str, Optional[ResellerSettings]]) -> Tuple[NotifySettings, NotifyFaultsOptions]
(_, notify_options) = customers_notify_settings
return notify_options.NOTIFY_OPTIONS_CUSTOMER, notify_options.NOTIFY_FAULTS_TYPES
return groupby(self._get_customers_options_filtered(), _group_by_customers_options)
def _get_resellers_options_filtered(self):
# type: () -> filter
"""
We do not need resellers with disabled notifications at all
also we do not need resellers that were notified recently.
Let's remove such resellers from check-list.
"""
resellers_settings = self._get_resellers_settings()
def _filter_by_enabled_notifications(reseller_notify_options):
# type: (Tuple[int, ResellerSettings]) -> bool
(reseller_id, notify_options) = reseller_notify_options
return notify_options.NOTIFY_RESELLER_ON_TOTAL_FAULTS \
and self._notifications.reseller_need_notification(reseller_id,
notify_options.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL)
return filter(_filter_by_enabled_notifications, # pylint: disable=filter-builtin-not-iterating
resellers_settings)
def _get_resellers_options_grouped(self):
# type: () -> groupby
"""
If some resellers have same settings
for notifywe can send them notifications
using only one database request.
For that, we must group them by settings
"""
resellers_settings = self._get_resellers_options_filtered()
def _group_by_self_options(reseller_options_grouped):
# type: (Tuple[int, Optional[ResellerSettings]]) -> Tuple[NotifySettings, NotifyFaultsOptions]
(_, notify_options) = reseller_options_grouped
return notify_options.NOTIFY_OPTIONS_SELF, notify_options.NOTIFY_FAULTS_TYPES
return groupby(resellers_settings, _group_by_self_options)
def _get_reseller_customers_faults_options_filtered(self):
# type: () -> filter
"""Get resellers with enabled NOTIFY_ON_CUSTOMERS_FAULTS"""
resellers_settings = self._get_resellers_settings(with_admin=True)
def _filter_by_enabled_notifications(resellers_settings):
# type: (Tuple[int, ResellerSettings]) -> bool
(reseller_id, notify_options) = resellers_settings
return notify_options.NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS and \
self._notifications.reseller_need_notification(reseller_id,
notify_options.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL)
return filter(_filter_by_enabled_notifications, # pylint: disable=filter-builtin-not-iterating
resellers_settings)
def _get_resellers_customers_faults_options_grouped(self):
# type: () -> groupby
resellers_settings = self._get_reseller_customers_faults_options_filtered()
def _group_by_self_options(reseller_customers_settings):
# type: (Tuple[int, ResellerSettings]) -> Tuple[NotifySettings, NotifyFaultsOptions]
(_, notify_options) = reseller_customers_settings
return notify_options.NOTIFY_OPTIONS_SELF, notify_options.NOTIFY_FAULTS_TYPES
return groupby(resellers_settings, _group_by_self_options)
def _prepare_default_mail_headers(self):
# type: () -> None
self.mail_headers = {
'Subject': self._admin_settings.NOTIFY_SUBJECT or DEFAULT_SUBJECT
}
def set_config(self, config):
self.server_id = config.get('server_id', 'localhost')
def set_db_engine(self, engine):
self.db_engine = engine
self.lve_version = get_lve_version(engine, self.server_id)
@staticmethod
def _get_panel_login_url(domain):
# type: (str) -> str
try:
return get_user_login_url(domain)
except cpapi.NotSupported:
return 'http://{domain}/'.format(domain=domain)
@staticmethod
def get_locales(localename):
# type: (str) -> str
return locale.normalize(localename).split('.')[0]
def get_users_fault(self, period, notify_min_faults, notify_faults_types, uids=None):
# type: (int, int, NotifyFaultsOptions, List[int]) -> List[Dict]
"""
Get statistics for given period;
"""
period_to = datetime.utcnow()
period_from = period_to - timedelta(seconds=period)
by_fault_arg = self._get_faults_keys(notify_faults_types)
if not by_fault_arg:
return []
history_show = HistoryShowUnion(
self.db_engine, period_from, period_to, server_id=self.server_id,
show_columns=FIELDS_ALL, order_by='anyF', by_fault=by_fault_arg,
threshold=notify_min_faults, uid=uids)
users_fault_data = history_show.proceed_dict()
users_stats = []
for user_stat in users_fault_data:
user_stat['PERIOD'] = period
# convert mempages to kbytes
user_stat.update(
{key: mempages_to_bytes(val) // 1024 for key, val in user_stat.items()
if key in ('aVMem', 'aPMem', 'lVMem', 'lPMem')})
# convert bytes/s to kbytes/s
user_stat.update(
{key: val // 1024 for key, val in user_stat.items()
if key in ('aIO', 'lIO')})
users_stats.append(user_stat)
return users_stats
@staticmethod
def _get_faults_keys(notify_types_options):
# type: (NotifyFaultsOptions) -> List[str]
"""
Get list of keys for 'by_faults';
If reseller_settings is not None, use admin settings;
Else - use reseller's settings;
"""
by_fault_arg = itertools.compress(
['CPUf', 'VMemF', 'PMemF', 'EPf', 'NprocF', 'IOf', 'IOPSf'],
[
notify_types_options.NOTIFY_CPU,
notify_types_options.NOTIFY_MEMORY, # deprecated
notify_types_options.NOTIFY_MEMORY,
notify_types_options.NOTIFY_EP,
notify_types_options.NOTIFY_NPROC,
notify_types_options.NOTIFY_IO,
notify_types_options.NOTIFY_IOPS])
return list(by_fault_arg)
def _get_resellers_data(self, faults_data):
# type: (Iterable[Dict]) -> Iterator[Dict]
"""
Get information about reseller's accounts;
"""
for reseller_data in faults_data:
reseller_id, _ = deserialize_lve_id(reseller_data['ID'])
reseller_data['LOGIN'] = self._name_map.get_name(reseller_id)
try:
cp_userinfo = cpapi.cpinfo(
reseller_data['LOGIN'],
keyls=('mail', 'locale'),
search_sys_users=False)[0]
except (cpapi.cpapiexceptions.NotSupported, IndexError):
continue
reseller_data['TOMAIL'] = cp_userinfo[0]
reseller_data['LOCALE'] = self.get_locales(cp_userinfo[1] or 'en')
yield reseller_data
def get_users_data(self, fault_users_data):
# type: (Iterable[Dict]) -> List[Dict]
"""
Get information about end user's accounts;
"""
users_data = []
for user_data in fault_users_data:
user_id, _ = deserialize_lve_id(int(user_data['ID']))
try:
user_pwd_data = pwd.getpwuid(user_id)
user_data['LOGIN'] = user_pwd_data.pw_name
user_data['TONAME'] = user_pwd_data.pw_gecos.split(',')[0]
except KeyError:
continue
try:
cp_userinfo = cpapi.cpinfo(
user_data['LOGIN'],
keyls=('mail', 'dns', 'locale', 'reseller'))[0]
except (IndexError, cpapi.cpapiexceptions.NotSupported):
continue
user_data['TOMAIL'] = cp_userinfo[0] # user's email
user_data['DOMAIN'] = cp_userinfo[1] # user's domain
user_data['LOCALE'] = self.get_locales(cp_userinfo[2] or 'en')
# Old key presents for backward compatibility
user_data['RESSELER'] = cp_userinfo[3] # reseller name
# Fixed reseller key without misspeling
# Now it works as it should be according to docs
user_data['RESELLER'] = cp_userinfo[3] # reseller name
user_data['HOSTNAME'] = self.hostname # server hostname
user_data['LOGIN_URL'] = self._get_panel_login_url(cp_userinfo[1])
user_data['FROMMAIL'] = self._get_notify_from_mail(user_data['RESSELER'])
users_data.append(user_data)
return users_data
def _get_notify_from_mail(self, reseller):
# type: (str) -> str
"""
If reseller is admin -> send email from admin's mail
If reseller is reseller -> send email from reseller's mail
"""
from_mail = self._admin_settings.NOTIFY_FROM_EMAIL
# probably we should replace condition with `not is_admin(reseller)`
if reseller in self.resellers:
try:
# TODO: add NOTIFY_FROM_EMAIL option for reseller
from_mail = cpapi.cpinfo(reseller,
keyls=('mail',),
search_sys_users=False)[0][0]
except (IndexError, cpapi.cpapiexceptions.NotSupported):
self.log.exception("can't obtain notify_from_mail for %s from cpapi", reseller)
return from_mail
def _detect_admin_email(self):
# type: () -> str
"""
Determine server admin email;
Priority:
1. report_admin_email from config
2. cpapi.get_admin_email()
3. DEFAULT_ADMIN_MAIL
"""
try:
email = self._admin_settings.REPORT_ADMIN_EMAIL or cpapi.get_admin_email()
except cpapi.NotSupported:
email = None
if not email:
email = DEFAULT_ADMIN_MAIL
self.log.warning(
"Can't obtain admin email from control panel. System admin email will be used '%s'" % email)
return email
def _detect_path_for_file(self, locale_name: str, templ_filename: str) -> Tuple[str, str]:
"""
Detects path for specified file. Checks directories in order:
1. self.TEMPLATE_CUSTOM_DIR = '/etc/cl.emails.d/<locale_name>'
2. self.TEMPLATE_DIR = '/usr/share/lve/emails/<locale_name>'
:param locale_name: locale name to check
:param templ_filename: template filename (without path)
:return: Tuple: (file_dir, full_path_to_file)
"""
# 1. check self.TEMPLATE_CUSTOM_DIR
templates_dir = os.path.join(self.TEMPLATE_CUSTOM_DIR, locale_name)
full_filename = os.path.join(templates_dir, templ_filename)
if os.path.exists(full_filename):
return templates_dir, full_filename
# 2. check self.TEMPLATE_DIR
templates_dir = os.path.join(self.TEMPLATE_DIR, locale_name)
return templates_dir, os.path.join(templates_dir, templ_filename)
def generate_msg_body(self, templ_data: dict, text_templ_name: str, html_templ_name: str = None):
locale_name = templ_data.get('LOCALE', DEFAULT_LOCALE)
templates_dir, text_templ_path = self._detect_path_for_file(locale_name, text_templ_name)
if not os.path.exists(templates_dir) or not os.path.exists(text_templ_path):
logging.info(
"Unable to find templates for locale '%s': file '%s' does not exist. "
"Statsnotifier will use default templates with locale %s. See "
"https://docs.cloudlinux.com/cloudlinux_os_components/#customize-lve-stats2-notifications"
" in order to find how to create localized template and hide this warning.",
locale_name, text_templ_path, DEFAULT_LOCALE)
templates_dir, text_templ_path = self._detect_path_for_file(DEFAULT_LOCALE, text_templ_name)
# html part is optional, just print warning when it is absent
if html_templ_name:
html_templ_path = os.path.join(templates_dir, html_templ_name)
if not os.path.exists(html_templ_path):
logging.info(
"Unable to find optional HTML message template '%s'. "
"Sending email with only TEXT part."
"You can safely ignore this message if you "
"do not want to use HTML email templates."
"See https://docs.cloudlinux.com/cloudlinux_os_components/#customize-lve-stats2-notifications",
html_templ_path)
html_templ_path = None
else:
html_templ_path = None
_templ_name = text_templ_name
# Load data from /usr/share/lve/emails/<locale_name>/locale_defines.json file
# locale_defines example:
# {'NOTIFY_FROM_SUBJECT': 'Ваш сервер болен',
# 'PERIOD': {'days': 'дней', 'hours': 'часов', 'minutes': 'минут', 'seconds': 'секунд'},
# 'TONAME': {'admin': 'Админ', 'reseller': 'Reseller', 'customer': 'Server user'}
# }
locale_defines = self._load_locales_data(templates_dir)
# Fill localized data according to self._locale_defines
try:
templ_data['TONAME'] = locale_defines['TONAME'][templ_data['user_type']]
except KeyError:
pass
try:
number, english_name = dateutil.seconds_to_human_view(templ_data['PERIOD'])
try:
localized_period_name = locale_defines['PERIOD'][english_name]
except KeyError:
localized_period_name = english_name
templ_data['PERIOD'] = f'{number} {localized_period_name}'
except KeyError:
pass
html_body = None
try:
subject, text_body = clemail.ClEmail.generate_mail_jinja2(text_templ_path, templ_data=templ_data)
if html_templ_path:
_templ_name = html_templ_name
_, html_body = clemail.ClEmail.generate_mail_jinja2(html_templ_path, templ_data=templ_data)
except (clemail.jinja2.exceptions.TemplateError, IOError) as e:
raise StatsNotifierTemplateError(
'Can not generate message for user "{}"; template "{}". '
'Jinja2: {}'.format(
templ_data.get('LOGIN'),
os.path.join(locale_name, _templ_name), str(e)))
# Put subject from locales
s_subject = locale_defines.get('NOTIFY_FROM_SUBJECT', subject)
return s_subject, text_body, html_body
def generate_msg(self, templ_data: dict, text_templ_name: str, html_templ_name: str = None) -> MIMEMultipart:
subject, text_body, html_body = self.generate_msg_body(templ_data, text_templ_name, html_templ_name)
if html_body: # generate multipart message text + html
# Clear "text" and "html" data from unreadable symbols
text_body = text_body \
.encode(self._admin_settings.NOTIFY_CHARSET_EMAIL, 'xmlcharrefreplace') \
.decode(self._admin_settings.NOTIFY_CHARSET_EMAIL)
html_body = html_body \
.encode(self._admin_settings.NOTIFY_CHARSET_EMAIL, 'xmlcharrefreplace') \
.decode(self._admin_settings.NOTIFY_CHARSET_EMAIL)
# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message,
# in this case
# the HTML message, is best and preferred.
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(text_body, 'plain', self._admin_settings.NOTIFY_CHARSET_EMAIL))
msg.attach(MIMEText(html_body, 'html', self._admin_settings.NOTIFY_CHARSET_EMAIL))
else: # generate simple message (text only)
text_body = text_body.encode(self._admin_settings.NOTIFY_CHARSET_EMAIL, 'xmlcharrefreplace') \
.decode(self._admin_settings.NOTIFY_CHARSET_EMAIL)
msg = message_from_string(text_body)
msg.add_header('Content-Type', 'text/plain; charset="%s"' % self._admin_settings.NOTIFY_CHARSET_EMAIL)
msg.add_header('Content-Transfer-Encoding', '8bit')
# configure message headers
msg['Subject'] = Header(subject or self.mail_headers['Subject'], 'utf-8').encode()
msg['From'] = templ_data.get('FROMMAIL') or self._admin_settings.NOTIFY_FROM_EMAIL
msg['To'] = templ_data['TOMAIL']
return msg
def send_msg(self, msg):
to_addrs = [_.strip() for _ in msg['To'].split(',')]
self._mail_helper.sendmail(msg['From'], to_addrs, msg, encoding_name=self._admin_settings.NOTIFY_CHARSET_EMAIL)
def send_notification(self, templ_data: dict, text_templ_name: str, html_templ_name: str = None):
try:
msg = self.generate_msg(templ_data, text_templ_name, html_templ_name)
self.send_msg(msg)
except StatsNotifierTemplateError as e:
self.log.warning(str(e))
def _has_fault(self, column_name, faults):
return any(user[column_name] > 0 for user in faults)
def _generate_table_data(self, users_data, notify_types_options):
# type: (List[Dict], NotifyFaultsOptions) -> Tuple[Optional[str], Optional[str]]
"""Generate summary table with info about users"""
table_columns = [('Username', 'LOGIN'), ('Domain', 'DOMAIN'), ('Name', 'FROMNAME')]
additional_columns = itertools.compress(
[
[('CPU Limit, %', 'lCPU'), ('CPU Faults', 'CPUf')],
[('VMem Limit, KB', 'lVMem'), ('VMem Faults', 'VMemF')],
[('PMem Limit, KB', 'lPMem'), ('PMem Faults', 'PMemF')],
[('EP Limit', 'lEP'), ('EP Faults', 'EPf')],
[('NPROC Limit', 'lNproc'), ('NPROC Faults', 'NprocF')],
[('IO Limit, KB/s', 'lIO'), ('IO Faults', 'IOf')]
],
[
notify_types_options.NOTIFY_CPU and self._has_fault('CPUf', users_data),
notify_types_options.NOTIFY_MEMORY and self._has_fault('VMemF', users_data), # deprecated
notify_types_options.NOTIFY_MEMORY and self._has_fault('PMemF', users_data),
notify_types_options.NOTIFY_EP and self._has_fault('EPf', users_data),
notify_types_options.NOTIFY_NPROC and self._has_fault('NprocF', users_data),
notify_types_options.NOTIFY_IO and self._has_fault('IOf', users_data)
]
)
table_columns.extend(sum(additional_columns, []))
if self.lve_version > 6:
if notify_types_options.NOTIFY_IOPS and self._has_fault('IOPSf', users_data):
table_columns.extend([('IOPS Limit', 'lIOPS'), ('IOPS Faults', 'IOPSf')])
columns = [header for header, _ in table_columns]
if len(users_data) == 0:
return None, None
# Generates admin/reseller table from users_data and table_columns
def gen_table_body():
table_b = list()
for data in users_data:
table_line = []
for _, user_data_key in table_columns:
cell = str(data.get(user_data_key, '---'))
table_line.append(cell)
table_b.append(table_line)
return table_b
table = PrettyTable(columns)
table.horizontal_char = '='
table.junction_char = "="
list(map(table.add_row, gen_table_body()))
s_table = table.get_string()
s_html_table = table.get_html_string(format=True,
border=True,
hrules=ALL,
vrules=ALL)
return s_table, s_html_table
def _prepare_template_data(self, user_data):
# type: (Dict) -> Dict
"""
Extend keys in new mode register ('aCPU' => 'acpu'),
correct 'aVMem', 'aPMem', 'lVMem', 'lPMem'
for backward compatibility with old templates
"""
templ_data_dict = merge_dicts(
user_data, {k.lower(): v for k, v in user_data.items()})
# old templates used camelCase-like variables
# and there, info about memory must be in bytes
templ_data_dict.update(
{key: var * 1024 for key, var in templ_data_dict.items()
if key in ('aVMem', 'aPMem', 'lVMem', 'lPMem')})
return templ_data_dict
def _notify_users(self, users_data):
"""
Send email to each user in 'users_data',
with given template path in 'templ_name'
:type users_data: collections.Iterable[dict]
:return: Nothing
"""
for templ_data in users_data:
templ_data['user_type'] = 'customer'
if not templ_data.get('TOMAIL'):
logging.debug('User %s has not set email, skip notification')
continue
user_template_data = self._prepare_template_data(templ_data)
self.send_notification(templ_data=user_template_data,
text_templ_name=USER_TEMPL,
html_templ_name=USER_TEMPL_HTML)
def _check_admin(self):
if not (self._admin_settings.NOTIFY_ADMIN and any(self._admin_settings.NOTIFY_FAULTS_TYPES)):
return
if not self._notifications.admin_need_notification(self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL):
return
self._notifications.mark_admin_notified()
period = self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_INTERVAL
users_fault_data = self.get_users_fault(period=period,
notify_min_faults=self._admin_settings.NOTIFY_OPTIONS_SELF.NOTIFY_MIN_FAULTS,
notify_faults_types=self._admin_settings.NOTIFY_FAULTS_TYPES,
uids=list(self._proc_lve.lve_id_list(0)))
# TODO: should we show admin info about reseller's end users?
users_data = self.get_users_data(fault_users_data=users_fault_data)
if not users_data:
return
s_text_table, s_html_table = self._generate_table_data(
users_data, self._admin_settings.NOTIFY_FAULTS_TYPES)
# do nothing, if no table generated
if not s_text_table:
self.log.debug("During admin check, no users faults found. Skipping...")
return
admin_template_data = {
'STATS': s_text_table,
'STATS_HTML': s_html_table,
'FROMMAIL': self._admin_settings.NOTIFY_FROM_EMAIL,
'LOCALE': self.get_locales(get_admin_locale()),
'TOMAIL': self._detect_admin_email(),
'PERIOD': period,
'TONAME': 'Administrator',
'user_type': 'admin',
'HOSTNAME': self.hostname}
self.send_notification(templ_data=admin_template_data,
text_templ_name=ADMIN_TEMPL,
html_templ_name=ADMIN_TEMPL_HTML)
return True
def get_panel_reseller_data(self, reseller_login):
try:
panel_user_data = cpapi.cpinfo(
reseller_login, keyls=('mail', 'locale'), search_sys_users=False)[0]
except (IOError, IndexError, cpapi.cpapiexceptions.NotSupported,):
return dict()
return {'TOMAIL': panel_user_data[0],
'LOCALE': self.get_locales(panel_user_data[1] or 'en'),
'RESELLER_USERNAME': reseller_login}
def prepare_resellers_data(self, users_data):
resellers_data = dict()
for user_data in users_data:
reseller_login = user_data.get('RESSELER')
if not reseller_login or reseller_login not in self.resellers:
continue
if reseller_login not in list(resellers_data.keys()):
reseller_data = self.get_panel_reseller_data(reseller_login)
if not reseller_data.get('TOMAIL'):
continue
resellers_data[reseller_login] = merge_dicts(
{'USERS': [user_data['LOGIN']],
'PERIOD': user_data['PERIOD']}, reseller_data)
else:
resellers_data[reseller_login]['USERS'].append(
user_data['LOGIN'])
result = []
# Cycle by resellers
for reseller_login, data in resellers_data.items():
data['LOGIN'] = reseller_login
reseller_settings = self.get_reseller_settings(reseller_login) or self._default_reseller_settings
s_text_table, s_html_table = self._generate_table_data(
[u for u in users_data if u['LOGIN'] in data['USERS']],
reseller_settings.NOTIFY_FAULTS_TYPES)
if not s_text_table:
continue
data['STATS'] = s_text_table
data['STATS_HTML'] = s_html_table
data['HOSTNAME'] = self.hostname
result.append(data)
return result
def _prepare_resellers_summary_data(self, resellers_data):
"""
With enabled second-level of resellers's limits
we have information about reseller's faults;
Here we prepare information about such resellers;
:type resellers_data: collections.Iterable[dict]
:return: dict
"""
for reseller_data in resellers_data:
# TODO: looks like never happens
reseller_login = reseller_data.get('LOGIN')
if not reseller_login:
continue
panel_data = self.get_panel_reseller_data(reseller_login)
if not panel_data.get('TOMAIL'):
continue
yield merge_dicts({'HOSTNAME': self.hostname}, reseller_data, panel_data)
def _load_locales_data(self, templates_dir: str) -> Dict:
"""
Load locales data from specified directory (file /usr/share/lve/emails/<locale_name>/locale_defines.json)
:param templates_dir: Directory name to search json file
:return: Dictionary loaded from JSON file
"""
# Try to get locale defines
locale_defines_file = os.path.join(templates_dir, self.LOCALE_DEFINES_FILE)
try:
with open(locale_defines_file) as json_file:
return json.load(json_file)
except Exception as e:
# Can't read /usr/share/lve/emails/<locale_name>/locale_defines.json file
self.log.warning("Can't read/parse email localization file %s: %s", locale_defines_file, str(e))
return {}
@staticmethod
def add_default_data(data_list, key, val):
for data in data_list:
data[key] = data.get(key) or val
yield data
def _get_admin_settings(self):
"""
Read admin settings config (using shared code in
lve-utils) and map it into our namedtuple structure
:rtype: AdminSettings
"""
raw_data = get_admin_notification()
notification_data = raw_data['faultsNotification']
faults_to_include = notification_data['faultsToInclude']
min_to_notify = notification_data['minimumNumberOfFaultsToNotify']
periods = notification_data['notify']
try:
notify_interval_admin = dateutil.time_dict_to_seconds(periods['admin'])
notify_interval_user = dateutil.time_dict_to_seconds(periods['user'])
except (ValueError, TypeError) as e:
notify_interval_user = self.DEFAULT_PERIOD
notify_interval_admin = self.DEFAULT_PERIOD
self.log.warning("Can't set interval to admin. Period value is incorrect: %s", str(e))
email_settings = notification_data['email']
settings = AdminSettings(
NOTIFY_ADMIN=notification_data['notifyAdmin'],
NOTIFY_CUSTOMERS_ON_FAULTS=notification_data['notifyCustomers'],
NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS=notification_data['notifyResellers'],
NOTIFY_RESELLER_CUSTOMERS=notification_data[
'notifyResellerCustomers'],
NOTIFY_FAULTS_TYPES=NotifyFaultsOptions(
NOTIFY_CPU=faults_to_include['cpu'],
NOTIFY_IO=faults_to_include['io'],
NOTIFY_IOPS=faults_to_include['iops'],
NOTIFY_MEMORY=faults_to_include['mem'],
NOTIFY_EP=faults_to_include['concurrentConnections'],
NOTIFY_NPROC=faults_to_include['nproc']),
NOTIFY_OPTIONS_SELF=NotifySettings(
NOTIFY_MIN_FAULTS=min_to_notify['admin'],
NOTIFY_INTERVAL=notify_interval_admin),
NOTIFY_OPTIONS_CUSTOMER=NotifySettings(
NOTIFY_MIN_FAULTS=min_to_notify['user'],
NOTIFY_INTERVAL=notify_interval_user),
NOTIFY_FROM_EMAIL=email_settings.get('notifyFromEmail') or DEFAULT_ADMIN_MAIL,
REPORT_ADMIN_EMAIL=email_settings.get('reportAdminMail') or '',
NOTIFY_SUBJECT=email_settings.get('notifySubject') or '',
NOTIFY_CHARSET_EMAIL=email_settings.get('notifyCharset') or 'us-ascii',)
return settings
def _get_default_reseller_settings(self):
# type: () -> ResellerSettings
return ResellerSettings(
NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS=self._admin_settings.NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS,
NOTIFY_CUSTOMERS_ON_FAULTS=self._admin_settings.NOTIFY_RESELLER_CUSTOMERS,
NOTIFY_RESELLER_ON_TOTAL_FAULTS=self._admin_settings.NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS,
NOTIFY_FAULTS_TYPES=self._admin_settings.NOTIFY_FAULTS_TYPES,
NOTIFY_OPTIONS_SELF=self._admin_settings.NOTIFY_OPTIONS_SELF,
NOTIFY_OPTIONS_CUSTOMER=self._admin_settings.NOTIFY_OPTIONS_CUSTOMER)
def get_reseller_settings(self, reseller, use_cache=True):
# type: (str, Optional[bool]) -> Optional[ResellerSettings]
if use_cache and reseller in self._reseller_settings_cache:
return self._reseller_settings_cache[reseller]
raw_data = get_notification(reseller)
if not raw_data or 'faultsNotification' not in raw_data:
if use_cache:
self._reseller_settings_cache[reseller] = None
return
notification_data = raw_data['faultsNotification']
faults_to_include = notification_data['faultsToInclude']
min_to_notify = notification_data['minimumNumberOfFaultsToNotify']
periods = notification_data['notify']
try:
notify_interval_reseller = dateutil.time_dict_to_seconds(periods['reseller'])
notify_interval_customer = dateutil.time_dict_to_seconds(periods['customer'])
except (ValueError, TypeError) as e:
notify_interval_customer = self.DEFAULT_PERIOD
notify_interval_reseller = self.DEFAULT_PERIOD
self.log.warning("Can't set interval to reseller %s. Period value is incorrect: %s", reseller, str(e))
res = ResellerSettings(
# Admin's global setting analogue is NOTIFY_RESELLER
NOTIFY_RESELLER_ON_CUSTOMERS_FAULTS=notification_data[
'notifyResellerOnCustomers'],
# Admin's global setting analogue is NOTIFY_CUSTOMER
NOTIFY_CUSTOMERS_ON_FAULTS=notification_data['notifyCustomers'],
NOTIFY_RESELLER_ON_TOTAL_FAULTS=notification_data['notifyReseller'],
NOTIFY_FAULTS_TYPES=NotifyFaultsOptions(
NOTIFY_CPU=faults_to_include['cpu'],
NOTIFY_IO=faults_to_include['io'],
NOTIFY_IOPS=faults_to_include['iops'],
NOTIFY_MEMORY=faults_to_include['mem'],
NOTIFY_EP=faults_to_include['concurrentConnections'],
NOTIFY_NPROC=faults_to_include['nproc']),
NOTIFY_OPTIONS_SELF=NotifySettings(
NOTIFY_MIN_FAULTS=min_to_notify['reseller'],
NOTIFY_INTERVAL=notify_interval_reseller),
NOTIFY_OPTIONS_CUSTOMER=NotifySettings(
NOTIFY_MIN_FAULTS=min_to_notify['customer'],
NOTIFY_INTERVAL=notify_interval_customer))
if use_cache:
self._reseller_settings_cache[reseller] = res
return res
def _process_resellers_mails(self, users_data_reseller):
"""
Decide and send mails to resellers as resellers (doesn't include
notification to reseller about it's personal account as a customer)
:return: Nothing
"""
resellers_data = self.prepare_resellers_data(users_data_reseller)
if not resellers_data:
return
resellers_data = self.add_default_data(resellers_data, 'TONAME', 'Reseller')
for reseller_info in resellers_data:
reseller_info['user_type'] = 'reseller'
if not reseller_info.get('TOMAIL'):
continue
self.send_notification(templ_data=reseller_info,
text_templ_name=RESELLER_TEMPL,
html_templ_name=RESELLER_TEMPL_HTML)
def _process_resellers_summary_info_mails(self, resellers_data):
# type: (Iterable[Dict]) -> None
"""
When second level of reseller limits is enabled
we collect summary info about reseller;
When total load makes fault, we notify reseller about it;
"""
resellers_faults_data = self._prepare_resellers_summary_data(resellers_data)
resellers_faults_data = self.add_default_data(resellers_faults_data, 'TONAME', 'Reseller')
for reseller_info in resellers_faults_data:
reseller_info['user_type'] = 'reseller'
self._notify_reseller_summary_faults(reseller_info)
def _notify_reseller_summary_faults(self, reseller_info):
# type: (Dict) -> None
"""Prepare data for reseller_faults_notify template"""
reseller_info = self._prepare_template_data(reseller_info)
self.send_notification(templ_data=reseller_info,
text_templ_name=RESELLER_FAULTS_TEMPL,
html_templ_name=RESELLER_FAULTS_TEMPL_HTML)
def _process_users_mails(self, users_data):
"""
Decide and send mails to customers(including resellers personal account
notification as a customer)
:return: Nothing
"""
for user_info in users_data[:]:
account_owner = user_info['RESSELER']
user_notification_enabled = user_should_be_notified(user_info['LOGIN'])
# reseller's will override
do_notify = self._admin_settings.NOTIFY_CUSTOMERS_ON_FAULTS and user_notification_enabled
# is user belongs to some reseller?
if account_owner in self.resellers:
reseller_settings = self.get_reseller_settings(account_owner) or self._default_reseller_settings
do_notify = reseller_settings.NOTIFY_CUSTOMERS_ON_FAULTS and user_notification_enabled
if not do_notify:
# Exclude only in users' data copy to prevent affecting
# resellers notifications(summary tables)
users_data.remove(user_info)
continue
self._notify_users(self.add_default_data(users_data, 'TONAME', 'Customer'))
def _iter_customers_faults_data(self):
# type: () -> Iterator[Dict]
"""
Find all customers that might cause faults
from /proc/lve/list, get additional data from
control panel and return it
"""
# iter over resellers and prepare data
for (notify_settings, notify_faults_types), resellers in self._get_customers_options_grouped():
# mark all resellers as those, whose users were notified
notified_resellers = [reseller_id for reseller_id, _ in resellers]
self._notifications.mark_users_notified(notified_resellers)
# list users that may cause faults during period
users_to_check = sum((list(self._proc_lve.lve_id_list(reseller_id)) for reseller_id in notified_resellers), [])
yield self.__get_customers_faults_data(users_to_check, notify_faults_types, notify_settings)
def __get_customers_faults_data(self, users_to_check, notify_faults_types, notify_settings):
# type: (list[int], NotifyFaultsOptions, NotifySettings) -> list[dict]
# search statistics in database for given users
users_fault_data = self.get_users_fault(period=notify_settings.NOTIFY_INTERVAL,
notify_min_faults=notify_settings.NOTIFY_MIN_FAULTS,
notify_faults_types=notify_faults_types,
uids=users_to_check)
# Receive users info from cpapi
# TODO rewrite with generators because modify or copy users_data[:]
# both may lead to problems
users_data = self.get_users_data(fault_users_data=users_fault_data)
return users_data
def _check_users(self):
for users_data in self._iter_customers_faults_data():
self._process_users_mails(users_data)
def _iter_resellers_faults_data(self):
# type: () -> Tuple[List[Dict], List[int]]
"""
Find all resellers that might cause faults
from /proc/lve/resellers, get additional data from
control panel and return it
"""
# get all resellers that may be notified
for (notify_settings, notify_faults_types), resellers in self._get_resellers_options_grouped():
resellers_uids = [reseller_id for reseller_id, _ in resellers]
# in our database resellers have serialized id's
resellers_to_check = [serialize_lve_id(LIMIT_LVP_ID, reseller_id) for reseller_id in resellers_uids]
# get data from database for those resellers that might cause faults
faults_data = self.get_users_fault(period=notify_settings.NOTIFY_INTERVAL,
notify_min_faults=notify_settings.NOTIFY_MIN_FAULTS,
notify_faults_types=notify_faults_types,
uids=resellers_to_check)
# receive resellers info from cpapi
resellers_data = self._get_resellers_data(faults_data=faults_data)
yield resellers_data, resellers_uids
def _iter_reseller_users_list_faults_data(self):
# type: () -> Tuple[List[Dict], List[int]]
for (notify_settings, notify_faults_types), resellers in \
self._get_resellers_customers_faults_options_grouped():
# mark all resellers as those, whose users were notified
notified_resellers = [reseller_id for reseller_id, _ in resellers]
# list of users that may cause faults during period
users_to_check = []
for reseller_id in notified_resellers:
users_to_check.extend(self._proc_lve.lve_id_list(reseller_id))
customers_faults = self.__get_customers_faults_data(
users_to_check, notify_faults_types, notify_settings)
yield customers_faults, notified_resellers
def _check_resellers(self):
notified_resellers = set()
for resellers_data, reseller_ids in self._iter_resellers_faults_data():
# send reseller his own total faults during PERIOD
# works only with enabled second level of reseller limits
self._process_resellers_summary_info_mails(resellers_data)
notified_resellers.update(reseller_ids)
for users_data, reseller_ids in self._iter_reseller_users_list_faults_data():
# send reseller table with his users faults during PERIOD
# works with and without second level of reseller limits
self._process_resellers_mails(users_data)
notified_resellers.update(reseller_ids)
self._notifications.mark_resellers_notified(notified_resellers)
def execute(self, lve_data):
# type: (Dict) -> None
# Reset this cache each time because resellers should have ability to
# change their notification preferences without restarting
# StatsNotifier plugin
self._reseller_settings_cache = {}
self._name_map.link_xml_node(use_cache=False)
self._check_users() # check if any users need to be notified
self._check_resellers() # check if some resellers must be notified on their faults
self._check_admin() # check if we must notify admin himself
self._notifications.save_to_persistent_storage() # save timestamps to file
def get_stats_notifier_parameters(user):
"""
Gets parameter for user
"""
default_reseller = 'root'
reseller = cpinfo(user, keyls=('reseller',))
if reseller:
default_reseller = reseller[0][0]
stat_notifier = StatsNotifier()
if default_reseller not in admins():
settings = stat_notifier.get_reseller_settings(default_reseller) or \
stat_notifier._get_default_reseller_settings()
else:
settings = stat_notifier._get_admin_settings()
return settings.NOTIFY_CUSTOMERS_ON_FAULTS
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists