Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ e739bd57

History | View | Annotate | Download (44.1 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 does two things:
68
    - run the master stop script
69
    - remove link to master cron script.
70

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

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

    
79
  return True
80

    
81

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

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

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

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

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

    
108
  utils.AddAuthorizedKey(auth_keys, sshpub)
109

    
110
  utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
111

    
112
  return True
113

    
114

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

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

    
125

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

    
132
  f = open(pub_key, 'r')
133
  try:
134
    utils.RemoveAuthorizedKey(auth_keys, f.read(8192))
135
  finally:
136
    f.close()
137

    
138
  utils.RemoveFile(priv_key)
139
  utils.RemoveFile(pub_key)
140

    
141

    
142
def GetNodeInfo(vgname):
143
  """Gives back a hash with different informations about the node.
144

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

155
  """
156
  outputarray = {}
157
  vginfo = _GetVGInfo(vgname)
158
  outputarray['vg_size'] = vginfo['vg_size']
159
  outputarray['vg_free'] = vginfo['vg_free']
160

    
161
  hyper = hypervisor.GetHypervisor()
162
  hyp_info = hyper.GetNodeInfo()
163
  if hyp_info is not None:
164
    outputarray.update(hyp_info)
165

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

    
172
  return outputarray
173

    
174

    
175
def VerifyNode(what):
176
  """Verify the status of the local node.
177

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

184
  Requested files on local node are checksummed and the result returned.
185

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

192
  """
193
  result = {}
194

    
195
  if 'hypervisor' in what:
196
    result['hypervisor'] = hypervisor.GetHypervisor().Verify()
197

    
198
  if 'filelist' in what:
199
    result['filelist'] = utils.FingerprintFiles(what['filelist'])
200

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

    
209

    
210
def GetVolumeList(vg_name):
211
  """Compute list of logical volumes and their size.
212

213
  Returns:
214
    dictionary of all partions (key) with their size:
215
    test1: 20.06MiB
216

217
  """
218
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m",
219
                         "-oname,size", vg_name])
220
  if result.failed:
221
    logger.Error("Failed to list logical volumes, lvs output: %s" %
222
                 result.output)
223
    return {}
224

    
225
  lvlist = [line.split() for line in result.output.splitlines()]
226
  return dict(lvlist)
227

    
228

    
229
def ListVolumeGroups():
230
  """List the volume groups and their size.
231

232
  Returns:
233
    Dictionary with keys volume name and values the size of the volume
234

235
  """
236
  return utils.ListVolumeGroups()
237

    
238

    
239
def NodeVolumes():
240
  """List all volumes on this node.
241

242
  """
243
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
244
                         "--separator=|",
245
                         "--options=lv_name,lv_size,devices,vg_name"])
246
  if result.failed:
247
    logger.Error("Failed to list logical volumes, lvs output: %s" %
248
                 result.output)
249
    return {}
250

    
251
  def parse_dev(dev):
252
    if '(' in dev:
253
      return dev.split('(')[0]
254
    else:
255
      return dev
256

    
257
  def map_line(line):
258
    return {
259
      'name': line[0].strip(),
260
      'size': line[1].strip(),
261
      'dev': parse_dev(line[2].strip()),
262
      'vg': line[3].strip(),
263
    }
264

    
265
  return [map_line(line.split('|')) for line in result.output.splitlines()]
266

    
267

    
268
def BridgesExist(bridges_list):
269
  """Check if a list of bridges exist on the current node.
270

271
  Returns:
272
    True if all of them exist, false otherwise
273

274
  """
275
  for bridge in bridges_list:
276
    if not utils.BridgeExists(bridge):
277
      return False
278

    
279
  return True
280

    
281

    
282
def GetInstanceList():
283
  """Provides a list of instances.
284

285
  Returns:
286
    A list of all running instances on the current node
287
    - instance1.example.com
288
    - instance2.example.com
289

290
  """
291
  try:
292
    names = hypervisor.GetHypervisor().ListInstances()
293
  except errors.HypervisorError, err:
294
    logger.Error("error enumerating instances: %s" % str(err))
295
    raise
296

    
297
  return names
298

    
299

    
300
def GetInstanceInfo(instance):
301
  """Gives back the informations about an instance as a dictionary.
302

303
  Args:
304
    instance: name of the instance (ex. instance1.example.com)
305

306
  Returns:
307
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
308
    where
309
    memory: memory size of instance (int)
310
    state: xen state of instance (string)
311
    time: cpu time of instance (float)
312

313
  """
314
  output = {}
315

    
316
  iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
317
  if iinfo is not None:
318
    output['memory'] = iinfo[2]
319
    output['state'] = iinfo[4]
320
    output['time'] = iinfo[5]
321

    
322
  return output
323

    
324

    
325
def GetAllInstancesInfo():
326
  """Gather data about all instances.
327

328
  This is the equivalent of `GetInstanceInfo()`, except that it
329
  computes data for all instances at once, thus being faster if one
330
  needs data about more than one instance.
331

332
  Returns: a dictionary of dictionaries, keys being the instance name,
333
    and with values:
334
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
335
    where
336
    memory: memory size of instance (int)
337
    state: xen state of instance (string)
338
    time: cpu time of instance (float)
339
    vcpus: the number of cpus
340

341
  """
