diff options
author | Jenkins <jenkins@review.openstack.org> | 2012-07-11 15:44:24 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2012-07-11 15:44:24 +0000 |
commit | 0c8d9c749a5d697c49ba45c08ba716c47809e2ab (patch) | |
tree | 6311f0f5bf42b3ff8693e6af1deafd9036727464 | |
parent | 8c4441822731933502e8a5e1149dc6e4888f8dcb (diff) | |
parent | 44db828e8c7bd5a0cdb794d35234d824261a4644 (diff) | |
download | nova-0c8d9c749a5d697c49ba45c08ba716c47809e2ab.tar.gz nova-0c8d9c749a5d697c49ba45c08ba716c47809e2ab.tar.xz nova-0c8d9c749a5d697c49ba45c08ba716c47809e2ab.zip |
Merge "Split xenapi agent code out to nova.virt.xenapi.agent"
-rw-r--r-- | nova/tests/test_xenapi.py | 5 | ||||
-rw-r--r-- | nova/virt/xenapi/agent.py | 253 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 216 |
3 files changed, 274 insertions, 200 deletions
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 301c78d69..b7caf63ba 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -40,6 +40,7 @@ from nova.tests import fake_network from nova.tests import fake_utils import nova.tests.image.fake as fake_image from nova.tests.xenapi import stubs +from nova.virt.xenapi import agent from nova.virt.xenapi import driver as xenapi_conn from nova.virt.xenapi import fake as xenapi_fake from nova.virt.xenapi import vm_utils @@ -862,8 +863,8 @@ class XenAPIDiffieHellmanTestCase(test.TestCase): """Unit tests for Diffie-Hellman code.""" def setUp(self): super(XenAPIDiffieHellmanTestCase, self).setUp() - self.alice = vmops.SimpleDH() - self.bob = vmops.SimpleDH() + self.alice = agent.SimpleDH() + self.bob = agent.SimpleDH() def test_shared(self): alice_pub = self.alice.get_public() diff --git a/nova/virt/xenapi/agent.py b/nova/virt/xenapi/agent.py new file mode 100644 index 000000000..33a778d9f --- /dev/null +++ b/nova/virt/xenapi/agent.py @@ -0,0 +1,253 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Citrix Systems, Inc. +# Copyright 2010-2012 OpenStack LLC. +# +# 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. + +import binascii +import os +import time +import uuid + +from nova import flags +from nova.openstack.common import cfg +from nova.openstack.common import jsonutils +from nova.openstack.common import log as logging +from nova import utils + + +LOG = logging.getLogger(__name__) + +xenapi_agent_opts = [ + cfg.IntOpt('agent_version_timeout', + default=300, + help='number of seconds to wait for agent ' + 'to be fully operational'), +] + +FLAGS = flags.FLAGS +FLAGS.register_opts(xenapi_agent_opts) + + +def _call_agent(session, instance, vm_ref, method, addl_args=None): + """Abstracts out the interaction with the agent xenapi plugin.""" + if addl_args is None: + addl_args = {} + + vm_rec = session.call_xenapi("VM.get_record", vm_ref) + + args = { + 'id': str(uuid.uuid4()), + 'dom_id': vm_rec['domid'], + } + args.update(addl_args) + + try: + ret = session.call_plugin('agent', method, args) + except session.XenAPI.Failure, e: + err_msg = e.details[-1].splitlines()[-1] + if 'TIMEOUT:' in err_msg: + LOG.error(_('TIMEOUT: The call to %(method)s timed out. ' + 'args=%(args)r'), locals(), instance=instance) + return {'returncode': 'timeout', 'message': err_msg} + elif 'NOT IMPLEMENTED:' in err_msg: + LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not' + ' supported by the agent. args=%(args)r'), + locals(), instance=instance) + return {'returncode': 'notimplemented', 'message': err_msg} + else: + LOG.error(_('The call to %(method)s returned an error: %(e)s. ' + 'args=%(args)r'), locals(), instance=instance) + return {'returncode': 'error', 'message': err_msg} + return None + + if isinstance(ret, dict): + return ret + try: + return jsonutils.loads(ret) + except TypeError: + LOG.error(_('The agent call to %(method)s returned an invalid' + ' response: %(ret)r. path=%(path)s; args=%(args)r'), + locals(), instance=instance) + return {'returncode': 'error', + 'message': 'unable to deserialize response'} + + +def _get_agent_version(session, instance, vm_ref): + resp = _call_agent(session, instance, vm_ref, 'version') + if resp['returncode'] != '0': + LOG.error(_('Failed to query agent version: %(resp)r'), + locals(), instance=instance) + return None + + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. + return resp['message'].replace('\\r\\n', '') + + +def get_agent_version(session, instance, vm_ref): + """Get the version of the agent running on the VM instance.""" + + # The agent can be slow to start for a variety of reasons. On Windows, + # it will generally perform a setup process on first boot that can + # take a couple of minutes and then reboot. On Linux, the system can + # also take a while to boot. So we need to be more patient than + # normal as well as watch for domid changes + + expiration = time.time() + FLAGS.agent_version_timeout + while time.time() < expiration: + ret = _get_agent_version(session, instance, vm_ref) + if ret: + return ret + + LOG.info(_('Reached maximum time attempting to query agent version'), + instance=instance) + + return None + + +def agent_update(session, instance, vm_ref, url, md5sum): + """Update agent on the VM instance.""" + + # Send the encrypted password + args = {'url': url, 'md5sum': md5sum} + resp = _call_agent(session, instance, vm_ref, 'agentupdate', args) + if resp['returncode'] != '0': + LOG.error(_('Failed to update agent: %(resp)r'), locals(), + instance=instance) + return None + return resp['message'] + + +def set_admin_password(session, instance, vm_ref, new_pass): + """Set the root/admin password on the VM instance. + + This is done via an agent running on the VM. Communication between nova + and the agent is done via writing xenstore records. Since communication + is done over the XenAPI RPC calls, we need to encrypt the password. + We're using a simple Diffie-Hellman class instead of a more advanced + library (such as M2Crypto) for compatibility with the agent code. + """ + dh = SimpleDH() + + # Exchange keys + args = {'pub': str(dh.get_public())} + resp = _call_agent(session, instance, vm_ref, 'key_init', args) + + # Successful return code from key_init is 'D0' + if resp['returncode'] != 'D0': + msg = _('Failed to exchange keys: %(resp)r') % locals() + LOG.error(msg, instance=instance) + raise Exception(msg) + + # Some old versions of the Windows agent have a trailing \\r\\n + # (ie CRLF escaped) for some reason. Strip that off. + agent_pub = int(resp['message'].replace('\\r\\n', '')) + dh.compute_shared(agent_pub) + + # Some old versions of Linux and Windows agent expect trailing \n + # on password to work correctly. + enc_pass = dh.encrypt(new_pass + '\n') + + # Send the encrypted password + args = {'enc_pass': enc_pass} + resp = _call_agent(session, instance, vm_ref, 'password', args) + + # Successful return code from password is '0' + if resp['returncode'] != '0': + msg = _('Failed to update password: %(resp)r') % locals() + LOG.error(msg, instance=instance) + raise Exception(msg) + + return resp['message'] + + +def inject_file(session, instance, vm_ref, path, contents): + # Files/paths must be base64-encoded for transmission to agent + b64_path = base64.b64encode(path) + b64_contents = base64.b64encode(contents) + + args = {'b64_path': b64_path, 'b64_contents': b64_contents} + + # If the agent doesn't support file injection, a NotImplementedError + # will be raised with the appropriate message. + resp = _call_agent(session, instance, vm_ref, 'inject_file', args) + if resp['returncode'] != '0': + LOG.error(_('Failed to inject file: %(resp)r'), locals(), + instance=instance) + return None + + return resp['message'] + + +def resetnetwork(session, instance, vm_ref): + """Calls resetnetwork method in agent.""" + _call_agent(session, instance, vm_ref, 'resetnetwork') + + +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. + """ + def __init__(self): + self._prime = 162259276829213363391578010288127 + self._base = 5 + self._public = None + self._shared = None + self.generate_private() + + def generate_private(self): + self._private = int(binascii.hexlify(os.urandom(10)), 16) + return self._private + + def get_public(self): + self._public = self.mod_exp(self._base, self._private, self._prime) + return self._public + + def compute_shared(self, other): + self._shared = self.mod_exp(other, self._private, self._prime) + return self._shared + + @staticmethod + def mod_exp(num, exp, mod): + """Efficient implementation of (num ** exp) % mod""" + result = 1 + while exp > 0: + if (exp & 1) == 1: + result = (result * num) % mod + exp = exp >> 1 + num = (num * num) % mod + return result + + def _run_ssl(self, text, decrypt=False): + cmd = ['openssl', 'aes-128-cbc', '-A', '-a', '-pass', + 'pass:%s' % self._shared, '-nosalt'] + if decrypt: + cmd.append('-d') + out, err = utils.execute(*cmd, process_input=text) + if err: + raise RuntimeError(_('OpenSSL error: %s') % err) + return out + + def encrypt(self, text): + return self._run_ssl(text).strip('\n') + + def decrypt(self, text): + return self._run_ssl(text, decrypt=True) diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 19c6c0b60..cf1d361f9 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -19,8 +19,6 @@ Management class for VM-related functions (spawn, reboot, etc). """ -import base64 -import binascii import cPickle as pickle import functools import os @@ -43,6 +41,7 @@ from nova.openstack.common import log as logging from nova.openstack.common import timeutils from nova import utils from nova.virt import driver +from nova.virt.xenapi import agent from nova.virt.xenapi import firewall from nova.virt.xenapi import network_utils from nova.virt.xenapi import vm_utils @@ -52,10 +51,6 @@ from nova.virt.xenapi import volume_utils LOG = logging.getLogger(__name__) xenapi_vmops_opts = [ - cfg.IntOpt('agent_version_timeout', - default=300, - help='number of seconds to wait for agent ' - 'to be fully operational'), cfg.IntOpt('xenapi_running_timeout', default=60, help='number of seconds to wait for instance ' @@ -508,7 +503,7 @@ class VMOps(object): # Update agent, if necessary # This also waits until the agent starts LOG.debug(_("Querying agent version"), instance=instance) - version = self._get_agent_version(instance) + version = agent.get_agent_version(self._session, instance, vm_ref) if version: LOG.info(_('Instance agent version: %s'), version, instance=instance) @@ -517,8 +512,8 @@ class VMOps(object): cmp_version(version, agent_build['version']) < 0): LOG.info(_('Updating Agent to %s'), agent_build['version'], instance=instance) - self._agent_update(instance, agent_build['url'], - agent_build['md5hash']) + agent.agent_update(self._session, instance, vm_ref, + agent_build['url'], agent_build['md5hash']) # if the guest agent is not available, configure the # instance, but skip the admin password configuration @@ -539,17 +534,19 @@ class VMOps(object): for path, contents in instance.injected_files: LOG.debug(_("Injecting file path: '%s'") % path, instance=instance) - self.inject_file(instance, path, contents) + agent.inject_file(self._session, instance, vm_ref, + path, contents) admin_password = instance.admin_pass # Set admin password, if necessary if admin_password and not no_agent: LOG.debug(_("Setting admin password"), instance=instance) - self.set_admin_password(instance, admin_password) + agent.set_admin_password(self._session, instance, vm_ref, + admin_password) # Reset network config LOG.debug(_("Resetting network"), instance=instance) - self.reset_network(instance, vm_ref) + agent.resetnetwork(self._session, instance, vm_ref) # Set VCPU weight inst_type = db.instance_type_get(ctx, instance.instance_type_id) @@ -871,118 +868,15 @@ class VMOps(object): return raise - def _get_agent_version(self, instance): - """Get the version of the agent running on the VM instance.""" - - # The agent can be slow to start for a variety of reasons. On Windows, - # it will generally perform a setup process on first boot that can - # take a couple of minutes and then reboot. On Linux, the system can - # also take a while to boot. So we need to be more patient than - # normal as well as watch for domid changes - - def _call(): - # Send the encrypted password - resp = self._make_agent_call('version', instance) - if resp['returncode'] != '0': - LOG.error(_('Failed to query agent version: %(resp)r'), - locals(), instance=instance) - return None - # Some old versions of the Windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off. - return resp['message'].replace('\\r\\n', '') - - vm_ref = self._get_vm_opaque_ref(instance) - vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) - - domid = vm_rec['domid'] - - expiration = time.time() + FLAGS.agent_version_timeout - while time.time() < expiration: - ret = _call() - if ret: - return ret - - vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) - if vm_rec['domid'] != domid: - newdomid = vm_rec['domid'] - LOG.info(_('domid changed from %(domid)s to %(newdomid)s'), - locals(), instance=instance) - domid = vm_rec['domid'] - - return None - - def _agent_update(self, instance, url, md5sum): - """Update agent on the VM instance.""" - - # Send the encrypted password - args = {'url': url, 'md5sum': md5sum} - resp = self._make_agent_call('agentupdate', instance, args) - if resp['returncode'] != '0': - LOG.error(_('Failed to update agent: %(resp)r'), locals(), - instance=instance) - return None - return resp['message'] - def set_admin_password(self, instance, new_pass): - """Set the root/admin password on the VM instance. - - This is done via an agent running on the VM. Communication between nova - and the agent is done via writing xenstore records. Since communication - is done over the XenAPI RPC calls, we need to encrypt the password. - We're using a simple Diffie-Hellman class instead of the more advanced - one in M2Crypto for compatibility with the agent code. - - """ - # The simple Diffie-Hellman class is used to manage key exchange. - dh = SimpleDH() - key_init_args = {'pub': str(dh.get_public())} - resp = self._make_agent_call('key_init', instance, key_init_args) - # Successful return code from key_init is 'D0' - if resp['returncode'] != 'D0': - msg = _('Failed to exchange keys: %(resp)r') % locals() - LOG.error(msg, instance=instance) - raise Exception(msg) - # Some old versions of the Windows agent have a trailing \\r\\n - # (ie CRLF escaped) for some reason. Strip that off. - agent_pub = int(resp['message'].replace('\\r\\n', '')) - dh.compute_shared(agent_pub) - # Some old versions of Linux and Windows agent expect trailing \n - # on password to work correctly. - enc_pass = dh.encrypt(new_pass + '\n') - # Send the encrypted password - password_args = {'enc_pass': enc_pass} - resp = self._make_agent_call('password', instance, password_args) - # Successful return code from password is '0' - if resp['returncode'] != '0': - msg = _('Failed to update password: %(resp)r') % locals() - LOG.error(msg, instance=instance) - raise Exception(msg) - return resp['message'] + """Set the root/admin password on the VM instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + agent.set_admin_password(self._session, instance, vm_ref, new_pass) def inject_file(self, instance, path, contents): - """Write a file to the VM instance. - - The path to which it is to be written and the contents of the file - need to be supplied; both will be base64-encoded to prevent errors - with non-ASCII characters being transmitted. If the agent does not - support file injection, or the user has disabled it, a - NotImplementedError will be raised. - - """ - # Files/paths must be base64-encoded for transmission to agent - b64_path = base64.b64encode(path) - b64_contents = base64.b64encode(contents) - - # Need to uniquely identify this request. - args = {'b64_path': b64_path, 'b64_contents': b64_contents} - # If the agent doesn't support file injection, a NotImplementedError - # will be raised with the appropriate message. - resp = self._make_agent_call('inject_file', instance, args) - if resp['returncode'] != '0': - LOG.error(_('Failed to inject file: %(resp)r'), locals(), - instance=instance) - return None - return resp['message'] + """Write a file to the VM instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + agent.inject_file(self._session, instance, vm_ref, path, contents) def _shutdown(self, instance, vm_ref, hard=True): """Shutdown an instance.""" @@ -1484,9 +1378,10 @@ class VMOps(object): for vif in network_info: self.vif_driver.unplug(instance, vif) - def reset_network(self, instance, vm_ref=None): + def reset_network(self, instance): """Calls resetnetwork method in agent.""" - self._make_agent_call('resetnetwork', instance, vm_ref=vm_ref) + vm_ref = self._get_vm_opaque_ref(instance) + agent.resetnetwork(self._session, instance, vm_ref) def inject_hostname(self, instance, vm_ref, hostname): """Inject the hostname of the instance into the xenstore.""" @@ -1507,24 +1402,6 @@ class VMOps(object): vm_ref=vm_ref, path=path, value=jsonutils.dumps(value)) - def _make_agent_call(self, method, instance, args=None, vm_ref=None): - """Abstracts out the interaction with the agent xenapi plugin.""" - if args is None: - args = {} - args['id'] = str(uuid.uuid4()) - ret = self._make_plugin_call('agent', method, instance, vm_ref=vm_ref, - **args) - if isinstance(ret, dict): - return ret - try: - return jsonutils.loads(ret) - except TypeError: - LOG.error(_('The agent call to %(method)s returned an invalid' - ' response: %(ret)r. path=%(path)s; args=%(args)r'), - locals(), instance=instance) - return {'returncode': 'error', - 'message': 'unable to deserialize response'} - def _make_plugin_call(self, plugin, method, instance, vm_ref=None, **addl_args): """ @@ -1586,60 +1463,3 @@ class VMOps(object): """Removes filters for each VIF of the specified instance.""" self.firewall_driver.unfilter_instance(instance_ref, network_info=network_info) - - -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. - """ - def __init__(self): - self._prime = 162259276829213363391578010288127 - self._base = 5 - self._public = None - self._shared = None - self.generate_private() - - def generate_private(self): - self._private = int(binascii.hexlify(os.urandom(10)), 16) - return self._private - - def get_public(self): - self._public = self.mod_exp(self._base, self._private, self._prime) - return self._public - - def compute_shared(self, other): - self._shared = self.mod_exp(other, self._private, self._prime) - return self._shared - - @staticmethod - def mod_exp(num, exp, mod): - """Efficient implementation of (num ** exp) % mod""" - result = 1 - while exp > 0: - if (exp & 1) == 1: - result = (result * num) % mod - exp = exp >> 1 - num = (num * num) % mod - return result - - def _run_ssl(self, text, decrypt=False): - cmd = ['openssl', 'aes-128-cbc', '-A', '-a', '-pass', - 'pass:%s' % self._shared, '-nosalt'] - if decrypt: - cmd.append('-d') - out, err = utils.execute(*cmd, process_input=text) - if err: - raise RuntimeError(_('OpenSSL error: %s') % err) - return out - - def encrypt(self, text): - return self._run_ssl(text).strip('\n') - - def decrypt(self, text): - return self._run_ssl(text, decrypt=True) |