Initial copy of RAPI filebase to the trunk
authorOleksiy Mishchenko <oleksiy@google.com>
Fri, 11 Jul 2008 09:47:51 +0000 (09:47 +0000)
committerOleksiy Mishchenko <oleksiy@google.com>
Fri, 11 Jul 2008 09:47:51 +0000 (09:47 +0000)
Reviewed-by: iustinp

Makefile.am
daemons/ganeti-rapi [new file with mode: 0755]
lib/rapi/RESTHTTPServer.py [new file with mode: 0644]
lib/rapi/__init__.py [new file with mode: 0644]
lib/rapi/httperror.py [new file with mode: 0644]
lib/rapi/resources.py [new file with mode: 0644]

index e96a50b..d06fc28 100644 (file)
@@ -14,6 +14,7 @@ DOCBOOK_WRAPPER = $(top_srcdir)/autotools/docbook-wrapper
 REPLACE_VARS_SED = autotools/replace_vars.sed
 
 hypervisordir = $(pkgpythondir)/hypervisor
+rapidir = $(pkgpythondir)/rapi
 toolsdir = $(pkglibdir)/tools
 docdir = $(datadir)/doc/$(PACKAGE)
 
@@ -25,6 +26,7 @@ DIRS = \
        doc/examples \
        lib \
        lib/hypervisor \
+       lib/rapi \
        man \
        qa \
        qa/hooks \
