Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ a2e60f14

History | View | Annotate | Download (8.6 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 3cd62121 Michael Hanselmann
from ganeti.rapi import connector
48 3cd62121 Michael Hanselmann
49 30e4e741 Iustin Pop
import ganeti.http.auth   # pylint: disable-msg=W0611
50 bc2929fc Michael Hanselmann
import ganeti.http.server
51 3cd62121 Michael Hanselmann
52 bc2929fc Michael Hanselmann
53 7e9760c3 Michael Hanselmann
class RemoteApiRequestContext(object):
54 7e9760c3 Michael Hanselmann
  """Data structure for Remote API requests.
55 7e9760c3 Michael Hanselmann
56 7e9760c3 Michael Hanselmann
  """
57 7e9760c3 Michael Hanselmann
  def __init__(self):
58 7e9760c3 Michael Hanselmann
    self.handler = None
59 7e9760c3 Michael Hanselmann
    self.handler_fn = None
60 b5b67ef9 Michael Hanselmann
    self.handler_access = None
61 ab221ddf Michael Hanselmann
    self.body_data = None
62 7e9760c3 Michael Hanselmann
63 7e9760c3 Michael Hanselmann
64 1f8588f6 Iustin Pop
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
65 1f8588f6 Iustin Pop
  """Custom Request Executor class that formats HTTP errors in JSON.
66 1f8588f6 Iustin Pop
67 1f8588f6 Iustin Pop
  """
68 16b037a9 Michael Hanselmann
  error_content_type = http.HTTP_APP_JSON
69 1f8588f6 Iustin Pop
70 1f8588f6 Iustin Pop
  def _FormatErrorMessage(self, values):
71 1f8588f6 Iustin Pop
    """Formats the body of an error message.
72 1f8588f6 Iustin Pop
73 1f8588f6 Iustin Pop
    @type values: dict
74 1f8588f6 Iustin Pop
    @param values: dictionary with keys code, message and explain.
75 1f8588f6 Iustin Pop
    @rtype: string
76 1f8588f6 Iustin Pop
    @return: the body of the message
77 1f8588f6 Iustin Pop
78 1f8588f6 Iustin Pop
    """
79 1f8588f6 Iustin Pop
    return serializer.DumpJson(values, indent=True)
80 1f8588f6 Iustin Pop
81 1f8588f6 Iustin Pop
82 b5b67ef9 Michael Hanselmann
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
83 b5b67ef9 Michael Hanselmann
                          http.server.HttpServer):
84 3cd62121 Michael Hanselmann
  """REST Request Handler Class.
85 3cd62121 Michael Hanselmann
86 3cd62121 Michael Hanselmann
  """
87 b5b67ef9 Michael Hanselmann
  AUTH_REALM = "Ganeti Remote API"
88 b5b67ef9 Michael Hanselmann
89 16a8967d Michael Hanselmann
  def __init__(self, *args, **kwargs):
90 71d23b33 Iustin Pop
    # pylint: disable-msg=W0233
91 71d23b33 Iustin Pop
  # it seems pylint doesn't see the second parent class there
92 bc2929fc Michael Hanselmann
    http.server.HttpServer.__init__(self, *args, **kwargs)
93 b5b67ef9 Michael Hanselmann
    http.auth.HttpServerRequestAuthentication.__init__(self)
94 3cd62121 Michael Hanselmann
    self._resmap = connector.Mapper()
95 3cd62121 Michael Hanselmann
96 b5b67ef9 Michael Hanselmann
    # Load password file
97 b5b67ef9 Michael Hanselmann
    if os.path.isfile(constants.RAPI_USERS_FILE):
98 a2e60f14 René Nussbaumer
      wm = pyinotify.WatchManager()
99 a2e60f14 René Nussbaumer
      hdl = asyncnotifier.SingleFileEventHandler(wm, self._OnUsersFileUpdate,
100 a2e60f14 René Nussbaumer
                                                 constants.RAPI_USERS_FILE)
101 a2e60f14 René Nussbaumer
      self._users_inotify_handler = hdl
102 a2e60f14 René Nussbaumer
      asyncnotifier.AsyncNotifier(wm, default_proc_fun=hdl)
103 a2e60f14 René Nussbaumer
      self._users = None
104 a2e60f14 René Nussbaumer
      self._OnUsersFileUpdate(False)
105 b5b67ef9 Michael Hanselmann
    else:
