Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 26d3fd2f

History | View | Annotate | Download (9.3 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 import compat
48
from ganeti.rapi import connector
49

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

    
53

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

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

    
64

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

    
68
  """
69
  error_content_type = http.HTTP_APP_JSON
70

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

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

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

    
82

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

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

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

    
98
  def LoadUsers(self, filename):
99
    """Loads a file containing users and passwords.
100

    
101
    @type filename: string
102
    @param filename: Path to file
103

    
104
    """
105
    if not os.path.isfile(constants.RAPI_USERS_FILE):
106
      logging.warning("Users file %s not found", filename)
107
      return False
108

    
109
    try:
110
      users = http.auth.ReadPasswordFile(filename)
111
    except Exception, err: # pylint: disable-msg=W0703
112
      # We don't care about the type of exception
113
      logging.error("Error while reading %s: %s", filename, err)
114
      return False
115

    
116
    self._users = users
117
    return True
118

    
119
  def _GetRequestContext(self, req):
120
    """Returns the context for a request.
121

    
122
    The context is cached in the req.private variable.
123

    
124
    """
125
    if req.private is None:
126
      (HandlerClass, items, args) = \
127
                     self._resmap.getController(req.request_path)
128

    
129
      ctx = RemoteApiRequestContext()
130
      ctx.handler = HandlerClass(items, args, req)
131

    
132
      method = req.request_method.upper()
133
      try:
134
        ctx.handler_fn = getattr(ctx.handler, method)
135
      except AttributeError:
136
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
137
                                      (method, req.request_path))
138

    
139
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
140

    
141
      # Require permissions definition (usually in the base class)
142
      if ctx.handler_access is None:
143
        raise AssertionError("Permissions definition missing")
144

    
145
      # This is only made available in HandleRequest
146
      ctx.body_data = None
147

    
148
      req.private = ctx
149

    
150
    # Check for expected attributes
151
    assert req.private.handler
152
    assert req.private.handler_fn
153
    assert req.private.handler_access is not None
154

    
155
    return req.private
156

    
157
  def AuthenticationRequired(self, req):
158
    """Determine whether authentication is required.
159

    
160
    """
161
    return bool(self._GetRequestContext(req).handler_access)
162

    
163
  def Authenticate(self, req, username, password):
164
    """Checks whether a user can access a resource.
165

    
166
    """
167
    ctx = self._GetRequestContext(req)
168

    
169
    # Check username and password
170
    valid_user = False
171
    if self._users:
172
      user = self._users.get(username, None)
173
      if user and self.VerifyBasicAuthPassword(req, username, password,
174
                                               user.password):
175
        valid_user = True
176

    
177
    if not valid_user:
178
      # Unknown user or password wrong
179
      return False
180

    
181
    if (not ctx.handler_access or
182
        set(user.options).intersection(ctx.handler_access)):
183
      # Allow access
184
      return True
185

    
186
    # Access forbidden
187
    raise http.HttpForbidden()
188

    
189
  def HandleRequest(self, req):
190
    """Handles a request.
191

    
192
    """
193
    ctx = self._GetRequestContext(req)
194

    
195
    # Deserialize request parameters
196
    if req.request_body:
197
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
198
      # include a Content-Type header field defining the media type of that
199
      # body. [...] If the media type remains unknown, the recipient SHOULD
200
      # treat it as type "application/octet-stream".
201
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
202
                                                 http.HTTP_APP_OCTET_STREAM)
203
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
204
        raise http.HttpUnsupportedMediaType()
205

    
206
      try:
207
        ctx.body_data = serializer.LoadJson(req.request_body)
208
      except Exception:
209
        raise http.HttpBadRequest(message="Unable to parse JSON data")
210
    else:
211
      ctx.body_data = None
212

    
213
    try:
214
      result = ctx.handler_fn()
215
    except luxi.TimeoutError:
216
      raise http.HttpGatewayTimeout()
217
    except luxi.ProtocolError, err:
218
      raise http.HttpBadGateway(str(err))
219
    except:
220
      method = req.request_method.upper()
221
      logging.exception("Error while handling the %s request", method)
222
      raise
223

    
224
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
225

    
226
    return serializer.DumpJson(result)
227

    
228

    
229
class FileWatcher:
230
  def __init__(self, filename, cb):
231
    """Initializes this class.
232

    
233
    @type filename: string
234
    @param filename: File to watch
235
    @type cb: callable
236
    @param cb: Function called on file change
237

    
238
    """
239
    self._filename = filename
240
    self._cb = cb
241

    
242
    wm = pyinotify.WatchManager()
243
    self._handler = asyncnotifier.SingleFileEventHandler(wm, self._OnInotify,
244
                                                         filename)
245
    asyncnotifier.AsyncNotifier(wm, default_proc_fun=self._handler)
246
    self._handler.enable()
247

    
248
  def _OnInotify(self, notifier_enabled):
249
    """Called upon update of the RAPI users file by pyinotify.
250

    
251
    @type notifier_enabled: boolean
252
    @param notifier_enabled: whether the notifier is still enabled
253

    
254
    """
255
    logging.info("Reloading modified %s", self._filename)
256

    
257
    self._cb()
258

    
259
    # Renable the watch again if we'd an atomic update of the file (e.g. mv)
260
    if not notifier_enabled:
261
      self._handler.enable()
262

    
263

    
264
def CheckRapi(options, args):
265
  """Initial checks whether to run or exit with a failure.
266

    
267
  """
268
  if args: # rapi doesn't take any arguments
269
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
270
                          sys.argv[0])
271
    sys.exit(constants.EXIT_FAILURE)
272

    
273
  ssconf.CheckMaster(options.debug)
274

    
275
  # Read SSL certificate (this is a little hackish to read the cert as root)
276
  if options.ssl:
277
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
278
                                            ssl_cert_path=options.ssl_cert)
279
  else:
280
    options.ssl_params = None
281

    
282

    
283
def ExecRapi(options, _):
284
  """Main remote API function, executed with the PID file held.
285

    
286
  """
287

    
288
  mainloop = daemon.Mainloop()
289
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
290
                               ssl_params=options.ssl_params,
291
                               ssl_verify_peer=False,
292
                               request_executor_class=JsonErrorRequestExecutor)
293

    
294
  if os.path.exists(constants.RAPI_USERS_FILE):
295
    # Setup file watcher (it'll be driven by asyncore)
296
    FileWatcher(constants.RAPI_USERS_FILE,
297
                compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
298

    
299
  server.LoadUsers(constants.RAPI_USERS_FILE)
300

    
301
  # pylint: disable-msg=E1101
302
  # it seems pylint doesn't see the second parent class there
303
  server.Start()
304
  try:
305
    mainloop.Run()
306
  finally:
307
    server.Stop()
308

    
309

    
310
def main():
311
  """Main function.
312

    
313
  """
314
  parser = optparse.OptionParser(description="Ganeti Remote API",
315
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
316
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
317

    
318
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, ExecRapi,
319
                     default_ssl_cert=constants.RAPI_CERT_FILE,
320
                     default_ssl_key=constants.RAPI_CERT_FILE)
321

    
322

    
323
if __name__ == "__main__":
324
  main()