342
  output = {}
343

    
344
  iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
345
  if iinfo:
346
    for name, inst_id, memory, vcpus, state, times in iinfo:
347
      output[name] = {
348
        'memory': memory,
349
        'vcpus': vcpus,
350
        'state': state,
351
        'time': times,
352
        }
353

    
354
  return output
355

    
356

    
357
def AddOSToInstance(instance, os_disk, swap_disk):
358
  """Add an OS to an instance.
359

360
  Args:
361
    instance: the instance object
362
    os_disk: the instance-visible name of the os device
363
    swap_disk: the instance-visible name of the swap device
364

365
  """
366
  inst_os = OSFromDisk(instance.os)
367

    
368
  create_script = inst_os.create_script
369

    
370
  os_device = instance.FindDisk(os_disk)
371
  if os_device is None:
372
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
373
    return False
374

    
375
  swap_device = instance.FindDisk(swap_disk)
376
  if swap_device is None:
377
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
378
    return False
379

    
380
  real_os_dev = _RecursiveFindBD(os_device)
381
  if real_os_dev is None:
382
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
383
                                  str(os_device))
384
  real_os_dev.Open()
385

    
386
  real_swap_dev = _RecursiveFindBD(swap_device)
387
  if real_swap_dev is None:
388
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
389
                                  str(swap_device))
390
  real_swap_dev.Open()
391

    
392
  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
393
                                     instance.name, int(time.time()))
394
  if not os.path.exists(constants.LOG_OS_DIR):
395
    os.mkdir(constants.LOG_OS_DIR, 0750)
396

    
397
  command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
398
                                inst_os.path, create_script, instance.name,
399
                                real_os_dev.dev_path, real_swap_dev.dev_path,
400
                                logfile)
401

    
402
  result = utils.RunCmd(command)
403

    
404
  if result.failed:
405
    logger.Error("os create command '%s' returned error: %s"
406
                 " output: %s" %
407
                 (command, result.fail_reason, result.output))
408
    return False
409

    
410
  return True
411

    
412

    
413
def RunRenameInstance(instance, old_name, os_disk, swap_disk):
414
  """Run the OS rename script for an instance.
415

416
  Args:
417
    instance: the instance object
418
    old_name: the old name of the instance
419
    os_disk: the instance-visible name of the os device
420
    swap_disk: the instance-visible name of the swap device
421

422
  """
423
  inst_os = OSFromDisk(instance.os)
424

    
425
  script = inst_os.rename_script
426

    
427
  os_device = instance.FindDisk(os_disk)
428
  if os_device is None:
429
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
430
    return False
431

    
432
  swap_device = instance.FindDisk(swap_disk)
433
  if swap_device is None:
434
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
435
    return False
436

    
437
  real_os_dev = _RecursiveFindBD(os_device)
438
  if real_os_dev is None:
439
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
440
                                  str(os_device))
441
  real_os_dev.Open()
442

    
443
  real_swap_dev = _RecursiveFindBD(swap_device)
444
  if real_swap_dev is None:
445
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
446
                                  str(swap_device))
447
  real_swap_dev.Open()
448

    
449
  logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
450
                                           old_name,
451
                                           instance.name, int(time.time()))
452
  if not os.path.exists(constants.LOG_OS_DIR):
453
    os.mkdir(constants.LOG_OS_DIR, 0750)
454

    
455
  command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
456
                                inst_os.path, script, old_name, instance.name,
457
                                real_os_dev.dev_path, real_swap_dev.dev_path,
458
                                logfile)
459

    
460
  result = utils.RunCmd(command)
461

    
462
  if result.failed:
463
    logger.Error("os create command '%s' returned error: %s"
464
                 " output: %s" %
465
                 (command, result.fail_reason, result.output))
466
    return False
467

    
468
  return True
469

    
470

    
471
def _GetVGInfo(vg_name):
472
  """Get informations about the volume group.
473

474
  Args:
475
    vg_name: the volume group
476

477
  Returns:
478
    { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
479
    where
480
    vg_size is the total size of the volume group in MiB
481
    vg_free is the free size of the volume group in MiB
482
    pv_count are the number of physical disks in that vg
483

484
  """
485
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
486
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
487

    
488
  if retval.failed:
489
    errmsg = "volume group %s not present" % vg_name
490
    logger.Error(errmsg)
491
    raise errors.LVMError(errmsg)
492
  valarr = retval.stdout.strip().split(':')
493
  retdic = {
494
    "vg_size": int(round(float(valarr[0]), 0)),
495
    "vg_free": int(round(float(valarr[1]), 0)),
496
    "pv_count": int(valarr[2]),
497
    }
498
  return retdic
499

    
500

    
501
def _GatherBlockDevs(instance):
502
  """Set up an instance's block device(s).
503

504
  This is run on the primary node at instance startup. The block
505
  devices must be already assembled.
506

507
  """
508
  block_devices = []
509
  for disk in instance.disks:
510
    device = _RecursiveFindBD(disk)
511
    if device is None:
512
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
513
                                    str(disk))
514
    device.Open()
515
    block_devices.append((disk, device))
516
  return block_devices
