Statistics
| Branch: | Tag: | Revision:

root / daemons / ganeti-rapi @ 1f8588f6

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

    
111
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
112

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

    
117
      req.private = ctx
118

    
119
    return req.private
120

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

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

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

    
134
    """
135
    ctx = self._GetRequestContext(req)
136

    
137
    # Check username and password
138
    valid_user = False
139
    if self._users:
140
      user = self._users.get(username, None)
141
      if user and user.password == password:
142
        valid_user = True
143

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

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

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

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

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

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

    
176
    return result
177

    
178

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

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

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

    
209
  options, args = parser.parse_args()
210

    
211
  if len(args) != 0:
212
    print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0]
213
    sys.exit(1)
214

    
215
  if options.ssl and not (options.ssl_cert and options.ssl_key):
216
    print >> sys.stderr, ("For secure mode please provide "
217
                         "--ssl-key and --ssl-cert arguments")
218
    sys.exit(1)
219

    
220
  return options, args
221

    
222

    
223
def main():
224
  """Main function.
225

    
226
  """
227
  options, args = ParseOptions()
228

    
229
  if options.fork:
230
    utils.CloseFDs()
231

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

    
243
  ssconf.CheckMaster(options.debug)
244

    
245
  if options.fork:
246
    utils.Daemonize(logfile=constants.LOG_RAPISERVER)
247

    
248
  utils.SetupLogging(constants.LOG_RAPISERVER, debug=options.debug,
249
                     stderr_logging=not options.fork)
250

    
251
  utils.WritePidFile(constants.RAPI_PID)
252
  try:
253
    mainloop = daemon.Mainloop()
254
    server = RemoteApiHttpServer(mainloop, "", options.port,
255
                                 ssl_params=ssl_params, ssl_verify_peer=False,
256
                                 request_executor_class=
257
                                 JsonErrorRequestExecutor)
258
    server.Start()
259
    try:
260
      mainloop.Run()
261
    finally:
262
      server.Stop()
263
  finally:
264
    utils.RemovePidFile(constants.RAPI_PID)
265

    
266

    
267
if __name__ == '__main__':
268
  main()