Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 26d3fd2f

History | View | Annotate | Download (9.3 kB)

1 8c229cc7 Oleksiy Mishchenko
#!/usr/bin/python
2 8c229cc7 Oleksiy Mishchenko
#
3 8c229cc7 Oleksiy Mishchenko
4 8c229cc7 Oleksiy Mishchenko
# Copyright (C) 2006, 2007 Google Inc.
5 8c229cc7 Oleksiy Mishchenko
#
6 8c229cc7 Oleksiy Mishchenko
# This program is free software; you can redistribute it and/or modify
7 8c229cc7 Oleksiy Mishchenko
# it under the terms of the GNU General Public License as published by
8 8c229cc7 Oleksiy Mishchenko
# the Free Software Foundation; either version 2 of the License, or
9 8c229cc7 Oleksiy Mishchenko
# (at your option) any later version.
10 8c229cc7 Oleksiy Mishchenko
#
11 8c229cc7 Oleksiy Mishchenko
# This program is distributed in the hope that it will be useful, but
12 8c229cc7 Oleksiy Mishchenko
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 8c229cc7 Oleksiy Mishchenko
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 8c229cc7 Oleksiy Mishchenko
# General Public License for more details.
15 8c229cc7 Oleksiy Mishchenko
#
16 8c229cc7 Oleksiy Mishchenko
# You should have received a copy of the GNU General Public License
17 8c229cc7 Oleksiy Mishchenko
# along with this program; if not, write to the Free Software
18 8c229cc7 Oleksiy Mishchenko
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 8c229cc7 Oleksiy Mishchenko
# 02110-1301, USA.
20 8c229cc7 Oleksiy Mishchenko
21 7260cfbe Iustin Pop
"""Ganeti Remote API master script.
22 7260cfbe Iustin Pop
23 8c229cc7 Oleksiy Mishchenko
"""
24 8c229cc7 Oleksiy Mishchenko
25 7260cfbe Iustin Pop
# pylint: disable-msg=C0103,W0142
26 7260cfbe Iustin Pop
27 7260cfbe Iustin Pop
# C0103: Invalid name ganeti-watcher
28 7260cfbe Iustin Pop
29 441e7cfd Oleksiy Mishchenko
import logging
30 8c229cc7 Oleksiy Mishchenko
import optparse
31 8c229cc7 Oleksiy Mishchenko
import sys
32 8c229cc7 Oleksiy Mishchenko
import os
33 b5b67ef9 Michael Hanselmann
import os.path
34 8c229cc7 Oleksiy Mishchenko
35 a2e60f14 René Nussbaumer
try:
36 a2e60f14 René Nussbaumer
  from pyinotify import pyinotify # pylint: disable-msg=E0611
37 a2e60f14 René Nussbaumer
except ImportError:
38 a2e60f14 René Nussbaumer
  import pyinotify
39 a2e60f14 René Nussbaumer
40 a2e60f14 René Nussbaumer
from ganeti import asyncnotifier
41 8c229cc7 Oleksiy Mishchenko
from ganeti import constants
42 3cd62121 Michael Hanselmann
from ganeti import http
43 16a8967d Michael Hanselmann
from ganeti import daemon
44 5675cd1f Iustin Pop
from ganeti import ssconf
45 77e1d753 Iustin Pop
from ganeti import luxi
46 1f8588f6 Iustin Pop
from ganeti import serializer
47 e4ef4343 Michael Hanselmann
from ganeti import compat
48 3cd62121 Michael Hanselmann
from ganeti.rapi import connector
49 3cd62121 Michael Hanselmann
50 30e4e741 Iustin Pop
import ganeti.http.auth   # pylint: disable-msg=W0611
51 bc2929fc Michael Hanselmann
import ganeti.http.server
52 3cd62121 Michael Hanselmann
53 bc2929fc Michael Hanselmann
54 7e9760c3 Michael Hanselmann
class RemoteApiRequestContext(object):
55 7e9760c3 Michael Hanselmann
  """Data structure for Remote API requests.
56 7e9760c3 Michael Hanselmann
57 7e9760c3 Michael Hanselmann
  """
58 7e9760c3 Michael Hanselmann
  def __init__(self):
59 7e9760c3 Michael Hanselmann
    self.handler = None
60 7e9760c3 Michael Hanselmann
    self.handler_fn = None
61 b5b67ef9 Michael Hanselmann
    self.handler_access = None
62 ab221ddf Michael Hanselmann
    self.body_data = None
