#!/omd/versions/1.5.0p24.cre/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
#
#       U  ___ u  __  __   ____
#        \/"_ \/U|' \/ '|u|  _"\
#        | | | |\| |\/| |/| | | |
#    .-,_| |_| | | |  | |U| |_| |\
#     \_)-\___/  |_|  |_| |____/ u
#          \\   <<,-,,-.   |||_
#         (__)   (./  \.) (__)_)
#
# This file is part of OMD - The Open Monitoring Distribution.
# The official homepage is at <http://omdistro.org>.
#
# OMD  is  free software;  you  can  redistribute it  and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the  Free Software  Foundation  in  version 2.  OMD  is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# This hack here is needed to prevent "omd update" problems when updating
# as site user from 1.4 versions older than 1.4.0p17.
# Previous versios did not unset PYTHONPATH/LD_LIBRARY_PATH before execv()
# to the newer version "omd" command which made the newer OMD load the old
# python libraries. Newer versions unset these variables before, so this
# additional execv() is only needed when updating from older versions.
import os, sys
if len(sys.argv) > 1 and sys.argv[1] == "update" \
   and (sys.version_info[0] == 2 and sys.version_info[1] == 7 and sys.version_info[2] < 14):
    # Prevent inheriting environment variables from this versions/site environment
    # into the execed omd call. The OMD call must import the python version related
    # modules and libaries. This only works when PYTHONPATH and LD_LIBRARY_PATH are
    # not already set when calling "omd update"
    try:
        del os.environ["PYTHONPATH"]
    except KeyError:
        pass

    try:
        del os.environ["LD_LIBRARY_PATH"]
    except KeyError:
        pass
    os.execv(sys.argv[0], sys.argv)
    bail_out("Cannot run execv() %s" % sys.argv[0])

import sys, os, shutil, pwd, grp, re, time, pprint, tty, termios, traceback
import errno
import fcntl
import string
import psutil
import random
import tarfile, fnmatch
import socket
import subprocess
import signal
import shlex
from subprocess import *

OMD_VERSION = "1.5.0p24.cre"

# Some global variables
opt_verbose = False
opt_interactive = False
opt_force = False

# Global variables, that are *always* present as soon as we deal with
# one specific site. They are set by set_site_globals()
g_sitename = None  # "mysite"
g_sitedir = None  # "/omd/sites/mysite"
g_site_conf = None  # { "CORE" : "nagios", ... } (contents of etc/omd/site.conf plus defaults from hooks)

#   .--Logging-------------------------------------------------------------.
#   |                _                      _                              |
#   |               | |    ___   __ _  __ _(_)_ __   __ _                  |
#   |               | |   / _ \ / _` |/ _` | | '_ \ / _` |                 |
#   |               | |__| (_) | (_| | (_| | | | | | (_| |                 |
#   |               |_____\___/ \__, |\__, |_|_| |_|\__, |                 |
#   |                           |___/ |___/         |___/                  |
#   +----------------------------------------------------------------------+
#   | Helper functions for output on the TTY                               |
#   '----------------------------------------------------------------------'

# colored output, if stdout is a tty
on_tty = sys.stdout.isatty()

if on_tty:
    tty_black = '\033[30m'
    tty_red = '\033[31m'
    tty_green = '\033[32m'
    tty_yellow = '\033[33m'
    tty_blue = '\033[34m'
    tty_magenta = '\033[35m'
    tty_cyan = '\033[36m'
    tty_white = '\033[37m'
    tty_bgblack = '\033[40m'
    tty_bgred = '\033[41m'
    tty_bggreen = '\033[42m'
    tty_bgyellow = '\033[43m'
    tty_bgblue = '\033[44m'
    tty_bgmagenta = '\033[45m'
    tty_bgcyan = '\033[46m'
    tty_bgwhite = '\033[47m'
    tty_bold = '\033[1m'
    tty_underline = '\033[4m'
    tty_normal = '\033[0m'
    tty_ok = tty_green + tty_bold + 'OK' + tty_normal
    tty_error = tty_red + tty_bold + 'ERROR' + tty_normal
    tty_warn = tty_yellow + tty_bold + 'WARNING' + tty_normal
else:
    tty_black = ''
    tty_red = ''
    tty_green = ''
    tty_yellow = ''
    tty_blue = ''
    tty_magenta = ''
    tty_cyan = ''
    tty_white = ''
    tty_bgred = ''
    tty_bggreen = ''
    tty_bgyellow = ''
    tty_bgblue = ''
    tty_bgmagenta = ''
    tty_bgcyan = ''
    tty_bold = ''
    tty_underline = ''
    tty_normal = ''
    tty_ok = 'OK'
    tty_error = 'ERROR'
    tty_warn = 'WARNING'


def ok():
    sys.stdout.write(tty_ok + "\n")


def bail_out(message):
    sys.exit(message)


# Symbols for update
good = " " + tty_green + tty_bold + "*" + tty_normal
warn = " " + tty_bgyellow + tty_black + tty_bold + "!" + tty_normal
error = " " + tty_bgred + tty_white + tty_bold + "!" + tty_normal


# Is used to duplicate output from stdout/stderr to a logfiles. This
# is e.g. used during "omd update" to have a chance to analyze errors
# during past updates
class Log(object):
    def __init__(self, fd, logfile):
        self.log = open(logfile, 'a')
        self.fd = fd

        if self.fd == 1:
            self.orig = sys.stdout
            sys.stdout = self
        else:
            self.orig = sys.stderr
            sys.stderr = self

        self.color_replace = re.compile("\033\[\d{1,2}m", re.UNICODE)

    def __del__(self):
        if self.fd == 1:
            sys.stdout = self.orig
        else:
            sys.stderr = self.orig
        self.log.close()

    def write(self, data):
        self.orig.write(data)
        self.log.write(self.color_replace.sub('', data))

    def flush(self):
        self.log.flush()
        self.orig.flush()


g_stdout_log = None
g_stderr_log = None


def start_logging(logfile):
    global g_stdout_log, g_stderr_log
    g_stdout_log = Log(1, logfile)
    g_stderr_log = Log(2, logfile)


def stop_logging():
    global g_stdout_log, g_stderr_log
    g_stdout_log = None
    g_stderr_log = None


def show_success(exit_code):
    if exit_code == True or exit_code == 0:
        ok()
    else:
        sys.stdout.write(tty_error + "\n")
    return exit_code


#.
#   .--Dialog--------------------------------------------------------------.
#   |                     ____  _       _                                  |
#   |                    |  _ \(_) __ _| | ___   __ _                      |
#   |                    | | | | |/ _` | |/ _ \ / _` |                     |
#   |                    | |_| | | (_| | | (_) | (_| |                     |
#   |                    |____/|_|\__,_|_|\___/ \__, |                     |
#   |                                           |___/                      |
#   +----------------------------------------------------------------------+
#   |  Wrapper functions for interactive dialogs using the dialog cmd tool |
#   '----------------------------------------------------------------------'

patch_supports_merge = None


def patch_has_merge():
    # check wether our version of patch supports the option '--merge'
    global patch_supports_merge
    if patch_supports_merge == None:
        patch_supports_merge = (0 == os.system(
            "true | PATH=/omd/versions/default/bin:$PATH patch --merge >/dev/null 2>&1"))
        if not patch_supports_merge:
            sys.stdout.write("Your version of patch does not support --merge.\n")
    return patch_supports_merge


def run_dialog(args):
    env = {"TERM": getenv("TERM", "linux"), "LANG": "de_DE.UTF-8"}
    p = Popen(["dialog", "--shadow"] + args, env=env, stderr=PIPE)
    response = p.stderr.read()
    return 0 == os.waitpid(p.pid, 0)[1], response


def dialog_menu(title, text, choices, defvalue, oktext, canceltext):
    args = ["--ok-label", oktext, "--cancel-label", canceltext]
    if defvalue != None:
        args += ["--default-item", defvalue]
    args += ["--title", title, "--menu", text, "0", "0", "0"]  # "20", "60", "17" ]
    for text, value in choices:
        args += [text, value]
    return run_dialog(args)


def dialog_regex(title, text, regex, value, oktext, canceltext):
    while True:
        args = [
            "--ok-label", oktext, "--cancel-label", canceltext, "--title", title, "--inputbox",
            text, "0", "0", value
        ]
        change, new_value = run_dialog(args)
        if not change:
            return False, value
        elif not regex.match(new_value):
            dialog_message("Invalid value. Please try again.")
            value = new_value
        else:
            return True, new_value


def dialog_yesno(text, yeslabel="yes", nolabel="no"):
    state, response = run_dialog(
        ["--yes-label", yeslabel, "--no-label", nolabel, "--yesno", text, "0", "0"])
    return state


def dialog_message(text, buttonlabel="OK"):
    run_dialog(["--ok-label", buttonlabel, "--msgbox", text, "0", "0"])


def user_confirms(title, message, relpath, yes_choice, yes_text, no_choice, no_text):
    # Handle non-interactive mode
    if opt_conflict == "abort":
        bail_out("Update aborted.")
    elif opt_conflict == "install":
        return False
    elif opt_conflict == "keepold":
        return True

    user_path = g_sitedir + "/" + relpath
    options = [(yes_choice, yes_text), (no_choice, no_text),
               ("shell", "Open a shell for looking around"), ("abort",
                                                              "Stop here and abort update!")]
    while True:
        choice = ask_user_choices(title, message, options)
        if choice == "abort":
            bail_out("Update aborted.")
        elif choice == "shell":
            thedir = "/".join(user_path.split("/")[:-1])
            sys.stdout.write("\n Starting BASH. Type CTRL-D to continue.\n\n")
            os.system("cd '%s' ; bash -i" % thedir)  # nosec
        else:
            return choice == yes_choice


def wrap_text(text, width):
    def fillup(line, width):
        if len(line) < width:
            line += " " * (width - len(line))
        return line

    def justify(line, width):
        need_spaces = float(width - len(line))
        spaces = float(line.count(' '))
        newline = ""
        x = 0.0
        s = 0.0
        words = line.split()
        newline = words[0]
        for word in words[1:]:
            newline += ' '
            x += 1.0
            if s / x < need_spaces / spaces:
                newline += ' '
                s += 1
            newline += word
        return newline

    wrapped = []
    line = ""
    col = 0
    for word in text.split():
        netto = len(word)
        if line != "" and netto + col + 1 > width:
            wrapped.append(justify(line, width))
            col = 0
            line = ""
        if line != "":
            line += ' '
            col += 1
        line += word
        col += netto
    if line != "":
        wrapped.append(fillup(line, width))

    # remove trailing empty lines
    while wrapped[-1].strip() == "":
        wrapped = wrapped[:-1]
    return wrapped


def getch():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    if ord(ch) == 3:
        raise KeyboardInterrupt()
    return ch


def ask_user_choices(title, message, choices):
    sys.stdout.write("\n")

    def pl(line):
        sys.stdout.write(" %s %-76s %s\n" % (tty_bgcyan + tty_white, line, tty_normal))

    pl("")
    sys.stdout.write(" %s %-76s %s\n" % (tty_bgcyan + tty_white + tty_bold, title, tty_normal))
    for line in wrap_text(message, 76):
        pl(line)
    pl("")
    chars = []
    empty_line = " %s%-78s%s\n" % (tty_bgblue + tty_white, "", tty_normal)
    sys.stdout.write(empty_line)
    for choice, title in choices:
        sys.stdout.write(
            " %s %s%s%s%-10s %-65s%s\n" % (tty_bgblue + tty_white, tty_bold, choice[0], tty_normal +
                                           tty_bgblue + tty_white, choice[1:], title, tty_normal))
        for c in choice:
            if c.lower() not in chars:
                chars.append(c)
                break
    sys.stdout.write(empty_line)

    choicetxt = (tty_bold + tty_magenta + "/").join(
        [(tty_bold + tty_white + char + tty_normal + tty_bgmagenta)
         for (char, (c, t)) in zip(chars, choices)])
    l = len(choices) * 2 - 1
    sys.stdout.write(" %s %s" % (tty_bgmagenta, choicetxt))
    sys.stdout.write(" ==> %s   %s" % (tty_bgred, tty_bgmagenta))
    sys.stdout.write(" " * (69 - l))
    sys.stdout.write("\b" * (71 - l))
    sys.stdout.write(tty_normal)
    while True:
        a = getch()
        for char, (choice, title) in zip(chars, choices):
            if a == char:
                sys.stdout.write(tty_bold + tty_bgred + tty_white + a + tty_normal + "\n\n")
                return choice


#.
#   .--Users/Groups--------------------------------------------------------.
#   |     _   _                      ______                                |
#   |    | | | |___  ___ _ __ ___   / / ___|_ __ ___  _   _ _ __  ___      |
#   |    | | | / __|/ _ \ '__/ __| / / |  _| '__/ _ \| | | | '_ \/ __|     |
#   |    | |_| \__ \  __/ |  \__ \/ /| |_| | | | (_) | |_| | |_) \__ \     |
#   |     \___/|___/\___|_|  |___/_/  \____|_|  \___/ \__,_| .__/|___/     |
#   |                                                      |_|             |
#   +----------------------------------------------------------------------+
#   |  Helper functions for dealing with Linux users and groups            |
#   '----------------------------------------------------------------------'


def find_processes_of_user(username):
    try:
        return os.popen("pgrep -u '%s'" % username).read().split()  # nosec
    except:
        return []


def groupdel(groupname):
    try:
        p = subprocess.Popen(["groupdel", groupname],
                             stdin=open(os.devnull, "r"),
                             stdout=open(os.devnull, "w"),
                             stderr=subprocess.PIPE,
                             close_fds=True)
    except OSError, e:
        bail_out("\n" + tty_error + ": Failed to delete user '%s': %s" % (groupname, e))

    stderr = p.communicate()[1]
    if p.returncode != 0:
        bail_out("\n" + tty_error + ": Failed to delete user '%s': %s" % (groupname, stderr))


def groupadd(groupname, gid=None):
    cmd = "groupadd "
    if gid != None:
        cmd += "-g %d " % int(gid)
    cmd += groupname

    if 0 != os.system(cmd):  # nosec
        bail_out("Cannot create group for site user.")


def useradd(sitename, uid=None, gid=None):
    # Create user for running site 'name'
    groupadd(sitename, gid)
    useradd_options = g_info["USERADD_OPTIONS"]
    if uid != None:
        useradd_options += " -u %d" % int(uid)
    if 0 != os.system(  # nosec
            "useradd %s -r -d '%s' -c 'OMD site %s' -g %s -G omd %s -s /bin/bash" % \
                      (useradd_options, site_dir(sitename), sitename, sitename, sitename)):
        groupdel(sitename)
        bail_out("Error creating site user.")

    # On SLES11+ there is a standard group "trusted" that the OMD site users should be members
    # of to be able to access CRON.
    if group_exists("trusted"):
        add_user_to_group(sitename, "trusted")

    # Add Apache to new group. It needs to be able to write in to the
    # command pipe and possible other stuff
    add_user_to_group(g_info["APACHE_USER"], sitename)


def add_user_to_group(user, group):
    cmd = g_info["ADD_USER_TO_GROUP"] % {"user": user, "group": group}
    return os.system(cmd + " >/dev/null") == 0  # nosec


def userdel(name):
    try:
        p = subprocess.Popen(["userdel", "-r", name],
                             stdin=open(os.devnull, "r"),
                             stdout=open(os.devnull, "w"),
                             stderr=subprocess.PIPE,
                             close_fds=True)
    except OSError, e:
        bail_out("\n" + tty_error + ": Failed to delete user '%s': %s" % (name, e))

    stderr = p.communicate()[1]
    if p.returncode != 0:
        bail_out("\n" + tty_error + ": Failed to delete user '%s': %s" % (name, stderr))

    # On some OSes (e.g. debian) the group is automatically removed if
    # it bears the same name as the user. So first check for the group.
    if group_exists(name):
        groupdel(name)


def user_by_id(id):
    try:
        return pwd.getpwuid(id)
    except:
        return None


def user_id(name):
    try:
        return pwd.getpwnam(name).pw_uid
    except:
        return False


def user_exists(name):
    try:
        pwd.getpwnam(name)
        return True
    except:
        return False


def user_has_group(user, group):
    try:
        u = user_by_id(user_id(user))
        g = group_by_id(u.pw_gid)
        if g.gr_name == group:
            return True
        g = group_by_id(group_id(group))
        if user in g.gr_mem:
            return True
    except:
        return False


def group_exists(name):
    try:
        grp.getgrnam(name)
        return True
    except:
        return False


def group_by_id(id):
    try:
        return grp.getgrgid(id)
    except:
        return None


def group_id(name):
    try:
        g = grp.getgrnam(name)
        return g.gr_gid
    except:
        return None


def user_logged_in(name):
    # Check, if processes of named user are existing
    return os.system("ps --no-headers --user '%s' >/dev/null 2>&1" % name) == 0  # nosec


def user_verify(name, allow_populated=False):

    if not user_exists(name):
        bail_out(tty_error + ": user %s does not exist" % name)

    user = user_by_id(user_id(name))
    if user.pw_dir != site_dir(name):
        bail_out(tty_error +
                 ": Wrong home directory for user %s, must be %s" % (name, site_dir(name)))

    if not os.path.exists(site_dir(name)):
        bail_out(tty_error +
                 ": home directory for user %s (%s) does not exist" % (name, site_dir(name)))

    if not allow_populated and os.path.exists(site_dir(name) + "/version"):
        bail_out(tty_error +
                 ": home directory for user %s (%s) must be empty" % (name, site_dir(name)))

    if not file_owner_verify(site_dir(name), user.pw_uid, user.pw_gid):
        bail_out(tty_error + ": home directory (%s) is not owned by user %s and group %s" %
                 (site_dir(name), name, name))

    group = group_by_id(user.pw_gid)
    if group == None or group.gr_name != name:
        bail_out(tty_error + ": primary group for siteuser must be %s" % name)

    if not user_has_group(g_info["APACHE_USER"], name):
        bail_out(tty_error +
                 ": apache user %s must be member of group %s" % (g_info["APACHE_USER"], name))

    if not user_has_group(name, "omd"):
        bail_out(tty_error + ": siteuser must be member of group omd")

    return True


def switch_to_site_user():
    p = pwd.getpwnam(g_sitename)
    uid = p.pw_uid
    gid = p.pw_gid
    os.chdir(p.pw_dir)
    os.setgid(gid)

    # Darn. The site user might have been put into further groups.
    # This is e.g. needed if you want to access the livestatus socket
    # from one site by another. We make use of the "id" command here.
    # If you know something better, that does not rely on an external
    # command (and that does not try to parse around /etc/group, of
    # course), then please tell mk -> mk@mathias-kettner.de.
    os.setgroups(groups_of(g_sitename))
    os.setuid(uid)


def groups_of(username):
    return map(int, os.popen("id -G '%s'" % username).read().split())  # nosec



#.
#   .--Sites---------------------------------------------------------------.
#   |                        ____  _ _                                     |
#   |                       / ___|(_) |_ ___  ___                          |
#   |                       \___ \| | __/ _ \/ __|                         |
#   |                        ___) | | ||  __/\__ \                         |
#   |                       |____/|_|\__\___||___/                         |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Helper functions for dealing with sites                             |
#   '----------------------------------------------------------------------'


