Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ fc1dc9d7

History | View | Annotate | Download (44.5 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
    mcn = disk.ChildrenNeeded()
726
    if mcn == -1:
727
      mcn = 0 # max number of Nones allowed
728
    else:
729
      mcn = len(disk.children) - mcn # max number of Nones
730
    for chld_disk in disk.children:
731
      try:
732
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
733
      except errors.BlockDeviceError, err:
734
        if children.count(None) > mcn:
735
          raise
736
        cdev = None
737
        logger.Debug("Error in child activation: %s" % str(err))
738
      children.append(cdev)
739

    
740
  if as_primary or disk.AssembleOnSecondary():
741
    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
742
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
743
    result = r_dev
744
    if as_primary or disk.OpenOnSecondary():
745
      r_dev.Open()
746
    else:
747
      r_dev.Close()
748
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
749
                                as_primary, disk.iv_name)
750

    
751
  else:
752
    result = True
753
  return result
754

    
755

    
756
def AssembleBlockDevice(disk, owner, as_primary):
757
  """Activate a block device for an instance.
758

759
  This is a wrapper over _RecursiveAssembleBD.
760

761
  Returns:
762
    a /dev path for primary nodes
763
    True for secondary nodes
764

765
  """
766
  result = _RecursiveAssembleBD(disk, owner, as_primary)
767
  if isinstance(result, bdev.BlockDev):
768
    result = result.dev_path
769
  return result
770

    
771

    
772
def ShutdownBlockDevice(disk):
773
  """Shut down a block device.
774

775
  First, if the device is assembled (can `Attach()`), then the device
776
  is shutdown. Then the children of the device are shutdown.
777

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

782
  """
783
  r_dev = _RecursiveFindBD(disk)
784
  if r_dev is not None:
785
    r_path = r_dev.dev_path
786
    result = r_dev.Shutdown()
787
    if result:
788
      DevCacheManager.RemoveCache(r_path)
789
  else:
790
    result = True
791
  if disk.children:
792
    for child in disk.children:
793
      result = result and ShutdownBlockDevice(child)
794
  return result
795

    
796

    
797
def MirrorAddChildren(parent_cdev, new_cdevs):
798
  """Extend a mirrored block device.
799

800
  """
801
  parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
802
  if parent_bdev is None:
803
    logger.Error("Can't find parent device")
804
    return False
805
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
806
  if new_bdevs.count(None) > 0:
807
    logger.Error("Can't find new device(s) to add: %s:%s" %
808
                 (new_bdevs, new_cdevs))
809
    return False
810
  parent_bdev.AddChildren(new_bdevs)
811
  return True
812

    
813

    
814
def MirrorRemoveChildren(parent_cdev, new_cdevs):
815
  """Shrink a mirrored block device.
816

817
  """
818
  parent_bdev = _RecursiveFindBD(parent_cdev)
819
  if parent_bdev is None:
820
    logger.Error("Can't find parent in remove children: %s" % parent_cdev)
821
    return False
822
  devs = []
823
  for disk in new_cdevs:
824
    rpath = disk.StaticDevPath()
825
    if rpath is None:
826
      bd = _RecursiveFindBD(disk)
827
      if bd is None:
828
        logger.Error("Can't find dynamic device %s while removing children" %
829
                     disk)
830
        return False
831
      else:
832
        devs.append(bd.dev_path)
833
    else:
834
      devs.append(rpath)
835
  parent_bdev.RemoveChildren(devs)
836
  return True
837

    
838

    
839
def GetMirrorStatus(disks):
840
  """Get the mirroring status of a list of devices.
841

842
  Args:
843
    disks: list of `objects.Disk`
844

845
  Returns:
846
    list of (mirror_done, estimated_time) tuples, which
847
    are the result of bdev.BlockDevice.CombinedSyncStatus()
848

849
  """
850
  stats = []
851
  for dsk in disks:
852
    rbd = _RecursiveFindBD(dsk)
853
    if rbd is None:
854
      raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
855
    stats.append(rbd.CombinedSyncStatus())
856
  return stats
857

    
858

    
859
def _RecursiveFindBD(disk, allow_partial=False):
860
  """Check if a device is activated.
861

862
  If so, return informations about the real device.
863

864
  Args:
865
    disk: the objects.Disk instance
866
    allow_partial: don't abort the find if a child of the
867
                   device can't be found; this is intended to be
868
                   used when repairing mirrors
869

870
  Returns:
871
    None if the device can't be found
872
    otherwise the device instance
873

874
  """
