Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 23ccba04

History | View | Annotate | Download (6.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

    
56

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

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

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

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

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

    
74

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

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

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

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

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

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

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

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

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

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

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

    
119
      req.private = ctx
120

    
121
    # Check for expected attributes
122
    assert req.private.handler
123
    assert req.private.handler_fn
124
    assert req.private.handler_access is not None
125

    
126
    return req.private
127

    
128
  def AuthenticationRequired(self, req):
129
    """Determine whether authentication is required.
130

    
131
    """
132
    return bool(self._GetRequestContext(req).handler_access)
133

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

    
137
    """
138
    ctx = self._GetRequestContext(req)
139

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

    
148
    if not valid_user:
149
      # Unknown user or password wrong
150
      return False
151

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

    
157
    # Access forbidden
158
    raise http.HttpForbidden()
159

    
160
  def HandleRequest(self, req):
161
    """Handles a request.
162

    
163
    """
164
    ctx = self._GetRequestContext(req)
165

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

    
180
    return result
181

    
182

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

    
186
  """
187
  if args: # rapi doesn't take any arguments
188
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
189
                          sys.argv[0])
190
    sys.exit(constants.EXIT_FAILURE)
191

    
192
  ssconf.CheckMaster(options.debug)
193

    
194

    
195
def ExecRapi(options, _):
196
  """Main remote API function, executed with the PID file held.
197

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

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

    
216

    
217
def main():
218
  """Main function.
219

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

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

    
229

    
230
if __name__ == "__main__":
231
  main()