# Sets all site global variables except g_sitename - which must already be set.
def set_site_globals():
    global g_sitedir
    g_sitedir = site_dir(g_sitename)
    load_site_conf()


def site_dir(sitename):
    return "/omd/sites/" + sitename


def tmp_dir(sitename):
    return "/omd/sites/%s/tmp" % sitename


def site_name():
    return pwd.getpwuid(os.getuid()).pw_name


def is_root():
    return os.getuid() == 0


def site_exists(sitename):
    return os.path.exists(site_dir(sitename))


def all_sites():
    l = [s for s in os.listdir("/omd/sites") if os.path.isdir(os.path.join("/omd/sites/", s))]
    l.sort()
    return l


# Check if site is completely stopped
def site_is_stopped(sitename):
    return check_status(sitename, False) == 1


def site_is_running(sitename):
    return check_status(sitename, False) == 0


def site_is_empty(sitename):
    sitedir = site_dir(sitename)
    for entry in os.listdir(sitedir):
        if entry not in ['.', '..']:
            return False
    return True


# Determines wether a specific site is set to autostart. Note that
# this needs to be called from a non-site-specific context and
# can there not use the g_site... variables.
def site_autostart(sitename):
    config = parse_site_conf(sitename)
    return config.get('AUTOSTART', 'on') == 'on'


# The version of a site is solely determined by the
# link ~SITE/version
def site_version(sitename):
    version_link = site_dir(sitename) + "/version"
    try:
        version = os.readlink(version_link).split("/")[-1]
        return version
    except:
        return None


def start_site(sitename):
    prepare_and_populate_tmpfs(sitename)
    call_init_scripts(sitename, "start")


def stop_if_not_stopped(sitename):
    if not site_is_stopped(sitename):
        stop_site(sitename)


def stop_site(sitename):
    call_init_scripts(sitename, "stop")



#.
#   .--Skeleton------------------------------------------------------------.
#   |                ____  _        _      _                               |
#   |               / ___|| | _____| | ___| |_ ___  _ __                   |
#   |               \___ \| |/ / _ \ |/ _ \ __/ _ \| '_ \                  |
#   |                ___) |   <  __/ |  __/ || (_) | | | |                 |
#   |               |____/|_|\_\___|_|\___|\__\___/|_| |_|                 |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Deal with file owners, permissions and the the skel hierarchy       |
#   '----------------------------------------------------------------------'


def read_skel_permissions():
    global g_skel_permissions
    g_skel_permissions = load_skel_permissions(OMD_VERSION)
    if not g_skel_permissions:
        bail_out("%s is missing or currupted." % file_path)


def load_skel_permissions(version):
    perms = {}
    file_path = "/omd/versions/%s/share/omd/skel.permissions" % version
    try:
        for line in file(file_path):
            line = line.strip()
            if line == "" or line[0] == "#":
                continue
            path, perm = line.split()
            mode = int(perm, 8)
            perms[path] = mode
        return perms
    except:
        return None


def get_skel_permissions(version, perms, relpath):
    try:
        return perms[relpath]
    except:
        return get_file_permissions("/omd/versions/%s/skel/%s" % (version, relpath))


def get_file_permissions(path):
    try:
        return os.stat(path).st_mode & 07777
    except:
        return 0


def get_file_owner(path):
    try:
        return pwd.getpwuid(os.stat(path).st_uid)[0]
    except:
        return None


def create_version_symlink(sitename, version):
    linkname = site_dir(sitename) + "/version"
    if os.path.exists(linkname):
        os.remove(linkname)
    os.symlink("../../versions/%s" % OMD_VERSION, linkname)


def calculate_admin_password(options):
    return options.get("admin-password", random_password())


def set_admin_password(pw):
    file("%s/etc/htpasswd" % g_sitedir, "w").write("cmkadmin:%s\n" % encrypt_password(pw))


def file_owner_verify(path, user_id, group_id):
    try:
        s = os.stat(path)
        if s.st_uid != user_id or s.st_gid != group_id:
            return False
    except:
        return False
    return True


def create_skeleton_files(sitename, dir):
    read_skel_permissions()
    sitedir = site_dir(sitename)
    replacements = {
        "###SITE###": sitename,
        "###ROOT###": sitedir,
    }
    # Hack: exclude tmp if dir is '.'
    exclude_tmp = dir == "."
    skelroot = "/omd/versions/%s/skel" % OMD_VERSION
    os.chdir(skelroot)  # make relative paths
    for dirpath, dirnames, filenames in os.walk(dir):
        if dirpath.startswith("./"):
            dirpath = dirpath[2:]
        for entry in dirnames + filenames:
            if exclude_tmp:
                if dirpath == "." and entry == "tmp":
                    continue
                if dirpath == "tmp" or dirpath.startswith("tmp/"):
                    continue
            create_skeleton_file(skelroot, sitedir, dirpath + "/" + entry, replacements)


def save_version_meta_data(sitename, version):
    """Make meta information from the version available in the site directory

    Currently it holds the following information
    A) A copy of the versions skel/ directory
    B) A copy of the skel.permissions file
    C) A version file containing the version number of the meta data
    """
    version_meta_dir = "%s/.version_meta" % site_dir(sitename)

    try:
        shutil.rmtree(version_meta_dir)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise

    skelroot = "/omd/versions/%s/skel" % version
    shutil.copytree(skelroot, "%s/skel" % version_meta_dir, symlinks=True)

    skel_permissions_file_path = "/omd/versions/%s/share/omd/skel.permissions" % version
    shutil.copy(skel_permissions_file_path, "%s/skel.permissions" % version_meta_dir)

    with open("%s/version" % version_meta_dir, "w") as f:
        f.write("%s\n" % version)


def delete_user_file(user_path):
    if not os.path.islink(user_path) and os.path.isdir(user_path):
        shutil.rmtree(user_path)
    else:
        os.remove(user_path)


def delete_directory_contents(d):
    for f in os.listdir(d):
        delete_user_file(d + '/' + f)


def create_skeleton_file(skelbase, userbase, relpath, replacements):
    skel_path = skelbase + "/" + relpath
    user_path = userbase + "/" + relpath

    # Remove old version, if existing (needed during update)
    if os.path.exists(user_path):
        delete_user_file(user_path)

    # Create directories, symlinks and files
    if os.path.islink(skel_path):
        os.symlink(os.readlink(skel_path), user_path)
    elif os.path.isdir(skel_path):
        os.makedirs(user_path)
    else:
        file(user_path, "w").write(replace_tags(file(skel_path).read(), replacements))

    if not os.path.islink(skel_path):
        mode = g_skel_permissions.get(relpath)
        if mode == None:
            if os.path.isdir(skel_path):
                mode = 0755
            else:
                mode = 0644
        os.chmod(user_path, mode)


def chown_tree(dir, user):
    uid = pwd.getpwnam(user).pw_uid
    gid = pwd.getpwnam(user).pw_gid
    os.chown(dir, uid, gid)
    for dirpath, dirnames, filenames in os.walk(dir):
        for entry in dirnames + filenames:
            os.lchown(dirpath + "/" + entry, uid, gid)


def try_chown(filename, user):
    if os.path.exists(filename):
        try:
            uid = pwd.getpwnam(user).pw_uid
            gid = pwd.getpwnam(user).pw_gid
            os.chown(filename, uid, gid)
        except Exception, e:
            sys.stderr.write("Cannot chown %s to %s: %s\n" % (filename, user, e))
            pass


def instantiate_skel(path):
    try:
        t = file(path).read()
        replacements = {
            "###SITE###": g_sitename,
            "###ROOT###": g_sitedir,
        }
        return replace_tags(t, replacements)
    except:
        return ""  # e.g. due to permission error


# Walks all files in the skeleton dir to execute a function for each file
# The given handler is called with the provided args. Additionally the relative
# path of the file to handle is handed over in the 'relpath' parameter.

# When called with a path in 'exclude_if_in' then paths existing relative to
# that are skipped. This is used for a second run during the update-process: to handle
# files that have vanished in the new version.

# The option 'relbase' is optional. It can contain a relative path which can be used
# as base for the walk instead of walking the whole tree.


# The function returns a set of already handled files.
def walk_skel(root, handler, args, depth_first, exclude_if_in=None, relbase='.'):
    os.chdir(root)

    # Note: os.walk first finds level 1 directories, then deeper
    # layers. If we need a real depth search instead, where we first
    # handle deep directories and files, then the top level ones.
    walk_entries = list(os.walk(relbase))
    if depth_first:
        walk_entries.reverse()

    for dirpath, dirnames, filenames in walk_entries:
        if dirpath.startswith("./"):
            dirpath = dirpath[2:]
        if dirpath.startswith("tmp"):
            continue

        # In depth first search first handle files, then directories
        if depth_first:
            entries = filenames + dirnames
        else:
            entries = dirnames + filenames
        for entry in entries:
            path = dirpath + "/" + entry
            if path.startswith("./"):
                path = path[2:]

            if exclude_if_in and os.path.exists(exclude_if_in + "/" + path):
                continue

            todo = True
            while todo:
                try:
                    handler(path, *args)
                    todo = False
                except Exception, e:
                    todo = False
                    sys.stderr.write(error * 40 + "\n")
                    sys.stderr.write(error + " Exception      %s\n" % (path))
                    sys.stderr.write(error + " " +
                                     traceback.format_exc().replace('\n', "\n" + error + " ") +
                                     "\n")
                    sys.stderr.write(error * 40 + "\n")

                    # If running in interactive mode ask the user to terminate or retry
                    # In case of non interactive mode just throw the exception
                    if opt_conflict == 'ask':
                        options = [("retry", "Retry the operation"),
                                   ("continue", "Continue with next files"),
                                   ("abort", "Stop here and abort update!")]
                        choice = ask_user_choices(
                            'Problem occured',
                            'We detected an exception (printed above). You have the '
                            'chance to fix things and retry the operation now.', options)
                        if choice == 'abort':
                            bail_out("Update aborted.")
                        elif choice == 'retry':
                            todo = True  # Try again


#.
#   .--omd update----------------------------------------------------------.
#   |                           _                   _       _              |
#   |        ___  _ __ ___   __| |  _   _ _ __   __| | __ _| |_ ___        |
#   |       / _ \| '_ ` _ \ / _` | | | | | '_ \ / _` |/ _` | __/ _ \       |
#   |      | (_) | | | | | | (_| | | |_| | |_) | (_| | (_| | ||  __/       |
#   |       \___/|_| |_| |_|\__,_|  \__,_| .__/ \__,_|\__,_|\__\___|       |
#   |                                    |_|                               |
#   +----------------------------------------------------------------------+
#   |  Complex handling of skeleton and user files during update           |
#   '----------------------------------------------------------------------'


# Change site specific information in files originally create from
# skeleton files. Skip files below tmp/
def patch_skeleton_files(old, new):
    skelroot = "/omd/versions/%s/skel" % OMD_VERSION
    os.chdir(skelroot)  # make relative paths
    for dirpath, dirnames, filenames in os.walk("."):
        if dirpath.startswith("./"):
            dirpath = dirpath[2:]
        targetdir = site_dir(new) + "/" + dirpath
        if targetdir.startswith(tmp_dir(new)):
            continue  # Skip files below tmp
        for fn in filenames:
            src = dirpath + "/" + fn
            dst = targetdir + "/" + fn
            if os.path.isfile(src) and not os.path.islink(src) \
                and os.path.exists(dst): # not deleted by user
                try:
                    patch_template_file(src, dst, old, new)
                except Exception, e:
                    sys.stderr.write("Error patching template file '%s': %s\n" % (dst, e))


def patch_template_file(src, dst, old, new):
    # Create patch from old instantiated skeleton file to new one
    content = file(src).read()
    for site in [old, new]:
        replacements = {
            "###SITE###": site,
            "###ROOT###": site_dir(site),
        }
        filename = "%s.skel.%s" % (dst, site)
        file(filename, "w").write(replace_tags(content, replacements))
        try_chown(filename, new)

    # If old and new skeleton file are identical, then do nothing
    old_orig_path = "%s.skel.%s" % (dst, old)
    new_orig_path = "%s.skel.%s" % (dst, new)
    if file(old_orig_path).read() == file(new_orig_path).read():
        os.remove(old_orig_path)
        os.remove(new_orig_path)
        return

    # Now create a patch from old to new and immediately apply on
    # existing - possibly user modified - file.

    result = os.system(  # nosec
        "diff -u %s %s | %s/bin/patch --force --backup --forward --silent %s" %
        (old_orig_path, new_orig_path, site_dir(new), dst))
    try_chown(dst, new)
    try_chown(dst + ".rej", new)
    try_chown(dst + ".orig", new)
    if result == 0:
        sys.stdout.write(good + " Converted      %s\n" % src)
    else:
        # Make conflict resolution interactive - similar to omd update
        options = [
            ("diff", "Show conversion patch, that I've tried to apply"),
            ("you", "Show your changes compared with the original default version"),
            ("edit", "Edit half-converted file (watch out for >>>> and <<<<)"),
            ("try again", "Edit your original file and try again"),
            ("keep", "Keep half-converted version of the file"),
            ("restore", "Restore your original version of the file"),
            ("install", "Install the default version of the file"),
            ("brute", "Simply replace /%s/ with /%s/ in that file" % (old, new)),
            ("shell", "Open a shell for looking around"),
            ("abort", "Stop here and abort!"),
        ]

        while True:
            if opt_conflict in ["abort", "install"]:
                choice = opt_conflict
            elif opt_conflict == "keepold":
                choice = "restore"
            else:
                choice = ask_user_choices(
                    "Conflicts in " + src + "!",
                    "I've tried to merge your changes with the renaming of %s into %s.\n"
                    "Unfortunately there are conflicts with your changes. \n"
                    "You have the following options: " % (old, new), options)

            if choice == "abort":
                bail_out("Renaming aborted.")
            elif choice == "keep":
                break
            elif choice == "edit":
                os.system("%s '%s'" % (get_editor(), dst))  # nosec
            elif choice == "diff":
                os.system("diff -u %s %s%s" % (old_orig_path, new_orig_path, pipe_pager()))  # nosec
            elif choice == "brute":
                os.system("sed 's@/%s/@/%s/@g' %s.orig > %s" % (old, new, dst, dst))  # nosec
                changed = len([
                    l for l in os.popen("diff %s.orig %s" % (dst, dst)).readlines()  # nosec
                    if l.startswith(">")
                ])
                if changed == 0:
                    sys.stdout.write("Found no matching line.\n")
                else:
                    sys.stdout.write("Did brute-force replace, changed %s%d%s lines:\n" %
                                     (tty_bold, changed, tty_normal))
                    os.system("diff -u %s.orig %s" % (dst, dst))  # nosec
                    break
            elif choice == "you":
                os.system(  # nosec
                    "pwd ; diff -u %s %s.orig%s" % (old_orig_path, dst, pipe_pager()))
            elif choice == "restore":
                os.rename(dst + ".orig", dst)
                sys.stdout.write("Restored your version.\n")
                break
            elif choice == "install":
                os.rename(new_orig_path, dst)
                sys.stdout.write("Installed default file (with site name %s).\n" % new)
                break
            elif choice == "shell":
                relname = src.split("/")[-1]
                sys.stdout.write(" %-35s the half-converted file\n" % (relname,))
                sys.stdout.write(" %-35s your original version\n" % (relname + ".orig"))
                sys.stdout.write(" %-35s the failed parts of the patch\n" % (relname + ".rej"))
                sys.stdout.write(" %-35s default version with the old site name\n" %
                                 (relname + ".skel.%s" % old))
                sys.stdout.write(" %-35s default version with the new site name\n" %
                                 (relname + ".skel.%s" % new))

                sys.stdout.write("\n Starting BASH. Type CTRL-D to continue.\n\n")
                thedir = "/".join(dst.split("/")[:-1])
                os.system("su - %s -c 'cd %s ; bash -i'" % (new, thedir))  # nosec

    # remove unnecessary files
    try:
        os.remove(dst + ".skel." + old)
        os.remove(dst + ".skel." + new)
        os.remove(dst + ".orig")
        os.remove(dst + ".rej")
    except:
        pass


