Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 2287b920

History | View | Annotate | Download (9.6 kB)

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

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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 import utils
49
from ganeti.rapi import connector
50

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

    
54

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

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

    
65

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

    
69
  """
70
  error_content_type = http.HTTP_APP_JSON
71

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

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

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

    
83

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

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

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

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

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

    
105
    """
106
    try:
107
      contents = utils.ReadFile(filename)
108
    except EnvironmentError, err:
109
      logging.warning("Error while reading %s: %s", filename, err)
110
      return False
111

    
112
    try:
113
      users = http.auth.ParsePasswordFile(contents)
114
    except Exception, err: # pylint: disable-msg=W0703
115
      # We don't care about the type of exception
116
      logging.error("Error while parsing %s: %s", filename, err)
117
      return False
118

    
119
    self._users = users
120
    return True
121

    
122
  def _GetRequestContext(self, req):
123
    """Returns the context for a request.
124

    
125
    The context is cached in the req.private variable.
126

    
127
    """
128
    if req.private is None:
129
      (HandlerClass, items, args) = \
130
                     self._resmap.getController(req.request_path)
131

    
132
      ctx = RemoteApiRequestContext()
133
      ctx.handler = HandlerClass(items, args, req)
134

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

    
142
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
143

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

    
148
      # This is only made available in HandleRequest
149
      ctx.body_data = None
150

    
151
      req.private = ctx
152

    
153
    # Check for expected attributes
154
    assert req.private.handler
155
    assert req.private.handler_fn
156
    assert req.private.handler_access is not None
157

    
158
    return req.private
159

    
160
  def AuthenticationRequired(self, req):
161
    """Determine whether authentication is required.
162

    
163
    """
164
    return bool(self._GetRequestContext(req).handler_access)
165

    
166
  def Authenticate(self, req, username, password):
167
    """Checks whether a user can access a resource.
168

    
169
    """
170
    ctx = self._GetRequestContext(req)
171

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

    
180
    if not valid_user:
181
      # Unknown user or password wrong
182
      return False
183

    
184
    if (not ctx.handler_access or
185
        set(user.options).intersection(ctx.handler_access)):
186
      # Allow access
187
      return True
188

    
189
    # Access forbidden
190
    raise http.HttpForbidden()
191

    
192
  def HandleRequest(self, req):
193
    """Handles a request.
194

    
195
    """
196
    ctx = self._GetRequestContext(req)
197

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

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

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

    
227
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
228

    
229
    return serializer.DumpJson(result)
230

    
231

    
232
class FileWatcher:
233
  def __init__(self, filename, cb):
234
    """Initializes this class.
235

    
236
    @type filename: string
237
    @param filename: File to watch
238
    @type cb: callable
239
    @param cb: Function called on file change
240

    
241
    """
242
    self._filename = filename
243
    self._cb = cb
244

    
245
    wm = pyinotify.WatchManager()
246
    self._handler = asyncnotifier.SingleFileEventHandler(wm, self._OnInotify,
247
                                                         filename)
248
    asyncnotifier.AsyncNotifier(wm, default_proc_fun=self._handler)
249
    self._handler.enable()
250

    
251
  def _OnInotify(self, notifier_enabled):
252
    """Called upon update of the RAPI users file by pyinotify.
253

    
254
    @type notifier_enabled: boolean
255
    @param notifier_enabled: whether the notifier is still enabled
256

    
257
    """
258
    logging.info("Reloading modified %s", self._filename)
259

    
260
    self._cb()
261

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

    
266

    
267
def CheckRapi(options, args):
268
  """Initial checks whether to run or exit with a failure.
269

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

    
276
  ssconf.CheckMaster(options.debug)
277

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

    
285

    
286
def PrepRapi(options, _):
287
  """Prep remote API function, executed with the PID file held.
288

    
289
  """
290

    
291
  mainloop = daemon.Mainloop()
292
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
293
                               ssl_params=options.ssl_params,
294
                               ssl_verify_peer=False,
295
                               request_executor_class=JsonErrorRequestExecutor)
296

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

    
302
  server.LoadUsers(constants.RAPI_USERS_FILE)
303

    
304
  # pylint: disable-msg=E1101
305
  # it seems pylint doesn't see the second parent class there
306
  server.Start()
307
  return (mainloop, server)
308

    
309
def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
310
  """Main remote API function, executed with the PID file held.
311

    
312
  """
313
  (mainloop, server) = prep_data
314
  try:
315
    mainloop.Run()
316
  finally:
317
    server.Stop()
318

    
319

    
320
def main():
321
  """Main function.
322

    
323
  """
324
  parser = optparse.OptionParser(description="Ganeti Remote API",
325
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
326
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
327

    
328
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
329
                     default_ssl_cert=constants.RAPI_CERT_FILE,
330
                     default_ssl_key=constants.RAPI_CERT_FILE)
331

    
332

    
333
if __name__ == "__main__":
334
  main()