# -*- 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
"""
This module contains classes implementing X-Ray Manager behaviour
for DirectAdmin
"""
import os
import re
import subprocess
import urllib.parse
from collections import ChainMap
from glob import glob
import chardet
from .base import BaseManager
from ..internal.exceptions import XRayManagerError
from ..internal.types import DomainInfo
from ..internal.user_plugin_utils import (
user_mode_verification,
with_fpm_reload_restricted
)
class DirectAdminManager(BaseManager):
"""
Class implementing an X-Ray manager behaviour for DirectAdmin
"""
da_options_conf = '/usr/local/directadmin/custombuild/options.conf'
da_domain_pattern = '/usr/local/directadmin/data/users/*/domains/*.conf'
da_subdomain_pattern = '/usr/local/directadmin/data/users/*/domains/*.subdomains'
da_alias_pattern = '/usr/local/directadmin/data/users/*/domains/*.pointers'
da_docroot_override_pattern = '/usr/local/directadmin/data/users/*/domains/*.subdomains.docroot.override'
VERSIONS_DA = {
'php54': '/usr/local/php54/lib/php.conf.d',
'php55': '/usr/local/php55/lib/php.conf.d',
'php56': '/usr/local/php56/lib/php.conf.d',
'php70': '/usr/local/php70/lib/php.conf.d',
'php71': '/usr/local/php71/lib/php.conf.d',
'php72': '/usr/local/php72/lib/php.conf.d',
'php73': '/usr/local/php73/lib/php.conf.d',
'php74': '/usr/local/php74/lib/php.conf.d',
'php80': '/usr/local/php80/lib/php.conf.d',
'php81': '/usr/local/php81/lib/php.conf.d',
'php82': '/usr/local/php82/lib/php.conf.d',
'php83': '/usr/local/php83/lib/php.conf.d'
}
def supported_versions(self) -> ChainMap:
"""
Get supported PHP versions
:return: a chained map with basic supported versions
and DirectAdmin supported versions
"""
return ChainMap(self.VERSIONS,
self.VERSIONS_DA)
def file_readlines(self, filename: str) -> list:
"""
Read lines from file
:param filename: a name of file to read
:return: list of stripped lines
"""
def get_file_encoding():
"""
Retrieve file encoding
"""
with open(filename, 'rb') as f:
result = chardet.detect(f.read())
return result['encoding']
try:
with open(filename, encoding=get_file_encoding()) as f:
return [line.strip() for line in f.readlines()]
except OSError as e:
self.logger.error('Failed to read [DA conf] file',
extra={'fname': filename,
'err': str(e)})
raise XRayManagerError(f'Failed to read file {filename}', needs_logging=False) from e
@property
def php_options(self) -> dict:
"""
Retrieve DirectAdmin PHP settings
:return: dict of format {'1': {ver, fpm}, '2': {ver, fpm}...}
where '1', '2' etc is an ordinal number of a handler as
it is defined in options.conf
"""
parsed_options = dict()
opts = self.file_readlines(self.da_options_conf)
def inner_filter(seq, marker):
"""
Filter PHP release|mode items in seq by marker
:param seq: initial sequence
:param marker: should be contained in seq item
:return: all items from seq containing marker
"""
return [l for l in seq if
marker in l and 'php' in l and not l.startswith('#')]
for index, o in enumerate(zip(inner_filter(opts, 'release'),
inner_filter(opts, 'mode')),
start=1):
release, mode = o
if 'no' not in release:
parsed_options[str(index)] = {
'ver': f"php{''.join(release.split('=')[-1].split('.'))}",
'fpm': 'fpm' in mode,
'handler': mode.split('=')[-1]
}
return parsed_options
@property
def main_domains(self) -> dict:
"""
Retrieve main domains configuration files
"""
domains = dict()
for dom_conf in glob(self.da_domain_pattern):
name = os.path.basename(dom_conf).split('.conf')[0]
domains[name] = dom_conf
return domains
@property
def subdomains(self) -> dict:
"""
Retrieve subdomains configuration files
"""
subdomains = dict()
for sub_conf in glob(self.da_subdomain_pattern):
for subdom in self.file_readlines(sub_conf):
sub_parent = f"{os.path.basename(sub_conf).split('.subdomains')[0]}"
sub_name = f"{subdom}.{sub_parent}"
subdomains[
sub_name] = f"{sub_conf.split('.subdomains')[0]}.conf"
return subdomains
@property
def aliases(self) -> dict:
"""
Retrieve aliases configuration files
"""
aliases = dict()
for alias_conf in glob(self.da_alias_pattern):
parent_domain_name = alias_conf.split('.pointers')[0]
for alias in self.file_readlines(alias_conf):
alias_info = alias.split('=')
alias_name = alias_info[0]
_type = alias_info[-1]
if _type == 'pointer':
# pointers are not considered as domains,
# because they just perform a redirect to parent domain
continue
aliases[alias_name] = f"{parent_domain_name}.conf"
try:
for sub in self.file_readlines(
f"{parent_domain_name}.subdomains"):
aliases[
f"{sub}.{alias_name}"] = f"{parent_domain_name}.conf"
except XRayManagerError:
# there could be no subdomains
pass
return aliases
@property
def subdomains_php_settings(self) -> dict:
"""
Retrieve subdomains_docroot_override configuration files
"""
sub_php_set = dict()
for sub_doc_override in glob(self.da_docroot_override_pattern):
for subdomline in self.file_readlines(sub_doc_override):
subdompart, data = urllib.parse.unquote(
subdomline).split('=', maxsplit=1)
php_select_value = re.search(r'(?<=php1_select=)\d(?=&)',
data)
if php_select_value is not None:
domname = f"{os.path.basename(sub_doc_override).split('.subdomains.docroot.override')[0]}"
subdomname = f"{subdompart}.{domname}"
sub_php_set[subdomname] = php_select_value.group()
return sub_php_set
@property
def all_sites(self) -> dict:
"""
Retrieve all domains and subdomains, existing on DA server,
including aliases
in the form of dict {domain_name: domain_config}
:return: {domain_name: domain_config} including subdomains
"""
da_sites = dict()
for bunch in self.main_domains, self.subdomains, self.aliases:
da_sites.update(bunch)
return da_sites
@user_mode_verification
@with_fpm_reload_restricted
def get_domain_info(self, domain_name: str) -> DomainInfo:
"""
Retrieve information about given domain from control panel environment:
PHP version, user of domain, fpm status
:param domain_name: name of domain
:return: a DomainInfo object
"""
try:
domain_conf = self.all_sites[domain_name]
except KeyError:
self.logger.warning(
'Domain does not exist on the server or is a pointer (no task allowed for pointers)',
extra={'domain_name': domain_name})
raise XRayManagerError(
f"Domain '{domain_name}' does not exist on this server or is a pointer (no task allowed for pointers)",
errno=1110, needs_logging=False)
data = self.file_readlines(domain_conf)
def find_item(item: str) -> str:
"""
Get config value of item (e.g. item=value)
:param item: key to get value of
:return: value of item
"""
found = [line.strip() for line in data if item in line]
try:
return found[0].split('=')[-1]
except IndexError:
return '1'
opts = self.php_options
# Trying to get the subdomain handler first,
# get main domain handler if nothing is set for subdomain
php_selected = self.subdomains_php_settings.get(
domain_name) or find_item('php1_select')
domain_info = DomainInfo(name=domain_name,
panel_php_version=opts[php_selected]['ver'],
user=find_item('username'),
panel_fpm=opts[php_selected]['fpm'],
handler=php_selected)
self.logger.info(
'Retrieved domain info: domain %s owned by %s uses php version %s',
domain_name, domain_info.user, domain_info.handler)
return domain_info
def panel_specific_selector_enabled(self, domain_info: DomainInfo) -> bool:
"""
Check if selector is enabled specifically for DirectAdmin
Required to be implemented by child classes
:param domain_info: a DomainInfo object
:return: True if yes, False otherwise
"""
compatible_handlers = ('suphp', 'lsphp', 'fastcgi')
current_handler = self.php_options[domain_info.handler]['handler']
return domain_info.handler == '1' and current_handler in compatible_handlers
def fpm_service_name(self, dom_info: DomainInfo) -> str:
"""
Get DirectAdmin FPM service name
:param dom_info: a DomainInfo object
:return: FPM service name
"""
return f'php-fpm{dom_info.panel_php_version[-2:]}'
def php_procs_reload(self, domain_info: DomainInfo) -> None:
"""
Copy xray.so for current version, create ini_location directory
Reload FPM service or kill all *php* processes of user
:param domain_info: a DomainInfo object
"""
try:
subprocess.run(['/usr/share/alt-php-xray/da_cp_xray',
domain_info.panel_php_version[-2:]],
capture_output=True, text=True)
except self.subprocess_errors as e:
self.logger.error('Failed to copy xray.so',
extra={'err': str(e),
'info': domain_info})
if self.php_options[domain_info.handler]['handler'] == 'mod_php':
try:
subprocess.run(['/usr/sbin/service',
'httpd',
'restart'],
capture_output=True, text=True)
self.logger.info('httpd restarted')
except self.subprocess_errors as e:
self.logger.error('Failed to restart httpd',
extra={'err': str(e),
'info': domain_info})
super().php_procs_reload(domain_info)
|