875
  children = []
876
  if disk.children:
877
    for chdisk in disk.children:
878
      children.append(_RecursiveFindBD(chdisk))
879

    
880
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
881

    
882

    
883
def FindBlockDevice(disk):
884
  """Check if a device is activated.
885

886
  If so, return informations about the real device.
887

888
  Args:
889
    disk: the objects.Disk instance
890
  Returns:
891
    None if the device can't be found
892
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
893

894
  """
895
  rbd = _RecursiveFindBD(disk)
896
  if rbd is None:
897
    return rbd
898
  sync_p, est_t, is_degr = rbd.GetSyncStatus()
899
  return rbd.dev_path, rbd.major, rbd.minor, sync_p, est_t, is_degr
900

    
901

    
902
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
903
  """Write a file to the filesystem.
904

905
  This allows the master to overwrite(!) a file. It will only perform
906
  the operation if the file belongs to a list of configuration files.
907

908
  """
909
  if not os.path.isabs(file_name):
910
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
911
                 file_name)
912
    return False
913

    
914
  allowed_files = [constants.CLUSTER_CONF_FILE, "/etc/hosts",
915
                   constants.SSH_KNOWN_HOSTS_FILE]
916
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
917
  if file_name not in allowed_files:
918
    logger.Error("Filename passed to UploadFile not in allowed"
919
                 " upload targets: '%s'" % file_name)
920
    return False
921

    
922
  dir_name, small_name = os.path.split(file_name)
923
  fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
924
  # here we need to make sure we remove the temp file, if any error
925
  # leaves it in place
926
  try:
927
    os.chown(new_name, uid, gid)
928
    os.chmod(new_name, mode)
929
    os.write(fd, data)
930
    os.fsync(fd)
931
    os.utime(new_name, (atime, mtime))
932
    os.rename(new_name, file_name)
933
  finally:
934
    os.close(fd)
935
    utils.RemoveFile(new_name)
936
  return True
937

    
938

    
939
def _ErrnoOrStr(err):
940
  """Format an EnvironmentError exception.
941

942
  If the `err` argument has an errno attribute, it will be looked up
943
  and converted into a textual EXXXX description. Otherwise the string
944
  representation of the error will be returned.
945

946
  """
947
  if hasattr(err, 'errno'):
948
    detail = errno.errorcode[err.errno]
949
  else:
950
    detail = str(err)
951
  return detail
952

    
953

    
954
def _OSSearch(name, search_path=None):
955
  """Search for OSes with the given name in the search_path.
956

957
  Args:
958
    name: The name of the OS to look for
959
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
960

961
  Returns:
962
    The base_dir the OS resides in
963

964
  """
965
  if search_path is None:
966
    search_path = constants.OS_SEARCH_PATH
967

    
968
  for dir_name in search_path:
969
    t_os_dir = os.path.sep.join([dir_name, name])
970
    if os.path.isdir(t_os_dir):
971
      return dir_name
972

    
973
  return None
974

    
975

    
976
def _OSOndiskVersion(name, os_dir):
977
  """Compute and return the API version of a given OS.
978

979
  This function will try to read the API version of the os given by
980
  the 'name' parameter and residing in the 'os_dir' directory.
981

982
  Return value will be either an integer denoting the version or None in the
983
  case when this is not a valid OS name.
984

985
  """
986
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
987

    
988
  try:
989
    st = os.stat(api_file)
990
  except EnvironmentError, err:
991
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
992
                           " found (%s)" % _ErrnoOrStr(err))
993

    
994
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
995
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
996
                           " a regular file")
997

    
998
  try:
999
    f = open(api_file)
1000
    try:
1001
      api_version = f.read(256)
1002
    finally:
1003
      f.close()
1004
  except EnvironmentError, err:
1005
    raise errors.InvalidOS(name, os_dir, "error while reading the"
1006
                           " API version (%s)" % _ErrnoOrStr(err))
1007

    
1008
  api_version = api_version.strip()
1009
  try:
1010
    api_version = int(api_version)
1011
  except (TypeError, ValueError), err:
1012
    raise errors.InvalidOS(name, os_dir,
1013
                           "API version is not integer (%s)" % str(err))
