#! /usr/bin/python3
# ------------------------------------------------------------------
#
#    Copyright (C) 2013-2015 Canonical Ltd.
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of version 2 of the GNU General Public
#    License published by the Free Software Foundation.
#
# ------------------------------------------------------------------

# FIXME: apparmor package from apparmor-utils is not a namespace package
from apparmor import click
from apparmor.common import AppArmorException
import fcntl
import optparse
import os
import sys

# Where easyprof generated profiles are stored
apparmor_profiles = '/var/lib/apparmor/profiles'
# Where apparmor caches its profiles
apparmor_cache = '/var/cache/apparmor'
# Where the apparmor-profile hook registers its entries to be stored
apparmor_hooks = '/var/lib/apparmor/snappy/profiles'
# Blocking lockfile
clickhook_lockfile = '/run/aa-profile-hook.lock'


def error(out, exit_code=1, do_exit=True):
    '''Print error message and exit'''
    try:
        sys.stderr.write("ERROR: %s\n" % (out))
    except IOError:
        pass

    if do_exit:
        sys.exit(exit_code)


def warn(out):
    '''Print warning message'''
    try:
        sys.stderr.write("WARN: %s\n" % (out))
    except IOError:
        pass


def main():
    parser = optparse.OptionParser()
    parser.add_option("-f", "--force",
                      dest='force',
                      help='force regeneration of all click profiles',
                      action='store_true',
                      default=False)
    parser.add_option("-d", "--debug",
                      dest='debug',
                      help='emit debugging information',
                      action='store_true',
                      default=False)
    (opt, args) = parser.parse_args()

    if not len(args) == 0:
        sys.exit(1)

    lock = open(clickhook_lockfile, 'w')
    fcntl.lockf(lock, fcntl.LOCK_EX)

    if not os.path.exists(apparmor_profiles):
        # FIXME log this
        os.makedirs(apparmor_profiles)

    if not os.path.exists(apparmor_cache):
        # FIXME log this
        os.makedirs(apparmor_cache)

    if opt.force:
        missing_profiles = []
        for p in os.listdir(apparmor_hooks):
            missing_profiles.append(p)
    else:
        missing_profiles = click.get_missing_raw_profiles(apparmor_hooks,
                                                          apparmor_profiles)
    missing_hooks = click.get_missing_rawhooks(apparmor_hooks,
                                               apparmor_profiles)

    load_profiles = []
    for p in missing_profiles:
        try:
            # FIXME: don't reference profile_ here
            full = os.path.join(apparmor_profiles, "profile_" + p)
            click._raw_transform(os.path.join(apparmor_hooks, p), full)
            load_profiles.append(full)
        except Exception:
            error("Error copying '%s' to '%s'" % (p, apparmor_profiles),
                  do_exit=False)
            continue

    # Don't try to load/unload profiles if apparmor isn't available, but be
    # sure to fail if there are problems when it is
    is_available = False
    try:
        click.apparmor_available()
        is_available = True
    except AppArmorException:
        warn("AppArmor not available when processing AppArmor profile hook")

    if is_available:
        # LP: #1383858 - expr tree simplification is too slow for click policy
        # so disable it for now
        click.load_profiles(load_profiles,
                            args=['-r', '--write-cache',
                                  '-O', 'no-expr-simplify',
                                  '--cache-loc=%s' % apparmor_cache])

        # missing_hooks has the profile filename so we need to find the
        # profile name to unload from the kernel.
        # TODO: when we can guarantee the app is not running, then we can
        #       remove the profile. For now leave the profile in place since
        #       the app may still be running
        # removed_profiles = []
        # for fn in missing_hooks:
        #     p = "".join(fn.split('_')[1:])
        #     removed_profiles.append(os.path.join(apparmor_profiles, p)
        # click.unload_profiles(removed_profiles)

    for m in missing_hooks:
        try:
            os.remove(os.path.join(apparmor_profiles, m))
        except Exception:
            error("Error removing '%s'" % os.path.join(apparmor_profiles, m),
                  do_exit=False)
        try:
            os.remove(os.path.join(apparmor_cache, m))
        except Exception:
            error("Error removing '%s'" % os.path.join(apparmor_cache, m),
                  do_exit=False)

    # Unlock and close the file, but don't remove it so we can properly
    # handle 3 or more processes contending for the lock
    fcntl.lockf(lock, fcntl.LOCK_UN)
    lock.close()

    return 0

if __name__ == "__main__":
    sys.exit(main())
