Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 9e47cad8

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