517

    
518

    
519
def StartInstance(instance, extra_args):
520
  """Start an instance.
521

522
  Args:
523
    instance - name of instance to start.
524

525
  """
526
  running_instances = GetInstanceList()
527

    
528
  if instance.name in running_instances:
529
    return True
530

    
531
  block_devices = _GatherBlockDevs(instance)
532
  hyper = hypervisor.GetHypervisor()
533

    
534
  try:
535
    hyper.StartInstance(instance, block_devices, extra_args)
536
  except errors.HypervisorError, err:
537
    logger.Error("Failed to start instance: %s" % err)
538
    return False
539

    
540
  return True
541

    
542

    
543
def ShutdownInstance(instance):
544
  """Shut an instance down.
545

546
  Args:
547
    instance - name of instance to shutdown.
548

549
  """
550
  running_instances = GetInstanceList()
551

    
552
  if instance.name not in running_instances:
553
    return True
554

    
555
  hyper = hypervisor.GetHypervisor()
556
  try:
557
    hyper.StopInstance(instance)
558
  except errors.HypervisorError, err:
559
    logger.Error("Failed to stop instance: %s" % err)
560
    return False
561

    
562
  # test every 10secs for 2min
563
  shutdown_ok = False
564

    
565
  time.sleep(1)
566
  for dummy in range(11):
567
    if instance.name not in GetInstanceList():
568
      break
569
    time.sleep(10)
570
  else:
571
    # the shutdown did not succeed
572
    logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
573

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

    
580
    time.sleep(1)
581
    if instance.name in GetInstanceList():
582
      logger.Error("could not shutdown instance '%s' even by destroy")
583
      return False
584

    
585
  return True
586

    
587

    
588
def RebootInstance(instance, reboot_type, extra_args):
589
  """Reboot an instance.
590

591
  Args:
592
    instance    - name of instance to reboot
593
    reboot_type - how to reboot [soft,hard,full]
594

595
  """
596
  running_instances = GetInstanceList()
597

    
598
  if instance.name not in running_instances:
599
    logger.Error("Cannot reboot instance that is not running")
600
    return False
601

    
602
  hyper = hypervisor.GetHypervisor()
603
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
604
    try:
605
      hyper.RebootInstance(instance)
606
    except errors.HypervisorError, err:
607
      logger.Error("Failed to soft reboot instance: %s" % err)
608
      return False
609
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
610
    try:
611
      ShutdownInstance(instance)
612
      StartInstance(instance, extra_args)
613
    except errors.HypervisorError, err:
614
      logger.Error("Failed to hard reboot instance: %s" % err)
615
      return False
616
  else:
617
    raise errors.ParameterError("reboot_type invalid")
618

    
619

    
620
  return True
621

    
622

    
623
def CreateBlockDevice(disk, size, owner, on_primary, info):
624
  """Creates a block device for an instance.
625

626
  Args:
627
   bdev: a ganeti.objects.Disk object
628
   size: the size of the physical underlying devices
629
   do_open: if the device should be `Assemble()`-d and
630
            `Open()`-ed after creation
631

632
  Returns:
633
    the new unique_id of the device (this can sometime be
634
    computed only after creation), or None. On secondary nodes,
635
    it's not required to return anything.
636

637
  """
638
  clist = []
639
  if disk.children:
640
    for child in disk.children:
641
      crdev = _RecursiveAssembleBD(child, owner, on_primary)
642
      if on_primary or disk.AssembleOnSecondary():
643
        # we need the children open in case the device itself has to
644
        # be assembled
645
        crdev.Open()
646
      else:
647
        crdev.Close()
648
      clist.append(crdev)
649
  try:
650
    device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
651
    if device is not None:
652
      logger.Info("removing existing device %s" % disk)
653
      device.Remove()
654
  except errors.BlockDeviceError, err:
655
    pass
656

    
657
  device = bdev.Create(disk.dev_type, disk.physical_id,
658
                       clist, size)
659
  if device is None:
660
    raise ValueError("Can't create child device for %s, %s" %
661
                     (disk, size))
662
  if on_primary or disk.AssembleOnSecondary():
663
    if not device.Assemble():
664
      raise errors.BlockDeviceError("Can't assemble device after creation,"
665
                                    " very unusual event - check the node"
666
                                    " daemon logs")
667
    device.SetSyncSpeed(constants.SYNC_SPEED)
668
    if on_primary or disk.OpenOnSecondary():
669
      device.Open(force=True)
670
    DevCacheManager.UpdateCache(device.dev_path, owner,
671
                                on_primary, disk.iv_name)
672

    
673
  device.SetInfo(info)
674

    
675
  physical_id = device.unique_id
676
  return physical_id
677

    
678

    
679
def RemoveBlockDevice(disk):
680
  """Remove a block device.
681

682
  This is intended to be called recursively.
683

684
  """
685
  try:
686
    # since we are removing the device, allow a partial match
687
    # this allows removal of broken mirrors
688
    rdev = _RecursiveFindBD(disk, allow_partial=True)
689
  except errors.BlockDeviceError, err:
690
    # probably can't attach
691
    logger.Info("Can't attach to device %s in remove" % disk)
692
    rdev = None
693
  if rdev is not None:
694
    r_path = rdev.dev_path
695
    result = rdev.Remove()
