File: //usr/share/imunify360-webshield/webshield-watchdog
#!/opt/imunify360/venv/bin/python3
"""
The watchdog script that checks the webshield and restarts it if error found
"""
import json
import logging
import logging.handlers
import os
import requests
import subprocess
import time
import uuid
import yaml
import sentry_sdk
from sentry_sdk import configure_scope
logging.raiseExceptions = False
class Watchdog:
port = 52224
request_timeout = 4
subprocess_timeout = 30
config_path = '/etc/sysconfig/imunify360/imunify360-merged.config'
user_agent = 'Webshield-watchdog-agent'
sentry_dsn_path = '/usr/share/imunify360-webshield/sentry'
package_name = 'imunify360-webshield-bundle'
license_path = '/var/imunify360/license.json'
flag_path = '/var/imunify360/webshield_broken'
integration_path = '/etc/sysconfig/imunify360/integration.conf'
services_full = ('imunify360-webshield', 'imunify360-webshield-ssl-cache')
services_ws_only = ('imunify360-webshield',)
def __init__(self):
self.services = (self.services_ws_only if
os.path.exists(self.integration_path) else self.services_full)
self.is_enabled = self._get_config_status()
self.is_running = self._get_current_status()
self.sentry_dsn = self._get_dsn()
self.log_level = logging.INFO
self.logger = self._setup_logging()
def _setup_logging(self):
logger = logging.getLogger('imunify360-webshield-watchdog')
logger.setLevel(self.log_level)
handler = logging.handlers.SysLogHandler('/dev/log')
formatter = logging.Formatter('%(name)s: %(message)s')
handler.formatter = formatter
logger.addHandler(handler)
self._init_sentry()
return logger
@classmethod
def _get_server_id(cls):
try:
with open(cls.license_path) as f:
data = json.load(f)
except Exception:
return 'none'
return data.get('id', 'none')
@classmethod
def _get_dsn(cls):
try:
with open(cls.sentry_dsn_path) as f:
return f.read().strip()
except Exception:
return
def _init_sentry(self):
sentry_sdk.init(dsn=self.sentry_dsn, release=self._imunify360_version())
with configure_scope() as scope:
scope.user = {'id': self._get_server_id()}
@classmethod
def _get_config_status(cls):
with open(cls.config_path) as f:
parsed_config = yaml.safe_load(f)
if not 'WEBSHIELD' in parsed_config:
return False
return parsed_config["WEBSHIELD"].get('enable', False)
def _get_current_status(self, attempts=3, wait=5):
for i in range(attempts):
errors = 0
for service in self.services:
try:
proc = subprocess.run(['service', service, 'status'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=self.subprocess_timeout)
except subprocess.TimeoutExpired:
errors = 124
continue
errors += proc.returncode
if not errors:
return True
time.sleep(wait)
return False
def _make_http_request(self, i):
url = "http://0.0.0.0:{}/selfcheck?uuid={}".format(
self.port, uuid.uuid4())
curr_timeout = self.request_timeout * i
try:
requests.get(
url,
headers={'User-Agent': self.user_agent},
allow_redirects=False,
timeout=curr_timeout)
except Exception:
return False
return True
def _check_http_request(self):
for i in range(1, 4):
if self._make_http_request(i):
return True
time.sleep(2)
return False
def _call_service(self, action='restart'):
service = self.services[0]
try:
proc = subprocess.run(
['service', service, action],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=self.subprocess_timeout)
except subprocess.TimeoutExpired:
return False
if proc.returncode != 0:
return False
return True
@classmethod
def _collect_output(cls, cmd):
try:
cp = subprocess.run(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
timeout=cls.subprocess_timeout)
except (OSError, subprocess.TimeoutExpired):
return ''
if cp.returncode != 0:
return ''
return cp.stdout.decode()
@classmethod
def _get_rpm_version(cls):
cmd = ['rpm', '-q', '--queryformat=%{VERSION}-%{RELEASE}',
cls.package_name]
return cls._collect_output(cmd)
@classmethod
def _get_dpkg_version(cls):
cmd = ['dpkg', '--status', cls.package_name]
out = cls._collect_output(cmd)
if not out:
return
for line in out.splitlines():
if line.startswith("Version:"):
return line.strip().split()[1]
@classmethod
def _imunify360_version(cls):
version = cls._get_rpm_version()
if not version:
version = cls._get_dpkg_version()
return version
@classmethod
def _get_flag_timestamp(cls):
try:
with open(cls.flag_path) as o:
return int(o.read().strip())
except Exception:
pass
@classmethod
def _put_flag_timestamp(cls):
tms = int(time.time())
try:
with open(cls.flag_path, 'w') as w:
w.write("{}".format(tms))
except Exception:
pass
@classmethod
def _set_flag(cls):
tms = cls._get_flag_timestamp()
if not tms or time.time() - tms >= 86400: # 24h
cls._put_flag_timestamp()
return True
return False
@classmethod
def _remove_flag_if_exists(cls):
if not os.path.exists(cls.flag_path):
return False
try:
os.unlink(cls.flag_path)
return True
except Exception:
pass
def run(self):
if self.is_enabled and self.is_running:
result = self._check_http_request()
if not result:
done = self._set_flag()
if done: # File has been created or updated
self.logger.error(
'%s is inaccessible', self.services[0])
self._call_service('restart')
else:
done = self._remove_flag_if_exists()
if done: # File has been deleted
self.logger.info('%s is resumed.', self.services[0])
return
if self.is_enabled and not self.is_running:
done = self._set_flag()
if done:
self.logger.error(
'%s is not running. Restart.', self.services[0])
self._call_service('restart')
return
if not self.is_enabled and self.is_running:
self.logger.warning(
'%s is running while being disabled. Stopping...',
self.services[0])
self._call_service('stop')
return
self.logger.info('%s is disabled. OK', self.services[0])
if __name__ == '__main__':
w = Watchdog()
w.run()