Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ c9064964

History | View | Annotate | Download (45.3 kB)

1
#
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

    
22
"""Functions used by the node daemon"""
23

    
24

    
25
import os
26
import os.path
27
import shutil
28
import time
29
import tempfile
30
import stat
31
import errno
32
import re
33
import subprocess
34

    
35
from ganeti import logger
36
from ganeti import errors
37
from ganeti import utils
38
from ganeti import ssh
39
from ganeti import hypervisor
40
from ganeti import constants
41
from ganeti import bdev
42
from ganeti import objects
43
from ganeti import ssconf
44

    
45

    
46
def StartMaster():
47
  """Activate local node as master node.
48

49
  There are two needed steps for this:
50
    - run the master script
51
    - register the cron script
52

53
  """
54
  result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "start"])
55

    
56
  if result.failed:
57
    logger.Error("could not activate cluster interface with command %s,"
58
                 " error: '%s'" % (result.cmd, result.output))
59
    return False
60

    
61
  return True
62

    
63

    
64
def StopMaster():
65
  """Deactivate this node as master.
66

67
  This runs the master stop script.
68

69
  """
70
  result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "stop"])
71

    
72
  if result.failed:
73
    logger.Error("could not deactivate cluster interface with command %s,"
74
                 " error: '%s'" % (result.cmd, result.output))
75
    return False
76

    
77
  return True
78

    
79

    
80
def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
81
  """Joins this node to the cluster.
82

83
  This does the following:
84
      - updates the hostkeys of the machine (rsa and dsa)
85
      - adds the ssh private key to the user
86
      - adds the ssh public key to the users' authorized_keys file
87

88
  """
89
  sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
90
                (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
91
                (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
92
                (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
93
  for name, content, mode in sshd_keys:
94
    utils.WriteFile(name, data=content, mode=mode)
95

    
96
  try:
97
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
98
                                                    mkdir=True)
99
  except errors.OpExecError, err:
100
    logger.Error("Error while processing user ssh files: %s" % err)
101
    return False
102

    
103
  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
104
    utils.WriteFile(name, data=content, mode=0600)
105

    
106
  utils.AddAuthorizedKey(auth_keys, sshpub)
107

    
108
  utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
109

    
110
  return True
111

    
112

    
113
def LeaveCluster():
114
  """Cleans up the current node and prepares it to be removed from the cluster.
115

116
  """
117
  if os.path.isdir(constants.DATA_DIR):
118
    for rel_name in utils.ListVisibleFiles(constants.DATA_DIR):
119
      full_name = os.path.join(constants.DATA_DIR, rel_name)
120
      if os.path.isfile(full_name) and not os.path.islink(full_name):
121
        utils.RemoveFile(full_name)
122

    
123
  try:
124
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
125
  except errors.OpExecError, err:
126
    logger.Error("Error while processing ssh files: %s" % err)
127
    return
128

    
129
  f = open(pub_key, 'r')
130
  try:
131
    utils.RemoveAuthorizedKey(auth_keys, f.read(8192))
132
  finally:
133
    f.close()
134

    
135
  utils.RemoveFile(priv_key)
136
  utils.RemoveFile(pub_key)
137

    
138

    
139
def GetNodeInfo(vgname):
140
  """Gives back a hash with different informations about the node.
141

142
  Returns:
143
    { 'vg_size' : xxx,  'vg_free' : xxx, 'memory_domain0': xxx,
144
      'memory_free' : xxx, 'memory_total' : xxx }
145
    where
146
    vg_size is the size of the configured volume group in MiB
147
    vg_free is the free size of the volume group in MiB
148
    memory_dom0 is the memory allocated for domain0 in MiB
149
    memory_free is the currently available (free) ram in MiB
150
    memory_total is the total number of ram in MiB
151

152
  """
153
  outputarray = {}
154
  vginfo = _GetVGInfo(vgname)
155
  outputarray['vg_size'] = vginfo['vg_size']
156
  outputarray['vg_free'] = vginfo['vg_free']
157

    
158
  hyper = hypervisor.GetHypervisor()
159
  hyp_info = hyper.GetNodeInfo()
160
  if hyp_info is not None:
161
    outputarray.update(hyp_info)
162

    
163
  f = open("/proc/sys/kernel/random/boot_id", 'r')
164
  try:
165
    outputarray["bootid"] = f.read(128).rstrip("\n")
166
  finally:
167
    f.close()
168

    
169
  return outputarray
170

    
171

    
172
def VerifyNode(what):
173
  """Verify the status of the local node.
174

175
  Args:
176
    what - a dictionary of things to check:
177
      'filelist' : list of files for which to compute checksums
178
      'nodelist' : list of nodes we should check communication with
179
      'hypervisor': run the hypervisor-specific verify
180

181
  Requested files on local node are checksummed and the result returned.
182

183
  The nodelist is traversed, with the following checks being made
184
  for each node:
185
  - known_hosts key correct
186
  - correct resolving of node name (target node returns its own hostname
187
    by ssh-execution of 'hostname', result compared against name in list.
188

189
  """
190
  result = {}
191

    
192
  if 'hypervisor' in what:
193
    result['hypervisor'] = hypervisor.GetHypervisor().Verify()
194

    
195
  if 'filelist' in what:
196
    result['filelist'] = utils.FingerprintFiles(what['filelist'])
197

    
198
  if 'nodelist' in what:
199
    result['nodelist'] = {}
200
    for node in what['nodelist']:
201
      success, message = ssh.VerifyNodeHostname(node)
202
      if not success:
203
        result['nodelist'][node] = message
204
  return result
205

    
206

    
207
def GetVolumeList(vg_name):
208
  """Compute list of logical volumes and their size.
209

210
  Returns:
211
    dictionary of all partions (key) with their size (in MiB), inactive
212
    and online status:
213
    {'test1': ('20.06', True, True)}
214

215
  """
216
  lvs = {}
217
  sep = '|'
218
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
219
                         "--separator=%s" % sep,
220
                         "-olv_name,lv_size,lv_attr", vg_name])
