Revision d9c82a4e

b/.gitignore
39 39
/daemons/ganeti-confd
40 40
/daemons/ganeti-masterd
41 41
/daemons/ganeti-noded
42
/daemons/ganeti-rapi
42 43
/daemons/ganeti-watcher
43 44

  
44 45
# devel
b/Makefile.am
199 199
	lib/server/__init__.py \
200 200
	lib/server/confd.py \
201 201
	lib/server/masterd.py \
202
	lib/server/noded.py
202
	lib/server/noded.py \
203
	lib/server/rapi.py
203 204

  
204 205
docrst = \
205 206
	doc/admin.rst \
......
287 288
	daemons/ganeti-masterd \
288 289
	daemons/ganeti-noded \
289 290
	daemons/ganeti-watcher \
291
	daemons/ganeti-rapi \
290 292
	scripts/gnt-backup \
291 293
	scripts/gnt-cluster \
292 294
	scripts/gnt-debug \
......
295 297
	scripts/gnt-node \
296 298
	scripts/gnt-os
297 299

  
298
dist_sbin_SCRIPTS = \
299
	daemons/ganeti-rapi
300
dist_sbin_SCRIPTS =
300 301

  
301 302
nodist_sbin_SCRIPTS = \
302 303
	$(PYTHON_BOOTSTRAP) \
/dev/null
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
import errno
35

  
36
try:
37
  from pyinotify import pyinotify # pylint: disable-msg=E0611
38
except ImportError:
39
  import pyinotify
40

  
41
from ganeti import asyncnotifier
42
from ganeti import constants
43
from ganeti import http
44
from ganeti import daemon
45
from ganeti import ssconf
46
from ganeti import luxi
47
from ganeti import serializer
48
from ganeti import compat
49
from ganeti import utils
50
from ganeti.rapi import connector
51

  
52
import ganeti.http.auth   # pylint: disable-msg=W0611
53
import ganeti.http.server
54

  
55

  
56
class RemoteApiRequestContext(object):
57
  """Data structure for Remote API requests.
58

  
59
  """
60
  def __init__(self):
61
    self.handler = None
62
    self.handler_fn = None
63
    self.handler_access = None
64
    self.body_data = None
65

  
66

  
67
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
68
  """Custom Request Executor class that formats HTTP errors in JSON.
69

  
70
  """
71
  error_content_type = http.HTTP_APP_JSON
72

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

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

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

  
84

  
85
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
86
                          http.server.HttpServer):
87
  """REST Request Handler Class.
88

  
89
  """
90
  AUTH_REALM = "Ganeti Remote API"
91

  
92
  def __init__(self, *args, **kwargs):
93
    # pylint: disable-msg=W0233
94
    # it seems pylint doesn't see the second parent class there
95
    http.server.HttpServer.__init__(self, *args, **kwargs)
96
    http.auth.HttpServerRequestAuthentication.__init__(self)
97
    self._resmap = connector.Mapper()
98
    self._users = None
99

  
100
  def LoadUsers(self, filename):
101
    """Loads a file containing users and passwords.
102

  
103
    @type filename: string
104
    @param filename: Path to file
105

  
106
    """
107
    logging.info("Reading users file at %s", filename)
108
    try:
109
      try:
110
        contents = utils.ReadFile(filename)
111
      except EnvironmentError, err:
112
        self._users = None
113
        if err.errno == errno.ENOENT:
114
          logging.warning("No users file at %s", filename)
115
        else:
116
          logging.warning("Error while reading %s: %s", filename, err)
117
        return False
118

  
119
      users = http.auth.ParsePasswordFile(contents)
120

  
121
    except Exception, err: # pylint: disable-msg=W0703
122
      # We don't care about the type of exception
123
      logging.error("Error while parsing %s: %s", filename, err)
124
      return False
125

  
126
    self._users = users
127

  
128
    return True
129

  
130
  def _GetRequestContext(self, req):