# Try to merge changes from old->new version and
# old->user version
def merge_update_file(relpath, old_version, new_version):
    fn = tty_bold + relpath + tty_normal

    replacements = {
        "###SITE###": g_sitename,
        "###ROOT###": g_sitedir,
    }
    user_path = g_sitedir + "/" + relpath
    content = file(user_path).read()
    permissions = os.stat(user_path).st_mode

    def try_merge():
        for version in [old_version, new_version]:
            p = "/omd/versions/%s/skel/%s" % (version, relpath)
            while True:
                try:
                    skel_content = file(p).read()
                    break
                except:
                    # Do not ask the user in non-interactive mode.
                    if opt_conflict in ["abort", "install"]:
                        bail_out("Skeleton file '%s' of version %s not readable." % (p, version))
                    elif opt_conflict == "keepold" or not user_confirms(
                            "Skeleton file of version %s not readable" % version,
                            "The file '%s' is not readable for the site user. "
                            "This is most probably due a bug in release 0.42. "
                            "You can either fix that problem by making the file "
                            "readable with doing as root: chmod +r '%s' "
                            "or assume the file as empty. In that case you might "
                            "damage your configuration file "
                            "in case you have made changes to it in your site. What shall we do?" %
                        (p, p), relpath, "retry", "Retry reading the file (after you've fixed it)",
                            "ignore", "Assume the file to be empty"):
                        skel_content = ""
                        break
            file("%s-%s" % (user_path, version), "w").write(
                replace_tags(skel_content, replacements))
        version_patch = os.popen("diff -u %s-%s %s-%s" % (  # nosec
            user_path, old_version, user_path, new_version)).read()

        # First try to merge the changes in the version into the users' file
        merge = patch_has_merge() and "--merge" or ""
        f = os.popen(  # nosec
            "PATH=/omd/versions/default/bin:$PATH patch --force --backup --forward --silent %s %s >/dev/null"
            % (merge, user_path), "w")
        f.write(version_patch)
        status = f.close()
        if status:
            return status / 256
        else:
            return 0

    if try_merge() == 0:
        # ACHTUNG: Hier müssen die Dateien $DATEI-alt, $DATEI-neu und $DATEI.orig
        # gelöscht werden
        sys.stdout.write(good + " Merged         %s\n" % fn)
        return

    # No success. Should we try merging the users' changes onto the new file?
    # user_patch = os.popen(
    merge_message = patch_has_merge() and ' (watch out for >>>>> and <<<<<)' or ''
    editor = get_editor()
    reject_file = user_path + ".rej"

    options = [("diff", "Show differences between the new default and your version"),
               ("you", "Show your changes compared with the old default version"),
               ("new", "Show what has changed from %s to %s" % (old_version, new_version))]
    if os.path.exists(reject_file):  # missing if patch has --merge
        options.append(("missing", "Show which changes from the update have not been merged"))
    options += [
        ("edit", "Edit half-merged file%s" % merge_message),
        ("try again", "Edit your original file and try again"),
        ("keep", "Keep half-merged version of the file"),
        ("restore", "Restore your original version of the file"),
        ("install", "Install the new default version"),
        ("shell", "Open a shell for looking around"),
        ("abort", "Stop here and abort update!"),
    ]

    while True:
        if opt_conflict in ["install", "abort"]:
            choice = opt_conflict
        elif opt_conflict == "keepold":
            choice = "restore"
        else:
            choice = ask_user_choices(
                "Conflicts in " + relpath + "!",
                "I've tried to merge the changes from version %s to %s into %s.\n"
                "Unfortunately there are conflicts with your changes. \n"
                "You have the following options: " % (old_version, new_version, relpath), options)

        if choice == "abort":
            bail_out("Update aborted.")
        elif choice == "keep":
            break
        elif choice == "edit":
            os.system("%s '%s'" % (editor, user_path))  # nosec
        elif choice == "diff":
            os.system("diff -u %s.orig %s-%s%s" % (  # nosec
                user_path, user_path, new_version, pipe_pager()))
        elif choice == "you":
            os.system("diff -u %s-%s %s.orig%s" % (  # nosec
                user_path, old_version, user_path, pipe_pager()))
        elif choice == "new":
            os.system("diff -u %s-%s %s-%s%s" % (  # nosec
                user_path, old_version, user_path, new_version, pipe_pager()))
        elif choice == "missing":
            if os.path.exists(reject_file):
                sys.stdout.write(tty_bgblue + tty_white + file(reject_file).read() + tty_normal)
            else:
                sys.stdout.write("File %s not found.\n" % reject_file)

        elif choice == "shell":
            relname = relpath.split("/")[-1]
            sys.stdout.write(" %-25s: the current half-merged file\n" % relname)
            sys.stdout.write(
                " %-25s: the default version of %s\n" % (relname + "." + old_version, old_version))
            sys.stdout.write(
                " %-25s: the default version of %s\n" % (relname + "." + new_version, new_version))
            sys.stdout.write(" %-25s: your original version\n" % (relname + ".orig"))
            if os.path.exists(reject_file):
                sys.stdout.write(" %-25s: changes that haven't been merged\n" % relname + ".rej")

            sys.stdout.write("\n Starting BASH. Type CTRL-D to continue.\n\n")
            thedir = "/".join(user_path.split("/")[:-1])
            os.system("cd '%s' ; bash -i" % thedir)  # nosec
        elif choice == "restore":
            os.rename(user_path + ".orig", user_path)
            os.chmod(user_path, permissions)
            sys.stdout.write("Restored your version.\n")
            break
        elif choice == "try again":
            os.rename(user_path + ".orig", user_path)
            os.system("%s '%s'" % (editor, user_path))  # nosec
            if 0 == try_merge():
                sys.stdout.write("Successfully merged changes from %s -> %s into %s\n" %
                                 (old_version, new_version, fn))
                return
            else:
                sys.stdout.write(" Merge failed again.\n")

        else:  # install
            os.rename("%s-%s" % (user_path, new_version), user_path)
            os.chmod(user_path, permissions)
            sys.stdout.write("Installed default file of version %s.\n" % new_version)
            break

    # Clean up temporary files
    for p in [
            "%s-%s" % (user_path, old_version),
            "%s-%s" % (user_path, new_version),
            "%s.orig" % user_path,
            "%s.rej" % user_path
    ]:
        try:
            os.remove(p)
        except:
            pass


# Compares two files and returns infos wether the file type or contants have changed """
def file_status(source_path, target_path):
    source_type = filetype(source_path)
    target_type = filetype(target_path)

    if source_type == "file":
        source_content = file_contents(source_path)

    if target_type == "file":
        target_content = file_contents(target_path)

    changed_type = source_type != target_type
    # FIXME: Was ist, wenn aus einer Datei ein Link gemacht wurde? Oder umgekehrt?
    changed_content = (source_type == "file" \
                       and target_type == "file" \
                       and source_content != target_content) or \
                      (source_type == "link" \
                       and target_type == "link" \
                       and os.readlink(source_path) != os.readlink(target_path))
    changed = changed_type or changed_content

    return (changed_type, changed_content, changed)


def update_file(relpath, old_version, new_version, userdir, old_perms):
    old_skel = "/omd/versions/%s/skel" % old_version
    new_skel = "/omd/versions/%s/skel" % new_version

    replacements = {
        "###SITE###": g_sitename,
        "###ROOT###": g_sitedir,
    }

    old_path = old_skel + "/" + relpath
    new_path = new_skel + "/" + relpath
    user_path = userdir + "/" + relpath

    old_type = filetype(old_path)
    new_type = filetype(new_path)
    user_type = filetype(user_path)

    # compare our new version with the user's version
    type_differs, content_differs, differs = file_status(user_path, new_path)

    # compare our old version with the user's version
    user_changed_type, user_changed_content, user_changed = file_status(old_path, user_path)

    # compare our old with our new version
    we_changed_type, we_changed_content, we_changed = file_status(old_path, new_path)

    non_empty_directory = not os.path.islink(user_path) and os.path.isdir(user_path) and bool(
        os.listdir(user_path))

    #     if opt_verbose:
    #         sys.stdout.write("%s%s%s:\n" % (tty_bold, relpath, tty_normal))
    #         sys.stdout.write("  you       : %s\n" % user_type)
    #         sys.stdout.write("  %-10s: %s\n" % (old_version, old_type))
    #         sys.stdout.write("  %-10s: %s\n" % (new_version, new_type))

    # A --> MISSING FILES

    # Handle cases with missing files first. At least old or new are present,
    # or this function would never have been invoked.
    fn = tty_bold + tty_bgblue + tty_white + relpath + tty_normal
    fn = tty_bold + relpath + tty_normal

    # 1) New version ships new skeleton file -> simply install
    if not old_type and not user_type:
        create_skeleton_file(new_skel, userdir, relpath, replacements)
        sys.stdout.write(good + " Installed %-4s %s\n" % (new_type, fn))

    # 2) new version ships new skeleton file, but user's own file/dir/link
    #    is in the way.
    # 2a) the users file is identical with our new version
    elif not old_type and not differs:
        sys.stdout.write(good + " Identical new  %s\n" % fn)

    # 2b) user's file has a different content or type
    elif not old_type:
        if user_confirms(
                "Conflict at " + relpath, "The new version ships the %s %s, "
                "but you have created a %s in that place "
                "yourself. Shall we keep your %s or replace "
                "is with my %s?" % (new_type, relpath, user_type, user_type, new_type), relpath,
                "keep", "Keep your %s" % user_type, "replace",
                "Replace your %s with the new default %s" % (user_type, new_type)):
            sys.stdout.write(warn + " Keeping your   %s\n" % fn)
        else:
            create_skeleton_file(new_skel, userdir, relpath, replacements)
            sys.stdout.write(good + " Installed %-4s %s\n" % (new_type, fn))

    # 3) old version had a file which has vanished in new (got obsolete). If the user
    #    has deleted it himself, we are just happy
    elif not new_type and not user_type:
        sys.stdout.write(good + " Obsolete       %s\n" % fn)

    # 3b) same, but user has not deleted and changed type
    elif not new_type and user_changed_type:
        if user_confirms(
                "Obsolete file " + relpath, "The %s %s has become obsolete in "
                "this version, but you have changed it into a "
                "%s. Do you want to keep your %s or "
                "may I delete it for you, please?" % (old_type, relpath, user_type, user_type),
                relpath, "keep", "Keep your %s" % user_type, "delete", "Delete it"):
            sys.stdout.write(warn + " Keeping your   %s\n" % fn)
        else:
            delete_user_file(user_path)
            sys.stdout.write(warn + " Deleted        %s\n" % fn)

    # 3c) same, but user has changed it contents
    elif not new_type and user_changed_content:
        if user_confirms(
                "Changes in obsolete %s %s" % (old_type, relpath),
                "The %s %s has become obsolete in "
                "the new version, but you have changed its contents. "
                "Do you want to keep your %s or "
                "may I delete it for you, please?" % (old_type, relpath, user_type), relpath,
                "keep", "keep your %s, though it is obsolete" % user_type, "delete",
                "delete your %s" % user_type):
            sys.stdout.write(warn + " Keeping your   %s\n" % fn)
        else:
            delete_user_file(user_path)
            sys.stdout.write(warn + " Deleted        %s\n" % fn)

    # 3d) same, but it is a directory which is not empty
    elif not new_type and non_empty_directory:
        if user_confirms(
                "Non empty obsolete directory %s" % (relpath),
                "The directory %s has become obsolete in "
                "the new version, but you have contents in it. "
                "Do you want to keep your directory or "
                "may I delete it for you, please?" % (relpath), relpath, "keep",
                "keep your directory, though it is obsolete", "delete", "delete your directory"):
            sys.stdout.write(warn + " Keeping your   %s\n" % fn)
        else:
            delete_user_file(user_path)
            sys.stdout.write(warn + " Deleted        %s\n" % fn)

    # 3e) same, but user hasn't changed anything -> silently delete
    elif not new_type:
        delete_user_file(user_path)
        sys.stdout.write(good + " Vanished       %s\n" % fn)

    # 4) old and new exist, but user file not. User has deleted that
    #    file. We simply do nothing in that case. The user surely has
    #    a good reason why he deleted the file.
    elif not user_type and not we_changed:
        sys.stdout.write(good + " Unwanted       %s (unchanged, deleted by you)\n" % fn)

    # 4b) File changed in new version. Simply warn if user has deleted it.
    elif not user_type:
        sys.stdout.write(warn + " Missing        %s\n" % fn)

    # B ---> UNCHANGED, EASY CASES

    # 5) New version didn't change anything -> no need to update
    elif not we_changed:
        pass

    # 6) User didn't change anything -> take over new version
    elif not user_changed:
        create_skeleton_file(new_skel, userdir, relpath, replacements)
        sys.stdout.write(good + " Updated        %s\n" % fn)

    # 7) User changed, but accidentally exactly as we did -> no action neccessary
    elif not differs:
        sys.stdout.write(good + " Identical      %s\n" % fn)

    # TEST UNTIL HERE

    # C ---> PATCH DAY, HANDLE FILES
    # 7) old, new and user are files. And all are different
    elif old_type == "file" and new_type == "file" and user_type == "file":
        try:
            merge_update_file(relpath, old_version, new_version)
        except KeyboardInterrupt:
            raise
        except Exception, e:
            sys.stdout.write(error + " Cannot merge: %s\n" % e)

    # D ---> SYMLINKS
    # 8) all are symlinks, all changed
    elif old_type == "link" and new_type == "link" and user_type == "link":
        if user_confirms(
                "Symbolic link conflict at " + relpath, "'%s' is a symlink that pointed to "
                "%s in the old version and to "
                "%s in the new version. But meanwhile you "
                "changed to link target to %s. "
                "Shall I keep your link or replace it with "
                "the new default target?" % (relpath, os.readlink(old_path), os.readlink(new_path),
                                             os.readlink(user_path)), relpath, "keep",
                "Keep your symbolic link pointing to %s" % os.readlink(user_path), "replace",
                "Change link target to %s" % os.readlink(new_path)):
            sys.stdout.write(warn + " Keeping your   %s\n" % fn)
        else:
            os.remove(user_path)
            os.symlink(os.readlink(new_path), user_path)
            sys.stdout.write(warn +
                             " Set link       %s to new target %s\n" % (fn, os.readlink(new_path)))

    # E ---> FILE TYPE HAS CHANGED (NASTY)

    # Now we have to handle cases, where the file types of the three
    # versions are not identical and at the same type the user or
    # have changed the third file to. We cannot merge here, the user
    # has to decide wether to keep his version of use ours.

    # 9) We have changed the file type
    elif old_type != new_type:
        if user_confirms(
                "File type change at " + relpath, "The %s %s has been changed into a %s in "
                "the new version. Meanwhile you have changed "
                "the %s of your copy of that %s. "
                "Do you want to keep your version or replace "
                "it with the new default? " % (old_type, relpath, new_type,
                                               user_changed_type and "type" or "content", old_type),
                relpath, "keep", "Keep your %s" % user_type, "replace",
                "Replace it with the new %s" % new_type):
            sys.stdout.write(warn + " Keeping your version of %s\n" % fn)
        else:
            create_skeleton_file(new_skel, userdir, relpath, replacements)
            sys.stdout.write(warn + " Replaced your %s %s by new default %s.\n" %
                             (user_type, relpath, new_type))

    # 10) The user has changed the file type, we just the content
    elif old_type != user_type:
        if user_confirms(
                "Type change conflicts with content change at " + relpath,
                "Usually %s is a %s in both the "
                "old and new version. But you have changed it "
                "into a %s. Do you want to keep that or may "
                "I replace your %s with the new default "
                "%s, please?" % (relpath, old_type, user_type, user_type, new_type), relpath,
                "keep", "Keep your %s" % user_type, "replace",
                "Replace it with the new %s" % new_type):
            sys.stdout.write(warn + " Keeping your %s %s.\n" % (user_type, fn))
        else:
            create_skeleton_file(new_skel, userdir, relpath, replacements)
            sys.stdout.write(warn + " Delete your %s and created new default %s %s.\n" %
                             (user_type, new_type, fn))

    # 11) This case should never happen, if I've not lost something
    else:
        if user_confirms(
                "Something nasty happened at " + relpath, "You somehow fiddled along with "
                "%s, and I do not have the "
                "slightest idea what's going on here. May "
                "I please install the new default %s "
                "here, or do you want to keep your %s?" % (relpath, new_type, user_type), relpath,
                "keep", "Keep your %s" % user_type, "replace",
                "Replace it with the new %s" % new_type):
            sys.stdout.write(warn + " Keeping your %s %s.\n" % (user_type, fn))
        else:
            create_skeleton_file(new_skel, userdir, relpath, replacements)
            sys.stdout.write(warn + " Delete your %s and created new default %s %s.\n" %
                             (user_type, new_type, fn))

    # Now the new file/link/dir is in place, deleted or whatever. The
    # user might have interferred and changed things. We need to make sure
    # that file permissions are also updated. But the user might have changed
    # something himself.

    user_type = filetype(user_path)
    old_perm = get_skel_permissions(old_version, old_perms, relpath)
    new_perm = get_skel_permissions(new_version, g_skel_permissions, relpath)
    user_perm = get_file_permissions(user_path)

    # Fix permissions not for links and only if the new type is as expected
    # and the current permissions are not as the should be
    what = None
    if new_type != "link" \
        and user_type == new_type \
        and user_perm != new_perm:

        # Permissions have changed, but file type not
        if old_type == new_type \
            and user_perm != old_perm \
            and old_perm != new_perm:
            if user_confirms(
                    "Permission conflict at " + relpath,
                    "The proposed permissions of %s have changed from %04o "
                    "to %04o in the new version, but you have set %04o. "
                    "May I use the new default permissions or do "
                    "you want to keep yours?" % (relpath, old_perm, new_perm, user_perm), relpath,
                    "keep", "Keep permissions at %04o" % user_perm, "default",
                    "Set permission to %04o" % new_perm):
                what = "keep"
            else:
                what = "default"

        # Permissions have changed, no conflict with user
        elif old_type == new_type \
            and user_perm == old_perm:
            what = "default"

        # Permissions are not correct: all other cases (where type is as expected)
        elif old_perm != new_perm:
            if user_confirms(
                    "Wrong permission of " + relpath,
                    "The proposed permissions of %s are %04o, but currently are "
                    "%04o. May I use the new default "
                    "permissions or keep yours?" % (relpath, new_perm, user_perm), relpath, "keep",
                    "Keep permissions at %04o" % user_perm, "default",
                    "Set permission to %04o" % new_perm):
                what = "keep"
            else:
                what = "default"

        if what == "keep":
            sys.stdout.write(warn + " Permissions    %04o %s (unchanged)\n" % (user_perm, fn))
        elif what == "default":
            try:
                os.chmod(user_path, new_perm)
                sys.stdout.write(good +
                                 " Permissions    %04o -> %04o %s\n" % (user_perm, new_perm, fn))
            except Exception, e:
                sys.stdout.write(error + " Permission:    cannot change %04o -> %04o %s: %s\n" %
                                 (user_perm, new_perm, fn, e))


def filetype(p):
    # check for symlinks first. Might be dangling. In that
    # case os.path.exists checks the links target for existance
    # and reports it is non-existing.
    if os.path.islink(p):
        tp = "link"
    elif not os.path.exists(p):
        tp = None
    elif os.path.isdir(p):
        tp = "dir"
    else:
        tp = "file"

    return tp


# Returns the file contents of a site file or a skel file
def file_contents(path):
    if '/skel/' in path:
        return instantiate_skel(path)
    else:
        return file(path).read()


#.
#   .--tmpfs---------------------------------------------------------------.
#   |                     _                    __                          |
#   |                    | |_ _ __ ___  _ __  / _|___                      |
#   |                    | __| '_ ` _ \| '_ \| |_/ __|                     |
#   |                    | |_| | | | | | |_) |  _\__ \                     |
#   |                     \__|_| |_| |_| .__/|_| |___/                     |
#   |                                  |_|                                 |
#   +----------------------------------------------------------------------+
#   |  Helper functions for dealing with the tmpfs                         |
#   '----------------------------------------------------------------------'


def tmpfs_mounted(sitename):
    # Problem here: if /omd is a symbolic link somewhere else,
    # then in /proc/mounts the physical path will appear and be
    # different from tmp_path. We just check the suffix therefore.
    path_suffix = "sites/%s/tmp" % sitename
    for line in file("/proc/mounts"):
        try:
            device, mp, fstype, options, dump, fsck = line.split()
            if mp.endswith(path_suffix) and fstype == 'tmpfs':
                return True
        except:
            continue
    return False


def prepare_and_populate_tmpfs(sitename):
    tmp = tmp_dir(sitename)
    prepare_tmpfs(sitename, tmp)

    if not os.listdir(tmp):
        create_skeleton_files(sitename, "tmp")
        chown_tree(tmp, sitename)

    ok()


def prepare_tmpfs(sitename, tmp):
    if tmpfs_mounted(sitename):
        return  # Fine: Mounted

    if g_site_conf["TMPFS"] != "on":
        sys.stdout.write("Preparing tmp directory %s..." % tmp)
        sys.stdout.flush()

        if os.path.exists(tmp):
            return

        try:
            os.mkdir(tmp)
        except OSError, e:
            if e.errno != errno.EEXIST:  # File exists
                raise
        return

    sys.stdout.write("Creating temporary filesystem %s..." % tmp)
    sys.stdout.flush()
    if not os.path.exists(tmp):
        os.mkdir(tmp)

    mount_options = shlex.split(g_info["MOUNT_OPTIONS"])
    p = subprocess.Popen(
        ["mount"] + mount_options + [tmp],
        shell=False,
        stdin=open(os.devnull),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT)
    exit_code = p.wait()
    if exit_code == 0:
        return  # Fine: Mounted

    sys.stdout.write(p.stdout.read())
    if is_dockerized():
        sys.stdout.write(tty_warn + ": "
                         "Could not mount tmpfs. You may either start the container in "
                         "privileged mode or use the \"docker run\" option \"--tmpfs\" to "
                         "make docker do the tmpfs mount for the site.\n")

    sys.stdout.write(tty_warn + ": You may continue without tmpfs, but the "
                     "performance of Check_MK may be degraded.\n")