63 7e9760c3 Michael Hanselmann
64 7e9760c3 Michael Hanselmann
65 1f8588f6 Iustin Pop
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
66 1f8588f6 Iustin Pop
  """Custom Request Executor class that formats HTTP errors in JSON.
67 1f8588f6 Iustin Pop
68 1f8588f6 Iustin Pop
  """
69 16b037a9 Michael Hanselmann
  error_content_type = http.HTTP_APP_JSON
70 1f8588f6 Iustin Pop
71 1f8588f6 Iustin Pop
  def _FormatErrorMessage(self, values):
72 1f8588f6 Iustin Pop
    """Formats the body of an error message.
73 1f8588f6 Iustin Pop
74 1f8588f6 Iustin Pop
    @type values: dict
75 1f8588f6 Iustin Pop
    @param values: dictionary with keys code, message and explain.
76 1f8588f6 Iustin Pop
    @rtype: string
77 1f8588f6 Iustin Pop
    @return: the body of the message
78 1f8588f6 Iustin Pop
79 1f8588f6 Iustin Pop
    """
80 1f8588f6 Iustin Pop
    return serializer.DumpJson(values, indent=True)
81 1f8588f6 Iustin Pop
82 1f8588f6 Iustin Pop
83 b5b67ef9 Michael Hanselmann
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
84 b5b67ef9 Michael Hanselmann
                          http.server.HttpServer):
85 3cd62121 Michael Hanselmann
  """REST Request Handler Class.
86 3cd62121 Michael Hanselmann
87 3cd62121 Michael Hanselmann
  """
88 b5b67ef9 Michael Hanselmann
  AUTH_REALM = "Ganeti Remote API"
89 b5b67ef9 Michael Hanselmann
90 16a8967d Michael Hanselmann
  def __init__(self, *args, **kwargs):
91 71d23b33 Iustin Pop
    # pylint: disable-msg=W0233
92 e4ef4343 Michael Hanselmann
    # it seems pylint doesn't see the second parent class there
93 bc2929fc Michael Hanselmann
    http.server.HttpServer.__init__(self, *args, **kwargs)
94 b5b67ef9 Michael Hanselmann
    http.auth.HttpServerRequestAuthentication.__init__(self)
95 3cd62121 Michael Hanselmann
    self._resmap = connector.Mapper()
96 e4ef4343 Michael Hanselmann
    self._users = None
97 3cd62121 Michael Hanselmann
98 e4ef4343 Michael Hanselmann
  def LoadUsers(self, filename):
99 e4ef4343 Michael Hanselmann
    """Loads a file containing users and passwords.
100 a2e60f14 René Nussbaumer
101 e4ef4343 Michael Hanselmann
    @type filename: string
102 e4ef4343 Michael Hanselmann
    @param filename: Path to file
103 a2e60f14 René Nussbaumer
104 a2e60f14 René Nussbaumer
    """
105 e4ef4343 Michael Hanselmann
    if not os.path.isfile(constants.RAPI_USERS_FILE):
106 e4ef4343 Michael Hanselmann
      logging.warning("Users file %s not found", filename)
107 e4ef4343 Michael Hanselmann
      return False
108 a2e60f14 René Nussbaumer
109 a2e60f14 René Nussbaumer
    try:
110 e4ef4343 Michael Hanselmann
      users = http.auth.ReadPasswordFile(filename)
111 a2e60f14 René Nussbaumer
    except Exception, err: # pylint: disable-msg=W0703
112 a2e60f14 René Nussbaumer
      # We don't care about the type of exception
113 e4ef4343 Michael Hanselmann
      logging.error("Error while reading %s: %s", filename, err)
114 e4ef4343 Michael Hanselmann
      return False
115 a2e60f14 René Nussbaumer
116 e4ef4343 Michael Hanselmann
    self._users = users
117 e4ef4343 Michael Hanselmann
    return True
118 a2e60f14 René Nussbaumer
119 7e9760c3 Michael Hanselmann
  def _GetRequestContext(self, req):
120 7e9760c3 Michael Hanselmann
    """Returns the context for a request.
121 7e9760c3 Michael Hanselmann
122 7e9760c3 Michael Hanselmann
    The context is cached in the req.private variable.
123 7e9760c3 Michael Hanselmann
124 7e9760c3 Michael Hanselmann
    """
125 7e9760c3 Michael Hanselmann
    if req.private is None:
126 85414b69 Iustin Pop
      (HandlerClass, items, args) = \
127 85414b69 Iustin Pop
                     self._resmap.getController(req.request_path)
128 7e9760c3 Michael Hanselmann
129 7e9760c3 Michael Hanselmann
      ctx = RemoteApiRequestContext()
