XylotrechusZ
#!/opt/imunify360/venv/bin/python3
"""This module import peewee_migrate and apply migrations, for Imunify-AV
it's entrypoint for service"""
import contextlib
import os
import sys
import signal
import threading
import time
from collections.abc import Iterable
from logging import getLogger
from peewee_migrate import migrator
from playhouse.sqlite_ext import SqliteExtDatabase
import defence360agent.internals.logger
from defence360agent.application import app
from defence360agent.application.settings import configure
from defence360agent.contracts.config import Core
from defence360agent.contracts.config import Model
from defence360agent.router import Router
from defence360agent.subsys import systemd_notifier
from defence360agent.model.instance import db as db_instance
from defence360agent.model import tls_check
from defence360agent.utils import (
write_pid_file,
IM360_RESIDENT_PID_PATH,
cleanup_pid_file,
)
from defence360agent.utils.check_db import (
recreate_schema_models,
)
logger = getLogger(__name__)
GO_SERVICE_NAME = "/usr/bin/imunify-resident"
@contextlib.contextmanager
def exc_handler(log_msg: str, reraise: bool):
"""
Logs error in case of exception.
Depending on `reraise`:
- re-raise exception and don't include exception info in the log operation
- do not re-raise exception and include exception info in the log operation
"""
try:
yield
except Exception:
logger.error(log_msg, exc_info=not reraise)
if reraise:
raise
def apply_migrations(db: SqliteExtDatabase, migrations_dirs: Iterable[str]):
"""Apply migrations: restructure db, config files, etc."""
router = Router(
db,
migrations_dirs=migrations_dirs,
logger=logger,
)
# HACK: Migrator uses global unconfigurable LOGGER,
# overrride it, to use our logging settings
migrator.LOGGER = logger
router.run()
def prepare_databases(
migrations_dirs: Iterable[str],
attached_dbs: tuple[tuple[str, str], ...] = tuple(),
):
"""
Apply migrations and recreate attached databases.
The workflow:
1. Apply migrations
2. Regardless whether the migrations were applied - recreate attached databases
3. If the recreation of the attached databases was successful - apply migrations again
- this is done to verify that migrations will successfully apply in future for the recreated databases
- the recreation + the migrations in this step are within the same transaction,
so databases will only be recreated if the migrations can applied after the recreation.
"""
# prepare database to operate in WAL journal_mode and run migrations
tls_check.reset()
db_instance.init(Model.PATH)
attached_schemas = []
for db_path, schema_name in attached_dbs:
db_instance.execute_sql("ATTACH ? AS ?", (db_path, schema_name))
attached_schemas.append(schema_name)
try:
logger.info("Applying database migrations...")
systemd_notifier.notify(systemd_notifier.AgentState.MIGRATING)
with db_instance.atomic("EXCLUSIVE"), exc_handler(
"Error applying migrations", reraise=False
):
apply_migrations(db_instance, migrations_dirs)
logger.info("Recreating attached databases...")
with db_instance.atomic("EXCLUSIVE"), exc_handler(
"Error recreating attached databases", reraise=True
):
# Migration history is stored in main db, so to automatically recreate
# attached dbs it is required to recreate schema for them from models
recreate_schema_models(db_instance, attached_schemas)
# verify migrations can be applied after the attached dbs recreation
with exc_handler(
"Error applying migrations after recreating attached"
" databases",
reraise=True,
):
apply_migrations(db_instance, migrations_dirs)
finally:
# close connection immediately since later this process
# will be replaced by execv
db_instance.close()
# required in case package manager or user sends signals while migrations are still running
def signal_handler(sig, _):
logger.warning("Received signal %s in signal_handler", sig)
logger.warning(
"waiting %d seconds so that migrations can finish",
Core.SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECS,
)
time.sleep(Core.SIGNAL_HANDLER_MIGRATION_TIMEOUT_SECS)
logger.info("Exiting")
sys.exit(0)
def run(*, start_pkg="defence360agent", configure=configure):
"""Entry point for Imunify-AV service. Apply migrations,
and then replace process with {start_pkg}.run module."""
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, signal_handler)
try:
if start_pkg == "im360.run_resident":
write_pid_file(IM360_RESIDENT_PID_PATH)
os.umask(Core.FILE_UMASK)
configure()
defence360agent.internals.logger.reconfigure()
migration_thread = threading.Thread(
target=prepare_databases,
args=(app.MIGRATIONS_DIRS, app.MIGRATIONS_ATTACHED_DBS),
)
migration_thread.start()
migration_thread.join()
systemd_notifier.notify(systemd_notifier.AgentState.READY)
logger.info("Starting main process...")
systemd_notifier.notify(systemd_notifier.AgentState.STARTING)
if start_pkg == "im360.run_resident":
Core.GO_FLAG_FILE.touch(exist_ok=True)
logger.info("Run imunify-resident service")
os.execv(
GO_SERVICE_NAME,
[
GO_SERVICE_NAME,
]
+ sys.argv[1:],
)
else:
os.execv(
sys.executable,
[sys.executable, "-m", "{}".format(start_pkg)] + sys.argv[1:],
)
except Exception:
if start_pkg == "im360.run_resident":
cleanup_pid_file(IM360_RESIDENT_PID_PATH)
if __name__ == "__main__":
run()