221
  if result.failed:
222
    logger.Error("Failed to list logical volumes, lvs output: %s" %
223
                 result.output)
224
    return result.output
225

    
226
  for line in result.stdout.splitlines():
227
    line = line.strip().rstrip(sep)
228
    name, size, attr = line.split(sep)
229
    if len(attr) != 6:
230
      attr = '------'
231
    inactive = attr[4] == '-'
232
    online = attr[5] == 'o'
233
    lvs[name] = (size, inactive, online)
234

    
235
  return lvs
236

    
237

    
238
def ListVolumeGroups():
239
  """List the volume groups and their size.
240

241
  Returns:
242
    Dictionary with keys volume name and values the size of the volume
243

244
  """
245
  return utils.ListVolumeGroups()
246

    
247

    
248
def NodeVolumes():
249
  """List all volumes on this node.
250

251
  """
252
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
253
                         "--separator=|",
254
                         "--options=lv_name,lv_size,devices,vg_name"])
255
  if result.failed:
256
    logger.Error("Failed to list logical volumes, lvs output: %s" %
257
                 result.output)
258
    return {}
259

    
260
  def parse_dev(dev):
261
    if '(' in dev:
262
      return dev.split('(')[0]
263
    else:
264
      return dev
265

    
266
  def map_line(line):
267
    return {
268
      'name': line[0].strip(),
269
      'size': line[1].strip(),
270
      'dev': parse_dev(line[2].strip()),
271
      'vg': line[3].strip(),
272
    }
273

    
274
  return [map_line(line.split('|')) for line in result.stdout.splitlines()]
275

    
276

    
277
def BridgesExist(bridges_list):
278
  """Check if a list of bridges exist on the current node.
279

280
  Returns:
281
    True if all of them exist, false otherwise
282

283
  """
284
  for bridge in bridges_list:
285
    if not utils.BridgeExists(bridge):
286
      return False
287

    
288
  return True
289

    
290

    
291
def GetInstanceList():
292
  """Provides a list of instances.
293

294
  Returns:
295
    A list of all running instances on the current node
296
    - instance1.example.com
297
    - instance2.example.com
298

299
  """
300
  try:
301
    names = hypervisor.GetHypervisor().ListInstances()
302
  except errors.HypervisorError, err:
303
    logger.Error("error enumerating instances: %s" % str(err))
304
    raise
305

    
306
  return names
307

    
308

    
309
def GetInstanceInfo(instance):
310
  """Gives back the informations about an instance as a dictionary.
311

312
  Args:
313
    instance: name of the instance (ex. instance1.example.com)
314

315
  Returns:
316
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
317
    where
318
    memory: memory size of instance (int)
319
    state: xen state of instance (string)
320
    time: cpu time of instance (float)
321

322
  """
323
  output = {}
324

    
325
  iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
326
  if iinfo is not None:
327
    output['memory'] = iinfo[2]
328
    output['state'] = iinfo[4]
329
    output['time'] = iinfo[5]
330

    
331
  return output
332

    
333

    
334
def GetAllInstancesInfo():
335
  """Gather data about all instances.
336

337
  This is the equivalent of `GetInstanceInfo()`, except that it
338
  computes data for all instances at once, thus being faster if one
339
  needs data about more than one instance.
340

341
  Returns: a dictionary of dictionaries, keys being the instance name,
342
    and with values:
343
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
344
    where
345
    memory: memory size of instance (int)
346
    state: xen state of instance (string)
347
    time: cpu time of instance (float)
348
    vcpus: the number of cpus
349

350
  """
351
  output = {}
352

    
353
  iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
354
  if iinfo:
355
    for name, inst_id, memory, vcpus, state, times in iinfo:
356
      output[name] = {
357
        'memory': memory,
358
        'vcpus': vcpus,
359
        'state': state,
360
        'time': times,
361
        }
362

    
363
  return output
364

    
365

    
366
def AddOSToInstance(instance, os_disk, swap_disk):
367
  """Add an OS to an instance.
368

369
  Args:
370
    instance: the instance object
371
    os_disk: the instance-visible name of the os device
372
    swap_disk: the instance-visible name of the swap device
373

374
  """
375
  inst_os = OSFromDisk(instance.os)
376

    
377
  create_script = inst_os.create_script
378

    
379
  os_device = instance.FindDisk(os_disk)
380
  if os_device is None:
381
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
382
    return False
383

    
384
  swap_device = instance.FindDisk(swap_disk)
385
  if swap_device is None:
386
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
387
    return False
388

    
389
  real_os_dev = _RecursiveFindBD(os_device)
390
  if real_os_dev is None:
391
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
392
                                  str(os_device))
393
  real_os_dev.Open()
394

    
395
  real_swap_dev = _RecursiveFindBD(swap_device)
396
  if real_swap_dev is None:
397
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
398
                                  str(swap_device))
399
  real_swap_dev.Open()
400

    
401
  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
402
                                     instance.name, int(time.time()))
403
  if not os.path.exists(constants.LOG_OS_DIR):
404
    os.mkdir(constants.LOG_OS_DIR, 0750)
405

    
406
  command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
407
                                inst_os.path, create_script, instance.name,
408
                                real_os_dev.dev_path, real_swap_dev.dev_path,
409
                                logfile)
410

    
411
  result = utils.RunCmd(command)
412
  if result.failed:
413
    logger.Error("os create command '%s' returned error: %s, logfile: %s,"
414
                 " output: %s" %
415
                 (command, result.fail_reason, logfile, result.output))
416
    return False
417

    
418
  return True
419

    
420

    
421
def RunRenameInstance(instance, old_name, os_disk, swap_disk):
422
  """Run the OS rename script for an instance.
423

424
  Args:
425
    instance: the instance object
426
    old_name: the old name of the instance
427
    os_disk: the instance-visible name of the os device
428
    swap_disk: the instance-visible name of the swap device
429

430
  """