def is_dockerized():
    return os.path.exists("/.dockerenv")


def unmount_tmpfs(sitename, output=True, kill=False):
    # Clear directory hierarchy when not using a tmpfs
    # During omd update TMPFS hook might not be set so assume
    # that the hook is enabled by default.
    # If kill is True, then we do an fuser -k on the tmp
    # directory first.
    if not tmpfs_mounted(sitename):
        tmp = tmp_dir(sitename)
        if os.path.exists(tmp):
            if output:
                sys.stdout.write("Cleaning up temp filesystem...")
                sys.stdout.flush()
            delete_directory_contents(tmp)
            if output:
                ok()
        return True

    else:
        if output:
            sys.stdout.write("Unmounting temporary filesystem...")

        for t in range(0, 10):
            if 0 == os.system("umount '%s'" % tmp_dir(sitename)):  # nosec
                if output:
                    ok()
                return True

            if kill:
                if output:
                    sys.stdout.write("Killing processes still using '%s'\n" % tmp_dir(sitename))
                os.system("fuser --silent -k '%s'" % tmp_dir(sitename))  # nosec

            if output:
                sys.stdout.write(kill and "K" or ".")
                sys.stdout.flush()
            time.sleep(1)

        if output:
            bail_out(tty_error + ": Cannot unmount tmp filesystem.")
        else:
            return False

    return True


def add_to_fstab(sitename, tmpfs_size=None):
    if not os.path.exists("/etc/fstab"):
        return  # Don't do anything in case there is no fstab

    # tmpfs                   /opt/omd/sites/b01/tmp  tmpfs   user,uid=b01,gid=b01 0 0
    mountpoint = "/opt" + tmp_dir(sitename)
    sys.stdout.write("Adding %s to /etc/fstab.\n" % mountpoint)

    # No size option: using up to 50% of the RAM
    sizespec = ''
    if tmpfs_size != None and re.match('^[0-9]+(G|M|%)$', tmpfs_size):
        sizespec = ',size=%s' % tmpfs_size

    # Ensure the fstab has a newline char at it's end before appending
    previous_fstab = file("/etc/fstab").read()
    complete_last_line = previous_fstab and not previous_fstab.endswith("\n")

    with file("/etc/fstab", "a+") as fstab:
        if complete_last_line:
            fstab.write("\n")

        fstab.write("tmpfs  %s tmpfs noauto,user,mode=755,uid=%s,gid=%s%s 0 0\n" % \
        (mountpoint, sitename, sitename, sizespec))


def remove_from_fstab(sitename):
    if not os.path.exists("/etc/fstab"):
        return  # Don't do anything in case there is no fstab

    mountpoint = tmp_dir(sitename)
    sys.stdout.write("Removing %s from /etc/fstab..." % mountpoint)
    newtab = file("/etc/fstab.new", "w")
    for line in file("/etc/fstab"):
        if "uid=%s," % sitename in line and mountpoint in line:
            continue
        newtab.write(line)
    os.rename("/etc/fstab.new", "/etc/fstab")
    ok()


#.
#   .--init.d--------------------------------------------------------------.
#   |                        _       _ _        _                          |
#   |                       (_)_ __ (_) |_   __| |                         |
#   |                       | | '_ \| | __| / _` |                         |
#   |                       | | | | | | |_ | (_| |                         |
#   |                       |_|_| |_|_|\__(_)__,_|                         |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Handling of site-internal init scripts                              |
#   '----------------------------------------------------------------------'


def init_scripts(sitename):
    rc_dir = "/omd/sites/%s/etc/rc.d" % sitename
    try:
        scripts = os.listdir(rc_dir)
        scripts.sort()
        return rc_dir, scripts
    except:
        return rc_dir, []


def call_init_script(scriptpath, command):
    if not os.path.exists(scriptpath):
        sys.stderr.write('ERROR: This daemon does not exist.\n')
        return False

    try:
        return subprocess.call([scriptpath, command]) in [0, 5]
    except OSError, e:
        sys.stderr.write("ERROR: Failed to run '%s': %s\n" % (scriptpath, e))
        if e.errno == 13:  # [Errno 13] Permission denied
            return False


def call_init_scripts(sitename, command, daemon=None, exclude_daemons=None):
    # Restart: Do not restart each service after another,
    # but first do stop all, then start all again! This
    # preserves the order.
    if command == "restart":
        call_init_scripts(sitename, "stop", daemon)
        call_init_scripts(sitename, "start", daemon)
        return

    # OMD guarantees OMD_ROOT to be the current directory
    os.chdir(site_dir(sitename))

    if daemon:
        ok = call_init_script("%s/etc/init.d/%s" % (g_sitedir, daemon), command)

    else:
        # Call stop scripts in reverse order. If daemon is set,
        # then only that start script will be affected
        rc_dir, scripts = init_scripts(sitename)
        if command == "stop":
            scripts.reverse()
        ok = True

        for script in scripts:
            if exclude_daemons and script in exclude_daemons:
                continue

            if not call_init_script("%s/%s" % (rc_dir, script), command):
                ok = False

    if ok:
        return 0
    else:
        return 2


def check_status(sitename, display=True, daemon=None, bare=False):
    num_running = 0
    num_unused = 0
    num_stopped = 0
    rc_dir, scripts = init_scripts(sitename)
    components = [s.split('-', 1)[-1] for s in scripts]
    if daemon and daemon not in components:
        if not bare:
            sys.stderr.write('ERROR: This daemon does not exist.\n')
        return 3
    for script in scripts:
        komponent = script.split("/")[-1].split('-', 1)[-1]
        if daemon and komponent != daemon:
            continue

        state = os.system("%s/%s status >/dev/null 2>&1" % (rc_dir, script)) >> 8  # nosec

        if display and (state != 5 or opt_verbose):
            if bare:
                sys.stdout.write(komponent + " ")
            else:
                sys.stdout.write("%-16s" % (komponent + ":"))
                sys.stdout.write(tty_bold)

        if bare:
            if state != 5 or opt_verbose:
                sys.stdout.write("%d\n" % state)

        if state == 0:
            if display and not bare:
                sys.stdout.write(tty_green + "running\n")
            num_running += 1
        elif state == 5:
            if display and opt_verbose and not bare:
                sys.stdout.write(tty_blue + "unused\n")
            num_unused += 1
        else:
            if display and not bare:
                sys.stdout.write(tty_red + "stopped\n")
            num_stopped += 1
        if display and not bare:
            sys.stdout.write(tty_normal)

    if num_stopped > 0 and num_running == 0:
        exit_code = 1
        ovstate = tty_red + "stopped"
    elif num_running > 0 and num_stopped == 0:
        exit_code = 0
        ovstate = tty_green + "running"
    elif num_running == 0 and num_stopped == 0:
        exit_code = 0
        ovstate = tty_blue + "unused"
    else:
        exit_code = 2
        ovstate = tty_yellow + "partially running"
    if display:
        if bare:
            sys.stdout.write("OVERALL %d\n" % exit_code)
        else:
            sys.stdout.write("-----------------------\n")
            sys.stdout.write("Overall state:  %s\n" % (tty_bold + ovstate + tty_normal))
    return exit_code




#.
#   .--Config & Hooks------------------------------------------------------.
#   |  ____             __ _          ___     _   _             _          |
#   | / ___|___  _ __  / _(_) __ _   ( _ )   | | | | ___   ___ | | _____   |
#   || |   / _ \| '_ \| |_| |/ _` |  / _ \/\ | |_| |/ _ \ / _ \| |/ / __|  |
#   || |__| (_) | | | |  _| | (_| | | (_>  < |  _  | (_) | (_) |   <\__ \  |
#   | \____\___/|_| |_|_| |_|\__, |  \___/\/ |_| |_|\___/ \___/|_|\_\___/  |
#   |                        |___/                                         |
#   +----------------------------------------------------------------------+
#   |  Site configuration and config hooks                                 |
#   '----------------------------------------------------------------------'

# Hooks are scripts in lib/omd/hooks that are being called with one
# of the following arguments:
#
# default - return the default value of the hook. Mandatory
# set     - implements a new setting for the hook
# choices - available choices for enumeration hooks
# depends - exists with 1, if this hook misses its dependent hook settings


# Parse the file site.conf of a site into a dictionary. Does
# not use any global variables. Has no side effects.
def parse_site_conf(sitename):
    config = {}
    confpath = "/omd/sites/%s/etc/omd/site.conf" % sitename
    if not os.path.exists(confpath):
        return {}

    for line in file(confpath):
        line = line.strip()
        if line == "" or line[0] == "#":
            continue
        var, value = line.split("=", 1)
        if not var.startswith("CONFIG_"):
            sys.stderr.write("Ignoring invalid variable %s.\n" % var)
        else:
            config[var[7:].strip()] = value.strip().strip("'")

    return config


# Load all variables from omd/sites.conf. These variables always begin with
# CONFIG_. The reason is that this file can be sources with the shell. Puts
# these variables into the global dict g_site_conf without the CONFIG_. Also
# puts the variables into the environment.
def load_site_conf():
    global g_site_conf
    g_site_conf = parse_site_conf(g_sitename)

    # Get the default values of all config hooks that are not contained
    # in the site configuration. This can happen if there are new hooks
    # after an update or when a site is being created.
    hook_dir = g_sitedir + "/lib/omd/hooks"
    if os.path.exists(hook_dir):
        for hook_name in sort_hooks(os.listdir(hook_dir)):
            if hook_name[0] != '.' and hook_name not in g_site_conf:
                content = call_hook(hook_name, ["default"])[1]
                g_site_conf[hook_name] = content


# Put all site configuration (explicit and defaults) into environment
# variables beginning with CONFIG_
def create_config_environment():
    for varname, value in g_site_conf.items():
        putenv("CONFIG_" + varname, value)


# TODO: RENAME
def save_site_conf():
    confdir = g_sitedir + "/etc/omd"

    if not os.path.exists(confdir):
        os.mkdir(confdir)

    f = file(g_sitedir + "/etc/omd/site.conf", "w")

    for hook_name, value in sorted(g_site_conf.items(), key=lambda x: x[0]):
        f.write("CONFIG_%s='%s'\n" % (hook_name, value))


# Get information about all hooks. Just needed for
# the "omd config" command.
def load_config_hooks():
    global g_config_hooks
    g_config_hooks = {}

    hook_dir = g_sitedir + "/lib/omd/hooks"
    for hook_name in os.listdir(hook_dir):
        try:
            if hook_name[0] != '.':
                hook = config_load_hook(hook_name)
                # only load configuration hooks
                if hook.get("choices", None) != None:
                    g_config_hooks[hook_name] = hook
        except:
            pass
    load_hook_dependencies()


def config_load_hook(hook_name):
    hook = {
        "name": hook_name,
        "deprecated": False,
    }

    description = ""
    description_active = False
    for line in file(g_sitedir + "/lib/omd/hooks/" + hook_name):
        if line.startswith("# Alias:"):
            hook["alias"] = line[8:].strip()
        elif line.startswith("# Menu:"):
            hook["menu"] = line[7:].strip()
        elif line.startswith("# Deprecated: yes"):
            hook["deprecated"] = True
        elif line.startswith("# Description:"):
            description_active = True
        elif line.startswith("#  ") and description_active:
            description += line[3:].strip() + "\n"
        else:
            description_active = False
    hook["description"] = description

    def get_hook_info(info):
        return call_hook(hook_name, [info])[1]

    # The choices can either be a list of possible keys. Then
    # the hook outputs one live for each choice where the key and a
    # description are separated by a colon. Or it outputs one line
    # where that line is an extended regular expression matching the
    # possible values.
    choicestxt = get_hook_info("choices").split("\n")
    if len(choicestxt) == 1:
        regextext = choicestxt[0].strip()
        if regextext != "":
            choices = re.compile(regextext + "$")
        else:
            choices = None
    else:
        choices = []
        try:
            for line in choicestxt:
                val, descr = line.split(":", 1)
                val = val.strip()
                descr = descr.strip()
                choices.append((val, descr))
        except:
            bail_out("Invalid output of hook: %s" % choicestxt)

    hook["choices"] = choices
    return hook


def load_hook_dependencies():
    for hook_name in sort_hooks(g_config_hooks.keys()):
        hook = g_config_hooks[hook_name]
        exitcode, content = call_hook(hook_name, ["depends"])
        if exitcode:
            hook["active"] = False
        else:
            hook["active"] = True


# Always sort CORE hook to the end because it runs "cmk -U" which
# relies on files created by other hooks.
def sort_hooks(hook_names):
    return sorted(hook_names, key=lambda n: (n == "CORE", n))


def hook_exists(hook_name):
    hook_file = g_sitedir + "/lib/omd/hooks/" + hook_name
    return os.path.exists(hook_file)


def call_hook(hook_name, args):
    hook_file = g_sitedir + "/lib/omd/hooks/" + hook_name
    argsstring = " ".join(["'%s'" % arg for arg in args])
    command = hook_file + " " + argsstring
    if opt_verbose:
        sys.stdout.write("Calling hook: %s\n" % command)
    putenv("OMD_ROOT", g_sitedir)
    putenv("OMD_SITE", g_sitename)
    pipe = os.popen(command)  # nosec
    content = pipe.read().strip()
    exitcode = pipe.close()
    if exitcode and args[0] != "depends":
        sys.stderr.write("Error running %s: %s\n" % (command, content))
    return exitcode, content


def config_change():
    # Check whether or not site needs to be stopped. Stop and remember to start again later
    site_was_stopped = False
    if not site_is_stopped(g_sitename):
        site_was_stopped = True
        stop_site(g_sitename)

    try:
        settings = read_config_change_commands()

        if not settings:
            bail_out("You need to provide config change commands via stdin: KEY=value\n")

        validate_config_change_commands(settings)

        for key, value in settings:
            config_set_value(key, value, save=False)

        save_site_conf()
    finally:
        if site_was_stopped:
            start_site(g_sitename)


def read_config_change_commands():
    settings = []
    for l in sys.stdin:
        line = l.strip()
        if not line:
            continue

        try:
            key, value = line.split("=", 1)
            settings.append((key, value))
        except ValueError, e:
            bail_out("Invalid config change command: %r" % line)
    return settings


def validate_config_change_commands(settings):
    # Validate the provided commands
    for key, value in settings:
        hook = g_config_hooks.get(key)
        if not hook:
            bail_out("Invalid config option: %r" % key)

        # Check if value is valid. Choices are either a list of allowed
        # keys or a regular expression
        if type(hook["choices"]) == list:
            choices = [var for (var, descr) in hook["choices"]]
            if value not in choices:
                bail_out("Invalid value %r for %r. Allowed are: %s\n" % \
                        (value, key, ", ".join(choices)))
        else:
            if not hook["choices"].match(value):
                bail_out(
                    "Invalid value %r for %r. Does not match allowed pattern.\n" % (value, key))


def config_set(args):
    if len(args) != 2:
        sys.stderr.write("Please specify variable name and value\n")
        config_usage()
        return

    if not site_is_stopped(g_sitename):
        sys.stderr.write("Cannot change config variables while site is running.\n")
        return

    hook_name = args[0]
    value = args[1]
    hook = g_config_hooks.get(hook_name)
    if not hook:
        sys.stderr.write("No such variable '%s'\n" % hook_name)
        return

    # Check if value is valid. Choices are either a list of allowed
    # keys or a regular expression
    if type(hook["choices"]) == list:
        choices = [var for (var, descr) in hook["choices"]]
        if value not in choices:
            sys.stderr.write("Invalid value for '%s'. Allowed are: %s\n" % \
                    (value, ", ".join(choices)))
            return
    else:
        if not hook["choices"].match(value):
            sys.stderr.write("Invalid value for '%s'. Does not match allowed pattern.\n" % value)
            return

    config_set_value(hook_name, value)


def config_set_all():
    for hook_name in sort_hooks(g_site_conf.keys()):
        value = g_site_conf[hook_name]
        # Hooks might vanish after and up- or downdate
        if hook_exists(hook_name):
            exitcode, output = call_hook(hook_name, ["set", value])
            if not exitcode:
                if output and output != value:
                    g_site_conf[hook_name] = output
                    putenv("CONFIG_" + hook_name, output)


def config_set_value(hook_name, value, save=True):
    hook = g_config_hooks.get(hook_name)

    # TODO: Warum wird hier nicht call_hook() aufgerufen!!

    # Call hook with 'set'. If it outputs something, that will
    # be our new value (i.e. hook disagrees with the new setting!)
    commandline = "%s/lib/omd/hooks/%s set '%s'" % (g_sitedir, hook_name, value)
    if os.getuid() == 0:
        sys.stderr.write("I am root. This should never happen!\n")
        sys.exit(1)

        # commandline = 'su -p -l %s -c "%s"' % (g_sitename, commandline)
    answer = os.popen(commandline).read()  # nosec
    if len(answer) > 0:
        value = answer.strip()

    g_site_conf[hook_name] = value
    putenv("CONFIG_" + hook_name, value)

    if save:
        save_site_conf()


def config_usage():
    sys.stdout.write("""Usage of config command:

omd config               - interactive configuration menu
omd config show          - show current settings of all configuration variables
omd config show VAR      - show current setting of variable VAR
omd config set VAR VALUE - set VAR to VALUE
omd config change        - change multiple at once. Provide newline separated
                           KEY=value pairs via stdin. The site is restarted
                           automatically once in case it's currently runnig.
""")


def config_show(args):
    if len(args) == 0:
        hook_names = g_config_hooks.keys()
        hook_names.sort()
        for hook_name in hook_names:
            hook = g_config_hooks[hook_name]
            if hook["active"] and not hook["deprecated"]:
                sys.stdout.write("%s: %s\n" % (hook_name, g_site_conf[hook_name]))
    else:
        output = []
        for hook_name in args:
            hook = g_config_hooks.get(hook_name)
            if not hook:
                sys.stderr.write("No such variable %s\n" % hook_name)
            else:
                output.append(g_site_conf[hook_name])

        sys.stdout.write(" ".join(output))
        sys.stdout.write("\n")


def config_configure():
    hook_names = g_config_hooks.keys()
    hook_names.sort()
    current_hook_name = ""
    menu_open = False
    current_menu = "Basic"

    # force certain order in main menu
    menu_choices = ["Basic", "Web GUI", "Addons", "Distributed Monitoring"]

    while True:
        choices = []

        # Rebuild hook information (values possible changed)
        menu = {}
        for hook_name in hook_names:
            hook = g_config_hooks[hook_name]
            if hook["active"] and not hook["deprecated"]:
                mp = hook.get("menu", "Other")
                entries = menu.get(mp, [])
                entries.append((hook_name, g_site_conf[hook_name]))
                menu[mp] = entries
                if mp not in menu_choices:
                    menu_choices.append(mp)

        # Handle main menu
        if not menu_open:
            change, current_menu = \
                dialog_menu("Configuration of site %s" % g_sitename,
                        "Interactive setting of site configuration variables. You "
                        "can change values only while the site is stopped.",
                        [ (e, "") for e in menu_choices ],
                        current_menu,
                        "Enter",
                        "Exit")
            if not change:
                return
            current_hook_name = None
            menu_open = True

        else:
            change, current_hook_name = \
                dialog_menu(
                    current_menu,
                    "",
                    menu[current_menu],
                    current_hook_name,
                    "Change",
                    "Main menu")
            if change:
                try:
                    config_configure_hook(current_hook_name)
                except Exception, e:
                    bail_out("Error in hook %s: %s" % (current_hook_name, e))
            else:
                menu_open = False


