ganeti-noded 15.8 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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.


"""Ganeti node daemon"""

24
25
26
# functions in this module need to have a given name structure, so:
# pylint: disable-msg=C0103

Iustin Pop's avatar
Iustin Pop committed
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import os
import sys
import resource
import traceback

from optparse import OptionParser


from ganeti import backend
from ganeti import logger
from ganeti import constants
from ganeti import objects
from ganeti import errors
from ganeti import ssconf
41
from ganeti import utils
Iustin Pop's avatar
Iustin Pop committed
42
43
44
45
46
47
48
49

from twisted.spread import pb
from twisted.internet import reactor
from twisted.cred import checkers, portal
from OpenSSL import SSL


class ServerContextFactory:
50
51
52
53
54
55
56
57
58
59
  """SSL context factory class that uses a given certificate.

  """
  @staticmethod
  def getContext():
    """Return a customized context.

    The context will be set to use our certificate.

    """
Iustin Pop's avatar
Iustin Pop committed
60
61
62
63
64
65
    ctx = SSL.Context(SSL.TLSv1_METHOD)
    ctx.use_certificate_file(constants.SSL_CERT_FILE)
    ctx.use_privatekey_file(constants.SSL_CERT_FILE)
    return ctx

class ServerObject(pb.Avatar):
66
67
68
69
70
  """The server implementation.

  This class holds all methods exposed over the RPC interface.

  """
Iustin Pop's avatar
Iustin Pop committed
71
72
73
74
  def __init__(self, name):
    self.name = name

  def perspectiveMessageReceived(self, broker, message, args, kw):
75
    """Custom message dispatching function.
Iustin Pop's avatar
Iustin Pop committed
76

77
78
    This function overrides the pb.Avatar function in order to provide
    a simple form of exception passing (as text only).
Iustin Pop's avatar
Iustin Pop committed
79

80
    """
Iustin Pop's avatar
Iustin Pop committed
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    args = broker.unserialize(args, self)
    kw = broker.unserialize(kw, self)
    method = getattr(self, "perspective_%s" % message)
    tb = None
    state = None
    try:
      state = method(*args, **kw)
    except:
      tb = traceback.format_exc()

    return broker.serialize((tb, state), self, method, args, kw)

  # the new block devices  --------------------------

95
96
97
98
99
  @staticmethod
  def perspective_blockdev_create(params):
    """Create a block device.

    """
100
    bdev_s, size, on_primary, info = params
101
    bdev = objects.Disk.FromDict(bdev_s)
Iustin Pop's avatar
Iustin Pop committed
102
103
    if bdev is None:
      raise ValueError("can't unserialize data!")
104
    return backend.CreateBlockDevice(bdev, size, on_primary, info)
Iustin Pop's avatar
Iustin Pop committed
105

106
107
108
109
110
  @staticmethod
  def perspective_blockdev_remove(params):
    """Remove a block device.

    """
Iustin Pop's avatar
Iustin Pop committed
111
    bdev_s = params[0]
112
    bdev = objects.Disk.FromDict(bdev_s)
Iustin Pop's avatar
Iustin Pop committed
113
114
    return backend.RemoveBlockDevice(bdev)

115
116
117
118
119
  @staticmethod
  def perspective_blockdev_assemble(params):
    """Assemble a block device.

    """
Iustin Pop's avatar
Iustin Pop committed
120
    bdev_s, on_primary = params
121
    bdev = objects.Disk.FromDict(bdev_s)
Iustin Pop's avatar
Iustin Pop committed
122
123
124
125
    if bdev is None:
      raise ValueError("can't unserialize data!")
    return backend.AssembleBlockDevice(bdev, on_primary)

126
127
128
129
130
  @staticmethod
  def perspective_blockdev_shutdown(params):
    """Shutdown a block device.

    """
Iustin Pop's avatar
Iustin Pop committed
131
    bdev_s = params[0]
132
    bdev = objects.Disk.FromDict(bdev_s)
Iustin Pop's avatar
Iustin Pop committed
133
134
135
136
    if bdev is None:
      raise ValueError("can't unserialize data!")
    return backend.ShutdownBlockDevice(bdev)

137
138
139
140
141
142
143
144
  @staticmethod
  def perspective_blockdev_addchild(params):
    """Add a child to a mirror device.

    Note: this is only valid for mirror devices. It's the caller's duty
    to send a correct disk, otherwise we raise an error.

    """
