Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 97628462

History | View | Annotate | Download (44.8 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
  try:
126
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
127
  except errors.OpExecError, err:
128
    logger.Error("Error while processing ssh files: %s" % err)
129
    return
130

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

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

    
140

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

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

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

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

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

    
171
  return outputarray
172

    
173

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

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

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

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

191
  """
192
  result = {}
193

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

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

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

    
208

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

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

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

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

    
237
  return lvs
238

    
239

    
240
def ListVolumeGroups():
241
  """List the volume groups and their size.
242

243
  Returns:
244
    Dictionary with keys volume name and values the size of the volume
245

246
  """
247
  return utils.ListVolumeGroups()
248

    
249

    
250
def NodeVolumes():
251
  """List all volumes on this node.
252

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

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

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

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

    
278

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

282
  Returns:
283
    True if all of them exist, false otherwise
284

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

    
290
  return True
291

    
292

    
293
def GetInstanceList():
294
  """Provides a list of instances.
295

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

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

    
308
  return names
309

    
310

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

314
  Args:
315
    instance: name of the instance (ex. instance1.example.com)
316

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

324
  """
325
  output = {}
326

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

    
333
  return output
334

    
335

    
336
def GetAllInstancesInfo():
337
  """Gather data about all instances.
338

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

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

352
  """
353
  output = {}
354

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

    
365
  return output
366

    
367

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

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

376
  """
377
  inst_os = OSFromDisk(instance.os)
378

    
379
  create_script = inst_os.create_script
380

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

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

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

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

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

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

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

    
420
  return True
421

    
422

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

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

432
  """
433
  inst_os = OSFromDisk(instance.os)
434

    
435
  script = inst_os.rename_script
436

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

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

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

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

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

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

    
470
  result = utils.RunCmd(command)
471

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

    
478
  return True
479

    
480

    
481
def _GetVGInfo(vg_name):
482
  """Get informations about the volume group.
483

484
  Args:
485
    vg_name: the volume group
486

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

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

    
498
  if retval.failed:
499
    errmsg = "volume group %s not present" % vg_name
500
    logger.Error(errmsg)
501
    raise errors.LVMError(errmsg)
502
  valarr = retval.stdout.strip().split(':')
503
  retdic = {
504
    "vg_size": int(round(float(valarr[0]), 0)),
505
    "vg_free": int(round(float(valarr[1]), 0)),
506
    "pv_count": int(valarr[2]),
507
    }
508
  return retdic
509

    
510

    
511
def _GatherBlockDevs(instance):
512
  """Set up an instance's block device(s).
513

514
  This is run on the primary node at instance startup. The block
515
  devices must be already assembled.
516

517
  """
518
  block_devices = []
519
  for disk in instance.disks:
520
    device = _RecursiveFindBD(disk)
521
    if device is None:
522
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
523
                                    str(disk))
524
    device.Open()
525
    block_devices.append((disk, device))
526
  return block_devices
527

    
528

    
529
def StartInstance(instance, extra_args):
530
  """Start an instance.
531

532
  Args:
533
    instance - name of instance to start.
534

535
  """
536
  running_instances = GetInstanceList()
537

    
538
  if instance.name in running_instances:
539
    return True
540

    
541
  block_devices = _GatherBlockDevs(instance)
542
  hyper = hypervisor.GetHypervisor()
543

    
544
  try:
545
    hyper.StartInstance(instance, block_devices, extra_args)
546
  except errors.HypervisorError, err:
547
    logger.Error("Failed to start instance: %s" % err)
548
    return False
549

    
550
  return True
551

    
552

    
553
def ShutdownInstance(instance):
554
  """Shut an instance down.
555

556
  Args:
557
    instance - name of instance to shutdown.
558

559
  """
560
  running_instances = GetInstanceList()
561

    
562
  if instance.name not in running_instances:
563
    return True
564

    
565
  hyper = hypervisor.GetHypervisor()
566
  try:
567
    hyper.StopInstance(instance)
568
  except errors.HypervisorError, err:
569
    logger.Error("Failed to stop instance: %s" % err)
570
    return False
571

    
572
  # test every 10secs for 2min
573
  shutdown_ok = False
574

    
575
  time.sleep(1)
576
  for dummy in range(11):
577
    if instance.name not in GetInstanceList():
578
      break
579
    time.sleep(10)
580
  else:
581
    # the shutdown did not succeed
582
    logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
583

    
584
    try:
585
      hyper.StopInstance(instance, force=True)
586
    except errors.HypervisorError, err:
587
      logger.Error("Failed to stop instance: %s" % err)
588
      return False
589

    
590
    time.sleep(1)
591
    if instance.name in GetInstanceList():
592
      logger.Error("could not shutdown instance '%s' even by destroy")
593
      return False
594

    
595
  return True
596

    
597

    
598
def RebootInstance(instance, reboot_type, extra_args):
599
  """Reboot an instance.
600

601
  Args:
602
    instance    - name of instance to reboot
603
    reboot_type - how to reboot [soft,hard,full]
604

605
  """
606
  running_instances = GetInstanceList()
607

    
608
  if instance.name not in running_instances:
609
    logger.Error("Cannot reboot instance that is not running")
610
    return False
611

    
612
  hyper = hypervisor.GetHypervisor()
613
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
614
    try:
615
      hyper.RebootInstance(instance)
616
    except errors.HypervisorError, err:
617
      logger.Error("Failed to soft reboot instance: %s" % err)
618
      return False
619
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
620
    try:
621
      ShutdownInstance(instance)
622
      StartInstance(instance, extra_args)
623
    except errors.HypervisorError, err:
624
      logger.Error("Failed to hard reboot instance: %s" % err)
625
      return False
626
  else:
627
    raise errors.ParameterError("reboot_type invalid")
628

    
629

    
630
  return True
631

    
632

    
633
def CreateBlockDevice(disk, size, owner, on_primary, info):
634
  """Creates a block device for an instance.
635

636
  Args:
637
   bdev: a ganeti.objects.Disk object
638
   size: the size of the physical underlying devices
639
   do_open: if the device should be `Assemble()`-d and
640
            `Open()`-ed after creation
641

642
  Returns:
643
    the new unique_id of the device (this can sometime be
644
    computed only after creation), or None. On secondary nodes,
645
    it's not required to return anything.
646

647
  """
648
  clist = []
649
  if disk.children:
650
    for child in disk.children:
651
      crdev = _RecursiveAssembleBD(child, owner, on_primary)
652
      if on_primary or disk.AssembleOnSecondary():
653
        # we need the children open in case the device itself has to
654
        # be assembled
655
        crdev.Open()
656
      else:
657
        crdev.Close()
658
      clist.append(crdev)
659
  try:
660
    device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
661
    if device is not None:
662
      logger.Info("removing existing device %s" % disk)
663
      device.Remove()
664
  except errors.BlockDeviceError, err:
665
    pass
666

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

    
684
  device.SetInfo(info)
685

    
686
  physical_id = device.unique_id
687
  return physical_id
688

    
689

    
690
def RemoveBlockDevice(disk):
691
  """Remove a block device.
692

693
  This is intended to be called recursively.
694

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

    
716

    
717
def _RecursiveAssembleBD(disk, owner, as_primary):
718
  """Activate a block device for an instance.
719

720
  This is run on the primary and secondary nodes for an instance.
721

722
  This function is called recursively.
723

724
  Args:
725
    disk: a objects.Disk object
726
    as_primary: if we should make the block device read/write
727

728
  Returns:
729
    the assembled device or None (in case no device was assembled)
730

731
  If the assembly is not successful, an exception is raised.
732

733
  """
734
  children = []
735
  if disk.children:
736
    mcn = disk.ChildrenNeeded()
737
    if mcn == -1:
738
      mcn = 0 # max number of Nones allowed
739
    else:
740
      mcn = len(disk.children) - mcn # max number of Nones
741
    for chld_disk in disk.children:
742
      try:
743
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
744
      except errors.BlockDeviceError, err:
745
        if children.count(None) >= mcn:
746
          raise
747
        cdev = None
748
        logger.Debug("Error in child activation: %s" % str(err))
749
      children.append(cdev)
750

    
751
  if as_primary or disk.AssembleOnSecondary():
752
    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
753
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
754
    result = r_dev
755
    if as_primary or disk.OpenOnSecondary():
756
      r_dev.Open()
757
    else:
758
      r_dev.Close()
759
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
760
                                as_primary, disk.iv_name)
