Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 7369e826

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

    
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
    return req.private
122

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

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

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

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

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

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

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

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

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

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

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

    
179
    return result
180

    
181

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

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

    
191
  ssconf.CheckMaster(options.debug)
192

    
193

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

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

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

    
215

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

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

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

    
228

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