Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 41a57aab

History | View | Annotate | Download (44.9 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 stat
30
import errno
31
import re
32
import subprocess
33

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

    
44

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

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

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

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

    
60
  return True
61

    
62

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

66
  This runs the master stop script.
67

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

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

    
76
  return True
77

    
78

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

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

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

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

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

    
105
  utils.AddAuthorizedKey(auth_keys, sshpub)
106

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

    
109
  return True
110

    
111

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

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

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

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

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

    
137

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

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

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

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

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

    
168
  return outputarray
169

    
170

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

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

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

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

188
  """
189
  result = {}
190

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

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

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

    
205

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

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

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

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

    
234
  return lvs
235

    
236

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

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

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

    
246

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

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

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

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

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

    
275

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

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

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

    
287
  return True
288

    
289

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

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

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

    
305
  return names
306

    
307

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

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

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

321
  """
322
  output = {}
323

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

    
330
  return output
331

    
332

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

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

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

349
  """
350
  output = {}
351

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

    
362
  return output
363

    
364

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

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

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

    
376
  create_script = inst_os.create_script
377

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

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

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

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

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

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

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

    
417
  return True
418

    
419

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

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

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

    
432
  script = inst_os.rename_script
433

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

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

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

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

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

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

    
467
  result = utils.RunCmd(command)
468

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

    
475
  return True
476

    
477

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

481
  Args:
482
    vg_name: the volume group
483

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

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

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

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

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

    
519

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

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

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

    
537

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

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

544
  """
545
  running_instances = GetInstanceList()
546

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

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

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

    
559
  return True
560

    
561

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

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

568
  """
569
  running_instances = GetInstanceList()
570

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

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

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

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

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

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

    
604
  return True
605

    
606

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

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

614
  """
615
  running_instances = GetInstanceList()
616

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

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

    
638

    
639
  return True
640

    
641

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

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

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

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

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

    
692
  device.SetInfo(info)
693

    
694
  physical_id = device.unique_id
695
  return physical_id
696

    
697

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

701
  This is intended to be called recursively.
702

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

    
724

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

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

730
  This function is called recursively.
731

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

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

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

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

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

    
768
  else:
769
    result = True
770
  return result
771

    
772

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

776
  This is a wrapper over _RecursiveAssembleBD.
777

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

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

    
788

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

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

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

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

    
813

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

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

    
830

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

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

    
855

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

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

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

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

    
875

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

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

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

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

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

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

    
899

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

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

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

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

    
917

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

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

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

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

    
941
  utils.WriteFile(file_name, data=data, mode=mode, uid=uid, gid=gid,
942
                  atime=atime, mtime=mtime)
943
  return True
944

    
945

    
946
def _ErrnoOrStr(err):
947
  """Format an EnvironmentError exception.
948

949
  If the `err` argument has an errno attribute, it will be looked up
950
  and converted into a textual EXXXX description. Otherwise the string
951
  representation of the error will be returned.
952

953
  """
954
  if hasattr(err, 'errno'):
955
    detail = errno.errorcode[err.errno]
956
  else:
957
    detail = str(err)
958
  return detail
959

    
960

    
961
def _OSSearch(name, search_path=None):
962
  """Search for OSes with the given name in the search_path.
963

964
  Args:
965
    name: The name of the OS to look for
966
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
967

968
  Returns:
969
    The base_dir the OS resides in
970

971
  """
972
  if search_path is None:
973
    search_path = constants.OS_SEARCH_PATH
974

    
975
  for dir_name in search_path:
976
    t_os_dir = os.path.sep.join([dir_name, name])
977
    if os.path.isdir(t_os_dir):
978
      return dir_name
979

    
980
  return None
981

    
982

    
983
def _OSOndiskVersion(name, os_dir):
984
  """Compute and return the API version of a given OS.
985

986
  This function will try to read the API version of the os given by
987
  the 'name' parameter and residing in the 'os_dir' directory.
988

989
  Return value will be either an integer denoting the version or None in the
990
  case when this is not a valid OS name.
991

992
  """
993
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
994

    
995
  try:
996
    st = os.stat(api_file)
997
  except EnvironmentError, err:
998
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
999
                           " found (%s)" % _ErrnoOrStr(err))
1000

    
1001
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1002
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
1003
                           " a regular file")
1004

    
1005
  try:
1006
    f = open(api_file)
1007
    try:
1008
      api_version = f.read(256)
1009
    finally:
1010
      f.close()
1011
  except EnvironmentError, err:
1012
    raise errors.InvalidOS(name, os_dir, "error while reading the"
1013
                           " API version (%s)" % _ErrnoOrStr(err))
1014

    
1015
  api_version = api_version.strip()
1016
  try:
1017
    api_version = int(api_version)
1018
  except (TypeError, ValueError), err:
1019
    raise errors.InvalidOS(name, os_dir,
1020
                           "API version is not integer (%s)" % str(err))
1021

    
1022
  return api_version
1023

    
1024

    
1025
def DiagnoseOS(top_dirs=None):
1026
  """Compute the validity for all OSes.
1027

1028
  Returns an OS object for each name in all the given top directories
1029
  (if not given defaults to constants.OS_SEARCH_PATH)
1030

1031
  Returns:
1032
    list of OS objects
1033

1034
  """
1035
  if top_dirs is None:
1036
    top_dirs = constants.OS_SEARCH_PATH
1037

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

    
1054
  return result
1055

    
1056

    
1057
def OSFromDisk(name, base_dir=None):
1058
  """Create an OS instance from disk.
1059

1060
  This function will return an OS instance if the given name is a
1061
  valid OS name. Otherwise, it will raise an appropriate
1062
  `errors.InvalidOS` exception, detailing why this is not a valid
1063
  OS.
1064

1065
  Args:
1066
    os_dir: Directory containing the OS scripts. Defaults to a search
1067
            in all the OS_SEARCH_PATH directories.
1068

1069
  """
1070

    
1071
  if base_dir is None:
1072
    base_dir = _OSSearch(name)
1073

    
1074
  if base_dir is None:
1075
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1076

    
1077
  os_dir = os.path.sep.join([base_dir, name])
1078
  api_version = _OSOndiskVersion(name, os_dir)
1079

    
1080
  if api_version != constants.OS_API_VERSION:
1081
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1082
                           " (found %s want %s)"
1083
                           % (api_version, constants.OS_API_VERSION))
1084

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

    
1088
  for script in os_scripts:
1089
    os_scripts[script] = os.path.sep.join([os_dir, script])
1090

    
1091
    try:
1092
      st = os.stat(os_scripts[script])
1093
    except EnvironmentError, err:
1094
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1095
                             (script, _ErrnoOrStr(err)))
1096

    
1097
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1098
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1099
                             script)
1100

    
1101
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1102
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1103
                             script)
1104

    
1105

    
1106
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1107
                    create_script=os_scripts['create'],
1108
                    export_script=os_scripts['export'],
1109
                    import_script=os_scripts['import'],
1110
                    rename_script=os_scripts['rename'],
1111
                    api_version=api_version)
1112

    
1113

    
1114
def SnapshotBlockDevice(disk):
1115
  """Create a snapshot copy of a block device.
1116

1117
  This function is called recursively, and the snapshot is actually created
1118
  just for the leaf lvm backend device.
1119

1120
  Args:
1121
    disk: the disk to be snapshotted
1122

1123
  Returns:
1124
    a config entry for the actual lvm device snapshotted.
1125

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

    
1149

    
1150
def ExportSnapshot(disk, dest_node, instance):
1151
  """Export a block device snapshot to a remote node.
1152

1153
  Args:
1154
    disk: the snapshot block device
1155
    dest_node: the node to send the image to
1156
    instance: instance being exported
1157

1158
  Returns:
1159
    True if successful, False otherwise.
1160

1161
  """
1162
  inst_os = OSFromDisk(instance.os)
1163
  export_script = inst_os.export_script
1164

    
1165
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1166
                                     instance.name, int(time.time()))
1167
  if not os.path.exists(constants.LOG_OS_DIR):
1168
    os.mkdir(constants.LOG_OS_DIR, 0750)
1169

    
1170
  real_os_dev = _RecursiveFindBD(disk)
1171
  if real_os_dev is None:
1172
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1173
                                  str(disk))
1174
  real_os_dev.Open()
1175

    
1176
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1177
  destfile = disk.physical_id[1]
1178

    
1179
  # the target command is built out of three individual commands,
1180
  # which are joined by pipes; we check each individual command for
1181
  # valid parameters
1182

    
1183
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1184
                               export_script, instance.name,
1185
                               real_os_dev.dev_path, logfile)
1186

    
1187
  comprcmd = "gzip"
1188

    
1189
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1190
                                destdir, destdir, destfile)
1191
  remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1192

    
1193

    
1194

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

    
1198
  result = utils.RunCmd(command)
1199

    
1200
  if result.failed:
1201
    logger.Error("os snapshot export command '%s' returned error: %s"
1202
                 " output: %s" %
1203
                 (command, result.fail_reason, result.output))
1204
    return False
1205

    
1206
  return True
1207

    
1208

    
1209
def FinalizeExport(instance, snap_disks):
1210
  """Write out the export configuration information.
1211

1212
  Args:
1213
    instance: instance configuration
1214
    snap_disks: snapshot block devices
1215

1216
  Returns:
1217
    False in case of error, True otherwise.
1218

