Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ a2e60f14

History | View | Annotate | Download (8.6 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 2007 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
"""Ganeti Remote API master script.
22

    
23
"""
24

    
25
# pylint: disable-msg=C0103,W0142
26

    
27
# C0103: Invalid name ganeti-watcher
28

    
29
import logging
30
import optparse
31
import sys
32
import os
33
import os.path
34

    
35
try:
36
  from pyinotify import pyinotify # pylint: disable-msg=E0611
37
except ImportError:
38
  import pyinotify
39

    
40
from ganeti import asyncnotifier
41
from ganeti import constants
42
from ganeti import http
43
from ganeti import daemon
44
from ganeti import ssconf
45
from ganeti import luxi
46
from ganeti import serializer
47
from ganeti.rapi import connector
48

    
49
import ganeti.http.auth   # pylint: disable-msg=W0611
50
import ganeti.http.server
51

    
52

    
53
class RemoteApiRequestContext(object):
54
  """Data structure for Remote API requests.
55

    
56
  """
57
  def __init__(self):
58
    self.handler = None
59
    self.handler_fn = None
60
    self.handler_access = None
61
    self.body_data = None
62

    
63

    
64
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
65
  """Custom Request Executor class that formats HTTP errors in JSON.
66

    
67
  """
68
  error_content_type = http.HTTP_APP_JSON
69

    
70
  def _FormatErrorMessage(self, values):
71
    """Formats the body of an error message.
72

    
73
    @type values: dict
74
    @param values: dictionary with keys code, message and explain.
75
    @rtype: string
76
    @return: the body of the message
77

    
78
    """
79
    return serializer.DumpJson(values, indent=True)
80

    
81

    
82
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
83
                          http.server.HttpServer):
84
  """REST Request Handler Class.
85

    
86
  """
87
  AUTH_REALM = "Ganeti Remote API"
88

    
89
  def __init__(self, *args, **kwargs):
90
    # pylint: disable-msg=W0233
91
  # it seems pylint doesn't see the second parent class there
92
    http.server.HttpServer.__init__(self, *args, **kwargs)
93
    http.auth.HttpServerRequestAuthentication.__init__(self)
94
    self._resmap = connector.Mapper()
95

    
96
    # Load password file
97
    if os.path.isfile(constants.RAPI_USERS_FILE):
98
      wm = pyinotify.WatchManager()
99
      hdl = asyncnotifier.SingleFileEventHandler(wm, self._OnUsersFileUpdate,
100
                                                 constants.RAPI_USERS_FILE)
101
      self._users_inotify_handler = hdl
102
      asyncnotifier.AsyncNotifier(wm, default_proc_fun=hdl)
103
      self._users = None
104
      self._OnUsersFileUpdate(False)
105
    else:
106
      self._users = None
107

    
108
  def _OnUsersFileUpdate(self, notifier_enabled):
109
    """Called upon update of the RAPI users file by pyinotify.
110

    
111
    @type notifier_enabled: boolean
112
    @param notifier_enabled: whether the notifier is still enabled
113

    
114
    """
115
    logging.info("Reloading modified %s", constants.RAPI_USERS_FILE)
116

    
117
    try:
118
      users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
119
      self._users = users
120
    except Exception, err: # pylint: disable-msg=W0703
121
      # We don't care about the type of exception
122
      logging.error("Error while reading %s: %s", constants.RAPI_USERS_FILE,
123
                    err)
124

    
125
    # Renable the watch again if we'd an atomic update of the file (e.g. mv)
126
    if not notifier_enabled:
127
      self._users_inotify_handler.enable()
128

    
129
  def _GetRequestContext(self, req):
130
    """Returns the context for a request.
131

    
132
    The context is cached in the req.private variable.
133

    
134
    """
135
    if req.private is None:
136
      (HandlerClass, items, args) = \
137
                     self._resmap.getController(req.request_path)
138

    
139
      ctx = RemoteApiRequestContext()
140
      ctx.handler = HandlerClass(items, args, req)
141

    
142
      method = req.request_method.upper()
143
      try:
144
        ctx.handler_fn = getattr(ctx.handler, method)
145
      except AttributeError:
146
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
147
                                      (method, req.request_path))
