Commit 8c229cc7 authored by Oleksiy Mishchenko's avatar Oleksiy Mishchenko
Browse files

Initial copy of RAPI filebase to the trunk

Reviewed-by: iustinp
parent 0ed468d3
......@@ -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; \
......
#!/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()
#
#
# 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()
#
#
# 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
#
#
# 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
#
#
# 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