696
    if result:
697
      DevCacheManager.RemoveCache(r_path)
698
  else:
699
    result = True
700
  if disk.children:
701
    for child in disk.children:
702
      result = result and RemoveBlockDevice(child)
703
  return result
704

    
705

    
706
def _RecursiveAssembleBD(disk, owner, as_primary):
707
  """Activate a block device for an instance.
708

709
  This is run on the primary and secondary nodes for an instance.
710

711
  This function is called recursively.
712

713
  Args:
714
    disk: a objects.Disk object
715
    as_primary: if we should make the block device read/write
716

717
  Returns:
718
    the assembled device or None (in case no device was assembled)
719

720
  If the assembly is not successful, an exception is raised.
721

722
  """
723
  children = []
724
  if disk.children:
725
    for chld_disk in disk.children:
726
      children.append(_RecursiveAssembleBD(chld_disk, owner, as_primary))
727

    
728
  if as_primary or disk.AssembleOnSecondary():
729
    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
730
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
731
    result = r_dev
732
    if as_primary or disk.OpenOnSecondary():
733
      r_dev.Open()
734
    else:
735
      r_dev.Close()
736
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
737
                                as_primary, disk.iv_name)
738

    
739
  else:
740
    result = True
741
  return result
742

    
743

    
744
def AssembleBlockDevice(disk, owner, as_primary):
745
  """Activate a block device for an instance.
746

747
  This is a wrapper over _RecursiveAssembleBD.
748

749
  Returns:
750
    a /dev path for primary nodes
751
    True for secondary nodes
752

753
  """
754
  result = _RecursiveAssembleBD(disk, owner, as_primary)
755
  if isinstance(result, bdev.BlockDev):
756
    result = result.dev_path
757
  return result
758

    
759

    
760
def ShutdownBlockDevice(disk):
761
  """Shut down a block device.
762

763
  First, if the device is assembled (can `Attach()`), then the device
764
  is shutdown. Then the children of the device are shutdown.
765

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

770
  """
771
  r_dev = _RecursiveFindBD(disk)
772
  if r_dev is not None:
773
    r_path = r_dev.dev_path
774
    result = r_dev.Shutdown()
775
    if result:
776
      DevCacheManager.RemoveCache(r_path)
777
  else:
778
    result = True
779
  if disk.children:
780
    for child in disk.children:
781
      result = result and ShutdownBlockDevice(child)
782
  return result
783

    
784

    
785
def MirrorAddChildren(parent_cdev, new_cdevs):
786
  """Extend a mirrored block device.
787

788
  """
789
  parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
790
  if parent_bdev is None:
791
    logger.Error("Can't find parent device")
792
    return False
793
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
794
  if new_bdevs.count(None) > 0:
795
    logger.Error("Can't find new device(s) to add: %s:%s" %
796
                 (new_bdevs, new_cdevs))
797
    return False
798
  parent_bdev.AddChildren(new_bdevs)
799
  return True
800

    
801

    
802
def MirrorRemoveChildren(parent_cdev, new_cdevs):
803
  """Shrink a mirrored block device.
804

805
  """
806
  parent_bdev = _RecursiveFindBD(parent_cdev)
807
  if parent_bdev is None:
808
    logger.Error("Can't find parent in remove children: %s" % parent_cdev)
809
    return False
810
  devs = []
811
  for disk in new_cdevs:
812
    rpath = disk.StaticDevPath()
813
    if rpath is None:
814
      bd = _RecursiveFindBD(disk)
815
      if bd is None:
816
        logger.Error("Can't find dynamic device %s while removing children" %
817
                     disk)
818
        return False
819
      else:
820
        devs.append(bd.dev_path)
821
    else:
822
      devs.append(rpath)
823
  parent_bdev.RemoveChildren(devs)
824
  return True
825

    
826

    
827
def GetMirrorStatus(disks):
828
  """Get the mirroring status of a list of devices.
829

830
  Args:
831
    disks: list of `objects.Disk`
832

833
  Returns:
834
    list of (mirror_done, estimated_time) tuples, which
835
    are the result of bdev.BlockDevice.CombinedSyncStatus()
836

837
  """
838
  stats = []
839
  for dsk in disks:
840
    rbd = _RecursiveFindBD(dsk)
841
    if rbd is None:
842
      raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
843
    stats.append(rbd.CombinedSyncStatus())
844
  return stats
845

    
846

    
847
def _RecursiveFindBD(disk, allow_partial=False):
848
  """Check if a device is activated.
849

850
  If so, return informations about the real device.
851

852
  Args:
853
    disk: the objects.Disk instance
854
    allow_partial: don't abort the find if a child of the
855
                   device can't be found; this is intended to be
856
                   used when repairing mirrors
857

858
  Returns:
859
    None if the device can't be found
860
    otherwise the device instance
861

862
  """
863
  children = []
864
  if disk.children:
865
    for chdisk in disk.children:
866
      children.append(_RecursiveFindBD(chdisk))
867

    
868
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
869

    
870

    
871
def FindBlockDevice(disk):
872
  """Check if a device is activated.
873

874
  If so, return informations about the real device.
875

876
  Args:
877
    disk: the objects.Disk instance
878
  Returns:
879
    None if the device can't be found
880
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
881

882
  """