761

    
762
  else:
763
    result = True
764
  return result
765

    
766

    
767
def AssembleBlockDevice(disk, owner, as_primary):
768
  """Activate a block device for an instance.
769

770
  This is a wrapper over _RecursiveAssembleBD.
771

772
  Returns:
773
    a /dev path for primary nodes
774
    True for secondary nodes
775

776
  """
777
  result = _RecursiveAssembleBD(disk, owner, as_primary)
778
  if isinstance(result, bdev.BlockDev):
779
    result = result.dev_path
780
  return result
781

    
782

    
783
def ShutdownBlockDevice(disk):
784
  """Shut down a block device.
785

786
  First, if the device is assembled (can `Attach()`), then the device
787
  is shutdown. Then the children of the device are shutdown.
788

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

793
  """
794
  r_dev = _RecursiveFindBD(disk)
795
  if r_dev is not None:
796
    r_path = r_dev.dev_path
797
    result = r_dev.Shutdown()
798
    if result:
799
      DevCacheManager.RemoveCache(r_path)
800
  else:
801
    result = True
802
  if disk.children:
803
    for child in disk.children:
804
      result = result and ShutdownBlockDevice(child)
805
  return result
806

    
807

    
808
def MirrorAddChildren(parent_cdev, new_cdevs):
809
  """Extend a mirrored block device.
810

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

    
824

    
825
def MirrorRemoveChildren(parent_cdev, new_cdevs):
826
  """Shrink a mirrored block device.
