All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC] native python Xenstore module
@ 2015-02-03 14:01 Simon Rowe
  2015-02-24 16:41 ` Ian Campbell
  0 siblings, 1 reply; 5+ messages in thread
From: Simon Rowe @ 2015-02-03 14:01 UTC (permalink / raw)
  To: xen-devel

The current Python interface to Xenstore is just a thin binding to the
C libxenstore library. This means that it is architecture-specific and
makes it awkward to use in platform-independent code like the XenServer
guest agent.

The Xenstore protocol is simple and quite straightforward to implement
natively in Python.


Design & Implementation

Users create an instance of the Xenstore object. This is a singleton
that holds all the global state (xenbus fd etc).

Users invoke methods e.g. read() that block awaiting a response from
xenstored. Methods return either the result or throw XenstoreException
with a string representation of the error e.g. 'ENOENT'.

The methods attempt to present complex results using native datatypes
(lists, dictionaries) rather the usual Xenstore string values e.g.

     {6: 'read-write', 9: 'none'}

rather than

     'b6n9'


Transactions are managed using instances of the Transaction object. Each
instance must be utilized thus, either

     t.start()
     # zero or more operations
     t.commit()

or

     t.start()
     # zero or more operations
     t.abort()


Users should handle commit() raising XenstoreException('EAGAIN')
appropriately, e.g. replaying the whole start(), operations, commit()
sequence.

Watches are complicated because they are not synchronous. When the first
watch is registered a background thread is started. This thread takes
responsibility for the reading of all data from the xenbus fd. Responses
for operations are forwarded to the foreground thread using a Queue.

Users of watch() supply the path to watch, a callback function and
(optionally) parameters. The callback function is invoked with the path
and the optional parameters. watch() returns a string token that is
passed to unwatch() to remove a watch.


#!/bin/env python

import os
import Queue
import select
import struct
import tempfile
import threading

class XenstoreException(StandardError):
     pass

"""
Background thread that reads from xenbus, acting on watch events and 
forwarding
operation responses to the foreground thread.
"""
def handle_watches(xs):
     while not xs.shutdown_event.isSet():
         r, _, __ = select.select([xs.pipe_r, xs.xb_fd], [], [])
         if xs.xb_fd in r:
             # read and unpack header
             hdr = xs.xb_fd.read(16)
             op, _, __, l = struct.unpack('=IIII', hdr)
             value = None
             if l > 0:
                 value = xs.xb_fd.read(l)
             if op == xs.XS_WATCH_EVENT:
                 path, token, _ = value.split('\0', 2)
                 if token in xs.watches:
                     # invoke user callback
                     xs.watches[token]['cb'](path, 
*xs.watches[token]['cbargs'])
             else:
                 # response to an operation, handle in main thread
                 xs.queue.put(hdr)
                 if value:
                     xs.queue.put(value)

class Xenstore(object):
     """
     Parent Xenstore handle (singleton).
     """

     # from xen/include/public/io/xs_wire.h
     XS_READ = 2
     XS_WATCH = 4
     XS_GET_PERMS = 3
     XS_UNWATCH = 5
     XS_TRANSACTION_START = 6
     XS_TRANSACTION_END = 7
     XS_WRITE = 11
     XS_MKDIR = 12
     XS_RM = 13
     XS_SET_PERMS = 14
     XS_WATCH_EVENT = 15
     XS_ERROR = 16

     if os.uname()[0] == 'Linux':
         DEV_PATH = '/proc/xen/xenbus'
     elif os.uname()[0] == 'NetBSD':
         DEV_PATH = '/kern/xen/xenbus'
     else:
         DEV_PATH = '/dev/xen/xenbus'

     PERM_NONE, PERM_READ, PERM_WRITE, PERM_READ_WRITE = range(4)

     __single = None

     def __new__(classtype, *args, **kwargs):
         if classtype != type(classtype.__single):
             classtype.__single = object.__new__(classtype, *args, **kwargs)
         return classtype.__single

     def __init__(self):
         self.xb_fd = open(self.DEV_PATH, 'r+', 0)
         self.watch_thread = None
         self.queue = None
         self.watches = {}

     def __del__(self):
         self.xb_fd.close()

     def do_op(self, ctxt, op, value = '', req = 0):
         """
         Perform Xenstore operation $op and return response.
         """

         if ctxt:
             tx_id = ctxt.tx_id
         else:
             tx_id = 0

         ret = None
         self.xb_fd.write(struct.pack('=IIII', op, req, tx_id, len(value)))
         if len(value):
             self.xb_fd.write(value)
         if self.queue:
             hdr = self.queue.get()
         else:
             hdr = self.xb_fd.read(16)
         r_op, req, tx, l = struct.unpack('=IIII', hdr)
         if l > 0:
             if self.queue:
                 ret = self.queue.get()
             else:
                 ret = self.xb_fd.read(l)
             if r_op == self.XS_ERROR:
                 raise XenstoreException, ret[:-1]
         return ret

     def _read(self, ctxt, *vars):
         d = {}
         for var in vars:
             d[var] = self.do_op(ctxt, self.XS_READ, var + '\0')
             if len(vars) == 1:
                 return d[var]
         return d

     def read(self, *vars):
         """
         Return a dictionary of Xenstore values for $vars (or a string 
if only a
         single key is passed in).
         """

         return self._read(None, *vars)

     def _mkdir(self, ctxt, *vars):
         for var in vars:
             self.do_op(ctxt, self.XS_MKDIR, var + '\0')

     def mkdir(self, *vars):
         """
         Create empty directories in Xenstore.
         """

         self._mkdir(None, *vars)

     def _rm(self, ctxt, *vars):
         for var in vars:
             self.do_op(ctxt, self.XS_RM, var + '\0')

     def rm(self, *vars):
         """
         Remove all keys in $vars from Xenstore.
         """
         self._rm(None, *vars)

     def write(self, vars, ctxt = None):
         """
         Update Xenstore with the keys and values in dictionary $vars.
         """

         for k, v in vars.items():
             self.do_op(ctxt, self.XS_WRITE, k + '\0' + v)

     perm_map = {'n': PERM_NONE, 'r': PERM_READ, 'w': PERM_WRITE, 'b': 
PERM_READ_WRITE}
     rev_perm_map = {PERM_NONE: 'n', PERM_READ: 'r', PERM_WRITE: 'w', 
PERM_READ_WRITE: 'b'}

     def _get_permissions(self, ctxt, *paths):
         d = {}
         for path in paths:
             t = self.do_op(ctxt, self.XS_GET_PERMS, path + '\0')
             perm_d = {}
             for e in t[:-1].split('\0'):
                 perm_d[int(e[1:])] = self.perm_map[e[0]]
             d[path] = perm_d
             if len(paths) == 1:
                 return d[path]
         return d

     def get_permissions(self, *paths):
         """
         Return a list of permission dictionaries (or a single if only a
         single key is passed in.
         """
         return self._get_permissions(None, *paths)

     def set_permissions(self, path, perms, ctxt = None):
         """
         Set the permissions of $path based on the dictionary $perms.
         """
         perm_l = []
         for k, v in perms.items():
             perm_l.append("%c%d" % (self.rev_perm_map.get(v, 'n'), k))
         self.do_op(None, self.XS_SET_PERMS, path + '\0' + 
'\0'.join(perm_l) + '\0')

     def watch(self, path, callback, *callback_args):
         """
         Watch $path (and its subordinates), invoking $callback($path, 
$callback_args)
         in a background thread.

         Returns a token to be used by unwatch().
         """
         if not self.watch_thread:
             self.watch_thread = threading.Thread(target=handle_watches, 
args=(self,))
             self.watch_thread.setDaemon(True)
             self.shutdown_event = threading.Event()
             self.queue = Queue.Queue(2)
             self.pipe_r, self.pipe_w = os.pipe()
             self.watches = {}
             self.watch_thread.start()

         fh = tempfile.NamedTemporaryFile(prefix = 'xs')
         token = fh.name
         self.watches[token] = {'fh': fh, 'path': path, 'cb': callback, 
'cbargs': callback_args}
         self.do_op(None, self.XS_WATCH, path + '\0' + token + '\0')
         return token

     def unwatch(self, path, token):
         """
         Stop watching the path monitored by the watch() call that 
returned $token.
         """
         if token in self.watches:
             self.do_op(None, self.XS_UNWATCH, path + '\0' + token + '\0')
             self.watches[token]['fh'].close()
             del self.watches[token]

     def unwatch_all(self):
         for k, v in self.watches.items():
             self.unwatch(v['path'], k)

     def watch_stop(self):
         if self.watch_thread:
             self.shutdown_event.set()
             os.write(self.pipe_w, 'x')
             self.watch_thread.join()
             self.watch_thread = None
             self.queue = None
             os.close(self.pipe_r)
             os.close(self.pipe_w)

class Transaction(object):
     """
     Xenstore transaction instance.
     """

     def __init__(self):
         self.xs = Xenstore()
         self.tx_id = 0

     def start(self):
         """
         Start a new transaction.
         """

         self.tx_id = 0
         self.tx_id = int(self.xs.do_op(self, 
self.xs.XS_TRANSACTION_START, '\0')[:-1])

     def commit(self):
         """
         Commit all modifications in this transaction to Xenstore.
         """

         self.xs.do_op(self, self.xs.XS_TRANSACTION_END, 'T\0')

     def abort(self):
         """
         Discard all modifications in this transaction.
         """

         self.xs.do_op(self, self.xs.XS_TRANSACTION_END, 'F\0')

     def read(self, *vars):
         return self.xs._read(self, *vars)

     def mkdir(self, *vars):
         self.xs._mkdir(self, *vars)

     def rm(self, *vars):
         self.xs._read(self, *vars)

     def write(self, vars):
         self.xs.write(vars, self)

     def get_permissions(self, *paths):
         return self.xs_get_permissions(self, *paths)

     def set_permissions(self, path, perms):
         self.xs.set_permissions(perms, self)


# example to code to dev test

if __name__ == '__main__':
     def watch_cb(key, n):
         print "Watch fired:", key, n

     repeat_count = 5

     # create xenstore handle
     xs = Xenstore()
     # read three keys, returns three values
     print xs.read('domid', 'vm', 'name')
     # write a key/value
     xs.write({'new': 'stuff'})
     # verify contents were written
     assert xs.read('new') == 'stuff'
     print xs.get_permissions('new')
     xs.set_permissions('new', {0: xs.PERM_READ_WRITE, os.getpid(): 
xs.PERM_READ})
     print xs.get_permissions('new')
     xs.mkdir('another-new')
     # write another key
     xs.write({'new/path': 'more stuff'})
     xs.read('new/path')
     # delete key
     xs.rm('new/path')
     # attempt to read deleted key
     try:
         print xs.read('new/path')
     except XenstoreException, e:
         print "Failed to read (expected)", e

     # create and start transaction
     tx = Transaction()
     tx.start()
     tx.write({'tnew': 'committed'})
     # commit and verify value is updated
     tx.commit()
     assert xs.read('tnew') == 'committed'

     # start new transaction
     tx.start()
     tx.write({'tnew': 'aborted'})
     # abort and verify value is NOT updated
     tx.abort()
     assert xs.read('tnew') != 'aborted'

     # watch key
     xs.watch('new', watch_cb, 7)

     d = {}
     # repeatedly run a large transaction to cause collisions between 
two program instances
     for n in range(1, 200):
         d["collision/key"+str(n)] = "value"+str(n)
     while True:
         try:
             tx.start()
             tx.write(d)
             tx.commit()
         except XenstoreException, e:
             if str(e) == 'EAGAIN':
                 repeat_count -= 1
                 if repeat_count == 0:
                     break
                 print "Collision on commit, repeating"
             else:
                 print "Unexpected Xenstore error", e
                 break

     print "End of test"
     xs.unwatch_all()
     # stop background watch thread
     xs.watch_stop()

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [RFC] native python Xenstore module
  2015-02-03 14:01 [RFC] native python Xenstore module Simon Rowe
@ 2015-02-24 16:41 ` Ian Campbell
  2015-02-24 17:06   ` Sander Eikelenboom
  2015-02-25  9:39   ` Simon Rowe
  0 siblings, 2 replies; 5+ messages in thread
