Import first version of Ganeti Remote API
authorOleksiy Mishchenko <oleksiy@google.com>
Fri, 11 Apr 2008 13:40:03 +0000 (13:40 +0000)
committerOleksiy Mishchenko <oleksiy@google.com>
Fri, 11 Apr 2008 13:40:03 +0000 (13:40 +0000)
Reviewed-by: iustinp

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/resources.py [new file with mode: 0644]

diff --git a/daemons/ganeti-rapi b/daemons/ganeti-rapi
new file mode 100755 (executable)
index 0000000..1136959
--- /dev/null
@@ -0,0 +1,193 @@
+#!/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 logging
+import optparse
+import sys
+import os
+
+import ganeti.rapi.RESTHTTPServer
+
+RELEASE_VERSION = 0.01
+API_PORT = 5080
+
+
+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" % RELEASE_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",
+                    default=API_PORT)
+  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")
+  options, args = parser.parse_args()
+
+  if len(args) != 1 or args[0] not in ("start", "stop"):
+    print >>sys.stderr, "Usage: %s [-d] [-p port] start|stop\n" % sys.argv[0]
+    sys.exit(1)
+
+  if options.ssl:
+    if 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 Port2PID(port):
+  """Map network port to PID.
+
+  Args:
+    port: A port number to map.
+
+  Return:
+    PID number.
+  """
+
+  _NET_STAT = ['/proc/net/tcp','/proc/net/udp']
+
+  inode2port = {}
+  port2pid = {}
+
+  for file in _NET_STAT:
+    try:
+      try:
+        f = open(file)
+        for line in f.readlines()[1:]:
+          d = line.split()
+          inode2port[long(d[9])] = int(d[1].split(':')[1], 16)
+      finally:
+        f.close()
+    except EnvironmentError:
+      # Nothing can be done
+      pass
+
+  fdlist = glob.glob('/proc/[0-9]*/fd/*')
+  for fd in fdlist:
+    try:
+      pid = int(fd.split('/')[2])
+      inode = long(os.stat(fd)[1])
+      if inode in inode2port:
+        port2pid[inode2port[inode]] = pid
+    except EnvironmentError:
+      # Nothing can be done
+      pass
+
+  if port in port2pid:
+    return port2pid[port]
+  else:
+    return None
+
+
+def StartAPI(options):
+  """Start the API.
+
+  Args:
+    options: arguments.
+
+  Return:
+    Exit code.
+  """
+  port = int(options.port)
+  # do the UNIX double-fork magic
+  try:
+    pid = os.fork()
+    if pid > 0:
+      # exit first parent
+      sys.exit(0)
+  except OSError, e:
+    print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
+    return 1
+
+  # decouple from parent environment
+  os.chdir("/")
+  os.setsid()
+  os.umask(0)
+
+  # do second fork
+  try:
+    pid = os.fork()
+    if pid > 0:
+      # exit from second parent, print eventual PID before
+      print "Ganeti-RAPI PID: %d port: %d" % (pid, port)
+      return 0
+  except OSError, e:
+    print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
+    return 1
+
+  # start the daemon main loop
+  ganeti.rapi.RESTHTTPServer.start(options)
+
+
+def StopAPI(options):
+  """Stop the API."""
+  port = int(options.port)
+  try:
+    pid = Port2PID(port)
+    if pid:
+      print "Stopping Ganeti-RAPI PID: %d, port: %d.... " % (pid, port),
+      os.kill(pid, 9)
+      print "done."
+    else:
+      print >>sys.stderr, "Unable to locate running Ganeti-RAPI on port: %d" % port
+  except Exception, ex:
+    print >>sys.stderr, ex
+    return 1
+  return 0
+
+
+def main():
+  """Main function.
+
+  """
+  result = 1
+  options, args = ParseOptions()
+  if args[0] == "start":
+    result = StartAPI(options)
+  else:
+    result = StopAPI(options)
+  sys.exit(result)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/lib/rapi/RESTHTTPServer.py b/lib/rapi/RESTHTTPServer.py
new file mode 100644 (file)
index 0000000..5025140
--- /dev/null
@@ -0,0 +1,178 @@
+#!/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.
+
+import socket
+import inspect
+import exceptions
+import SocketServer
+import BaseHTTPServer
+import OpenSSL
+import logging
+import logging.handlers
+import sys
+import os
+
+from optparse import OptionParser
+from ganeti.rapi import resources
+
+"""RESTfull HTTPS Server module.
+
+"""
+
+def OpenLog():
+  """Set up logging to the syslog.
+  """
+  log = logging.getLogger('ganeti-rapi')
+  slh = logging.handlers.SysLogHandler('/dev/log',
+                            logging.handlers.SysLogHandler.LOG_DAEMON)
+  fmt = logging.Formatter('ganeti-rapi[%(process)d]:%(levelname)s: %(message)s')
+  slh.setFormatter(fmt)
+  log.addHandler(slh)
+  log.setLevel(logging.INFO)
+  log.debug("Logging initialized")
+
+  return log
+
+
+class RESTHTTPServer(BaseHTTPServer.HTTPServer):
+  def __init__(self, server_address, HandlerClass, options):
+    """ REST Server Constructor.
+
+    Args:
+      server_address - a touple with pair:
+        ip - a string with IP address, localhost if null-string
+        port - port number, integer
+      HandlerClass - HTTPRequestHandler object.
+      options: command-line options.
+    """
+
+
+    SocketServer.BaseServer.__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 RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+  """REST Request Handler Class."""
+
+  def authenticate(self):
+    """This method performs authentication check."""
+    return True
+
+  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.map = resources.Mapper()
+    self.log = OpenLog()
+    self.log.debug("Request handler setup.")
+
+  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:
+      self.log.exception("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
+    if not self.authenticate():
+      self.send_error(401, "Acces Denied")
+      return
+    try:
+      rname = self.R_Resource(self.path)
+      mname = 'do_' + self.command
+      if not hasattr(rname, mname):
+        self.send_error(501, "Unsupported method (%r)" % self.command)
+        return
+      method = getattr(rname, mname)
+      method()
+    except AttributeError, msg:
+      self.send_error(501, "Resource is not available: %s" % msg)
+
+  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!).
+
+    The client host and current date/time are prefixed to
+    every message.
+
+    """
+    level = logging.INFO
+    # who is calling?
+    origin = inspect.stack()[1][0].f_code.co_name
+    if origin == "log_error":
+      level = logging.ERROR
+
+    self.log.log(level, "%s - - %s\n" %
+                     (self.address_string(),
+                      format%args))
+
+  def R_Resource(self, uri):
+    """Create controller from the URL.
+
+    Args:
+      uri - a string with requested URL.
+
+    Returns:
+      R_Generic class inheritor.
+    """
+    controller = self.map.getController(uri)
+    if controller:
+        return eval("resources.%s(self, %s, %s)" % controller)
+    else:
+      raise exceptions.AttribureError
+
+
+def start(options):
+  port = int(options.port)
+  httpd = RESTHTTPServer(("", port), RESTRequestHandler, options)
+  try:
+    httpd.serve_forever()
+  finally:
+    httpd.close()
+    del httpd
+    return 1
+
+
+if __name__ == "__main__":
+  pass
diff --git a/lib/rapi/__init__.py b/lib/rapi/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/rapi/resources.py b/lib/rapi/resources.py
new file mode 100644 (file)
index 0000000..31b503c
--- /dev/null
@@ -0,0 +1,302 @@
+#!/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.
+
+
+"""
+resources.py
+
+"""
+
+import simplejson
+import cgi
+import sys
+import os
+import re
+
+import ganeti.opcodes
+import ganeti.errors
+import ganeti.cli
+
+CONNECTOR = {
+    'R_instances_name':'^/instances/([\w\._-]+)$',
+    'R_tags':'^/tags$',
+    'R_status':'^/status$',
+    'R_os':'^/os$',
+    'R_info':'^/info$',
+
+    'R_instances':'^/instances$',
+    'R_instances_name_tags':'^/instances/([\w\._-]+)/tags$',
+
+    'R_nodes':'^/nodes$',
+    'R_nodes_name':'^/nodes/([\w\._-]+)$',
+    'R_nodes_name_tags':'^/nodes/([\w\._-]+)/tags$',
+
+    'R_jobs':'^/jobs$',
+    'R_jobs_id':'^/jobs/([\w\._-]+)$',
+
+    'R_index_html':'^/index.html$',
+}
+
+
+class RemoteAPIError(ganeti.errors.GenericError):
+  """ Remote API exception."""
+  pass
+
+
+class Mapper:
+  """Map resource to method."""
+
+  def __init__(self,con=CONNECTOR):
+    """Resource mapper constructor.
+
+    Args:
+      con - a dictionary, mapping method name with URL path regexp.
+    """
+    self._map = {}
+    for methd in con:
+      self._map[methd] = re.compile(con[methd])
+
+  def getController(self, uri):
+    """Loking for a map of given path.
+
+    Args:
+      uri - string with URI.
+
+    Returns:
+      A tuple with 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.
+      None, if no method found.
+    """
+    result = None
+    args = {}
+    d_uri = uri.split('?', 1)
+    if len(d_uri) > 1:
+      args = cgi.parse_qs(d_uri[1])
+    path = d_uri[0]
+    for methd in self._map:
+      items = self._map[methd].findall(path)
+      if items:
+        result = (methd, items, args)
+        break
+    return result
+
+
+class R_Generic(object):
+  """ Generic class for resources. """
+
+  def __init__(self, dispatcher, items, args):
+    """ Gentric resource constructor.
+
+    Args:
+      dispatcher - HTTPRequestHandler object.
+      items - a list with variables encoded in the URL.
+      args - a dictionary with additional options from URL.
+    """
+    self.dispatcher = dispatcher
+    self.items = items
+    self.args = args
+    self.code = 200
+
+  def do_GET(self):
+    """Default GET flow."""
+    try:
+      self._get()
+      self.send(self.code, self.result)
+    except RemoteAPIError, msg:
+      self.send_error(self.code, str(msg))
+    except ganeti.errors.OpPrereqError, msg:
+      self.send_error(404, str(msg))
+    except AttributeError, msg:
+      self.send_error(405, 'Method not Implemented: %s' % msg)
+    except Exception, msg:
+      self.send_error(500, 'Internal Server Error: %s' % msg)
+
+  def _get(self):
+    raise AttributeError("GET method is not implemented")
+
+
+  def send(self, code, data=None):
+    """ Printout data.
+
+    Args:
+      code - int, the HTTP response code.
+      data - message body.
+    """
+    self.dispatcher.send_response(code)
+    self.dispatcher.send_header("Content-type", "application/json") # rfc4627.txt
+    self.dispatcher.end_headers()
+    if data:
+      self.dispatcher.wfile.write(simplejson.dumps(data))
+
+  def send_error(self, code, message):
+    self.dispatcher.send_error(code, message)
+
+
+class R_instances(R_Generic):
+  """Implementation of /instances resource"""
+
+  def _get(self):
+    """ Send back to client list of available instances.
+    """
+    result = []
+    request = ganeti.opcodes.OpQueryInstances(output_fields=["name"], names=[])
+    instancelist = ganeti.cli.SubmitOpCode(request)
+    for instance in instancelist:
+      result.append({
+        'name':instance[0],
+        'uri':'/instances/%s' % instance[0]})
+    self.result = result
+
+
+class R_tags(R_Generic):
+  """docstring for R_tag."""
+
+  def _get(self):
+    """docstring for _get."""
+    request = ganeti.opcodes.OpDumpClusterConfig()
+    config = ganeti.cli.SubmitOpCode(request)
+    self.result = list(config.cluster.tags)
+
+
+class R_status(R_Generic):
+  """Docstring for R_status."""
+
+  def _get(self):
+    """docstring for _get"""
+    self.result = '{status}'
+
+
+class R_info(R_Generic):
+  """Cluster Info.
+  """
+
+  def _get(self):
+    request = ganeti.opcodes.OpQueryClusterInfo()
+    self.result = ganeti.cli.SubmitOpCode(request)
+
+
+class R_nodes(R_Generic):
+  """Class to dispatch /nodes requests."""
+
+  def _get(self):
+    """Send back to cliet list of cluster nodes."""
+    result = []
+    request = ganeti.opcodes.OpQueryNodes(output_fields=["name"], names=[])
+    nodelist = ganeti.cli.SubmitOpCode(request)
+    for node in nodelist:
+      result.append({
+        'name':node[0],
+        'uri':'/nodes/%s' % node[0]})
+    self.result = result
+
+
+class R_nodes_name(R_Generic):
+  """Class to dispatch /nodes/[node_name] requests."""
+
+  def _get(self):
+    result = {}
+    fields = ["dtotal", "dfree",
+              "mtotal", "mnode", "mfree",
+              "pinst_cnt", "sinst_cnt"]
+
+    request = ganeti.opcodes.OpQueryNodes(output_fields=fields,
+                                          names=self.items)
+    [r_list] = ganeti.cli.SubmitOpCode(request)
+
+    for i in range(len(fields)):
+      result[fields[i]]=r_list[i]
+
+    self.result = result
+
+
+class R_nodes_name_tags(R_Generic):
+  """docstring for R_nodes_name_tags."""
+
+  def _get(self):
+    """docstring for _get."""
+    op = ganeti.opcodes.OpGetTags(kind='node', name=self.items[0])
+    tags = ganeti.cli.SubmitOpCode(op)
+    self.result = list(tags)
+
+
+class R_instances_name(R_Generic):
+
+  def _get(self):
+    fields = ["name", "os", "pnode", "snodes",
+              "admin_state", "admin_ram",
+              "disk_template", "ip", "mac", "bridge",
+              "sda_size", "sdb_size", "vcpus"]
+
+    request = ganeti.opcodes.OpQueryInstances(output_fields=fields,
+                                              names=self.items)
+    data = ganeti.cli.SubmitOpCode(request)
+
+    result = {}
+    for i in range(len(fields)):
+      result[fields[i]] = data[0][i]
+    self.result = result
+
+
+class R_instances_name_tags(R_Generic):
+  """docstring for R_instances_name_tags."""
+
+  def _get(self):
+    """docstring for _get."""
+    op = ganeti.opcodes.OpGetTags(kind='instance', name=self.items[0])
+    tags = ganeti.cli.SubmitOpCode(op)
+    self.result = list(tags)
+
+
+class R_os(R_Generic):
+  """Class to povide list of valid OS."""
+
+  valid_os_list = []
+
+  def __valid_list(self, oslist):
+    for os in oslist:
+      if os.status == 'VALID':
+        if os.name not in self.valid_os_list:
+          self.valid_os_list.append(os.name)
+      else:
+        if os.name in valid_os_list:
+          self.valid_oslist.remove(os.name)
+
+  def _get(self):
+    request = ganeti.opcodes.OpDiagnoseOS()
+    diagnose_data = ganeti.cli.SubmitOpCode(request)
+    result = []
+    if not diagnose_data:
+      self.code = 500
+      raise RemoteAPIError("Can't get the OS list")
+    else:
+      for node_name in diagnose_data:
+        self.__valid_list(diagnose_data[node_name])
+
+    self.result = self.valid_os_list
+
+
+def main():
+  pass
+
+
+if __name__ == '__main__':
+  main()