import agent_util
from plugins.process import ProcessPlugin
try:
import ssl
except:
ssl = None
try:
# Python 2.x
from httplib import HTTPConnection, HTTPSConnection
except:
from http.client import HTTPConnection, HTTPSConnection
from library.log_matcher import LogMatcher
import traceback
# ON FREEBSD/CENTOS, THEY MAY NEED TO ADD THIS TO THEIR HTTPD.CONF/APACHE2.CONF:
# LoadModule status_module libexec/apache22/mod_status.so
# <IfModule status_module>
# ExtendedStatus On
# <Location /server-status>
# SetHandler server-status
# Order deny,allow
# Allow from all
# </Location>
# </IfModule>
class ApachePlugin(agent_util.Plugin):
textkey = "apache"
label = "Apache Webserver"
DEFAULTS = {
"server_status_protocol": "http",
"server_status_host": "localhost",
"server_status_url": "server-status",
"apache_log_files": ["/var/log/apache2/access.log", "/var/log/httpd/access.log", "/var/log/httpd-access.log"]
}
LOG_COUNT_EXPRESSIONS = {
'apache.4xx': r"4\d{2}",
'apache.5xx': r"5\d{2}",
'apache.2xx': r"2\d{2}"
}
@classmethod
def get_data(self, textkey, ip, config):
server_status_path = ""
server_status_url = config.get('server_status_url')
server_status_protocol = config.get('server_status_protocol')
server_status_port = config.get('server_status_port', None)
if not server_status_url.startswith("/"):
server_status_path += "/"
server_status_path += (server_status_url + "?auto")
if server_status_protocol == "https" and ssl is not None:
if server_status_port is None:
conn = HTTPSConnection(ip, context=ssl._create_unverified_context())
else:
conn = HTTPSConnection(ip, int(server_status_port), context=ssl._create_unverified_context())
else:
if server_status_port:
conn = HTTPConnection(ip, server_status_port)
else:
conn = HTTPConnection(ip)
try:
conn.request("GET", server_status_path)
r = conn.getresponse()
output = r.read().decode()
conn.close()
except:
self.log.info("""
Unable to access the Apache status page at %s%s. Please ensure Apache is running and
the server status url is correctly specified.
""" % (ip, server_status_path))
self.log.info("error: %s" % traceback.format_exc())
return None
data = dict()
for line in output.splitlines():
if ':' not in line:
continue
k, v = line.split(': ', 1)
data.update({k: v})
def get_param_value(data, param, output_type):
try: return output_type(data[param])
except KeyError: return None
if textkey.endswith("uptime"):
return get_param_value(data, "Uptime", int)
elif textkey.endswith("total_accesses"):
return get_param_value(data, "Total Accesses", int)
elif textkey.endswith("total_traffic"):
val = get_param_value(data, "Total kBytes", int)
# Convert to MB for backwards compatibility
if val:
return val / 1000.
else: return None
elif textkey.endswith("cpu_load"):
return get_param_value(data, "CPULoad", float)
elif textkey.endswith("connections"):
return get_param_value(data, "ReqPerSec", float)
elif textkey.endswith("transfer_rate"):
val = get_param_value(data, "BytesPerSec", float)
# Convert to MB for backwards compatibility
return val / (1000.**2)
elif textkey.endswith("avg_request_size"):
val = get_param_value(data, "BytesPerReq", float)
# Convert to MB for backwards compatibility
return val / (1000.**2)
elif textkey.endswith("workers_used_count"):
return get_param_value(data, "BusyWorkers", int)
elif textkey.endswith("workers_idle_count"):
return get_param_value(data, "IdleWorkers", int)
elif textkey in ("apache.workers_used", "apache.workers_idle"):
busy = get_param_value(data, "BusyWorkers", int)
idle = get_param_value(data, "IdleWorkers", int)
if busy is None or idle is None:
return None
total = busy + idle
if textkey.endswith("workers_used"):
return float(100. * busy / total)
elif textkey.endswith("workers_idle"):
return float(100. * idle / total)
@classmethod
def get_metadata(self, config):
status = agent_util.SUPPORTED
msg = None
self.log.info("Looking for apache2ctl to confirm apache is installed")
# look for either overrides on how to access the apache healthpoint or one of the apachectl bins
if not agent_util.which("apache2ctl") and not agent_util.which("apachectl") \
and not config.get("from_docker") and not agent_util.which("httpd"):
self.log.info("Couldn't find apachectl or apache2ctl")
status = agent_util.UNSUPPORTED
msg = "Apache wasn't detected (apachectl or apache2ctl)"
return {}
# update default config with anything provided in the config file
if config:
new_config = self.DEFAULTS.copy()
new_config.update(config)
config = new_config
# Look for Apache server-status endpoint
server_status_path = ""
server_status_protocol = config.get('server_status_protocol', self.DEFAULTS['server_status_protocol'])
server_status_url = config.get('server_status_url', self.DEFAULTS['server_status_url'])
server_status_port = config.get('server_status_port', None)
if not server_status_url.startswith("/"):
server_status_path += "/"
server_status_path += (server_status_url + "?auto")
not_found_error = """
Unable to access the Apache status page at %s. Please ensure the status page module is enabled,
Apache is running, and, optionally, the server status url is correctly specified. See docs.fortimonitor.forticloud.com/
for more information.
""" % server_status_path
host_list = []
# support optional comma delimitted addresses
ip_list = config.get('server_status_host', 'localhost')
ip_list = ip_list.split(',')
# loop over each IP from config and check to see if the status endpoint is reachable
for ip in ip_list:
ip_working = True
try:
if server_status_protocol == "https" and ssl is not None:
if server_status_port is None:
conn = HTTPSConnection(ip, context=ssl._create_unverified_context())
else:
conn = HTTPSConnection(ip, int(server_status_port), context=ssl._create_unverified_context())
else:
if server_status_port:
conn = HTTPConnection(ip, server_status_port)
else:
conn = HTTPConnection(ip)
conn.request("GET", server_status_path)
r = conn.getresponse()
conn.close()
except Exception:
import sys
_, err_msg, _ = sys.exc_info()
ip_working = False
self.log.info(not_found_error)
self.log.info("error: %s" % err_msg)
msg = not_found_error
continue
if r.status != 200:
self.log.info(not_found_error)
msg = not_found_error
ip_working = False
if ip_working:
host_list.append(ip)
output = r.read()
if config.get("debug", False):
self.log.info('#####################################################')
self.log.info("Apache server-status output:")
self.log.info(output)
self.log.info('#####################################################')
if not host_list:
status = agent_util.MISCONFIGURED
msg = not_found_error
return {}
# Checking log files access
if not config.get('apache_log_files'):
log_files = self.DEFAULTS.get('apache_log_files')
else:
log_files = config.get('apache_log_files')
try:
if type(log_files) in (str, unicode):
log_files = log_files.split(',')
except NameError:
if type(log_files) in (str, bytes):
log_files = log_files.split(',')
can_access = False
log_file_msg = ''
log_file_status = status
for log_file in log_files:
try:
opened = open(log_file, 'r')
opened.close()
# Can access at least one file. Support log access
can_access = True
except Exception:
import sys
_, error, _ = sys.exc_info()
message = 'Error opening the file %s. Ensure the fm-agent user has access to read this file' % log_file
if 'Permission denied' in str(error):
self.log.error(error)
self.log.error(message)
if log_file not in self.DEFAULTS.get('apache_log_files', []):
self.log.error(error)
self.log.error(message)
log_file_msg = message
log_file_status = agent_util.MISCONFIGURED
if can_access:
log_file_status = agent_util.SUPPORTED
log_file_msg = ''
metadata = {
"apache.workers_used": {
"label": "Workers - percent serving requests",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "%"
},
"apache.workers_idle": {
"label": "Workers - percent idle",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "%"
},
"apache.workers_used_count": {
"label": "Workers - count serving requests",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "workers"
},
"apache.workers_idle_count": {
"label": "Workers - count idle",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "workers"
},
"apache.uptime": {
"label": "Server uptime",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "seconds"
},
"apache.total_accesses": {
"label": "Request count",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "requests"
},
"apache.total_traffic": {
"label": "Total content served",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "MB"
},
"apache.cpu_load": {
"label": "Percentage of CPU used by all workers",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "%"
},
"apache.connections": {
"label": "Requests per second",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "requests"
},
"apache.transfer_rate": {
"label": "Transfer rate",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "MB/s"
},
"apache.avg_request_size": {
"label": "Request size average",
"options": host_list,
"status": status,
"error_message": msg,
"unit": "MB"
},
"apache.2xx": {
"label": "Rate of 2xx's events",
"options": None,
"status": log_file_status,
"error_message": log_file_msg,
"unit": "entries/s"
},
"apache.4xx": {
"label": "Rate of 4xx's events",
"options": None,
"status": log_file_status,
"error_message": log_file_msg,
"unit": "entries/s"
},
"apache.5xx": {
"label": "Rate of 5xx's events",
"options": None,
"status": log_file_status,
"error_message": log_file_msg,
"unit": "entries/s"
},
"apache.is_running": {
"label": "Apache is running",
"options": None,
"status": status,
"error_message": msg
}
}
return metadata
@classmethod
def get_metadata_docker(self, container, config):
if 'server_status_host' not in config:
try:
ip = agent_util.get_container_ip(container)
config['server_status_host'] = ip
except Exception:
import sys
_, e, _ = sys.exc_info()
self.log.exception(e)
config["from_docker"] = True
return self.get_metadata(config)
def get_apache_process_name(self):
if agent_util.which("apache2ctl"):
return "apache2"
if agent_util.which("httpd"):
return "httpd"
if agent_util.which("httpd22"):
return "httpd22"
return None
def check(self, textkey, ip, config):
# update default config with anything provided in the config file
new_config = self.DEFAULTS.copy()
new_config.update(config)
config = new_config
# add backwards compatibility for older entries where IP was not an option. Pull from config instead.
if not ip:
ip = config['server_status_host'].split(',')[0]
if textkey == "apache.is_running" and not config.get("from_docker"):
apache_process = ProcessPlugin(None)
apache_process_name = self.get_apache_process_name()
if apache_process_name is not None:
apache_is_running = apache_process.check("process.exists", apache_process_name, {})
return apache_is_running
return None
if textkey in (
'apache.2xx', 'apache.4xx', 'apache.5xx'
):
file_inodes = {}
total_metrics = 0
timescale = 1
column = 8
expression = self.LOG_COUNT_EXPRESSIONS.get(textkey)
if not config.get('apache_log_files'):
log_files = config['apache_log_files']
else:
log_files = config.get('apache_log_files')
try:
if type(log_files) in (str, unicode):
log_files = log_files.split(',')
except NameError:
if type(log_files) in (str, bytes):
log_files = log_files.split(',')
for target in log_files:
try:
file_inodes[target] = LogMatcher.get_file_inode(target)
except OSError:
import sys
_, error, _ = sys.exc_info()
if 'Permission denied' in str(error):
self.log.error('Error opening the file %s. Ensure the fm-agent user has access to read this file' % target)
self.log.error(str(error))
if target not in self.DEFAULTS.get('apache_log_files', []):
self.log.error(str(error))
self.log.error('Error opening the file %s. Ensure the fm-agent user has access to read this file' % target)
continue
log_data = self.get_cache_results(textkey, "%s/%s" % (self.schedule.id, target))
if log_data:
log_data = log_data[0][-1]
else:
log_data = dict()
last_line_number = log_data.get('last_known_line')
stored_inode = log_data.get('inode')
results = log_data.get('results', [])
try:
total_lines, current_lines = LogMatcher.get_file_lines(
last_line_number, target, file_inodes[target], stored_inode
)
except IOError:
import sys
_, error, _ = sys.exc_info()
self.log.error('Unable to read the file %s. Ensure the fm-agent user has access to read this file' % target)
continue
self.log.info('Stored line %s Current line %s looking at %s lines' % (
str(last_line_number), str(total_lines), str(len(current_lines))
))
log_matcher = LogMatcher(stored_inode)
results = log_matcher.match_in_column(current_lines, expression, column)
metric, results = log_matcher.calculate_metric(results, timescale)
total_metrics += metric and metric or 0
self.log.info('Found %s instances of "%s" in %s' % (
str(metric or 0), expression, target
))
previous_result = self.get_cache_results(
textkey, "%s/%s" % (self.schedule.id, target)
)
cache_data = dict(
inode=file_inodes[target],
last_known_line=total_lines,
results=results
)
self.cache_result(
textkey, "%s/%s" % (self.schedule.id, target),
cache_data,
replace=True
)
if not previous_result:
return None
else:
delta, prev_data = previous_result[0]
try:
curr_count = cache_data.get('results')[0][-1]
result = curr_count / float(delta)
except IndexError:
result = None
return result
else:
return ApachePlugin.get_data(textkey, ip, config)
def check_docker(self, container, textkey, ip, config):
try:
ip = agent_util.get_container_ip(container)
except:
return None
return self.check(textkey, ip, config)