827

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

    
849

    
850
def GetMirrorStatus(disks):
851
  """Get the mirroring status of a list of devices.
852

853
  Args:
854
    disks: list of `objects.Disk`
855

856
  Returns:
857
    list of (mirror_done, estimated_time) tuples, which
858
    are the result of bdev.BlockDevice.CombinedSyncStatus()
859

860
  """
861
  stats = []
862
  for dsk in disks:
863
    rbd = _RecursiveFindBD(dsk)
864
    if rbd is None:
865
      raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
866
    stats.append(rbd.CombinedSyncStatus())
867
  return stats
868

    
869

    
870
def _RecursiveFindBD(disk, allow_partial=False):
871
  """Check if a device is activated.
872

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

875
  Args:
876
    disk: the objects.Disk instance
877
    allow_partial: don't abort the find if a child of the
878
                   device can't be found; this is intended to be
879
                   used when repairing mirrors
880

881
  Returns:
882
    None if the device can't be found
883
    otherwise the device instance
884

885
  """
886
  children = []
887
  if disk.children:
888
    for chdisk in disk.children:
889
      children.append(_RecursiveFindBD(chdisk))
890

    
891
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
892

    
893

    
894
def FindBlockDevice(disk):
895
  """Check if a device is activated.
896

897
  If so, return informations about the real device.
898

899
  Args:
900
    disk: the objects.Disk instance
901
  Returns:
902
    None if the device can't be found
903
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
904

905
  """
906
  rbd = _RecursiveFindBD(disk)
907
  if rbd is None:
908
    return rbd
909
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
910

    
911

    
912
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
913
  """Write a file to the filesystem.
914

915
  This allows the master to overwrite(!) a file. It will only perform
916
  the operation if the file belongs to a list of configuration files.
917

918
  """
919
  if not os.path.isabs(file_name):
920
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
921
                 file_name)
922
    return False
923

    
924
  allowed_files = [
925
    constants.CLUSTER_CONF_FILE,
926
    constants.ETC_HOSTS,
927
    constants.SSH_KNOWN_HOSTS_FILE,
928
    ]
929
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
930
  if file_name not in allowed_files:
931
    logger.Error("Filename passed to UploadFile not in allowed"
932
                 " upload targets: '%s'" % file_name)
933
    return False
934

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

    
951

    
952
def _ErrnoOrStr(err):
953
  """Format an EnvironmentError exception.
954

955
  If the `err` argument has an errno attribute, it will be looked up
956
  and converted into a textual EXXXX description. Otherwise the string
957
  representation of the error will be returned.
958

959
  """
960
  if hasattr(err, 'errno'):
961
    detail = errno.errorcode[err.errno]
962
  else:
963
    detail = str(err)
964
  return detail
965

    
966

    
967
def _OSSearch(name, search_path=None):
968
  """Search for OSes with the given name in the search_path.
969

970
  Args:
971
    name: The name of the OS to look for
972
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
973

974
  Returns:
975
    The base_dir the OS resides in
976

977
  """
978
  if search_path is None:
979
    search_path = constants.OS_SEARCH_PATH
980

    
981
  for dir_name in search_path:
982
    t_os_dir = os.path.sep.join([dir_name, name])
983
    if os.path.isdir(t_os_dir):