148

    
149
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
150

    
151
      # Require permissions definition (usually in the base class)
152
      if ctx.handler_access is None:
153
        raise AssertionError("Permissions definition missing")
154

    
155
      # This is only made available in HandleRequest
156
      ctx.body_data = None
157

    
158
      req.private = ctx
159

    
160
    # Check for expected attributes
161
    assert req.private.handler
162
    assert req.private.handler_fn
163
    assert req.private.handler_access is not None
164

    
165
    return req.private
166

    
167
  def AuthenticationRequired(self, req):
168
    """Determine whether authentication is required.
169

    
170
    """
171
    return bool(self._GetRequestContext(req).handler_access)
172

    
173
  def Authenticate(self, req, username, password):
174
    """Checks whether a user can access a resource.
175

    
176
    """
177
    ctx = self._GetRequestContext(req)
178

    
179
    # Check username and password
180
    valid_user = False
181
    if self._users:
182
      user = self._users.get(username, None)
183
      if user and self.VerifyBasicAuthPassword(req, username, password,
184
                                               user.password):
185
        valid_user = True
186

    
187
    if not valid_user:
188
      # Unknown user or password wrong
189
      return False
190

    
191
    if (not ctx.handler_access or
192
        set(user.options).intersection(ctx.handler_access)):
193
      # Allow access
194
      return True
195

    
196
    # Access forbidden
197
    raise http.HttpForbidden()
198

    
199
  def HandleRequest(self, req):
200
    """Handles a request.
201

    
202
    """
203
    ctx = self._GetRequestContext(req)
204

    
205
    # Deserialize request parameters
206
    if req.request_body:
207
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
208
      # include a Content-Type header field defining the media type of that
209
      # body. [...] If the media type remains unknown, the recipient SHOULD
210
      # treat it as type "application/octet-stream".
211
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
212
                                                 http.HTTP_APP_OCTET_STREAM)
213
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
214
        raise http.HttpUnsupportedMediaType()
215

    
216
      try:
217
        ctx.body_data = serializer.LoadJson(req.request_body)
218
      except Exception:
219
        raise http.HttpBadRequest(message="Unable to parse JSON data")
220
    else:
221
      ctx.body_data = None
222

    
223
    try:
224
      result = ctx.handler_fn()
225
    except luxi.TimeoutError:
226
      raise http.HttpGatewayTimeout()
227
    except luxi.ProtocolError, err:
228
      raise http.HttpBadGateway(str(err))
229
    except:
230
      method = req.request_method.upper()
231
      logging.exception("Error while handling the %s request", method)
232
      raise
233

    
234
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
235

    
236
    return serializer.DumpJson(result)
237

    
238

    
239
def CheckRapi(options, args):
240
  """Initial checks whether to run or exit with a failure.
241

    
242
  """
243
  if args: # rapi doesn't take any arguments
244
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
245
                          sys.argv[0])
246
    sys.exit(constants.EXIT_FAILURE)
247

    
248
  ssconf.CheckMaster(options.debug)
249

    
250
  # Read SSL certificate (this is a little hackish to read the cert as root)
251
  if options.ssl:
252
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
253
                                            ssl_cert_path=options.ssl_cert)
254
  else:
255
    options.ssl_params = None
256

    
257

    
258
def ExecRapi(options, _):
259
  """Main remote API function, executed with the PID file held.
260

    
261
  """
262

    
263
  mainloop = daemon.Mainloop()
264
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
265
                               ssl_params=options.ssl_params,
266
                               ssl_verify_peer=False,
267
                               request_executor_class=JsonErrorRequestExecutor)
268
  # pylint: disable-msg=E1101
269
  # it seems pylint doesn't see the second parent class there
270
  server.Start()
271
  try:
272
    mainloop.Run()
273
  finally:
274
    server.Stop()
275

    
276

    
277
def main():
278
  """Main function.
279

    
280
  """
281
  parser = optparse.OptionParser(description="Ganeti Remote API",
282
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
283
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
284

    
285
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, ExecRapi,
286
                     default_ssl_cert=constants.RAPI_CERT_FILE,
287
                     default_ssl_key=constants.RAPI_CERT_FILE)
288

    
289

    
290
if __name__ == "__main__":
291
  main()