431
  inst_os = OSFromDisk(instance.os)
432

    
433
  script = inst_os.rename_script
434

    
435
  os_device = instance.FindDisk(os_disk)
436
  if os_device is None:
437
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
438
    return False
439

    
440
  swap_device = instance.FindDisk(swap_disk)
441
  if swap_device is None:
442
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
443
    return False
444

    
445
  real_os_dev = _RecursiveFindBD(os_device)
446
  if real_os_dev is None:
447
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
448
                                  str(os_device))
449
  real_os_dev.Open()
450

    
451
  real_swap_dev = _RecursiveFindBD(swap_device)
452
  if real_swap_dev is None:
453
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
454
                                  str(swap_device))
455
  real_swap_dev.Open()
456

    
457
  logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
458
                                           old_name,
459
                                           instance.name, int(time.time()))
460
  if not os.path.exists(constants.LOG_OS_DIR):
461
    os.mkdir(constants.LOG_OS_DIR, 0750)
462

    
463
  command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
464
                                inst_os.path, script, old_name, instance.name,
465
                                real_os_dev.dev_path, real_swap_dev.dev_path,
466
                                logfile)
467

    
468
  result = utils.RunCmd(command)
469

    
470
  if result.failed:
471
    logger.Error("os create command '%s' returned error: %s"
472
                 " output: %s" %
473
                 (command, result.fail_reason, result.output))
474
    return False
475

    
476
  return True
477

    
478

    
479
def _GetVGInfo(vg_name):
480
  """Get informations about the volume group.
481

482
  Args:
483
    vg_name: the volume group
484

485
  Returns:
486
    { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
487
    where
488
    vg_size is the total size of the volume group in MiB
489
    vg_free is the free size of the volume group in MiB
490
    pv_count are the number of physical disks in that vg
491

492
  If an error occurs during gathering of data, we return the same dict
493
  with keys all set to None.
494

495
  """
496
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
497

    
498
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
499
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
500

    
501
  if retval.failed:
502
    errmsg = "volume group %s not present" % vg_name
503
    logger.Error(errmsg)
504
    return retdic
505
  valarr = retval.stdout.strip().rstrip(':').split(':')
506
  if len(valarr) == 3:
507
    try:
508
      retdic = {
509
        "vg_size": int(round(float(valarr[0]), 0)),
510
        "vg_free": int(round(float(valarr[1]), 0)),
511
        "pv_count": int(valarr[2]),
512
        }
513
    except ValueError, err:
514
      logger.Error("Fail to parse vgs output: %s" % str(err))
515
  else:
516
    logger.Error("vgs output has the wrong number of fields (expected"
517
                 " three): %s" % str(valarr))
518
  return retdic
519

    
520

    
521
def _GatherBlockDevs(instance):
522
  """Set up an instance's block device(s).
523

524
  This is run on the primary node at instance startup. The block
525
  devices must be already assembled.
526

527
  """
528
  block_devices = []
529
  for disk in instance.disks:
530
    device = _RecursiveFindBD(disk)
531
    if device is None:
532
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
533
                                    str(disk))
534
    device.Open()
535
    block_devices.append((disk, device))
536
  return block_devices
537

    
538

    
539
def StartInstance(instance, extra_args):
540
  """Start an instance.
541

542
  Args:
543
    instance - name of instance to start.
544

545
  """
546
  running_instances = GetInstanceList()
547

    
548
  if instance.name in running_instances:
549
    return True
550

    
551
  block_devices = _GatherBlockDevs(instance)
552
  hyper = hypervisor.GetHypervisor()
553

    
554
  try:
555
    hyper.StartInstance(instance, block_devices, extra_args)
556
  except errors.HypervisorError, err:
557
    logger.Error("Failed to start instance: %s" % err)
558
    return False
559

    
560
  return True
561

    
562

    
563
def ShutdownInstance(instance):
564
  """Shut an instance down.
565

566
  Args:
567
    instance - name of instance to shutdown.
568

569
  """
570
  running_instances = GetInstanceList()
571

    
572
  if instance.name not in running_instances:
573
    return True
574

    
575
  hyper = hypervisor.GetHypervisor()
576
  try:
577
    hyper.StopInstance(instance)
578
  except errors.HypervisorError, err:
579
    logger.Error("Failed to stop instance: %s" % err)
580
    return False
581

    
582
  # test every 10secs for 2min
583
  shutdown_ok = False
584

    
585
  time.sleep(1)
586
  for dummy in range(11):
587
    if instance.name not in GetInstanceList():
588
      break
589
    time.sleep(10)
590
  else:
591
    # the shutdown did not succeed
592
    logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
593

    
594
    try:
595
      hyper.StopInstance(instance, force=True)
596
    except errors.HypervisorError, err:
597
      logger.Error("Failed to stop instance: %s" % err)
598
      return False
599

    
600
    time.sleep(1)
601
    if instance.name in GetInstanceList():
602
      logger.Error("could not shutdown instance '%s' even by destroy")
603
      return False
604

    
605
  return True
606

    
607

    
608
def RebootInstance(instance, reboot_type, extra_args):
609
  """Reboot an instance.
610

611
  Args:
612
    instance    - name of instance to reboot
613
    reboot_type - how to reboot [soft,hard,full]
614

615
  """
616
  running_instances = GetInstanceList()
617

    
618
  if instance.name not in running_instances:
619
    logger.Error("Cannot reboot instance that is not running")
620
    return False
621

    
622
  hyper = hypervisor.GetHypervisor()
623
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
624
    try:
625
      hyper.RebootInstance(instance)
626
    except errors.HypervisorError, err:
627
      logger.Error("Failed to soft reboot instance: %s" % err)
628
      return False
629
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
630
    try:
631
      ShutdownInstance(instance)
632
      StartInstance(instance, extra_args)
633
    except errors.HypervisorError, err:
634
      logger.Error("Failed to hard reboot instance: %s" % err)