984
      return dir_name
985

    
986
  return None
987

    
988

    
989
def _OSOndiskVersion(name, os_dir):
990
  """Compute and return the API version of a given OS.
991

992
  This function will try to read the API version of the os given by
993
  the 'name' parameter and residing in the 'os_dir' directory.
994

995
  Return value will be either an integer denoting the version or None in the
996
  case when this is not a valid OS name.
997

998
  """
999
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
1000

    
1001
  try:
1002
    st = os.stat(api_file)
1003
  except EnvironmentError, err:
1004
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
1005
                           " found (%s)" % _ErrnoOrStr(err))
1006

    
1007
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1008
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
1009
                           " a regular file")
1010

    
1011
  try:
1012
    f = open(api_file)
1013
    try:
1014
      api_version = f.read(256)
1015
    finally:
1016
      f.close()
1017
  except EnvironmentError, err:
1018
    raise errors.InvalidOS(name, os_dir, "error while reading the"
1019
                           " API version (%s)" % _ErrnoOrStr(err))
1020

    
1021
  api_version = api_version.strip()
1022
  try:
1023
    api_version = int(api_version)
1024
  except (TypeError, ValueError), err:
1025
    raise errors.InvalidOS(name, os_dir,
1026
                           "API version is not integer (%s)" % str(err))
1027

    
1028
  return api_version
1029

    
1030

    
1031
def DiagnoseOS(top_dirs=None):
1032
  """Compute the validity for all OSes.
1033

1034
  Returns an OS object for each name in all the given top directories
1035
  (if not given defaults to constants.OS_SEARCH_PATH)
1036

1037
  Returns:
1038
    list of OS objects
1039

1040
  """
1041
  if top_dirs is None:
1042
    top_dirs = constants.OS_SEARCH_PATH
1043

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

    
1060
  return result
1061

    
1062

    
1063
def OSFromDisk(name, base_dir=None):
1064
  """Create an OS instance from disk.
1065

1066
  This function will return an OS instance if the given name is a
1067
  valid OS name. Otherwise, it will raise an appropriate
1068
  `errors.InvalidOS` exception, detailing why this is not a valid
1069
  OS.
1070

1071
  Args:
1072
    os_dir: Directory containing the OS scripts. Defaults to a search
1073
            in all the OS_SEARCH_PATH directories.
1074

1075
  """
1076

    
1077
  if base_dir is None:
1078
    base_dir = _OSSearch(name)
1079

    
1080
  if base_dir is None:
1081
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1082

    
1083
  os_dir = os.path.sep.join([base_dir, name])
1084
  api_version = _OSOndiskVersion(name, os_dir)
1085

    
1086
  if api_version != constants.OS_API_VERSION:
1087
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1088
                           " (found %s want %s)"
1089
                           % (api_version, constants.OS_API_VERSION))
1090

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

    
1094
  for script in os_scripts:
1095
    os_scripts[script] = os.path.sep.join([os_dir, script])
1096

    
1097
    try:
1098
      st = os.stat(os_scripts[script])
1099
    except EnvironmentError, err:
1100
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1101
                             (script, _ErrnoOrStr(err)))
1102

    
1103
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1104
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1105
                             script)
1106

    
1107
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1108
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1109
                             script)
1110

    
1111

    
1112
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1113
                    create_script=os_scripts['create'],
1114
                    export_script=os_scripts['export'],
1115
                    import_script=os_scripts['import'],
1116
                    rename_script=os_scripts['rename'],
1117
                    api_version=api_version)
1118

    
1119

    
1120
def SnapshotBlockDevice(disk):
1121
  """Create a snapshot copy of a block device.
1122

1123
  This function is called recursively, and the snapshot is actually created
1124
  just for the leaf lvm backend device.
1125

1126
  Args:
1127
    disk: the disk to be snapshotted
1128

1129
  Returns:
1130
    a config entry for the actual lvm device snapshotted.
1131

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

    
1155

    
1156
def ExportSnapshot(disk, dest_node, instance):
1157
  """Export a block device snapshot to a remote node.
1158

1159
  Args:
1160
    disk: the snapshot block device
1161
    dest_node: the node to send the image to
1162
    instance: instance being exported
1163

1164
  Returns:
1165
    True if successful, False otherwise.
1166

