1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright (c) 2010 Citrix Systems, Inc.
4
# Copyright 2010-2012 OpenStack LLC.
6
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7
# not use this file except in compliance with the License. You may obtain
8
# a copy of the License at
10
# http://www.apache.org/licenses/LICENSE-2.0
12
# Unless required by applicable law or agreed to in writing, software
13
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15
# License for the specific language governing permissions and limitations
24
from nova import flags
25
from nova.openstack.common import cfg
26
from nova.openstack.common import jsonutils
27
from nova.openstack.common import log as logging
28
from nova import utils
31
LOG = logging.getLogger(__name__)
34
cfg.IntOpt('agent_version_timeout',
36
help='number of seconds to wait for agent '
37
'to be fully operational'),
41
FLAGS.register_opts(xenapi_agent_opts)
44
def _call_agent(session, instance, vm_ref, method, addl_args=None):
45
"""Abstracts out the interaction with the agent xenapi plugin."""
49
vm_rec = session.call_xenapi("VM.get_record", vm_ref)
52
'id': str(uuid.uuid4()),
53
'dom_id': vm_rec['domid'],
55
args.update(addl_args)
58
ret = session.call_plugin('agent', method, args)
59
except session.XenAPI.Failure, e:
60
err_msg = e.details[-1].splitlines()[-1]
61
if 'TIMEOUT:' in err_msg:
62
LOG.error(_('TIMEOUT: The call to %(method)s timed out. '
63
'args=%(args)r'), locals(), instance=instance)
64
return {'returncode': 'timeout', 'message': err_msg}
65
elif 'NOT IMPLEMENTED:' in err_msg:
66
LOG.error(_('NOT IMPLEMENTED: The call to %(method)s is not'
67
' supported by the agent. args=%(args)r'),
68
locals(), instance=instance)
69
return {'returncode': 'notimplemented', 'message': err_msg}
71
LOG.error(_('The call to %(method)s returned an error: %(e)s. '
72
'args=%(args)r'), locals(), instance=instance)
73
return {'returncode': 'error', 'message': err_msg}
76
if isinstance(ret, dict):
79
return jsonutils.loads(ret)
81
LOG.error(_('The agent call to %(method)s returned an invalid'
82
' response: %(ret)r. path=%(path)s; args=%(args)r'),
83
locals(), instance=instance)
84
return {'returncode': 'error',
85
'message': 'unable to deserialize response'}
88
def _get_agent_version(session, instance, vm_ref):
89
resp = _call_agent(session, instance, vm_ref, 'version')
90
if resp['returncode'] != '0':
91
LOG.error(_('Failed to query agent version: %(resp)r'),
92
locals(), instance=instance)
95
# Some old versions of the Windows agent have a trailing \\r\\n
96
# (ie CRLF escaped) for some reason. Strip that off.
97
return resp['message'].replace('\\r\\n', '')
100
def get_agent_version(session, instance, vm_ref):
101
"""Get the version of the agent running on the VM instance."""
103
LOG.debug(_('Querying agent version'), instance=instance)
105
# The agent can be slow to start for a variety of reasons. On Windows,
106
# it will generally perform a setup process on first boot that can
107
# take a couple of minutes and then reboot. On Linux, the system can
108
# also take a while to boot. So we need to be more patient than
109
# normal as well as watch for domid changes
111
expiration = time.time() + FLAGS.agent_version_timeout
112
while time.time() < expiration:
113
ret = _get_agent_version(session, instance, vm_ref)
117
LOG.info(_('Reached maximum time attempting to query agent version'),
123
def agent_update(session, instance, vm_ref, agent_build):
124
"""Update agent on the VM instance."""
126
LOG.info(_('Updating agent to %s'), agent_build['version'],
129
# Send the encrypted password
130
args = {'url': agent_build['url'], 'md5sum': agent_build['md5hash']}
131
resp = _call_agent(session, instance, vm_ref, 'agentupdate', args)
132
if resp['returncode'] != '0':
133
LOG.error(_('Failed to update agent: %(resp)r'), locals(),
136
return resp['message']
139
def set_admin_password(session, instance, vm_ref, new_pass):
140
"""Set the root/admin password on the VM instance.
142
This is done via an agent running on the VM. Communication between nova
143
and the agent is done via writing xenstore records. Since communication
144
is done over the XenAPI RPC calls, we need to encrypt the password.
145
We're using a simple Diffie-Hellman class instead of a more advanced
146
library (such as M2Crypto) for compatibility with the agent code.
148
LOG.debug(_('Setting admin password'), instance=instance)
153
args = {'pub': str(dh.get_public())}
154
resp = _call_agent(session, instance, vm_ref, 'key_init', args)
156
# Successful return code from key_init is 'D0'
157
if resp['returncode'] != 'D0':
158
msg = _('Failed to exchange keys: %(resp)r') % locals()
159
LOG.error(msg, instance=instance)
162
# Some old versions of the Windows agent have a trailing \\r\\n
163
# (ie CRLF escaped) for some reason. Strip that off.
164
agent_pub = int(resp['message'].replace('\\r\\n', ''))
165
dh.compute_shared(agent_pub)
167
# Some old versions of Linux and Windows agent expect trailing \n
168
# on password to work correctly.
169
enc_pass = dh.encrypt(new_pass + '\n')
171
# Send the encrypted password
172
args = {'enc_pass': enc_pass}
173
resp = _call_agent(session, instance, vm_ref, 'password', args)
175
# Successful return code from password is '0'
176
if resp['returncode'] != '0':
177
msg = _('Failed to update password: %(resp)r') % locals()
178
LOG.error(msg, instance=instance)
181
return resp['message']
184
def inject_file(session, instance, vm_ref, path, contents):
185
LOG.debug(_('Injecting file path: %r'), path, instance=instance)
187
# Files/paths must be base64-encoded for transmission to agent
188
b64_path = base64.b64encode(path)
189
b64_contents = base64.b64encode(contents)
191
args = {'b64_path': b64_path, 'b64_contents': b64_contents}
193
# If the agent doesn't support file injection, a NotImplementedError
194
# will be raised with the appropriate message.
195
resp = _call_agent(session, instance, vm_ref, 'inject_file', args)
196
if resp['returncode'] != '0':
197
LOG.error(_('Failed to inject file: %(resp)r'), locals(),
201
return resp['message']
204
def resetnetwork(session, instance, vm_ref):
205
LOG.debug(_('Resetting network'), instance=instance)
207
resp = _call_agent(session, instance, vm_ref, 'resetnetwork')
208
if resp['returncode'] != '0':
209
LOG.error(_('Failed to reset network: %(resp)r'), locals(),
213
return resp['message']
216
class SimpleDH(object):
218
This class wraps all the functionality needed to implement
219
basic Diffie-Hellman-Merkle key exchange in Python. It features
220
intelligent defaults for the prime and base numbers needed for the
221
calculation, while allowing you to supply your own. It requires that
222
the openssl binary be installed on the system on which this is run,
223
as it uses that to handle the encryption and decryption. If openssl
224
is not available, a RuntimeError will be raised.
227
self._prime = 162259276829213363391578010288127
231
self.generate_private()
233
def generate_private(self):
234
self._private = int(binascii.hexlify(os.urandom(10)), 16)
237
def get_public(self):
238
self._public = self.mod_exp(self._base, self._private, self._prime)
241
def compute_shared(self, other):
242
self._shared = self.mod_exp(other, self._private, self._prime)
246
def mod_exp(num, exp, mod):
247
"""Efficient implementation of (num ** exp) % mod"""
251
result = (result * num) % mod
253
num = (num * num) % mod
256
def _run_ssl(self, text, decrypt=False):
257
cmd = ['openssl', 'aes-128-cbc', '-A', '-a', '-pass',
258
'pass:%s' % self._shared, '-nosalt']
261
out, err = utils.execute(*cmd, process_input=text)
263
raise RuntimeError(_('OpenSSL error: %s') % err)
266
def encrypt(self, text):
267
return self._run_ssl(text).strip('\n')
269
def decrypt(self, text):
270
return self._run_ssl(text, decrypt=True)