635
      return False
636
  else:
637
    raise errors.ParameterError("reboot_type invalid")
638

    
639

    
640
  return True
641

    
642

    
643
def CreateBlockDevice(disk, size, owner, on_primary, info):
644
  """Creates a block device for an instance.
645

646
  Args:
647
   disk: a ganeti.objects.Disk object
648
   size: the size of the physical underlying device
649
   owner: a string with the name of the instance
650
   on_primary: a boolean indicating if it is the primary node or not
651
   info: string that will be sent to the physical device creation
652

653
  Returns:
654
    the new unique_id of the device (this can sometime be
655
    computed only after creation), or None. On secondary nodes,
656
    it's not required to return anything.
657

658
  """
659
  clist = []
660
  if disk.children:
661
    for child in disk.children:
662
      crdev = _RecursiveAssembleBD(child, owner, on_primary)
663
      if on_primary or disk.AssembleOnSecondary():
664
        # we need the children open in case the device itself has to
665
        # be assembled
666
        crdev.Open()
667
      clist.append(crdev)
668
  try:
669
    device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
670
    if device is not None:
671
      logger.Info("removing existing device %s" % disk)
672
      device.Remove()
673
  except errors.BlockDeviceError, err:
674
    pass
675

    
676
  device = bdev.Create(disk.dev_type, disk.physical_id,
677
                       clist, size)
678
  if device is None:
679
    raise ValueError("Can't create child device for %s, %s" %
680
                     (disk, size))
681
  if on_primary or disk.AssembleOnSecondary():
682
    if not device.Assemble():
683
      errorstring = "Can't assemble device after creation"
684
      logger.Error(errorstring)
685
      raise errors.BlockDeviceError("%s, very unusual event - check the node"
686
                                    " daemon logs" % errorstring)
687
    device.SetSyncSpeed(constants.SYNC_SPEED)
688
    if on_primary or disk.OpenOnSecondary():
689
      device.Open(force=True)
690
    DevCacheManager.UpdateCache(device.dev_path, owner,
691
                                on_primary, disk.iv_name)
692

    
693
  device.SetInfo(info)
694

    
695
  physical_id = device.unique_id
696
  return physical_id
697

    
698

    
699
def RemoveBlockDevice(disk):
700
  """Remove a block device.
701

702
  This is intended to be called recursively.
703

704
  """
705
  try:
706
    # since we are removing the device, allow a partial match
707
    # this allows removal of broken mirrors
708
    rdev = _RecursiveFindBD(disk, allow_partial=True)
709
  except errors.BlockDeviceError, err:
710
    # probably can't attach
711
    logger.Info("Can't attach to device %s in remove" % disk)
712
    rdev = None
713
  if rdev is not None:
714
    r_path = rdev.dev_path
715
    result = rdev.Remove()
716
    if result:
717
      DevCacheManager.RemoveCache(r_path)
718
  else:
719
    result = True
720
  if disk.children:
721
    for child in disk.children:
722
      result = result and RemoveBlockDevice(child)
723
  return result
724

    
725

    
726
def _RecursiveAssembleBD(disk, owner, as_primary):
727
  """Activate a block device for an instance.
728

729
  This is run on the primary and secondary nodes for an instance.
730

731
  This function is called recursively.
732

733
  Args:
734
    disk: a objects.Disk object
735
    as_primary: if we should make the block device read/write
736

737
  Returns:
738
    the assembled device or None (in case no device was assembled)
739

740
  If the assembly is not successful, an exception is raised.
741

742
  """
743
  children = []
744
  if disk.children:
745
    mcn = disk.ChildrenNeeded()
746
    if mcn == -1:
747
      mcn = 0 # max number of Nones allowed
748
    else:
749
      mcn = len(disk.children) - mcn # max number of Nones
750
    for chld_disk in disk.children:
751
      try:
752
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
753
      except errors.BlockDeviceError, err:
754
        if children.count(None) >= mcn:
755
          raise
756
        cdev = None
757
        logger.Debug("Error in child activation: %s" % str(err))
758
      children.append(cdev)
759

    
760
  if as_primary or disk.AssembleOnSecondary():
761
    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
762
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
763
    result = r_dev
764
    if as_primary or disk.OpenOnSecondary():
765
      r_dev.Open()
766
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
767
                                as_primary, disk.iv_name)
768

    
769
  else:
770
    result = True
771
  return result
772

    
773

    
774
def AssembleBlockDevice(disk, owner, as_primary):
775
  """Activate a block device for an instance.
776

777
  This is a wrapper over _RecursiveAssembleBD.
778

779
  Returns:
780
    a /dev path for primary nodes
781
    True for secondary nodes
782

783
  """
784
  result = _RecursiveAssembleBD(disk, owner, as_primary)
785
  if isinstance(result, bdev.BlockDev):
786
    result = result.dev_path
787
  return result
788

    
789

    
790
def ShutdownBlockDevice(disk):
791
  """Shut down a block device.
792

793
  First, if the device is assembled (can `Attach()`), then the device
794
  is shutdown. Then the children of the device are shutdown.
795

796
  This function is called recursively. Note that we don't cache the
797
  children or such, as oppossed to assemble, shutdown of different
798
  devices doesn't require that the upper device was active.
799

800
  """
801
  r_dev = _RecursiveFindBD(disk)
802
  if r_dev is not None:
803
    r_path = r_dev.dev_path
804
    result = r_dev.Shutdown()
805
    if result:
806
      DevCacheManager.RemoveCache(r_path)
807
  else:
808
    result = True
809
  if disk.children:
810
    for child in disk.children:
811
      result = result and ShutdownBlockDevice(child)
812
  return result
813

    
814

    
815
def MirrorAddChildren(parent_cdev, new_cdevs):
816
  """Extend a mirrored block device.
817

818
  """
819
  parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
820
  if parent_bdev is None:
821
    logger.Error("Can't find parent device")
822
    return False
823
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
824
  if new_bdevs.count(None) > 0:
