Revision a0638838
b/Makefile.am | ||
---|---|---|
195 | 195 |
test/ganeti.serializer_unittest.py \ |
196 | 196 |
test/ganeti.workerpool_unittest.py \ |
197 | 197 |
test/ganeti.rapi.resources_unittest.py \ |
198 |
test/ganeti.http_unittest.py \ |
|
198 | 199 |
test/ganeti.constants_unittest.py |
199 | 200 |
|
200 | 201 |
nodist_TESTS = |
b/lib/http.py | ||
---|---|---|
108 | 108 |
# Message |
109 | 109 |
format % args, |
110 | 110 |
)) |
111 |
self._fd.flush() |
|
111 | 112 |
|
112 | 113 |
def _FormatCurrentTime(self): |
113 | 114 |
"""Formats current time in Common Log Format. |
b/lib/rapi/RESTHTTPServer.py | ||
---|---|---|
20 | 20 |
|
21 | 21 |
""" |
22 | 22 |
|
23 |
import BaseHTTPServer |
|
24 |
import OpenSSL |
|
25 |
import re |
|
26 |
import socket |
|
27 |
import time |
|
28 |
|
|
29 | 23 |
from ganeti import constants |
24 |
from ganeti import http |
|
30 | 25 |
from ganeti import errors |
31 |
from ganeti import logger |
|
32 | 26 |
from ganeti import rpc |
33 |
from ganeti import serializer |
|
34 |
|
|
35 | 27 |
from ganeti.rapi import connector |
36 | 28 |
from ganeti.rapi import httperror |
37 | 29 |
|
38 | 30 |
|
39 |
class HttpLogfile: |
|
40 |
"""Utility class to write HTTP server log files. |
|
41 |
|
|
42 |
The written format is the "Common Log Format" as defined by Apache: |
|
43 |
http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples |
|
44 |
|
|
45 |
""" |
|
46 |
MONTHNAME = [None, |
|
47 |
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
|
48 |
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] |
|
49 |
|
|
50 |
def __init__(self, path): |
|
51 |
self._fd = open(path, 'a', 1) |
|
52 |
|
|
53 |
def __del__(self): |
|
54 |
try: |
|
55 |
self.Close() |
|
56 |
except: |
|
57 |
# Swallow exceptions |
|
58 |
pass |
|
59 |
|
|
60 |
def Close(self): |
|
61 |
if self._fd is not None: |
|
62 |
self._fd.close() |
|
63 |
self._fd = None |
|
64 |
|
|
65 |
def LogRequest(self, request, format, *args): |
|
66 |
if self._fd is None: |
|
67 |
raise errors.ProgrammerError("Logfile already closed") |
|
68 |
|
|
69 |
request_time = self._FormatCurrentTime() |
|
70 |
|
|
71 |
self._fd.write("%s %s %s [%s] %s\n" % ( |
|
72 |
# Remote host address |
|
73 |
request.address_string(), |
|
74 |
|
|
75 |
# RFC1413 identity (identd) |
|
76 |
"-", |
|
77 |
|
|
78 |
# Remote user |
|
79 |
"-", |
|
80 |
|
|
81 |
# Request time |
|
82 |
request_time, |
|
83 |
|
|
84 |
# Message |
|
85 |
format % args, |
|
86 |
)) |
|
87 |
|
|
88 |
def _FormatCurrentTime(self): |
|
89 |
"""Formats current time in Common Log Format. |
|
90 |
|
|
91 |
""" |
|
92 |
return self._FormatLogTime(time.time()) |
|
93 |
|
|
94 |
def _FormatLogTime(self, seconds): |
|
95 |
"""Formats time for Common Log Format. |
|
96 |
|
|
97 |
All timestamps are logged in the UTC timezone. |
|
98 |
|
|
99 |
Args: |
|
100 |
- seconds: Time in seconds since the epoch |
|
101 |
|
|
102 |
""" |
|
103 |
(_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds) |
|
104 |
format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000" |
|
105 |
return time.strftime(format, tm) |
|
106 |
|
|
107 |
|
|
108 |
class RESTHTTPServer(BaseHTTPServer.HTTPServer): |
|
109 |
"""Class to provide an HTTP/HTTPS server. |
|
110 |
|
|
111 |
""" |
|
112 |
allow_reuse_address = True |
|
113 |
|
|
114 |
def __init__(self, server_address, HandlerClass, options): |
|
115 |
"""REST Server Constructor. |
|
116 |
|
|
117 |
Args: |
|
118 |
server_address: a touple containing: |
|
119 |
ip: a string with IP address, localhost if empty string |
|
120 |
port: port number, integer |
|
121 |
HandlerClass: HTTPRequestHandler object |
|
122 |
options: Command-line options |
|
123 |
|
|
124 |
""" |
|
125 |
logger.SetupLogging(debug=options.debug, program='ganeti-rapi') |
|
126 |
|
|
127 |
self.httplog = HttpLogfile(constants.LOG_RAPIACCESS) |
|
128 |
|
|
129 |
BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass) |
|
130 |
if options.ssl: |
|
131 |
# Set up SSL |
|
132 |
context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) |
|
133 |
context.use_privatekey_file(options.ssl_key) |
|
134 |
context.use_certificate_file(options.ssl_cert) |
|
135 |
self.socket = OpenSSL.SSL.Connection(context, |
|
136 |
socket.socket(self.address_family, |
|
137 |
self.socket_type)) |
|
138 |
else: |
|
139 |
self.socket = socket.socket(self.address_family, self.socket_type) |
|
140 |
|
|
141 |
self.server_bind() |
|
142 |
self.server_activate() |
|
143 |
|
|
144 |
|
|
145 |
class JsonResponse: |
|
146 |
CONTENT_TYPE = "application/json" |
|
147 |
|
|
148 |
def Encode(self, data): |
|
149 |
return serializer.DumpJson(data) |
|
150 |
|
|
151 |
|
|
152 |
class RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
|
31 |
class RESTRequestHandler(http.HTTPRequestHandler): |
|
153 | 32 |
"""REST Request Handler Class. |
154 | 33 |
|
155 | 34 |
""" |
156 | 35 |
def setup(self): |
157 |
"""Setup secure read and write file objects. |
|
158 |
|
|
159 |
""" |
|
160 |
self.connection = self.request |
|
161 |
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) |
|
162 |
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) |
|
36 |
super(RESTRequestHandler, self).setup() |
|
163 | 37 |
self._resmap = connector.Mapper() |
164 |
|
|
165 |
def handle_one_request(self):
|
|
166 |
"""Handle a single REST request.
|
|
38 |
|
|
39 |
def HandleRequest(self):
|
|
40 |
""" Handels a request.
|
|
167 | 41 |
|
168 | 42 |
""" |
169 |
self.raw_requestline = None |
|
170 |
try: |
|
171 |
self.raw_requestline = self.rfile.readline() |
|
172 |
except OpenSSL.SSL.Error, ex: |
|
173 |
logger.Error("Error in SSL: %s" % str(ex)) |
|
174 |
if not self.raw_requestline: |
|
175 |
self.close_connection = 1 |
|
176 |
return |
|
177 |
if not self.parse_request(): # An error code has been sent, just exit |
|
178 |
return |
|
43 |
(HandlerClass, items, args) = self._resmap.getController(self.path) |
|
44 |
handler = HandlerClass(self, items, args) |
|
179 | 45 |
|
46 |
command = self.command.upper() |
|
180 | 47 |
try: |
181 |
(HandlerClass, items, args) = self._resmap.getController(self.path) |
|
182 |
handler = HandlerClass(self, items, args) |
|
48 |
fn = getattr(handler, command) |
|
49 |
except AttributeError, err: |
|
50 |
raise httperror.HTTPBadRequest() |
|
183 | 51 |
|
184 |
command = self.command.upper() |
|
185 |
try: |
|
186 |
fn = getattr(handler, command) |
|
187 |
except AttributeError, err: |
|
188 |
raise httperror.HTTPBadRequest() |
|
189 |
|
|
190 |
try: |
|
191 |
result = fn() |
|
192 |
|
|
193 |
except errors.OpPrereqError, err: |
|
194 |
# TODO: "Not found" is not always the correct error. Ganeti's core must |
|
195 |
# differentiate between different error types. |
|
196 |
raise httperror.HTTPNotFound(message=str(err)) |
|
197 |
|
|
198 |
encoder = JsonResponse() |
|
199 |
encoded_result = encoder.Encode(result) |
|
200 |
|
|
201 |
self.send_response(200) |
|
202 |
self.send_header("Content-Type", encoder.CONTENT_TYPE) |
|
203 |
self.end_headers() |
|
204 |
self.wfile.write(encoded_result) |
|
205 |
|
|
206 |
except httperror.HTTPException, err: |
|
207 |
self.send_error(err.code, message=err.message) |
|
208 |
|
|
209 |
except Exception, err: |
|
210 |
self.send_error(httperror.HTTPInternalError.code, message=str(err)) |
|
211 |
|
|
212 |
def log_message(self, format, *args): |
|
213 |
"""Log an arbitrary message. |
|
214 |
|
|
215 |
This is used by all other logging functions. |
|
216 |
|
|
217 |
The first argument, FORMAT, is a format string for the |
|
218 |
message to be logged. If the format string contains |
|
219 |
any % escapes requiring parameters, they should be |
|
220 |
specified as subsequent arguments (it's just like |
|
221 |
printf!). |
|
52 |
try: |
|
53 |
result = fn() |
|
222 | 54 |
|
223 |
""" |
|
224 |
self.server.httplog.LogRequest(self, format, *args) |
|
55 |
except errors.OpPrereqError, err: |
|
56 |
# TODO: "Not found" is not always the correct error. Ganeti's core must |
|
57 |
# differentiate between different error types. |
|
58 |
raise httperror.HTTPNotFound(message=str(err)) |
|
59 |
|
|
60 |
return result |
|
225 | 61 |
|
226 | 62 |
|
227 | 63 |
def start(options): |
228 |
# Disable signal handlers, otherwise we can't exit the daemon in a clean way |
|
229 |
# by sending a signal. |
|
230 |
rpc.install_twisted_signal_handlers = False |
|
231 |
|
|
232 |
httpd = RESTHTTPServer(("", options.port), RESTRequestHandler, options) |
|
64 |
log_fd = open(constants.LOG_RAPIACCESS, 'a') |
|
233 | 65 |
try: |
234 |
httpd.serve_forever() |
|
66 |
apache_log = http.ApacheLogfile(log_fd) |
|
67 |
httpd = http.HTTPServer(("", options.port), RESTRequestHandler, |
|
68 |
httplog=apache_log) |
|
69 |
try: |
|
70 |
httpd.serve_forever() |
|
71 |
finally: |
|
72 |
httpd.server_close() |
|
73 |
|
|
235 | 74 |
finally: |
236 |
httpd.server_close() |
|
75 |
log_fd.close() |
b/lib/rapi/connector.py | ||
---|---|---|
26 | 26 |
import re |
27 | 27 |
|
28 | 28 |
from ganeti import constants |
29 |
from ganeti import http |
|
29 | 30 |
|
30 | 31 |
from ganeti.rapi import baserlib |
31 |
from ganeti.rapi import httperror |
|
32 | 32 |
from ganeti.rapi import rlib1 |
33 | 33 |
from ganeti.rapi import rlib2 |
34 | 34 |
|
... | ... | |
85 | 85 |
result = (handler, [], args) |
86 | 86 |
break |
87 | 87 |
|
88 |
if result is not None:
|
|
88 |
if result: |
|
89 | 89 |
return result |
90 | 90 |
else: |
91 |
raise httperror.HTTPNotFound()
|
|
91 |
raise http.HTTPNotFound() |
|
92 | 92 |
|
93 | 93 |
|
94 | 94 |
class R_root(baserlib.R_Generic): |
b/test/ganeti.http_unittest.py | ||
---|---|---|
1 |
#!/usr/bin/python |
|
2 |
# |
|
3 |
|
|
4 |
# Copyright (C) 2007, 2008 Google Inc. |
|
5 |
# |
|
6 |
# This program is free software; you can redistribute it and/or modify |
|
7 |
# it under the terms of the GNU General Public License as published by |
|
8 |
# the Free Software Foundation; either version 2 of the License, or |
|
9 |
# (at your option) any later version. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, but |
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
14 |
# General Public License for more details. |
|
15 |
# |
|
16 |
# You should have received a copy of the GNU General Public License |
|
17 |
# along with this program; if not, write to the Free Software |
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
19 |
# 02110-1301, USA. |
|
20 |
|
|
21 |
|
|
22 |
"""Script for unittesting the http module""" |
|
23 |
|
|
24 |
|
|
25 |
import os |
|
26 |
import unittest |
|
27 |
import tempfile |
|
28 |
import time |
|
29 |
|
|
30 |
from ganeti import http |
|
31 |
|
|
32 |
|
|
33 |
class HttpLogfileTests(unittest.TestCase): |
|
34 |
"""Rests for ApacheLogfile class.""" |
|
35 |
|
|
36 |
class FakeRequest: |
|
37 |
FAKE_ADDRESS = "1.2.3.4" |
|
38 |
|
|
39 |
def address_string(self): |
|
40 |
return self.FAKE_ADDRESS |
|
41 |
|
|
42 |
def setUp(self): |
|
43 |
self.tmpfile = tempfile.NamedTemporaryFile() |
|
44 |
self.logfile = http.ApacheLogfile(self.tmpfile) |
|
45 |
|
|
46 |
def tearDown(self): |
|
47 |
self.tmpfile.close() |
|
48 |
|
|
49 |
def testFormatLogTime(self): |
|
50 |
self._TestInTimezone(1208646123.0, "Europe/London", |
|
51 |
"19/Apr/2008:23:02:03 +0000") |
|
52 |
self._TestInTimezone(1208646123, "Europe/Zurich", |
|
53 |
"19/Apr/2008:23:02:03 +0000") |
|
54 |
self._TestInTimezone(1208646123, "Australia/Sydney", |
|
55 |
"19/Apr/2008:23:02:03 +0000") |
|
56 |
|
|
57 |
def _TestInTimezone(self, seconds, timezone, expected): |
|
58 |
"""Tests HttpLogfile._FormatLogTime with a specific timezone |
|
59 |
|
|
60 |
""" |
|
61 |
# Preserve environment |
|
62 |
old_TZ = os.environ.get("TZ", None) |
|
63 |
try: |
|
64 |
os.environ["TZ"] = timezone |
|
65 |
time.tzset() |
|
66 |
result = self.logfile._FormatLogTime(seconds) |
|
67 |
finally: |
|
68 |
# Restore environment |
|
69 |
if old_TZ is not None: |
|
70 |
os.environ["TZ"] = old_TZ |
|
71 |
elif "TZ" in os.environ: |
|
72 |
del os.environ["TZ"] |
|
73 |
time.tzset() |
|
74 |
|
|
75 |
self.assertEqual(result, expected) |
|
76 |
|
|
77 |
|
|
78 |
def testLogRequest(self): |
|
79 |
request = self.FakeRequest() |
|
80 |
self.logfile.LogRequest(request, "This is only a %s", "test") |
|
81 |
|
|
82 |
|
|
83 |
if __name__ == '__main__': |
|
84 |
unittest.main() |
b/test/ganeti.rapi.resources_unittest.py | ||
---|---|---|
22 | 22 |
"""Script for unittesting the RAPI resources module""" |
23 | 23 |
|
24 | 24 |
|
25 |
import os |
|
26 | 25 |
import unittest |
27 | 26 |
import tempfile |
28 |
import time |
|
29 | 27 |
|
30 | 28 |
from ganeti import errors |
29 |
from ganeti import http |
|
30 |
|
|
31 | 31 |
from ganeti.rapi import connector |
32 |
from ganeti.rapi import httperror |
|
33 | 32 |
from ganeti.rapi import RESTHTTPServer |
34 | 33 |
from ganeti.rapi import rlib1 |
35 | 34 |
|
... | ... | |
44 | 43 |
self.assertEquals(self.map.getController(uri), result) |
45 | 44 |
|
46 | 45 |
def _TestFailingUri(self, uri): |
47 |
self.failUnlessRaises(httperror.HTTPNotFound, self.map.getController, uri)
|
|
46 |
self.failUnlessRaises(http.HTTPNotFound, self.map.getController, uri) |
|
48 | 47 |
|
49 | 48 |
def testMapper(self): |
50 | 49 |
"""Testing Mapper""" |
... | ... | |
86 | 85 |
self.assertEquals(self.root.GET(), expected) |
87 | 86 |
|
88 | 87 |
|
89 |
class HttpLogfileTests(unittest.TestCase): |
|
90 |
"""Rests for HttpLogfile class.""" |
|
91 |
|
|
92 |
class FakeRequest: |
|
93 |
FAKE_ADDRESS = "1.2.3.4" |
|
94 |
|
|
95 |
def address_string(self): |
|
96 |
return self.FAKE_ADDRESS |
|
97 |
|
|
98 |
def setUp(self): |
|
99 |
self.tmpfile = tempfile.NamedTemporaryFile() |
|
100 |
self.logfile = RESTHTTPServer.HttpLogfile(self.tmpfile.name) |
|
101 |
|
|
102 |
def testFormatLogTime(self): |
|
103 |
self._TestInTimezone(1208646123.0, "Europe/London", |
|
104 |
"19/Apr/2008:23:02:03 +0000") |
|
105 |
self._TestInTimezone(1208646123, "Europe/Zurich", |
|
106 |
"19/Apr/2008:23:02:03 +0000") |
|
107 |
self._TestInTimezone(1208646123, "Australia/Sydney", |
|
108 |
"19/Apr/2008:23:02:03 +0000") |
|
109 |
|
|
110 |
def _TestInTimezone(self, seconds, timezone, expected): |
|
111 |
"""Tests HttpLogfile._FormatLogTime with a specific timezone |
|
112 |
|
|
113 |
""" |
|
114 |
# Preserve environment |
|
115 |
old_TZ = os.environ.get("TZ", None) |
|
116 |
try: |
|
117 |
os.environ["TZ"] = timezone |
|
118 |
time.tzset() |
|
119 |
result = self.logfile._FormatLogTime(seconds) |
|
120 |
finally: |
|
121 |
# Restore environment |
|
122 |
if old_TZ is not None: |
|
123 |
os.environ["TZ"] = old_TZ |
|
124 |
elif "TZ" in os.environ: |
|
125 |
del os.environ["TZ"] |
|
126 |
time.tzset() |
|
127 |
|
|
128 |
self.assertEqual(result, expected) |
|
129 |
|
|
130 |
def testClose(self): |
|
131 |
self.logfile.Close() |
|
132 |
|
|
133 |
def testCloseAndWrite(self): |
|
134 |
request = self.FakeRequest() |
|
135 |
self.logfile.Close() |
|
136 |
self.assertRaises(errors.ProgrammerError, self.logfile.LogRequest, |
|
137 |
request, "Message") |
|
138 |
|
|
139 |
def testLogRequest(self): |
|
140 |
request = self.FakeRequest() |
|
141 |
self.logfile.LogRequest(request, "This is only a %s", "test") |
|
142 |
self.logfile.Close() |
|
143 |
|
|
144 |
|
|
145 | 88 |
if __name__ == '__main__': |
146 | 89 |
unittest.main() |
Also available in: Unified diff