Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 16b037a9

History | View | Annotate | Download (7.2 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
    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() != http.HTTP_APP_JSON.lower():
178
        raise http.HttpUnsupportedMediaType()
179

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

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

    
198
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
199

    
200
    return serializer.DumpJson(result)
201

    
202

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

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

    
212
  ssconf.CheckMaster(options.debug)
213

    
214

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

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

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

    
236

    
237
def main():
238
  """Main function.
239

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

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

    
251

    
252
if __name__ == "__main__":
253
  main()