825
    logger.Error("Can't find new device(s) to add: %s:%s" %
826
                 (new_bdevs, new_cdevs))
827
    return False
828
  parent_bdev.AddChildren(new_bdevs)
829
  return True
830

    
831

    
832
def MirrorRemoveChildren(parent_cdev, new_cdevs):
833
  """Shrink a mirrored block device.
834

835
  """
836
  parent_bdev = _RecursiveFindBD(parent_cdev)
837
  if parent_bdev is None:
838
    logger.Error("Can't find parent in remove children: %s" % parent_cdev)
839
    return False
840
  devs = []
841
  for disk in new_cdevs:
842
    rpath = disk.StaticDevPath()
843
    if rpath is None:
844
      bd = _RecursiveFindBD(disk)
845
      if bd is None:
846
        logger.Error("Can't find dynamic device %s while removing children" %
847
                     disk)
848
        return False
849
      else:
850
        devs.append(bd.dev_path)
851
    else:
852
      devs.append(rpath)
853
  parent_bdev.RemoveChildren(devs)
854
  return True
855

    
856

    
857
def GetMirrorStatus(disks):
858
  """Get the mirroring status of a list of devices.
859

860
  Args:
861
    disks: list of `objects.Disk`
862

863
  Returns:
864
    list of (mirror_done, estimated_time) tuples, which
865
    are the result of bdev.BlockDevice.CombinedSyncStatus()
866

867
  """
868
  stats = []
869
  for dsk in disks:
870
    rbd = _RecursiveFindBD(dsk)
871
    if rbd is None:
872
      raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
873
    stats.append(rbd.CombinedSyncStatus())
874
  return stats
875

    
876

    
877
def _RecursiveFindBD(disk, allow_partial=False):
878
  """Check if a device is activated.
879

880
  If so, return informations about the real device.
881

882
  Args:
883
    disk: the objects.Disk instance
884
    allow_partial: don't abort the find if a child of the
885
                   device can't be found; this is intended to be
886
                   used when repairing mirrors
887

888
  Returns:
889
    None if the device can't be found
890
    otherwise the device instance
891

892
  """
893
  children = []
894
  if disk.children:
895
    for chdisk in disk.children:
896
      children.append(_RecursiveFindBD(chdisk))
897

    
898
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
899

    
900

    
901
def FindBlockDevice(disk):
902
  """Check if a device is activated.
903

904
  If so, return informations about the real device.
905

906
  Args:
907
    disk: the objects.Disk instance
908
  Returns:
909
    None if the device can't be found
910
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
911

912
  """
913
  rbd = _RecursiveFindBD(disk)
914
  if rbd is None:
915
    return rbd
916
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
917

    
918

    
919
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
920
  """Write a file to the filesystem.
921

922
  This allows the master to overwrite(!) a file. It will only perform
923
  the operation if the file belongs to a list of configuration files.
924

925
  """
926
  if not os.path.isabs(file_name):
927
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
928
                 file_name)
929
    return False
930

    
931
  allowed_files = [
932
    constants.CLUSTER_CONF_FILE,
933
    constants.ETC_HOSTS,
934
    constants.SSH_KNOWN_HOSTS_FILE,
935
    ]
936
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
937
  if file_name not in allowed_files:
938
    logger.Error("Filename passed to UploadFile not in allowed"
939
                 " upload targets: '%s'" % file_name)
940
    return False
941

    
942
  dir_name, small_name = os.path.split(file_name)
943
  fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
944
  # here we need to make sure we remove the temp file, if any error
945
  # leaves it in place
946
  try:
947
    os.chown(new_name, uid, gid)
948
    os.chmod(new_name, mode)
949
    os.write(fd, data)
950
    os.fsync(fd)
951
    os.utime(new_name, (atime, mtime))
952
    os.rename(new_name, file_name)
953
  finally:
954
    os.close(fd)
955
    utils.RemoveFile(new_name)
956
  return True
957

    
958

    
959
def _ErrnoOrStr(err):
960
  """Format an EnvironmentError exception.
961

962
  If the `err` argument has an errno attribute, it will be looked up
963
  and converted into a textual EXXXX description. Otherwise the string
964
  representation of the error will be returned.
965

966
  """
967
  if hasattr(err, 'errno'):
968
    detail = errno.errorcode[err.errno]
969
  else:
970
    detail = str(err)
971
  return detail
972

    
973

    
974
def _OSSearch(name, search_path=None):
975
  """Search for OSes with the given name in the search_path.
976

977
  Args:
978
    name: The name of the OS to look for
979
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
980

981
  Returns:
982
    The base_dir the OS resides in
983

984
  """
985
  if search_path is None:
986
    search_path = constants.OS_SEARCH_PATH
987

    
988
  for dir_name in search_path:
989
    t_os_dir = os.path.sep.join([dir_name, name])
990
    if os.path.isdir(t_os_dir):
991
      return dir_name
992

    
993
  return None
994

    
995

    
996
def _OSOndiskVersion(name, os_dir):
997
  """Compute and return the API version of a given OS.
998

999
  This function will try to read the API version of the os given by
1000
  the 'name' parameter and residing in the 'os_dir' directory.
1001

1002
  Return value will be either an integer denoting the version or None in the
1003
  case when this is not a valid OS name.
1004

1005
  """
1006
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
1007

    
1008
  try:
1009
    st = os.stat(api_file)
1010
  except EnvironmentError, err:
1011
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
1012
                           " found (%s)" % _ErrnoOrStr(err))
1013

    
1014
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1015
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
1016
                           " a regular file")
1017

    
1018
  try:
1019
    f = open(api_file)
1020
    try:
1021
      api_version = f.read(256)
1022
    finally:
1023
      f.close()
1024
  except EnvironmentError, err:
1025
    raise errors.InvalidOS(name, os_dir, "error while reading the"
1026
                           " API version (%s)" % _ErrnoOrStr(err))
