Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 150e978f

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 lvs
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 = [constants.CLUSTER_CONF_FILE, "/etc/hosts",
925
                   constants.SSH_KNOWN_HOSTS_FILE]
926
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
927
  if file_name not in allowed_files:
928
    logger.Error("Filename passed to UploadFile not in allowed"
929
                 " upload targets: '%s'" % file_name)
930
    return False
931

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

    
948

    
949
def _ErrnoOrStr(err):
950
  """Format an EnvironmentError exception.
951

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

956
  """
957
  if hasattr(err, 'errno'):
958
    detail = errno.errorcode[err.errno]
959
  else:
960
    detail = str(err)
961
  return detail
962

    
963

    
964
def _OSSearch(name, search_path=None):
965
  """Search for OSes with the given name in the search_path.
966

967
  Args:
968
    name: The name of the OS to look for
969
    search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
970

971
  Returns:
972
    The base_dir the OS resides in
973

974
  """
975
  if search_path is None:
976
    search_path = constants.OS_SEARCH_PATH
977

    
978
  for dir_name in search_path:
979
    t_os_dir = os.path.sep.join([dir_name, name])
980
    if os.path.isdir(t_os_dir):
981
      return dir_name
982

    
983
  return None
984

    
985

    
986
def _OSOndiskVersion(name, os_dir):
987
  """Compute and return the API version of a given OS.
988

989
  This function will try to read the API version of the os given by
990
  the 'name' parameter and residing in the 'os_dir' directory.
991

992
  Return value will be either an integer denoting the version or None in the
993
  case when this is not a valid OS name.
994

995
  """
996
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
997

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

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

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

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

    
1025
  return api_version
1026

    
1027

    
1028
def DiagnoseOS(top_dirs=None):
1029
  """Compute the validity for all OSes.
1030

1031
  Returns an OS object for each name in all the given top directories
1032
  (if not given defaults to constants.OS_SEARCH_PATH)
1033

1034
  Returns:
1035
    list of OS objects
1036

1037
  """
1038
  if top_dirs is None:
1039
    top_dirs = constants.OS_SEARCH_PATH
1040

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

    
1057
  return result
1058

    
1059

    
1060
def OSFromDisk(name, base_dir=None):
1061
  """Create an OS instance from disk.
1062

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

1068
  Args:
1069
    os_dir: Directory containing the OS scripts. Defaults to a search
1070
            in all the OS_SEARCH_PATH directories.
1071

1072
  """
1073

    
1074
  if base_dir is None:
1075
    base_dir = _OSSearch(name)
1076

    
1077
  if base_dir is None:
1078
    raise errors.InvalidOS(name, None, "OS dir not found in search path")
1079

    
1080
  os_dir = os.path.sep.join([base_dir, name])
1081
  api_version = _OSOndiskVersion(name, os_dir)
1082

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

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

    
1091
  for script in os_scripts:
1092
    os_scripts[script] = os.path.sep.join([os_dir, script])
1093

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

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

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

    
1108

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

    
1116

    
1117
def SnapshotBlockDevice(disk):
1118
  """Create a snapshot copy of a block device.
1119

1120
  This function is called recursively, and the snapshot is actually created
1121
  just for the leaf lvm backend device.
1122

1123
  Args:
1124
    disk: the disk to be snapshotted
1125

1126
  Returns:
1127
    a config entry for the actual lvm device snapshotted.
1128

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

    
1152

    
1153
def ExportSnapshot(disk, dest_node, instance):
1154
  """Export a block device snapshot to a remote node.
1155

1156
  Args:
1157
    disk: the snapshot block device
1158
    dest_node: the node to send the image to
1159
    instance: instance being exported
1160

1161
  Returns:
1162
    True if successful, False otherwise.
1163

1164
  """
1165
  inst_os = OSFromDisk(instance.os)
1166
  export_script = inst_os.export_script
1167

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

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

    
1179
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1180
  destfile = disk.physical_id[1]
1181

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

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

    
1190
  comprcmd = "gzip"
1191

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

    
1196

    
1197

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

    
1201
  result = utils.RunCmd(command)
1202

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

    
1209
  return True
1210

    
1211

    
1212
def FinalizeExport(instance, snap_disks):
1213
  """Write out the export configuration information.
1214

1215
  Args:
1216
    instance: instance configuration
1217
    snap_disks: snapshot block devices
1218

1219
  Returns:
1220
    False in case of error, True otherwise.
1221

1222
  """
1223
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1224
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1225

    
1226
  config = objects.SerializableConfigParser()
1227

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

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

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

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

    
1263
  shutil.rmtree(finaldestdir, True)
1264
  shutil.move(destdir, finaldestdir)
1265

    
1266
  return True
1267

    
1268

    
1269
def ExportInfo(dest):
1270
  """Get export configuration information.
1271

1272
  Args:
1273
    dest: directory containing the export
1274

1275
  Returns:
1276
    A serializable config file containing the export info.
1277

1278
  """
1279
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1280

    
1281
  config = objects.SerializableConfigParser()
1282
  config.read(cff)
1283

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

    
1288
  return config
1289

    
1290

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

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

1301
  Returns:
1302
    False in case of error, True otherwise.
1303

1304
  """
1305
  inst_os = OSFromDisk(instance.os)
1306
  import_script = inst_os.import_script
1307

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

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

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

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

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

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

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

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

    
1346
  result = utils.RunCmd(command)
1347

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

    
1354
  return True
1355

    
1356

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

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

    
1366

    
1367
def RemoveExport(export):
1368
  """Remove an existing export from the node.
1369

1370
  Args:
1371
    export: the name of the export to remove
1372

1373
  Returns:
1374
    False in case of error, True otherwise.
1375

1376
  """
1377
  target = os.path.join(constants.EXPORT_DIR, export)
1378

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

    
1383
  return True
1384

    
1385

    
1386
def RenameBlockDevices(devlist):
1387
  """Rename a list of block devices.
1388

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

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

    
1417

    
1418
class HooksRunner(object):
1419
  """Hook runner.
1420

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

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

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

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

1437
    """
1438
    if hooks_base_dir is None:
1439
      hooks_base_dir = constants.HOOKS_BASE_DIR
1440
    self._BASE_DIR = hooks_base_dir
1441

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

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

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

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

    
1485
    return result == 0, output
1486

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

1490
    This method will not be usually overriden by child opcodes.
1491

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

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

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

    
1526
    return rr
1527

    
1528

    
1529
class DevCacheManager(object):
1530
  """Simple class for managing a chache of block device information.
1531

1532
  """
1533
  _DEV_PREFIX = "/dev/"
1534
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1535

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

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

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

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

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

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

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