130 7e9760c3 Michael Hanselmann
      ctx.handler = HandlerClass(items, args, req)
131 7e9760c3 Michael Hanselmann
132 7e9760c3 Michael Hanselmann
      method = req.request_method.upper()
133 7e9760c3 Michael Hanselmann
      try:
134 7e9760c3 Michael Hanselmann
        ctx.handler_fn = getattr(ctx.handler, method)
135 f4ad2ef0 Iustin Pop
      except AttributeError:
136 33664046 René Nussbaumer
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
137 33664046 René Nussbaumer
                                      (method, req.request_path))
138 7e9760c3 Michael Hanselmann
139 b5b67ef9 Michael Hanselmann
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
140 b5b67ef9 Michael Hanselmann
141 b5b67ef9 Michael Hanselmann
      # Require permissions definition (usually in the base class)
142 b5b67ef9 Michael Hanselmann
      if ctx.handler_access is None:
143 b5b67ef9 Michael Hanselmann
        raise AssertionError("Permissions definition missing")
144 b5b67ef9 Michael Hanselmann
145 ab221ddf Michael Hanselmann
      # This is only made available in HandleRequest
146 ab221ddf Michael Hanselmann
      ctx.body_data = None
147 ab221ddf Michael Hanselmann
148 7e9760c3 Michael Hanselmann
      req.private = ctx
149 7e9760c3 Michael Hanselmann
150 23ccba04 Michael Hanselmann
    # Check for expected attributes
151 23ccba04 Michael Hanselmann
    assert req.private.handler
152 23ccba04 Michael Hanselmann
    assert req.private.handler_fn
153 23ccba04 Michael Hanselmann
    assert req.private.handler_access is not None
154 23ccba04 Michael Hanselmann
155 7e9760c3 Michael Hanselmann
    return req.private
156 7e9760c3 Michael Hanselmann
157 23ccba04 Michael Hanselmann
  def AuthenticationRequired(self, req):
158 23ccba04 Michael Hanselmann
    """Determine whether authentication is required.
159 85414b69 Iustin Pop
160 85414b69 Iustin Pop
    """
161 23ccba04 Michael Hanselmann
    return bool(self._GetRequestContext(req).handler_access)
162 85414b69 Iustin Pop
163 b5b67ef9 Michael Hanselmann
  def Authenticate(self, req, username, password):
164 b5b67ef9 Michael Hanselmann
    """Checks whether a user can access a resource.
165 b5b67ef9 Michael Hanselmann
166 b5b67ef9 Michael Hanselmann
    """
167 b5b67ef9 Michael Hanselmann
    ctx = self._GetRequestContext(req)
168 b5b67ef9 Michael Hanselmann
169 b5b67ef9 Michael Hanselmann
    # Check username and password
170 b5b67ef9 Michael Hanselmann
    valid_user = False
171 b5b67ef9 Michael Hanselmann
    if self._users:
172 b5b67ef9 Michael Hanselmann
      user = self._users.get(username, None)
173 0b08f096 Michael Hanselmann
      if user and self.VerifyBasicAuthPassword(req, username, password,
174 0b08f096 Michael Hanselmann
                                               user.password):
175 b5b67ef9 Michael Hanselmann
        valid_user = True
176 b5b67ef9 Michael Hanselmann
177 b5b67ef9 Michael Hanselmann
    if not valid_user:
178 b5b67ef9 Michael Hanselmann
      # Unknown user or password wrong
179 b5b67ef9 Michael Hanselmann
      return False
180 b5b67ef9 Michael Hanselmann
181 b5b67ef9 Michael Hanselmann
    if (not ctx.handler_access or
182 b5b67ef9 Michael Hanselmann
        set(user.options).intersection(ctx.handler_access)):
183 b5b67ef9 Michael Hanselmann
      # Allow access
184 b5b67ef9 Michael Hanselmann
      return True
185 b5b67ef9 Michael Hanselmann
186 b5b67ef9 Michael Hanselmann
    # Access forbidden
187 b5b67ef9 Michael Hanselmann
    raise http.HttpForbidden()
188 b5b67ef9 Michael Hanselmann
189 16a8967d Michael Hanselmann
  def HandleRequest(self, req):
190 16a8967d Michael Hanselmann
    """Handles a request.
191 3cd62121 Michael Hanselmann
192 3cd62121 Michael Hanselmann
    """
193 7e9760c3 Michael Hanselmann
    ctx = self._GetRequestContext(req)
194 3cd62121 Michael Hanselmann
195 ab221ddf Michael Hanselmann
    # Deserialize request parameters
196 ab221ddf Michael Hanselmann
    if req.request_body:
197 ab221ddf Michael Hanselmann
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
198 ab221ddf Michael Hanselmann
      # include a Content-Type header field defining the media type of that
199 ab221ddf Michael Hanselmann
      # body. [...] If the media type remains unknown, the recipient SHOULD
200 ab221ddf Michael Hanselmann
      # treat it as type "application/octet-stream".
201 ab221ddf Michael Hanselmann
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
202 ab221ddf Michael Hanselmann
                                                 http.HTTP_APP_OCTET_STREAM)
203 16b037a9 Michael Hanselmann
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
204 ab221ddf Michael Hanselmann
        raise http.HttpUnsupportedMediaType()
205 ab221ddf Michael Hanselmann
206 ab221ddf Michael Hanselmann
      try:
207 ab221ddf Michael Hanselmann
        ctx.body_data = serializer.LoadJson(req.request_body)
208 ab221ddf Michael Hanselmann
      except Exception:
209 ab221ddf Michael Hanselmann
        raise http.HttpBadRequest(message="Unable to parse JSON data")
210 ab221ddf Michael Hanselmann
    else:
211 ab221ddf Michael Hanselmann
      ctx.body_data = None
212 ab221ddf Michael Hanselmann
213 3cd62121 Michael Hanselmann
    try:
214 7e9760c3 Michael Hanselmann
      result = ctx.handler_fn()
215 77e1d753 Iustin Pop
    except luxi.TimeoutError:
216 77e1d753 Iustin Pop
      raise http.HttpGatewayTimeout()
217 77e1d753 Iustin Pop
    except luxi.ProtocolError, err:
218 77e1d753 Iustin Pop
      raise http.HttpBadGateway(str(err))
219 16a8967d Michael Hanselmann
    except:
220 e09fdcfa Iustin Pop
      method = req.request_method.upper()
221 16a8967d Michael Hanselmann
      logging.exception("Error while handling the %s request", method)
222 16a8967d Michael Hanselmann
      raise
223 3cd62121 Michael Hanselmann
224 16b037a9 Michael Hanselmann
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
225 ab221ddf Michael Hanselmann
226 ab221ddf Michael Hanselmann
    return serializer.DumpJson(result)
227 8c229cc7 Oleksiy Mishchenko
228 8c229cc7 Oleksiy Mishchenko
229 e4ef4343 Michael Hanselmann
class FileWatcher:
230 e4ef4343 Michael Hanselmann
  def __init__(self, filename, cb):
231 e4ef4343 Michael Hanselmann
    """Initializes this class.
232 e4ef4343 Michael Hanselmann
233 e4ef4343 Michael Hanselmann
    @type filename: string
234 e4ef4343 Michael Hanselmann
    @param filename: File to watch
235 e4ef4343 Michael Hanselmann
    @type cb: callable
236 e4ef4343 Michael Hanselmann
    @param cb: Function called on file change
237 e4ef4343 Michael Hanselmann
238 e4ef4343 Michael Hanselmann
    """
239 e4ef4343 Michael Hanselmann
    self._filename = filename
240 e4ef4343 Michael Hanselmann
    self._cb = cb
241 e4ef4343 Michael Hanselmann
242 e4ef4343 Michael Hanselmann
    wm = pyinotify.WatchManager()
243 e4ef4343 Michael Hanselmann
    self._handler = asyncnotifier.SingleFileEventHandler(wm, self._OnInotify,
244 e4ef4343 Michael Hanselmann
                                                         filename)
245 e4ef4343 Michael Hanselmann
    asyncnotifier.AsyncNotifier(wm, default_proc_fun=self._handler)
246 e4ef4343 Michael Hanselmann
    self._handler.enable()
247 e4ef4343 Michael Hanselmann
248 e4ef4343 Michael Hanselmann
  def _OnInotify(self, notifier_enabled):
249 e4ef4343 Michael Hanselmann
    """Called upon update of the RAPI users file by pyinotify.
250 e4ef4343 Michael Hanselmann
251 e4ef4343 Michael Hanselmann
    @type notifier_enabled: boolean
252 e4ef4343 Michael Hanselmann
    @param notifier_enabled: whether the notifier is still enabled
253 e4ef4343 Michael Hanselmann
254 e4ef4343 Michael Hanselmann
    """
255 e4ef4343 Michael Hanselmann
    logging.info("Reloading modified %s", self._filename)
256 e4ef4343 Michael Hanselmann
257 e4ef4343 Michael Hanselmann
    self._cb()
258 e4ef4343 Michael Hanselmann
259 e4ef4343 Michael Hanselmann
    # Renable the watch again if we'd an atomic update of the file (e.g. mv)