1014

    
1015
  return api_version
1016

    
1017

    
1018
def DiagnoseOS(top_dirs=None):
1019
  """Compute the validity for all OSes.
1020

1021
  Returns an OS object for each name in all the given top directories
1022
  (if not given defaults to constants.OS_SEARCH_PATH)
1023

1024
  Returns:
1025
    list of OS objects
1026

1027
  """
1028
  if top_dirs is None:
1029
    top_dirs = constants.OS_SEARCH_PATH
1030

    
1031
  result = []
1032
  for dir_name in top_dirs:
1033
    if os.path.isdir(dir_name):
1034
      try:
1035
        f_names = utils.ListVisibleFiles(dir_name)
1036
      except EnvironmentError, err:
1037
        logger.Error("Can't list the OS directory %s: %s" %
1038
                     (dir_name, str(err)))
1039
        break
1040
      for name in f_names:
1041
        try:
1042
          os_inst = OSFromDisk(name, base_dir=dir_name)
1043
          result.append(os_inst)
1044
        except errors.InvalidOS, err:
1045
          result.append(objects.OS.FromInvalidOS(err))
1046

    
1047
  return result
1048

    
1049

    
1050
def OSFromDisk(name, base_dir=None):
1051
  """Create an OS instance from disk.
1052

1053
  This function will return an OS instance if the given name is a
1054
  valid OS name. Otherwise, it will raise an appropriate
1055
  `errors.InvalidOS` exception, detailing why this is not a valid
1056
  OS.
1057

1058
  Args:
1059
    os_dir: Directory containing the OS scripts. Defaults to a search
1060
            in all the OS_SEARCH_PATH directories.
1061

1062
  """
1063

    
1064
  if base_dir is None:
1065
    base_dir = _OSSearch(name)
1066

    
1067
  if base_dir is None:
1068
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1069

    
1070
  os_dir = os.path.sep.join([base_dir, name])
1071
  api_version = _OSOndiskVersion(name, os_dir)
1072

    
1073
  if api_version != constants.OS_API_VERSION:
1074
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1075
                           " (found %s want %s)"
1076
                           % (api_version, constants.OS_API_VERSION))
1077

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

    
1081
  for script in os_scripts:
1082
    os_scripts[script] = os.path.sep.join([os_dir, script])
1083

    
1084
    try:
1085
      st = os.stat(os_scripts[script])
1086
    except EnvironmentError, err:
1087
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1088
                             (script, _ErrnoOrStr(err)))
1089

    
1090
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1091
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1092
                             script)
1093

    
1094
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1095
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1096
                             script)
1097

    
1098

    
1099
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1100
                    create_script=os_scripts['create'],
1101
                    export_script=os_scripts['export'],
1102
                    import_script=os_scripts['import'],
1103
                    rename_script=os_scripts['rename'],
1104
                    api_version=api_version)
1105

    
1106

    
1107
def SnapshotBlockDevice(disk):
1108
  """Create a snapshot copy of a block device.
1109

1110
  This function is called recursively, and the snapshot is actually created
1111
  just for the leaf lvm backend device.
1112

1113
  Args:
1114
    disk: the disk to be snapshotted
1115

1116
  Returns:
1117
    a config entry for the actual lvm device snapshotted.
1118

1119
  """
1120
  if disk.children:
1121
    if len(disk.children) == 1:
1122
      # only one child, let's recurse on it
1123
      return SnapshotBlockDevice(disk.children[0])
1124
    else:
1125
      # more than one child, choose one that matches
1126
      for child in disk.children:
1127
        if child.size == disk.size:
1128
          # return implies breaking the loop
1129
          return SnapshotBlockDevice(child)
1130
  elif disk.dev_type == constants.LD_LV:
1131
    r_dev = _RecursiveFindBD(disk)
1132
    if r_dev is not None:
1133
      # let's stay on the safe side and ask for the full size, for now
1134
      return r_dev.Snapshot(disk.size)
1135
    else:
1136
      return None
1137
  else:
1138
    raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1139
                                 "'%s' of type '%s'" %
1140
                                 (disk.unique_id, disk.dev_type))
