226 lines
7.3 KiB
Python
226 lines
7.3 KiB
Python
#!/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})
|