1027

    
1028
  api_version = api_version.strip()
1029
  try:
1030
    api_version = int(api_version)
1031
  except (TypeError, ValueError), err:
1032
    raise errors.InvalidOS(name, os_dir,
1033
                           "API version is not integer (%s)" % str(err))
1034

    
1035
  return api_version
1036

    
1037

    
1038
def DiagnoseOS(top_dirs=None):
1039
  """Compute the validity for all OSes.
1040

1041
  Returns an OS object for each name in all the given top directories
1042
  (if not given defaults to constants.OS_SEARCH_PATH)
1043

1044
  Returns:
1045
    list of OS objects
1046

1047
  """
1048
  if top_dirs is None:
1049
    top_dirs = constants.OS_SEARCH_PATH
1050

    
1051
  result = []
1052
  for dir_name in top_dirs:
1053
    if os.path.isdir(dir_name):
1054
      try:
1055
        f_names = utils.ListVisibleFiles(dir_name)
1056
      except EnvironmentError, err:
1057
        logger.Error("Can't list the OS directory %s: %s" %
1058
                     (dir_name, str(err)))
1059
        break
1060
      for name in f_names:
1061
        try:
1062
          os_inst = OSFromDisk(name, base_dir=dir_name)
1063
          result.append(os_inst)
1064
        except errors.InvalidOS, err:
1065
          result.append(objects.OS.FromInvalidOS(err))
1066

    
1067
  return result
1068

    
1069

    
1070
def OSFromDisk(name, base_dir=None):
1071
  """Create an OS instance from disk.
1072

1073
  This function will return an OS instance if the given name is a
1074
  valid OS name. Otherwise, it will raise an appropriate
1075
  `errors.InvalidOS` exception, detailing why this is not a valid
1076
  OS.
1077

1078
  Args:
1079
    os_dir: Directory containing the OS scripts. Defaults to a search
1080
            in all the OS_SEARCH_PATH directories.
1081

1082
  """
1083

    
1084
  if base_dir is None:
1085
    base_dir = _OSSearch(name)
1086

    
1087
  if base_dir is None:
1088
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1089

    
1090
  os_dir = os.path.sep.join([base_dir, name])
1091
  api_version = _OSOndiskVersion(name, os_dir)
1092

    
1093
  if api_version != constants.OS_API_VERSION:
1094
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1095
                           " (found %s want %s)"
1096
                           % (api_version, constants.OS_API_VERSION))
1097

    
1098
  # OS Scripts dictionary, we will populate it with the actual script names
1099
  os_scripts = {'create': '', 'export': '', 'import': '', 'rename': ''}
1100

    
1101
  for script in os_scripts:
1102
    os_scripts[script] = os.path.sep.join([os_dir, script])
1103

    
1104
    try:
1105
      st = os.stat(os_scripts[script])
1106
    except EnvironmentError, err:
1107
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1108
                             (script, _ErrnoOrStr(err)))
1109

    
1110
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1111
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1112
                             script)
1113

    
1114
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1115
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1116
                             script)
1117

    
1118

    
1119
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1120
                    create_script=os_scripts['create'],
1121
                    export_script=os_scripts['export'],
1122
                    import_script=os_scripts['import'],
1123
                    rename_script=os_scripts['rename'],
1124
                    api_version=api_version)
1125

    
1126

    
1127
def SnapshotBlockDevice(disk):
1128
  """Create a snapshot copy of a block device.
1129

1130
  This function is called recursively, and the snapshot is actually created
1131
  just for the leaf lvm backend device.
1132

1133
  Args:
1134
    disk: the disk to be snapshotted
1135

1136
  Returns:
1137
    a config entry for the actual lvm device snapshotted.
1138

1139
  """
1140
  if disk.children:
1141
    if len(disk.children) == 1:
1142
      # only one child, let's recurse on it
1143
      return SnapshotBlockDevice(disk.children[0])
1144
    else:
1145
      # more than one child, choose one that matches
1146
      for child in disk.children:
1147
        if child.size == disk.size:
1148
          # return implies breaking the loop
1149
          return SnapshotBlockDevice(child)
1150
  elif disk.dev_type == constants.LD_LV:
1151
    r_dev = _RecursiveFindBD(disk)
1152
    if r_dev is not None:
1153
      # let's stay on the safe side and ask for the full size, for now
1154
      return r_dev.Snapshot(disk.size)
1155
    else:
1156
      return None
1157
  else:
1158
    raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1159
                                 " '%s' of type '%s'" %
1160
                                 (disk.unique_id, disk.dev_type))
1161

    
1162

    
1163
def ExportSnapshot(disk, dest_node, instance):
1164
  """Export a block device snapshot to a remote node.
1165

1166
  Args:
1167
    disk: the snapshot block device
1168
    dest_node: the node to send the image to
1169
    instance: instance being exported
1170

1171
  Returns:
1172
    True if successful, False otherwise.
1173

1174
  """
1175
  inst_os = OSFromDisk(instance.os)
1176
  export_script = inst_os.export_script
1177

    
1178
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1179
                                     instance.name, int(time.time()))
1180
  if not os.path.exists(constants.LOG_OS_DIR):
1181
    os.mkdir(constants.LOG_OS_DIR, 0750)
1182

    
1183
  real_os_dev = _RecursiveFindBD(disk)
1184
  if real_os_dev is None:
1185
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1186
                                  str(disk))
1187
  real_os_dev.Open()
1188

    
1189
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1190
  destfile = disk.physical_id[1]
1191

    
1192
  # the target command is built out of three individual commands,
1193
  # which are joined by pipes; we check each individual command for
1194
  # valid parameters
1195

    
1196
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1197
                               export_script, instance.name,
1198
                               real_os_dev.dev_path, logfile)
1199

    
1200
  comprcmd = "gzip"
1201

    
1202
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1203
                                destdir, destdir, destfile)
1204
  remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1205

    
1206

    
1207

    
1208
  # all commands have been checked, so we're safe to combine them