883
  rbd = _RecursiveFindBD(disk)
884
  if rbd is None:
885
    return rbd
886
  sync_p, est_t, is_degr = rbd.GetSyncStatus()
887
  return rbd.dev_path, rbd.major, rbd.minor, sync_p, est_t, is_degr
888

    
889

    
890
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
891
  """Write a file to the filesystem.
892

893
  This allows the master to overwrite(!) a file. It will only perform
894
  the operation if the file belongs to a list of configuration files.
895

896
  """
897
  if not os.path.isabs(file_name):
898
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
899
                 file_name)
900
    return False
901

    
902
  allowed_files = [constants.CLUSTER_CONF_FILE, "/etc/hosts",
903
                   constants.SSH_KNOWN_HOSTS_FILE]
904
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
905
  if file_name not in allowed_files:
906
    logger.Error("Filename passed to UploadFile not in allowed"
907
                 " upload targets: '%s'" % file_name)
908
    return False
909

    
910
  dir_name, small_name = os.path.split(file_name)
911
  fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
912
  # here we need to make sure we remove the temp file, if any error
913
  # leaves it in place
914
  try:
915
    os.chown(new_name, uid, gid)
916
    os.chmod(new_name, mode)
917
    os.write(fd, data)
918
    os.fsync(fd)
919
    os.utime(new_name, (atime, mtime))
920
    os.rename(new_name, file_name)
921
  finally:
922
    os.close(fd)
923
    utils.RemoveFile(new_name)
924
  return True
925

    
926

    
927
def _ErrnoOrStr(err):
928
  """Format an EnvironmentError exception.
929

930
  If the `err` argument has an errno attribute, it will be looked up
931
  and converted into a textual EXXXX description. Otherwise the string
932
  representation of the error will be returned.
933

934
  """
935
  if hasattr(err, 'errno'):
936
    detail = errno.errorcode[err.errno]
937
  else:
938
    detail = str(err)
939
  return detail
940

    
941

    
942
def _OSSearch(name, search_path=None):
943
  """Search for OSes with the given name in the search_path.
944

945
  Args:
946
    name: The name of the OS to look for
947
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
948

949
  Returns:
950
    The base_dir the OS resides in
951

952
  """
953
  if search_path is None:
954
    search_path = constants.OS_SEARCH_PATH
955

    
956
  for dir_name in search_path:
957
    t_os_dir = os.path.sep.join([dir_name, name])
958
    if os.path.isdir(t_os_dir):
959
      return dir_name
960

    
961
  return None
962

    
963

    
964
def _OSOndiskVersion(name, os_dir):
965
  """Compute and return the API version of a given OS.
966

967
  This function will try to read the API version of the os given by
968
  the 'name' parameter and residing in the 'os_dir' directory.
969

970
  Return value will be either an integer denoting the version or None in the
971
  case when this is not a valid OS name.
972

973
  """
974
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
975

    
976
  try:
977
    st = os.stat(api_file)
978
  except EnvironmentError, err:
979
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
980
                           " found (%s)" % _ErrnoOrStr(err))
981

    
982
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
983
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
984
                           " a regular file")
985

    
986
  try:
987
    f = open(api_file)
988
    try:
989
      api_version = f.read(256)
990
    finally:
991
      f.close()
992
  except EnvironmentError, err:
993
    raise errors.InvalidOS(name, os_dir, "error while reading the"
994
                           " API version (%s)" % _ErrnoOrStr(err))
995

    
996
  api_version = api_version.strip()
997
  try:
998
    api_version = int(api_version)
999
  except (TypeError, ValueError), err:
1000
    raise errors.InvalidOS(name, os_dir,
1001
                           "API version is not integer (%s)" % str(err))
1002

    
1003
  return api_version
1004

    
1005

    
1006
def DiagnoseOS(top_dirs=None):
1007
  """Compute the validity for all OSes.
1008

1009
  Returns an OS object for each name in all the given top directories
1010
  (if not given defaults to constants.OS_SEARCH_PATH)
1011

1012
  Returns:
1013
    list of OS objects
1014

1015
  """
1016
  if top_dirs is None:
1017
    top_dirs = constants.OS_SEARCH_PATH
1018

    
1019
  result = []
1020
  for dir_name in top_dirs:
1021
    if os.path.isdir(dir_name):
1022
      try:
1023
        f_names = utils.ListVisibleFiles(dir_name)
1024
      except EnvironmentError, err:
1025
        logger.Error("Can't list the OS directory %s: %s" %
1026
                     (dir_name, str(err)))
1027
        break
1028
      for name in f_names:
1029
        try:
1030
          os_inst = OSFromDisk(name, base_dir=dir_name)
1031
          result.append(os_inst)
1032
        except errors.InvalidOS, err:
1033
          result.append(objects.OS.FromInvalidOS(err))
1034

    
1035
  return result
1036

    
1037

    
1038
def OSFromDisk(name, base_dir=None):
1039
  """Create an OS instance from disk.
1040

1041
  This function will return an OS instance if the given name is a
1042
  valid OS name. Otherwise, it will raise an appropriate
1043
  `errors.InvalidOS` exception, detailing why this is not a valid
1044
  OS.
1045

1046
  Args:
1047
    os_dir: Directory containing the OS scripts. Defaults to a search
1048
            in all the OS_SEARCH_PATH directories.
1049

1050
  """
