Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 8b72b05c

History | View | Annotate | Download (7.7 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
from ganeti import constants
36
from ganeti import http
37
from ganeti import daemon
38
from ganeti import ssconf
39
from ganeti import luxi
40
from ganeti import serializer
41
from ganeti.rapi import connector
42

    
43
import ganeti.http.auth   # pylint: disable-msg=W0611
44
import ganeti.http.server
45

    
46

    
47
class RemoteApiRequestContext(object):
48
  """Data structure for Remote API requests.
49

    
50
  """
51
  def __init__(self):
52
    self.handler = None
53
    self.handler_fn = None
54
    self.handler_access = None
55
    self.body_data = None
56

    
57

    
58
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
59
  """Custom Request Executor class that formats HTTP errors in JSON.
60

    
61
  """
62
  error_content_type = http.HTTP_APP_JSON
63

    
64
  def _FormatErrorMessage(self, values):
65
    """Formats the body of an error message.
66

    
67
    @type values: dict
68
    @param values: dictionary with keys code, message and explain.
69
    @rtype: string
70
    @return: the body of the message
71

    
72
    """
73
    return serializer.DumpJson(values, indent=True)
74

    
75

    
76
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
77
                          http.server.HttpServer):
78
  """REST Request Handler Class.
79

    
80
  """
81
  AUTH_REALM = "Ganeti Remote API"
82

    
83
  def __init__(self, *args, **kwargs):
84
    # pylint: disable-msg=W0233
85
  # it seems pylint doesn't see the second parent class there
86
    http.server.HttpServer.__init__(self, *args, **kwargs)
87
    http.auth.HttpServerRequestAuthentication.__init__(self)
88
    self._resmap = connector.Mapper()
89

    
90
    # Load password file
91
    if os.path.isfile(constants.RAPI_USERS_FILE):
92
      self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
93
    else:
94
      self._users = None
95

    
96
  def _GetRequestContext(self, req):
97
    """Returns the context for a request.
98

    
99
    The context is cached in the req.private variable.
100

    
101
    """
102
    if req.private is None:
103
      (HandlerClass, items, args) = \
104
                     self._resmap.getController(req.request_path)
105

    
106
      ctx = RemoteApiRequestContext()
107
      ctx.handler = HandlerClass(items, args, req)
108

    
109
      method = req.request_method.upper()
110
      try:
111
        ctx.handler_fn = getattr(ctx.handler, method)
112
      except AttributeError:
113
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
114
                                      (method, req.request_path))
115

    
116
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
117

    
118
      # Require permissions definition (usually in the base class)
119
      if ctx.handler_access is None:
120
        raise AssertionError("Permissions definition missing")
121

    
122
      # This is only made available in HandleRequest
123
      ctx.body_data = None
124

    
125
      req.private = ctx
126

    
127
    # Check for expected attributes
128
    assert req.private.handler
129
    assert req.private.handler_fn
130
    assert req.private.handler_access is not None
131

    
132
    return req.private
133

    
134
  def AuthenticationRequired(self, req):
135
    """Determine whether authentication is required.
136

    
137
    """
138
    return bool(self._GetRequestContext(req).handler_access)
139

    
140
  def Authenticate(self, req, username, password):
141
    """Checks whether a user can access a resource.
142

    
143
    """
144
    ctx = self._GetRequestContext(req)
145

    
146
    # Check username and password
147
    valid_user = False
148
    if self._users:
149
      user = self._users.get(username, None)
150
      if user and self.VerifyBasicAuthPassword(req, username, password,
151
                                               user.password):
152
        valid_user = True
153

    
154
    if not valid_user:
155
      # Unknown user or password wrong
156
      return False
157

    
158
    if (not ctx.handler_access or
159
        set(user.options).intersection(ctx.handler_access)):
160
      # Allow access
161
      return True
162

    
163
    # Access forbidden
164
    raise http.HttpForbidden()
165

    
166
  def HandleRequest(self, req):
167
    """Handles a request.
168

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

    
172
    # Deserialize request parameters
173
    if req.request_body:
174
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
175
      # include a Content-Type header field defining the media type of that
176
      # body. [...] If the media type remains unknown, the recipient SHOULD
177
      # treat it as type "application/octet-stream".
178
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
179
                                                 http.HTTP_APP_OCTET_STREAM)
180
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
181
        raise http.HttpUnsupportedMediaType()
182

    
183
      try:
184
        ctx.body_data = serializer.LoadJson(req.request_body)
185
      except Exception:
186
        raise http.HttpBadRequest(message="Unable to parse JSON data")
187
    else:
188
      ctx.body_data = None
189

    
190
    try:
191
      result = ctx.handler_fn()
192
    except luxi.TimeoutError:
193
      raise http.HttpGatewayTimeout()
194
    except luxi.ProtocolError, err:
195
      raise http.HttpBadGateway(str(err))
196
    except:
197
      method = req.request_method.upper()
198
      logging.exception("Error while handling the %s request", method)
199
      raise
200

    
201
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
202

    
203
    return serializer.DumpJson(result)
204

    
205

    
206
def CheckRapi(options, args):
207
  """Initial checks whether to run or exit with a failure.
208

    
209
  """
210
  if args: # rapi doesn't take any arguments
211
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
212
                          sys.argv[0])
213
    sys.exit(constants.EXIT_FAILURE)
214

    
215
  ssconf.CheckMaster(options.debug)
216

    
217
  # Read SSL certificate (this is a little hackish to read the cert as root)
218
  if options.ssl:
219
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
220
                                            ssl_cert_path=options.ssl_cert)
221
  else:
222
    options.ssl_params = None
223

    
224

    
225
def ExecRapi(options, _):
226
  """Main remote API function, executed with the PID file held.
227

    
228
  """
229

    
230
  mainloop = daemon.Mainloop()
231
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
232
                               ssl_params=options.ssl_params,
233
                               ssl_verify_peer=False,
234
                               request_executor_class=JsonErrorRequestExecutor)
235
  # pylint: disable-msg=E1101
236
  # it seems pylint doesn't see the second parent class there
237
  server.Start()
238
  try:
239
    mainloop.Run()
240
  finally:
241
    server.Stop()
242

    
243

    
244
def main():
245
  """Main function.
246

    
247
  """
248
  parser = optparse.OptionParser(description="Ganeti Remote API",
249
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
250
                    version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
251

    
252
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
253
  dirs.append((constants.LOG_OS_DIR, 0750))
254
  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
255
                     default_ssl_cert=constants.RAPI_CERT_FILE,
256
                     default_ssl_key=constants.RAPI_CERT_FILE,
257
                     user=constants.RAPI_USER, group=constants.DAEMONS_GROUP)
258

    
259

    
260
if __name__ == "__main__":
261
  main()