Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ e18def2a

History | View | Annotate | Download (7.8 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 user.password == password:
143
        valid_user = True
144

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

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

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

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

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

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

    
177
    return result
178

    
179

    
180
def ParseOptions():
181
  """Parse the command line options.
182

    
183
  @return: (options, args) as from OptionParser.parse_args()
184

    
185
  """
186
  parser = optparse.OptionParser(description="Ganeti Remote API",
187
                    usage="%prog [-d] [-p port]",
188
                    version="%%prog (ganeti) %s" %
189
                                 constants.RAPI_VERSION)
190
  parser.add_option("-d", "--debug", dest="debug",
191
                    help="Enable some debug messages",
192
                    default=False, action="store_true")
193
  parser.add_option("-p", "--port", dest="port",
194
                    help="Port to run API (%s default)." %
195
                                 constants.RAPI_PORT,
196
                    default=constants.RAPI_PORT, type="int")
197
  parser.add_option("--no-ssl", dest="ssl",
198
                    help="Do not secure HTTP protocol with SSL",
199
                    default=True, action="store_false")
200
  parser.add_option("-K", "--ssl-key", dest="ssl_key",
201
                    help="SSL key",
202
                    default=constants.RAPI_CERT_FILE, type="string")
203
  parser.add_option("-C", "--ssl-cert", dest="ssl_cert",
204
                    help="SSL certificate",
205
                    default=constants.RAPI_CERT_FILE, type="string")
206
  parser.add_option("-f", "--foreground", dest="fork",
207
                    help="Don't detach from the current terminal",
208
                    default=True, action="store_false")
209
  parser.add_option("-b", "--bind", dest="bind_address",
210
                     help="Bind address",
211
                     default="", metavar="ADDRESS")
212

    
213
  options, args = parser.parse_args()
214

    
215
  if len(args) != 0:
216
    print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
217
    sys.exit(constants.EXIT_FAILURE)
218

    
219
  if options.ssl and not (options.ssl_cert and options.ssl_key):
220
    print >> sys.stderr, ("For secure mode please provide "
221
                          "--ssl-key and --ssl-cert arguments")
222
    sys.exit(constants.EXIT_FAILURE)
223

    
224
  return options, args
225

    
226

    
227
def main():
228
  """Main function.
229

    
230
  """
231
  options, args = ParseOptions()
232

    
233
  if options.fork:
234
    utils.CloseFDs()
235

    
236
  if options.ssl:
237
    # Read SSL certificate
238
    try:
239
      ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
240
                                      ssl_cert_path=options.ssl_cert)
241
    except Exception, err:
242
      sys.stderr.write("Can't load the SSL certificate/key: %s\n" % (err,))
243
      sys.exit(constants.EXIT_FAILURE)
244
  else:
245
    ssl_params = None
246

    
247
  ssconf.CheckMaster(options.debug)
248

    
249
  if options.fork:
250
    utils.Daemonize(logfile=constants.LOG_RAPISERVER)
251

    
252
  utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
253
                     stderr_logging=not options.fork)
254

    
255
  utils.WritePidFile(constants.RAPI_PID)
256
  try:
257
    mainloop = daemon.Mainloop()
258
    server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
259
                                 ssl_params=ssl_params, ssl_verify_peer=False,
260
                                 request_executor_class=
261
                                 JsonErrorRequestExecutor)
262
    server.Start()
263
    try:
264
      mainloop.Run()
265
    finally:
266
      server.Stop()
267
  finally:
268
    utils.RemovePidFile(constants.RAPI_PID)
269

    
270

    
271
if __name__ == '__main__':
272
  main()