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
1=== modified file 'nova/virt/xenapi/vmops.py'
2--- nova/virt/xenapi/vmops.py 2010-12-30 21:23:14 +0000
3+++ nova/virt/xenapi/vmops.py 2011-01-04 21:22:15 +0000
4@@ -1,6 +1,7 @@
5 # vim: tabstop=4 shiftwidth=4 softtabstop=4
6
7 # Copyright (c) 2010 Citrix Systems, Inc.
8+# Copyright 2010 OpenStack LLC.
9 #
10 # Licensed under the Apache License, Version 2.0 (the "License"); you may
11 # not use this file except in compliance with the License. You may obtain
12@@ -18,6 +19,7 @@
13 Management class for VM-related functions (spawn, reboot, etc).
14 """
15
16+import json
17 import logging
18
19 from nova import db
20@@ -36,7 +38,6 @@
21 """
22 Management class for VM-related tasks
23 """
24-
25 def __init__(self, session):
26 self.XenAPI = session.get_imported_xenapi()
27 self._session = session
28@@ -120,6 +121,20 @@
29 timer.f = _wait_for_boot
30 return timer.start(interval=0.5, now=True)
31
32+ def _get_vm_opaque_ref(self, instance_or_vm):
33+ """Refactored out the common code of many methods that receive either
34+ a vm name or a vm instance, and want a vm instance in return.
35+ """
36+ try:
37+ instance_name = instance_or_vm.name
38+ vm = VMHelper.lookup(self._session, instance_name)
39+ except AttributeError:
40+ # A vm opaque ref was passed
41+ vm = instance_or_vm
42+ if vm is None:
43+ raise Exception(_('Instance not present %s') % instance_name)
44+ return vm
45+
46 def snapshot(self, instance, name):
47 """ Create snapshot from a running VM instance
48
49@@ -168,11 +183,7 @@
50
51 def reboot(self, instance):
52 """Reboot VM instance"""
53- instance_name = instance.name
54- vm = VMHelper.lookup(self._session, instance_name)
55- if vm is None:
56- raise exception.NotFound(_('instance not'
57- ' found %s') % instance_name)
58+ vm = self._get_vm_opaque_ref(instance)
59 task = self._session.call_xenapi('Async.VM.clean_reboot', vm)
60 self._session.wait_for_task(instance.id, task)
61
62@@ -215,27 +226,19 @@
63 ret = None
64 try:
65 ret = self._session.wait_for_task(instance_id, task)
66- except XenAPI.Failure, exc:
67+ except self.XenAPI.Failure, exc:
68 logging.warn(exc)
69 callback(ret)
70
71 def pause(self, instance, callback):
72 """Pause VM instance"""
73- instance_name = instance.name
74- vm = VMHelper.lookup(self._session, instance_name)
75- if vm is None:
76- raise exception.NotFound(_('Instance not'
77- ' found %s') % instance_name)
78+ vm = self._get_vm_opaque_ref(instance)
79 task = self._session.call_xenapi('Async.VM.pause', vm)
80 self._wait_with_callback(instance.id, task, callback)
81
82 def unpause(self, instance, callback):
83 """Unpause VM instance"""
84- instance_name = instance.name
85- vm = VMHelper.lookup(self._session, instance_name)
86- if vm is None:
87- raise exception.NotFound(_('Instance not'
88- ' found %s') % instance_name)
89+ vm = self._get_vm_opaque_ref(instance)
90 task = self._session.call_xenapi('Async.VM.unpause', vm)
91 self._wait_with_callback(instance.id, task, callback)
92
93@@ -270,10 +273,7 @@
94
95 def get_diagnostics(self, instance):
96 """Return data about VM diagnostics"""
97- vm = VMHelper.lookup(self._session, instance.name)
98- if vm is None:
99- raise exception.NotFound(_("Instance not found %s") %
100- instance.name)
101+ vm = self._get_vm_opaque_ref(instance)
102 rec = self._session.get_xenapi().VM.get_record(vm)
103 return VMHelper.compile_diagnostics(self._session, rec)
104
105@@ -281,3 +281,175 @@
106 """Return snapshot of console"""
107 # TODO: implement this to fix pylint!
108 return 'FAKE CONSOLE OUTPUT of instance'
109+
110+ def list_from_xenstore(self, vm, path):
111+ """Runs the xenstore-ls command to get a listing of all records
112+ from 'path' downward. Returns a dict with the sub-paths as keys,
113+ and the value stored in those paths as values. If nothing is
114+ found at that path, returns None.
115+ """
116+ ret = self._make_xenstore_call('list_records', vm, path)
117+ return json.loads(ret)
118+
119+ def read_from_xenstore(self, vm, path):
120+ """Returns the value stored in the xenstore record for the given VM
121+ at the specified location. A XenAPIPlugin.PluginError will be raised
122+ if any error is encountered in the read process.
123+ """
124+ try:
125+ ret = self._make_xenstore_call('read_record', vm, path,
126+ {'ignore_missing_path': 'True'})
127+ except self.XenAPI.Failure, e:
128+ return None
129+ ret = json.loads(ret)
130+ if ret == "None":
131+ # Can't marshall None over RPC calls.
132+ return None
133+ return ret
134+
135+ def write_to_xenstore(self, vm, path, value):
136+ """Writes the passed value to the xenstore record for the given VM
137+ at the specified location. A XenAPIPlugin.PluginError will be raised
138+ if any error is encountered in the write process.
139+ """
140+ return self._make_xenstore_call('write_record', vm, path,
141+ {'value': json.dumps(value)})
142+
143+ def clear_xenstore(self, vm, path):
144+ """Deletes the VM's xenstore record for the specified path.
145+ If there is no such record, the request is ignored.
146+ """
147+ self._make_xenstore_call('delete_record', vm, path)
148+
149+ def _make_xenstore_call(self, method, vm, path, addl_args={}):
150+ """Handles calls to the xenstore xenapi plugin."""
151+ return self._make_plugin_call('xenstore.py', method=method, vm=vm,
152+ path=path, addl_args=addl_args)
153+
154+ def _make_plugin_call(self, plugin, method, vm, path, addl_args={}):
155+ """Abstracts out the process of calling a method of a xenapi plugin.
156+ Any errors raised by the plugin will in turn raise a RuntimeError here.
157+ """
158+ vm = self._get_vm_opaque_ref(vm)
159+ rec = self._session.get_xenapi().VM.get_record(vm)
160+ args = {'dom_id': rec['domid'], 'path': path}
161+ args.update(addl_args)
162+ # If the 'testing_mode' attribute is set, add that to the args.
163+ if getattr(self, 'testing_mode', False):
164+ args['testing_mode'] = 'true'
165+ try:
166+ task = self._session.async_call_plugin(plugin, method, args)
167+ ret = self._session.wait_for_task(0, task)
168+ except self.XenAPI.Failure, e:
169+ raise RuntimeError("%s" % e.details[-1])
170+ return ret
171+
172+ def add_to_xenstore(self, vm, path, key, value):
173+ """Adds the passed key/value pair to the xenstore record for
174+ the given VM at the specified location. A XenAPIPlugin.PluginError
175+ will be raised if any error is encountered in the write process.
176+ """
177+ current = self.read_from_xenstore(vm, path)
178+ if not current:
179+ # Nothing at that location
180+ current = {key: value}
181+ else:
182+ current[key] = value
183+ self.write_to_xenstore(vm, path, current)
184+
185+ def remove_from_xenstore(self, vm, path, key_or_keys):
186+ """Takes either a single key or a list of keys and removes
187+ them from the xenstoreirecord data for the given VM.
188+ If the key doesn't exist, the request is ignored.
189+ """
190+ current = self.list_from_xenstore(vm, path)
191+ if not current:
192+ return
193+ if isinstance(key_or_keys, basestring):
194+ keys = [key_or_keys]
195+ else:
196+ keys = key_or_keys
197+ keys.sort(lambda x, y: cmp(y.count('/'), x.count('/')))
198+ for key in keys:
199+ if path:
200+ keypath = "%s/%s" % (path, key)
201+ else:
202+ keypath = key
203+ self._make_xenstore_call('delete_record', vm, keypath)
204+
205+ ########################################################################
206+ ###### The following methods interact with the xenstore parameter
207+ ###### record, not the live xenstore. They were created before I
208+ ###### knew the difference, and are left in here in case they prove
209+ ###### to be useful. They all have '_param' added to their method
210+ ###### names to distinguish them. (dabo)
211+ ########################################################################
212+ def read_partial_from_param_xenstore(self, instance_or_vm, key_prefix):
213+ """Returns a dict of all the keys in the xenstore parameter record
214+ for the given instance that begin with the key_prefix.
215+ """
216+ data = self.read_from_param_xenstore(instance_or_vm)
217+ badkeys = [k for k in data.keys()
218+ if not k.startswith(key_prefix)]
219+ for badkey in badkeys:
220+ del data[badkey]
221+ return data
222+
223+ def read_from_param_xenstore(self, instance_or_vm, keys=None):
224+ """Returns the xenstore parameter record data for the specified VM
225+ instance as a dict. Accepts an optional key or list of keys; if a
226+ value for 'keys' is passed, the returned dict is filtered to only
227+ return the values for those keys.
228+ """
229+ vm = self._get_vm_opaque_ref(instance_or_vm)
230+ data = self._session.call_xenapi_request('VM.get_xenstore_data',
231+ (vm, ))
232+ ret = {}
233+ if keys is None:
234+ keys = data.keys()
235+ elif isinstance(keys, basestring):
236+ keys = [keys]
237+ for key in keys:
238+ raw = data.get(key)
239+ if raw:
240+ ret[key] = json.loads(raw)
241+ else:
242+ ret[key] = raw
243+ return ret
244+
245+ def add_to_param_xenstore(self, instance_or_vm, key, val):
246+ """Takes a key/value pair and adds it to the xenstore parameter
247+ record for the given vm instance. If the key exists in xenstore,
248+ it is overwritten"""
249+ vm = self._get_vm_opaque_ref(instance_or_vm)
250+ self.remove_from_param_xenstore(instance_or_vm, key)
251+ jsonval = json.dumps(val)
252+ self._session.call_xenapi_request('VM.add_to_xenstore_data',
253+ (vm, key, jsonval))
254+
255+ def write_to_param_xenstore(self, instance_or_vm, mapping):
256+ """Takes a dict and writes each key/value pair to the xenstore
257+ parameter record for the given vm instance. Any existing data for
258+ those keys is overwritten.
259+ """
260+ for k, v in mapping.iteritems():
261+ self.add_to_param_xenstore(instance_or_vm, k, v)
262+
263+ def remove_from_param_xenstore(self, instance_or_vm, key_or_keys):
264+ """Takes either a single key or a list of keys and removes
265+ them from the xenstore parameter record data for the given VM.
266+ If the key doesn't exist, the request is ignored.
267+ """
268+ vm = self._get_vm_opaque_ref(instance_or_vm)
269+ if isinstance(key_or_keys, basestring):
270+ keys = [key_or_keys]
271+ else:
272+ keys = key_or_keys
273+ for key in keys:
274+ self._session.call_xenapi_request('VM.remove_from_xenstore_data',
275+ (vm, key))
276+
277+ def clear_param_xenstore(self, instance_or_vm):
278+ """Removes all data from the xenstore parameter record for this VM."""
279+ self.write_to_param_xenstore(instance_or_vm, {})
280+ ########################################################################
281
282=== modified file 'nova/virt/xenapi_conn.py'
283--- nova/virt/xenapi_conn.py 2010-12-30 21:23:14 +0000
284+++ nova/virt/xenapi_conn.py 2011-01-04 21:22:15 +0000
285@@ -1,6 +1,7 @@
286 # vim: tabstop=4 shiftwidth=4 softtabstop=4
287
288 # Copyright (c) 2010 Citrix Systems, Inc.
289+# Copyright 2010 OpenStack LLC.
290 #
291 # Licensed under the Apache License, Version 2.0 (the "License"); you may
292 # not use this file except in compliance with the License. You may obtain
293@@ -19,15 +20,15 @@
294
295 The concurrency model for this class is as follows:
296
297-All XenAPI calls are on a thread (using t.i.t.deferToThread, via the decorator
298-deferredToThread). They are remote calls, and so may hang for the usual
299-reasons. They should not be allowed to block the reactor thread.
300+All XenAPI calls are on a green thread (using eventlet's "tpool"
301+thread pool). They are remote calls, and so may hang for the usual
302+reasons.
303
304 All long-running XenAPI calls (VM.start, VM.reboot, etc) are called async
305-(using XenAPI.VM.async_start etc). These return a task, which can then be
306-polled for completion. Polling is handled using reactor.callLater.
307+(using XenAPI.VM.async_start etc). These return a task, which can then be
308+polled for completion.
309
310-This combination of techniques means that we don't block the reactor thread at
311+This combination of techniques means that we don't block the main thread at
312 all, and at the same time we don't hold lots of threads waiting for
313 long-running operations.
314
315@@ -81,7 +82,7 @@
316 flags.DEFINE_float('xenapi_task_poll_interval',
317 0.5,
318 'The interval used for polling of remote tasks '
319- '(Async.VM.start, etc). Used only if '
320+ '(Async.VM.start, etc). Used only if '
321 'connection_type=xenapi.')
322 flags.DEFINE_float('xenapi_vhd_coalesce_poll_interval',
323 5.0,
324@@ -213,6 +214,14 @@
325 f = f.__getattr__(m)
326 return tpool.execute(f, *args)
327
328+ def call_xenapi_request(self, method, *args):
329+ """Some interactions with dom0, such as interacting with xenstore's
330+ param record, require using the xenapi_request method of the session
331+ object. This wraps that call on a background thread.
332+ """
333+ f = self._session.xenapi_request
334+ return tpool.execute(f, method, *args)
335+
336 def async_call_plugin(self, plugin, fn, args):
337 """Call Async.host.call_plugin on a background thread."""
338 return tpool.execute(self._unwrap_plugin_exceptions,
339@@ -222,7 +231,6 @@
340 def wait_for_task(self, id, task):
341 """Return the result of the given task. The task is polled
342 until it completes."""
343-
344 done = event.Event()
345 loop = utils.LoopingCall(self._poll_task, id, task, done)
346 loop.start(FLAGS.xenapi_task_poll_interval, now=True)
347@@ -235,7 +243,7 @@
348 return self.XenAPI.Session(url)
349
350 def _poll_task(self, id, task, done):
351- """Poll the given XenAPI task, and fire the given Deferred if we
352+ """Poll the given XenAPI task, and fire the given action if we
353 get a result."""
354 try:
355 name = self._session.xenapi.task.get_name_label(task)
356@@ -290,7 +298,7 @@
357
358
359 def _parse_xmlrpc_value(val):
360- """Parse the given value as if it were an XML-RPC value. This is
361+ """Parse the given value as if it were an XML-RPC value. This is
362 sometimes used as the format for the task.result field."""
363 if not val:
364 return val
365
366=== modified file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py'
367--- plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 2010-08-02 23:52:06 +0000
368+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/pluginlib_nova.py 2011-01-04 21:22:15 +0000
369@@ -45,6 +45,7 @@
370 def __init__(self, *args):
371 Exception.__init__(self, *args)
372
373+
374 class ArgumentError(PluginError):
375 """Raised when required arguments are missing, argument values are invalid,
376 or incompatible arguments are given.
377@@ -67,6 +68,7 @@
378
379 ARGUMENT_PATTERN = re.compile(r'^[a-zA-Z0-9_:\.\-,]+$')
380
381+
382 def validate_exists(args, key, default=None):
383 """Validates that a string argument to a RPC method call is given, and
384 matches the shell-safe regex, with an optional default value in case it
385@@ -76,20 +78,24 @@
386 """
387 if key in args:
388 if len(args[key]) == 0:
389- raise ArgumentError('Argument %r value %r is too short.' % (key, args[key]))
390+ raise ArgumentError('Argument %r value %r is too short.' %
391+ (key, args[key]))
392 if not ARGUMENT_PATTERN.match(args[key]):
393- raise ArgumentError('Argument %r value %r contains invalid characters.' % (key, args[key]))
394+ raise ArgumentError('Argument %r value %r contains invalid '
395+ 'characters.' % (key, args[key]))
396 if args[key][0] == '-':
397- raise ArgumentError('Argument %r value %r starts with a hyphen.' % (key, args[key]))
398+ raise ArgumentError('Argument %r value %r starts with a hyphen.'
399+ % (key, args[key]))
400 return args[key]
401 elif default is not None:
402 return default
403 else:
404 raise ArgumentError('Argument %s is required.' % key)
405
406+
407 def validate_bool(args, key, default=None):
408- """Validates that a string argument to a RPC method call is a boolean string,
409- with an optional default value in case it does not exist.
410+ """Validates that a string argument to a RPC method call is a boolean
411+ string, with an optional default value in case it does not exist.
412
413 Returns the python boolean value.
414 """
415@@ -99,7 +105,9 @@
416 elif value.lower() == 'false':
417 return False
418 else:
419- raise ArgumentError("Argument %s may not take value %r. Valid values are ['true', 'false']." % (key, value))
420+ raise ArgumentError("Argument %s may not take value %r. "
421+ "Valid values are ['true', 'false']." % (key, value))
422+
423
424 def exists(args, key):
425 """Validates that a freeform string argument to a RPC method call is given.
426@@ -110,6 +118,7 @@
427 else:
428 raise ArgumentError('Argument %s is required.' % key)
429
430+
431 def optional(args, key):
432 """If the given key is in args, return the corresponding value, otherwise
433 return None"""
434@@ -122,13 +131,14 @@
435
436 def get_domain_0(session):
437 this_host_ref = get_this_host(session)
438- expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"' % this_host_ref
439+ expr = 'field "is_control_domain" = "true" and field "resident_on" = "%s"'
440+ expr = expr % this_host_ref
441 return session.xenapi.VM.get_all_records_where(expr).keys()[0]
442
443
444 def create_vdi(session, sr_ref, name_label, virtual_size, read_only):
445 vdi_ref = session.xenapi.VDI.create(
446- { 'name_label': name_label,
447+ {'name_label': name_label,
448 'name_description': '',
449 'SR': sr_ref,
450 'virtual_size': str(virtual_size),
451@@ -138,7 +148,7 @@
452 'xenstore_data': {},
453 'other_config': {},
454 'sm_config': {},
455- 'tags': [] })
456+ 'tags': []})
457 logging.debug('Created VDI %s (%s, %s, %s) on %s.', vdi_ref, name_label,
458 virtual_size, read_only, sr_ref)
459 return vdi_ref
460
461=== added file 'plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py'
462--- plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py 1970-01-01 00:00:00 +0000
463+++ plugins/xenserver/xenapi/etc/xapi.d/plugins/xenstore.py 2011-01-04 21:22:15 +0000
464@@ -0,0 +1,180 @@
465+#!/usr/bin/env python
466+
467+# Copyright (c) 2010 Citrix Systems, Inc.
468+# Copyright 2010 OpenStack LLC.
469+# Copyright 2010 United States Government as represented by the
470+# Administrator of the National Aeronautics and Space Administration.
471+# All Rights Reserved.
472+#
473+# Licensed under the Apache License, Version 2.0 (the "License"); you may
474+# not use this file except in compliance with the License. You may obtain
475+# a copy of the License at
476+#
477+# http://www.apache.org/licenses/LICENSE-2.0
478+#
479+# Unless required by applicable law or agreed to in writing, software
480+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
481+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
482+# License for the specific language governing permissions and limitations
483+# under the License.
484+
485+#
486+# XenAPI plugin for reading/writing information to xenstore
487+#
488+
489+try:
490+ import json
491+except ImportError:
492+ import simplejson as json
493+import subprocess
494+
495+import XenAPIPlugin
496+
497+import pluginlib_nova as pluginlib
498+pluginlib.configure_logging("xenstore")
499+
500+
501+def jsonify(fnc):
502+ def wrapper(*args, **kwargs):
503+ return json.dumps(fnc(*args, **kwargs))
504+ return wrapper
505+
506+
507+@jsonify
508+def read_record(self, arg_dict):
509+ """Returns the value stored at the given path for the given dom_id.
510+ These must be encoded as key/value pairs in arg_dict. You can
511+ optinally include a key 'ignore_missing_path'; if this is present
512+ and boolean True, attempting to read a non-existent path will return
513+ the string 'None' instead of raising an exception.
514+ """
515+ cmd = "xenstore-read /local/domain/%(dom_id)s/%(path)s" % arg_dict
516+ try:
517+ return _run_command(cmd).rstrip("\n")
518+ except pluginlib.PluginError, e:
519+ if arg_dict.get("ignore_missing_path", False):
520+ cmd = "xenstore-exists /local/domain/%(dom_id)s/%(path)s; echo $?"
521+ cmd = cmd % arg_dict
522+ ret = _run_command(cmd).strip()
523+ # If the path exists, the cmd should return "0"
524+ if ret != "0":
525+ # No such path, so ignore the error and return the
526+ # string 'None', since None can't be marshalled
527+ # over RPC.
528+ return "None"
529+ # Either we shouldn't ignore path errors, or another
530+ # error was hit. Re-raise.
531+ raise
532+
533+
534+@jsonify
535+def write_record(self, arg_dict):
536+ """Writes to xenstore at the specified path. If there is information
537+ already stored in that location, it is overwritten. As in read_record,
538+ the dom_id and path must be specified in the arg_dict; additionally,
539+ you must specify a 'value' key, whose value must be a string. Typically,
540+ you can json-ify more complex values and store the json output.
541+ """
542+ cmd = "xenstore-write /local/domain/%(dom_id)s/%(path)s '%(value)s'"
543+ cmd = cmd % arg_dict
544+ _run_command(cmd)
545+ return arg_dict["value"]
546+
547+
548+@jsonify
549+def list_records(self, arg_dict):
550+ """Returns all the stored data at or below the given path for the
551+ given dom_id. The data is returned as a json-ified dict, with the
552+ path as the key and the stored value as the value. If the path
553+ doesn't exist, an empty dict is returned.
554+ """
555+ cmd = "xenstore-ls /local/domain/%(dom_id)s/%(path)s" % arg_dict
556+ cmd = cmd.rstrip("/")
557+ try:
558+ recs = _run_command(cmd)
559+ except pluginlib.PluginError, e:
560+ if "No such file or directory" in "%s" % e:
561+ # Path doesn't exist.
562+ return {}
563+ return str(e)
564+ raise
565+ base_path = arg_dict["path"]
566+ paths = _paths_from_ls(recs)
567+ ret = {}
568+ for path in paths:
569+ if base_path:
570+ arg_dict["path"] = "%s/%s" % (base_path, path)
571+ else:
572+ arg_dict["path"] = path
573+ rec = read_record(self, arg_dict)
574+ try:
575+ val = json.loads(rec)
576+ except ValueError:
577+ val = rec
578+ ret[path] = val
579+ return ret
580+
581+
582+@jsonify
583+def delete_record(self, arg_dict):
584+ """Just like it sounds: it removes the record for the specified
585+ VM and the specified path from xenstore.
586+ """
587+ cmd = "xenstore-rm /local/domain/%(dom_id)s/%(path)s" % arg_dict
588+ return _run_command(cmd)
589+
590+
591+def _paths_from_ls(recs):
592+ """The xenstore-ls command returns a listing that isn't terribly
593+ useful. This method cleans that up into a dict with each path
594+ as the key, and the associated string as the value.
595+ """
596+ ret = {}
597+ last_nm = ""
598+ level = 0
599+ path = []
600+ ret = []
601+ for ln in recs.splitlines():
602+ nm, val = ln.rstrip().split(" = ")
603+ barename = nm.lstrip()
604+ this_level = len(nm) - len(barename)
605+ if this_level == 0:
606+ ret.append(barename)
607+ level = 0
608+ path = []
609+ elif this_level == level:
610+ # child of same parent
611+ ret.append("%s/%s" % ("/".join(path), barename))
612+ elif this_level > level:
613+ path.append(last_nm)
614+ ret.append("%s/%s" % ("/".join(path), barename))
615+ level = this_level
616+ elif this_level < level:
617+ path = path[:this_level]
618+ ret.append("%s/%s" % ("/".join(path), barename))
619+ level = this_level
620+ last_nm = barename
621+ return ret
622+
623+
624+def _run_command(cmd):
625+ """Abstracts out the basics of issuing system commands. If the command
626+ returns anything in stderr, a PluginError is raised with that information.
627+ Otherwise, the output from stdout is returned.
628+ """
629+ pipe = subprocess.PIPE
630+ proc = subprocess.Popen([cmd], shell=True, stdin=pipe, stdout=pipe,
631+ stderr=pipe, close_fds=True)
632+ proc.wait()
633+ err = proc.stderr.read()
634+ if err:
635+ raise pluginlib.PluginError(err)
636+ return proc.stdout.read()
637+
638+
639+if __name__ == "__main__":
640+ XenAPIPlugin.dispatch(
641+ {"read_record": read_record,
642+ "write_record": write_record,
643+ "list_records": list_records,
644+ "delete_record": delete_record})