def config_configure_hook(hook_name):
    if not site_is_stopped(g_sitename):
        if not dialog_yesno("You cannot change configuration value while the "
                            "site is running. Do you want me to stop the site now?"):
            return
        stop_site(g_sitename)
        dialog_message("The site has been stopped.")

    hook = g_config_hooks[hook_name]
    title = hook["alias"]
    descr = hook["description"].replace("\n\n", "\001").replace("\n", " ").replace("\001", "\n\n")
    value = g_site_conf[hook_name]
    choices = hook["choices"]
    if type(choices) == list:
        dialog_function = dialog_menu
    else:
        dialog_function = dialog_regex
    change, new_value = \
        dialog_function(title, descr, choices, value, "Change", "Cancel")
    if change:
        config_set_value(hook["name"], new_value)
        g_site_conf[hook_name] = new_value
        save_site_conf()
        load_hook_dependencies()


def init_action(command, args, options):
    if site_is_disabled(g_sitename):
        bail_out("This site is disabled.")

    if command in ["start", "restart"]:
        prepare_and_populate_tmpfs(g_sitename)

    if len(args) > 0:
        daemon = args[0]  # restrict to this daemon
    else:
        daemon = None

    # OMD guarantees that we are in OMD_ROOT
    os.chdir(g_sitedir)

    if command == "status":
        return check_status(g_sitename, True, daemon, "bare" in options)
    else:
        return call_init_scripts(g_sitename, command, daemon)


#.
#   .--Helpers-------------------------------------------------------------.
#   |                  _   _      _                                        |
#   |                 | | | | ___| |_ __   ___ _ __ ___                    |
#   |                 | |_| |/ _ \ | '_ \ / _ \ '__/ __|                   |
#   |                 |  _  |  __/ | |_) |  __/ |  \__ \                   |
#   |                 |_| |_|\___|_| .__/ \___|_|  |___/                   |
#   |                              |_|                                     |
#   +----------------------------------------------------------------------+
#   |  Various helper functions                                            |
#   '----------------------------------------------------------------------'


def omd_root():
    return "/omd/versions/" + OMD_VERSION


# Read distro- and version specific values
def read_info():
    global g_info
    g_info = {}
    info_dir = omd_root() + "/share/omd"
    for f in os.listdir(info_dir):
        if f.endswith(".info"):
            for line in file(info_dir + "/" + f):
                try:
                    line = line.strip()
                    # Skip comment and empty lines
                    if line.startswith('#') or line == '':
                        continue
                    # Remove everything after the first comment sign
                    if '#' in line:
                        line = line[:line.index('#')].strip()
                    var, value = line.split('=')
                    value = value.strip()
                    if var.endswith("+"):
                        var = var[:-1]  # remove +
                        g_info[var.strip()] += " " + value
                    else:
                        g_info[var.strip()] = value
                except Exception, e:
                    bail_out('Unable to parse line "%s" in file "%s"' % (line, info_dir + "/" + f))


def fstab_verify(name):
    """Ensure that there is an fstab entry for the tmpfs of the site.
    In case there is no fstab (seen in some containers) assume everything
    is OK without fstab entry."""

    if not os.path.exists("/etc/fstab"):
        return True

    mountpoint = tmp_dir(name)
    for line in file("/etc/fstab"):
        if "uid=%s," % name in line and mountpoint in line:
            return True
    bail_out(tty_error + ": fstab entry for %s does not exist" % mountpoint)


# No using os.putenv, os.getenv os.unsetenv directly because
# they seem not to work correctly in debian 503.
#
# Unsetting all vars with os.unsetenv and after that using os.getenv to read
# some vars did not bring the expected result that the environment was empty.
# The vars were still set.
#
# Same for os.putenv. Executing os.getenv right after os.putenv did not bring
# the expected result.
#
# Directly modifying os.environ seems to work.
def putenv(key, value):
    os.environ[key] = value


def getenv(key, default=None):
    if not key in os.environ:
        return default
    return os.environ[key]


def clear_environment():
    # first remove *all* current environment variables
    keep = ["TERM"]
    for key in os.environ.keys():
        if key not in keep:
            del os.environ[key]


def set_environment():
    putenv("OMD_SITE", g_sitename)
    putenv("OMD_ROOT", g_sitedir)
    putenv(
        "PATH",
        "%s/local/bin:%s/bin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin" % (g_sitedir, g_sitedir))
    putenv("USER", g_sitename)

    putenv("LD_LIBRARY_PATH", "%s/local/lib:%s/lib" % (g_sitedir, g_sitedir))
    putenv("HOME", g_sitedir)

    # allow user to define further environment variable in ~/etc/environment
    envfile = g_sitedir + "/etc/environment"
    if os.path.exists(envfile):
        lineno = 0
        for line in file(envfile):
            lineno += 1
            line = line.strip()
            if line == "" or line[0] == "#":
                continue  # allow empty lines and comments
            parts = line.split("=")
            if len(parts) != 2:
                bail_out("%s: syntax error in line %d" % (envfile, lineno))
            varname = parts[0]
            value = parts[1]
            if value.startswith('"'):
                value = value.strip('"')

            # Add the present environment when someone wants to append some
            if value.startswith("$%s:" % varname):
                before = getenv(varname, None)
                if before:
                    value = before + ":" + value.replace("$%s:" % varname, '')

            if value.startswith("'"):
                value = value.strip("'")
            putenv(varname, value)

    create_config_environment()


def hostname():
    try:
        return os.popen("hostname").read().strip()
    except:
        return "localhost"


def create_apache_hook(sitename):
    file("/omd/apache/%s.conf" % sitename, "w")\
        .write("Include %s/etc/apache/mode.conf\n" % site_dir(sitename))


def delete_apache_hook(sitename):
    hook_path = "/omd/apache/%s.conf" % sitename
    if not os.path.exists(hook_path):
        return
    try:
        os.remove(hook_path)
    except Exception, e:
        sys.stderr.write("Cannot remove apache hook %s: %s\n" % (hook_path, e))


def init_cmd(name, action):
    return g_info['INIT_CMD'] % {
        'name': name,
        'action': action,
    }


def reload_apache():
    sys.stdout.write("Reloading Apache...")
    sys.stdout.flush()
    show_success(os.system("%s graceful" % g_info["APACHE_CTL"]) >> 8)  # nosec


def restart_apache():
    if os.system(  # nosec
            init_cmd(g_info['APACHE_INIT_NAME'], 'status') + ' >/dev/null 2>&1') >> 8 == 0:
        sys.stdout.write("Restarting Apache...")
        sys.stdout.flush()
        show_success(
            os.system(  # nosec
                init_cmd(g_info['APACHE_INIT_NAME'], 'restart') + ' >/dev/null') >> 8)


def replace_tags(content, replacements):
    for var, value in replacements.items():
        content = content.replace(var, value)
    return content


def get_editor():
    editor = getenv("VISUAL", getenv("EDITOR", "/usr/bin/vi"))
    if not os.path.exists(editor):
        editor = 'vi'
    return editor


# return "| $PAGER", if a pager is available
def pipe_pager():
    pager = getenv("PAGER")
    if not pager and os.path.exists("/usr/bin/less"):
        pager = "less -F -X"
    if pager:
        return "| %s" % pager
    else:
        return ""


def call_scripts(phase):
    path = g_sitedir + "/lib/omd/scripts/" + phase
    if os.path.exists(path):
        putenv("OMD_ROOT", g_sitedir)
        putenv("OMD_SITE", g_sitename)
        for f in os.listdir(path):
            if f[0] == '.':
                continue
            sys.stdout.write('Executing %s script "%s"...' % (phase, f))
            p = Popen('%s/%s' % (path, f), shell=True, stdout=PIPE, stderr=PIPE)
            stdout = p.stdout.read()
            stderr = p.stderr.read()
            exitcode = p.wait()
            if exitcode == 0:
                sys.stdout.write(tty_ok + '\n')
            else:
                sys.stdout.write(tty_error + ' (exit code: %d, use -v for details)\n' % exitcode)
            if opt_verbose:
                if stdout:
                    sys.stdout.write('Output: %s\n' % stdout)
                if stderr:
                    sys.stdout.write('Errors: %s\n' % stderr)


def check_site_user(site_must_exist):
    if g_sitename != None and site_must_exist and not site_exists(g_sitename):
        bail_out("omd: The site '%s' does not exist. You need to execute "
                 "omd as root or site user." % g_sitename)


#.
#   .--Commands------------------------------------------------------------.
#   |         ____                                          _              |
#   |        / ___|___  _ __ ___  _ __ ___   __ _ _ __   __| |___          |
#   |       | |   / _ \| '_ ` _ \| '_ ` _ \ / _` | '_ \ / _` / __|         |
#   |       | |__| (_) | | | | | | | | | | | (_| | | | | (_| \__ \         |
#   |        \____\___/|_| |_| |_|_| |_| |_|\__,_|_| |_|\__,_|___/         |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Implementation of actual omd commands                               |
#   '----------------------------------------------------------------------'


def main_help(args=[], options={}):
    am_root = os.getuid() == 0
    if am_root:
        sys.stdout.write("Usage (called as root):\n\n")
    else:
        sys.stdout.write("Usage (called as site user):\n\n")

    for command, only_root, no_suid, needs_site, site_must_exist, confirm, synopsis, command_function, options, descr, confirm_text in commands:
        if only_root and not am_root:
            continue
        if am_root:
            if needs_site == 2:
                synopsis = "[SITE] " + synopsis
            elif needs_site == 1:
                synopsis = "SITE " + synopsis

        synopsis_width = am_root and '23' or '16'
        sys.stdout.write((" omd %-10s %-" + synopsis_width + "s %s\n") % (command, synopsis, descr))
    sys.stdout.write(
        "\nGeneral Options:\n"
        " -V <version>                    set specific version, useful in combination with update/create\n"
        " omd COMMAND -h, --help          show available options of COMMAND\n")


def main_setup(args, options={}):
    packages = g_info["OS_PACKAGES"].split()
    install_cmd = g_info["PACKAGE_INSTALL"]
    command = "%s %s" % (install_cmd, " ".join(packages))
    sys.stdout.write("Going to execute '%s'\n" % command)
    if 0 == os.system(command):  # nosec
        ok()

    # Enable Apache modules
    for mod in ["proxy", "proxy_http", "rewrite"]:
        command = g_info["APACHE_ENMOD"] % mod
        if 0 != os.system(command):  # nosec
            sys.stdout.write("ERROR: Could not enable Apache module mod_%s\n" % mod)

    # Activate init script of OMD and APACHE
    for service in ['omd', g_info["APACHE_INIT_NAME"]]:
        sys.stdout.write("Activating init script for \"%s\"\n" % service)
        command = g_info["ACTIVATE_INITSCRIPT"].replace("%s", service)
        sys.stdout.write("Going to execute '%s'\n" % command)
        if 0 != os.system(command):  # nosec
            sys.stdout.write("ERROR\n")

    # Create group 'omd'
    if not group_exists('omd'):
        sys.stdout.write("Creating new group 'omd'\n")
        groupadd('omd')

    # set SUID bit for certain plugins (neccessary for
    # non RPM/DEB installations (FIXME: find a more elegant
    # solution later)
    gid = group_id('omd')
    for plugin in [
            'nagios/plugins/check_icmp', 'nagios/plugins/check_dhcp', 'cmc/icmpsender',
            'cmc/icmpreceiver'
    ]:
        path = "/omd/versions/%s/lib/%s" % (OMD_VERSION, plugin)
        os.chown(path, -1, gid)
        os.chmod(path, 04750)


def main_uninstall(args, options={}):
    global g_sitename
    global g_sitedir
    for sitename in all_sites():
        g_sitename = sitename
        g_sitedir = site_dir(g_sitename)
        main_rm([])

    for path in [
            g_info["OMD_PHYSICAL_BASE"], "/omd", g_info["APACHE_CONF_DIR"] + "/zzz_omd.conf",
            "/etc/init.d/omd", "/usr/bin/omd"
    ]:
        shutil.rmtree(path, ignore_errors=True)

    groupdel('omd')

    sys.stdout.write("Good bye.\n")


def main_setversion(args, options={}):
    if len(args) == 0:
        versions = [(v, "Version %s" % v) for v in omd_versions() if not v == default_version()]

        if use_update_alternatives():
            versions = [('auto', 'Auto (Update-Alternatives)')] + versions

        ok, version = dialog_menu("Choose new default",
                                  "Please choose the version to make the new default", versions,
                                  None, "Make default", "Cancel")
        if not ok:
            bail_out("Aborted.")
    else:
        version = args[0]

    if version != 'auto' and not version_exists(version):
        bail_out("The given version does not exist.")
    if version == default_version():
        bail_out("The given version is already default.")

    # Special handling for debian based distros which use update-alternatives
    # to control the path to the omd binary, manpage and so on
    if use_update_alternatives():
        if version == 'auto':
            os.system("update-alternatives --auto omd")
        else:
            os.system("update-alternatives --set omd /omd/versions/%s" % version)  # nosec
    else:
        if os.path.islink("/omd/versions/default"):
            os.remove("/omd/versions/default")
        os.symlink("/omd/versions/%s" % version, "/omd/versions/default")


def use_update_alternatives():
    return os.path.exists("/var/lib/dpkg/alternatives/omd")


def main_version(args, options={}):
    if len(args) > 0:
        site = args[0]
        if not site_exists(site):
            bail_out("No such site: %s" % site)
        version = site_version(site)
    else:
        version = g_info["OMD_VERSION"]
    if "bare" in options:
        sys.stdout.write(version + "\n")
    else:
        sys.stdout.write("OMD - Open Monitoring Distribution Version %s\n" % version)


def main_versions(args, options={}):
    for v in omd_versions():
        if v == default_version() and not "bare" in options:
            sys.stdout.write("%s (default)\n" % v)
        else:
            sys.stdout.write("%s\n" % v)


def default_version():
    return os.path.basename(os.path.realpath("/omd/versions/default"))


def omd_versions():
    v = [v for v in os.listdir("/omd/versions") if v != "default"]
    v.sort()
    return v


def version_exists(v):
    return v in omd_versions()


def main_sites(args, options={}):
    if on_tty and not "bare" in options:
        sys.stdout.write("SITE             VERSION          COMMENTS\n")
    for site in all_sites():
        tags = []
        if "bare" in options:
            sys.stdout.write("%s\n" % site)
        else:
            disabled = site_is_disabled(site)
            v = site_version(site)
            if v == None:
                v = "(none)"
                tags.append("empty site dir")
            elif v == default_version():
                tags.append("default version")
            if disabled:
                tags.append(tty_bold + tty_red + "disabled" + tty_normal)
            sys.stdout.write("%-16s %-16s %s " % (site, v, ", ".join(tags)))
            sys.stdout.write("\n")


# Bail out if name for new site is not valid (needed by create/mv/cp)
def sitename_must_be_valid(name, reuse=False):
    # Make sanity checks before starting any action
    if not reuse and site_exists(name):
        bail_out("Site '%s' already existing." % name)
    if not reuse and group_exists(name):
        bail_out("Group '%s' already existing." % name)
    if not reuse and user_exists(name):
        bail_out("User '%s' already existing." % name)
    if not re.match("^[a-zA-Z_][a-zA-Z_0-9]{0,15}$", name):
        bail_out(
            "Invalid site name. Must begin with a character, may contain characters, digits and _ and have length 1 up to 16"
        )


def main_create(args, options={}):
    reuse = False
    if "reuse" in options:
        reuse = True
        if not user_verify(g_sitename):
            bail_out("Error verifying site user.")

    sitename_must_be_valid(g_sitename, reuse)

    # Create operating system user for site
    uid = options.get("uid")
    gid = options.get("gid")
    if not reuse:
        useradd(g_sitename, uid, gid)

    sitedir = site_dir(g_sitename)

    if reuse:
        fstab_verify(g_sitename)
    else:
        create_site_dir(g_sitename)
        add_to_fstab(g_sitename, tmpfs_size=options.get('tmpfs-size'))

    config_settings = {}
    if "no-autostart" in options:
        config_settings["AUTOSTART"] = "off"
        sys.stdout.write("Going to set AUTOSTART to off.\n")

    if "no-tmpfs" in options:
        config_settings["TMPFS"] = "off"
        sys.stdout.write("Going to set TMPFS to off.\n")

    if "no-init" not in options:
        admin_password = init_site(config_settings, options)
        welcome_message(admin_password)

    else:
        sys.stdout.write(
            "Create new site %s in disabled state and with empty %s.\n" % (g_sitename, sitedir))
        sys.stdout.write("You can now mount a filesystem to %s.\n" % (sitedir))
        sys.stdout.write("Afterwards you can initialize the site with 'omd init'.\n")


def welcome_message(admin_password):
    sys.stdout.write("Created new site %s with version %s.\n\n" % (g_sitename, OMD_VERSION))
    sys.stdout.write(
        "  The site can be started with %somd start %s%s.\n" % (tty_bold, g_sitename, tty_normal))
    sys.stdout.write("  The default web UI is available at %shttp://%s/%s/%s\n" %
                     (tty_bold, hostname(), g_sitename, tty_normal))
    sys.stdout.write("\n")
    sys.stdout.write(
        "  The admin user for the web applications is %scmkadmin%s with password: %s%s%s\n" %
        (tty_bold, tty_normal, tty_bold, admin_password, tty_normal))
    sys.stdout.write(
        "  (It can be changed with 'htpasswd -m ~/etc/htpasswd cmkadmin' as site user.\n)")
    sys.stdout.write("\n")
    sys.stdout.write("  Please do a %ssu - %s%s for administration of this site.\n" %
                     (tty_bold, g_sitename, tty_normal))
    sys.stdout.write("\n")


def main_init(args, options):
    if not site_is_disabled(g_sitename):
        bail_out("Cannot initialize site that is not disabled.\n"
                 "Please call 'omd disable %s' first." % g_sitename)

    if not site_is_empty(g_sitename):
        if not opt_force:
            bail_out("The site's home directory is not empty. Please add use\n"
                     "'omd --force init %s' if you want to erase all data." % g_sitename)

        sitedir = site_dir(g_sitename)
        # We must not delete the directory itself, just its contents.
        # The directory might be a separate filesystem. This is not quite
        # unlikely, since people using 'omd init' are doing this most times
        # because they are working with clusters and separate filesystems for
        # each site.
        sys.stdout.write("Wiping the contents of %s..." % sitedir)
        for entry in os.listdir(sitedir):
            if entry not in ['.', '..']:
                path = sitedir + "/" + entry
                if opt_verbose:
                    sys.stdout.write("\n   deleting %s..." % path)
                if os.path.islink(path) or not os.path.isdir(path):
                    os.remove(path)
                else:
                    shutil.rmtree(sitedir + "/" + entry)
        ok()

    # Do the things that have been ommited on omd create --disabled
    admin_password = init_site(config_settings=None, options=options)
    welcome_message(admin_password)


