[node-patches] Change in ovirt-node[master]: [RFE] tools: Add a simple "service" pub/call tool

fabiand at fedoraproject.org fabiand at fedoraproject.org
Mon Aug 26 18:07:22 UTC 2013


Fabian Deutsch has uploaded a new change for review.

Change subject: [RFE] tools: Add a simple "service" pub/call tool
......................................................................

[RFE] tools: Add a simple "service" pub/call tool

This new tool is inteded to make the discovery of relevant nodes easier.
The tool provides a framework for the registration of "features" and
"methods".
Features are used to state the presence of a specific high-level feature
e.g. presence of VDSM, or Gluster.
Methods are actually a simple RPC mechanism to trigger methods on the
Node.

Demo
----
This tech demo includes a simple daemon which offers this API via a http
daemon.

$ python info.py

launches the daemon, which is then listening on port 8082.

$ curl http://127.0.0.1:8082

Can be used to view the static informations

$ curl 127.0.0.1:8082/methods/foo/bar?paramA=1&paramB=2

can be used to trigger the method bar in the namespace foo with the
params paramA and paramB, so something like:

foo.bar(paramA=1, paramB=2)

Concepts
--------
A feature or method is always owned by someone and is always sitting in
some namespace.
The registration mechanism is intended to work well with Node's current
plugin mechanism.

The documentation is still sparse. Mabye the name feature should be
replaced by property. But those are details.

Change-Id: If081e5cd5fc9d038c9afaef0c97349f83e6f3a39
Bug-Url: https://bugzilla.redhat.com/show_bug.cgi?id=999325
Signed-off-by: Fabian Deutsch <fabiand at fedoraproject.org>
---
A src/ovirt/node/tools/info.py
A src/ovirt/node/tools/info.xsl
2 files changed, 391 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/ovirt-node refs/changes/77/18577/1