131
    """Returns the context for a request.
132

  
133
    The context is cached in the req.private variable.
134

  
135
    """
136
    if req.private is None:
137
      (HandlerClass, items, args) = \
138
                     self._resmap.getController(req.request_path)
139

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

  
143
      method = req.request_method.upper()
144
      try:
145
        ctx.handler_fn = getattr(ctx.handler, method)
146
      except AttributeError:
147
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
148
                                      (method, req.request_path))
149

  
150
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
151

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

  
156
      # This is only made available in HandleRequest
157
      ctx.body_data = None
158

  
159
      req.private = ctx
160

  
161
    # Check for expected attributes
162
    assert req.private.handler
163
    assert req.private.handler_fn
164
    assert req.private.handler_access is not None
165

  
166
    return req.private
167

  
168
  def AuthenticationRequired(self, req):
169
    """Determine whether authentication is required.
170

  
171
    """
172
    return bool(self._GetRequestContext(req).handler_access)
173

  
174
  def Authenticate(self, req, username, password):
175
    """Checks whether a user can access a resource.
176

  
177
    """
178
    ctx = self._GetRequestContext(req)
179

  
180
    # Check username and password
181
    valid_user = False
182
    if self._users:
183
      user = self._users.get(username, None)
184
      if user and self.VerifyBasicAuthPassword(req, username, password,
185
                                               user.password):
186
        valid_user = True
187

  
188
    if not valid_user:
189
      # Unknown user or password wrong
190
      return False
191

  
192
    if (not ctx.handler_access or
193
        set(user.options).intersection(ctx.handler_access)):
194
      # Allow access
195
      return True
196

  
197
    # Access forbidden
198
    raise http.HttpForbidden()
199

  
200
  def HandleRequest(self, req):
201
    """Handles a request.
202

  
203
    """
204
    ctx = self._GetRequestContext(req)
205

  
206
    # Deserialize request parameters
207
    if req.request_body:
208
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
209
      # include a Content-Type header field defining the media type of that
210
      # body. [...] If the media type remains unknown, the recipient SHOULD
211
      # treat it as type "application/octet-stream".
212
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
213
                                                 http.HTTP_APP_OCTET_STREAM)
214
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
215
        raise http.HttpUnsupportedMediaType()
216

  
217
      try:
218
        ctx.body_data = serializer.LoadJson(req.request_body)
219
      except Exception:
220
        raise http.HttpBadRequest(message="Unable to parse JSON data")
221
    else:
222
      ctx.body_data = None
223

  
224
    try:
225
      result = ctx.handler_fn()
226
    except luxi.TimeoutError:
227
      raise http.HttpGatewayTimeout()
228
    except luxi.ProtocolError, err:
229
      raise http.HttpBadGateway(str(err))
230
    except:
231
      method = req.request_method.upper()
232
      logging.exception("Error while handling the %s request", method)
233
      raise
234

  
235
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
236

  
237
    return serializer.DumpJson(result)
238

  
239

  
240
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
241
  def __init__(self, wm, path, cb):
242
    """Initializes this class.
243

  
244
    @param wm: Inotify watch manager
245
    @type path: string
246
    @param path: File path
247
    @type cb: callable
248
    @param cb: Function called on file change
249

  
250
    """
251
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
252

  
253
    self._cb = cb
254
    self._filename = os.path.basename(path)
255

  
256
    # Different Pyinotify versions have the flag constants at different places,
257
    # hence not accessing them directly
258
    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
259
            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
260
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
261
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
262

  
263
    self._handle = self.AddWatch(os.path.dirname(path), mask)
264

  
265
  def process_default(self, event):
266
    """Called upon inotify event.
267

  
268
    """
269
    if event.name == self._filename:
270
      logging.debug("Received inotify event %s", event)
271
      self._cb()
