Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 71d23b33

History | View | Annotate | Download (6.5 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
    # pylint: disable-msg=W0233
84
  # it seems pylint doesn't see the second parent class there
85
    http.server.HttpServer.__init__(self, *args, **kwargs)
86
    http.auth.HttpServerRequestAuthentication.__init__(self)
87
    self._resmap = connector.Mapper()
88

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

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

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

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

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

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

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

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

    
121
      req.private = ctx
122

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

    
128
    return req.private
129

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

    
133
    """
134
    return bool(self._GetRequestContext(req).handler_access)
135

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

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

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

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

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

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

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

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

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

    
182
    return result
183

    
184

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

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

    
194
  ssconf.CheckMaster(options.debug)
195

    
196

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

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

    
208
  mainloop = daemon.Mainloop()
209
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
210
                               ssl_params=ssl_params, ssl_verify_peer=False,
211
                               request_executor_class=JsonErrorRequestExecutor)
212
  # pylint: disable-msg=E1101
213
  # it seems pylint doesn't see the second parent class there
214
  server.Start()
215
  try:
216
    mainloop.Run()
217
  finally:
218
    server.Stop()
219

    
220

    
221
def main():
222
  """Main function.
223

    
224
  """
225
  parser = optparse.OptionParser(description="Ganeti Remote API",
226
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
227
                    version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
228

    
229
  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
230
  dirs.append((constants.LOG_OS_DIR, 0750))
231
  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
232

    
233

    
234
if __name__ == "__main__":
235
  main()