1167
  """
1168
  inst_os = OSFromDisk(instance.os)
1169
  export_script = inst_os.export_script
1170

    
1171
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1172
                                     instance.name, int(time.time()))
1173
  if not os.path.exists(constants.LOG_OS_DIR):
1174
    os.mkdir(constants.LOG_OS_DIR, 0750)
1175

    
1176
  real_os_dev = _RecursiveFindBD(disk)
1177
  if real_os_dev is None:
1178
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1179
                                  str(disk))
1180
  real_os_dev.Open()
1181

    
1182
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1183
  destfile = disk.physical_id[1]
1184

    
1185
  # the target command is built out of three individual commands,
1186
  # which are joined by pipes; we check each individual command for
1187
  # valid parameters
1188

    
1189
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1190
                               export_script, instance.name,
1191
                               real_os_dev.dev_path, logfile)
1192

    
1193
  comprcmd = "gzip"
1194

    
1195
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1196
                                destdir, destdir, destfile)
1197
  remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1198

    
1199

    
1200

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

    
1204
  result = utils.RunCmd(command)
1205

    
1206
  if result.failed:
1207
    logger.Error("os snapshot export command '%s' returned error: %s"
1208
                 " output: %s" %
1209
                 (command, result.fail_reason, result.output))
1210
    return False
1211

    
1212
  return True
1213

    
1214

    
1215
def FinalizeExport(instance, snap_disks):
1216
  """Write out the export configuration information.
1217

1218
  Args:
1219
    instance: instance configuration
1220
    snap_disks: snapshot block devices
1221

1222
  Returns:
1223
    False in case of error, True otherwise.
1224

1225
  """
1226
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1227
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1228

    
1229
  config = objects.SerializableConfigParser()
1230

    
1231
  config.add_section(constants.INISECT_EXP)
1232
  config.set(constants.INISECT_EXP, 'version', '0')
1233
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1234
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1235
  config.set(constants.INISECT_EXP, 'os', instance.os)
1236
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1237

    
1238
  config.add_section(constants.INISECT_INS)
1239
  config.set(constants.INISECT_INS, 'name', instance.name)
1240
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1241
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1242
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1243
  for nic_count, nic in enumerate(instance.nics):
1244
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1245
               nic_count, '%s' % nic.mac)
1246
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1247
  # TODO: redundant: on load can read nics until it doesn't exist
1248
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1249

    
1250
  for disk_count, disk in enumerate(snap_disks):
1251
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1252
               ('%s' % disk.iv_name))
1253
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1254
               ('%s' % disk.physical_id[1]))
1255
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1256
               ('%d' % disk.size))
1257
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1258

    
1259
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1260
  cfo = open(cff, 'w')
1261
  try:
1262
    config.write(cfo)
1263
  finally:
1264
    cfo.close()
1265

    
1266
  shutil.rmtree(finaldestdir, True)
1267
  shutil.move(destdir, finaldestdir)
1268

    
1269
  return True
1270

    
1271

    
1272
def ExportInfo(dest):
1273
  """Get export configuration information.
1274

1275
  Args:
1276
    dest: directory containing the export
1277

1278
  Returns:
1279
    A serializable config file containing the export info.
1280

1281
  """
1282
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1283

    
1284
  config = objects.SerializableConfigParser()
1285
  config.read(cff)
1286

    
1287
  if (not config.has_section(constants.INISECT_EXP) or
1288
      not config.has_section(constants.INISECT_INS)):
1289
    return None
1290

    
1291
  return config
1292

    
1293

    
1294
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1295
  """Import an os image into an instance.
1296

1297
  Args:
1298
    instance: the instance object
1299
    os_disk: the instance-visible name of the os device
1300
    swap_disk: the instance-visible name of the swap device
1301
    src_node: node holding the source image
1302
    src_image: path to the source image on src_node
1303

1304
  Returns:
1305
    False in case of error, True otherwise.
1306

1307
  """
1308
  inst_os = OSFromDisk(instance.os)
1309
  import_script = inst_os.import_script
1310

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

    
1316
  swap_device = instance.FindDisk(swap_disk)
1317
  if swap_device is None:
1318
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1319
    return False
1320

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

    
1327
  real_swap_dev = _RecursiveFindBD(swap_device)
1328
  if real_swap_dev is None:
1329
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1330
                                  str(swap_device))
1331
  real_swap_dev.Open()
1332

    
1333
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1334
                                        instance.name, int(time.time()))
1335
  if not os.path.exists(constants.LOG_OS_DIR):
1336
    os.mkdir(constants.LOG_OS_DIR, 0750)
1337

    
1338
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1339
  remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1340

    
1341
  comprcmd = "gunzip"
1342
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1343
                               inst_os.path, import_script, instance.name,
1344
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1345
                               logfile)
1346

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

    
1349
  result = utils.RunCmd(command)
1350

    
1351
  if result.failed:
1352
    logger.Error("os import command '%s' returned error: %s"
1353
                 " output: %s" %
1354
                 (command, result.fail_reason, result.output))
1355
    return False
1356

    
1357
  return True
1358

    
1359

    
1360
def ListExports():
1361
  """Return a list of exports currently available on this machine.