Iustin Pop's avatar
Iustin Pop committed
145
    bdev_s, ndev_s = params
146
147
    bdev = objects.Disk.FromDict(bdev_s)
    ndev = objects.Disk.FromDict(ndev_s)
Iustin Pop's avatar
Iustin Pop committed
148
149
150
151
    if bdev is None or ndev is None:
      raise ValueError("can't unserialize data!")
    return backend.MirrorAddChild(bdev, ndev)

152
153
154
155
156
157
158
159
  @staticmethod
  def perspective_blockdev_removechild(params):
    """Remove a child from a mirror device.

    This is only valid for mirror devices, of course. It's the callers
    duty to send a correct disk, otherwise we raise an error.

    """
Iustin Pop's avatar
Iustin Pop committed
160
    bdev_s, ndev_s = params
161
162
    bdev = objects.Disk.FromDict(bdev_s)
    ndev = objects.Disk.FromDict(ndev_s)
Iustin Pop's avatar
Iustin Pop committed
163
164
165
166
    if bdev is None or ndev is None:
      raise ValueError("can't unserialize data!")
    return backend.MirrorRemoveChild(bdev, ndev)

167
168
169
170
171
  @staticmethod
  def perspective_blockdev_getmirrorstatus(params):
    """Return the mirror status for a list of disks.

    """
172
    disks = [objects.Disk.FromDict(dsk_s)
Iustin Pop's avatar
Iustin Pop committed
173
174
175
            for dsk_s in params]
    return backend.GetMirrorStatus(disks)

176
177
178
179
180
181
182
  @staticmethod
  def perspective_blockdev_find(params):
    """Expose the FindBlockDevice functionality for a disk.

    This will try to find but not activate a disk.

    """
183
    disk = objects.Disk.FromDict(params[0])
Iustin Pop's avatar
Iustin Pop committed
184
185
    return backend.FindBlockDevice(disk)

186
187
188
189
190
191
192
193
194
  @staticmethod
  def perspective_blockdev_snapshot(params):
    """Create a snapshot device.

    Note that this is only valid for LVM disks, if we get passed
    something else we raise an exception. The snapshot device can be
    remove by calling the generic block device remove call.

    """
195
    cfbd = objects.Disk.FromDict(params[0])
Iustin Pop's avatar
Iustin Pop committed
196
197
198
199
    return backend.SnapshotBlockDevice(cfbd)

  # export/import  --------------------------

200
201
202
203
204
  @staticmethod
  def perspective_snapshot_export(params):
    """Export a given snapshot.

    """
205
    disk = objects.Disk.FromDict(params[0])
Iustin Pop's avatar
Iustin Pop committed
206
    dest_node = params[1]
207
    instance = objects.Instance.FromDict(params[2])
208
209
210
211
212
    return backend.ExportSnapshot(disk, dest_node, instance)

  @staticmethod
  def perspective_finalize_export(params):
    """Expose the finalize export functionality.
Iustin Pop's avatar
Iustin Pop committed
213

214
    """
215
216
    instance = objects.Instance.FromDict(params[0])
    snap_disks = [objects.Disk.FromDict(str_data)
Iustin Pop's avatar
Iustin Pop committed
217
218
219
                  for str_data in params[1]]
    return backend.FinalizeExport(instance, snap_disks)

220
221
222
223
224
225
226
227
228
229
  @staticmethod
  def perspective_export_info(params):
    """Query information about an existing export on this node.

    The given path may not contain an export, in which case we return
    None.

    """
    path = params[0]
    einfo = backend.ExportInfo(path)
Iustin Pop's avatar
Iustin Pop committed
230
231
232
233
    if einfo is None:
      return einfo
    return einfo.Dumps()

234
235
236
237
238
239
240
241
242
  @staticmethod
  def perspective_export_list(params):
    """List the available exports on this node.

    Note that as opposed to export_info, which may query data about an
    export in any path, this only queries the standard Ganeti path
    (constants.EXPORT_DIR).

    """
Iustin Pop's avatar
Iustin Pop committed
243
244
    return backend.ListExports()

245
246
247
248
249
  @staticmethod
  def perspective_export_remove(params):
    """Remove an export.

    """
Iustin Pop's avatar
Iustin Pop committed
250
251
252
253
254
    export = params[0]
    return backend.RemoveExport(export)

  # volume  --------------------------

