Merge lp:~ed-leafe/nova/xenstore-plugin into lp:~hudson-openstack/nova/trunk

Proposed by Ed Leafe
Status: Merged
Approved by: Cory Wright
Approved revision: 513
Merged at revision: 517
Proposed branch: lp:~ed-leafe/nova/xenstore-plugin
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 644 lines (+410/-40)
4 files modified
nova/virt/xenapi/vmops.py (+193/-21)
nova/virt/xenapi_conn.py (+18/-10)
plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py (+19/-9)
plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py (+180/-0)
To merge this branch: bzr merge lp:~ed-leafe/nova/xenstore-plugin
Reviewer Review Type Date Requested Status
Josh Kearney (community) Approve
Cory Wright (community) Approve
Review via email: mp+44931@code.launchpad.net

Description of the change

Created a XenAPI plugin that will allow nova code to read/write/delete from xenstore records for a given instance. Added the basic methods for working with xenstore data to the vmops script, as well as plugin support to xenapi_conn.py

To post a comment you must log in.
lp:~ed-leafe/nova/xenstore-plugin updated
507. By Ed Leafe

removed some debugging code left in previous push.

508. By Ed Leafe

Corrected the sloppy import in the xenstore plugin that was copied from other plugins.

509. By Ed Leafe

fixed pep8 issues

510. By Ed Leafe

Added OpenStack's copyright to the xenstore plugin.

511. By Ed Leafe

merged in trunk changes

512. By Ed Leafe

Merged trunk changes

Revision history for this message
Cory Wright (corywright) wrote :

I've tested this and everything seems to work as advertised. Some feedback:

1) pep8:

$ pep8 -r xenstore.py
xenstore.py:39:66: W291 trailing whitespace
xenstore.py:40:70: W291 trailing whitespace
xenstore.py:49:80: E501 line too long (89 characters)
xenstore.py:54:30: W291 trailing whitespace
xenstore.py:59:1: E302 expected 2 blank lines, found 1
xenstore.py:66:80: E501 line too long (83 characters)
xenstore.py:70:1: E302 expected 2 blank lines, found 1
xenstore.py:101:1: E302 expected 2 blank lines, found 1
xenstore.py:108:1: E302 expected 2 blank lines, found 1
xenstore.py:140:1: E302 expected 2 blank lines, found 1
xenstore.py:146:80: E501 line too long (100 characters)

2) Exception handling:

There are a few places in nova/virt/xenapi/vmops.py where we try to parse the JSON, fail, and then return the original response. For example, in list_from_xenstore:

    ret = self._make_xenstore_call('list_records', vm, path)
    try:
        return json.loads(ret)
    except ValueError:
        # Not a valid JSON value
        return ret

Shouldn't this re-raise the exception if we have received an invalid JSON response? It seems that when this succeeds we get a Python data structure back, and when it fails we instead get some garbage from the XenAPI.

3) There is a multi-line comment about several methods that were created to interact with the xenstore parameter record. You mention that they aren't used, but might be useful. Should we avoid adding this code until its needed? Otherwise, it's code bloat.

review: Needs Fixing
Revision history for this message
Ed Leafe (ed-leafe) wrote :

Just pushed an update that addresses your concerns:

1) Fixed. Also fixed pluginlib_nova.py, which also had pep8 errors. Odd that pep8 didn't pick those up unless given an explicit path.

2) Good point. I modified the plugin to jsonify all return values, and removed the try/except from the vmops.py file.

3) I wasn't sure about this, as I don't know enough about the way that you are supposed to interact with xenstore. The param record methods persist across reboots, while the primary xenstore records do not. If someone with more knowledge can chime in, that would be great.

lp:~ed-leafe/nova/xenstore-plugin updated
513. By Ed Leafe

Made the plugin output fully json-ified, so I could remove the exception handlers in vmops.py. Cleaned up some pep8 issues that weren't caught in earlier runs.

Revision history for this message
Cory Wright (corywright) wrote :

The changes look good to me.