1051

    
1052
  if base_dir is None:
1053
    base_dir = _OSSearch(name)
1054

    
1055
  if base_dir is None:
1056
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1057

    
1058
  os_dir = os.path.sep.join([base_dir, name])
1059
  api_version = _OSOndiskVersion(name, os_dir)
1060

    
1061
  if api_version != constants.OS_API_VERSION:
1062
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1063
                           " (found %s want %s)"
1064
                           % (api_version, constants.OS_API_VERSION))
1065

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

    
1069
  for script in os_scripts:
1070
    os_scripts[script] = os.path.sep.join([os_dir, script])
1071

    
1072
    try:
1073
      st = os.stat(os_scripts[script])
1074
    except EnvironmentError, err:
1075
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1076
                             (script, _ErrnoOrStr(err)))
1077

    
1078
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1079
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1080
                             script)
1081

    
1082
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1083
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1084
                             script)
1085

    
1086

    
1087
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1088
                    create_script=os_scripts['create'],
1089
                    export_script=os_scripts['export'],
1090
                    import_script=os_scripts['import'],
1091
                    rename_script=os_scripts['rename'],
1092
                    api_version=api_version)
1093

    
1094

    
1095
def SnapshotBlockDevice(disk):
1096
  """Create a snapshot copy of a block device.
1097

1098
  This function is called recursively, and the snapshot is actually created
1099
  just for the leaf lvm backend device.
1100

1101
  Args:
1102
    disk: the disk to be snapshotted
1103

1104
  Returns:
1105
    a config entry for the actual lvm device snapshotted.
1106

1107
  """
1108
  if disk.children:
1109
    if len(disk.children) == 1:
1110
      # only one child, let's recurse on it
1111
      return SnapshotBlockDevice(disk.children[0])
1112
    else:
1113
      # more than one child, choose one that matches
1114
      for child in disk.children:
1115
        if child.size == disk.size:
1116
          # return implies breaking the loop
1117
          return SnapshotBlockDevice(child)
1118
  elif disk.dev_type == constants.LD_LV:
1119
    r_dev = _RecursiveFindBD(disk)
1120
    if r_dev is not None:
1121
      # let's stay on the safe side and ask for the full size, for now
1122
      return r_dev.Snapshot(disk.size)
1123
    else:
1124
      return None
1125
  else:
1126
    raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1127
                                 "'%s' of type '%s'" %
1128
                                 (disk.unique_id, disk.dev_type))
1129

    
1130

    
1131
def ExportSnapshot(disk, dest_node, instance):
1132
  """Export a block device snapshot to a remote node.
1133

1134
  Args:
1135
    disk: the snapshot block device
1136
    dest_node: the node to send the image to
1137
    instance: instance being exported
1138

1139
  Returns:
1140
    True if successful, False otherwise.
1141

1142
  """
1143
  inst_os = OSFromDisk(instance.os)
1144
  export_script = inst_os.export_script
1145

    
1146
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1147
                                     instance.name, int(time.time()))
1148
  if not os.path.exists(constants.LOG_OS_DIR):
1149
    os.mkdir(constants.LOG_OS_DIR, 0750)
1150

    
1151
  real_os_dev = _RecursiveFindBD(disk)
1152
  if real_os_dev is None:
1153
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1154
                                  str(disk))
1155
  real_os_dev.Open()
1156

    
1157
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1158
  destfile = disk.physical_id[1]
1159

    
1160
  # the target command is built out of three individual commands,
1161
  # which are joined by pipes; we check each individual command for
1162
  # valid parameters
1163

    
1164
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1165
                               export_script, instance.name,
1166
                               real_os_dev.dev_path, logfile)
1167

    
1168
  comprcmd = "gzip"
1169

    
1170
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1171
                                destdir, destdir, destfile)
1172
  remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1173

    
1174

    
1175

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

    
1179
  result = utils.RunCmd(command)
1180

    
1181
  if result.failed:
1182
    logger.Error("os snapshot export command '%s' returned error: %s"
1183
                 " output: %s" %
1184
                 (command, result.fail_reason, result.output))
1185
    return False
1186

    
1187
  return True
1188

    
1189

    
1190
def FinalizeExport(instance, snap_disks):
1191
  """Write out the export configuration information.
1192

1193
  Args:
1194
    instance: instance configuration
1195
    snap_disks: snapshot block devices
1196

1197
  Returns:
1198
    False in case of error, True otherwise.
1199

1200
  """
1201
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1202
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1203

    
1204
  config = objects.SerializableConfigParser()
1205

    
1206
  config.add_section(constants.INISECT_EXP)
1207
  config.set(constants.INISECT_EXP, 'version', '0')
1208
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1209
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1210
  config.set(constants.INISECT_EXP, 'os', instance.os)
1211
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1212

    
1213
  config.add_section(constants.INISECT_INS)
1214
  config.set(constants.INISECT_INS, 'name', instance.name)
1215
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1216
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1217
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1218
  for nic_count, nic in enumerate(instance.nics):
1219
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1220
               nic_count, '%s' % nic.mac)