def init_site(config_settings=None, options=False):
    apache_reload = "apache-reload" in options

    # Create symbolic link to version
    create_version_symlink(g_sitename, OMD_VERSION)

    # Build up directory structure with symbolic links relative to
    # the version link we just create
    for d in ['bin', 'include', 'lib', 'share']:
        os.symlink("version/" + d, g_sitedir + "/" + d)

    # Create skeleton files of non-tmp directories
    create_skeleton_files(g_sitename, '.')

    # Save the skeleton files used to initialize this site
    save_version_meta_data(g_sitename, OMD_VERSION)

    # Set the initial password of the default admin user
    admin_password = calculate_admin_password(options)
    set_admin_password(admin_password)

    # Special hack for 1.5: Requirement is to activate the new facelift theme
    # for new sites. Normally we use the "WATO sample configuration" for setting
    # specific options for sites created with the new version. But this sample
    # configuration is not set too late to affect the first login.
    # It is set when a user accesses WATO for the first time, which happens
    # after the first login.
    # It is a open task to move this initial configuration step to an earlier
    # stage for the 1.6. But this is nothing we can change now for the 1.5
    # because it would involve some bigger changes.
    # We decided to reach this goal using hard coded specific hack here for the
    # 1.5 and solve the issue completely (also for the other sample config) in
    # the future.
    with open(os.path.join(g_sitedir, "etc/check_mk/multisite.d/wato/global.mk"), "w") as f:
        f.write("# Created by 'omd create'\n" "ui_theme = 'facelift'\n")

    # Change ownership of all files and dirs to site user
    chown_tree(g_sitedir, g_sitename)

    load_site_conf()  # load default values from all hooks
    if config_settings:  # add specific settings
        for hook_name, value in config_settings.items():
            g_site_conf[hook_name] = value
    create_config_environment()

    # Change the few files that config save as created as root
    chown_tree(g_sitedir, g_sitename)

    finalize_site("create", apache_reload)

    return admin_password


# Is being called at the end of create, cp and mv.
# What is "create", "mv" or "cp". It is used for
# running the appropriate hooks.
def finalize_site(what, apache_reload):

    # Now we need to do a few things as site user. Note:
    # - We cannot use setuid() here, since we need to get back to root.
    # - We cannot use seteuid() here, since the id command call will then still
    #   report root and confuse some tools
    # - We cannot sue setresuid() here, since that is not supported an Python 2.4
    # So we need to fork() and use a real setuid() here and leave the main process
    # at being root.
    pid = os.fork()
    if pid == 0:
        try:
            # From now on we run as normal site user!
            switch_to_site_user()

            finalize_size_as_user(what)
            sys.exit(0)
        except Exception, e:
            bail_out(e)
    else:
        wpid, status = os.waitpid(pid, 0)
        if status:
            bail_out("Error in non-priviledged sub-process.")

    # Finally reload global apache - with root permissions - and
    # create include-hook for Apache and reload apache
    create_apache_hook(g_sitename)
    if apache_reload:
        reload_apache()
    else:
        restart_apache()


def finalize_size_as_user(what):
    # Mount and create contents of tmpfs. This must be done as normal
    # user. We also could do this at 'omd start', but this might confuse
    # users. They could create files below tmp which would be shadowed
    # by the mount.
    prepare_and_populate_tmpfs(g_sitename)

    # Run all hooks in order to setup things according to the
    # configuration settings
    config_set_all()
    save_site_conf()

    call_scripts('post-' + what)


def main_rm(args, options={}):
    # omd rm is called as root but the init scripts need to be called as
    # site user but later steps need root privilegies. So a simple user
    # switch to the site user would not work. Better create a subprocess
    # for this dedicated action and switch to the user in that subprocess
    os.system('omd stop %s' % g_sitename)  # nosec

    reuse = "reuse" in options
    kill = "kill" in options

    if user_logged_in(g_sitename):
        if not kill:
            bail_out("User '%s' still logged in or running processes." % g_sitename)
        else:
            kill_site_user_processes(exclude_current_and_parents=True)

    if tmpfs_mounted(g_sitename):
        unmount_tmpfs(g_sitename, kill=kill)

    if not reuse:
        remove_from_fstab(g_sitename)
        sys.stdout.write("Deleting user and group %s..." % g_sitename)
        os.chdir("/")  # Site dir not longer existant after userdel
        userdel(g_sitename)
        ok()

    if os.path.exists(g_sitedir):  # should be done by userdel
        sys.stdout.write("Deleting all data (%s)..." % g_sitedir)
        shutil.rmtree(site_dir(g_sitename))
        ok()

    if reuse:
        create_site_dir(g_sitename)
        os.mkdir(tmp_dir(g_sitename))
        os.chown(tmp_dir(g_sitename), user_id(g_sitename), group_id(g_sitename))

    # remove include-hook for Apache and tell apache
    delete_apache_hook(g_sitename)
    if "apache-reload" in options:
        reload_apache()
    else:
        restart_apache()


def create_site_dir(sitename):
    os.makedirs(site_dir(sitename))
    os.chown(site_dir(sitename), user_id(sitename), group_id(sitename))


def site_is_disabled(g_sitename):
    apache_conf = "/omd/apache/%s.conf" % g_sitename
    return not os.path.exists(apache_conf)


def main_disable(args, options):
    if site_is_disabled(g_sitename):
        sys.stderr.write("This site is already disabled.\n")
        sys.exit(0)

    stop_if_not_stopped(g_sitename)
    unmount_tmpfs(g_sitename, kill="kill" in options)
    sys.stdout.write("Disabling Apache configuration for this site...")
    delete_apache_hook(g_sitename)
    ok()
    restart_apache()


def main_enable(args, options):
    if not site_is_disabled(g_sitename):
        sys.stderr.write("This site is already enabled.\n")
        sys.exit(0)
    sys.stdout.write("Re-enabling Apache configuration for this site...")
    create_apache_hook(g_sitename)
    ok()
    restart_apache()


def set_conflict_option(options):
    global opt_conflict
    opt_conflict = options.get("conflict", "ask")

    if opt_conflict not in ["ask", "install", "keepold", "abort"]:
        bail_out("Argument to --conflict must be one of ask, install, keepold and abort.")


def get_exclude_patterns(options):
    excludes = []
    if "no-rrds" in options or "no-past" in options:
        excludes.append("var/pnp4nagios/perfdata/*")
        excludes.append("var/pnp4nagios/spool/*")
        excludes.append("var/rrdcached/*")
        excludes.append("var/pnp4nagios/states/*")
        excludes.append("var/check_mk/rrd/*")

    if "no-logs" in options or "no-past" in options:
        # Logs of different components
        excludes.append("var/log/*.log")
        excludes.append("var/log/*/*")
        excludes.append("var/pnp4nagios/log/*")
        excludes.append("var/pnp4nagios/perfdata.dump")
        # Nagios monitoring history
        excludes.append("var/nagios/nagios.log")
        excludes.append("var/nagios/archive/")
        # Event console
        excludes.append("var/mkeventd/history/*")
        # Microcore monitoring history
        excludes.append("var/check_mk/core/history")
        excludes.append("var/check_mk/core/archive/*")
        # HW/SW Inventory history
        excludes.append("var/check_mk/inventory_archive/*/*")
        # WATO
        excludes.append("var/check_mk/wato/snapshots/*.tar")

    return excludes


def main_mv_or_cp(what, args, options={}):
    set_conflict_option(options)
    action = what == "mv" and "rename" or "copy"

    global g_sitename
    global g_sitedir
    if len(args) != 1:
        bail_out("omd: Usage: omd %s oldname newname" % what)
    new = args[0]

    reuse = False
    if "reuse" in options:
        reuse = True
        if not user_verify(new):
            bail_out("Error verifying site user.")
        fstab_verify(new)

    sitename_must_be_valid(new, reuse)

    old = g_sitename
    if not site_is_stopped(old):
        bail_out("Cannot %s site '%s' while it is running." % (action, old))

    pids = find_processes_of_user(old)
    if pids:
        bail_out("Cannot %s site '%s' while there are processes owned by %s.\n"
                 "PIDs: %s" % (action, old, old, " ".join(pids)))

    if what == "mv":
        unmount_tmpfs(old, kill="kill" in options)
        if not reuse:
            remove_from_fstab(old)

    sys.stdout.write("%sing site %s to %s..." % (what == "mv" and "Mov" or "Copy", old, new))
    sys.stdout.flush()

    # Create new user. Note: even on mv we need to create a new user.
    # Linux does not (officially) allow to rename a user.
    uid = options.get("uid")
    gid = options.get("gid")
    if not reuse:
        useradd(new, uid, gid)  # None for uid/gid means: let Linux decide

    if what == "mv" and not reuse:
        # Rename base directory and apache config
        os.rename(site_dir(old), site_dir(new))
        delete_apache_hook(old)
    else:
        # Make exact file-per-file copy with same user but already new name
        if not reuse:
            os.mkdir(site_dir(new))

        addopts = ""
        for p in get_exclude_patterns(options):
            addopts += " --exclude '/%s'" % p

        if opt_verbose:
            addopts += " -v"

        os.system("rsync -arx %s '%s/' '%s/'" %  # nosec
                  (addopts, site_dir(g_sitename), site_dir(new)))

        httpdlogdir = site_dir(new) + "/var/log/apache"
        if not os.path.exists(httpdlogdir):
            os.mkdir(httpdlogdir)

        rrdcacheddir = site_dir(new) + "/var/rrdcached"
        if not os.path.exists(rrdcacheddir):
            os.mkdir(rrdcacheddir)

    # give new user all files
    chown_tree(site_dir(new), new)

    # Change config files from old to new site (see rename_site())
    patch_skeleton_files(g_sitename, new)

    # In case of mv now delete old user
    if what == "mv" and not reuse:
        userdel(old)

    # clean up old site
    if what == "mv" and reuse:
        g_sitename = old
        main_rm([], {'reuse': None})

    sys.stdout.write("OK\n")

    # Now switch over to the new site as currently active site
    g_sitename = new
    set_site_globals()
    set_environment()

    # Entry for tmps in /etc/fstab
    if not reuse:
        add_to_fstab(new, tmpfs_size=options.get('tmpfs-size'))

    finalize_site(what, "apache-reload" in options)


def main_diff(args, options={}):
    from_version = site_version(g_sitename)
    from_skelroot = "/omd/versions/%s/skel" % from_version

    # If arguments are added and those arguments are directories,
    # then we just output the general state of the file. If only
    # one file is specified, we directly show the unified diff.
    # This behaviour can also be forced by the OMD option -v.

    if len(args) == 0:
        args = ["."]
    elif len(args) == 1 and os.path.isfile(args[0]):
        global opt_verbose
        opt_verbose = True

    global bare
    bare = "bare" in options  # sorry for the global variable here

    for arg in args:
        diff_list(from_skelroot, g_sitedir, from_version, arg)


def diff_list(from_skelroot, g_sitedir, from_version, orig_path):
    # Compare a list of files/directories with the original state
    # and output differences. If opt_verbose then we output the complete
    # diff, otherwise just the state. Only files present in skel/ are
    # handled at all.

    read_skel_permissions()
    old_perms = load_skel_permissions(from_version)

    # Prepare paths:
    # orig_path: this was specified by the user
    # rel_path:  path relative to the site's dir
    # abs_path:  absolute path

    # Get absolute path to site dir. This can be (/opt/omd/sites/XXX)
    # due to the symbolic link /omd
    old_dir = os.getcwd()
    os.chdir(g_sitedir)
    abs_sitedir = os.getcwd()
    os.chdir(old_dir)

    # Create absolute paths first
    abs_path = orig_path
    if not abs_path.startswith("/"):
        if abs_path == ".":
            abs_path = ""
        elif abs_path.startswith("./"):
            abs_path = path[2:]
        abs_path = os.getcwd() + "/" + abs_path
    abs_path = abs_path.rstrip("/")

    # Make sure that path does not lie outside the OMD site
    if abs_path.startswith(g_sitedir):
        rel_path = abs_path[len(g_sitedir) + 1:]
    elif abs_path.startswith(abs_sitedir):
        rel_path = abs_path[len(abs_sitedir) + 1:]
    else:
        bail_out("Sorry, 'omd diff' only works for files in the site's directory.")

    if not os.path.isdir(abs_path):
        print_diff(rel_path, from_skelroot, g_sitedir, from_version, old_perms)
    else:
        if not rel_path:
            rel_path = "."
        walk_skel(
            from_skelroot,
            print_diff, (from_skelroot, g_sitedir, from_version, old_perms),
            depth_first=False,
            relbase=rel_path)


def print_diff(rel_path, source_path, target_path, source_version, source_perms):
    source_file = source_path + '/' + rel_path
    target_file = target_path + '/' + rel_path

    source_perm = get_skel_permissions(source_version, source_perms, rel_path)
    target_perm = get_file_permissions(target_file)

    source_type = filetype(source_file)
    target_type = filetype(target_file)

    changed_type, changed_content, changed = file_status(source_file, target_file)

    if not changed:
        return

    fn = tty_bold + tty_bgblue + tty_white + rel_path + tty_normal
    fn = tty_bold + rel_path + tty_normal

    def print_status(color, f, status, long_out):
        if bare:
            sys.stdout.write("%s %s\n" % (status, f))
        elif not opt_verbose:
            sys.stdout.write(color + " %s %s\n" % (long_out, f))
        else:
            arrow = tty_magenta + '->' + tty_normal
            if 'c' in status:
                source_content = instantiate_skel(source_file)
                if 0 == os.system("which colordiff > /dev/null 2>&1"):
                    diff = "colordiff"
                else:
                    diff = "diff"
                os.popen("bash -c \"%s -u <(cat) '%s'\"" % (diff, target_file), "w").write(  # nosec
                    source_content)
            elif status == 'p':
                sys.stdout.write("    %s %s %s\n" % (source_perm, arrow, target_perm))
            elif 't' in status:
                sys.stdout.write("    %s %s %s\n" % (source_type, arrow, target_type))

    if not target_type:
        print_status(good, fn, 'd', 'Deleted')
        return

    elif changed_type and changed_content:
        print_status(good, fn, 'tc', 'Changed type and content')

    elif changed_type and not changed_content:
        print_status(good, fn, 't', 'Changed type')

    elif changed_content and not changed_type:
        print_status(good, fn, 'c', 'Changed content')

    if source_perm != target_perm:
        print_status(warn, fn, 'p', 'Changed permissions')


def main_update(args, options={}):
    set_conflict_option(options)

    if not site_is_stopped(g_sitename):
        bail_out("Please completely stop '%s' before updating it." % g_sitename)

    # Unmount tmp. We need to recreate the files and directories
    # from the new version after updating.
    unmount_tmpfs(g_sitename)

    # Source version: the version of the site we deal with
    from_version = site_version(g_sitename)

    # Target version: the version of the OMD binary
    to_version = OMD_VERSION

    # source and target are identical if 'omd update' is called
    # from within a site. In that case we make the user choose
    # the target version explicitely and the re-exec the bin/omd
    # of the target version he has choosen.
    if from_version == to_version:
        possible_versions = [v for v in omd_versions() if v != from_version]
        possible_versions.sort(reverse=True)
        if len(possible_versions) == 0:
            bail_out("There is no other OMD version to update to.")
        elif len(possible_versions) == 1:
            to_version = possible_versions[0]
        else:
            ok, to_version = dialog_menu(
                "Choose target version", "Please choose the version this site should be updated to",
                [(v, "Version %s" % v) for v in possible_versions], possible_versions[0],
                "Update now", "Cancel")
            if not ok:
                bail_out("Aborted.")
        exec_other_omd(to_version)

    # This line is reached, if the version of the OMD binary (the target)
    # is different from the current version of the site.
    if not opt_force and not dialog_yesno(
            "You are going to update the site %s from version %s to version %s. "
            "This will include updating all of you configuration files and merging "
            "changes in the default files with changes made by you. In case of conflicts "
            "your help will be needed." %
        (g_sitename, from_version, to_version), "Update!", "Abort"):
        bail_out("Aborted.")

    # In case the user changes the installed Check_MK Edition during update let the
    # user confirm this step.
    from_edition, to_edition = _get_edition(from_version), _get_edition(to_version)
    if from_edition != to_edition and not opt_force and not dialog_yesno(
            "You are updating from %s Edition to %s Edition. Is this intended?" %
        (from_edition.title(), to_edition.title())):
        bail_out("Aborted.")

    start_logging(g_sitedir + '/var/log/update.log')

    sys.stdout.write("%s - Updating site '%s' from version %s to %s...\n\n" %
                     (time.strftime('%Y-%m-%d %H:%M:%S'), g_sitename, from_version, to_version))

    # Now apply changes of skeleton files. This can be done
    # in two ways:
    # 1. creating a patch from the old default files to the new
    #    default files and applying that to the current files
    # 2. creating a patch from the old default files to the current
    #    files and applying that to the new default files
    # We implement the first method.

    # read permissions
    read_skel_permissions()
    old_perms = load_skel_permissions(from_version)

    from_skelroot = "/omd/versions/%s/skel" % from_version
    to_skelroot = "/omd/versions/%s/skel" % to_version
    tmp = tmp_dir(g_sitename)

    # First walk through skeleton files of new version
    walk_skel(
        to_skelroot,
        update_file, (from_version, to_version, g_sitedir, old_perms),
        depth_first=False)

    # Now handle files present in old but not in new skel files
    walk_skel(
        from_skelroot,
        update_file, (from_version, to_version, g_sitedir, old_perms),
        depth_first=True,
        exclude_if_in=to_skelroot)

    # Change symbolic link pointing to new version
    create_version_symlink(g_sitename, to_version)
    save_version_meta_data(g_sitename, to_version)

    # Let hooks do their work and update configuration.
    config_set_all()

    call_scripts('post-update')

    sys.stdout.write('Finished update.\n\n')
    stop_logging()


def _get_edition(omd_version):
    """Returns the long Check_MK Edition name or "unknown" of the given OMD version"""
    parts = omd_version.split(".")
    if parts[-1] == "demo":
        edition_short = parts[-2]
    else:
        edition_short = parts[-1]

    if edition_short == "cre":
        return "raw"
    elif edition_short == "cee":
        return "enterprise"
    elif edition_short == "cme":
        return "managed"
    else:
        return "unknown"


def main_umount(args, options={}):
    global g_sitename
    global g_sitedir

    only_version = options.get("version")

    # if no site is selected, all sites are affected
    exit_status = 0
    if not g_sitename:
        for site in all_sites():
            # Set global vars for the current site
            g_sitename = site
            g_sitedir = site_dir(g_sitename)

            if only_version and site_version(site) != only_version:
                continue

            # Skip the site even when it is partly running
            if not site_is_stopped(site):
                sys.stderr.write("Cannot unmount tmpfs of site '%s' while it is running.\n" % site)
                continue

            sys.stdout.write("%sUnmounting tmpfs of site %s%s..." % (tty_bold, site, tty_normal))
            sys.stdout.flush()

            if not show_success(unmount_tmpfs(site, False, kill="kill" in options)):
                exit_status = 1
    else:
        # Skip the site even when it is partly running
        if not site_is_stopped(g_sitename):
            bail_out("Cannot unmount tmpfs of site '%s' while it is running." % g_sitename)
        unmount_tmpfs(g_sitename, kill="kill" in options)
    sys.exit(exit_status)