255
256
257
258
259
  @staticmethod
  def perspective_volume_list(params):
    """Query the list of logical volumes in a given volume group.

    """
Iustin Pop's avatar
Iustin Pop committed
260
261
262
    vgname = params[0]
    return backend.GetVolumeList(vgname)

263
264
265
266
267
  @staticmethod
  def perspective_vg_list(params):
    """Query the list of volume groups.

    """
Iustin Pop's avatar
Iustin Pop committed
268
269
270
271
    return backend.ListVolumeGroups()

  # bridge  --------------------------

272
273
274
275
276
  @staticmethod
  def perspective_bridges_exist(params):
    """Check if all bridges given exist on this node.

    """
Iustin Pop's avatar
Iustin Pop committed
277
278
279
280
281
    bridges_list = params[0]
    return backend.BridgesExist(bridges_list)

  # instance  --------------------------

282
283
284
285
286
  @staticmethod
  def perspective_instance_os_add(params):
    """Install an OS on a given instance.

    """
Iustin Pop's avatar
Iustin Pop committed
287
    inst_s, os_disk, swap_disk = params
288
    inst = objects.Instance.FromDict(inst_s)
Iustin Pop's avatar
Iustin Pop committed
289
290
    return backend.AddOSToInstance(inst, os_disk, swap_disk)

291
292
293
294
295
296
  @staticmethod
  def perspective_instance_run_rename(params):
    """Runs the OS rename script for an instance.

    """
    inst_s, old_name, os_disk, swap_disk = params
297
    inst = objects.Instance.FromDict(inst_s)
298
299
    return backend.RunRenameInstance(inst, old_name, os_disk, swap_disk)

300
301
302
303
304
  @staticmethod
  def perspective_instance_os_import(params):
    """Run the import function of an OS onto a given instance.

    """
Iustin Pop's avatar
Iustin Pop committed
305
    inst_s, os_disk, swap_disk, src_node, src_image = params
306
    inst = objects.Instance.FromDict(inst_s)
Iustin Pop's avatar
Iustin Pop committed
307
308
309
    return backend.ImportOSIntoInstance(inst, os_disk, swap_disk,
                                        src_node, src_image)

310
311
312
313
314
  @staticmethod
  def perspective_instance_shutdown(params):
    """Shutdown an instance.

    """
315
    instance = objects.Instance.FromDict(params[0])
Iustin Pop's avatar
Iustin Pop committed
316
317
    return backend.ShutdownInstance(instance)

318
319
320
321
322
  @staticmethod
  def perspective_instance_start(params):
    """Start an instance.

    """
323
    instance = objects.Instance.FromDict(params[0])
Iustin Pop's avatar
Iustin Pop committed
324
325
326
    extra_args = params[1]
    return backend.StartInstance(instance, extra_args)

327
328
329
330
331
  @staticmethod
  def perspective_instance_info(params):
    """Query instance information.

    """
Iustin Pop's avatar
Iustin Pop committed
332
333
    return backend.GetInstanceInfo(params[0])

334
335
336
337
338
  @staticmethod
  def perspective_all_instances_info(params):
    """Query information about all instances.

    """
Iustin Pop's avatar
Iustin Pop committed
339
340
    return backend.GetAllInstancesInfo()

341
342
343
344
345
  @staticmethod
  def perspective_instance_list(params):
    """Query the list of running instances.

    """
Iustin Pop's avatar
Iustin Pop committed
346
347
348
349
    return backend.GetInstanceList()

  # node --------------------------

350
351
352
353
354
355
356
357
  @staticmethod
  def perspective_node_tcp_ping(params):
    """Do a TcpPing on the remote node.

    """
    return utils.TcpPing(params[0], params[1], params[2],
                         timeout=params[3], live_port_needed=params[4])

358
359
360
361
362
  @staticmethod
  def perspective_node_info(params):
    """Query node information.

    """
Iustin Pop's avatar
Iustin Pop committed
363
364
365
    vgname = params[0]
    return backend.GetNodeInfo(vgname)

366
367
368
369
370
  @staticmethod
  def perspective_node_add(params):
    """Complete the registration of this node in the cluster.

    """
Iustin Pop's avatar
Iustin Pop committed
371
372
373
    return backend.AddNode(params[0], params[1], params[2],
                           params[3], params[4], params[5])