1221
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1222
  # TODO: redundant: on load can read nics until it doesn't exist
1223
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1224

    
1225
  for disk_count, disk in enumerate(snap_disks):
1226
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1227
               ('%s' % disk.iv_name))
1228
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1229
               ('%s' % disk.physical_id[1]))
1230
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1231
               ('%d' % disk.size))
1232
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1233

    
1234
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1235
  cfo = open(cff, 'w')
1236
  try:
1237
    config.write(cfo)
1238
  finally:
1239
    cfo.close()
1240

    
1241
  shutil.rmtree(finaldestdir, True)
1242
  shutil.move(destdir, finaldestdir)
1243

    
1244
  return True
1245

    
1246

    
1247
def ExportInfo(dest):
1248
  """Get export configuration information.
1249

1250
  Args:
1251
    dest: directory containing the export
1252

1253
  Returns:
1254
    A serializable config file containing the export info.
1255

1256
  """
1257
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1258

    
1259
  config = objects.SerializableConfigParser()
1260
  config.read(cff)
1261

    
1262
  if (not config.has_section(constants.INISECT_EXP) or
1263
      not config.has_section(constants.INISECT_INS)):
1264
    return None
1265

    
1266
  return config
1267

    
1268

    
1269
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1270
  """Import an os image into an instance.
1271

1272
  Args:
1273
    instance: the instance object
1274
    os_disk: the instance-visible name of the os device
1275
    swap_disk: the instance-visible name of the swap device
1276
    src_node: node holding the source image
1277
    src_image: path to the source image on src_node
1278

1279
  Returns:
1280
    False in case of error, True otherwise.
1281

1282
  """
1283
  inst_os = OSFromDisk(instance.os)
1284
  import_script = inst_os.import_script
1285

    
1286
  os_device = instance.FindDisk(os_disk)
1287
  if os_device is None:
1288
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1289
    return False
1290

    
1291
  swap_device = instance.FindDisk(swap_disk)
1292
  if swap_device is None:
1293
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1294
    return False
1295

    
1296
  real_os_dev = _RecursiveFindBD(os_device)
1297
  if real_os_dev is None:
1298
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1299
                                  str(os_device))
1300
  real_os_dev.Open()
1301

    
1302
  real_swap_dev = _RecursiveFindBD(swap_device)
1303
  if real_swap_dev is None:
1304
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1305
                                  str(swap_device))
1306
  real_swap_dev.Open()
1307

    
1308
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1309
                                        instance.name, int(time.time()))
1310
  if not os.path.exists(constants.LOG_OS_DIR):
1311
    os.mkdir(constants.LOG_OS_DIR, 0750)
1312

    
1313
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1314
  remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1315

    
1316
  comprcmd = "gunzip"
1317
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1318
                               inst_os.path, import_script, instance.name,
1319
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1320
                               logfile)
1321

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

    
1324
  result = utils.RunCmd(command)
1325

    
1326
  if result.failed:
1327
    logger.Error("os import command '%s' returned error: %s"
1328
                 " output: %s" %
1329
                 (command, result.fail_reason, result.output))
1330
    return False
1331

    
1332
  return True
1333

    
1334

    
1335
def ListExports():
1336
  """Return a list of exports currently available on this machine.
1337

1338
  """
1339
  if os.path.isdir(constants.EXPORT_DIR):
1340
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1341
  else:
1342
    return []
1343

    
1344

    
1345
def RemoveExport(export):
1346
  """Remove an existing export from the node.
1347

1348
  Args:
1349
    export: the name of the export to remove
1350

1351
  Returns:
1352
    False in case of error, True otherwise.
1353

1354
  """
1355
  target = os.path.join(constants.EXPORT_DIR, export)
1356

    
1357
  shutil.rmtree(target)
1358
  # TODO: catch some of the relevant exceptions and provide a pretty
1359
  # error message if rmtree fails.
1360

    
1361
  return True
1362

    
1363

    
1364
def RenameBlockDevices(devlist):
1365
  """Rename a list of block devices.
1366

1367
  The devlist argument is a list of tuples (disk, new_logical,
1368
  new_physical). The return value will be a combined boolean result
1369
  (True only if all renames succeeded).
1370

1371
  """
1372
  result = True
1373
  for disk, unique_id in devlist:
1374
    dev = _RecursiveFindBD(disk)
1375
    if dev is None:
1376
      result = False
1377
      continue
1378
    try:
1379
      old_rpath = dev.dev_path
1380
      dev.Rename(unique_id)
1381
      new_rpath = dev.dev_path
1382
      if old_rpath != new_rpath:
1383
        DevCacheManager.RemoveCache(old_rpath)
1384
        # FIXME: we should add the new cache information here, like:
1385
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1386
        # but we don't have the owner here - maybe parse from existing
1387
        # cache? for now, we only lose lvm data when we rename, which
1388
        # is less critical than DRBD or MD
1389
    except errors.BlockDeviceError, err:
1390
      logger.Error("Can't rename device '%s' to '%s': %s" %
1391
                   (dev, unique_id, err))
1392
      result = False
1393
  return result
1394

    
1395

    
1396
class HooksRunner(object):
1397
  """Hook runner.
1398

1399
  This class is instantiated on the node side (ganeti-noded) and not on
1400
  the master side.
1401

1402
  """
