????
Current Path : /usr/share/imunify360-webshield/ |
Current 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 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()