Merge lp:~ed-leafe/nova/xenstore-plugin into lp:~hudson-openstack/nova/trunk
- xenstore-plugin
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Josh Kearney (community) | Approve | ||
Cory Wright (community) | Approve | ||
Review via email: mp+44931@code.launchpad.net |
Commit message
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
- 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
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.
Cory Wright (corywright) wrote : | # |
The changes look good to me.
Josh Kearney (jk0) wrote : | # |
I like the addition and usage of _get_vm_
Everything looks good to me.
Preview Diff
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}) |
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.