From e66f3017373dcf9135c53ae4d510b0b2a5dcecf0 Mon Sep 17 00:00:00 2001 From: Ed Leafe Date: Thu, 6 Jan 2011 15:53:11 -0600 Subject: Got the basic 'set admin password' stuff working --- nova/api/openstack/servers.py | 17 +- nova/compute/manager.py | 3 - nova/exception.py | 4 + nova/virt/xenapi/vmops.py | 22 +- nova/virt/xenapi_conn.py | 16 +- plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 235 +++++++++++++++++++++ .../xenserver/xenapi/etc/xapi.d/plugins/agent.py | 221 ------------------- 7 files changed, 258 insertions(+), 260 deletions(-) create mode 100755 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent delete mode 100644 plugins/xenserver/xenapi/etc/xapi.d/plugins/agent.py diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index e0513e4c1..bc89f696c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -104,7 +104,8 @@ class Controller(wsgi.Controller): def show(self, req, id): """ Returns server details by server id """ try: - instance = self.compute_api.get_instance(req.environ['nova.context'], id) + instance = self.compute_api.get_instance( + req.environ['nova.context'], id) return _translate_detail_keys(instance) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -141,6 +142,7 @@ class Controller(wsgi.Controller): if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) + ctxt = req.environ['nova.context'] update_dict = {} func = None if 'adminPass' in inst_dict['server']: @@ -148,21 +150,18 @@ class Controller(wsgi.Controller): func = self.compute_api.set_admin_password if 'name' in inst_dict['server']: update_dict['display_name'] = inst_dict['server']['name'] - + if func: + try: + func(ctxt, id) + except exception.TimeoutException, e: + return exc.HTTPRequestTimeout() try: - ctxt = req.environ['nova.context'] # The ID passed in is actually the internal_id of the # instance, not the value of the id column in the DB. instance = self.compute_api.get_instance(ctxt, id) self.compute_api.update(ctxt, instance.id, **update_dict) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) - - logging.error("ZZZZ func=%s" % func) - logging.error("ZZZZ UPD=%s" % id) - - if func: - func(ctxt, id) return exc.HTTPNoContent() def action(self, req, id): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 201fffc68..52acfebea 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -83,7 +83,6 @@ class ComputeManager(manager.Manager): # FIXME(ja): include other fields from state? instance_ref = self.db.instance_get(context, instance_id) try: - #info = self.driver.get_info(instance_ref['name']) info = self.driver.get_info(instance_ref) state = info['state'] except exception.NotFound: @@ -266,8 +265,6 @@ class ComputeManager(manager.Manager): logging.debug('instance %s: setting admin password', instance_ref['name']) - self.db.instance_set_state(context, instance_id, - power_state.NOSTATE, 'setting_password') if new_pass is None: # Generate a random password new_pass = self._generate_password(FLAGS.password_length) diff --git a/nova/exception.py b/nova/exception.py index 277033e0f..52bf2a2a7 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -77,6 +77,10 @@ class InvalidInputException(Error): pass +class TimeoutException(Error): + pass + + def wrap_exception(f): def _wrap(*args, **kw): try: diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index e092601b9..5f1654a49 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -130,21 +130,17 @@ class VMOps(object): """Refactored out the common code of many methods that receive either a vm name or a vm instance, and want a vm instance in return. """ - logging.error("ZZZZ opaq instance_or_vm=%s" % instance_or_vm) vm = None try: if instance_or_vm.startswith("OpaqueRef:"): - logging.error("ZZZZ opaq startswith") # Got passed an opaque ref; return it return instance_or_vm else: # Must be the instance name - logging.error("ZZZZ opaq inst name") instance_name = instance_or_vm except AttributeError: # Not a string; must be a vm instance instance_name = instance_or_vm.name - logging.error("ZZZZ opaq instance, name=%s" % instance_name) vm = VMHelper.lookup(self._session, instance_name) if vm is None: raise Exception(_('Instance not present %s') % instance_name) @@ -210,9 +206,6 @@ class VMOps(object): simple Diffie-Hellman class instead of the more advanced one in M2Crypto for compatibility with the agent code. """ - - logging.error("ZZZZ SET PASS CALLED") - # Need to uniquely identify this request. transaction_id = str(uuid.uuid4()) # The simple Diffie-Hellman class is used to manage key exchange. @@ -306,9 +299,6 @@ class VMOps(object): def get_info(self, instance): """Return data about VM instance""" - - logging.error("ZZZZ get_info instance=%s" % instance) - vm = self._get_vm_opaque_ref(instance) rec = self._session.get_xenapi().VM.get_record(vm) return VMHelper.compile_info(rec) @@ -370,13 +360,14 @@ class VMOps(object): def _make_agent_call(self, method, vm, path, addl_args={}): """Abstracts out the interaction with the agent xenapi plugin.""" - return self._make_plugin_call('agent.py', method=method, vm=vm, + return self._make_plugin_call('agent', method=method, vm=vm, path=path, addl_args=addl_args) def _make_plugin_call(self, plugin, method, vm, path, addl_args={}): """Abstracts out the process of calling a method of a xenapi plugin. Any errors raised by the plugin will in turn raise a RuntimeError here. """ + instance_id = vm.id vm = self._get_vm_opaque_ref(vm) rec = self._session.get_xenapi().VM.get_record(vm) args = {'dom_id': rec['domid'], 'path': path} @@ -386,9 +377,14 @@ class VMOps(object): args['testing_mode'] = 'true' try: task = self._session.async_call_plugin(plugin, method, args) - ret = self._session.wait_for_task(0, task) + ret = self._session.wait_for_task(instance_id, task) except self.XenAPI.Failure, e: - raise RuntimeError("%s" % e.details[-1]) + err_trace = e.details[-1] + err_msg = err_trace.splitlines()[-1] + if 'TIMEOUT:' in err_msg: + raise exception.TimeoutException(err_msg) + else: + raise RuntimeError("%s" % e.details[-1]) return ret def add_to_xenstore(self, vm, path, key, value): diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 0ceac8c97..c9428e3a6 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -170,9 +170,6 @@ class XenAPIConnection(object): def get_info(self, instance_id): """Return data about VM instance""" - - logging.error("ZZZZ conn get_info id=%s" % instance_id) - return self._vmops.get_info(instance_id) def get_diagnostics(self, instance): @@ -251,7 +248,8 @@ class XenAPISession(object): def _poll_task(self, id, task, done): """Poll the given XenAPI task, and fire the given action if we - get a result.""" + get a result. + """ try: name = self._session.xenapi.task.get_name_label(task) status = self._session.xenapi.task.get_status(task) @@ -278,16 +276,6 @@ class XenAPISession(object): error_info)) done.send_exception(self.XenAPI.Failure(error_info)) db.instance_action_create(context.get_admin_context(), action) -# import sqlalchemy -# from sqlalchemy.exc import IntegrityError as IntegrityError -# try: -# db.instance_action_create(context.get_admin_context(), action) -# except IntegrityError: -# # Some methods don't pass unique IDs, so the call to -# # instance_action_create() will raise IntegrityErrors. Rather -# # than bomb out, I'm explicitly silencing them so that the -# # code can continue to work until they fix that method. -# pass except self.XenAPI.Failure, exc: logging.warn(exc) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent new file mode 100755 index 000000000..244509f3f --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -0,0 +1,235 @@ +#!/usr/bin/env python + +# Copyright (c) 2010 Citrix Systems, Inc. +# 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. + +# +# XenAPI plugin for reading/writing information to xenstore +# + +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 +# Used for simulating an external agent for testing +PRETEND_SECRET = 11111 + + +def jsonify(fnc): + def wrapper(*args, **kwargs): + return json.dumps(fnc(*args, **kwargs)) + return wrapper + + +class TimeoutError(StandardError): + pass + + +class SimpleDH(object): + """This class wraps all the functionality needed to implement + basic Diffie-Hellman-Merkle key exchange in Python. It features + intelligent defaults for the prime and base numbers needed for the + calculation, while allowing you to supply your own. It requires that + the openssl binary be installed on the system on which this is run, + as it uses that to handle the encryption and decryption. If openssl + is not available, a RuntimeError will be raised. + + Please note that nova already uses the M2Crypto library for most + cryptographic functions, and that it includes a Diffie-Hellman + implementation. However, that is a much more complex implementation, + and is not compatible with the DH algorithm that the agent uses. Hence + the need for this 'simple' version. + """ + def __init__(self, prime=None, base=None, secret=None): + """You can specify the values for prime and base if you wish; + otherwise, reasonable default values will be used. + """ + if prime is None: + self._prime = 162259276829213363391578010288127 + else: + self._prime = prime + if base is None: + self._base = 5 + else: + self._base = base + if secret is None: + self._secret = random.randint(5000, 15000) + else: + self._secret = secret + self._shared = self._public = None + + def get_public(self): + """Return the public key""" + self._public = (self._base ** self._secret) % self._prime + return self._public + + def compute_shared(self, other): + """Given the other end's public key, compute the + shared secret. + """ + self._shared = (other ** self._secret) % self._prime + return self._shared + + def _run_ssl(self, text, which): + """The encryption/decryption methods require running the openssl + installed on the system. This method abstracts out the common + code required. + """ + base_cmd = ("cat %(tmpfile)s | openssl enc -aes-128-cbc " + "-a -pass pass:%(shared)s -nosalt %(dec_flag)s") + if which.lower()[0] == "d": + dec_flag = " -d" + else: + dec_flag = "" + # Note: instead of using 'cat' and a tempfile, it is also + # possible to just 'echo' the value. However, we can not assume + # that the value is 'safe'; i.e., it may contain semi-colons, + # octothorpes, or other characters that would not be allowed + # in an 'echo' construct. + fd, tmpfile = tempfile.mkstemp() + os.close(fd) + file(tmpfile, "w").write(text) + shared = self._shared + cmd = base_cmd % locals() + try: + return _run_command(cmd) + except PluginError, e: + raise RuntimeError("OpenSSL error: %s" % e) + + def encrypt(self, text): + """Uses the shared key to encrypt the given text.""" + return self._run_ssl(text, "enc") + + def decrypt(self, text): + """Uses the shared key to decrypt the given text.""" + return self._run_ssl(text, "dec") + + +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 PluginError(err) + return proc.stdout.read() + + +@jsonify +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"] + if arg_dict.get("testing_mode"): + # Pretend! + pretend = SimpleDH(secret=PRETEND_SECRET) + shared = pretend.compute_shared(pub) + # Simulate the agent's response + ret = {"returncode": "D0", "message": "%s", "shared": "%s"} % (pretend.get_public(), shared) + return ret + 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("%s" % e) + return resp + + +@jsonify +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. + """ + pub = int(arg_dict["pub"]) + enc_pass = arg_dict["enc_pass"] + if arg_dict.get("testing_mode"): + # Decrypt the password, and send it back to verify + pretend = SimpleDH(secret=PRETEND_SECRET) + pretend.compute_shared(pub) + pw = pretend.decrypt(enc_pass) + ret = {"returncode": "0", "message": "%s"} % pw + return ret + 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("%s" % e) + return resp + + +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( + {"key_init": key_init, + "password": password}) diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent.py b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent.py deleted file mode 100644 index 4b072ce67..000000000 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2010 Citrix Systems, Inc. -# 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. - -# -# XenAPI plugin for reading/writing information to xenstore -# - -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 -# Used for simulating an external agent for testing -PRETEND_SECRET = 11111 - - -class TimeoutError(StandardError): - pass - -class SimpleDH(object): - """This class wraps all the functionality needed to implement - basic Diffie-Hellman-Merkle key exchange in Python. It features - intelligent defaults for the prime and base numbers needed for the - calculation, while allowing you to supply your own. It requires that - the openssl binary be installed on the system on which this is run, - as it uses that to handle the encryption and decryption. If openssl - is not available, a RuntimeError will be raised. - - Please note that nova already uses the M2Crypto library for most - cryptographic functions, and that it includes a Diffie-Hellman - implementation. However, that is a much more complex implementation, - and is not compatible with the DH algorithm that the agent uses. Hence - the need for this 'simple' version. - """ - def __init__(self, prime=None, base=None, secret=None): - """You can specify the values for prime and base if you wish; - otherwise, reasonable default values will be used. - """ - if prime is None: - self._prime = 162259276829213363391578010288127 - else: - self._prime = prime - if base is None: - self._base = 5 - else: - self._base = base - if secret is None: - self._secret = random.randint(5000, 15000) - else: - self._secret = secret - self._shared = self._public = None - - def get_public(self): - """Return the public key""" - self._public = (self._base ** self._secret) % self._prime - return self._public - - def compute_shared(self, other): - """Given the other end's public key, compute the - shared secret. - """ - self._shared = (other ** self._secret) % self._prime - return self._shared - - def _run_ssl(self, text, which): - """The encryption/decryption methods require running the openssl - installed on the system. This method abstracts out the common - code required. - """ - base_cmd = ("cat %(tmpfile)s | openssl enc -aes-128-cbc " - "-a -pass pass:%(shared)s -nosalt %(dec_flag)s") - if which.lower()[0] == "d": - dec_flag = " -d" - else: - dec_flag = "" - # Note: instead of using 'cat' and a tempfile, it is also - # possible to just 'echo' the value. However, we can not assume - # that the value is 'safe'; i.e., it may contain semi-colons, - # octothorpes, or other characters that would not be allowed - # in an 'echo' construct. - fd, tmpfile = tempfile.mkstemp() - os.close(fd) - file(tmpfile, "w").write(text) - shared = self._shared - cmd = base_cmd % locals() - try: - return _run_command(cmd) - except PluginError, e: - raise RuntimeError("OpenSSL error: %s" % e) - - def encrypt(self, text): - """Uses the shared key to encrypt the given text.""" - return self._run_ssl(text, "enc") - - def decrypt(self, text): - """Uses the shared key to decrypt the given text.""" - return self._run_ssl(text, "dec") - - -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 PluginError(err) - return proc.stdout.read() - -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"] - if arg_dict.get("testing_mode"): - # Pretend! - pretend = SimpleDH(secret=PRETEND_SECRET) - shared = pretend.compute_shared(pub) - # Simulate the agent's response - ret = '{ "returncode": "D0", "message": "%s", "shared": "%s" }' % (pretend.get_public(), shared) - return ret - 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("%s" % 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. - """ - pub = int(arg_dict["pub"]) - enc_pass = arg_dict["enc_pass"] - if arg_dict.get("testing_mode"): - # Decrypt the password, and send it back to verify - pretend = SimpleDH(secret=PRETEND_SECRET) - pretend.compute_shared(pub) - pw = pretend.decrypt(enc_pass) - ret = '{ "returncode": "0", "message": "%s" }' % pw - return ret - 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("%s" % e) - return resp - -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("No response from agent within %s seconds." % - AGENT_TIMEOUT) - ret = xenstore.read_record(self, arg_dict) - if ret != "None": - # The agent responded - return ret - else: - time.sleep(3) - - -if __name__ == "__main__": - XenAPIPlugin.dispatch( - {"key_init": key_init, - "password": password}) -- cgit