260 e4ef4343 Michael Hanselmann
    if not notifier_enabled:
261 e4ef4343 Michael Hanselmann
      self._handler.enable()
262 e4ef4343 Michael Hanselmann
263 e4ef4343 Michael Hanselmann
264 6c948699 Michael Hanselmann
def CheckRapi(options, args):
265 6c948699 Michael Hanselmann
  """Initial checks whether to run or exit with a failure.
266 8c229cc7 Oleksiy Mishchenko
267 8c229cc7 Oleksiy Mishchenko
  """
268 f93427cd Iustin Pop
  if args: # rapi doesn't take any arguments
269 f93427cd Iustin Pop
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
270 f93427cd Iustin Pop
                          sys.argv[0])
271 be73fc79 Guido Trotter
    sys.exit(constants.EXIT_FAILURE)
272 8c229cc7 Oleksiy Mishchenko
273 04ccf5e9 Guido Trotter
  ssconf.CheckMaster(options.debug)
274 8c229cc7 Oleksiy Mishchenko
275 8b72b05c René Nussbaumer
  # Read SSL certificate (this is a little hackish to read the cert as root)
276 8b72b05c René Nussbaumer
  if options.ssl:
277 8b72b05c René Nussbaumer
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
278 8b72b05c René Nussbaumer
                                            ssl_cert_path=options.ssl_cert)
279 8b72b05c René Nussbaumer
  else:
280 8b72b05c René Nussbaumer
    options.ssl_params = None
281 8b72b05c René Nussbaumer
282 8c229cc7 Oleksiy Mishchenko
283 2d54e29c Iustin Pop
def ExecRapi(options, _):
284 6c948699 Michael Hanselmann
  """Main remote API function, executed with the PID file held.
285 8c229cc7 Oleksiy Mishchenko
286 8c229cc7 Oleksiy Mishchenko
  """
287 2ed6a7d6 Iustin Pop
288 04ccf5e9 Guido Trotter
  mainloop = daemon.Mainloop()
289 04ccf5e9 Guido Trotter
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
290 8b72b05c René Nussbaumer
                               ssl_params=options.ssl_params,
291 8b72b05c René Nussbaumer
                               ssl_verify_peer=False,
292 04ccf5e9 Guido Trotter
                               request_executor_class=JsonErrorRequestExecutor)
293 e4ef4343 Michael Hanselmann
294 e4ef4343 Michael Hanselmann
  if os.path.exists(constants.RAPI_USERS_FILE):
295 e4ef4343 Michael Hanselmann
    # Setup file watcher (it'll be driven by asyncore)
296 e4ef4343 Michael Hanselmann
    FileWatcher(constants.RAPI_USERS_FILE,
297 e4ef4343 Michael Hanselmann
                compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
298 e4ef4343 Michael Hanselmann
299 e4ef4343 Michael Hanselmann
  server.LoadUsers(constants.RAPI_USERS_FILE)
300 e4ef4343 Michael Hanselmann
301 71d23b33 Iustin Pop
  # pylint: disable-msg=E1101
302 71d23b33 Iustin Pop
  # it seems pylint doesn't see the second parent class there
303 04ccf5e9 Guido Trotter
  server.Start()
304 04ccf5e9 Guido Trotter
  try:
305 04ccf5e9 Guido Trotter
    mainloop.Run()
306 04ccf5e9 Guido Trotter
  finally:
307 04ccf5e9 Guido Trotter
    server.Stop()
308 5675cd1f Iustin Pop
309 3cd62121 Michael Hanselmann
310 04ccf5e9 Guido Trotter
def main():
311 04ccf5e9 Guido Trotter
  """Main function.
312 441e7cfd Oleksiy Mishchenko
313 04ccf5e9 Guido Trotter
  """
314 04ccf5e9 Guido Trotter
  parser = optparse.OptionParser(description="Ganeti Remote API",
315 04ccf5e9 Guido Trotter
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
316 9e47cad8 Iustin Pop
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
317 04ccf5e9 Guido Trotter
318 fd346851 René Nussbaumer
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, ExecRapi,
319 0648750e Michael Hanselmann
                     default_ssl_cert=constants.RAPI_CERT_FILE,
320 69d89cb5 René Nussbaumer
                     default_ssl_key=constants.RAPI_CERT_FILE)
321 8c229cc7 Oleksiy Mishchenko
322 8c229cc7 Oleksiy Mishchenko
323 6c948699 Michael Hanselmann
if __name__ == "__main__":
324 8c229cc7 Oleksiy Mishchenko
  main()