From: Ian Campbell @ 2015-02-24 16:41 UTC (permalink / raw)
  To: Simon Rowe; +Cc: xen-devel

On Tue, 2015-02-03 at 14:01 +0000, Simon Rowe wrote:
> The current Python interface to Xenstore is just a thin binding to the
> C libxenstore library. This means that it is architecture-specific and
> makes it awkward to use in platform-independent code like the XenServer
> guest agent.

Are/were you aware of https://pypi.python.org/pypi/pyxs which sounds
like something similar, judging from its blurb alone?

https://launchpad.net/pyxenstore/ might be too, although I don't know if
that one is pure python.

Anyhow is your intention to have this added to the xen.git tree, perhaps
even replacing tools/python/xen/lowlevel/xs/ or just to gather feedback
on some code destined for an external project?

Personally I don't see a problem with including it in tree, replacing
xen.lowlevel.xs might require thinking a little about API compatibility,
which might be a pain...

Ian.

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [RFC] native python Xenstore module
  2015-02-24 16:41 ` Ian Campbell
@ 2015-02-24 17:06   ` Sander Eikelenboom
  2015-02-25  9:39   ` Simon Rowe
  1 sibling, 0 replies; 5+ messages in thread
From: Sander Eikelenboom @ 2015-02-24 17:06 UTC (permalink / raw)
  To: Ian Campbell; +Cc: xen-devel, Simon Rowe


Tuesday, February 24, 2015, 5:41:40 PM, you wrote:

> On Tue, 2015-02-03 at 14:01 +0000, Simon Rowe wrote:
>> The current Python interface to Xenstore is just a thin binding to the
>> C libxenstore library. This means that it is architecture-specific and
>> makes it awkward to use in platform-independent code like the XenServer
>> guest agent.

> Are/were you aware of https://pypi.python.org/pypi/pyxs which sounds
> like something similar, judging from its blurb alone?

I'm using the above for a project, seems to work fine for my purposes. 
Although you have to get a hang for working around some of the xenstore
watch oddities/limitations (firing once after setup, firing on child changes
without you knowing what actually changed because you don't have the previous 
value, but most of the things can be worked around using the watch 
token. I use a pickled dict of the watched path and the current value as the 
token at that seems to work).

--
Sander

> https://launchpad.net/pyxenstore/ might be too, although I don't know if
> that one is pure python.

> Anyhow is your intention to have this added to the xen.git tree, perhaps
> even replacing tools/python/xen/lowlevel/xs/ or just to gather feedback
> on some code destined for an external project?

> Personally I don't see a problem with including it in tree, replacing
> xen.lowlevel.xs might require thinking a little about API compatibility,
> which might be a pain...

> Ian.

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [RFC] native python Xenstore module
  2015-02-24 16:41 ` Ian Campbell
  2015-02-24 17:06   ` Sander Eikelenboom
@ 2015-02-25  9:39   ` Simon Rowe
  2015-02-25 10:12     ` Ian Campbell
  1 sibling, 1 reply; 5+ messages in thread
From: Simon Rowe @ 2015-02-25  9:39 UTC (permalink / raw)
  To: Ian Campbell; +Cc: xen-devel

On 24/02/15 16:41, Ian Campbell wrote:
> Are/were you aware ofhttps://pypi.python.org/pypi/pyxs  which sounds
> like something similar, judging from its blurb alone?

No, I wasn't. It certainly looks a more solid implementation than mine.
>
> https://launchpad.net/pyxenstore/  might be too, although I don't know if
> that one is pure python.

This looks to be just a higher level C to python interface than 
xen.lowlevel.xs.

> Anyhow is your intention to have this added to the xen.git tree, perhaps
> even replacing tools/python/xen/lowlevel/xs/ or just to gather feedback
> on some code destined for an external project?
I was interested in feedback about having implementations such as these 
more easily consumed by guest distros. At present using something linked 
against libxenstore.so requires Xen tools to have been built 
(obviously). If there are already serviceable implementations available 
(which it appears there is) then the best policy is probably to 
encourage distros to package and include them.

> Personally I don't see a problem with including it in tree, replacing
> xen.lowlevel.xs might require thinking a little about API compatibility,
> which might be a pain...
pyxs even has a compat interface.

Simon

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [RFC] native python Xenstore module
  2015-02-25  9:39   ` Simon Rowe