def main_init_action(command, args, options={}):
    if g_sitename:
        exit_status = init_action(command, args, options)

        # When the whole site is about to be stopped check for remaining
        # processes and terminate them
        if command == "stop" and not args and exit_status == 0:
            terminate_site_user_processes()

        sys.exit(exit_status)

    # if no site is selected, all sites are affected

    only_version = options.get("version")
    bare = "bare" in options
    parallel = "parallel" in options

    max_site_len = max([8] + [len(site_id) for site_id in all_sites()])

    def parallel_output(site_id, line):
        sys.stdout.write(("%-" + str(max_site_len) + "s - %s") % (site_id, line))

    exit_states, processes = [], []
    for site in all_sites():
        v = site_version(site)
        if v == None:  # skip partially created sites
            continue

        if only_version and v != only_version:
            continue

        # Skip disabled sites completely
        if site_is_disabled(site):
            continue

        # Handle non autostart sites
        if command in [ "start", "restart", "reload" ] or \
            ( "auto" in options and command == "status" ):
            if not opt_force and not site_autostart(site):
                if bare:
                    continue
                elif not parallel:
                    sys.stdout.write("Ignoring site '%s': AUTOSTART != on\n" % site)
                else:
                    parallel_output(site, "Ignoring since autostart is disabled\n")
                continue

        if command == "status" and bare:
            sys.stdout.write('[%s]\n' % site)
        elif not parallel:
            sys.stdout.write("%sDoing '%s' on site %s:%s\n" % (tty_bold, command, site, tty_normal))
        else:
            parallel_output(site, "Invoking '%s'\n" % (command))
        sys.stdout.flush()

        # We need to open a subprocess, because each site must be started with the account of the
        # site user. And after setuid() we cannot return.
        stdout = sys.stdout if not parallel else subprocess.PIPE
        stderr = sys.stderr if not parallel else subprocess.STDOUT
        bare_arg = ["--bare"] if bare else []
        p = subprocess.Popen(
            [sys.argv[0], command] + bare_arg + [site] + args,
            stdin=open(os.devnull, "r"),
            stdout=stdout,
            stderr=stderr)

        if parallel:
            # Make the output non blocking
            fd = p.stdout.fileno()
            fl = fcntl.fcntl(fd, fcntl.F_GETFL)
            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

            processes.append((site, p))
        else:
            exit_states.append(p.wait())
            if not bare:
                sys.stdout.write("\n")

    # In parallel mode wait for completion of all processes and collect
    # the output produced on stdout in the meantime. Since the processes
    # work in parallel and we want to have nearly "live" output, we process
    # the output line by line and prefix each line with the ID of the site.
    # The output of a single process must not block the output of the others,
    # so it seems we need to do some low level stuff here :-/.
    site_buf = {}
    while parallel and processes:
        for site_id, p in processes[:]:
            buf = site_buf.get(site_id, "")
            try:
                while True:
                    b = p.stdout.read(1024)
                    if not b:
                        break
                    buf += b
            except IOError, e:
                if e.errno == 11:  # Resource temporarily unavailable
                    pass
                else:
                    raise

            while True:
                pos = buf.find("\n")
                if pos == -1:
                    break
                line, buf = buf[:pos + 1], buf[pos + 1:]
                parallel_output(site_id, line)

            site_buf[site_id] = buf

            if not buf and p.poll() is not None:
                exit_states.append(p.returncode)
                processes.remove((site_id, p))
        time.sleep(0.01)

    # Do not simply take the highest exit code from the single sites.
    # We want to be able to output the fact that either none of the
    # sites is running or just some of the sites. For this we transform
    # the sites states 1 (not running) to 2 (partially running) if at least
    # one other site has state 0 (running) or 2 (partially running).
    if 1 in exit_states and (0 in exit_states or 2 in exit_states):
        exit_status = 2  # not all sites running, but at least one
    elif exit_states:
        exit_status = max(exit_states)
    else:
        exit_status = 0  # No OMD site existing

    sys.exit(exit_status)


def main_config(args, options={}):
    if (len(args) == 0 or args[0] != "show") and \
        not site_is_stopped(g_sitename) and opt_force:
        need_start = True
        stop_site(g_sitename)
    else:
        need_start = False

    load_config_hooks()  # needed for all config commands
    if len(args) == 0:
        config_configure()
    else:
        command = args[0]
        args = args[1:]
        if command == "show":
            config_show(args)
        elif command == "set":
            config_set(args)
        elif command == "change":
            config_change()
        else:
            config_usage()

    if need_start:
        start_site(g_sitename)


def main_su(args, options={}):
    try:
        os.execl("/bin/su", "su", "-", "%s" % g_sitename)
    except OSError:
        bail_out("Cannot open a shell for user %s" % g_sitename)


def backup_site_files_to_tarfile(tar, options):
    exclude = get_exclude_patterns(options)
    exclude.append("tmp/*")  # Exclude all tmpfs files

    # exclude all temporary files that are created during cmk.store writes
    exclude.append("*.mk.new*")
    exclude.append("var/log/.liveproxyd.state.new*")

    # exclude section cache because files may vanish during backup. It would
    # be better to have them in the backup and simply don't make the backup
    # fail in case a file vanishes during the backup, but the tarfile module
    # does not allow this.
    exclude.append("var/check_mk/persisted/*")
    exclude.append("var/check_mk/persisted_sections/*")

    def filter_files(filename):
        for glob_pattern in exclude:
            # patterns are relative to site dir, filename is full path.
            # strip of the g_sitedir prefix from full path
            if fnmatch.fnmatch(filename[len(g_sitedir) + 1:], glob_pattern):
                return True  # exclude this file
        return False

    tar.add(g_sitedir, g_sitename, exclude=filter_files)


# We need to use our tarfile class here to perform a rrdcached SUSPEND/RESUME
# to prevent writing to individual RRDs during backups.
class BackupTarFile(tarfile.TarFile):
    def __init__(self, name, mode, fileobj, **kwargs):
        self._site_stopped = site_is_stopped(g_sitename)
        self._rrdcached_socket_path = g_sitedir + "/tmp/run/rrdcached.sock"
        self._sock = None
        self._sites_path = os.path.realpath("/omd/sites")

        super(BackupTarFile, self).__init__(name, mode, fileobj, **kwargs)

    # We override this function to workaround an issue in the builtin add() method in
    # case it is called in recursive mode and a file vanishes between the os.listdir()
    # and the first file access (often seen os.lstat()) during backup. Instead of failing
    # like this we want to skip those files silently during backup.
    def add(self, name, arcname=None, recursive=True, exclude=None, filter=None):  # pylint: disable=redefined-builtin
        try:
            super(BackupTarFile, self).add(name, arcname, recursive, exclude, filter)
        except OSError as e:
            if e.errno != errno.ENOENT or arcname == g_sitename:
                raise

            if opt_verbose:
                sys.stdout.write("Skipping vanished file: %s\n" % arcname)

    def addfile(self, tarinfo, fileobj=None):
        # In case of a stopped site or stopped rrdcached there is no
        # need to suspend rrd updates
        if self._site_stopped or not os.path.exists(self._rrdcached_socket_path):
            super(BackupTarFile, self).addfile(tarinfo, fileobj)
            return

        site_rel_path = tarinfo.name[len(g_sitename) + 1:]

        is_rrd = (site_rel_path.startswith("var/pnp4nagios/perfdata") \
                  or site_rel_path.startswith("var/check_mk/rrd")) \
                 and site_rel_path.endswith(".rrd")

        # rrdcached works realpath
        rrd_file_path = os.path.join(self._sites_path, tarinfo.name)

        if is_rrd:
            self._suspend_rrd_update(rrd_file_path)

        try:
            super(BackupTarFile, self).addfile(tarinfo, fileobj)
        finally:
            if is_rrd:
                self._resume_rrd_update(rrd_file_path)

    def _suspend_rrd_update(self, path):
        if opt_verbose:
            sys.stdout.write("Pausing RRD updates for %s\n" % path)
        self._send_rrdcached_command("SUSPEND %s" % path)

    def _resume_rrd_update(self, path):
        if opt_verbose:
            sys.stdout.write("Resuming RRD updates for %s\n" % path)
        self._send_rrdcached_command("RESUME %s" % path)

    def _resume_all_rrds(self):
        if opt_verbose:
            sys.stdout.write("Resuming RRD updates for ALL\n")
        self._send_rrdcached_command("RESUMEALL")

    def _send_rrdcached_command(self, cmd):
        if not self._sock:
            self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            try:
                self._sock.connect(self._rrdcached_socket_path)
            except socket.error, e:
                if e.errno == 2:  # No such file or directory
                    self._sock = None
                    if opt_verbose:
                        sys.stdout.write("skipping rrdcached command (socket missing)\n")
                    return
                else:
                    raise

        try:
            if opt_verbose:
                sys.stdout.write("rrdcached command: %s\n" % cmd)
            self._sock.sendall("%s\n" % cmd)

            answer = ""
            while not answer.endswith("\n"):
                answer += self._sock.recv(1024)
        except socket.error, e:
            # ECONNRESET: Broken pipe
            # EPIPE:      Connection reset by peer
            #             Happens, for example, when the rrdcached is reloaded/restarted during backup
            if e.errno in (errno.ECONNRESET, errno.EPIPE):
                self._sock = None
                if opt_verbose:
                    sys.stdout.write("skipping rrdcached command (%s)\n" % e)
                return
            else:
                raise

        code, msg = answer.strip().split(" ", 1)
        if code == "-1":
            if opt_verbose:
                sys.stdout.write("rrdcached response: %r\n" % (answer))

            if cmd.startswith("SUSPEND") and msg.endswith("already suspended"):
                pass  # is fine when trying to suspend
            elif cmd.startswith("RESUME") and msg.endswith("not suspended"):
                pass  # is fine when trying to resume
            elif msg.endswith("No such file or directory"):
                pass  # is fine (unknown RRD)
            else:
                raise Exception("Error while processing rrdcached command (%s): %s" % (cmd, msg))

        elif opt_verbose:
            sys.stdout.write("rrdcached response: %r\n" % (answer))

    def close(self):
        super(BackupTarFile, self).close()

        if self._sock:
            self._resume_all_rrds()
            self._sock.close()


def backup_site_to_tarfile(fh, mode, options):
    tar = BackupTarFile.open(fileobj=fh, mode=mode)
    try:
        # Add the version symlink as first file to be able to
        # check a) the sitename and b) the version before reading
        # the whole tar archive. Important for streaming.
        # The file is added twice to get the first for validation
        # and the second for excration during restore.
        tar.add(g_sitedir + "/version", g_sitename + "/version")
        backup_site_files_to_tarfile(tar, options)
        tar.close()
    except IOError, e:
        bail_out("Failed to perform backup: %s" % e)


def main_backup(args, options={}):
    if len(args) == 0:
        bail_out("You need to provide either a path to the destination "
                 "file or \"-\" for backup to stdout.")

    dest = args[0]

    if dest == '-':
        fh = sys.stdout
        tar_mode = 'w|'
    else:
        if dest[0] != '/':
            dest = g_orig_wd + '/' + dest
        fh = file(dest, 'w')
        tar_mode = 'w:'

    if "no-compression" not in options:
        tar_mode += "gz"

    backup_site_to_tarfile(fh, tar_mode, options)


def main_restore(args, options={}):
    global g_sitename
    global g_sitedir

    set_conflict_option(options)

    if len(args) == 0:
        bail_out("You need to provide either a path to the source "
                 "file or \"-\" for restore from stdin.")

    source = args[-1]
    if source == '-':
        fh = sys.stdin
        tar_mode = 'r|*'
    elif os.path.exists(source):
        fh = file(source)
        tar_mode = 'r:*'
    else:
        bail_out("The backup archive does not exist.")

    try:
        tar = tarfile.open(fileobj=fh, mode=tar_mode)
    except tarfile.ReadError, e:
        bail_out("Failed to open the backup: %s" % e)

    # Get the first file of the tar archive. Expecting <site>/version symlink
    # for validation reasons.
    site_tarinfo = tar.next()
    try:
        sitename, version_name = site_tarinfo.name.split("/", 1)
    except ValueError:
        bail_out("Failed to detect version of backed up site. "
                 "Maybe the backup is from an incompatible version.")

    if version_name == "version":
        version = site_tarinfo.linkname.split('/')[-1]
    else:
        bail_out("Failed to detect version of backed up site.")

    if not version_exists(version):
        bail_out("You need to have version %s installed to be able to restore "
                 "this backup." % version)

    if is_root():
        # Ensure the restore is done with the sites version
        if version != OMD_VERSION:
            exec_other_omd(version)

        # Restore site with its original name, or specify a new one
        new_sitename = sitename
        if len(args) == 2:
            new_sitename = args[0]
    else:
        new_sitename = site_name()

    g_sitename = new_sitename
    g_sitedir = site_dir(new_sitename)

    source_txt = source == '-' and 'stdin' or source
    if is_root():
        sys.stdout.write("Restoring site %s from %s...\n" % (g_sitename, source_txt))
        sys.stdout.flush()

        prepare_restore_as_root(options)

    else:
        sys.stdout.write("Restoring site from %s...\n" % source_txt)
        sys.stdout.flush()

        load_site_conf()
        orig_apache_port = g_site_conf["APACHE_TCP_PORT"]

        prepare_restore_as_site_user(options)

    # Now extract all files
    for tarinfo in tar:
        # The files in the tar archive start with the siteid as first element.
        # Remove this first element from the file paths and also care for hard link
        # targets.

        # Remove leading site name from paths
        tarinfo.name = '/'.join(tarinfo.name.split('/')[1:])
        if opt_verbose:
            sys.stdout.write("Restoring %s...\n" % tarinfo.name)

        # Handle hard links from var/check_mk/core/autochecks/*.mk
        # to -> var/check_mk/autochecks/*.mk files.
        if tarinfo.islnk() and (tarinfo.name.startswith("var/check_mk/core/autochecks/") or
                                tarinfo.name.startswith("var/check_mk/autochecks/")):
            parts = tarinfo.linkname.split('/')

            if parts[0] == sitename:
                new_linkname = '/'.join(parts[1:])

                if opt_verbose:
                    sys.stdout.write("  Rewriting link target from %s to %s\n" % (tarinfo.linkname,
                                                                                  new_linkname))
                tarinfo.linkname = new_linkname

        tar.extract(tarinfo, path=g_sitedir)
    tar.close()

    load_site_conf()

    # give new user all files
    chown_tree(g_sitedir, g_sitename)

    # Change config files from old to new site (see rename_site())
    if sitename != g_sitename:
        patch_skeleton_files(sitename, g_sitename)

    # Now switch over to the new site as currently active site
    os.chdir(g_sitedir)
    set_environment()

    if is_root():
        postprocess_restore_as_root(options)
    else:
        postprocess_restore_as_site_user(options, orig_apache_port)


def prepare_restore_as_root(options):
    reuse = False
    if "reuse" in options:
        reuse = True
        if not user_verify(g_sitename, allow_populated=True):
            bail_out("Error verifying site user.")
        fstab_verify(g_sitename)

    sitename_must_be_valid(g_sitename, reuse)

    if reuse:
        if not site_is_stopped(g_sitename) and not "kill" in options:
            bail_out("Cannot restore '%s' while it is running." % (g_sitename))
        else:
            os.system('omd stop %s' % g_sitename)  # nosec
        unmount_tmpfs(g_sitename, kill="kill" in options)

    if not reuse:
        uid = options.get("uid")
        gid = options.get("gid")
        useradd(g_sitename, uid, gid)  # None for uid/gid means: let Linux decide
    else:
        sys.stdout.write("Deleting existing site data...\n")
        shutil.rmtree(g_sitedir)
        ok()

    os.mkdir(g_sitedir)


def prepare_restore_as_site_user(options):
    if not site_is_stopped(g_sitename) and not "kill" in options:
        bail_out("Cannot restore site while it is running.")

    verify_directory_write_access()

    sys.stdout.write("Stopping site processes...\n")
    stop_site(g_sitename)
    kill_site_user_processes(exclude_current_and_parents=True)
    ok()

    unmount_tmpfs(g_sitename)

    sys.stdout.write("Deleting existing site data...")
    for f in os.listdir(g_sitedir):
        path = g_sitedir + "/" + f
        if os.path.islink(path) or os.path.isfile(path):
            os.unlink(path)
        else:
            shutil.rmtree(path)
    ok()


# Scans all site directories and ensures the site user is able to write all directories.
# This is needed to prevent eventual permission issues during the rmtree process.
def verify_directory_write_access():
    wrong = []
    for dirpath, dirnames, filenames in os.walk(g_sitedir):
        for dirname in dirnames:
            path = dirpath + "/" + dirname
            if os.path.islink(path):
                continue

            if not os.access(path, os.W_OK):
                wrong.append(path)

    if wrong:
        bail_out("Unable to start restore because of a permission issue.\n\n"
                 "The restore needs to be able to clean the whole site to be able to restore "
                 "the backup. Missing write access on the following paths:\n\n"
                 "    %s" % "\n    ".join(wrong))


def terminate_site_user_processes():
    """Sends a SIGTERM to all running site processes and waits up to 5 seconds for termination

    In case one or more processes are still running after the timeout, the method will make
    the current OMD call terminate.
    """

    pids = site_user_processes(exclude_current_and_parents=True)
    if not pids:
        return

    sys.stdout.write("Stopping %d remaining site processes..." % len(pids))

    timeout_at = time.time() + 5
    sent_terminate = False
    while pids and time.time() < timeout_at:
        for pid in pids[:]:
            try:
                if not sent_terminate:
                    if opt_verbose:
                        sys.stdout.write("%d..." % pid)
                    os.kill(pid, signal.SIGTERM)
                else:
                    os.kill(pid, signal.SIG_DFL)
            except OSError as e:
                if e.errno == errno.ESRCH:  # No such process
                    pids.remove(pid)
                else:
                    raise

        sent_terminate = True
        time.sleep(0.1)

    if pids:
        bail_out("\nFailed to stop remaining site processes: %s" % ", ".join(map(str, pids)))
    else:
        ok()


def kill_site_user_processes(exclude_current_and_parents=False):
    pids = site_user_processes(exclude_current_and_parents)
    tries = 5
    while tries > 0 and pids:
        for pid in pids[:]:
            try:
                if opt_verbose:
                    sys.stdout.write("Killing process %d...\n" % pid)
                os.kill(pid, signal.SIGKILL)
            except OSError, e:
                if e.errno == 3:
                    pids.remove(pid)
                    pass  # No such process
                else:
                    raise
        time.sleep(1)
        tries -= 1

    if pids:
        bail_out("Failed to kill site processes: %s" % ", ".join(map(str, pids)))


def get_current_and_parent_pids():
    """Return list of PIDs of the current process and parent process tree till pid 0"""
    pids = []
    process = psutil.Process()
    while process and process.pid != 0:
        pids.append(process.pid)
        process = process.parent()
    return pids