1403
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1404

    
1405
  def __init__(self, hooks_base_dir=None):
1406
    """Constructor for hooks runner.
1407

1408
    Args:
1409
      - hooks_base_dir: if not None, this overrides the
1410
        constants.HOOKS_BASE_DIR (useful for unittests)
1411
      - logs_base_dir: if not None, this overrides the
1412
        constants.LOG_HOOKS_DIR (useful for unittests)
1413
      - logging: enable or disable logging of script output
1414

1415
    """
1416
    if hooks_base_dir is None:
1417
      hooks_base_dir = constants.HOOKS_BASE_DIR
1418
    self._BASE_DIR = hooks_base_dir
1419

    
1420
  @staticmethod
1421
  def ExecHook(script, env):
1422
    """Exec one hook script.
1423

1424
    Args:
1425
     - phase: the phase
1426
     - script: the full path to the script
1427
     - env: the environment with which to exec the script
1428

1429
    """
1430
    # exec the process using subprocess and log the output
1431
    fdstdin = None
1432
    try:
1433
      fdstdin = open("/dev/null", "r")
1434
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1435
                               stderr=subprocess.STDOUT, close_fds=True,
1436
                               shell=False, cwd="/",env=env)
1437
      output = ""
1438
      try:
1439
        output = child.stdout.read(4096)
1440
        child.stdout.close()
1441
      except EnvironmentError, err:
1442
        output += "Hook script error: %s" % str(err)
1443

    
1444
      while True:
1445
        try:
1446
          result = child.wait()
1447
          break
1448
        except EnvironmentError, err:
1449
          if err.errno == errno.EINTR:
1450
            continue
1451
          raise
1452
    finally:
1453
      # try not to leak fds
1454
      for fd in (fdstdin, ):
1455
        if fd is not None:
1456
          try:
1457
            fd.close()
1458
          except EnvironmentError, err:
1459
            # just log the error
1460
            #logger.Error("While closing fd %s: %s" % (fd, err))
1461
            pass
1462

    
1463
    return result == 0, output
1464

    
1465
  def RunHooks(self, hpath, phase, env):
1466
    """Run the scripts in the hooks directory.
1467

1468
    This method will not be usually overriden by child opcodes.
1469

1470
    """
1471
    if phase == constants.HOOKS_PHASE_PRE:
1472
      suffix = "pre"
1473
    elif phase == constants.HOOKS_PHASE_POST:
1474
      suffix = "post"
1475
    else:
1476
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1477
    rr = []
1478

    
1479
    subdir = "%s-%s.d" % (hpath, suffix)
1480
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1481
    try:
1482
      dir_contents = utils.ListVisibleFiles(dir_name)
1483
    except OSError, err:
1484
      # must log
1485
      return rr
1486

    
1487
    # we use the standard python sort order,
1488
    # so 00name is the recommended naming scheme
1489
    dir_contents.sort()
1490
    for relname in dir_contents:
1491
      fname = os.path.join(dir_name, relname)
1492
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1493
          self.RE_MASK.match(relname) is not None):
1494
        rrval = constants.HKR_SKIP
1495
        output = ""
1496
      else:
1497
        result, output = self.ExecHook(fname, env)
1498
        if not result:
1499
          rrval = constants.HKR_FAIL
1500
        else:
1501
          rrval = constants.HKR_SUCCESS
1502
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1503

    
1504
    return rr
1505

    
1506

    
1507
class DevCacheManager(object):
1508
  """Simple class for managing a chache of block device information.
1509

1510
  """
1511
  _DEV_PREFIX = "/dev/"
1512
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1513

    
1514
  @classmethod
1515
  def _ConvertPath(cls, dev_path):
1516
    """Converts a /dev/name path to the cache file name.
1517

1518
    This replaces slashes with underscores and strips the /dev
1519
    prefix. It then returns the full path to the cache file
1520

1521
    """
1522
    if dev_path.startswith(cls._DEV_PREFIX):
1523
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1524
    dev_path = dev_path.replace("/", "_")
1525
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1526
    return fpath
1527

    
1528
  @classmethod
1529
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1530
    """Updates the cache information for a given device.
1531

1532
    """
1533
    if dev_path is None:
1534
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1535
      return
1536
    fpath = cls._ConvertPath(dev_path)
1537
    if on_primary:
1538
      state = "primary"
1539
    else:
1540
      state = "secondary"
1541
    if iv_name is None:
1542
      iv_name = "not_visible"
1543
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1544
    try:
1545
      utils.WriteFile(fpath, data=fdata)
1546
    except EnvironmentError, err:
1547
      logger.Error("Can't update bdev cache for %s, error %s" %
1548
                   (dev_path, str(err)))
1549

    
1550
  @classmethod
1551
  def RemoveCache(cls, dev_path):
1552
    """Remove data for a dev_path.
1553

1554
    """
1555
    if dev_path is None:
1556
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1557
      return
1558
    fpath = cls._ConvertPath(dev_path)
1559
    try:
1560
      utils.RemoveFile(fpath)
1561
    except EnvironmentError, err:
1562
      logger.Error("Can't update bdev cache for %s, error %s" %
1563
                   (dev_path, str(err)))