@ 2015-02-25 10:12     ` Ian Campbell
  0 siblings, 0 replies; 5+ messages in thread
From: Ian Campbell @ 2015-02-25 10:12 UTC (permalink / raw)
  To: Simon Rowe; +Cc: xen-devel

On Wed, 2015-02-25 at 09:39 +0000, Simon Rowe wrote:
> On 24/02/15 16:41, Ian Campbell wrote:
> > Are/were you aware ofhttps://pypi.python.org/pypi/pyxs  which sounds
> > like something similar, judging from its blurb alone?
> 
> No, I wasn't. It certainly looks a more solid implementation than mine.

https://github.com/selectel/pyxs seems to be the real upstream, pypi
seems a bit behind.

> >
> > https://launchpad.net/pyxenstore/  might be too, although I don't know if
> > that one is pure python.
> 
> This looks to be just a higher level C to python interface than 
> xen.lowlevel.xs.
> 
> > Anyhow is your intention to have this added to the xen.git tree, perhaps
> > even replacing tools/python/xen/lowlevel/xs/ or just to gather feedback
> > on some code destined for an external project?
> I was interested in feedback about having implementations such as these 
> more easily consumed by guest distros. At present using something linked 
> against libxenstore.so requires Xen tools to have been built 
> (obviously).

Yes, I can see that being rather annoying to have to arrange.

>  If there are already serviceable implementations available 
> (which it appears there is) then the best policy is probably to 
> encourage distros to package and include them.

I think that's probably the best approach.

(https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=692516 for Debian)

> > Personally I don't see a problem with including it in tree, replacing
> > xen.lowlevel.xs might require thinking a little about API compatibility,
> > which might be a pain...
> pyxs even has a compat interface.

Neat!

Since today happens to be a doc day I've updated
http://wiki.xen.org/wiki/XenStore_Reference to point to pyxs (and nuked
a load of old xend related stuff).

/me ponders "git rm tools/python/xen/lowlevel/xs" ;-)

Ian.

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2015-02-25 10:14 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2015-02-03 14:01 [RFC] native python Xenstore module Simon Rowe
2015-02-24 16:41 ` Ian Campbell
2015-02-24 17:06   ` Sander Eikelenboom
2015-02-25  9:39   ` Simon Rowe
2015-02-25 10:12     ` Ian Campbell

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.