def site_user_processes(exclude_current_and_parents):
    """Return list of PIDs of all running site user processes (that are not excluded)"""
    exclude = []
    if exclude_current_and_parents:
        exclude = get_current_and_parent_pids()

    p = subprocess.Popen(["ps", "-U", g_sitename, "-o", "pid", "--no-headers"],
                         close_fds=True,
                         stdin=open(os.devnull),
                         stdout=subprocess.PIPE)
    exclude.append(p.pid)

    pids = []
    for l in p.communicate()[0].split("\n"):
        line = l.strip()
        if not line:
            continue

        pid = int(line)

        if pid in exclude:
            continue

        pids.append(pid)
    return pids


def postprocess_restore_as_root(options):
    # Entry for tmps in /etc/fstab
    if "reuse" not in options:
        add_to_fstab(g_sitename, tmpfs_size=options.get('tmpfs-size'))

    finalize_site("restore", "apache-reload" in options)


def postprocess_restore_as_site_user(options, orig_apache_port):
    # Keep the apache port the site currently being replaced had before
    # (we can not restart the system apache as site user)
    g_site_conf["APACHE_TCP_PORT"] = orig_apache_port
    save_site_conf()

    finalize_size_as_user("restore")


exclude_options = [
    ("no-rrds", None, False, "do not copy RRD files (performance data)"),
    ("no-logs", None, False, "do not copy the monitoring history and log files"),
    ("no-past", "N", False, "do not copy RRD files, the monitoring history and log files"),
]

commands = [
    #  command       The id of the command
    #  only_root     This option is only available when omd command is run as root
    #  no_suid       The command is available for root and site-user, but no switch
    #                to the site user is performed before execution the mode function
    #  needs_site    When run as root:
    #                0: No site must be specified
    #                1: A site must be specified
    #                2: A site is optional
    #  must_exist    Site must be existant for this command
    #  confirm       Is a confirm dialog shown before command execution?
    #  args          Help text for command individual arguments
    #  function      Handler function for this command
    #  options_spec  List of individual arguments for this command
    #  description   Text for the help of omd
    #  confirm_text  Confirm text to show before calling the handler function
    ("help", False, False, 0, 0, False, "", main_help, [], "Show general help", ""),
    ("setup", True, False, 0, 0, True, "", main_setup, [],
     "Prepare operating system for OMD (installs packages)",
     "We will install missing packages from your operating system and setup the\n"
     "system apache daemon (add configuration files and modules needed by omd)\n"),
    ("uninstall", True, False, 0, 0, True, "", main_uninstall, [], "Remove OMD and all sites!",
     "BE WARNED: You are about to remove everything your system\n"
     "           have ever known about omd."),
    ("setversion", True, False, 0, 0, False, "VERSION", main_setversion, [],
     "Sets the default version of OMD which will be used by new sites", ""),
    ("version", False, False, 0, 0, False, "[SITE]", main_version,
     [("bare", "b", False, "output plain text optimized for parsing")], "Show version of OMD", ""),
    ("versions", False, False, 0, 0, False, "", main_versions,
     [("bare", "b", False,
       "output plain text optimized for parsing")], "List installed OMD versions", ""),
    ("sites", False, False, 0, 0, False, "", main_sites,
     [("bare", "b", False, "output plain text for easy parsing")], "Show list of sites", ""),
    ("create", True, False, 1, 0, False, "", main_create, [
        ("uid", "u", True, "create site user with UID ARG"),
        ("gid", "g", True, "create site group with GID ARG"),
        ("admin-password", None, True, "set initial password instead of generating one"),
        ("reuse", None, False, "do not create a site user, reuse existing one"),
        ("no-init", "n", False, "leave new site directory empty (a later omd init does this"),
        ("no-autostart", "A", False, "set AUTOSTART to off (useful for test sites)"),
        ("apache-reload", False, False, "Issue a reload of the system apache instead of a restart"),
        ("no-tmpfs", None, False, "set TMPFS to off"),
        ("tmpfs-size", "t", True,
         "specify the maximum size of the tmpfs (defaults to 50% of RAM), examples: 500M, 20G, 60%"
        ),
    ], "Create a new site (-u UID, -g GID)",
     "This command performs the following actions on your system:\n"
     "- Create the system user <SITENAME>\n"
     "- Create the system group <SITENAME>\n"
     "- Create and populate the site home directory\n"
     "- Restart the system wide apache daemon\n"
     "- Add tmpfs for the site to fstab and mount it"),
    ("init", True, False, 1, 1, False, "", main_init, [
        ("apache-reload", False, False, "Issue a reload of the system apache instead of a restart"),
    ], "Populate site directory with default files and enable the site", ""),
    ("rm", True, True, 1, 1, True, "", main_rm, [
        ("reuse", None, False, "assume --reuse on create, do not delete site user/group"),
        ("kill", None, False, "kill processes of the site before deleting it"),
        ("apache-reload", False, False, "Issue a reload of the system apache instead of a restart"),
    ], "Remove a site (and its data)", "PLEASE NOTE: This action removes all configuration files\n"
     "             and variable data of the site.\n"
     "\n"
     "In detail the following steps will be done:\n"
     "- Stop all processes of the site\n"
     "- Unmount tmpfs of the site\n"
     "- Remove tmpfs of the site from fstab\n"
     "- Remove the system user <SITENAME>\n"
     "- Remove the system group <SITENAME>\n"
     "- Remove the site home directory\n"
     "- Restart the system wide apache daemon\n"),
    ("disable", True, False, 1, 1, False, "", main_disable,
     [("kill", None, False, "kill processes using tmpfs before unmounting it")],
     "Disable a site (stop it, unmount tmpfs, remove Apache hook)", ""),
    ("enable", True, False, 1, 1, False, "", main_enable, [],
     "Enable a site (reenable a formerly disabled site)", ""),
    ("mv", True, False, 1, 1, False, "NEWNAME", lambda args, opts: main_mv_or_cp("mv", args, opts),
     [
         ("uid", "u", True, "create site user with UID ARG"),
         ("gid", "g", True, "create site group with GID ARG"),
         ("reuse", None, False, "do not create a site user, reuse existing one"),
         ("conflict", None, True,
          "non-interactive conflict resolution. ARG is install, keepold, abort or ask"),
         ("tmpfs-size", "t", True,
          "specify the maximum size of the tmpfs (defaults to 50% of RAM), examples: 500M, 20G, 60%"
         ),
         ("apache-reload", False, False,
          "Issue a reload of the system apache instead of a restart"),
     ], "Rename a site", ""),
    ("cp", True, False, 1, 1, False, "NEWNAME", lambda args, opts: main_mv_or_cp("cp", args, opts),
     [("uid", "u", True, "create site user with UID ARG"),
      ("gid", "g", True, "create site group with GID ARG"),
      ("reuse", None, False, "do not create a site user, reuse existing one")
     ] + exclude_options + [
         ("conflict", None, True,
          "non-interactive conflict resolution. ARG is install, keepold, abort or ask"),
         ("tmpfs-size", "t", True,
          "specify the maximum size of the tmpfs (defaults to 50% of RAM), examples: 500M, 20G, 60%"
         ),
         ("apache-reload", False, False,
          "Issue a reload of the system apache instead of a restart"),
     ], "Make a copy of a site", ""),
    ("update", False, False, 1, 1, False, "", main_update,
     [("conflict", None, True,
       "non-interactive conflict resolution. ARG is install, keepold, abort or ask")],
     "Update site to other version of OMD", ""),
    ("start", False, False, 2, 1, False, "[SERVICE]",
     lambda args, opts: main_init_action("start", args, opts), [
         ("version", "V", True, "only start services having version ARG"),
         ("parallel", "p", False, "Invoke start of sites in parallel"),
     ], "Start services of one or all sites", ""),
    ("stop", False, False, 2, 1, False, "[SERVICE]",
     lambda args, opts: main_init_action("stop", args, opts), [
         ("version", "V", True, "only stop sites having version ARG"),
         ("parallel", "p", False, "Invoke stop of sites in parallel"),
     ], "Stop services of site(s)", ""),
    ("restart", False, False, 2, 1, False, "[SERVICE]",
     lambda args, opts: main_init_action("restart", args, opts),
     [("version", "V", True,
       "only restart sites having version ARG")], "Restart services of site(s)", ""),
    ("reload", False, False, 2, 1, False, "[SERVICE]",
     lambda args, opts: main_init_action("reload", args, opts),
     [("version", "V", True,
       "only reload sites having version ARG")], "Reload services of site(s)", ""),
    ("status", False, False, 2, 1, False, "[SERVICE]",
     lambda args, opts: main_init_action("status", args, opts), [
         ("version", "V", True, "show only sites having version ARG"),
         ("auto", None, False, "show only sites with AUTOSTART = on"),
         ("bare", "b", False, "output plain format optimized for parsing"),
     ], "Show status of services of site(s)", ""),
    ("config", False, False, 1, 1, False, "...", main_config, [],
     "Show and set site configuration parameters", ""),
    ("diff", False, False, 1, 1, False, "([RELBASE])", main_diff,
     [("bare", "b", False, "output plain diff format, no beautifying")],
     "Shows differences compared to the original version files", ""),
    ("su", True, False, 1, 1, False, "", main_su, [], "Run a shell as a site-user", ""),
    ("umount", False, False, 2, 1, False, "", main_umount,
     [("version", "V", True, "unmount only sites with version ARG"),
      ("kill", None, False, "kill processes using the tmpfs before unmounting it")],
     "Umount ramdisk volumes of site(s)", ""),
    ("backup", False, True, 1, 1, False, "[SITE] [-|ARCHIVE_PATH]", main_backup, exclude_options + [
        ("no-compression", None, False, "do not compress tar archive"),
    ], "Create a backup tarball of a site, writing it to a file or stdout", ""),
    ("restore", False, False, 0, 0, False, "[SITE] [-|ARCHIVE_PATH]", main_restore, [
        ("uid", "u", True, "create site user with UID ARG"),
        ("gid", "g", True, "create site group with GID ARG"),
        ("reuse", None, False, "do not create a site user, reuse existing one"),
        ("kill", None, False,
         "kill processes of site when reusing an existing one before restoring"),
        ("apache-reload", False, False, "Issue a reload of the system apache instead of a restart"),
        ("conflict", None, True,
         "non-interactive conflict resolution. ARG is install, keepold, abort or ask"),
        ("tmpfs-size", "t", True, "specify the maximum size of the tmpfs (defaults to 50% of RAM)"),
    ], "Restores the backup of a site to an existing site or creates a new site", ""),
]


def handle_global_option(opt, orig):
    global opt_verbose
    global opt_force
    global opt_interactive

    def opt_arg():
        global main_args
        if len(main_args) < 1:
            bail_out("Option %s needs an argument." % opt)
        arg = main_args[0]
        main_args = main_args[1:]
        return arg

    if opt in ['V', 'version']:
        # Switch to other version of bin/omd
        version = opt_arg()
        if version != OMD_VERSION:
            omd_path = "/omd/versions/%s/bin/omd" % version
            if not os.path.exists(omd_path):
                bail_out("OMD version '%s' is not installed." % version)
            os.execv(omd_path, sys.argv)
            bail_out("Cannot execute %s." % omd_path)
    elif opt in ['f', 'force']:
        opt_force = True
        opt_interactive = False
    elif opt in ['i', 'interactive']:
        opt_force = False
        opt_interactive = True
    elif opt in ['v', 'verbose']:
        opt_verbose = True
    else:
        bail_out("Invalid global option %s.\n" "Call omd help for available options." % orig)


def parse_command_options(args, options_spec):
    # Give a short overview over the command specific options
    # when the user specifies --help:
    if len(args) and args[0] in ['-h', '--help']:
        sys.stdout.write("Possible options for this command:\n")
        for llong, sshort, needarg, help in options_spec:
            args_text = "%s--%s" % (sshort and "-%s," % sshort or "", llong)
            sys.stdout.write(" %-15s %3s  %s\n" % (args_text, needarg and "ARG" or "", help))
        sys.exit(0)

    options = {}

    while len(args) >= 1 and args[0][0] == '-' and len(args[0]) > 1:
        opt = args[0]
        args = args[1:]
        entries = []
        if opt.startswith("--"):
            # Handle --foo=bar
            if "=" in opt:
                opt, optarg = opt.split("=", 1)
                args = [optarg] + args
                for e in options_spec:
                    if e[0] == opt[2:] and not e[2]:
                        bail_out("The option %s does not take an argument" % opt)

            for e in options_spec:
                if e[0] == opt[2:]:
                    entries = [e]
        else:
            for char in opt:
                for e in options_spec:
                    if e[1] == char:
                        entries.append(e)

        if len(entries) == 0:
            bail_out("Invalid option '%s'" % opt)

        for llong, sshort, needs_arg, help in entries:
            arg = None
            if needs_arg:
                if len(args) == 0:
                    bail_out("Option '%s' needs an argument." % opt)
                arg = args[0]
                args = args[1:]
            options[llong] = arg
    return (args, options)


def exec_other_omd(version):
    # Rerun with omd of other version
    omd_path = "/omd/versions/%s/bin/omd" % version
    if os.path.exists(omd_path):
        if command == "update":
            # Prevent inheriting environment variables from this versions/site environment
            # into the execed omd call. The OMD call must import the python version related
            # modules and libaries. This only works when PYTHONPATH and LD_LIBRARY_PATH are
            # not already set when calling "omd update"
            try:
                del os.environ["PYTHONPATH"]
            except KeyError:
                pass

            try:
                del os.environ["LD_LIBRARY_PATH"]
            except KeyError:
                pass

        os.execv(omd_path, sys.argv)
        bail_out("Cannot run bin/omd of version %s." % version)
    else:
        bail_out("Site %s uses version %s which is not installed.\n"
                 "Please reinstall that version and retry this command." % (g_sitename, version))


def random_password():
    return ''.join(random.choice(string.ascii_letters + string.digits) for i in range(8))


def encrypt_password(password, salt=None):
    if not salt:
        salt = "%06d" % (1000000 * (time.time() % 1.0))
    return md5crypt(password, salt, '$1$')


# -----------------------------------------------------------------------------
# Based on FreeBSD src/lib/libcrypt/crypt.c 1.2
# http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain

# Original license:
# * "THE BEER-WARE LICENSE" (Revision 42):
# * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
# * can do whatever you want with this stuff. If we meet some day, and you think
# * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp

# This port adds no further stipulations.  I forfeit any copyright interest.

try:
    from hashlib import md5
except ImportError:
    from md5 import md5  # deprecated with python 2.5


def md5crypt(password, salt, magic='$1$'):
    # /* The password first, since that is what is most unknown */ /* Then our magic string */ /* Then the raw salt */
    m = md5()
    m.update(password + magic + salt)

    # /* Then just as many characters of the MD5(pw,salt,pw) */
    mixin = md5(password + salt + password).digest()
    for i in range(0, len(password)):
        m.update(mixin[i % 16])

    # /* Then something really weird... */
    # Also really broken, as far as I can tell.  -m
    i = len(password)
    while i:
        if i & 1:
            m.update('\x00')
        else:
            m.update(password[0])
        i >>= 1

    final = m.digest()

    # /* and now, just to make sure things don't run too fast */
    for i in range(1000):
        m2 = md5()
        if i & 1:
            m2.update(password)
        else:
            m2.update(final)

        if i % 3:
            m2.update(salt)

        if i % 7:
            m2.update(password)

        if i & 1:
            m2.update(final)
        else:
            m2.update(password)

        final = m2.digest()

    # This is the bit that uses to64() in the original code.

    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

    rearranged = ''
    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
        v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c])
        for i in range(4):
            rearranged += itoa64[v & 0x3f]
            v >>= 6

    v = ord(final[11])
    for i in range(2):
        rearranged += itoa64[v & 0x3f]
        v >>= 6

    return magic + salt + '$' + rearranged


# -----------------------------------------------------------------------------

#.
#   .--Main----------------------------------------------------------------.
#   |                        __  __       _                                |
#   |                       |  \/  | __ _(_)_ __                           |
#   |                       | |\/| |/ _` | | '_ \                          |
#   |                       | |  | | (_| | | | | |                         |
#   |                       |_|  |_|\__,_|_|_| |_|                         |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Main entry point                                                    |
#   '----------------------------------------------------------------------'

# Handle global options. We might convert this to getopt
# later. But a problem here is that we have options appearing
# *before* the command and command specific ones. We handle
# the options before the command here only

main_args = sys.argv[1:]

while len(main_args) >= 1 and main_args[0].startswith("-"):
    opt = main_args[0]
    main_args = main_args[1:]
    if opt.startswith("--"):
        handle_global_option(opt[2:], opt)
    else:
        for c in opt[1:]:
            handle_global_option(c, opt)

if len(main_args) < 1:
    main_help()
    sys.exit(1)

command = main_args[0]
args = main_args[1:]
found = False
for c, only_root, no_suid, needs_site, site_must_exist, confirm, argumentlist, \
    command_function, option_spec, description, confirm_text in commands:
    if c == command:
        found = True
        break

if not found:
    sys.stderr.write("omd: no such command: %s\n" % command)
    main_help()
    sys.exit(1)

if os.getuid() != 0 and only_root:
    bail_out("omd: root permissions are needed for this command.")

# Parse command options. We need to do this now in order to know,
# if a site name has been specified or not
args, command_options = parse_command_options(args, option_spec)

# Some commands need a site to be specified. If we are
# called as root, this must be done explicitely. If we
# are site user, the site name is our user name
g_sitename = None
if needs_site > 0:
    if os.getuid() == 0:
        if len(args) >= 1:
            g_sitename = args[0]
            args = args[1:]
        elif needs_site == 1:
            bail_out("omd: please specify site.")
    else:
        g_sitename = site_name()

check_site_user(site_must_exist)

# Commands operating on an existing site *must* run omd in
# the same version as the site has! Sole exception: update.
# That command must be run in the target version
if g_sitename and site_must_exist and command != "update":
    v = site_version(g_sitename)
    if v == None:  # Site has no homedirectory
        if command == "rm":
            sys.stdout.write(
                "WARNING: This site has an empty home directory and is not\n"
                "assigned to any OMD version. You are running version %s.\n" % OMD_VERSION)
        elif command != "init":
            bail_out("This site has an empty home directory /omd/sites/%s.\n"
                     "If you have created that site with 'omd create --no-init %s'\n"
                     "then please first do an 'omd init %s'." % (3 * (g_sitename,)))
    elif OMD_VERSION != v:
        exec_other_omd(v)

read_info()

if g_sitename:
    set_site_globals()

# Commands which affect a site and can be called as root *or* as
# site user should always run with site user priviledges. That way
# we are sure that new files and processes are created under the
# site user and never as root.
try:
    g_orig_wd = os.getcwd()
except OSError, e:
    if e.errno == 2:
        g_orig_wd = "/"
    else:
        raise

if not no_suid and g_sitename and os.getuid() == 0 and not only_root:
    switch_to_site_user()

# Make sure environment is in a defined state
if g_sitename:
    clear_environment()
    set_environment()

if (opt_interactive or confirm) and not opt_force:
    sys.stdout.write("%s (yes/NO): " % confirm_text)
    sys.stdout.flush()
    a = sys.stdin.readline().strip()
    if a.lower() != "yes":
        sys.exit(0)

try:
    command_function(args, command_options)
except KeyboardInterrupt:
    bail_out(tty_normal + "Aborted.")