1209
  command = '|'.join([expcmd, comprcmd, utils.ShellQuoteArgs(remotecmd)])
1210

    
1211
  result = utils.RunCmd(command)
1212

    
1213
  if result.failed:
1214
    logger.Error("os snapshot export command '%s' returned error: %s"
1215
                 " output: %s" %
1216
                 (command, result.fail_reason, result.output))
1217
    return False
1218

    
1219
  return True
1220

    
1221

    
1222
def FinalizeExport(instance, snap_disks):
1223
  """Write out the export configuration information.
1224

1225
  Args:
1226
    instance: instance configuration
1227
    snap_disks: snapshot block devices
1228

1229
  Returns:
1230
    False in case of error, True otherwise.
1231

1232
  """
1233
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1234
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1235

    
1236
  config = objects.SerializableConfigParser()
1237

    
1238
  config.add_section(constants.INISECT_EXP)
1239
  config.set(constants.INISECT_EXP, 'version', '0')
1240
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1241
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1242
  config.set(constants.INISECT_EXP, 'os', instance.os)
1243
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1244

    
1245
  config.add_section(constants.INISECT_INS)
1246
  config.set(constants.INISECT_INS, 'name', instance.name)
1247
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1248
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1249
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1250
  for nic_count, nic in enumerate(instance.nics):
1251
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1252
               nic_count, '%s' % nic.mac)
1253
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1254
    config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count, '%s' % nic.bridge)
1255
  # TODO: redundant: on load can read nics until it doesn't exist
1256
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1257

    
1258
  for disk_count, disk in enumerate(snap_disks):
1259
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1260
               ('%s' % disk.iv_name))
1261
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1262
               ('%s' % disk.physical_id[1]))
1263
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1264
               ('%d' % disk.size))
1265
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1266

    
1267
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1268
  cfo = open(cff, 'w')
1269
  try:
1270
    config.write(cfo)
1271
  finally:
1272
    cfo.close()
1273

    
1274
  shutil.rmtree(finaldestdir, True)
1275
  shutil.move(destdir, finaldestdir)
1276

    
1277
  return True
1278

    
1279

    
1280
def ExportInfo(dest):
1281
  """Get export configuration information.
1282

1283
  Args:
1284
    dest: directory containing the export
1285

1286
  Returns:
1287
    A serializable config file containing the export info.
1288

1289
  """
1290
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1291

    
1292
  config = objects.SerializableConfigParser()
1293
  config.read(cff)
1294

    
1295
  if (not config.has_section(constants.INISECT_EXP) or
1296
      not config.has_section(constants.INISECT_INS)):
1297
    return None
1298

    
1299
  return config
1300

    
1301

    
1302
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1303
  """Import an os image into an instance.
1304

1305
  Args:
1306
    instance: the instance object
1307
    os_disk: the instance-visible name of the os device
1308
    swap_disk: the instance-visible name of the swap device
1309
    src_node: node holding the source image
1310
    src_image: path to the source image on src_node
1311

1312
  Returns:
1313
    False in case of error, True otherwise.
1314

1315
  """
1316
  inst_os = OSFromDisk(instance.os)
1317
  import_script = inst_os.import_script
1318

    
1319
  os_device = instance.FindDisk(os_disk)
1320
  if os_device is None:
1321
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1322
    return False
1323

    
1324
  swap_device = instance.FindDisk(swap_disk)
1325
  if swap_device is None:
1326
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1327
    return False
1328

    
1329
  real_os_dev = _RecursiveFindBD(os_device)
1330
  if real_os_dev is None:
1331
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1332
                                  str(os_device))
1333
  real_os_dev.Open()
1334

    
1335
  real_swap_dev = _RecursiveFindBD(swap_device)
1336
  if real_swap_dev is None:
1337
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1338
                                  str(swap_device))
1339
  real_swap_dev.Open()
1340

    
1341
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1342
                                        instance.name, int(time.time()))
1343
  if not os.path.exists(constants.LOG_OS_DIR):
1344
    os.mkdir(constants.LOG_OS_DIR, 0750)
1345

    
1346
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1347
  remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1348

    
1349
  comprcmd = "gunzip"
1350
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1351
                               inst_os.path, import_script, instance.name,
1352
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1353
                               logfile)
1354

    
1355
  command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
1356

    
1357
  result = utils.RunCmd(command)
1358

    
1359
  if result.failed:
1360
    logger.Error("os import command '%s' returned error: %s"
1361
                 " output: %s" %
1362
                 (command, result.fail_reason, result.output))
1363
    return False
1364

    
1365
  return True
1366

    
1367

    
1368
def ListExports():
1369
  """Return a list of exports currently available on this machine.
1370

1371
  """
1372
  if os.path.isdir(constants.EXPORT_DIR):
1373
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1374
  else:
1375
    return []
1376

    
1377

    
1378
def RemoveExport(export):
1379
  """Remove an existing export from the node.
1380

1381
  Args:
1382
    export: the name of the export to remove
1383

1384
  Returns:
1385
    False in case of error, True otherwise.
1386

1387
  """
1388
  target = os.path.join(constants.EXPORT_DIR, export)
1389

    
1390
  shutil.rmtree(target)
1391
  # TODO: catch some of the relevant exceptions and provide a pretty
1392
  # error message if rmtree fails.
1393

    
1394
  return True
1395

    
1396

    
1397
def RenameBlockDevices(devlist):
1398
  """Rename a list of block devices.
1399

1400
  The devlist argument is a list of tuples (disk, new_logical,
1401
  new_physical). The return value will be a combined boolean result
1402
  (True only if all renames succeeded).
1403

1404
  """
1405
  result = True
1406
  for disk, unique_id in devlist:
1407
    dev = _RecursiveFindBD(disk)
1408
    if dev is None:
1409
      result = False
1410
      continue
1411
    try:
1412
      old_rpath = dev.dev_path
1413
      dev.Rename(unique_id)
1414
      new_rpath = dev.dev_path
