#!/usr/bin/env python3
# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Upgrade conversion script.

Usage: %prog DATAVERSION LIBVERSION

This script carries out any and all required conversions from the data
format created by agent version DATAVERSION to the format requried by
agent version LIBVERSION.

It also runs the post-postinstall script, if present.

The script currently doesn't support downgrading.

See EEP 10.

"""

# Python imports
import os
import re
import sys
import glob
import pickle
import shutil
import logging
import optparse

# Sigma imports
from sigma.util import avalog, config, dance
from sigma.util.errors import usage

# Global objects
options = config.Options()
logger = logging.getLogger("conversion")
parser = optparse.OptionParser(usage=__doc__.strip())

def main(args=None):
    opts, args = parser.parse_args(args)
    if len(args) != 2:
        return usage("This program takes exactly two arguments.")
    dataversion, libversion = args
    avalog.setupLogging(options)
    logger.info("Begin conversion from %s to %s", dataversion, libversion)
    if not run_post_postinstall():
        logger.error("Post-postinstall script failed!")
        return 1
    delete_obsolete_pickles()
    c = Converter()
    if not c.convert(dataversion, libversion):
        logger.error("Conversion from %s to %s failed!",
                     dataversion, libversion)
        return 1
    logger.info("Completed conversion from %s to %s", dataversion, libversion)
    return 0

def run_post_postinstall():
    """Run the post-postinstall script, if it exists.

    Return True if successful (no script is also a success).

    The script should be executable.

    """
    script = os.path.join(options.agentactive, "scripts", "post_postinstall")
    if os.name == "nt":
        script += ".bat"
    if not os.path.exists(script):
        logger.info("Script %s not found, skipping post-postinstall", script)
        return True
    logger.info("Running post-postinstall script %s", script)
    sts = os.system("sh %s %s" % (script, options.agenthome))
    return sts == 0

def delete_obsolete_pickles():
    """Delete $AVDATA/*.pickle files that can't be loaded any more."""
    files = glob.glob(options.datapath("*.pickle"))
    for file in files:
        try:
            f = open(file, "rb")
        except IOError as err:
            logger.warn("Can't open %s: %s", file, err)
        else:
            try:
                try:
                    p = pickle.load(f)
                    if os.path.normcase(file).endswith("matrix.pickle"):
                        # The matrix.picke file contains TWO pickles!
                        q = pickle.load(f)
                    continue
                except Exception as err:
                    logger.warn("Can't read pickle from %s: %s", file, err)
            finally:
                f.close()
        try:
            os.remove(file)
        except os.error as err:
            logger.warn("Can't remove %s: %s", file, err)
        else:
            logger.info("Deleted bad pickle file %s", file)

class Converter(object):

    """A little framework for writing conversion methods.

    Versions must be positive integers.

    For consecutive integers X and Y (Y == X+1), the conversion from X
    to Y should be a method conv_X_to_Y().  The method should return
    True for success, False for failure.

    For non-consecutive integers X and Z (Z > X+1), the stepwise
    conversion methods conv_X_to_Y(), ..., conv_Y_to_Z() are
    automatically called.

    """

    def convert(self, dataversion, libversion):
        try:
            dv = int(dataversion)
            lv = int(libversion)
        except ValueError as err:
            logger.error("Non-integral version (%s -> %s): %s",
                         dataversion, libversion, err)
            return False
        if dv < 0 or lv < 0:
            logger.error("Negative version (%s -> %s)",
                         dataversion, libversion)
            return False
        if dv > lv:
            logger.error("Can't downgrade (%s -> %s)",
                         dataversion, libversion)
            return False
        ok = True
        for v in range(dv, lv):
            methodname = "conv_%d_to_%d" % (v, v+1)
            method = getattr(self, methodname, None)
            if not method:
                logger.error("No conversion method found (%s -> %s)",
                             dataversion, libversion)
                ok = False
            elif not method():
                ok = False
        return ok

    def conv_0_to_1(self):
        # Example; version 0 shouldn't be used
        return True

    # Rev 2

    def conv_1_to_2(self):
        # P4 change 10444 -- change in matrix data format
        return True

    # Rev 3

    def conv_2_to_3(self):
        # P4 change 10653 -- change [Aa]valon to [Ee]lemental everywhere
        # Change [avalon-xyzzy] headings in the config file to [xyzzy]
        fn = options.configfile
        if not os.path.exists(fn):
            return True
        try:
            f = open(fn, "r")
        except IOError as err:
            logger.error("Can't open %s: %s", fn, err)
            return False
        try:
            lines = f.readlines()
        finally:
            f.close()
        changes = 0
        for i, line in enumerate(lines):
            if line.startswith("[avalon-"):
                lines[i] = "[" + line[8:]
                changes += 1
        if not changes:
            return True
        fnew = fn + ".new"
        try:
            f = open(fnew, "w")
        except IOError as err:
            logger.error("Can't create %s: %s", fnew, err)
            return False
        try:
            f.writelines(lines)
        finally:
            f.close()
        err = dance.rename(fnew, fn, logger)
        if err is not None:
            return False
        if os.name == "nt":
            # XXX The ice is rather thin here
            opt2 = config.Options(options.configfile)
            svcname = config.optstr("host", "service-name").__get__(opt2)
            if svcname is None:
                opt2.service_name = "AvalonAgent"
                try:
                    opt2.write()
                except EnvironmentError as err:
                    logger.error("Can't write %s: %s", opt2.configfile, err)
                else:
                    logger.info("Set service-name = AvalonAgent")
        return True

    # Rev 4

    # Fix screwup in installer
    conv_3_to_4 = conv_2_to_3

    # Rev 5

    def conv_4_to_5(self):
        """Rename [Aa]valon to [Ee]lemental in installer.

        The post-postinstall script should have installed the new
        service; need to get rid of the old service now.

        Note: we are *not* changing /opt/Avalon to /opt/sigma; but
        we *are* changing /etc/avalon to /etc/sigma and
        /etc/init.d/avalon_agent to /etc/init.d/sigma_agent, and
        ditto for all symbolic links to it.  The new files are
        installed by the post-postinstall script; here all we have to
        do is get rid of the old symbolic links.  (We are leaving the
        scripts in place because they are referenced by the RPM
        uninstall scripts.)

        """
        if os.name == "nt":
            # XXX Is there anything we should do on Windows?
            return True
        if not os.path.isdir("/etc/sigma"):
            logging.error("Can't find /etc/sigma")
            return False
        # Delete links first, then service script, then /etc/avalon
        # (since this is how the dependencies work)
        links = glob.glob("/etc/rc?.d/[KS][0-9][0-9]avalon_agent")
        for name in links:
            try:
                os.remove(name)
            except os.error as err:
                logger.error("Can't remove link %s: %s", name, err)
                return False
            else:
                logger.info("Removed link %s", name)
        return True

    # Rev 6

    def conv_5_to_6(self):
        """Add the upgrade-cleanup task."""
        import datetime
        from sigma.sched import tasks, timezones, scheduling
        t = tasks.newtask("upgrade-cleanup",
                          "upgrade.py cleanup",
                          datetime.datetime(2004, 1, 2, 8, 0, 0,
                                            tzinfo=timezones.utc),
                          86400)
        sfn = options.datapath("schedule.xml")
        s = scheduling.TaskScheduler(sfn)
        s.add_list([t])
        s.save()
        return True

    # Rev 7 (release ECS 1.1)

    def conv_6_to_7(self):
        "Needed because some /etc/ files changed."
        return True

    # Rev 8

    def conv_7_to_8(self):
        "Create registry entry to start AvSysTray."
        if os.name != "nt":
            return True # This only applies to Windows
        import winreg as _winreg
        keypath = r"Software\Microsoft\Windows\CurrentVersion\Run"
        avsystraypath = os.path.join(options.agentbin, "AvSystray.exe")
        try:
            key = _winreg.CreateKey(_winreg.HKEY_LOCAL_MACHINE, keypath)
            _winreg.SetValueEx(key, "AvSysTray", 0,
                               _winreg.REG_SZ, avsystraypath)
            _winreg.CloseKey(key)
        except EnvironmentError as err:
            logger.exception("Can't create AvSysTray registry entry")
            return False
        return True

    # Rev 9

    def conv_8_to_9(self):
        "Rename mandate.* tasks to engine.*, and mandate.* files to dir.*."
        from sigma.sched import tasks, scheduling
        from sigma.protocol import xmlproto
        sfn = options.datapath("schedule.xml")
        s = scheduling.TaskScheduler(sfn)
        s.load(sfn)
        renames = []
        taskdict = s.list_tasks()
        for id in taskdict:
            if id.startswith("mandate."):
                renames.append(tasks.newtask(id)) # delete the old task
                t = taskdict[id]
                t.id = "engine." + id[8:]
                logger.info("Renaming task %s to %s", id, t.id)
                renames.append(t) # add the new task
        if renames:
            s.add_list(renames)
            s.save()
        names = xmlproto.list_policydir()
        ok = True
        for name in names:
            if name.startswith("mandate."):
                newname = "dir." + name[8:]
                logger.info("Renaming policy/%s to policy/%s", name, newname)
                fullname = os.path.join(xmlproto.policydir, name)
                fullnewname = os.path.join(xmlproto.policydir, newname)
                try:
                    os.rename(fullname, fullnewname)
                except os.error as err:
                    logger.error("Rename failed:", err)
                    ok = False
        return ok

if __name__ == "__main__":
    try:
        sys.exit(main())
    except (SystemExit, KeyboardInterrupt):
        raise
    except:
        avalog.setupLogging(options)
        logger.exception("Unexpected exception:")
        sys.exit(1)
