267 lines
9.5 KiB
Python
Executable File
267 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2011 Citrix Systems, Inc.
|
|
# Copyright 2011 OpenStack Foundation
|
|
# 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.
|
|
|
|
# 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
|
|
|
|
# TODO(sfinucan): Resolve all 'noqa' items once the above is no longer true
|
|
|
|
#
|
|
# XenAPI plugin for reading/writing information to xenstore
|
|
#
|
|
|
|
import base64
|
|
import commands # noqa
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
import time
|
|
|
|
import XenAPIPlugin
|
|
|
|
import pluginlib_nova
|
|
pluginlib_nova.configure_logging("agent")
|
|
import xenstore
|
|
|
|
|
|
DEFAULT_TIMEOUT = 30
|
|
PluginError = pluginlib_nova.PluginError
|
|
_ = pluginlib_nova._
|
|
|
|
|
|
class TimeoutError(StandardError):
|
|
pass
|
|
|
|
|
|
class RebootDetectedError(StandardError):
|
|
pass
|
|
|
|
|
|
def version(self, arg_dict):
|
|
"""Get version of agent."""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
arg_dict["value"] = json.dumps({"name": "version", "value": "agent"})
|
|
request_id = arg_dict["id"]
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def key_init(self, arg_dict):
|
|
"""Handles the Diffie-Hellman key exchange with the agent to
|
|
establish the shared secret key used to encrypt/decrypt sensitive
|
|
info to be passed, such as passwords. Returns the shared
|
|
secret key value.
|
|
"""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
# WARNING: Some older Windows agents will crash if the public key isn't
|
|
# a string
|
|
pub = arg_dict["pub"]
|
|
arg_dict["value"] = json.dumps({"name": "keyinit", "value": pub})
|
|
request_id = arg_dict["id"]
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def password(self, arg_dict):
|
|
"""Writes a request to xenstore that tells the agent to set
|
|
the root password for the given VM. The password should be
|
|
encrypted using the shared secret key that was returned by a
|
|
previous call to key_init. The encrypted password value should
|
|
be passed as the value for the 'enc_pass' key in arg_dict.
|
|
"""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
enc_pass = arg_dict["enc_pass"]
|
|
arg_dict["value"] = json.dumps({"name": "password", "value": enc_pass})
|
|
request_id = arg_dict["id"]
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def resetnetwork(self, arg_dict):
|
|
"""Writes a request to xenstore that tells the agent
|
|
to reset networking.
|
|
"""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
arg_dict['value'] = json.dumps({'name': 'resetnetwork', 'value': ''})
|
|
request_id = arg_dict['id']
|
|
arg_dict['path'] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def inject_file(self, arg_dict):
|
|
"""Expects a file path and the contents of the file to be written. Both
|
|
should be base64-encoded in order to eliminate errors as they are passed
|
|
through the stack. Writes that information to xenstore for the agent,
|
|
which will decode the file and intended path, and create it on the
|
|
instance. The original agent munged both of these into a single entry;
|
|
the new agent keeps them separate. We will need to test for the new agent,
|
|
and write the xenstore records to match the agent version. We will also
|
|
need to test to determine if the file injection method on the agent has
|
|
been disabled, and raise a NotImplemented error if that is the case.
|
|
"""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
b64_path = arg_dict["b64_path"]
|
|
b64_file = arg_dict["b64_contents"]
|
|
request_id = arg_dict["id"]
|
|
agent_features = _get_agent_features(self, arg_dict)
|
|
if "file_inject" in agent_features:
|
|
# New version of the agent. Agent should receive a 'value'
|
|
# key whose value is a dictionary containing 'b64_path' and
|
|
# 'b64_file'. See old version below.
|
|
arg_dict["value"] = json.dumps({"name": "file_inject",
|
|
"value": {"b64_path": b64_path, "b64_file": b64_file}})
|
|
elif "injectfile" in agent_features:
|
|
# Old agent requires file path and file contents to be
|
|
# combined into one base64 value.
|
|
raw_path = base64.b64decode(b64_path)
|
|
raw_file = base64.b64decode(b64_file)
|
|
new_b64 = base64.b64encode("%s,%s" % (raw_path, raw_file))
|
|
arg_dict["value"] = json.dumps({"name": "injectfile",
|
|
"value": new_b64})
|
|
else:
|
|
# Either the methods don't exist in the agent, or they
|
|
# have been disabled.
|
|
raise NotImplementedError(_("NOT IMPLEMENTED: Agent does not"
|
|
" support file injection."))
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def agent_update(self, arg_dict):
|
|
"""Expects an URL and md5sum of the contents, then directs the agent to
|
|
update itself.
|
|
"""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
request_id = arg_dict["id"]
|
|
url = arg_dict["url"]
|
|
md5sum = arg_dict["md5sum"]
|
|
arg_dict["value"] = json.dumps({"name": "agentupdate",
|
|
"value": "%s,%s" % (url, md5sum)})
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.write_record(self, arg_dict)
|
|
try:
|
|
resp = _wait_for_agent(self, request_id, arg_dict, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def _get_agent_features(self, arg_dict):
|
|
"""Return an array of features that an agent supports."""
|
|
timeout = int(arg_dict.pop('timeout', DEFAULT_TIMEOUT))
|
|
tmp_id = commands.getoutput("uuidgen")
|
|
dct = {}
|
|
dct.update(arg_dict)
|
|
dct["value"] = json.dumps({"name": "features", "value": ""})
|
|
dct["path"] = "data/host/%s" % tmp_id
|
|
xenstore.write_record(self, dct)
|
|
try:
|
|
resp = _wait_for_agent(self, tmp_id, dct, timeout)
|
|
except TimeoutError, e: # noqa
|
|
raise PluginError(e)
|
|
response = json.loads(resp)
|
|
if response['returncode'] != 0:
|
|
return response["message"].split(",")
|
|
else:
|
|
return {}
|
|
|
|
|
|
def _wait_for_agent(self, request_id, arg_dict, timeout):
|
|
"""Periodically checks xenstore for a response from the agent.
|
|
The request is always written to 'data/host/{id}', and
|
|
the agent's response for that request will be in 'data/guest/{id}'.
|
|
If no value appears from the agent within the timeout specified,
|
|
the original request is deleted and a TimeoutError is raised.
|
|
"""
|
|
arg_dict["path"] = "data/guest/%s" % request_id
|
|
arg_dict["ignore_missing_path"] = True
|
|
start = time.time()
|
|
reboot_detected = False
|
|
while time.time() - start < timeout:
|
|
ret = xenstore.read_record(self, arg_dict)
|
|
# Note: the response for None with be a string that includes
|
|
# double quotes.
|
|
if ret != '"None"':
|
|
# The agent responded
|
|
return ret
|
|
|
|
time.sleep(.5)
|
|
|
|
# NOTE(johngarbutt) If we can't find this domid, then
|
|
# the VM has rebooted, so we must trigger domid refresh.
|
|
# Check after the sleep to give xenstore time to update
|
|
# after the VM reboot.
|
|
exists_args = {
|
|
"dom_id": arg_dict["dom_id"],
|
|
"path": "name",
|
|
}
|
|
dom_id_is_present = xenstore.record_exists(exists_args)
|
|
if not dom_id_is_present:
|
|
reboot_detected = True
|
|
break
|
|
|
|
# No response within the timeout period; bail out
|
|
# First, delete the request record
|
|
arg_dict["path"] = "data/host/%s" % request_id
|
|
xenstore.delete_record(self, arg_dict)
|
|
|
|
if reboot_detected:
|
|
raise RebootDetectedError(_("REBOOT: dom_id %s no longer "
|
|
"present") % arg_dict["dom_id"])
|
|
else:
|
|
raise TimeoutError(_("TIMEOUT: No response from agent within"
|
|
" %s seconds.") % timeout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
XenAPIPlugin.dispatch(
|
|
{"version": version,
|
|
"key_init": key_init,
|
|
"password": password,
|
|
"resetnetwork": resetnetwork,
|
|
"inject_file": inject_file,
|
|
"agentupdate": agent_update})
|