272

  
273

  
274
def SetupFileWatcher(filename, cb):
275
  """Configures an inotify watcher for a file.
276

  
277
  @type filename: string
278
  @param filename: File to watch
279
  @type cb: callable
280
  @param cb: Function called on file change
281

  
282
  """
283
  wm = pyinotify.WatchManager()
284
  handler = FileEventHandler(wm, filename, cb)
285
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
286

  
287

  
288
def CheckRapi(options, args):
289
  """Initial checks whether to run or exit with a failure.
290

  
291
  """
292
  if args: # rapi doesn't take any arguments
293
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
294
                          sys.argv[0])
295
    sys.exit(constants.EXIT_FAILURE)
296

  
297
  ssconf.CheckMaster(options.debug)
298

  
299
  # Read SSL certificate (this is a little hackish to read the cert as root)
300
  if options.ssl:
301
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
302
                                            ssl_cert_path=options.ssl_cert)
303
  else:
304
    options.ssl_params = None
305

  
306

  
307
def PrepRapi(options, _):
308
  """Prep remote API function, executed with the PID file held.
309

  
310
  """
311

  
312
  mainloop = daemon.Mainloop()
313
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
314
                               ssl_params=options.ssl_params,
315
                               ssl_verify_peer=False,
316
                               request_executor_class=JsonErrorRequestExecutor)
317

  
318
  # Setup file watcher (it'll be driven by asyncore)
319
  SetupFileWatcher(constants.RAPI_USERS_FILE,
320
                   compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
321

  
322
  server.LoadUsers(constants.RAPI_USERS_FILE)
323

  
324
  # pylint: disable-msg=E1101
325
  # it seems pylint doesn't see the second parent class there
326
  server.Start()
327

  
328
  return (mainloop, server)
329

  
330

  
331
def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
332
  """Main remote API function, executed with the PID file held.
333

  
334
  """
335
  (mainloop, server) = prep_data
336
  try:
337
    mainloop.Run()
338
  finally:
339
    server.Stop()
340

  
341

  
342
def main():
343
  """Main function.
344

  
345
  """
346
  parser = optparse.OptionParser(description="Ganeti Remote API",
347
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
348
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
349

  
350
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
351
                     default_ssl_cert=constants.RAPI_CERT_FILE,
352
                     default_ssl_key=constants.RAPI_CERT_FILE)
353

  
354

  
355
if __name__ == "__main__":
356
  main()
b/lib/server/rapi.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
import errno
35

  
36
try:
37
  from pyinotify import pyinotify # pylint: disable-msg=E0611
38
except ImportError:
39
  import pyinotify
40

  
41
from ganeti import asyncnotifier
42
from ganeti import constants
43
from ganeti import http
44
from ganeti import daemon
45
from ganeti import ssconf
46
from ganeti import luxi
47
from ganeti import serializer
48
from ganeti import compat
49
from ganeti import utils
50
from ganeti.rapi import connector
51

  
52
import ganeti.http.auth   # pylint: disable-msg=W0611
53
import ganeti.http.server
54

  
55

  
56
class RemoteApiRequestContext(object):
57
  """Data structure for Remote API requests.
58

  
59
  """
60
  def __init__(self):
61
    self.handler = None
62
    self.handler_fn = None
63
    self.handler_access = None
64
    self.body_data = None
65

  
66

  
67
class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
68
  """Custom Request Executor class that formats HTTP errors in JSON.
69

  
70
  """
71
  error_content_type = http.HTTP_APP_JSON
72

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

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

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

  
84

  
85
class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
86
                          http.server.HttpServer):
87
  """REST Request Handler Class.
88

  
89
  """
90
  AUTH_REALM = "Ganeti Remote API"
91

  
92
  def __init__(self, *args, **kwargs):
93
    # pylint: disable-msg=W0233
94
    # it seems pylint doesn't see the second parent class there
95
    http.server.HttpServer.__init__(self, *args, **kwargs)