1141

    
1142

    
1143
def ExportSnapshot(disk, dest_node, instance):
1144
  """Export a block device snapshot to a remote node.
1145

1146
  Args:
1147
    disk: the snapshot block device
1148
    dest_node: the node to send the image to
1149
    instance: instance being exported
1150

1151
  Returns:
1152
    True if successful, False otherwise.
1153

1154
  """
1155
  inst_os = OSFromDisk(instance.os)
1156
  export_script = inst_os.export_script
1157

    
1158
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1159
                                     instance.name, int(time.time()))
1160
  if not os.path.exists(constants.LOG_OS_DIR):
1161
    os.mkdir(constants.LOG_OS_DIR, 0750)
1162

    
1163
  real_os_dev = _RecursiveFindBD(disk)
1164
  if real_os_dev is None:
1165
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1166
                                  str(disk))
1167
  real_os_dev.Open()
1168

    
1169
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1170
  destfile = disk.physical_id[1]
1171

    
1172
  # the target command is built out of three individual commands,
1173
  # which are joined by pipes; we check each individual command for
1174
  # valid parameters
1175

    
1176
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1177
                               export_script, instance.name,
1178
                               real_os_dev.dev_path, logfile)
1179

    
1180
  comprcmd = "gzip"
1181

    
1182
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1183
                                destdir, destdir, destfile)
1184
  remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1185

    
1186

    
1187

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

    
1191
  result = utils.RunCmd(command)
1192

    
1193
  if result.failed:
1194
    logger.Error("os snapshot export command '%s' returned error: %s"
1195
                 " output: %s" %
1196
                 (command, result.fail_reason, result.output))
1197
    return False
1198

    
1199
  return True
1200

    
1201

    
1202
def FinalizeExport(instance, snap_disks):
1203
  """Write out the export configuration information.
1204

1205
  Args:
1206
    instance: instance configuration
1207
    snap_disks: snapshot block devices
1208

1209
  Returns:
1210
    False in case of error, True otherwise.
1211

1212
  """
1213
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1214
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1215

    
1216
  config = objects.SerializableConfigParser()
1217

    
1218
  config.add_section(constants.INISECT_EXP)
1219
  config.set(constants.INISECT_EXP, 'version', '0')
1220
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1221
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1222
  config.set(constants.INISECT_EXP, 'os', instance.os)
1223
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1224

    
1225
  config.add_section(constants.INISECT_INS)
1226
  config.set(constants.INISECT_INS, 'name', instance.name)
1227
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1228
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1229
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1230
  for nic_count, nic in enumerate(instance.nics):
1231
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1232
               nic_count, '%s' % nic.mac)
1233
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1234
  # TODO: redundant: on load can read nics until it doesn't exist
1235
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1236

    
1237
  for disk_count, disk in enumerate(snap_disks):
1238
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1239
               ('%s' % disk.iv_name))
1240
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1241
               ('%s' % disk.physical_id[1]))
1242
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1243
               ('%d' % disk.size))
1244
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1245

    
1246
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1247
  cfo = open(cff, 'w')
1248
  try:
1249
    config.write(cfo)
1250
  finally:
1251
    cfo.close()
1252

    
1253
  shutil.rmtree(finaldestdir, True)
1254
  shutil.move(destdir, finaldestdir)
1255

    
1256
  return True
1257

    
1258

    
1259
def ExportInfo(dest):
1260
  """Get export configuration information.
1261

1262
  Args:
1263
    dest: directory containing the export
1264

1265
  Returns:
1266
    A serializable config file containing the export info.
1267

1268
  """
1269
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1270

    
1271
  config = objects.SerializableConfigParser()
1272
  config.read(cff)
1273

    
1274
  if (not config.has_section(constants.INISECT_EXP) or
1275
      not config.has_section(constants.INISECT_INS)):
1276
    return None
1277

    
1278
  return config
1279

    
1280

    
1281
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1282
  """Import an os image into an instance.
1283

1284
  Args:
1285
    instance: the instance object
1286
    os_disk: the instance-visible name of the os device
1287
    swap_disk: the instance-visible name of the swap device
1288
    src_node: node holding the source image
1289
    src_image: path to the source image on src_node
1290

1291
  Returns:
1292
    False in case of error, True otherwise.
1293

1294
  """
1295
  inst_os = OSFromDisk(instance.os)
1296
  import_script = inst_os.import_script
1297

    
1298
  os_device = instance.FindDisk(os_disk)
1299
  if os_device is None:
1300
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1301
    return False
