KVM: only export instance tags if present
[ganeti-local] / daemons / ganeti-rapi
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     # pylint: disable-msg=W0233
84   # it seems pylint doesn't see the second parent class there
85     http.server.HttpServer.__init__(self, *args, **kwargs)
86     http.auth.HttpServerRequestAuthentication.__init__(self)
87     self._resmap = connector.Mapper()
88
89     # Load password file
90     if os.path.isfile(constants.RAPI_USERS_FILE):
91       self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
92     else:
93       self._users = None
94
95   def _GetRequestContext(self, req):
96     """Returns the context for a request.
97
98     The context is cached in the req.private variable.
99
100     """
101     if req.private is None:
102       (HandlerClass, items, args) = \
103                      self._resmap.getController(req.request_path)
104
105       ctx = RemoteApiRequestContext()
106       ctx.handler = HandlerClass(items, args, req)
107
108       method = req.request_method.upper()
109       try:
110         ctx.handler_fn = getattr(ctx.handler, method)
111       except AttributeError:
112         raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
113                                       (method, req.request_path))
114
115       ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
116
117       # Require permissions definition (usually in the base class)
118       if ctx.handler_access is None:
119         raise AssertionError("Permissions definition missing")
120
121       req.private = ctx
122
123     # Check for expected attributes
124     assert req.private.handler
125     assert req.private.handler_fn
126     assert req.private.handler_access is not None
127
128     return req.private
129
130   def AuthenticationRequired(self, req):
131     """Determine whether authentication is required.
132
133     """
134     return bool(self._GetRequestContext(req).handler_access)
135
136   def Authenticate(self, req, username, password):
137     """Checks whether a user can access a resource.
138
139     """
140     ctx = self._GetRequestContext(req)
141
142     # Check username and password
143     valid_user = False
144     if self._users:
145       user = self._users.get(username, None)
146       if user and self.VerifyBasicAuthPassword(req, username, password,
147                                                user.password):
148         valid_user = True
149
150     if not valid_user:
151       # Unknown user or password wrong
152       return False
153
154     if (not ctx.handler_access or
155         set(user.options).intersection(ctx.handler_access)):
156       # Allow access
157       return True
158
159     # Access forbidden
160     raise http.HttpForbidden()
161
162   def HandleRequest(self, req):
163     """Handles a request.
164
165     """
166     ctx = self._GetRequestContext(req)
167
168     try:
169       result = ctx.handler_fn()
170       sn = ctx.handler.getSerialNumber()
171       if sn:
172         req.response_headers[http.HTTP_ETAG] = str(sn)
173     except luxi.TimeoutError:
174       raise http.HttpGatewayTimeout()
175     except luxi.ProtocolError, err:
176       raise http.HttpBadGateway(str(err))
177     except:
178       method = req.request_method.upper()
179       logging.exception("Error while handling the %s request", method)
180       raise
181
182     return result
183
184
185 def CheckRapi(options, args):
186   """Initial checks whether to run or exit with a failure.
187
188   """
189   if args: # rapi doesn't take any arguments
190     print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
191                           sys.argv[0])
192     sys.exit(constants.EXIT_FAILURE)
193
194   ssconf.CheckMaster(options.debug)
195
196
197 def ExecRapi(options, _):
198   """Main remote API function, executed with the PID file held.
199
200   """
201   # Read SSL certificate
202   if options.ssl:
203     ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
204                                     ssl_cert_path=options.ssl_cert)
205   else:
206     ssl_params = None
207
208   mainloop = daemon.Mainloop()
209   server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
210                                ssl_params=ssl_params, ssl_verify_peer=False,
211                                request_executor_class=JsonErrorRequestExecutor)
212   # pylint: disable-msg=E1101
213   # it seems pylint doesn't see the second parent class there
214   server.Start()
215   try:
216     mainloop.Run()
217   finally:
218     server.Stop()
219
220
221 def main():
222   """Main function.
223
224   """
225   parser = optparse.OptionParser(description="Ganeti Remote API",
226                     usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
227                     version="%%prog (ganeti) %s" % constants.RAPI_VERSION)
228
229   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
230   dirs.append((constants.LOG_OS_DIR, 0750))
231   daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi)
232
233
234 if __name__ == "__main__":
235   main()