diff --git a/src/ovirt/node/tools/info.py b/src/ovirt/node/tools/info.py
new file mode 100644
index 0000000..336d1b1
--- /dev/null
+++ b/src/ovirt/node/tools/info.py
@@ -0,0 +1,283 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# info.py - Copyright (C) 2013 Red Hat, Inc.
+# Written by Fabian Deutsch <fabiand at redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.  A copy of the GNU General Public License is
+# also available at http://www.gnu.org/copyleft/gpl.html.
+from bottle import app
+from lxml import etree
+from ovirt.node import base
+import bottle
+import os.path
+
+
+class Namespaces(base.Base):
+    """A class to organize Objects which reside in namespaces
+    But the namespace is part of the object, and not a property of container.
+    This class ensures that only 
+    """
+    items = None
+
+    def __init__(self):
+        self.items = set()
+
+    def __find(self, path):
+        """Find the item with path <path> in items
+        Returns:
+            None if no item with this path was found.
+            Otherwise the item with the given path.
+        """
+        for item in self.items:
+            if item.path() == path:
+                return item
+        return None
+
+    def __contains__(self, path):
+        """Check if an item with the path <path> is already known
+        """
+        return self.__find(path) is not None
+
+    def __getitem__(self, path):
+        """Get the item with the path <path> or raise a KeyError
+        """
+        if not path in self:
+            raise KeyError
+        return self.__find(path)
+
+    def add(self, item):
+        """Add an item and ensure that no other item with that namespace exists
+        """
+        candidate = self.__find(item.path())
+        if candidate and candidate != item:
+            raise KeyError("An item with this path already exists: %s" %
+                           item.path())
+        return self.items.add(item)
+
+    def remove(self, item):
+        """Remove an item
+        """
+        self.items.remove(item)
+
+    def __iter__(self):
+        """Iter through all items in all namespaces
+        """
+        for item in self.items:
+            yield item
+
+
+class NodeInfo(base.Base):
+    features = Namespaces()
+    methods = Namespaces()
+
+    def register(self, oobj):
+        if issubclass(type(oobj), NodeInfo.Feature):
+            self.features.add(oobj)
+        elif issubclass(type(oobj), NodeInfo.Method):
+            self.methods.add(oobj)
+        else:
+            raise RuntimeError("Can not register object: %s" % oobj)
+
+
+    class Owner(base.Base):
+        name = None
+
+        def __init__(self, **kwargs):
+            super(NodeInfo.Owner, self).__init__()
+            for k, v in kwargs.items():
+                if any(k in t.__dict__ for t in type(self).mro()):
+                    self.__dict__[k] = v
+
+    class OwnedObject(Owner):
+        owner = None
+        version = None
+        namespace = None
+        description = None
+        documentation = None
+
+        def __init__(self, **kwargs):
+            super(NodeInfo.OwnedObject, self).__init__(**kwargs)
+            self.namespace = self.namespace or self.owner.name
+
+        def path(self):
+            return "%s/%s" % (self.namespace, self.name)
+
+    class Feature(OwnedObject):
+        pass
+
+    class Method(OwnedObject):
+        """Bang
+        """
+        func = None
+
+        @property
+        def arguments(self):
+            varnames = list(self.func.func_code.co_varnames)
+            return varnames[1:] if varnames[0] == "self" else varnames
+
+        class Result(base.Base):
+            retval = None
+            exception = None
+
+        def __call__(self, **kwargs):
+            if sorted(kwargs.keys()) != sorted(self.arguments):
+                raise RuntimeError("%s vs %s" % (kwargs.keys(), self.arguments))
+            result = NodeInfo.Method.Result()
+            try:
+                # pylint: disable-msg=E1102
+                result.retval = self.func(**kwargs)
+                # pylint: enable-msg=E1102
+            except Exception as e:
+                result.exception = e
+            return result
+
+
+class XmlBuilder(base.Base):
+    root = None
+    xslt_url = None
+
+    def build(self, nodeinfo):
+        self.root = etree.Element("node", {"version": "0.1"})
+        if self.xslt_url:
+            pi = etree.PI('xml-stylesheet', 'type="text/xsl" href="%s"' %
+                          self.xslt_url)
+            self.root.addprevious(pi)
+        if issubclass(type(nodeinfo), NodeInfo):
+            self.build_features(nodeinfo.features)
+            self.build_methods(nodeinfo.methods)
+        elif isinstance(nodeinfo, NodeInfo.Method.Result):
+            self.build_result(nodeinfo)
+        else:
+            raise RuntimeError("Can not build XML for object: %s" % nodeinfo)
+        return etree.tostring(self.root.getroottree(), pretty_print=True,
+                              xml_declaration=True,
+                              encoding='utf-8')
+
+    def _build_ownedobject(self, parent, tag, obj, attrs={}):
+        attrs.update({"owner": obj.owner.name,
+                      "namespace": obj.namespace,
+                      "version": obj.version or "",
+                      "description": obj.description or ""})
+        element = etree.SubElement(parent, tag, attrs)
+        doc = obj.documentation or obj.__doc__
+        if doc:
+            docnode = etree.SubElement(element, "documentation")
+            docnode.text = doc
+        return element
+
+    def build_features(self, features):
+        subroot = etree.SubElement(self.root, "features")
+        for feature in features:
+            self._build_ownedobject(subroot, "feature", feature,
+                                    {"name": feature.name})
+
+    def build_methods(self, methods):
+        subroot = etree.SubElement(self.root, "methods")
+        for method in methods:
+            methodroot = self._build_ownedobject(subroot, "method", method,
+                                                 {"name": method.name})
+            argumentsroot = etree.SubElement(methodroot, "arguments")
+            for position, argument in enumerate(method.arguments):
+                etree.SubElement(argumentsroot, "argument",
+                                 {"position": str(position),
+                                  "name": argument})
+
+    def build_result(self, result):
+        root = etree.SubElement(self.root, "method")
+        res = etree.SubElement(root, "result",
+                               {"success": "success"} if not result.exception else {})
+        retval = etree.SubElement(res, "retval",
+                                  {"type": type(result.retval).__name__})
+        retval.text = bytes(result.retval)
+        exception = etree.SubElement(res, "exception",
+                                     {"type":
+                                      type(result.exception).__name__
+                                      if result.exception else ""})
+        exception.text = bytes(result.exception or "")
+
+
+"""
+An example of how to use the stuff above
+"""
+
+class MyBooFeature(NodeInfo.Feature):
+    """This feature represents a major improvement which is required by
+    FooManagement
+    """
+    name = "FooClient"
+    namespace = "FooManagement"
+    version = "1.0"
+
+class MyBooMethod(NodeInfo.Method):
+    """This method can be used to register Foo with FooManagement
+
+    Arguments:
+        host: The address of the FooManagement server
+        ssh_fingerprint: The fingerprint of the public key 
+    """
+    name = "register"
+    namespace = "FooManagement"
+
+    def func(self, host, ssh_fingerprint):
+        if host == ssh_fingerprint:
+            raise RuntimeError()
+        return (host, ssh_fingerprint)
+
+
+def boo(a, b):
+    return MyBooMethod().func(a,b)
+
+
+if __name__ == "__main__":
+    i = NodeInfo()
+
+    # Simple registration
+    owner = NodeInfo.Owner(name="A.I.")
+    i.register(NodeInfo.Feature(owner=owner,
+                                name="foo"))
+    i.register(NodeInfo.Method(owner=owner,
+                               name="foo",
+                               func=boo))
+
+    # "Clean" registration using objects
+    i.register(MyBooFeature(owner=owner))
+    i.register(MyBooMethod(owner=owner))
+
+    app = bottle.Bottle()
+
+    xmlbuilder = XmlBuilder()
+    xmlbuilder.xslt_url = "/info.xsl"
+
+    @app.route("/info.xsl")
+    def xslt():
+        bottle.response.headers['Content-Type'] = 'application/xml'
+        fn = os.path.dirname(__file__) + "info.xsl"
+        print fn, __file__
+        with open(fn) as src:
+            return src.read()
+
+    @app.route("/")
+    def index():
+        bottle.response.headers['Content-Type'] = 'application/xml'
+        return xmlbuilder.build(i)
+
+    @app.route("/methods/<path:path>")
+    def call_method(path):
+        bottle.response.headers['Content-Type'] = 'application/xml'
+        kwargs = dict(bottle.request.query.items())
+        return xmlbuilder.build(i.methods[path](**kwargs))
+
+    bottle.run(app, host='0.0.0.0', port=8082, reloader=True, debug=True)
diff --git a/src/ovirt/node/tools/info.xsl b/src/ovirt/node/tools/info.xsl
new file mode 100644
index 0000000..976dc50
--- /dev/null
+++ b/src/ovirt/node/tools/info.xsl
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="2.0" 
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+  xmlns:fn="http://www.w3.org/2005/xpath-functions">
+
+
+<xsl:template match="/">
+  <html>
+  <head>
+    <title>NodeInfo</title>
+    <style type="text/css">
+    body
+    {
+        font-family: sans-serif;
+    }
+    h1
+    {
+        color: white;
+        background: #888;
+        padding: 1em;
+        margin: -.3em;
+    }
+    h1, h2, h3
+    {
+        font-weight: lighter;
+    }
+    form
+    {
+        display: inline;
+    }
+    </style>
+  </head>
+  <body>
+  <h1>Node Dashboard</h1>
+  <xsl:apply-templates />
+  </body>
+  </html>
+</xsl:template>
+
+<xsl:template match="features">
+<h2>Features</h2>
+<ul>
+<xsl:for-each select="feature">
+    <xsl:call-template name="ownedobject"/>
+</xsl:for-each>
+</ul>
+</xsl:template>
+
+<xsl:template match="methods">
+<h2>Methods</h2>
+<ul>
+<xsl:for-each select="method">
+    <xsl:call-template name="ownedobject"/>
+</xsl:for-each>
+</ul>
+</xsl:template>
+
+<xsl:template name="ownedobject">
+<li>
+<b>
+<xsl:value-of select="@namespace"/>/<xsl:value-of select="@name"/>
+<xsl:if test="name() = 'method'">
+<form method="GET">
+    <xsl:attribute name="action">
+/methods/<xsl:value-of select="@namespace"/>/<xsl:value-of select="@name"/>
+    </xsl:attribute>
+    (
+    <xsl:for-each select="arguments/argument">
+        <xsl:sort select="@position" order="ascending"/>
+        <xsl:value-of select="@name"/>:
+        <input type="text" size="2">
+            <xsl:attribute name="name">
+                <xsl:value-of select="@name"/>
+            </xsl:attribute>
+        </input>
+    </xsl:for-each>
+    )
+    <input type="submit" value="Call"/>
+</form>
+</xsl:if>
+</b>
+<xsl:if test="@description != ''">
+ - <i><xsl:value-of select="@description"/></i>
+</xsl:if>
+<pre><xsl:value-of select="documentation"/></pre>
+<p>(Owned by: <xsl:value-of select="@owner"/>)</p>
+<p>(v<xsl:value-of select="@version"/>)</p>
+</li>
+</xsl:template>
+
+
+<xsl:template match="//method/result">
+<h2>Result</h2>
+<a href="/">Back</a>
+<h3>Returnvalue</h3>
+<pre>
+<xsl:value-of select="retval/text()"/>
+</pre>
+<h3>Exception</h3>
+<div style="color: red">
+<pre>
+<xsl:value-of select="exception/@type"/>
+<xsl:value-of select="exception/text()"/>
+</pre>
+</div>
+</xsl:template>
+
+</xsl:stylesheet> 
\ No newline at end of file


-- 
To view, visit http://gerrit.ovirt.org/18577
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: If081e5cd5fc9d038c9afaef0c97349f83e6f3a39
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-node
Gerrit-Branch: master
Gerrit-Owner: Fabian Deutsch <fabiand at fedoraproject.org>



More information about the node-patches mailing list