374
375
376
377
378
  @staticmethod
  def perspective_node_verify(params):
    """Run a verify sequence on this node.

    """
Iustin Pop's avatar
Iustin Pop committed
379
380
    return backend.VerifyNode(params[0])

381
382
383
384
385
  @staticmethod
  def perspective_node_start_master(params):
    """Promote this node to master status.

    """
Iustin Pop's avatar
Iustin Pop committed
386
387
    return backend.StartMaster()

388
389
390
391
392
  @staticmethod
  def perspective_node_stop_master(params):
    """Demote this node from master status.

    """
Iustin Pop's avatar
Iustin Pop committed
393
394
    return backend.StopMaster()

395
396
397
398
399
  @staticmethod
  def perspective_node_leave_cluster(params):
    """Cleanup after leaving a cluster.

    """
Iustin Pop's avatar
Iustin Pop committed
400
401
    return backend.LeaveCluster()

402
403
404
405
406
  @staticmethod
  def perspective_node_volumes(params):
    """Query the list of all logical volume groups.

    """
407
408
    return backend.NodeVolumes()

Iustin Pop's avatar
Iustin Pop committed
409
410
  # cluster --------------------------

411
412
413
414
415
  @staticmethod
  def perspective_version(params):
    """Query version information.

    """
Iustin Pop's avatar
Iustin Pop committed
416
417
    return constants.PROTOCOL_VERSION

418
419
420
421
422
423
424
425
  @staticmethod
  def perspective_upload_file(params):
    """Upload a file.

    Note that the backend implementation imposes strict rules on which
    files are accepted.

    """
Iustin Pop's avatar
Iustin Pop committed
426
427
428
429
430
    return backend.UploadFile(*params)


  # os -----------------------

431
432
433
434
435
  @staticmethod
  def perspective_os_diagnose(params):
    """Query detailed information about existing OSes.

    """
Iustin Pop's avatar
Iustin Pop committed
436
437
438
439
440
441
442
443
    os_list = backend.DiagnoseOS()
    if not os_list:
      # this catches also return values of 'False',
      # for which we can't iterate over
      return os_list
    result = []
    for data in os_list:
      if isinstance(data, objects.OS):
444
        result.append(data.ToDict())
Iustin Pop's avatar
Iustin Pop committed
445
446
447
      elif isinstance(data, errors.InvalidOS):
        result.append(data.args)
      else:
448
449
450
        raise errors.ProgrammerError("Invalid result from backend.DiagnoseOS"
                                     " (class %s, %s)" %
                                     (str(data.__class__), data))
Iustin Pop's avatar
Iustin Pop committed
451
452
453

    return result

454
455
456
457
458
  @staticmethod
  def perspective_os_get(params):
    """Query information about a given OS.

    """
Iustin Pop's avatar
Iustin Pop committed
459
460
    name = params[0]
    try:
461
      os_obj = backend.OSFromDisk(name).ToDict()
Iustin Pop's avatar
Iustin Pop committed
462
    except errors.InvalidOS, err:
463
464
      os_obj = err.args
    return os_obj
Iustin Pop's avatar
Iustin Pop committed
465
466
467

  # hooks -----------------------

468
469
470
471
472
  @staticmethod
  def perspective_hooks_runner(params):
    """Run hook scripts.

    """
Iustin Pop's avatar
Iustin Pop committed
473
474
475
476
477
478
    hpath, phase, env = params
    hr = backend.HooksRunner()
    return hr.RunHooks(hpath, phase, env)


class MyRealm:
479
480
481
  """Simple realm that forwards all requests to a ServerObject.

  """
Iustin Pop's avatar
Iustin Pop committed
482
  __implements__ = portal.IRealm
483

Iustin Pop's avatar
Iustin Pop committed
484
  def requestAvatar(self, avatarId, mind, *interfaces):
485
486
487
    """Return an avatar based on our ServerObject class.

    """
