Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ ab221ddf

History | View | Annotate | Download (7.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
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.HttpJsonConverter.CONTENT_TYPE
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
    http.server.HttpServer.__init__(self, *args, **kwargs)
85
    http.auth.HttpServerRequestAuthentication.__init__(self)
86
    self._resmap = connector.Mapper()
87

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

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

    
97
    The context is cached in the req.private variable.
98

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

    
104
      ctx = RemoteApiRequestContext()
105
      ctx.handler = HandlerClass(items, args, req)
106

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

    
114
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
115

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

    
120
      # This is only made available in HandleRequest
121
      ctx.body_data = None
122

    
123
      req.private = ctx
124

    
125
    return req.private
126

    
127
  def GetAuthRealm(self, req):
128
    """Override the auth realm for queries.
129

    
130
    """
131
    ctx = self._GetRequestContext(req)
132
    if ctx.handler_access:
133
      return self.AUTH_REALM
134
    else:
135
      return None
136

    
137
  def Authenticate(self, req, username, password):
138
    """Checks whether a user can access a resource.
139

    
140
    """
141
    ctx = self._GetRequestContext(req)
142

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

    
151
    if not valid_user:
152
      # Unknown user or password wrong
153
      return False
154

    
155
    if (not ctx.handler_access or
156
        set(user.options).intersection(ctx.handler_access)):
157
      # Allow access
158
      return True
159

    
160
    # Access forbidden
161
    raise http.HttpForbidden()
162

    
163
  def HandleRequest(self, req):
164
    """Handles a request.
165

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

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

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

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

    
199
    req.resp_headers[http.HTTP_CONTENT_TYPE] = \
200
      http.HttpJsonConverter.CONTENT_TYPE
201

    
202
    return serializer.DumpJson(result)
203

    
204

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

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

    
214
  ssconf.CheckMaster(options.debug)
215

    
216

    
217
def ExecRapi(options, _):
218
  """Main remote API function, executed with the PID file held.
219

    
220
  """
221
  # Read SSL certificate
222
  if options.ssl:
223
    ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
224
                                    ssl_cert_path=options.ssl_cert)
225
  else:
226
    ssl_params = None
227

    
228
  mainloop = daemon.Mainloop()
229
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
230
                               ssl_params=ssl_params, ssl_verify_peer=False,
231
                               request_executor_class=JsonErrorRequestExecutor)
232
  server.Start()
233
  try:
234
    mainloop.Run()
235
  finally:
236
    server.Stop()
237

    
238

    
239
def main():
240
  """Main function.
241

    
242
  """
243
  parser = optparse.OptionParser(description="Ganeti Remote API",
244
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
245
                    version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
246

    
247
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
248
  dirs.append((constants.LOG_OS_DIR, 0750))
249
  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
250
                     default_ssl_cert=constants.RAPI_CERT_FILE,
251
                     default_ssl_key=constants.RAPI_CERT_FILE)
252

    
253

    
254
if __name__ == "__main__":
255
  main()