#!/usr/bin/env python

# Copyright (c) 2010 Citrix Systems, Inc.
# Copyright 2010 OpenStack Foundation
# Copyright 2010 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.

# NOTE: XenServer still only supports Python 2.4 in it's dom0 userspace
# which means the Nova xenapi plugins must use only Python 2.4 features

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

try:
    import json
except ImportError:
    import simplejson as json

import utils    # noqa

import XenAPIPlugin    # noqa

import dom0_pluginlib as pluginlib  # noqa
pluginlib.configure_logging("xenstore")


class XenstoreError(pluginlib.PluginError):
    """Errors that occur when calling xenstore-* through subprocesses."""

    def __init__(self, cmd, return_code, stderr, stdout):
        msg = "cmd: %s; returncode: %d; stderr: %s; stdout: %s"
        msg = msg % (cmd, return_code, stderr, stdout)
        self.cmd = cmd
        self.return_code = return_code
        self.stderr = stderr
        self.stdout = stdout
        pluginlib.PluginError.__init__(self, msg)


def jsonify(fnc):
    def wrapper(*args, **kwargs):
        ret = fnc(*args, **kwargs)
        try:
            json.loads(ret)
        except ValueError:
            # Value should already be JSON-encoded, but some operations
            # may write raw sting values; this will catch those and
            # properly encode them.
            ret = json.dumps(ret)
        return ret
    return wrapper


def record_exists(arg_dict):
    """Returns whether or not the given record exists.

    The record path is determined from the given path and dom_id in the
    arg_dict.
    """
    cmd = ["xenstore-exists", "/local/domain/%(dom_id)s/%(path)s" % arg_dict]
    try:
        _run_command(cmd)
        return True
    except XenstoreError as e:    # noqa
        if e.stderr == '':
            # if stderr was empty, this just means the path did not exist
            return False
        # otherwise there was a real problem
        raise


@jsonify
def read_record(self, arg_dict):
    """Returns the value stored at the given path for the given dom_id.

    These must be encoded as key/value pairs in arg_dict. You can
    optionally include a key 'ignore_missing_path'; if this is present
    and boolean True, attempting to read a non-existent path will return
    the string 'None' instead of raising an exception.
    """
    cmd = ["xenstore-read", "/local/domain/%(dom_id)s/%(path)s" % arg_dict]
    try:
        result = _run_command(cmd)
        return result.strip()
    except XenstoreError as e:    # noqa
        if not arg_dict.get("ignore_missing_path", False):
            raise
        if not record_exists(arg_dict):
            return "None"
        # Just try again in case the agent write won the race against
        # the record_exists check. If this fails again, it will likely raise
        # an equally meaningful XenstoreError as the one we just caught
        result = _run_command(cmd)
        return result.strip()


@jsonify
def write_record(self, arg_dict):
    """Writes to xenstore at the specified path.

    If there is information already stored in that location, it is overwritten.
    As in read_record, the dom_id and path must be specified in the arg_dict;
    additionally, you must specify a 'value' key, whose value must be a string.
    Typically, you can json-ify more complex values and store the json output.
    """
    cmd = ["xenstore-write",
           "/local/domain/%(dom_id)s/%(path)s" % arg_dict,
           arg_dict["value"]]
    _run_command(cmd)
    return arg_dict["value"]


@jsonify
def list_records(self, arg_dict):
    """Returns all stored data at or below the given path for the given dom_id.

    The data is returned as a json-ified dict, with the
    path as the key and the stored value as the value. If the path
    doesn't exist, an empty dict is returned.
    """
    dirpath = "/local/domain/%(dom_id)s/%(path)s" % arg_dict
    cmd = ["xenstore-ls", dirpath.rstrip("/")]
    try:
        recs = _run_command(cmd)
    except XenstoreError as e:    # noqa
        if not record_exists(arg_dict):
            return {}
        # Just try again in case the path was created in between
        # the "ls" and the existence check. If this fails again, it will
        # likely raise an equally meaningful XenstoreError
        recs = _run_command(cmd)
    base_path = arg_dict["path"]
    paths = _paths_from_ls(recs)
    ret = {}
    for path in paths:
        if base_path:
            arg_dict["path"] = "%s/%s" % (base_path, path)
        else:
            arg_dict["path"] = path
        rec = read_record(self, arg_dict)
        try:
            val = json.loads(rec)
        except ValueError:
            val = rec
        ret[path] = val
    return ret


@jsonify
def delete_record(self, arg_dict):
    """Just like it sounds:

    it removes the record for the specified VM and the specified path from
    xenstore.
    """
    cmd = ["xenstore-rm", "/local/domain/%(dom_id)s/%(path)s" % arg_dict]
    try:
        return _run_command(cmd)
    except XenstoreError as e:    # noqa
        if 'could not remove path' in e.stderr:
            # Entry already gone.  We're good to go.
            return ''
        raise


def _paths_from_ls(recs):
    """The xenstore-ls command returns a listing that isn't terribly useful.

    This method cleans that up into a dict with each path
    as the key, and the associated string as the value.
    """
    last_nm = ""
    level = 0
    path = []
    ret = []
    for ln in recs.splitlines():
        nm, val = ln.rstrip().split(" = ")
        barename = nm.lstrip()
        this_level = len(nm) - len(barename)
        if this_level == 0:
            ret.append(barename)
            level = 0
            path = []
        elif this_level == level:
            # child of same parent
            ret.append("%s/%s" % ("/".join(path), barename))
        elif this_level > level:
            path.append(last_nm)
            ret.append("%s/%s" % ("/".join(path), barename))
            level = this_level
        elif this_level < level:
            path = path[:this_level]
            ret.append("%s/%s" % ("/".join(path), barename))
            level = this_level
        last_nm = barename
    return ret


def _run_command(cmd):
    """Wrap utils.run_command to raise XenstoreError on failure"""
    try:
        return utils.run_command(cmd)
    except utils.SubprocessException as e:    # noqa
        raise XenstoreError(e.cmdline, e.ret, e.err, e.out)

if __name__ == "__main__":
    XenAPIPlugin.dispatch(
        {"read_record": read_record,
         "write_record": write_record,
         "list_records": list_records,
         "delete_record": delete_record})
