Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/cl_plus/utils/web_server_helper.py |
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
import logging
import glob
import os
from traceback import format_exc
from lxml import etree # NOQA
from clcommon.utils import run_command, ExternalProgramFailed, grep, is_litespeed_running
from clcommon.cpapi import get_apache_ports_list, get_apache_connections_number, get_apache_max_request_workers
class WebServerHelper:
"""
Helper class for apache/Litespeed collector.
"""
APACHE_NAME = 'httpd'
LITESPEED_NAME = 'litespeed'
def __init__(self, _logger: logging.Logger):
self._logger = _logger
self._web_ports_list = []
self._is_apache = False
self._is_litespeed = False
self._netstat_bin = '/bin/netstat'
self._no_server_message = 'There is no server working at 80 port'
self._lsws_config = '/usr/local/lsws/conf/httpd_config.xml'
self._is_server_absent_logged = False
self.detect_active_server()
def get_total_connections(self):
"""
Get web server total connections (from web server config)
:return: tuple (max_req_num, message)
max_req_num - Maximum request apache workers number or 0 if error
message - OK/Error text
"""
if self._is_apache:
return get_apache_max_request_workers()
elif self._is_litespeed:
return self._get_ls_total_connections()
else:
return 0, self._no_server_message
def get_current_connections_number(self):
"""
Retrieves web server's current connections number from system netstat utility
:return: tuple (conn_num, message)
conn_num - current connections number
message - OK/Error text
"""
# Read apache ports list. Litespeed uses apache's config, so this method works for both servers
_httpd_ports_list = get_apache_ports_list()
if self._is_litespeed:
port_offset, message = self._get_ls_apache_port_offset()
if message == "OK":
_httpd_ports_list = [port + port_offset for port in _httpd_ports_list]
else:
return 0, message
return self._get_current_connections_number_from_netstat(_httpd_ports_list)
def get_connections_number(self):
"""
Retrieves web server connections number (from apache's mod_status or analog mechanism)
:return: tuple (conn_num, message)
conn_num - current connections number, 0 if error
message - OK/Error text
"""
if self._is_apache:
return get_apache_connections_number()
elif self._is_litespeed:
return self._get_ls_get_connections_number()
else:
return 0, self._no_server_message
def detect_active_server(self):
"""
Determine is httpd/Litespeed running
:return: True/False running/not running
"""
main_server_name = self._get_main_web_server_name()
if not main_server_name:
# Apache/Litespeed not working
self._is_apache = False
self._is_litespeed = False
if not self._is_server_absent_logged:
self._logger.info("Apache/Litespeed collector: Apache or Litespeed stopped or absent, collector will not work")
self._is_server_absent_logged = True
return False
self._is_server_absent_logged = False
self._web_ports_list = get_apache_ports_list()
# Both httpd and litespeed working, select by port
if main_server_name == self.LITESPEED_NAME:
if not self._is_litespeed:
self._logger.info("Apache/Litespeed collector: Using Litespeed")
self._is_litespeed = True
self._is_apache = False
elif main_server_name == self.APACHE_NAME:
if not self._is_apache:
self._logger.info("Apache/Litespeed collector: Using Apache")
self._is_apache = True
self._is_litespeed = False
return self._is_apache or self._is_litespeed
def _get_current_connections_number_from_netstat(self, ports_list):
"""
Retrieves web server's current connections number from system netstat utility
:param ports_list: Port list to search
:return: tuple (conn_num, message)
conn_num - current connections number
message - OK/Error text
"""
# Build grep line, for example ':80|:443'
_grep_ports_line = '|'.join([':{}'.format(val) for val in ports_list])
# /bin/netstat -nt | egrep ':80|:443' | wc -l
try:
# /bin/netstat -nt
cmd_list = [self._netstat_bin, '-nt']
std_out = run_command(cmd_list)
std_out_list = std_out.split('\n')
out_list = list(grep(_grep_ports_line, match_any_position=True, multiple_search=True,
data_from_file=std_out_list))
return len(out_list), 'OK'
except ExternalProgramFailed:
return 0, format_exc()
def _get_ls_total_connections(self):
"""
Retrieve litespeed total connection number (from config)
:return: tuple (max_conn, message)
max_conn - Litespeed's total connection number or 0 if error
message - OK/Trace
"""
try:
# grep "maxConnections" /usr/local/lsws/conf/httpd_config.xml
# XML path httpServerConfig/tuning/maxConnections
with open(self._lsws_config) as f:
x = etree.parse(f).getroot()
element_list = x.xpath("tuning/maxConnections")
max_conn = element_list[0].text
return int(max_conn), "OK"
except (OSError, IOError, IndexError, ValueError, etree.XMLSyntaxError, etree.XMLSchemaParseError,
etree.XMLSchemaError):
return 0, format_exc()
def _get_ls_get_connections_number(self):
"""
Retrieve litespeed connections number (from LS statistics)
:return: tuple (max_conn, message)
max_conn - Litespeed's connections number or 0 if error
message - OK/Trace
"""
try:
con_num = 0
# ls -la /tmp/lshttpd/.rtr*
ls_status_files_list = glob.glob('/tmp/lshttpd/.rtreport*')
# ['/tmp/lshttpd/.rtreport.2', '/tmp/lshttpd/.rtreport']
for ls_filename in ls_status_files_list:
if not os.path.isfile(os.path.realpath(ls_filename)):
self._logger.warning('Skipping %s, because symlink does not point to real file',
ls_filename)
continue
con_num += self._get_connections_num_from_ls_temp_file(ls_filename)
return con_num, "OK"
except (OSError, IOError, ValueError):
return 0, format_exc()
@staticmethod
def _get_connections_num_from_ls_temp_file(ls_temp_filename: str):
"""
Retrieve connections number from litespeed's plain text answer
:param ls_temp_filename: string with litespeed;s answer
:return: tuple (conn_num, message)
conn_num - current connections number, 0 if error
"""
with open(ls_temp_filename, 'r') as content_file:
content = content_file.read()
# content example:
# VERSION: LiteSpeed Web Server/Enterprise/5.4.9
# UPTIME: 1 day 17:41:40
# BPS_IN: 0, BPS_OUT: 0, SSL_BPS_IN: 0, SSL_BPS_OUT: 0
# MAXCONN: 10000, MAXSSL_CONN: 10000, PLAINCONN: 0, AVAILCONN: 9999, IDLECONN: 0, SSLCONN: 1, AVAILSSL: 9999
# REQ_RATE []: REQ_PROCESSING: 1, REQ_PER_SEC: 0.0, TOT_REQS: 54997, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 50018
# REQ_RATE [_AdminVHost]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.0, TOT_REQS: 4989, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 10
# BLOCKED_IP:
# EOF
# Extract and return TOT_REQS metric value
lines_list = content.split('\n')
empty_rrate_lines = list(grep('REQ_RATE []:', fixed_string=True, match_any_position=False,
multiple_search=True, data_from_file=lines_list))
for line in empty_rrate_lines:
line = line.strip()
# line example:
# REQ_RATE []: REQ_PROCESSING: 0, REQ_PER_SEC: 0.0, TOT_REQS: 50448, ....
l_parts = line.split(',')
# Search 'TOT_REQS' parameter
for l_part in l_parts:
# l_part example: ' REQ_PER_SEC: 0.0'
l_part = l_part.strip()
if l_part.startswith('TOT_REQS:'):
return int(l_part.replace('TOT_REQS:', '').strip())
return 0
def _get_main_web_server_name(self):
"""
Detects main web server (httpd or litespeed)
:return: Name - Main server name: 'litespeed', 'httpd', None
"""
if is_litespeed_running():
return self.LITESPEED_NAME
try:
# /sbin/service httpd status
# retcode != 0 - litespeed/httpd not running
# == 0 - litespeed/httpd running
# if 'litespeed' present in stdout - this is litespeed
# else - httpd
returncode, _, _ = run_command(['/sbin/service', 'httpd', 'status'], return_full_output=True)
if returncode != 0:
return None
return self.APACHE_NAME
except ExternalProgramFailed:
pass
return None
def _get_ls_apache_port_offset(self):
"""
Get Apache port offset for Litespeed
There are several cases to handle:
- port is defined in config
<apachePortOffset>1234</apachePortOffset> => offset = 1234
- parameter is absent in config => offset = 0
- parameter is empty => offset = 0
- errors while reading config => offset = 0
"""
offset = 0
try:
# grep "apachePortOffset" /usr/local/lsws/conf/httpd_config.xml
# XML path httpServerConfig/apachePortOffset
with open(self._lsws_config) as f:
x = etree.parse(f).getroot()
element_list = x.xpath("apachePortOffset")
if element_list:
offset = element_list[0].text or 0
return int(offset), "OK"
except (OSError,
IOError,
IndexError,
ValueError,
etree.XMLSyntaxError,
etree.XMLSchemaParseError,
etree.XMLSchemaError):
return 0, format_exc()