Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ c8a0948f

History | View | Annotate | Download (44.4 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:
214
    test1: 20.06MiB
215

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

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

    
227

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

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

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

    
237

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

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

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

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

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

    
266

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

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

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

    
278
  return True
279

    
280

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

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

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

    
296
  return names
297

    
298

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

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

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

312
  """
313
  output = {}
314

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

    
321
  return output
322

    
323

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

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

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

340
  """
341
  output = {}
342

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

    
353
  return output
354

    
355

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

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

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

    
367
  create_script = inst_os.create_script
368

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

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

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

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

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

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

    
401
  result = utils.RunCmd(command)
402

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

    
409
  return True
410

    
411

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

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

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

    
424
  script = inst_os.rename_script
425

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

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

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

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

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

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

    
459
  result = utils.RunCmd(command)
460

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

    
467
  return True
468

    
469

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

473
  Args:
474
    vg_name: the volume group
475

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

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

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

    
499

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

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

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

    
517

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

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

524
  """
525
  running_instances = GetInstanceList()
526

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

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

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

    
539
  return True
540

    
541

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

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

548
  """
549
  running_instances = GetInstanceList()
550

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

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

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

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

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

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

    
584
  return True
585

    
586

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

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

594
  """
595
  running_instances = GetInstanceList()
596

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

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

    
618

    
619
  return True
620

    
621

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

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

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

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

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

    
672
  device.SetInfo(info)
673

    
674
  physical_id = device.unique_id
675
  return physical_id
676

    
677

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

681
  This is intended to be called recursively.
682

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

    
704

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

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

710
  This function is called recursively.
711

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

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

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

721
  """
722
  children = []
723
  if disk.children:
724
    mcn = disk.ChildrenNeeded()
725
    if mcn == -1:
726
      mcn = 0 # max number of Nones allowed
727
    else:
728
      mcn = len(disk.children) - mcn # max number of Nones
729
    for chld_disk in disk.children:
730
      try:
731
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
732
      except errors.BlockDeviceError, err:
733
        if children.count(None) > mcn:
734
          raise
735
        cdev = None
736
        logger.Debug("Error in child activation: %s" % str(err))
737
      children.append(cdev)
738

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

    
750
  else:
751
    result = True
752
  return result
753

    
754

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

758
  This is a wrapper over _RecursiveAssembleBD.
759

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

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

    
770

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

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

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

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

    
795

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

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

    
812

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

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

    
837

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

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

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

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

    
857

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

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

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

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

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

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

    
881

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

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

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

893
  """
894
  rbd = _RecursiveFindBD(disk)
895
  if rbd is None:
896
    return rbd
897
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
898

    
899

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

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

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

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

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

    
936

    
937
def _ErrnoOrStr(err):
938
  """Format an EnvironmentError exception.
939

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

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

    
951

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

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

959
  Returns:
960
    The base_dir the OS resides in
961

962
  """
963
  if search_path is None:
964
    search_path = constants.OS_SEARCH_PATH
965

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

    
971
  return None
972

    
973

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

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

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

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

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

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

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

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

    
1013
  return api_version
1014

    
1015

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

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

1022
  Returns:
1023
    list of OS objects
1024

1025
  """
1026
  if top_dirs is None:
1027
    top_dirs = constants.OS_SEARCH_PATH
1028

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

    
1045
  return result
1046

    
1047

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

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

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

1060
  """
1061

    
1062
  if base_dir is None:
1063
    base_dir = _OSSearch(name)
1064

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

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

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

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

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

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

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

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

    
1096

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

    
1104

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

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

1111
  Args:
1112
    disk: the disk to be snapshotted
1113

1114
  Returns:
1115
    a config entry for the actual lvm device snapshotted.
1116

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

    
1140

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

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

1149
  Returns:
1150
    True if successful, False otherwise.
1151

1152
  """
1153
  inst_os = OSFromDisk(instance.os)
1154
  export_script = inst_os.export_script
1155

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

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

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

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

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

    
1178
  comprcmd = "gzip"
1179

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

    
1184

    
1185

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

    
1189
  result = utils.RunCmd(command)
1190

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

    
1197
  return True
1198

    
1199

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

1203
  Args:
1204
    instance: instance configuration
1205
    snap_disks: snapshot block devices
1206

1207
  Returns:
1208
    False in case of error, True otherwise.
1209

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

    
1214
  config = objects.SerializableConfigParser()
1215

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

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

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

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

    
1251
  shutil.rmtree(finaldestdir, True)
1252
  shutil.move(destdir, finaldestdir)
1253

    
1254
  return True
1255

    
1256

    
1257
def ExportInfo(dest):
1258
  """Get export configuration information.
1259

1260
  Args:
1261
    dest: directory containing the export
1262

1263
  Returns:
1264
    A serializable config file containing the export info.
1265

1266
  """
1267
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1268

    
1269
  config = objects.SerializableConfigParser()
1270
  config.read(cff)
1271

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

    
1276
  return config
1277

    
1278

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

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

1289
  Returns:
1290
    False in case of error, True otherwise.
1291

1292
  """
1293
  inst_os = OSFromDisk(instance.os)
1294
  import_script = inst_os.import_script
1295

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

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

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

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

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

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

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

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

    
1334
  result = utils.RunCmd(command)
1335

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

    
1342
  return True
1343

    
1344

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

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

    
1354

    
1355
def RemoveExport(export):
1356
  """Remove an existing export from the node.
1357

1358
  Args:
1359
    export: the name of the export to remove
1360

1361
  Returns:
1362
    False in case of error, True otherwise.
1363

1364
  """
1365
  target = os.path.join(constants.EXPORT_DIR, export)
1366

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

    
1371
  return True
1372

    
1373

    
1374
def RenameBlockDevices(devlist):
1375
  """Rename a list of block devices.
1376

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

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

    
1405

    
1406
class HooksRunner(object):
1407
  """Hook runner.
1408

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

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

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

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

1425
    """
1426
    if hooks_base_dir is None:
1427
      hooks_base_dir = constants.HOOKS_BASE_DIR
1428
    self._BASE_DIR = hooks_base_dir
1429

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

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

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

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

    
1473
    return result == 0, output
1474

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

1478
    This method will not be usually overriden by child opcodes.
1479

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

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

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

    
1514
    return rr
1515

    
1516

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

1520
  """
1521
  _DEV_PREFIX = "/dev/"
1522
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1523

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

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

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

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

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

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

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