1302

    
1303
  swap_device = instance.FindDisk(swap_disk)
1304
  if swap_device is None:
1305
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1306
    return False
1307

    
1308
  real_os_dev = _RecursiveFindBD(os_device)
1309
  if real_os_dev is None:
1310
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1311
                                  str(os_device))
1312
  real_os_dev.Open()
1313

    
1314
  real_swap_dev = _RecursiveFindBD(swap_device)
1315
  if real_swap_dev is None:
1316
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1317
                                  str(swap_device))
1318
  real_swap_dev.Open()
1319

    
1320
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1321
                                        instance.name, int(time.time()))
1322
  if not os.path.exists(constants.LOG_OS_DIR):
1323
    os.mkdir(constants.LOG_OS_DIR, 0750)
1324

    
1325
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1326
  remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1327

    
1328
  comprcmd = "gunzip"
1329
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1330
                               inst_os.path, import_script, instance.name,
1331
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1332
                               logfile)
1333

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

    
1336
  result = utils.RunCmd(command)
1337

    
1338
  if result.failed:
1339
    logger.Error("os import command '%s' returned error: %s"
1340
                 " output: %s" %
1341
                 (command, result.fail_reason, result.output))
1342
    return False
1343

    
1344
  return True
1345

    
1346

    
1347
def ListExports():
1348
  """Return a list of exports currently available on this machine.
1349

1350
  """
1351
  if os.path.isdir(constants.EXPORT_DIR):
1352
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1353
  else:
1354
    return []
1355

    
1356

    
1357
def RemoveExport(export):
1358
  """Remove an existing export from the node.
1359

1360
  Args:
1361
    export: the name of the export to remove
1362

1363
  Returns:
1364
    False in case of error, True otherwise.
1365

1366
  """
1367
  target = os.path.join(constants.EXPORT_DIR, export)
1368

    
1369
  shutil.rmtree(target)
1370
  # TODO: catch some of the relevant exceptions and provide a pretty
1371
  # error message if rmtree fails.
1372

    
1373
  return True
1374

    
1375

    
1376
def RenameBlockDevices(devlist):
1377
  """Rename a list of block devices.
1378

1379
  The devlist argument is a list of tuples (disk, new_logical,
1380
  new_physical). The return value will be a combined boolean result
1381
  (True only if all renames succeeded).
1382

1383
  """
1384
  result = True
1385
  for disk, unique_id in devlist:
1386
    dev = _RecursiveFindBD(disk)
1387
    if dev is None:
1388
      result = False
1389
      continue
1390
    try:
1391
      old_rpath = dev.dev_path
1392
      dev.Rename(unique_id)
1393
      new_rpath = dev.dev_path
1394
      if old_rpath != new_rpath:
1395
        DevCacheManager.RemoveCache(old_rpath)
1396
        # FIXME: we should add the new cache information here, like:
1397
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1398
        # but we don't have the owner here - maybe parse from existing
1399
        # cache? for now, we only lose lvm data when we rename, which
1400
        # is less critical than DRBD or MD
1401
    except errors.BlockDeviceError, err:
1402
      logger.Error("Can't rename device '%s' to '%s': %s" %
1403
                   (dev, unique_id, err))
1404
      result = False
1405
  return result
1406

    
1407

    
1408
class HooksRunner(object):
1409
  """Hook runner.
1410

1411
  This class is instantiated on the node side (ganeti-noded) and not on
1412
  the master side.
1413

1414
  """
1415
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1416

    
1417
  def __init__(self, hooks_base_dir=None):
1418
    """Constructor for hooks runner.
1419

1420
    Args:
1421
      - hooks_base_dir: if not None, this overrides the
1422
        constants.HOOKS_BASE_DIR (useful for unittests)
1423
      - logs_base_dir: if not None, this overrides the
1424
        constants.LOG_HOOKS_DIR (useful for unittests)
1425
      - logging: enable or disable logging of script output
1426

1427
    """
1428
    if hooks_base_dir is None:
1429
      hooks_base_dir = constants.HOOKS_BASE_DIR
1430
    self._BASE_DIR = hooks_base_dir
1431

    
1432
  @staticmethod
1433
  def ExecHook(script, env):
1434
    """Exec one hook script.
1435

1436
    Args:
1437
     - phase: the phase
1438
     - script: the full path to the script
1439
     - env: the environment with which to exec the script
1440

1441
    """