96
    http.auth.HttpServerRequestAuthentication.__init__(self)
97
    self._resmap = connector.Mapper()
98
    self._users = None
99

  
100
  def LoadUsers(self, filename):
101
    """Loads a file containing users and passwords.
102

  
103
    @type filename: string
104
    @param filename: Path to file
105

  
106
    """
107
    logging.info("Reading users file at %s", filename)
108
    try:
109
      try:
110
        contents = utils.ReadFile(filename)
111
      except EnvironmentError, err:
112
        self._users = None
113
        if err.errno == errno.ENOENT:
114
          logging.warning("No users file at %s", filename)
115
        else:
116
          logging.warning("Error while reading %s: %s", filename, err)
117
        return False
118

  
119
      users = http.auth.ParsePasswordFile(contents)
120

  
121
    except Exception, err: # pylint: disable-msg=W0703
122
      # We don't care about the type of exception
123
      logging.error("Error while parsing %s: %s", filename, err)
124
      return False
125

  
126
    self._users = users
127

  
128
    return True
129

  
130
  def _GetRequestContext(self, req):
131
    """Returns the context for a request.
132

  
133
    The context is cached in the req.private variable.
134

  
135
    """
136
    if req.private is None:
137
      (HandlerClass, items, args) = \
138
                     self._resmap.getController(req.request_path)
139

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

  
143
      method = req.request_method.upper()
144
      try:
145
        ctx.handler_fn = getattr(ctx.handler, method)
146
      except AttributeError:
147
        raise http.HttpNotImplemented("Method %s is unsupported for path %s" %
148
                                      (method, req.request_path))
149

  
150
      ctx.handler_access = getattr(ctx.handler, "%s_ACCESS" % method, None)
151

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

  
156
      # This is only made available in HandleRequest
157
      ctx.body_data = None
158

  
159
      req.private = ctx
160

  
161
    # Check for expected attributes
162
    assert req.private.handler
163
    assert req.private.handler_fn
164
    assert req.private.handler_access is not None
165

  
166
    return req.private
167

  
168
  def AuthenticationRequired(self, req):
169
    """Determine whether authentication is required.
170

  
171
    """
172
    return bool(self._GetRequestContext(req).handler_access)
173

  
174
  def Authenticate(self, req, username, password):
175
    """Checks whether a user can access a resource.
176

  
177
    """
178
    ctx = self._GetRequestContext(req)
179

  
180
    # Check username and password
181
    valid_user = False
182
    if self._users:
183
      user = self._users.get(username, None)
184
      if user and self.VerifyBasicAuthPassword(req, username, password,
185
                                               user.password):
186
        valid_user = True
187

  
188
    if not valid_user:
189
      # Unknown user or password wrong
190
      return False
191

  
192
    if (not ctx.handler_access or
193
        set(user.options).intersection(ctx.handler_access)):
194
      # Allow access
195
      return True
196

  
197
    # Access forbidden
198
    raise http.HttpForbidden()
199

  
200
  def HandleRequest(self, req):
201
    """Handles a request.
202

  
203
    """
204
    ctx = self._GetRequestContext(req)
205

  
206
    # Deserialize request parameters
207
    if req.request_body:
208
      # RFC2616, 7.2.1: Any HTTP/1.1 message containing an entity-body SHOULD
209
      # include a Content-Type header field defining the media type of that
210
      # body. [...] If the media type remains unknown, the recipient SHOULD
211
      # treat it as type "application/octet-stream".
212
      req_content_type = req.request_headers.get(http.HTTP_CONTENT_TYPE,
213
                                                 http.HTTP_APP_OCTET_STREAM)
214
      if req_content_type.lower() != http.HTTP_APP_JSON.lower():
215
        raise http.HttpUnsupportedMediaType()
216

  
217
      try:
218
        ctx.body_data = serializer.LoadJson(req.request_body)
219
      except Exception:
220
        raise http.HttpBadRequest(message="Unable to parse JSON data")
221
    else:
222
      ctx.body_data = None
223

  
224
    try:
225
      result = ctx.handler_fn()
226
    except luxi.TimeoutError:
227
      raise http.HttpGatewayTimeout()
228
    except luxi.ProtocolError, err:
229
      raise http.HttpBadGateway(str(err))
230
    except:
231
      method = req.request_method.upper()
232
      logging.exception("Error while handling the %s request", method)
233
      raise
234

  
235
    req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
236

  
237
    return serializer.DumpJson(result)
238

  
239

  
240
class FileEventHandler(asyncnotifier.FileEventHandlerBase):
241
  def __init__(self, wm, path, cb):
242
    """Initializes this class.
243

  
244
    @param wm: Inotify watch manager
245
    @type path: string
246
    @param path: File path
247
    @type cb: callable
248
    @param cb: Function called on file change
249

  
250
    """
251
    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
252

  
253
    self._cb = cb
254
    self._filename = os.path.basename(path)
255

  
256
    # Different Pyinotify versions have the flag constants at different places,
257
    # hence not accessing them directly
258
    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
259
            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
260
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
261
            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
262

  
263
    self._handle = self.AddWatch(os.path.dirname(path), mask)
264

  
265
  def process_default(self, event):
266
    """Called upon inotify event.
267

  
268
    """
269
    if event.name == self._filename:
270
      logging.debug("Received inotify event %s", event)
271
      self._cb()
272

  
273

  
274
def SetupFileWatcher(filename, cb):
275
  """Configures an inotify watcher for a file.
276

  
277
  @type filename: string
278
  @param filename: File to watch
279
  @type cb: callable
280
  @param cb: Function called on file change
281

  
282
  """
283
  wm = pyinotify.WatchManager()
284
  handler = FileEventHandler(wm, filename, cb)
285
  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
286

  
287

  
288
def CheckRapi(options, args):
289
  """Initial checks whether to run or exit with a failure.
290

  
291
  """
292
  if args: # rapi doesn't take any arguments
293
    print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
294
                          sys.argv[0])
295
    sys.exit(constants.EXIT_FAILURE)
296

  
297
  ssconf.CheckMaster(options.debug)
298

  
299
  # Read SSL certificate (this is a little hackish to read the cert as root)
300
  if options.ssl:
301
    options.ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
302
                                            ssl_cert_path=options.ssl_cert)
303
  else:
304
    options.ssl_params = None
305

  
306

  
307
def PrepRapi(options, _):
308
  """Prep remote API function, executed with the PID file held.
309

  
310
  """
311

  
312
  mainloop = daemon.Mainloop()
313
  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
314
                               ssl_params=options.ssl_params,
315
                               ssl_verify_peer=False,
316
                               request_executor_class=JsonErrorRequestExecutor)
317

  
318
  # Setup file watcher (it'll be driven by asyncore)
319
  SetupFileWatcher(constants.RAPI_USERS_FILE,
320
                   compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
321

  
322
  server.LoadUsers(constants.RAPI_USERS_FILE)
323

  
324
  # pylint: disable-msg=E1101
325
  # it seems pylint doesn't see the second parent class there
326
  server.Start()
327

  
328
  return (mainloop, server)
329

  
330

  
331
def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
332
  """Main remote API function, executed with the PID file held.
333

  
334
  """
335
  (mainloop, server) = prep_data
336
  try:
337
    mainloop.Run()
338
  finally:
339
    server.Stop()
340

  
341

  
342
def Main():
343
  """Main function.
344

  
345
  """
346
  parser = optparse.OptionParser(description="Ganeti Remote API",
347
                    usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
348
                    version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
349

  
350
  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
351
                     default_ssl_cert=constants.RAPI_CERT_FILE,
352
                     default_ssl_key=constants.RAPI_CERT_FILE)

Also available in: Unified diff