#!/usr/bin/env python

# Copyright 2011 OpenStack LLC.
# Copyright 2011 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

#
# XenAPI plugin for reading/writing information to xenstore
#

try:
    import json
except ImportError:
    import simplejson as json
import os
import random
import re
import subprocess
import tempfile
import time

import XenAPIPlugin
import pluginlib_nova as pluginlib


pluginlib.configure_logging("xenhost")

host_data_pattern = re.compile(r"\s*(\S+) \([^\)]+\) *: ?(.*)")
config_file_path = "/usr/etc/xenhost.conf"


def jsonify(fnc):
    def wrapper(*args, **kwargs):
        return json.dumps(fnc(*args, **kwargs))
    return wrapper


class TimeoutError(StandardError):
    pass


def _run_command(cmd):
    """Abstracts out the basics of issuing system commands. If the command
    returns anything in stderr, a PluginError is raised with that information.
    Otherwise, the output from stdout is returned.
    """
    pipe = subprocess.PIPE
    proc = subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe,
            stderr=pipe, close_fds=True)
    proc.wait()
    err = proc.stderr.read()
    if err:
        raise pluginlib.PluginError(err)
    return proc.stdout.read()


def _get_host_uuid():
    cmd = "xe host-list | grep uuid"
    resp = _run_command(cmd)
    return resp.split(":")[-1].strip()


@jsonify
def set_host_enabled(self, arg_dict):
    """Sets this host's ability to accept new instances.
    It will otherwise continue to operate normally.
    """
    enabled = arg_dict.get("enabled")
    if enabled is None:
        raise pluginlib.PluginError(
                _("Missing 'enabled' argument to set_host_enabled"))
    if enabled == "true":
        result = _run_command("xe host-enable")
    elif enabled == "false":
        result = _run_command("xe host-disable")
    else:
        raise pluginlib.PluginError(_("Illegal enabled status: %s") % enabled)
    # Should be empty string
    if result:
        raise pluginlib.PluginError(result)
    # Return the current enabled status
    host_uuid = _get_host_uuid()
    cmd = "xe host-param-list uuid=%s | grep enabled" % host_uuid
    resp = _run_command(cmd)
    # Response should be in the format: "enabled ( RO): true"
    host_enabled = resp.strip().split()[-1]
    if host_enabled == "true":
        status = "enabled"
    else:
        status = "disabled"
    return {"status": status}


def _write_config_dict(dct):
    conf_file = file(config_file_path, "w")
    json.dump(dct, conf_file)
    conf_file.close()


def _get_config_dict():
    """Returns a dict containing the key/values in the config file.
    If the file doesn't exist, it is created, and an empty dict
    is returned.
    """
    try:
        conf_file = file(config_file_path)
        config_dct = json.load(conf_file)
        conf_file.close()
    except IOError:
        # File doesn't exist
        config_dct = {}
        # Create the file
        _write_config_dict(config_dct)
    return config_dct


@jsonify
def get_config(self, arg_dict):
    """Return the value stored for the specified key, or None if no match."""
    conf = _get_config_dict()
    params = arg_dict["params"]
    try:
        dct = json.loads(params)
    except Exception, e:
        dct = params
    key = dct["key"]
    ret = conf.get(key)
    if ret is None:
        # Can't jsonify None
        return "None"
    return ret


@jsonify
def set_config(self, arg_dict):
    """Write the specified key/value pair, overwriting any existing value."""
    conf = _get_config_dict()
    params = arg_dict["params"]
    try:
        dct = json.loads(params)
    except Exception, e:
        dct = params
    key = dct["key"]
    val = dct["value"]
    if val is None:
        # Delete the key, if present
        conf.pop(key, None)
    else:
        conf.update({key: val})
    _write_config_dict(conf)


def _power_action(action):
    host_uuid = _get_host_uuid()
    # Host must be disabled first
    result = _run_command("xe host-disable")
    if result:
        raise pluginlib.PluginError(result)
    # All running VMs must be shutdown
    result = _run_command("xe vm-shutdown --multiple power-state=running")
    if result:
        raise pluginlib.PluginError(result)
    cmds = {"reboot": "xe host-reboot", "startup": "xe host-power-on",
            "shutdown": "xe host-shutdown"}
    result = _run_command(cmds[action])
    # Should be empty string
    if result:
        raise pluginlib.PluginError(result)
    return {"power_action": action}


@jsonify
def host_reboot(self, arg_dict):
    """Reboots the host."""
    return _power_action("reboot")


@jsonify
def host_shutdown(self, arg_dict):
    """Reboots the host."""
    return _power_action("shutdown")