106 b5b67ef9 Michael Hanselmann
      self._users = None
107 b5b67ef9 Michael Hanselmann
108 a2e60f14 René Nussbaumer
  def _OnUsersFileUpdate(self, notifier_enabled):
109 a2e60f14 René Nussbaumer
    """Called upon update of the RAPI users file by pyinotify.
110 a2e60f14 René Nussbaumer
111 a2e60f14 René Nussbaumer
    @type notifier_enabled: boolean
112 a2e60f14 René Nussbaumer
    @param notifier_enabled: whether the notifier is still enabled
113 a2e60f14 René Nussbaumer
114 a2e60f14 René Nussbaumer
    """
115 a2e60f14 René Nussbaumer
    logging.info("Reloading modified %s", constants.RAPI_USERS_FILE)
116 a2e60f14 René Nussbaumer
117 a2e60f14 René Nussbaumer
    try:
118 a2e60f14 René Nussbaumer
      users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
119 a2e60f14 René Nussbaumer
      self._users = users
120 a2e60f14 René Nussbaumer
    except Exception, err: # pylint: disable-msg=W0703
121 a2e60f14 René Nussbaumer
      # We don't care about the type of exception
122 a2e60f14 René Nussbaumer
      logging.error("Error while reading %s: %s", constants.RAPI_USERS_FILE,
123 a2e60f14 René Nussbaumer
                    err)
124 a2e60f14 René Nussbaumer
125 a2e60f14 René Nussbaumer
    # Renable the watch again if we'd an atomic update of the file (e.g. mv)
126 a2e60f14 René Nussbaumer
    if not notifier_enabled:
127 a2e60f14 René Nussbaumer
      self._users_inotify_handler.enable()
128 a2e60f14 René Nussbaumer
129 7e9760c3 Michael Hanselmann
  def _GetRequestContext(self, req):
130 7e9760c3 Michael Hanselmann
    """Returns the context for a request.
131 7e9760c3 Michael Hanselmann
132 7e9760c3 Michael Hanselmann
    The context is cached in the req.private variable.
133 7e9760c3 Michael Hanselmann
134 7e9760c3 Michael Hanselmann
    """
135 7e9760c3 Michael Hanselmann
    if req.private is None:
136 85414b69 Iustin Pop
      (HandlerClass, items, args) = \
137 85414b69 Iustin Pop
                     self._resmap.getController(req.request_path)
138 7e9760c3 Michael Hanselmann
139 7e9760c3 Michael Hanselmann
      ctx = RemoteApiRequestContext()
140 7e9760c3 Michael Hanselmann
      ctx.handler = HandlerClass(items, args, req)
141 7e9760c3 Michael Hanselmann
142 7e9760c3 Michael Hanselmann
      method = req.request_method.upper()
143 7e9760c3 Michael Hanselmann
      try:
144 7e9760c3 Michael Hanselmann
        ctx.handler_fn = getattr(ctx.handler, method)
145 f4ad2ef0 Iustin Pop
      except AttributeError:
146 33664046 René Nussbaumer
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
147 33664046 René Nussbaumer
                                      (method, req.request_path))
148 7e9760c3 Michael Hanselmann
149 b5b67ef9 Michael Hanselmann
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
150 b5b67ef9 Michael Hanselmann
151 b5b67ef9 Michael Hanselmann
      # Require permissions definition (usually in the base class)
152 b5b67ef9 Michael Hanselmann
      if ctx.handler_access is None:
153 b5b67ef9 Michael Hanselmann
        raise AssertionError("Permissions definition missing")
154 b5b67ef9 Michael Hanselmann
155 ab221ddf Michael Hanselmann
      # This is only made available in HandleRequest
156 ab221ddf Michael Hanselmann
      ctx.body_data = None
157 ab221ddf Michael Hanselmann
158 7e9760c3 Michael Hanselmann
      req.private = ctx
159 7e9760c3 Michael Hanselmann
160 23ccba04 Michael Hanselmann
    # Check for expected attributes
161 23ccba04 Michael Hanselmann
    assert req.private.handler
162 23ccba04 Michael Hanselmann
    assert req.private.handler_fn
163 23ccba04 Michael Hanselmann
    assert req.private.handler_access is not None
164 23ccba04 Michael Hanselmann
165 7e9760c3 Michael Hanselmann
    return req.private
