Mini Shell
#!/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 sys
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',)
mode_flag_path = '/usr/share/imunify360-webshield/modularity_mode'
wafd_sock_path = "/var/run/imunify360/libiplists-daemon.sock"
wafd_check_binary = "i360_wafd_check"
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])
def check_wafd(self):
"""
The wafd is expected to be running by all means
because not only the webshield is dependent on it.
We call small wafd utility to check wafd is responsive.
Otherwise we'll try to restart wafd.
"""
check_ip = "93.89.215.4"
wafd_path = "/var/run/imunify360/libiplists-daemon.sock"
cmd = [self.wafd_check_binary, "-path", self.wafd_sock_path, check_ip]
try:
p = subprocess.run(cmd, check=True, timeout=2, capture_output=True)
except Exception:
# On any exception we just fall through to restart wafd
pass
else:
out = p.stdout.decode("utf-8")
if "Response" in out and check_ip in out and "status: 0" in out:
# We got a sensible response, so wafd is running and responsible.
# Nothing to do, return
return
# If we got here it means that wafd is not responsible. Trying to restart it
cmd = ["systemctl", "restart", "imunify360-wafd"]
try:
subprocess.run(cmd, check=True)
except Exception as e:
self.logger.error("Failed to restart wafd: %s", e)
@classmethod
def is_standalone(cls):
try:
with open(cls.mode_flag_path) as f:
mode = f.read().strip()
if mode in ('nginx', 'apache'):
return False
return True
except Exception:
# A file read error we treat as standalone mode
return True
if __name__ == '__main__':
w = Watchdog()
w.check_wafd()
if not Watchdog.is_standalone():
sys.exit()
w.run()
Zerion Mini Shell 1.0