1442
    # exec the process using subprocess and log the output
1443
    fdstdin = None
1444
    try:
1445
      fdstdin = open("/dev/null", "r")
1446
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1447
                               stderr=subprocess.STDOUT, close_fds=True,
1448
                               shell=False, cwd="/",env=env)
1449
      output = ""
1450
      try:
1451
        output = child.stdout.read(4096)
1452
        child.stdout.close()
1453
      except EnvironmentError, err:
1454
        output += "Hook script error: %s" % str(err)
1455

    
1456
      while True:
1457
        try:
1458
          result = child.wait()
1459
          break
1460
        except EnvironmentError, err:
1461
          if err.errno == errno.EINTR:
1462
            continue
1463
          raise
1464
    finally:
1465
      # try not to leak fds
1466
      for fd in (fdstdin, ):
1467
        if fd is not None:
1468
          try:
1469
            fd.close()
1470
          except EnvironmentError, err:
1471
            # just log the error
1472
            #logger.Error("While closing fd %s: %s" % (fd, err))
1473
            pass
1474

    
1475
    return result == 0, output
1476

    
1477
  def RunHooks(self, hpath, phase, env):
1478
    """Run the scripts in the hooks directory.
1479

1480
    This method will not be usually overriden by child opcodes.
1481

1482
    """
1483
    if phase == constants.HOOKS_PHASE_PRE:
1484
      suffix = "pre"
1485
    elif phase == constants.HOOKS_PHASE_POST:
1486
      suffix = "post"
1487
    else:
1488
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1489
    rr = []
1490

    
1491
    subdir = "%s-%s.d" % (hpath, suffix)
1492
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1493
    try:
1494
      dir_contents = utils.ListVisibleFiles(dir_name)
1495
    except OSError, err:
1496
      # must log
1497
      return rr
1498

    
1499
    # we use the standard python sort order,
1500
    # so 00name is the recommended naming scheme
1501
    dir_contents.sort()
1502
    for relname in dir_contents:
1503
      fname = os.path.join(dir_name, relname)
1504
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1505
          self.RE_MASK.match(relname) is not None):
1506
        rrval = constants.HKR_SKIP
1507
        output = ""
1508
      else:
1509
        result, output = self.ExecHook(fname, env)
1510
        if not result:
1511
          rrval = constants.HKR_FAIL
1512
        else:
1513
          rrval = constants.HKR_SUCCESS
1514
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1515

    
1516
    return rr
1517

    
1518

    
1519
class DevCacheManager(object):
1520
  """Simple class for managing a chache of block device information.
1521

1522
  """
1523
  _DEV_PREFIX = "/dev/"
1524
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1525

    
1526
  @classmethod
1527
  def _ConvertPath(cls, dev_path):
1528
    """Converts a /dev/name path to the cache file name.
1529

1530
    This replaces slashes with underscores and strips the /dev
1531
    prefix. It then returns the full path to the cache file
1532

1533
    """
1534
    if dev_path.startswith(cls._DEV_PREFIX):
1535
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1536
    dev_path = dev_path.replace("/", "_")
1537
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1538
    return fpath
1539

    
1540
  @classmethod
1541
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1542
    """Updates the cache information for a given device.
1543

1544
    """
1545
    if dev_path is None:
1546
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1547
      return
1548
    fpath = cls._ConvertPath(dev_path)
1549
    if on_primary:
1550
      state = "primary"
1551
    else:
1552
      state = "secondary"
1553
    if iv_name is None:
1554
      iv_name = "not_visible"
1555
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1556
    try:
1557
      utils.WriteFile(fpath, data=fdata)
1558
    except EnvironmentError, err:
1559
      logger.Error("Can't update bdev cache for %s, error %s" %
1560
                   (dev_path, str(err)))
1561

    
1562
  @classmethod
1563
  def RemoveCache(cls, dev_path):
1564
    """Remove data for a dev_path.
1565

1566
    """
1567
    if dev_path is None:
1568
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1569
      return
1570
    fpath = cls._ConvertPath(dev_path)
1571
    try:
1572
      utils.RemoveFile(fpath)
1573
    except EnvironmentError, err:
1574
      logger.Error("Can't update bdev cache for %s, error %s" %
1575
                   (dev_path, str(err)))