166 7e9760c3 Michael Hanselmann
167 23ccba04 Michael Hanselmann
  def AuthenticationRequired(self, req):
168 23ccba04 Michael Hanselmann
    """Determine whether authentication is required.
169 85414b69 Iustin Pop
170 85414b69 Iustin Pop
    """
171 23ccba04 Michael Hanselmann
    return bool(self._GetRequestContext(req).handler_access)
172 85414b69 Iustin Pop
173 b5b67ef9 Michael Hanselmann
  def Authenticate(self, req, username, password):
174 b5b67ef9 Michael Hanselmann
    """Checks whether a user can access a resource.
175 b5b67ef9 Michael Hanselmann
176 b5b67ef9 Michael Hanselmann
    """
177 b5b67ef9 Michael Hanselmann
    ctx = self._GetRequestContext(req)
178 b5b67ef9 Michael Hanselmann
179 b5b67ef9 Michael Hanselmann
    # Check username and password
180 b5b67ef9 Michael Hanselmann
    valid_user = False
181 b5b67ef9 Michael Hanselmann
    if self._users:
182 b5b67ef9 Michael Hanselmann
      user = self._users.get(username, None)
183 0b08f096 Michael Hanselmann
      if user and self.VerifyBasicAuthPassword(req, username, password,
184 0b08f096 Michael Hanselmann
                                               user.password):
185 b5b67ef9 Michael Hanselmann
        valid_user = True
186 b5b67ef9 Michael Hanselmann
187 b5b67ef9 Michael Hanselmann
    if not valid_user:
188 b5b67ef9 Michael Hanselmann
      # Unknown user or password wrong
189 b5b67ef9 Michael Hanselmann
      return False
190 b5b67ef9 Michael Hanselmann
191 b5b67ef9 Michael Hanselmann
    if (not ctx.handler_access or
192 b5b67ef9 Michael Hanselmann
        set(user.options).intersection(ctx.handler_access)):
193 b5b67ef9 Michael Hanselmann
      # Allow access
194 b5b67ef9 Michael Hanselmann
      return True
195 b5b67ef9 Michael Hanselmann
196 b5b67ef9 Michael Hanselmann
    # Access forbidden
197 b5b67ef9 Michael Hanselmann
    raise http.HttpForbidden()
198 b5b67ef9 Michael Hanselmann
199 16a8967d Michael Hanselmann
  def HandleRequest(self, req):
200 16a8967d Michael Hanselmann
    """Handles a request.
201 3cd62121 Michael Hanselmann
202 3cd62121 Michael Hanselmann
    """
203 7e9760c3 Michael Hanselmann
    ctx = self._GetRequestContext(req)
204 3cd62121 Michael Hanselmann
205 ab221ddf Michael Hanselmann
    # Deserialize request parameters
206 ab221ddf Michael Hanselmann
    if req.request_body:
207 ab221ddf Michael Hanselmann
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
208 ab221ddf Michael Hanselmann
      # include a Content-Type header field defining the media type of that
209 ab221ddf Michael Hanselmann
      # body. [...] If the media type remains unknown, the recipient SHOULD
210 ab221ddf Michael Hanselmann
      # treat it as type "application/octet-stream".
211 ab221ddf Michael Hanselmann
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
212 ab221ddf Michael Hanselmann
                                                 http.HTTP_APP_OCTET_STREAM)
213 16b037a9 Michael Hanselmann
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
214 ab221ddf Michael Hanselmann
        raise http.HttpUnsupportedMediaType()
215 ab221ddf Michael Hanselmann
216 ab221ddf Michael Hanselmann
      try:
217 ab221ddf Michael Hanselmann
        ctx.body_data = serializer.LoadJson(req.request_body)
218 ab221ddf Michael Hanselmann
      except Exception:
219 ab221ddf Michael Hanselmann
        raise http.HttpBadRequest(message="Unable to parse JSON data")
220 ab221ddf Michael Hanselmann
    else:
221 ab221ddf Michael Hanselmann
      ctx.body_data = None
222 ab221ddf Michael Hanselmann
223 3cd62121 Michael Hanselmann
    try:
224 7e9760c3 Michael Hanselmann
      result = ctx.handler_fn()
225 77e1d753 Iustin Pop
    except luxi.TimeoutError:
226 77e1d753 Iustin Pop
      raise http.HttpGatewayTimeout()
227 77e1d753 Iustin Pop
    except luxi.ProtocolError, err:
