240 lines
8.3 KiB
Python
Executable File
240 lines
8.3 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# Copyright (c) 2011 Citrix Systems, Inc.
|
|
# 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
|
|
#
|
|
|
|
import base64
|
|
import commands
|
|
try:
|
|
import json
|
|
except ImportError:
|
|
import simplejson as json
|
|
import os
|
|
import random
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
|
|
import XenAPIPlugin
|
|
|
|
from pluginlib_nova import *
|
|
configure_logging("xenstore")
|
|
import xenstore
|
|
|
|
AGENT_TIMEOUT = 30
|
|
|
|
|
|
def jsonify(fnc):
|
|
def wrapper(*args, **kwargs):
|
|
return json.dumps(fnc(*args, **kwargs))
|
|
return wrapper
|
|
|
|
|
|
class TimeoutError(StandardError):
|
|
pass
|
|
|
|
|
|
def version(self, arg_dict):
|
|
"""Get version of agent."""
|
|
arg_dict["value"] = json.dumps({"name": "version", "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)
|
|
except TimeoutError, e:
|
|
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.
|
|
"""
|
|
pub = int(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)
|
|
except TimeoutError, e:
|
|
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.
|
|
"""
|
|
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)
|
|
except TimeoutError, e:
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
@jsonify
|
|
def resetnetwork(self, arg_dict):
|
|
"""Writes a resquest to xenstore that tells the agent
|
|
to reset networking.
|
|
"""
|
|
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)
|
|
|
|
|
|
@jsonify
|
|
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.
|
|
"""
|
|
b64_path = arg_dict["b64_path"]
|
|
b64_file = arg_dict["b64_file"]
|
|
request_id = arg_dict["id"]
|
|
if self._agent_has_method("file_inject"):
|
|
# 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 self._agent_has_method("injectfile"):
|
|
# 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)
|
|
except TimeoutError, e:
|
|
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."""
|
|
request_id = arg_dict["id"]
|
|
url = arg_dict["url"]
|
|
md5sum = arg_dict["md5sum"]
|
|
arg_dict["value"] = json.dumps({"name": "agentupdate",
|
|
"value": {"url": url, "md5sum": 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)
|
|
except TimeoutError, e:
|
|
raise PluginError(e)
|
|
return resp
|
|
|
|
|
|
def _agent_has_method(self, method):
|
|
"""Check that the agent has a particular method by checking its
|
|
features. Cache the features so we don't have to query the agent
|
|
every time we need to check.
|
|
"""
|
|
try:
|
|
self._agent_methods
|
|
except AttributeError:
|
|
self._agent_methods = []
|
|
if not self._agent_methods:
|
|
# Haven't been defined
|
|
tmp_id = commands.getoutput("uuidgen")
|
|
dct = {}
|
|
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)
|
|
except TimeoutError, e:
|
|
raise PluginError(e)
|
|
response = json.loads(resp)
|
|
# The agent returns a comma-separated list of methods.
|
|
self._agent_methods = response.split(",")
|
|
return method in self._agent_methods
|
|
|
|
|
|
def _wait_for_agent(self, request_id, arg_dict):
|
|
"""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 time specified by
|
|
AGENT_TIMEOUT, the original request is deleted and a TimeoutError
|
|
is returned.
|
|
"""
|
|
arg_dict["path"] = "data/guest/%s" % request_id
|
|
arg_dict["ignore_missing_path"] = True
|
|
start = time.time()
|
|
while True:
|
|
if time.time() - start > AGENT_TIMEOUT:
|
|
# 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)
|
|
raise TimeoutError(_("TIMEOUT: No response from agent within"
|
|
" %s seconds.") % AGENT_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
|
|
else:
|
|
time.sleep(3)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
XenAPIPlugin.dispatch(
|
|
{"version": version,
|
|
"key_init": key_init,
|
|
"password": password,
|
|
"resetnetwork": resetnetwork,
|
|
"inject_file": inject_file,
|
|
"agentupdate": agent_update})
|