1362

1363
  """
1364
  if os.path.isdir(constants.EXPORT_DIR):
1365
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1366
  else:
1367
    return []
1368

    
1369

    
1370
def RemoveExport(export):
1371
  """Remove an existing export from the node.
1372

1373
  Args:
1374
    export: the name of the export to remove
1375

1376
  Returns:
1377
    False in case of error, True otherwise.
1378

1379
  """
1380
  target = os.path.join(constants.EXPORT_DIR, export)
1381

    
1382
  shutil.rmtree(target)
1383
  # TODO: catch some of the relevant exceptions and provide a pretty
1384
  # error message if rmtree fails.
1385

    
1386
  return True
1387

    
1388

    
1389
def RenameBlockDevices(devlist):
1390
  """Rename a list of block devices.
1391

1392
  The devlist argument is a list of tuples (disk, new_logical,
1393
  new_physical). The return value will be a combined boolean result
1394
  (True only if all renames succeeded).
1395

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

    
1420

    
1421
class HooksRunner(object):
1422
  """Hook runner.
1423

1424
  This class is instantiated on the node side (ganeti-noded) and not on
1425
  the master side.
1426

1427
  """
1428
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1429

    
1430
  def __init__(self, hooks_base_dir=None):
1431
    """Constructor for hooks runner.
1432

1433
    Args:
1434
      - hooks_base_dir: if not None, this overrides the
1435
        constants.HOOKS_BASE_DIR (useful for unittests)
1436
      - logs_base_dir: if not None, this overrides the
1437
        constants.LOG_HOOKS_DIR (useful for unittests)
1438
      - logging: enable or disable logging of script output
1439

1440
    """
1441
    if hooks_base_dir is None:
1442
      hooks_base_dir = constants.HOOKS_BASE_DIR
1443
    self._BASE_DIR = hooks_base_dir
1444

    
1445
  @staticmethod
1446
  def ExecHook(script, env):
1447
    """Exec one hook script.
1448

1449
    Args:
1450
     - phase: the phase
1451
     - script: the full path to the script
1452
     - env: the environment with which to exec the script
1453

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

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

    
1488
    return result == 0, output
1489

    
1490
  def RunHooks(self, hpath, phase, env):
1491
    """Run the scripts in the hooks directory.
1492

1493
    This method will not be usually overriden by child opcodes.
1494

1495
    """
1496
    if phase == constants.HOOKS_PHASE_PRE:
1497
      suffix = "pre"
1498
    elif phase == constants.HOOKS_PHASE_POST:
1499
      suffix = "post"
1500
    else:
1501
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1502
    rr = []
1503

    
1504
    subdir = "%s-%s.d" % (hpath, suffix)
1505
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1506
    try:
1507
      dir_contents = utils.ListVisibleFiles(dir_name)
1508
    except OSError, err:
1509
      # must log
1510
      return rr
1511

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

    
1529
    return rr
1530

    
1531

    
1532
class DevCacheManager(object):
1533
  """Simple class for managing a chache of block device information.
1534

1535
  """
1536
  _DEV_PREFIX = "/dev/"
1537
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1538

    
1539
  @classmethod
1540
  def _ConvertPath(cls, dev_path):
1541
    """Converts a /dev/name path to the cache file name.
1542

1543
    This replaces slashes with underscores and strips the /dev
1544
    prefix. It then returns the full path to the cache file
1545

1546
    """
1547
    if dev_path.startswith(cls._DEV_PREFIX):
1548
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1549
    dev_path = dev_path.replace("/", "_")
1550
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1551
    return fpath
1552

    
1553
  @classmethod
1554
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1555
    """Updates the cache information for a given device.
1556

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

    
1575
  @classmethod
1576
  def RemoveCache(cls, dev_path):
1577
    """Remove data for a dev_path.
1578

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