@@ -43,6 +45,7 @@ CLEANFILES = \
        doc/examples/ganeti.cron \
        lib/*.py[co] \
        lib/hypervisor/*.py[co] \
+       lib/rapi/*.py[co] \
        man/*.[78] \
        man/*.in \
        qa/*.py[co] \
@@ -84,6 +87,13 @@ hypervisor_PYTHON = \
        lib/hypervisor/hv_fake.py \
        lib/hypervisor/hv_xen.py
 
+rapi_PYTHON = \
+       lib/rapi/__init__.py \
+       lib/rapi/RESTHTTPServer.py \
+       lib/rapi/httperror.py \
+       lib/rapi/resources.py
+
+
 docsgml = \
        doc/hooks.sgml \
        doc/install.sgml \
@@ -99,6 +109,7 @@ dist_sbin_SCRIPTS = \
        daemons/ganeti-watcher \
        daemons/ganeti-master \
        daemons/ganeti-masterd \
+       daemons/ganeti-rapi \
        scripts/gnt-backup \
        scripts/gnt-cluster \
        scripts/gnt-debug \
@@ -254,7 +265,7 @@ $(REPLACE_VARS_SED): Makefile stamp-directories
 #.PHONY: srclinks
 srclinks: stamp-directories
        set -e; \
-       for i in man/footer.sgml $(pkgpython_PYTHON) $(hypervisor_PYTHON); do \
+       for i in man/footer.sgml $(pkgpython_PYTHON) $(hypervisor_PYTHON) $(rapi_PYTHON); do \
                if test ! -f $$i -a -f $(abs_top_srcdir)/$$i; then \
                        $(LN_S) $(abs_top_srcdir)/$$i $$i; \
                fi; \
diff --git a/daemons/ganeti-rapi b/daemons/ganeti-rapi
new file mode 100755 (executable)
index 0000000..fa1d41f
--- /dev/null
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+""" Ganeti Remote API master script.
+"""
+
+import glob
+import optparse
+import sys
+import os
+
+# we need to import rpc early in order to get our custom reactor,
+# instead of the default twisted one; without this, things breaks in a
+# not-nice-to-debug way
+from ganeti import rpc
+from ganeti import constants
+from ganeti import utils
+from ganeti.rapi import RESTHTTPServer
+
+
+def ParseOptions():
+  """Parse the command line options.
+
+  Returns:
+    (options, args) as from OptionParser.parse_args()
+
+  """
+  parser = optparse.OptionParser(description="Ganeti Remote API",
+                    usage="%prog [-d] [-p port]",
+                    version="%%prog (ganeti) %s" %
+                                 constants.RAPI_VERSION)
+  parser.add_option("-d", "--debug", dest="debug",
+                    help="Enable some debug messages",
+                    default=False, action="store_true")
+  parser.add_option("-p", "--port", dest="port",
+                    help="Port to run API (%s default)." %
+                                 constants.RAPI_PORT,
+                    default=constants.RAPI_PORT, type="int")
+  parser.add_option("-S", "--https", dest="ssl",
+                    help="Secure HTTP protocol with SSL",
+                    default=False, action="store_true")
+  parser.add_option("-K", "--ssl-key", dest="ssl_key",
+                    help="SSL key",
+                    default=None, type="string")
+  parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
+                    help="SSL certificate",
+                    default=None, type="string")
+  parser.add_option("-f", "--foreground", dest="fork",
+                    help="Don't detach from the current terminal",
+                    default=True, action="store_false")
+
+  options, args = parser.parse_args()
+
+  if len(args) != 0:
+    print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
+    sys.exit(1)
+
+  if options.ssl and not (options.ssl_cert and options.ssl_key):
+    print >> sys.stderr, ("For secure mode please provide "
+                         "--ssl-key and --ssl-cert arguments")
+    sys.exit(1)
+
+  return options, args
+
+
+def main():
+  """Main function.
+
+  """
+  options, args = ParseOptions()
+  if options.fork:
+    utils.Daemonize(logfile=constants.LOG_RAPISERVER)
+  RESTHTTPServer.start(options)
+  sys.exit(0)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/lib/rapi/RESTHTTPServer.py b/lib/rapi/RESTHTTPServer.py
new file mode 100644 (file)
index 0000000..c480ff8
--- /dev/null
@@ -0,0 +1,234 @@
+#
+#
+
+# 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.
+
+"""RESTfull HTTPS Server module.
+
+"""
+
+import socket
+import BaseHTTPServer
+import OpenSSL
+import time
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import logger
+from ganeti import rpc
+from ganeti import serializer
+from ganeti.rapi import resources
+from ganeti.rapi import httperror
+
+
+class HttpLogfile:
+  """Utility class to write HTTP server log files.
+
+  The written format is the "Common Log Format" as defined by Apache:
+  http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples
+
+  """
+  MONTHNAME = [None,
+               'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+  def __init__(self, path):
+    self._fd = open(path, 'a', 1)
+
+  def __del__(self):
+    try:
+      self.Close()
+    except:
+      # Swallow exceptions
+      pass
+
+  def Close(self):
+    if self._fd is not None:
+      self._fd.close()
+      self._fd = None
+
+  def LogRequest(self, request, format, *args):
+    if self._fd is None:
+      raise errors.ProgrammerError("Logfile already closed")
+
+    request_time = self._FormatCurrentTime()
+
+    self._fd.write("%s %s %s [%s] %s\n" % (
+      # Remote host address
+      request.address_string(),
+
+      # RFC1413 identity (identd)
+      "-",
+
+      # Remote user
+      "-",
+
+      # Request time
+      request_time,
+
+      # Message
+      format % args,
+      ))
+
+  def _FormatCurrentTime(self):
+    """Formats current time in Common Log Format.
+
+    """
+    return self._FormatLogTime(time.time())
+
+  def _FormatLogTime(self, seconds):
+    """Formats time for Common Log Format.
+
+    All timestamps are logged in the UTC timezone.
+
+    Args:
+    - seconds: Time in seconds since the epoch
+
+    """
+    (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds)
+    format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000"
+    return time.strftime(format, tm)
+
+
+class RESTHTTPServer(BaseHTTPServer.HTTPServer):
+  """Class to provide an HTTP/HTTPS server.
+
+  """
+  allow_reuse_address = True
+
+  def __init__(self, server_address, HandlerClass, options):
+    """REST Server Constructor.
+
+    Args:
+      server_address: a touple containing:
+        ip: a string with IP address, localhost if empty string
+        port: port number, integer
+      HandlerClass: HTTPRequestHandler object
+      options: Command-line options
+
+    """
+    logger.SetupLogging(debug=options.debug, program='ganeti-rapi')
+
+    self.httplog = HttpLogfile(constants.LOG_RAPIACCESS)
+
+    BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass)
+    if options.ssl:
+      # Set up SSL
+      context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+      context.use_privatekey_file(options.ssl_key)
+      context.use_certificate_file(options.ssl_cert)
+      self.socket = OpenSSL.SSL.Connection(context,
+                                           socket.socket(self.address_family,
+                                           self.socket_type))
+    else:
+      self.socket = socket.socket(self.address_family, self.socket_type)
+
+    self.server_bind()
+    self.server_activate()
+
+
+class JsonResponse:
+  CONTENT_TYPE = "application/json"
+
+  def Encode(self, data):
+    return serializer.DumpJson(data)
+
+
+class RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+  """REST Request Handler Class.
+
+  """
+  def setup(self):
+    """Setup secure read and write file objects.
+
+    """
+    self.connection = self.request
+    self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
+    self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
+    self._resmap = resources.Mapper()
+
+  def handle_one_request(self):
+    """Handle a single REST request.
+
+    """
+    self.raw_requestline = None
+    try:
+      self.raw_requestline = self.rfile.readline()
+    except OpenSSL.SSL.Error, ex:
+      logger.Error("Error in SSL: %s" % str(ex))
+    if not self.raw_requestline:
+      self.close_connection = 1
+      return
+    if not self.parse_request(): # An error code has been sent, just exit
+      return
+
+    try:
+      (HandlerClass, items, args) = self._resmap.getController(self.path)
+      handler = HandlerClass(self, items, args)
+
+      command = self.command.upper()
+      try:
+        fn = getattr(handler, command)
+      except AttributeError, err:
+        raise httperror.HTTPBadRequest()
+
+      try:
+        result = fn()
+
+      except errors.OpPrereqError, err:
+        # TODO: "Not found" is not always the correct error. Ganeti's core must
+        # differentiate between different error types.
+        raise httperror.HTTPNotFound(message=str(err))
+
+      encoder = JsonResponse()
+      encoded_result = encoder.Encode(result)
+
+      self.send_response(200)
+      self.send_header("Content-Type", encoder.CONTENT_TYPE)
+      self.end_headers()
+      self.wfile.write(encoded_result)
+
+    except httperror.HTTPException, err:
+      self.send_error(err.code, message=err.message)
+
+    except Exception, err:
+      self.send_error(httperror.HTTPInternalError.code, message=str(err))
+
+  def log_message(self, format, *args):
+    """Log an arbitrary message.
+
+    This is used by all other logging functions.
+
+    The first argument, FORMAT, is a format string for the
+    message to be logged.  If the format string contains
+    any % escapes requiring parameters, they should be
+    specified as subsequent arguments (it's just like
+    printf!).
+
+    """
+    self.server.httplog.LogRequest(self, format, *args)
+
+
+def start(options):
+  # Disable signal handlers, otherwise we can't exit the daemon in a clean way
+  # by sending a signal.
+  rpc.install_twisted_signal_handlers = False
+
+  httpd = RESTHTTPServer(("", options.port), RESTRequestHandler, options)
+  try:
+    httpd.serve_forever()
+  finally:
+    httpd.server_close()
diff --git a/lib/rapi/__init__.py b/lib/rapi/__init__.py
new file mode 100644 (file)
index 0000000..15360b5
--- /dev/null
@@ -0,0 +1,22 @@
+#
+#
+
+# 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.
+
+
+# empty file for package definition
diff --git a/lib/rapi/httperror.py b/lib/rapi/httperror.py
new file mode 100644 (file)
index 0000000..cbb8adb
--- /dev/null
@@ -0,0 +1,49 @@
+#
+#
+
+# Copyright (C) 2006, 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.
+
+
+"""HTTP errors.
+
+"""
+
+
+class HTTPException(Exception):
+  code = None
+  message = None
+
+  def __init__(self, message=None):
+    if message is not None:
+      self.message = message
+
+
+class HTTPBadRequest(HTTPException):
+  code = 400
+
+
+class HTTPNotFound(HTTPException):
+  code = 404
+
+
+class HTTPInternalError(HTTPException):
+  code = 500
+
+
+class HTTPServiceUnavailable(HTTPException):
+  code = 503
diff --git a/lib/rapi/resources.py b/lib/rapi/resources.py
new file mode 100644 (file)
index 0000000..3d5ce24
--- /dev/null
@@ -0,0 +1,576 @@
+#
+#
+
+# Copyright (C) 2006, 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.
+
+
+"""Remote API resources.
+
+"""
+
+import cgi
+import re
+
+import ganeti.opcodes
+import ganeti.errors
+import ganeti.cli
+
+from ganeti import constants
+from ganeti import utils
+from ganeti.rapi import httperror
+
+
+# Initialized at the end of this file.
+_CONNECTOR = {}
+
+
+def BuildUriList(names, uri_format):
+  """Builds a URI list as used by index resources.
+
+  Args:
+  - names: List of names as strings
+  - uri_format: Format to be applied for URI
+
+  """
+  def _MapName(name):
+    return { "name": name, "uri": uri_format % name, }
+
+  # Make sure the result is sorted, makes it nicer to look at and simplifies
+  # unittests.
+  names.sort()
+
+  return map(_MapName, names)
+
+
+def ExtractField(sequence, index):
+  """Creates a list containing one column out of a list of lists.
+
+  Args:
+  - sequence: Sequence of lists
+  - index: Index of field
+
+  """
+  return map(lambda item: item[index], sequence)
+
+
+def MapFields(names, data):
+  """Maps two lists into one dictionary.
+
+  Args:
+  - names: Field names (list of strings)
+  - data: Field data (list)
+
+  Example:
+  >>> MapFields(["a", "b"], ["foo", 123])
+  {'a': 'foo', 'b': 123}
+
+  """
+  if len(names) != len(data):
+    raise AttributeError("Names and data must have the same length")
+  return dict([(names[i], data[i]) for i in range(len(names))])
+
+
+def RequireLock(name='cmd'):
+  """Function decorator to automatically acquire locks.
+
+  PEP-318 style function decorator.
+
+  """
+  def wrapper(fn):
+    def new_f(*args, **kwargs):
+      try:
+        utils.Lock(name, max_retries=15)
+        try:
+          # Call real function
+          return fn(*args, **kwargs)
+        finally:
+          utils.Unlock(name)
+          utils.LockCleanup()
+      except ganeti.errors.LockError, err:
+        raise httperror.HTTPServiceUnavailable(message=str(err))
+
+    # Override function metadata
+    new_f.func_name = fn.func_name
+    new_f.func_doc = fn.func_doc
+
+    return new_f
+
+  return wrapper
+
+
+def _Tags_GET(kind, name=None):
+  """Helper function to retrieve tags.
+
+  """
+  if name is None:
+    # Do not cause "missing parameter" error, which happens if a parameter
+    # is None.
+    name = ""
+  op = ganeti.opcodes.OpGetTags(kind=kind, name=name)
+  tags = ganeti.cli.SubmitOpCode(op)
+  return list(tags)
+
+
+class Mapper:
+  """Map resource to method.
+
+  """
+  def __init__(self, connector=_CONNECTOR):
+    """Resource mapper constructor.
+
+    Args:
+      con: a dictionary, mapping method name with URL path regexp
+
+    """
+    self._connector = connector
+
+  def getController(self, uri):
+    """Find method for a given URI.
+
+    Args:
+      uri: string with URI
+
+    Returns:
+      None if no method is found or a tuple containing the following fields:
+        methd: name of method mapped to URI
+        items: a list of variable intems in the path
+        args: a dictionary with additional parameters from URL
+
+    """
+    if '?' in uri:
+      (path, query) = uri.split('?', 1)
+      args = cgi.parse_qs(query)
+    else:
+      path = uri
+      query = None
+      args = {}
+
+    result = None
+
+    for key, handler in self._connector.iteritems():
+      # Regex objects
+      if hasattr(key, "match"):
+        m = key.match(path)
+        if m:
+          result = (handler, list(m.groups()), args)
+          break
+
+      # String objects
+      elif key == path:
+        result = (handler, [], args)
+        break
+
+    if result is not None:
+      return result
+    else:
+      raise httperror.HTTPNotFound()
+
+
+class R_Generic(object):
+  """Generic class for resources.
+
+  """
+  def __init__(self, request, items, queryargs):
+    """Generic resource constructor.
+
+    Args:
+      request: HTTPRequestHandler object
+      items: a list with variables encoded in the URL
+      queryargs: a dictionary with additional options from URL
+
+    """
+    self.request = request
+    self.items = items
+    self.queryargs = queryargs
+
+
+class R_root(R_Generic):
+  """/ resource.
+
+  """
+  DOC_URI = "/"
+
+  def GET(self):
+    """Show the list of mapped resources.
+    
+    Returns:
+      A dictionary with 'name' and 'uri' keys for each of them.
+
+    """
+    root_pattern = re.compile('^R_([a-zA-Z0-9]+)$')
+
+    rootlist = []
+    for handler in _CONNECTOR.values():
+      m = root_pattern.match(handler.__name__)
+      if m:
+        name = m.group(1)
+        if name != 'root':
+          rootlist.append(name)
+
+    return BuildUriList(rootlist, "/%s")
+
+
+class R_version(R_Generic):
+  """/version resource.
+
+  This resource should be used to determine the remote API version and to adapt
+  clients accordingly.
+
+  """
+  DOC_URI = "/version"
+
+  def GET(self):
+    """Returns the remote API version.
+
+    """
+    return constants.RAPI_VERSION
+
+
+class R_tags(R_Generic):
+  """/tags resource.
+
+  Manages cluster tags.
+
+  """
+  DOC_URI = "/tags"
+
+  def GET(self):
+    """Returns a list of all cluster tags.
+
+    Example: ["tag1", "tag2", "tag3"]
+
+    """
+    return _Tags_GET(constants.TAG_CLUSTER)
+
+
+class R_info(R_Generic):
+  """Cluster info.
+
+  """
+  DOC_URI = "/info"
+
+  def GET(self):
+    """Returns cluster information.
+
+    Example: {
+      "config_version": 3,
+      "name": "cluster1.example.com",
+      "software_version": "1.2.4",
+      "os_api_version": 5,
+      "export_version": 0,
+      "master": "node1.example.com",
+      "architecture": [
+        "64bit",
+        "x86_64"
+      ],
+      "hypervisor_type": "xen-3.0",
+      "protocol_version": 12
+    }
+
+    """
+    op = ganeti.opcodes.OpQueryClusterInfo()
+    return ganeti.cli.SubmitOpCode(op)
+
+
+class R_nodes(R_Generic):
+  """/nodes resource.
+
+  """
+  DOC_URI = "/nodes"
+
+  @RequireLock()
+  def _GetDetails(self, nodeslist):
+    """Returns detailed instance data for bulk output.
+
+    Args:
+      instance: A list of nodes names.
+
+    Returns:
+      A list of nodes properties
+
+    """
+    fields = ["name","dtotal", "dfree",
+              "mtotal", "mnode", "mfree",
+              "pinst_cnt", "sinst_cnt", "tags"]
+
+    op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
+                                     names=nodeslist)
+    result = ganeti.cli.SubmitOpCode(op)
+
+    nodes_details = []
+    for node in result:
+      mapped = MapFields(fields, node)
+      nodes_details.append(mapped)
+    return nodes_details
+  def GET(self):
+    """Returns a list of all nodes.
+    
+    Returns:
+      A dictionary with 'name' and 'uri' keys for each of them.
+
+    Example: [
+        {
+          "name": "node1.example.com",
+          "uri": "\/instances\/node1.example.com"
+        },
+        {
+          "name": "node2.example.com",
+          "uri": "\/instances\/node2.example.com"
+        }]
+
+    If the optional 'bulk' argument is provided and set to 'true' 
+    value (i.e '?bulk=1'), the output contains detailed
+    information about nodes as a list. Note: Lock required.
+
+    Example: [
+        {
+          "pinst_cnt": 1,
+          "mfree": 31280,
+          "mtotal": 32763,
+          "name": "www.example.com",
+          "tags": [],
+          "mnode": 512,
+          "dtotal": 5246208,
+          "sinst_cnt": 2,
+          "dfree": 5171712
+        },
+        ...
+    ]
+
+    """
+    op = ganeti.opcodes.OpQueryNodes(output_fields=["name"], names=[])
+    nodeslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
+    
+    if 'bulk' in self.queryargs:
+      return self._GetDetails(nodeslist)
+
+    return BuildUriList(nodeslist, "/nodes/%s")
+
+
+class R_nodes_name(R_Generic):
+  """/nodes/[node_name] resources.
+
+  """
+  DOC_URI = "/nodes/[node_name]"
+
+  @RequireLock()
+  def GET(self):
+    """Send information about a node. 
+
+    """
+    node_name = self.items[0]
+    fields = ["name","dtotal", "dfree",
+              "mtotal", "mnode", "mfree",
+              "pinst_cnt", "sinst_cnt", "tags"]
+
+    op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
+                                     names=[node_name])
+    result = ganeti.cli.SubmitOpCode(op)
+
+    return MapFields(fields, result[0])
+
+
+class R_nodes_name_tags(R_Generic):
+  """/nodes/[node_name]/tags resource.
+
+  Manages per-node tags.
+
+  """
+  DOC_URI = "/nodes/[node_name]/tags"
+
+  def GET(self):
+    """Returns a list of node tags.
+
+    Example: ["tag1", "tag2", "tag3"]
+
+    """
+    return _Tags_GET(constants.TAG_NODE, name=self.items[0])
+
+
+class R_instances(R_Generic):
+  """/instances resource.
+
+  """
+  DOC_URI = "/instances"
+
+  @RequireLock()
+  def _GetDetails(self, instanceslist):
+    """Returns detailed instance data for bulk output.
+
+    Args:
+      instance: A list of instances names.
+
+    Returns:
+      A list with instances properties.
+
+    """
+    fields = ["name", "os", "pnode", "snodes",
+              "admin_state", "admin_ram",
+              "disk_template", "ip", "mac", "bridge",
+              "sda_size", "sdb_size", "vcpus",
+              "oper_state", "status", "tags"]
+
+    op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
+                                         names=instanceslist)
+    result = ganeti.cli.SubmitOpCode(op)
+
+    instances_details = []
+    for instance in result:
+      mapped = MapFields(fields, instance)
+      instances_details.append(mapped)
+    return instances_details
+   
+  def GET(self):
+    """Returns a list of all available instances.
+    
+    Returns:
+       A dictionary with 'name' and 'uri' keys for each of them.
+
+    Example: [
+        {
+          "name": "web.example.com",
+          "uri": "\/instances\/web.example.com"
+        },
+        {
+          "name": "mail.example.com",
+          "uri": "\/instances\/mail.example.com"
+        }]
+
+    If the optional 'bulk' argument is provided and set to 'true' 
+    value (i.e '?bulk=1'), the output contains detailed
+    information about instances as a list. Note: Lock required.
+
+    Example: [
+        {
+           "status": "running",
+           "bridge": "xen-br0",
+           "name": "web.example.com",
+           "tags": ["tag1", "tag2"],
+           "admin_ram": 512,
+           "sda_size": 20480,
+           "pnode": "node1.example.com",
+           "mac": "01:23:45:67:89:01",
+           "sdb_size": 4096,
+           "snodes": ["node2.example.com"],
+           "disk_template": "drbd",
+           "ip": null,
+           "admin_state": true,
+           "os": "debian-etch",
+           "vcpus": 2,
+           "oper_state": true
+        },
+        ...
+    ]
+
+    """
+    op = ganeti.opcodes.OpQueryInstances(output_fields=["name"], names=[])
+    instanceslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
+    
+    if 'bulk' in self.queryargs:
+      return self._GetDetails(instanceslist)  
+
+    else:
+      return BuildUriList(instanceslist, "/instances/%s")
+
+
+class R_instances_name(R_Generic):
+  """/instances/[instance_name] resources.
+
+  """
+  DOC_URI = "/instances/[instance_name]"
+
+  @RequireLock()
+  def GET(self):
+    """Send information about an instance.
+
+    """
+    instance_name = self.items[0]
+    fields = ["name", "os", "pnode", "snodes",
+              "admin_state", "admin_ram",
+              "disk_template", "ip", "mac", "bridge",
+              "sda_size", "sdb_size", "vcpus",
+              "oper_state", "status", "tags"]
+
+    op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
+                                         names=[instance_name])
+    result = ganeti.cli.SubmitOpCode(op)
+
+    return MapFields(fields, result[0])
+
+
+class R_instances_name_tags(R_Generic):
+  """/instances/[instance_name]/tags resource.
+
+  Manages per-instance tags.
+
+  """
+  DOC_URI = "/instances/[instance_name]/tags"
+
+  def GET(self):
+    """Returns a list of instance tags.
+
+    Example: ["tag1", "tag2", "tag3"]
+
+    """
+    return _Tags_GET(constants.TAG_INSTANCE, name=self.items[0])
+
+
+class R_os(R_Generic):
+  """/os resource.
+
+  """
+  DOC_URI = "/os"
+
+  @RequireLock()
+  def GET(self):
+    """Return a list of all OSes.
+
+    Can return error 500 in case of a problem.
+
+    Example: ["debian-etch"]
+
+    """
+    op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
+                                     names=[])
+    diagnose_data = ganeti.cli.SubmitOpCode(op)
+
+    if not isinstance(diagnose_data, list):
+      raise httperror.HTTPInternalError(message="Can't get OS list")
+
+    return [row[0] for row in diagnose_data if row[1]]
+
+
+_CONNECTOR.update({
+  "/": R_root,
+
+  "/version": R_version,
+
+  "/tags": R_tags,
+  "/info": R_info,
+
+  "/nodes": R_nodes,
+  re.compile(r'^/nodes/([\w\._-]+)$'): R_nodes_name,
+  re.compile(r'^/nodes/([\w\._-]+)/tags$'): R_nodes_name_tags,
+
+  "/instances": R_instances,
+  re.compile(r'^/instances/([\w\._-]+)$'): R_instances_name,
+  re.compile(r'^/instances/([\w\._-]+)/tags$'): R_instances_name_tags,
+
+  "/os": R_os,
+  })