ganeti-rapi 6.22 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
#

# Copyright (C) 2006, 2007 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

Iustin Pop's avatar
Iustin Pop committed
21
22
"""Ganeti Remote API master script.

23
24
"""

Iustin Pop's avatar
Iustin Pop committed
25
26
27
28
# pylint: disable-msg=C0103,W0142

# C0103: Invalid name ganeti-watcher

29
import glob
30
import logging
31
32
33
import optparse
import sys
import os
34
import os.path
35
import signal
36
37

from ganeti import constants
38
39
from ganeti import errors
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
46
from ganeti.rapi import connector

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

50

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

  """
  def __init__(self):
    self.handler = None
    self.handler_fn = None
58
    self.handler_access = None
59
60


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
  """Custom Request Executor class that formats HTTP errors in JSON.

  """
  error_content_type = "application/json"

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

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

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


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

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

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

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

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

    The context is cached in the req.private variable.

    """
    if req.private is None:
104
105
      (HandlerClass, items, args) = \
                     self._resmap.getController(req.request_path)
106
107
108
109
110
111
112
113

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

      method = req.request_method.upper()
      try:
        ctx.handler_fn = getattr(ctx.handler, method)
      except AttributeError, err:
Iustin Pop's avatar
Iustin Pop committed
114
115
        raise http.HttpBadRequest("Method %s is unsupported for path %s" %
                                  (method, req.request_path))
116

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

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

123
124
125
126
      req.private = ctx

    return req.private

127
128
129
130
131
132
133
134
135
136
  def GetAuthRealm(self, req):
    """Override the auth realm for queries.

    """
    ctx = self._GetRequestContext(req)
    if ctx.handler_access:
      return self.AUTH_REALM
    else:
      return None

137
138
139
140
141
142
143
144
145
146
  def Authenticate(self, req, username, password):
    """Checks whether a user can access a resource.

    """
    ctx = self._GetRequestContext(req)

    # Check username and password
    valid_user = False
    if self._users:
      user = self._users.get(username, None)
147
148
      if user and self.VerifyBasicAuthPassword(req, username, password,
                                               user.password):
149
150
151
152
153
154
155
156
157
158
159
160
161
162
        valid_user = True

    if not valid_user:
      # Unknown user or password wrong
      return False

    if (not ctx.handler_access or
        set(user.options).intersection(ctx.handler_access)):
      # Allow access
      return True

    # Access forbidden
    raise http.HttpForbidden()

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

    """
167
    ctx = self._GetRequestContext(req)
168
169

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

    return result
184
185


Michael Hanselmann's avatar
Michael Hanselmann committed
186
187
def CheckRapi(options, args):
  """Initial checks whether to run or exit with a failure.
188
189
190

  """
  if len(args) != 0:
191
192
    print >> sys.stderr, "Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" % \
        sys.argv[0]
193
    sys.exit(constants.EXIT_FAILURE)
194

195
  ssconf.CheckMaster(options.debug)
196
197


Michael Hanselmann's avatar
Michael Hanselmann committed
198
199
def ExecRapi(options, args):
  """Main remote API function, executed with the PID file held.
200
201

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

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

219

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

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

  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
  dirs.append((constants.LOG_OS_DIR, 0750))
Michael Hanselmann's avatar
Michael Hanselmann committed
230
  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
231
232


Michael Hanselmann's avatar
Michael Hanselmann committed
233
if __name__ == "__main__":
234
  main()