--- /dev/null
+#!/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()
--- /dev/null
+#!/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
--- /dev/null
+#!/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()