Iustin Pop's avatar
Iustin Pop committed
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
    if pb.IPerspective not in interfaces:
      raise NotImplementedError
    return pb.IPerspective, ServerObject(avatarId), lambda:None


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

  Returns:
    (options, args) as from OptionParser.parse_args()

  """
  parser = OptionParser(description="Ganeti node daemon",
                        usage="%prog [-f] [-d]",
                        version="%%prog (ganeti) %s" %
                        constants.RELEASE_VERSION)

  parser.add_option("-f", "--foreground", dest="fork",
                    help="Don't detach from the current terminal",
                    default=True, action="store_false")
  parser.add_option("-d", "--debug", dest="debug",
                    help="Enable some debug messages",
                    default=False, action="store_true")
  options, args = parser.parse_args()
  return options, args


def main():
516
517
518
  """Main function for the node daemon.

  """
Iustin Pop's avatar
Iustin Pop committed
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
  options, args = ParseOptions()
  for fname in (constants.SSL_CERT_FILE,):
    if not os.path.isfile(fname):
      print "config %s not there, will not run." % fname
      sys.exit(5)

  try:
    ss = ssconf.SimpleStore()
    port = ss.GetNodeDaemonPort()
    pwdata = ss.GetNodeDaemonPassword()
  except errors.ConfigurationError, err:
    print "Cluster configuration incomplete: '%s'" % str(err)
    sys.exit(5)

  # become a daemon
  if options.fork:
    createDaemon()

  logger.SetupLogging(twisted_workaround=True, debug=options.debug,
                      program="ganeti-noded")

  p = portal.Portal(MyRealm())
  p.registerChecker(
    checkers.InMemoryUsernamePasswordDatabaseDontUse(master_node=pwdata))
  reactor.listenSSL(port, pb.PBServerFactory(p), ServerContextFactory())
  reactor.run()


def createDaemon():
  """Detach a process from the controlling terminal and run it in the
  background as a daemon.
550

Iustin Pop's avatar
Iustin Pop committed
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
  """
  UMASK = 077
  WORKDIR = "/"
  # Default maximum for the number of available file descriptors.
  if 'SC_OPEN_MAX' in os.sysconf_names:
    try:
      MAXFD = os.sysconf('SC_OPEN_MAX')
      if MAXFD < 0:
        MAXFD = 1024
    except OSError:
      MAXFD = 1024
  else:
    MAXFD = 1024
  # The standard I/O file descriptors are redirected to /dev/null by default.
  #REDIRECT_TO = getattr(os, "devnull", "/dev/null")
  REDIRECT_TO = constants.LOG_NODESERVER
  try:
    pid = os.fork()
  except OSError, e:
570
    raise Exception("%s [%d]" % (e.strerror, e.errno))
Michael Hanselmann's avatar
Michael Hanselmann committed
571
  if (pid == 0):  # The first child.
Iustin Pop's avatar
Iustin Pop committed
572
573
    os.setsid()
    try:
Michael Hanselmann's avatar
Michael Hanselmann committed
574
      pid = os.fork() # Fork a second child.
Iustin Pop's avatar
Iustin Pop committed
575
    except OSError, e:
576
      raise Exception("%s [%d]" % (e.strerror, e.errno))
Michael Hanselmann's avatar
Michael Hanselmann committed
577
    if (pid == 0):  # The second child.
Iustin Pop's avatar
Iustin Pop committed
578
579
580
581
      os.chdir(WORKDIR)
      os.umask(UMASK)
    else:
      # exit() or _exit()?  See below.
Michael Hanselmann's avatar
Michael Hanselmann committed
582
      os._exit(0) # Exit parent (the first child) of the second child.
Iustin Pop's avatar
Iustin Pop committed
583
  else:
Michael Hanselmann's avatar
Michael Hanselmann committed
584
    os._exit(0) # Exit parent of the first child.
Iustin Pop's avatar
Iustin Pop committed
585
586
587
588
589
590
591
592
  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
  if (maxfd == resource.RLIM_INFINITY):
    maxfd = MAXFD

  # Iterate through and close all file descriptors.
  for fd in range(0, maxfd):
    try:
      os.close(fd)
Michael Hanselmann's avatar
Michael Hanselmann committed
593
    except OSError: # ERROR, fd wasn't open to begin with (ignored)
Iustin Pop's avatar
Iustin Pop committed
594
      pass
595
  os.open(REDIRECT_TO, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
Iustin Pop's avatar
Iustin Pop committed
596
  # Duplicate standard input to standard output and standard error.
Michael Hanselmann's avatar
Michael Hanselmann committed
597
598
  os.dup2(0, 1)     # standard output (1)
  os.dup2(0, 2)     # standard error (2)
Iustin Pop's avatar
Iustin Pop committed
599
600
601
  return(0)


602
if __name__ == '__main__':
Iustin Pop's avatar
Iustin Pop committed
603
  main()