Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 2d54e29c

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 glob
30
import logging
31
import optparse
32
import sys
33
import os
34
import os.path
35
import signal
36

    
37
from ganeti import constants
38
from ganeti import errors
39
from ganeti import http
40
from ganeti import daemon
41
from ganeti import ssconf
42
from ganeti import utils
43
from ganeti import luxi
44
from ganeti import serializer
45
from ganeti.rapi import connector
46

    
47
import ganeti.http.auth
48
import ganeti.http.server
49

    
50

    
51
class RemoteApiRequestContext(object):
52
  """Data structure for Remote API requests.
53

    
54
  """
55
  def __init__(self):
56
    self.handler = None
57
    self.handler_fn = None
58
    self.handler_access = None
59

    
60

    
61
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
62
  """Custom Request Executor class that formats HTTP errors in JSON.
63

    
64
  """
65
  error_content_type = "application/json"
66

    
67
  def _FormatErrorMessage(self, values):
68
    """Formats the body of an error message.
69

    
70
    @type values: dict
71
    @param values: dictionary with keys code, message and explain.
72
    @rtype: string
73
    @return: the body of the message
74

    
75
    """
76
    return serializer.DumpJson(values, indent=True)
77

    
78

    
79
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
80
                          http.server.HttpServer):
81
  """REST Request Handler Class.
82

    
83
  """
84
  AUTH_REALM = "Ganeti Remote API"
85

    
86
  def __init__(self, *args, **kwargs):
87
    http.server.HttpServer.__init__(self, *args, **kwargs)
88
    http.auth.HttpServerRequestAuthentication.__init__(self)
89
    self._resmap = connector.Mapper()
90

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

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

    
100
    The context is cached in the req.private variable.
101

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

    
107
      ctx = RemoteApiRequestContext()
108
      ctx.handler = HandlerClass(items, args, req)
109

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

    
117
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
118

    
119
      # Require permissions definition (usually in the base class)
120
      if ctx.handler_access is None:
121
        raise AssertionError("Permissions definition missing")
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
    try:
170
      result = ctx.handler_fn()
171
      sn = ctx.handler.getSerialNumber()
172
      if sn:
173
        req.response_headers[http.HTTP_ETAG] = str(sn)
174
    except luxi.TimeoutError:
175
      raise http.HttpGatewayTimeout()
176
    except luxi.ProtocolError, err:
177
      raise http.HttpBadGateway(str(err))
178
    except:
179
      method = req.request_method.upper()
180
      logging.exception("Error while handling the %s request", method)
181
      raise
182

    
183
    return result
184

    
185

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

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

    
195
  ssconf.CheckMaster(options.debug)
196

    
197

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

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

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

    
219

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

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

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

    
232

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