228 77e1d753 Iustin Pop
      raise http.HttpBadGateway(str(err))
229 16a8967d Michael Hanselmann
    except:
230 e09fdcfa Iustin Pop
      method = req.request_method.upper()
231 16a8967d Michael Hanselmann
      logging.exception("Error while handling the %s request", method)
232 16a8967d Michael Hanselmann
      raise
233 3cd62121 Michael Hanselmann
234 16b037a9 Michael Hanselmann
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
235 ab221ddf Michael Hanselmann
236 ab221ddf Michael Hanselmann
    return serializer.DumpJson(result)
237 8c229cc7 Oleksiy Mishchenko
238 8c229cc7 Oleksiy Mishchenko
239 6c948699 Michael Hanselmann
def CheckRapi(options, args):
240 6c948699 Michael Hanselmann
  """Initial checks whether to run or exit with a failure.
241 8c229cc7 Oleksiy Mishchenko
242 8c229cc7 Oleksiy Mishchenko
  """
243 f93427cd Iustin Pop
  if args: # rapi doesn't take any arguments
244 f93427cd Iustin Pop
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
245 f93427cd Iustin Pop
                          sys.argv[0])
246 be73fc79 Guido Trotter
    sys.exit(constants.EXIT_FAILURE)
247 8c229cc7 Oleksiy Mishchenko
248 04ccf5e9 Guido Trotter
  ssconf.CheckMaster(options.debug)
249 8c229cc7 Oleksiy Mishchenko
250 8b72b05c René Nussbaumer
  # Read SSL certificate (this is a little hackish to read the cert as root)
251 8b72b05c René Nussbaumer
  if options.ssl:
252 8b72b05c René Nussbaumer
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
253 8b72b05c René Nussbaumer
                                            ssl_cert_path=options.ssl_cert)
254 8b72b05c René Nussbaumer
  else:
255 8b72b05c René Nussbaumer
    options.ssl_params = None
256 8b72b05c René Nussbaumer
257 8c229cc7 Oleksiy Mishchenko
258 2d54e29c Iustin Pop
def ExecRapi(options, _):
259 6c948699 Michael Hanselmann
  """Main remote API function, executed with the PID file held.
260 8c229cc7 Oleksiy Mishchenko
261 8c229cc7 Oleksiy Mishchenko
  """
262 2ed6a7d6 Iustin Pop
263 04ccf5e9 Guido Trotter
  mainloop = daemon.Mainloop()
264 04ccf5e9 Guido Trotter
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
265 8b72b05c René Nussbaumer
                               ssl_params=options.ssl_params,
266 8b72b05c René Nussbaumer
                               ssl_verify_peer=False,
267 04ccf5e9 Guido Trotter
                               request_executor_class=JsonErrorRequestExecutor)
268 71d23b33 Iustin Pop
  # pylint: disable-msg=E1101
269 71d23b33 Iustin Pop
  # it seems pylint doesn't see the second parent class there
270 04ccf5e9 Guido Trotter
  server.Start()
271 04ccf5e9 Guido Trotter
  try:
272 04ccf5e9 Guido Trotter
    mainloop.Run()
273 04ccf5e9 Guido Trotter
  finally:
274 04ccf5e9 Guido Trotter
    server.Stop()
275 5675cd1f Iustin Pop
276 3cd62121 Michael Hanselmann
277 04ccf5e9 Guido Trotter
def main():
278 04ccf5e9 Guido Trotter
  """Main function.
279 441e7cfd Oleksiy Mishchenko
280 04ccf5e9 Guido Trotter
  """
281 04ccf5e9 Guido Trotter
  parser = optparse.OptionParser(description="Ganeti Remote API",
282 04ccf5e9 Guido Trotter
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
283 9e47cad8 Iustin Pop
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
284 04ccf5e9 Guido Trotter
285 fd346851 René Nussbaumer
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, ExecRapi,
286 0648750e Michael Hanselmann
                     default_ssl_cert=constants.RAPI_CERT_FILE,
287 69d89cb5 René Nussbaumer
                     default_ssl_key=constants.RAPI_CERT_FILE)
288 8c229cc7 Oleksiy Mishchenko
289 8c229cc7 Oleksiy Mishchenko
290 6c948699 Michael Hanselmann
if __name__ == "__main__":
291 8c229cc7 Oleksiy Mishchenko
  main()