1415
      if old_rpath != new_rpath:
1416
        DevCacheManager.RemoveCache(old_rpath)
1417
        # FIXME: we should add the new cache information here, like:
1418
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1419
        # but we don't have the owner here - maybe parse from existing
1420
        # cache? for now, we only lose lvm data when we rename, which
1421
        # is less critical than DRBD or MD
1422
    except errors.BlockDeviceError, err:
1423
      logger.Error("Can't rename device '%s' to '%s': %s" %
1424
                   (dev, unique_id, err))
1425
      result = False
1426
  return result
1427

    
1428

    
1429
class HooksRunner(object):
1430
  """Hook runner.
1431

1432
  This class is instantiated on the node side (ganeti-noded) and not on
1433
  the master side.
1434

1435
  """
1436
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1437

    
1438
  def __init__(self, hooks_base_dir=None):
1439
    """Constructor for hooks runner.
1440

1441
    Args:
1442
      - hooks_base_dir: if not None, this overrides the
1443
        constants.HOOKS_BASE_DIR (useful for unittests)
1444
      - logs_base_dir: if not None, this overrides the
1445
        constants.LOG_HOOKS_DIR (useful for unittests)
1446
      - logging: enable or disable logging of script output
1447

1448
    """
1449
    if hooks_base_dir is None:
1450
      hooks_base_dir = constants.HOOKS_BASE_DIR
1451
    self._BASE_DIR = hooks_base_dir
1452

    
1453
  @staticmethod
1454
  def ExecHook(script, env):
1455
    """Exec one hook script.
1456

1457
    Args:
1458
     - phase: the phase
1459
     - script: the full path to the script
1460
     - env: the environment with which to exec the script
1461

1462
    """
1463
    # exec the process using subprocess and log the output
1464
    fdstdin = None
1465
    try:
1466
      fdstdin = open("/dev/null", "r")
1467
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1468
                               stderr=subprocess.STDOUT, close_fds=True,
1469
                               shell=False, cwd="/", env=env)
1470
      output = ""
1471
      try:
1472
        output = child.stdout.read(4096)
1473
        child.stdout.close()
1474
      except EnvironmentError, err:
1475
        output += "Hook script error: %s" % str(err)
1476

    
1477
      while True:
1478
        try:
1479
          result = child.wait()
1480
          break
1481
        except EnvironmentError, err:
1482
          if err.errno == errno.EINTR:
1483
            continue
1484
          raise
1485
    finally:
1486
      # try not to leak fds
1487
      for fd in (fdstdin, ):
1488
        if fd is not None:
1489
          try:
1490
            fd.close()
1491
          except EnvironmentError, err:
1492
            # just log the error
1493
            #logger.Error("While closing fd %s: %s" % (fd, err))
1494
            pass
1495

    
1496
    return result == 0, output
1497

    
1498
  def RunHooks(self, hpath, phase, env):
1499
    """Run the scripts in the hooks directory.
1500

1501
    This method will not be usually overriden by child opcodes.
1502

1503
    """
1504
    if phase == constants.HOOKS_PHASE_PRE:
1505
      suffix = "pre"
1506
    elif phase == constants.HOOKS_PHASE_POST:
1507
      suffix = "post"
1508
    else:
1509
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1510
    rr = []
1511

    
1512
    subdir = "%s-%s.d" % (hpath, suffix)
1513
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1514
    try:
1515
      dir_contents = utils.ListVisibleFiles(dir_name)
1516
    except OSError, err:
1517
      # must log
1518
      return rr
1519

    
1520
    # we use the standard python sort order,
1521
    # so 00name is the recommended naming scheme
1522
    dir_contents.sort()
1523
    for relname in dir_contents:
1524
      fname = os.path.join(dir_name, relname)
1525
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1526
          self.RE_MASK.match(relname) is not None):
1527
        rrval = constants.HKR_SKIP
1528
        output = ""
1529
      else:
1530
        result, output = self.ExecHook(fname, env)
1531
        if not result:
1532
          rrval = constants.HKR_FAIL
1533
        else:
1534
          rrval = constants.HKR_SUCCESS
1535
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1536

    
1537
    return rr
1538

    
1539

    
1540
class DevCacheManager(object):
1541
  """Simple class for managing a cache of block device information.
1542

1543
  """
1544
  _DEV_PREFIX = "/dev/"
1545
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1546

    
1547
  @classmethod
1548
  def _ConvertPath(cls, dev_path):
1549
    """Converts a /dev/name path to the cache file name.
1550

1551
    This replaces slashes with underscores and strips the /dev
1552
    prefix. It then returns the full path to the cache file
1553

1554
    """
1555
    if dev_path.startswith(cls._DEV_PREFIX):
1556
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1557
    dev_path = dev_path.replace("/", "_")
1558
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1559
    return fpath
1560

    
1561
  @classmethod
1562
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1563
    """Updates the cache information for a given device.
1564

1565
    """
1566
    if dev_path is None:
1567
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1568
      return
1569
    fpath = cls._ConvertPath(dev_path)
1570
    if on_primary:
1571
      state = "primary"
1572
    else:
1573
      state = "secondary"
1574
    if iv_name is None:
1575
      iv_name = "not_visible"
1576
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1577
    try:
1578
      utils.WriteFile(fpath, data=fdata)
1579
    except EnvironmentError, err:
1580
      logger.Error("Can't update bdev cache for %s, error %s" %
1581
                   (dev_path, str(err)))
1582

    
1583
  @classmethod
1584
  def RemoveCache(cls, dev_path):
1585
    """Remove data for a dev_path.
1586

1587
    """
1588
    if dev_path is None:
1589
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1590
      return
1591
    fpath = cls._ConvertPath(dev_path)
1592
    try:
1593
      utils.RemoveFile(fpath)
1594
    except EnvironmentError, err:
1595
      logger.Error("Can't update bdev cache for %s, error %s" %
1596
                   (dev_path, str(err)))