Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 7e1fac25

History | View | Annotate | Download (6.1 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
import glob
25
import logging
26
import optparse
27
import sys
28
import os
29
import os.path
30
import signal
31

    
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import http
35
from ganeti import daemon
36
from ganeti import ssconf
37
from ganeti import utils
38
from ganeti import luxi
39
from ganeti import serializer
40
from ganeti.rapi import connector
41

    
42
import ganeti.http.auth
43
import ganeti.http.server
44

    
45

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

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

    
55

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

    
59
  """
60
  error_content_type = "application/json"
61

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

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

    
70
    """
71
    return serializer.DumpJson(values, indent=True)
72

    
73

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

    
78
  """
79
  AUTH_REALM = "Ganeti Remote API"
80

    
81
  def __init__(self, *args, **kwargs):
82
    http.server.HttpServer.__init__(self, *args, **kwargs)
83
    http.auth.HttpServerRequestAuthentication.__init__(self)
84
    self._resmap = connector.Mapper()
85

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

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

    
95
    The context is cached in the req.private variable.
96

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

    
102
      ctx = RemoteApiRequestContext()
103
      ctx.handler = HandlerClass(items, args, req)
104

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

    
112
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
113

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

    
118
      req.private = ctx
119

    
120
    return req.private
121

    
122
  def GetAuthRealm(self, req):
123
    """Override the auth realm for queries.
124

    
125
    """
126
    ctx = self._GetRequestContext(req)
127
    if ctx.handler_access:
128
      return self.AUTH_REALM
129
    else:
130
      return None
131

    
132
  def Authenticate(self, req, username, password):
133
    """Checks whether a user can access a resource.
134

    
135
    """
136
    ctx = self._GetRequestContext(req)
137

    
138
    # Check username and password
139
    valid_user = False
140
    if self._users:
141
      user = self._users.get(username, None)
142
      if user and self.VerifyBasicAuthPassword(req, username, password,
143
                                               user.password):
144
        valid_user = True
145

    
146
    if not valid_user:
147
      # Unknown user or password wrong
148
      return False
149

    
150
    if (not ctx.handler_access or
151
        set(user.options).intersection(ctx.handler_access)):
152
      # Allow access
153
      return True
154

    
155
    # Access forbidden
156
    raise http.HttpForbidden()
157

    
158
  def HandleRequest(self, req):
159
    """Handles a request.
160

    
161
    """
162
    ctx = self._GetRequestContext(req)
163

    
164
    try:
165
      result = ctx.handler_fn()
166
      sn = ctx.handler.getSerialNumber()
167
      if sn:
168
        req.response_headers[http.HTTP_ETAG] = str(sn)
169
    except luxi.TimeoutError:
170
      raise http.HttpGatewayTimeout()
171
    except luxi.ProtocolError, err:
172
      raise http.HttpBadGateway(str(err))
173
    except:
174
      method = req.request_method.upper()
175
      logging.exception("Error while handling the %s request", method)
176
      raise
177

    
178
    return result
179

    
180

    
181
def CheckRapi(options, args):
182
  """Initial checks whether to run or exit with a failure.
183

    
184
  """
185
  if len(args) != 0:
186
    print >> sys.stderr, "Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % \
187
        sys.argv[0]
188
    sys.exit(constants.EXIT_FAILURE)
189

    
190
  ssconf.CheckMaster(options.debug)
191

    
192

    
193
def ExecRapi(options, args):
194
  """Main remote API function, executed with the PID file held.
195

    
196
  """
197
  # Read SSL certificate
198
  if options.ssl:
199
    ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
200
                                    ssl_cert_path=options.ssl_cert)
201
  else:
202
    ssl_params = None
203

    
204
  mainloop = daemon.Mainloop()
205
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
206
                               ssl_params=ssl_params, ssl_verify_peer=False,
207
                               request_executor_class=JsonErrorRequestExecutor)
208
  server.Start()
209
  try:
210
    mainloop.Run()
211
  finally:
212
    server.Stop()
213

    
214

    
215
def main():
216
  """Main function.
217

    
218
  """
219
  parser = optparse.OptionParser(description="Ganeti Remote API",
220
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
221
                    version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
222

    
223
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
224
  dirs.append((constants.LOG_OS_DIR, 0750))
225
  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
226

    
227

    
228
if __name__ == "__main__":
229
  main()