diff --git a/Makefile.am b/Makefile.am
index 7b8bae0b33c083938095d76a85a177fff51a4cbf..1156730d4b97290529c8d5cf931a67189e71146a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -99,6 +99,7 @@ docsgml = \
doc/hooks.sgml \
doc/install.sgml \
doc/admin.sgml \
+ doc/rapi.sgml \
doc/iallocator.sgml
doc_DATA = \
@@ -131,6 +132,7 @@ EXTRA_DIST = \
autotools/docbook-wrapper \
devel/upload.in \
$(docsgml) \
+ doc/build-rapi-resources-doc \
doc/examples/ganeti.initd.in \
doc/examples/ganeti.cron.in \
doc/examples/dumb-allocator \
@@ -186,6 +188,7 @@ dist_TESTS = \
test/ganeti.locking_unittest.py \
test/ganeti.serializer_unittest.py \
test/ganeti.workerpool_unittest.py \
+ test/ganeti.rapi.resources_unittest.py \
test/ganeti.constants_unittest.py
nodist_TESTS =
@@ -218,6 +221,13 @@ doc/%.pdf: doc/%.in $(DOCBOOK_WRAPPER)
doc/%.html: doc/%.in $(DOCBOOK_WRAPPER)
$(DOCBOOK_WRAPPER) $< $@
+doc/rapi.pdf doc/rapi.html: doc/rapi-resources.sgml
+
+doc/rapi-resources.sgml: doc/build-rapi-resources-doc lib/rapi/resources.py
+ PYTHONPATH=.:$(top_builddir) $< > $@ || rm -f $@
+
+.INTERMEDIATE: doc/rapi-resources.sgml
+
man/%.7: man/%.in man/footer.sgml $(DOCBOOK_WRAPPER)
$(DOCBOOK_WRAPPER) $< $@
@@ -226,7 +236,7 @@ man/%.8: man/%.in man/footer.sgml $(DOCBOOK_WRAPPER)
man/footer.sgml $(TESTS): srclinks
-$(TESTS): ganeti
+$(TESTS) doc/build-rapi-resources-doc: ganeti lib/_autoconf.py
lib/_autoconf.py: Makefile stamp-directories
set -e; \
diff --git a/doc/build-rapi-resources-doc b/doc/build-rapi-resources-doc
new file mode 100755
index 0000000000000000000000000000000000000000..c73e51883b664d33f58eda7677a34d714c7b7831
--- /dev/null
+++ b/doc/build-rapi-resources-doc
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008 Google Inc.
+#
+# 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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.
+
+"""Script to generate documentation for remote API resources.
+
+"""
+
+import re
+import cgi
+import inspect
+
+from ganeti.rapi import resources
+
+
+CHECKED_COMMANDS = ["GET", "POST", "PUT", "DELETE"]
+
+
+def main():
+ # Get list of all resources
+ all = list(resources._CONNECTOR.itervalues())
+
+ # Sort resources by URI
+ all.sort(cmp=lambda a, b: cmp(a.DOC_URI, b.DOC_URI))
+
+ print ""
+
+ for cls in all:
+ print ""
+ print "%s" % cgi.escape(cls.DOC_URI)
+
+ # Class docstring
+ description = inspect.getdoc(cls)
+ if description:
+ print ("%s" %
+ cgi.escape(description.strip()))
+
+ print ''
+ print ''
+ print ''
+ print ""
+ print " "
+ print " Method"
+ print " Description"
+ print "
"
+ print ""
+ print ''
+
+ for cmd in CHECKED_COMMANDS:
+ if not hasattr(cls, cmd):
+ continue
+
+ # Get docstring
+ text = inspect.getdoc(getattr(cls, cmd))
+ if not text:
+ text = ""
+
+ print ""
+ print " %s" % cgi.escape(cmd)
+ print (" %s" %
+ cgi.escape(text.strip()))
+ print "
"
+
+ print ""
+ print ""
+
+ print ""
+
+
+if __name__ == "__main__":
+ main()
diff --git a/doc/rapi.sgml b/doc/rapi.sgml
new file mode 100644
index 0000000000000000000000000000000000000000..51bf56e4fa36f5579b76f13e54d41bda88236b50
--- /dev/null
+++ b/doc/rapi.sgml
@@ -0,0 +1,94 @@
+
+
+
+]>
+
+
+ Ganeti remote API
+
+
+Documents Ganeti version 1.2
+
+
+ Introduction
+
+ Ganeti supports a remote API for enable external tools to easily
+ retrieve information about a cluster's state. The remote API daemon,
+ ganeti-rapi, is automatically started on
+ the master node if the --enable-rapi
+ parameter is passed to the configure
+ script. Alternatively you can start it manually. By default it runs on TCP
+ port 5080, but this can be changed either in
+ …/constants.py or via the command line
+ parameter -p. SSL support can also be
+ enabled by passing command line parameters.
+
+
+ Ganeti 1.2 only supports a limited set of calls, all of them
+ read-only. The next major version will have support for write
+ operations.
+
+
+
+
+ Protocol
+
+ The protocol used is JSON over HTTP
+ designed after the REST principle.
+
+
+
+
+ Usage examples
+
+ You can access the API using your favorite programming language as long
+ as it supports network connections.
+
+
+ Shell
+ wget -q -O - http://CLUSTERNAME:5080/info
+
+
+
+ Python
+ import urllib2
+f = urllib2.urlopen('http://CLUSTERNAME:5080/info')
+print f.read()
+
+
+
+ JavaScript
+
+ While it's possible to use JavaScript, it poses several potential
+ problems, including browser blocking request due to non-standard ports
+ or different domain names. Fetching the data on the webserver is
+ easier.
+
+ var url = 'http://CLUSTERNAME:5080/info';
+var info;
+
+var xmlreq = new XMLHttpRequest();
+xmlreq.onreadystatechange = function () {
+ if (xmlreq.readyState != 4) return;
+ if (xmlreq.status == 200) {
+ info = eval("(" + xmlreq.responseText + ")");
+ alert(info);
+ } else {
+ alert('Error fetching cluster info');
+ }
+ xmlreq = null;
+};
+xmlreq.open('GET', url, true);
+xmlreq.send(null);
+
+
+
+
+
+ Resources
+ &IncludeResources;
+
+
+
diff --git a/test/ganeti.rapi.resources_unittest.py b/test/ganeti.rapi.resources_unittest.py
new file mode 100755
index 0000000000000000000000000000000000000000..8fc40eee797d6c8df5b41928694e5322b339557b
--- /dev/null
+++ b/test/ganeti.rapi.resources_unittest.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2007, 2008 Google Inc.
+#
+# 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; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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.
+
+
+"""Script for unittesting the rapi.resources module"""
+
+
+import os
+import unittest
+import tempfile
+import time
+
+from ganeti import errors
+from ganeti.rapi import httperror
+from ganeti.rapi import resources
+from ganeti.rapi import RESTHTTPServer
+
+
+class MapperTests(unittest.TestCase):
+ """Tests for remote API URI mapper."""
+
+ def setUp(self):
+ self.map = resources.Mapper()
+
+ def _TestUri(self, uri, result):
+ self.assertEquals(self.map.getController(uri), result)
+
+ def _TestFailingUri(self, uri):
+ self.failUnlessRaises(httperror.HTTPNotFound, self.map.getController, uri)
+
+ def testMapper(self):
+ """Testing resources.Mapper"""
+
+ self._TestUri("/tags", (resources.R_tags, [], {}))
+
+ self._TestUri('/instances/www.test.com',
+ (resources.R_instances_name,
+ ['www.test.com'],
+ {}))
+
+ self._TestUri('/instances/www.test.com/tags?f=5&f=6&alt=html',
+ (resources.R_instances_name_tags,
+ ['www.test.com'],
+ {'alt': ['html'],
+ 'f': ['5', '6'],
+ }))
+
+ self._TestFailingUri("/tag")
+ self._TestFailingUri("/instances/does/not/exist")
+
+
+class R_RootTests(unittest.TestCase):
+ """Testing for R_root class."""
+
+ def setUp(self):
+ self.root = resources.R_root(None, None, None)
+
+ def testGet(self):
+ expected = [
+ {'name': 'info', 'uri': '/info'},
+ {'name': 'instances', 'uri': '/instances'},
+ {'name': 'nodes', 'uri': '/nodes'},
+ {'name': 'os', 'uri': '/os'},
+ {'name': 'tags', 'uri': '/tags'},
+ {'name': 'version', 'uri': '/version'},
+ ]
+ self.assertEquals(self.root.GET(), expected)
+
+
+class HttpLogfileTests(unittest.TestCase):
+ """Rests for HttpLogfile class."""
+
+ class FakeRequest:
+ FAKE_ADDRESS = "1.2.3.4"
+
+ def address_string(self):
+ return self.FAKE_ADDRESS
+
+ def setUp(self):
+ self.tmpfile = tempfile.NamedTemporaryFile()
+ self.logfile = RESTHTTPServer.HttpLogfile(self.tmpfile.name)
+
+ def testFormatLogTime(self):
+ self._TestInTimezone(1208646123.0, "Europe/London",
+ "19/Apr/2008:23:02:03 +0000")
+ self._TestInTimezone(1208646123, "Europe/Zurich",
+ "19/Apr/2008:23:02:03 +0000")
+ self._TestInTimezone(1208646123, "Australia/Sydney",
+ "19/Apr/2008:23:02:03 +0000")
+
+ def _TestInTimezone(self, seconds, timezone, expected):
+ """Tests HttpLogfile._FormatLogTime with a specific timezone
+
+ """
+ # Preserve environment
+ old_TZ = os.environ.get("TZ", None)
+ try:
+ os.environ["TZ"] = timezone
+ time.tzset()
+ result = self.logfile._FormatLogTime(seconds)
+ finally:
+ # Restore environment
+ if old_TZ is not None:
+ os.environ["TZ"] = old_TZ
+ elif "TZ" in os.environ:
+ del os.environ["TZ"]
+ time.tzset()
+
+ self.assertEqual(result, expected)
+
+ def testClose(self):
+ self.logfile.Close()
+
+ def testCloseAndWrite(self):
+ request = self.FakeRequest()
+ self.logfile.Close()
+ self.assertRaises(errors.ProgrammerError, self.logfile.LogRequest,
+ request, "Message")
+
+ def testLogRequest(self):
+ request = self.FakeRequest()
+ self.logfile.LogRequest(request, "This is only a %s", "test")
+ self.logfile.Close()
+
+
+if __name__ == '__main__':
+ unittest.main()