1219
  """
1220
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1221
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1222

    
1223
  config = objects.SerializableConfigParser()
1224

    
1225
  config.add_section(constants.INISECT_EXP)
1226
  config.set(constants.INISECT_EXP, 'version', '0')
1227
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1228
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1229
  config.set(constants.INISECT_EXP, 'os', instance.os)
1230
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1231

    
1232
  config.add_section(constants.INISECT_INS)
1233
  config.set(constants.INISECT_INS, 'name', instance.name)
1234
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1235
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1236
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1237
  for nic_count, nic in enumerate(instance.nics):
1238
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1239
               nic_count, '%s' % nic.mac)
1240
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1241
    config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count, '%s' % nic.bridge)
1242
  # TODO: redundant: on load can read nics until it doesn't exist
1243
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1244

    
1245
  for disk_count, disk in enumerate(snap_disks):
1246
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1247
               ('%s' % disk.iv_name))
1248
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1249
               ('%s' % disk.physical_id[1]))
1250
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1251
               ('%d' % disk.size))
1252
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1253

    
1254
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1255
  cfo = open(cff, 'w')
1256
  try:
1257
    config.write(cfo)
1258
  finally:
1259
    cfo.close()
1260

    
1261
  shutil.rmtree(finaldestdir, True)
1262
  shutil.move(destdir, finaldestdir)
1263

    
1264
  return True
1265

    
1266

    
1267
def ExportInfo(dest):
1268
  """Get export configuration information.
1269

1270
  Args:
1271
    dest: directory containing the export
1272

1273
  Returns:
1274
    A serializable config file containing the export info.
1275

1276
  """
1277
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1278

    
1279
  config = objects.SerializableConfigParser()
1280
  config.read(cff)
1281

    
1282
  if (not config.has_section(constants.INISECT_EXP) or
1283
      not config.has_section(constants.INISECT_INS)):
1284
    return None
1285

    
1286
  return config
1287

    
1288

    
1289
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1290
  """Import an os image into an instance.
1291

1292
  Args:
1293
    instance: the instance object
1294
    os_disk: the instance-visible name of the os device
1295
    swap_disk: the instance-visible name of the swap device
1296
    src_node: node holding the source image
1297
    src_image: path to the source image on src_node
1298

1299
  Returns:
1300
    False in case of error, True otherwise.
1301

1302
  """
1303
  inst_os = OSFromDisk(instance.os)
1304
  import_script = inst_os.import_script
1305

    
1306
  os_device = instance.FindDisk(os_disk)
1307
  if os_device is None:
1308
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1309
    return False
1310

    
1311
  swap_device = instance.FindDisk(swap_disk)
1312
  if swap_device is None:
1313
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1314
    return False
1315

    
1316
  real_os_dev = _RecursiveFindBD(os_device)
1317
  if real_os_dev is None:
1318
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1319
                                  str(os_device))
1320
  real_os_dev.Open()
1321

    
1322
  real_swap_dev = _RecursiveFindBD(swap_device)
1323
  if real_swap_dev is None:
1324
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1325
                                  str(swap_device))
1326
  real_swap_dev.Open()
1327

    
1328
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1329
                                        instance.name, int(time.time()))
1330
  if not os.path.exists(constants.LOG_OS_DIR):
1331
    os.mkdir(constants.LOG_OS_DIR, 0750)
1332

    
1333
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1334
  remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1335

    
1336
  comprcmd = "gunzip"
1337
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1338
                               inst_os.path, import_script, instance.name,
1339
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1340
                               logfile)
1341

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

    
1344
  result = utils.RunCmd(command)
1345

    
1346
  if result.failed:
1347
    logger.Error("os import command '%s' returned error: %s"
1348
                 " output: %s" %
1349
                 (command, result.fail_reason, result.output))
1350
    return False
1351

    
1352
  return True
1353

    
1354

    
1355
def ListExports():
1356
  """Return a list of exports currently available on this machine.
1357

1358
  """
1359
  if os.path.isdir(constants.EXPORT_DIR):
1360
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1361
  else:
1362
    return []
1363

    
1364

    
1365
def RemoveExport(export):
1366
  """Remove an existing export from the node.
1367

1368
  Args:
1369
    export: the name of the export to remove
1370

1371
  Returns:
1372
    False in case of error, True otherwise.
1373

1374
  """
1375
  target = os.path.join(constants.EXPORT_DIR, export)
1376

    
1377
  shutil.rmtree(target)
1378
  # TODO: catch some of the relevant exceptions and provide a pretty
1379
  # error message if rmtree fails.
1380

    
1381
  return True
1382

    
1383

    
1384
def RenameBlockDevices(devlist):
1385
  """Rename a list of block devices.
1386

1387
  The devlist argument is a list of tuples (disk, new_logical,
1388
  new_physical). The return value will be a combined boolean result
1389
  (True only if all renames succeeded).