@jsonify
def host_start(self, arg_dict):
    """Starts the host. Currently not feasible, since the host
    runs on the same machine as Xen.
    """
    return _power_action("startup")


@jsonify
def host_data(self, arg_dict):
    """Runs the commands on the xenstore host to return the current status
    information.
    """
    host_uuid = _get_host_uuid()
    cmd = "xe host-param-list uuid=%s" % host_uuid
    resp = _run_command(cmd)
    parsed_data = parse_response(resp)
    # We have the raw dict of values. Extract those that we need,
    # and convert the data types as needed.
    ret_dict = cleanup(parsed_data)
    # Add any config settings
    config = _get_config_dict()
    ret_dict.update(config)
    return ret_dict


def parse_response(resp):
    data = {}
    for ln in resp.splitlines():
        if not ln:
            continue
        mtch = host_data_pattern.match(ln.strip())
        try:
            k, v = mtch.groups()
            data[k] = v
        except AttributeError:
            # Not a valid line; skip it
            continue
    return data


def cleanup(dct):
    """Take the raw KV pairs returned and translate them into the
    appropriate types, discarding any we don't need.
    """
    def safe_int(val):
        """Integer values will either be string versions of numbers,
        or empty strings. Convert the latter to nulls.
        """
        try:
            return int(val)
        except ValueError:
            return None

    def strip_kv(ln):
        return [val.strip() for val in ln.split(":", 1)]

    out = {}

#    sbs = dct.get("supported-bootloaders", "")
#    out["host_supported-bootloaders"] = sbs.split("; ")
#    out["host_suspend-image-sr-uuid"] = dct.get("suspend-image-sr-uuid", "")
#    out["host_crash-dump-sr-uuid"] = dct.get("crash-dump-sr-uuid", "")
#    out["host_local-cache-sr"] = dct.get("local-cache-sr", "")
    out["host_memory"] = omm = {}
    omm["total"] = safe_int(dct.get("memory-total", ""))
    omm["overhead"] = safe_int(dct.get("memory-overhead", ""))
    omm["free"] = safe_int(dct.get("memory-free", ""))
    omm["free-computed"] = safe_int(
            dct.get("memory-free-computed", ""))

#    out["host_API-version"] = avv = {}
#    avv["vendor"] = dct.get("API-version-vendor", "")
#    avv["major"] = safe_int(dct.get("API-version-major", ""))
#    avv["minor"] = safe_int(dct.get("API-version-minor", ""))

    out["host_uuid"] = dct.get("uuid", None)
    out["host_name-label"] = dct.get("name-label", "")
    out["host_name-description"] = dct.get("name-description", "")
#    out["host_host-metrics-live"] = dct.get(
#            "host-metrics-live", "false") == "true"
    out["host_hostname"] = dct.get("hostname", "")
    out["host_ip_address"] = dct.get("address", "")
    oc = dct.get("other-config", "")
    out["host_other-config"] = ocd = {}
    if oc:
        for oc_fld in oc.split("; "):
            ock, ocv = strip_kv(oc_fld)
            ocd[ock] = ocv
#    out["host_capabilities"] = dct.get("capabilities", "").split("; ")
#    out["host_allowed-operations"] = dct.get(
#            "allowed-operations", "").split("; ")
#    lsrv = dct.get("license-server", "")
#    out["host_license-server"] = ols = {}
#    if lsrv:
#        for lspart in lsrv.split("; "):
#            lsk, lsv = lspart.split(": ")
#            if lsk == "port":
#                ols[lsk] = safe_int(lsv)
#            else:
#                ols[lsk] = lsv
#    sv = dct.get("software-version", "")
#    out["host_software-version"] = osv = {}
#    if sv:
#        for svln in sv.split("; "):
#            svk, svv = strip_kv(svln)
#            osv[svk] = svv
    cpuinf = dct.get("cpu_info", "")
    out["host_cpu_info"] = ocp = {}
    if cpuinf:
        for cpln in cpuinf.split("; "):
            cpk, cpv = strip_kv(cpln)
            if cpk in ("cpu_count", "family", "model", "stepping"):
                ocp[cpk] = safe_int(cpv)
            else:
                ocp[cpk] = cpv
#    out["host_edition"] = dct.get("edition", "")
#    out["host_external-auth-service-name"] = dct.get(
#            "external-auth-service-name", "")
    return out


if __name__ == "__main__":
    XenAPIPlugin.dispatch(
            {"host_data": host_data,
            "set_host_enabled": set_host_enabled,
            "host_shutdown": host_shutdown,
            "host_reboot": host_reboot,
            "host_start": host_start,
            "get_config": get_config,
            "set_config": set_config})