review: Approve
Revision history for this message
Josh Kearney (jk0) wrote :

I like the addition and usage of _get_vm_opaque_ref(). That really helps clean up the code.

Everything looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'nova/virt/xenapi/vmops.py'
--- nova/virt/xenapi/vmops.py 2010-12-30 21:23:14 +0000
+++ nova/virt/xenapi/vmops.py 2011-01-04 21:22:15 +0000
@@ -1,6 +1,7 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=41# vim: tabstop=4 shiftwidth=4 softtabstop=4
22
3# Copyright (c) 2010 Citrix Systems, Inc.3# Copyright (c) 2010 Citrix Systems, Inc.
4# Copyright 2010 OpenStack LLC.
4#5#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may6# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain7# not use this file except in compliance with the License. You may obtain
@@ -18,6 +19,7 @@
18Management class for VM-related functions (spawn, reboot, etc).19Management class for VM-related functions (spawn, reboot, etc).
19"""20"""
2021
22import json
21import logging23import logging
2224
23from nova import db25from nova import db
@@ -36,7 +38,6 @@
36 """38 """
37 Management class for VM-related tasks39 Management class for VM-related tasks
38 """40 """
39
40 def __init__(self, session):41 def __init__(self, session):
41 self.XenAPI = session.get_imported_xenapi()42 self.XenAPI = session.get_imported_xenapi()
42 self._session = session43 self._session = session
@@ -120,6 +121,20 @@
120 timer.f = _wait_for_boot121 timer.f = _wait_for_boot
121 return timer.start(interval=0.5, now=True)122 return timer.start(interval=0.5, now=True)
122123
124 def _get_vm_opaque_ref(self, instance_or_vm):
125 """Refactored out the common code of many methods that receive either
126 a vm name or a vm instance, and want a vm instance in return.
127 """
128 try:
129 instance_name = instance_or_vm.name
130 vm = VMHelper.lookup(self._session, instance_name)
131 except AttributeError:
132 # A vm opaque ref was passed
133 vm = instance_or_vm
134 if vm is None:
135 raise Exception(_('Instance not present %s') % instance_name)
136 return vm
137
123 def snapshot(self, instance, name):138 def snapshot(self, instance, name):
124 """ Create snapshot from a running VM instance139 """ Create snapshot from a running VM instance
125140
@@ -168,11 +183,7 @@
168183
169 def reboot(self, instance):184 def reboot(self, instance):
170 """Reboot VM instance"""185 """Reboot VM instance"""
171 instance_name = instance.name186 vm = self._get_vm_opaque_ref(instance)
172 vm = VMHelper.lookup(self._session, instance_name)
173 if vm is None:
174 raise exception.NotFound(_('instance not'
175 ' found %s') % instance_name)
176 task = self._session.call_xenapi('Async.VM.clean_reboot', vm)187 task = self._session.call_xenapi('Async.VM.clean_reboot', vm)
177 self._session.wait_for_task(instance.id, task)188 self._session.wait_for_task(instance.id, task)
178189
@@ -215,27 +226,19 @@
215 ret = None226 ret = None
216 try:227 try:
217 ret = self._session.wait_for_task(instance_id, task)228 ret = self._session.wait_for_task(instance_id, task)
218 except XenAPI.Failure, exc:229 except self.XenAPI.Failure, exc:
219 logging.warn(exc)230 logging.warn(exc)
220 callback(ret)231 callback(ret)
221232
222 def pause(self, instance, callback):233 def pause(self, instance, callback):
223 """Pause VM instance"""234 """Pause VM instance"""
224 instance_name = instance.name235 vm = self._get_vm_opaque_ref(instance)
225 vm = VMHelper.lookup(self._session, instance_name)
226 if vm is None:
227 raise exception.NotFound(_('Instance not'
228 ' found %s') % instance_name)
229 task = self._session.call_xenapi('Async.VM.pause', vm)236 task = self._session.call_xenapi('Async.VM.pause', vm)
230 self._wait_with_callback(instance.id, task, callback)237 self._wait_with_callback(instance.id, task, callback)
231238
232 def unpause(self, instance, callback):239 def unpause(self, instance, callback):
233 """Unpause VM instance"""240 """Unpause VM instance"""
234 instance_name = instance.name241 vm = self._get_vm_opaque_ref(instance)
235 vm = VMHelper.lookup(self._session, instance_name)
236 if vm is None:
237 raise exception.NotFound(_('Instance not'
238 ' found %s') % instance_name)
239 task = self._session.call_xenapi('Async.VM.unpause', vm)242 task = self._session.call_xenapi('Async.VM.unpause', vm)
240 self._wait_with_callback(instance.id, task, callback)243 self._wait_with_callback(instance.id, task, callback)
241244
@@ -270,10 +273,7 @@
270273
271 def get_diagnostics(self, instance):274 def get_diagnostics(self, instance):
272 """Return data about VM diagnostics"""275 """Return data about VM diagnostics"""
273 vm = VMHelper.lookup(self._session, instance.name)276 vm = self._get_vm_opaque_ref(instance)
274 if vm is None:
275 raise exception.NotFound(_("Instance not found %s") %
276 instance.name)
277 rec = self._session.get_xenapi().VM.get_record(vm)277 rec = self._session.get_xenapi().VM.get_record(vm)
278 return VMHelper.compile_diagnostics(self._session, rec)278 return VMHelper.compile_diagnostics(self._session, rec)
279279
@@ -281,3 +281,175 @@
281 """Return snapshot of console"""281 """Return snapshot of console"""
282 # TODO: implement this to fix pylint!282 # TODO: implement this to fix pylint!
283 return 'FAKE CONSOLE OUTPUT of instance'283 return 'FAKE CONSOLE OUTPUT of instance'
284
285 def list_from_xenstore(self, vm, path):
286 """Runs the xenstore-ls command to get a listing of all records
287 from 'path' downward. Returns a dict with the sub-paths as keys,
288 and the value stored in those paths as values. If nothing is
289 found at that path, returns None.
290 """
291 ret = self._make_xenstore_call('list_records', vm, path)
292 return json.loads(ret)
293
294 def read_from_xenstore(self, vm, path):
295 """Returns the value stored in the xenstore record for the given VM
296 at the specified location. A XenAPIPlugin.PluginError will be raised
297 if any error is encountered in the read process.
298 """
299 try:
300 ret = self._make_xenstore_call('read_record', vm, path,
301 {'ignore_missing_path': 'True'})
302 except self.XenAPI.Failure, e:
303 return None
304 ret = json.loads(ret)
305 if ret == "None":
306 # Can't marshall None over RPC calls.
307 return None
308 return ret
309
310 def write_to_xenstore(self, vm, path, value):
311 """Writes the passed value to the xenstore record for the given VM
312 at the specified location. A XenAPIPlugin.PluginError will be raised
313 if any error is encountered in the write process.
314 """
315 return self._make_xenstore_call('write_record', vm, path,
316 {'value': json.dumps(value)})
317
318 def clear_xenstore(self, vm, path):
319 """Deletes the VM's xenstore record for the specified path.
320 If there is no such record, the request is ignored.
321 """
322 self._make_xenstore_call('delete_record', vm, path)
323
324 def _make_xenstore_call(self, method, vm, path, addl_args={}):
325 """Handles calls to the xenstore xenapi plugin."""
326 return self._make_plugin_call('xenstore.py', method=method, vm=vm,
327 path=path, addl_args=addl_args)
328
329 def _make_plugin_call(self, plugin, method, vm, path, addl_args={}):
330 """Abstracts out the process of calling a method of a xenapi plugin.
331 Any errors raised by the plugin will in turn raise a RuntimeError here.
332 """
333 vm = self._get_vm_opaque_ref(vm)
334 rec = self._session.get_xenapi().VM.get_record(vm)
335 args = {'dom_id': rec['domid'], 'path': path}
336 args.update(addl_args)
337 # If the 'testing_mode' attribute is set, add that to the args.
338 if getattr(self, 'testing_mode', False):
339 args['testing_mode'] = 'true'
340 try:
341 task = self._session.async_call_plugin(plugin, method, args)
342 ret = self._session.wait_for_task(0, task)
343 except self.XenAPI.Failure, e:
344 raise RuntimeError("%s" % e.details[-1])
345 return ret
346
347 def add_to_xenstore(self, vm, path, key, value):
348 """Adds the passed key/value pair to the xenstore record for
349 the given VM at the specified location. A XenAPIPlugin.PluginError
350 will be raised if any error is encountered in the write process.
351 """
352 current = self.read_from_xenstore(vm, path)
353 if not current:
354 # Nothing at that location
355 current = {key: value}
356 else:
357 current[key] = value
358 self.write_to_xenstore(vm, path, current)
359
360 def remove_from_xenstore(self, vm, path, key_or_keys):
361 """Takes either a single key or a list of keys and removes
362 them from the xenstoreirecord data for the given VM.
363 If the key doesn't exist, the request is ignored.
364 """
365 current = self.list_from_xenstore(vm, path)
366 if not current:
367 return
368 if isinstance(key_or_keys, basestring):
369 keys = [key_or_keys]
370 else:
371 keys = key_or_keys
372 keys.sort(lambda x, y: cmp(y.count('/'), x.count('/')))
373 for key in keys:
374 if path:
375 keypath = "%s/%s" % (path, key)
376 else:
377 keypath = key
378 self._make_xenstore_call('delete_record', vm, keypath)
379
380 ########################################################################
381 ###### The following methods interact with the xenstore parameter
382 ###### record, not the live xenstore. They were created before I
383 ###### knew the difference, and are left in here in case they prove
384 ###### to be useful. They all have '_param' added to their method
385 ###### names to distinguish them. (dabo)
386 ########################################################################
387 def read_partial_from_param_xenstore(self, instance_or_vm, key_prefix):
388 """Returns a dict of all the keys in the xenstore parameter record
389 for the given instance that begin with the key_prefix.
390 """
391 data = self.read_from_param_xenstore(instance_or_vm)
392 badkeys = [k for k in data.keys()
393 if not k.startswith(key_prefix)]
394 for badkey in badkeys:
395 del data[badkey]
396 return data
397
398 def read_from_param_xenstore(self, instance_or_vm, keys=None):
399 """Returns the xenstore parameter record data for the specified VM
400 instance as a dict. Accepts an optional key or list of keys; if a
401 value for 'keys' is passed, the returned dict is filtered to only
402 return the values for those keys.
403 """
404 vm = self._get_vm_opaque_ref(instance_or_vm)
405 data = self._session.call_xenapi_request('VM.get_xenstore_data',
406 (vm, ))
407 ret = {}
408 if keys is None:
409 keys = data.keys()
410 elif isinstance(keys, basestring):
411 keys = [keys]
412 for key in keys:
413 raw = data.get(key)
414 if raw:
415 ret[key] = json.loads(raw)
416 else:
417 ret[key] = raw
418 return ret
419
420 def add_to_param_xenstore(self, instance_or_vm, key, val):
421 """Takes a key/value pair and adds it to the xenstore parameter
422 record for the given vm instance. If the key exists in xenstore,
423 it is overwritten"""
424 vm = self._get_vm_opaque_ref(instance_or_vm)
425 self.remove_from_param_xenstore(instance_or_vm, key)
426 jsonval = json.dumps(val)
427 self._session.call_xenapi_request('VM.add_to_xenstore_data',
428 (vm, key, jsonval))
429
430 def write_to_param_xenstore(self, instance_or_vm, mapping):
431 """Takes a dict and writes each key/value pair to the xenstore
432 parameter record for the given vm instance. Any existing data for
433 those keys is overwritten.
434 """
435 for k, v in mapping.iteritems():
436 self.add_to_param_xenstore(instance_or_vm, k, v)
437
438 def remove_from_param_xenstore(self, instance_or_vm, key_or_keys):
439 """Takes either a single key or a list of keys and removes
440 them from the xenstore parameter record data for the given VM.
441 If the key doesn't exist, the request is ignored.
442 """
443 vm = self._get_vm_opaque_ref(instance_or_vm)
444 if isinstance(key_or_keys, basestring):
445 keys = [key_or_keys]
446 else:
447 keys = key_or_keys
448 for key in keys:
449 self._session.call_xenapi_request('VM.remove_from_xenstore_data',
450 (vm, key))
451
452 def clear_param_xenstore(self, instance_or_vm):
453 """Removes all data from the xenstore parameter record for this VM."""
454 self.write_to_param_xenstore(instance_or_vm, {})
455 ########################################################################
284456
=== modified file 'nova/virt/xenapi_conn.py'
--- nova/virt/xenapi_conn.py 2010-12-30 21:23:14 +0000
+++ nova/virt/xenapi_conn.py 2011-01-04 21:22:15 +0000
@@ -1,6 +1,7 @@
1# vim: tabstop=4 shiftwidth=4 softtabstop=41# vim: tabstop=4 shiftwidth=4 softtabstop=4
22
3# Copyright (c) 2010 Citrix Systems, Inc.3# Copyright (c) 2010 Citrix Systems, Inc.
4# Copyright 2010 OpenStack LLC.
4#5#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may6# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain7# not use this file except in compliance with the License. You may obtain
@@ -19,15 +20,15 @@
1920
20The concurrency model for this class is as follows:21The concurrency model for this class is as follows:
2122
22All XenAPI calls are on a thread (using t.i.t.deferToThread, via the decorator23All XenAPI calls are on a green thread (using eventlet's "tpool"
23deferredToThread). They are remote calls, and so may hang for the usual24thread pool). They are remote calls, and so may hang for the usual
24reasons. They should not be allowed to block the reactor thread.25reasons.
2526
26All long-running XenAPI calls (VM.start, VM.reboot, etc) are called async27All long-running XenAPI calls (VM.start, VM.reboot, etc) are called async
27(using XenAPI.VM.async_start etc). These return a task, which can then be28(using XenAPI.VM.async_start etc). These return a task, which can then be
28polled for completion. Polling is handled using reactor.callLater.29polled for completion.
2930
30This combination of techniques means that we don't block the reactor thread at31This combination of techniques means that we don't block the main thread at
31all, and at the same time we don't hold lots of threads waiting for32all, and at the same time we don't hold lots of threads waiting for
32long-running operations.33long-running operations.
3334
@@ -81,7 +82,7 @@
81flags.DEFINE_float('xenapi_task_poll_interval',82flags.DEFINE_float('xenapi_task_poll_interval',
82 0.5,83 0.5,
83 'The interval used for polling of remote tasks '84 'The interval used for polling of remote tasks '
84 '(Async.VM.start, etc). Used only if '85 '(Async.VM.start, etc). Used only if '
85 'connection_type=xenapi.')86 'connection_type=xenapi.')
86flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',87flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',
87 5.0,88 5.0,
@@ -213,6 +214,14 @@
213 f = f.__getattr__(m)214 f = f.__getattr__(m)
214 return tpool.execute(f, *args)215 return tpool.execute(f, *args)
215216
217 def call_xenapi_request(self, method, *args):
218 """Some interactions with dom0, such as interacting with xenstore's
219 param record, require using the xenapi_request method of the session
220 object. This wraps that call on a background thread.
221 """
222 f = self._session.xenapi_request
223 return tpool.execute(f, method, *args)
224
216 def async_call_plugin(self, plugin, fn, args):225 def async_call_plugin(self, plugin, fn, args):
217 """Call Async.host.call_plugin on a background thread."""226 """Call Async.host.call_plugin on a background thread."""
218 return tpool.execute(self._unwrap_plugin_exceptions,227 return tpool.execute(self._unwrap_plugin_exceptions,
@@ -222,7 +231,6 @@
222 def wait_for_task(self, id, task):231 def wait_for_task(self, id, task):
223 """Return the result of the given task. The task is polled232 """Return the result of the given task. The task is polled
224 until it completes."""233 until it completes."""
225
226 done = event.Event()234 done = event.Event()
227 loop = utils.LoopingCall(self._poll_task, id, task, done)235 loop = utils.LoopingCall(self._poll_task, id, task, done)
228 loop.start(FLAGS.xenapi_task_poll_interval, now=True)236 loop.start(FLAGS.xenapi_task_poll_interval, now=True)
@@ -235,7 +243,7 @@
235 return self.XenAPI.Session(url)243 return self.XenAPI.Session(url)
236244
237 def _poll_task(self, id, task, done):245 def _poll_task(self, id, task, done):
238 """Poll the given XenAPI task, and fire the given Deferred if we246 """Poll the given XenAPI task, and fire the given action if we
239 get a result."""247 get a result."""
240 try:248 try:
241 name = self._session.xenapi.task.get_name_label(task)249 name = self._session.xenapi.task.get_name_label(task)
@@ -290,7 +298,7 @@
290298
291299
292def _parse_xmlrpc_value(val):300def _parse_xmlrpc_value(val):
293 """Parse the given value as if it were an XML-RPC value. This is301 """Parse the given value as if it were an XML-RPC value. This is
294 sometimes used as the format for the task.result field."""302 sometimes used as the format for the task.result field."""
295 if not val:303 if not val:
296 return val304 return val
297305
=== modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py'
--- plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 2010-08-02 23:52:06 +0000
+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 2011-01-04 21:22:15 +0000
@@ -45,6 +45,7 @@
45 def __init__(self, *args):45 def __init__(self, *args):
46 Exception.__init__(self, *args)46 Exception.__init__(self, *args)
4747
48
48class ArgumentError(PluginError):49class ArgumentError(PluginError):
49 """Raised when required arguments are missing, argument values are invalid,50 """Raised when required arguments are missing, argument values are invalid,
50 or incompatible arguments are given.51 or incompatible arguments are given.
@@ -67,6 +68,7 @@
6768
68ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$')69ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$')
6970
71
70def validate_exists(args, key, default=None):72def validate_exists(args, key, default=None):
71 """Validates that a string argument to a RPC method call is given, and73 """Validates that a string argument to a RPC method call is given, and
72 matches the shell-safe regex, with an optional default value in case it74 matches the shell-safe regex, with an optional default value in case it
@@ -76,20 +78,24 @@
76 """78 """
77 if key in args:79 if key in args:
78 if len(args[key]) == 0:80 if len(args[key]) == 0:
79 raise ArgumentError('Argument %r value %r is too short.' % (key, args[key]))81 raise ArgumentError('Argument %r value %r is too short.' %
82 (key, args[key]))
80 if not ARGUMENT_PATTERN.match(args[key]):83 if not ARGUMENT_PATTERN.match(args[key]):
81 raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key]))84 raise ArgumentError('Argument %r value %r contains invalid '
85 'characters.' % (key, args[key]))
82 if args[key][0] == '-':86 if args[key][0] == '-':
83 raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key]))87 raise ArgumentError('Argument %r value %r starts with a hyphen.'
88 % (key, args[key]))
84 return args[key]89 return args[key]
85 elif default is not None:90 elif default is not None:
86 return default91 return default
87 else:92 else:
88 raise ArgumentError('Argument %s is required.' % key)93 raise ArgumentError('Argument %s is required.' % key)
8994
95
90def validate_bool(args, key, default=None):96def validate_bool(args, key, default=None):
91 """Validates that a string argument to a RPC method call is a boolean string,97 """Validates that a string argument to a RPC method call is a boolean
92 with an optional default value in case it does not exist.98 string, with an optional default value in case it does not exist.
9399
94 Returns the python boolean value.100 Returns the python boolean value.
95 """101 """
@@ -99,7 +105,9 @@
99 elif value.lower() == 'false':105 elif value.lower() == 'false':
100 return False106 return False
101 else:107 else:
102 raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value))108 raise ArgumentError("Argument %s may not take value %r. "
109 "Valid values are ['true', 'false']." % (key, value))
110
103111
104def exists(args, key):112def exists(args, key):
105 """Validates that a freeform string argument to a RPC method call is given.113 """Validates that a freeform string argument to a RPC method call is given.
@@ -110,6 +118,7 @@
110 else:118 else:
111 raise ArgumentError('Argument %s is required.' % key)119 raise ArgumentError('Argument %s is required.' % key)
112120
121
113def optional(args, key):122def optional(args, key):
114 """If the given key is in args, return the corresponding value, otherwise123 """If the given key is in args, return the corresponding value, otherwise
115 return None"""124 return None"""
@@ -122,13 +131,14 @@
122131
123def get_domain_0(session):132def get_domain_0(session):
124 this_host_ref = get_this_host(session)133 this_host_ref = get_this_host(session)
125 expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref134 expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"'
135 expr = expr % this_host_ref
126 return session.xenapi.VM.get_all_records_where(expr).keys()[0]136 return session.xenapi.VM.get_all_records_where(expr).keys()[0]
127137
128138
129def create_vdi(session, sr_ref, name_label, virtual_size, read_only):139def create_vdi(session, sr_ref, name_label, virtual_size, read_only):
130 vdi_ref = session.xenapi.VDI.create(140 vdi_ref = session.xenapi.VDI.create(
131 { 'name_label': name_label,141 {'name_label': name_label,
132 'name_description': '',142 'name_description': '',
133 'SR': sr_ref,143 'SR': sr_ref,
134 'virtual_size': str(virtual_size),144 'virtual_size': str(virtual_size),
@@ -138,7 +148,7 @@
138 'xenstore_data': {},148 'xenstore_data': {},
139 'other_config': {},149 'other_config': {},
140 'sm_config': {},150 'sm_config': {},
141 'tags': [] })151 'tags': []})
142 logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label,152 logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label,
143 virtual_size, read_only, sr_ref)153 virtual_size, read_only, sr_ref)
144 return vdi_ref154 return vdi_ref
145155
=== added file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py'
--- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py 1970-01-01 00:00:00 +0000
+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py 2011-01-04 21:22:15 +0000
@@ -0,0 +1,180 @@
1#!/usr/bin/env python
2
3# Copyright (c) 2010 Citrix Systems, Inc.
4# Copyright 2010 OpenStack LLC.
5# Copyright 2010 United States Government as represented by the
6# Administrator of the National Aeronautics and Space Administration.
7# All Rights Reserved.
8#
9# Licensed under the Apache License, Version 2.0 (the "License"); you may
10# not use this file except in compliance with the License. You may obtain
11# a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18# License for the specific language governing permissions and limitations
19# under the License.
20
21#
22# XenAPI plugin for reading/writing information to xenstore
23#
24
25try:
26 import json
27except ImportError:
28 import simplejson as json
29import subprocess
30
31import XenAPIPlugin
32
33import pluginlib_nova as pluginlib
34pluginlib.configure_logging("xenstore")
35
36
37def jsonify(fnc):
38 def wrapper(*args, **kwargs):
39 return json.dumps(fnc(*args, **kwargs))
40 return wrapper
41
42
43@jsonify
44def read_record(self, arg_dict):
45 """Returns the value stored at the given path for the given dom_id.
46 These must be encoded as key/value pairs in arg_dict. You can
47 optinally include a key 'ignore_missing_path'; if this is present
48 and boolean True, attempting to read a non-existent path will return
49 the string 'None' instead of raising an exception.
50 """
51 cmd = "xenstore-read /local/domain/%(dom_id)s/%(path)s" % arg_dict
52 try:
53 return _run_command(cmd).rstrip("\n")
54 except pluginlib.PluginError, e:
55 if arg_dict.get("ignore_missing_path", False):
56 cmd = "xenstore-exists /local/domain/%(dom_id)s/%(path)s; echo $?"
57 cmd = cmd % arg_dict
58 ret = _run_command(cmd).strip()
59 # If the path exists, the cmd should return "0"
60 if ret != "0":
61 # No such path, so ignore the error and return the
62 # string 'None', since None can't be marshalled
63 # over RPC.
64 return "None"
65 # Either we shouldn't ignore path errors, or another
66 # error was hit. Re-raise.
67 raise
68
69
70@jsonify
71def write_record(self, arg_dict):
72 """Writes to xenstore at the specified path. If there is information
73 already stored in that location, it is overwritten. As in read_record,
74 the dom_id and path must be specified in the arg_dict; additionally,
75 you must specify a 'value' key, whose value must be a string. Typically,
76 you can json-ify more complex values and store the json output.
77 """
78 cmd = "xenstore-write /local/domain/%(dom_id)s/%(path)s '%(value)s'"
79 cmd = cmd % arg_dict
80 _run_command(cmd)
81 return arg_dict["value"]
82
83
84@jsonify
85def list_records(self, arg_dict):
86 """Returns all the stored data at or below the given path for the
87 given dom_id. The data is returned as a json-ified dict, with the
88 path as the key and the stored value as the value. If the path
89 doesn't exist, an empty dict is returned.
90 """
91 cmd = "xenstore-ls /local/domain/%(dom_id)s/%(path)s" % arg_dict
92 cmd = cmd.rstrip("/")
93 try:
94 recs = _run_command(cmd)
95 except pluginlib.PluginError, e:
96 if "No such file or directory" in "%s" % e:
97 # Path doesn't exist.
98 return {}
99 return str(e)
100 raise
101 base_path = arg_dict["path"]
102 paths = _paths_from_ls(recs)
103 ret = {}
104 for path in paths:
105 if base_path:
106 arg_dict["path"] = "%s/%s" % (base_path, path)
107 else:
108 arg_dict["path"] = path
109 rec = read_record(self, arg_dict)
110 try:
111 val = json.loads(rec)
112 except ValueError:
113 val = rec
114 ret[path] = val
115 return ret
116
117
118@jsonify
119def delete_record(self, arg_dict):
120 """Just like it sounds: it removes the record for the specified
121 VM and the specified path from xenstore.
122 """
123 cmd = "xenstore-rm /local/domain/%(dom_id)s/%(path)s" % arg_dict
124 return _run_command(cmd)
125
126
127def _paths_from_ls(recs):
128 """The xenstore-ls command returns a listing that isn't terribly
129 useful. This method cleans that up into a dict with each path
130 as the key, and the associated string as the value.
131 """
132 ret = {}
133 last_nm = ""
134 level = 0
135 path = []
136 ret = []
137 for ln in recs.splitlines():
138 nm, val = ln.rstrip().split(" = ")
139 barename = nm.lstrip()
140 this_level = len(nm) - len(barename)
141 if this_level == 0:
142 ret.append(barename)
143 level = 0
144 path = []
145 elif this_level == level:
146 # child of same parent
147 ret.append("%s/%s" % ("/".join(path), barename))
148 elif this_level > level:
149 path.append(last_nm)
150 ret.append("%s/%s" % ("/".join(path), barename))
151 level = this_level
152 elif this_level < level:
153 path = path[:this_level]
154 ret.append("%s/%s" % ("/".join(path), barename))
155 level = this_level
156 last_nm = barename
157 return ret
158
159
160def _run_command(cmd):
161 """Abstracts out the basics of issuing system commands. If the command
162 returns anything in stderr, a PluginError is raised with that information.
163 Otherwise, the output from stdout is returned.
164 """
165 pipe = subprocess.PIPE
166 proc = subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe,
167 stderr=pipe, close_fds=True)
168 proc.wait()
169 err = proc.stderr.read()
170 if err:
171 raise pluginlib.PluginError(err)
172 return proc.stdout.read()
173
174
175if __name__ == "__main__":
176 XenAPIPlugin.dispatch(
177 {"read_record": read_record,
178 "write_record": write_record,
179 "list_records": list_records,
180 "delete_record": delete_record})