1390

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

    
1415

    
1416
class HooksRunner(object):
1417
  """Hook runner.
1418

1419
  This class is instantiated on the node side (ganeti-noded) and not on
1420
  the master side.
1421

1422
  """
1423
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1424

    
1425
  def __init__(self, hooks_base_dir=None):
1426
    """Constructor for hooks runner.
1427

1428
    Args:
1429
      - hooks_base_dir: if not None, this overrides the
1430
        constants.HOOKS_BASE_DIR (useful for unittests)
1431
      - logs_base_dir: if not None, this overrides the
1432
        constants.LOG_HOOKS_DIR (useful for unittests)
1433
      - logging: enable or disable logging of script output
1434

1435
    """
1436
    if hooks_base_dir is None:
1437
      hooks_base_dir = constants.HOOKS_BASE_DIR
1438
    self._BASE_DIR = hooks_base_dir
1439

    
1440
  @staticmethod
1441
  def ExecHook(script, env):
1442
    """Exec one hook script.
1443

1444
    Args:
1445
     - phase: the phase
1446
     - script: the full path to the script
1447
     - env: the environment with which to exec the script
1448

1449
    """
1450
    # exec the process using subprocess and log the output
1451
    fdstdin = None
1452
    try:
1453
      fdstdin = open("/dev/null", "r")
1454
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1455
                               stderr=subprocess.STDOUT, close_fds=True,
1456
                               shell=False, cwd="/", env=env)
1457
      output = ""
1458
      try:
1459
        output = child.stdout.read(4096)
1460
        child.stdout.close()
1461
      except EnvironmentError, err:
1462
        output += "Hook script error: %s" % str(err)
1463

    
1464
      while True:
1465
        try:
1466
          result = child.wait()
1467
          break
1468
        except EnvironmentError, err:
1469
          if err.errno == errno.EINTR:
1470
            continue
1471
          raise
1472
    finally:
1473
      # try not to leak fds
1474
      for fd in (fdstdin, ):
1475
        if fd is not None:
1476
          try:
1477
            fd.close()
1478
          except EnvironmentError, err:
1479
            # just log the error
1480
            #logger.Error("While closing fd %s: %s" % (fd, err))
1481
            pass
1482

    
1483
    return result == 0, output
1484

    
1485
  def RunHooks(self, hpath, phase, env):
1486
    """Run the scripts in the hooks directory.
1487

1488
    This method will not be usually overriden by child opcodes.
1489

1490
    """
1491
    if phase == constants.HOOKS_PHASE_PRE:
1492
      suffix = "pre"
1493
    elif phase == constants.HOOKS_PHASE_POST:
1494
      suffix = "post"
1495
    else:
1496
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1497
    rr = []
1498

    
1499
    subdir = "%s-%s.d" % (hpath, suffix)
1500
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1501
    try:
1502
      dir_contents = utils.ListVisibleFiles(dir_name)
1503
    except OSError, err:
1504
      # must log
1505
      return rr
1506

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

    
1524
    return rr
1525

    
1526

    
1527
class DevCacheManager(object):
1528
  """Simple class for managing a cache of block device information.
1529

1530
  """
1531
  _DEV_PREFIX = "/dev/"
1532
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1533

    
1534
  @classmethod
1535
  def _ConvertPath(cls, dev_path):
1536
    """Converts a /dev/name path to the cache file name.
1537

1538
    This replaces slashes with underscores and strips the /dev
1539
    prefix. It then returns the full path to the cache file
1540

1541
    """
1542
    if dev_path.startswith(cls._DEV_PREFIX):
1543
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1544
    dev_path = dev_path.replace("/", "_")
1545
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1546
    return fpath
1547

    
1548
  @classmethod
1549
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1550
    """Updates the cache information for a given device.
1551

1552
    """
1553
    if dev_path is None:
1554
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1555
      return
1556
    fpath = cls._ConvertPath(dev_path)
1557
    if on_primary:
1558
      state = "primary"
1559
    else:
1560
      state = "secondary"
1561
    if iv_name is None:
1562
      iv_name = "not_visible"
1563
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1564
    try:
1565
      utils.WriteFile(fpath, data=fdata)
1566
    except EnvironmentError, err:
1567
      logger.Error("Can't update bdev cache for %s, error %s" %
1568
                   (dev_path, str(err)))
1569

    
1570
  @classmethod
1571
  def RemoveCache(cls, dev_path):
1572
    """Remove data for a dev_path.
1573

1574
    """
1575
    if dev_path is None:
1576
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1577
      return
1578
    fpath = cls._ConvertPath(dev_path)
1579
    try:
1580
      utils.RemoveFile(fpath)
1581
    except EnvironmentError, err:
1582
      logger.Error("Can't update bdev cache for %s, error %s" %
1583
                   (dev_path, str(err)))