Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ f362096f

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
      errorstring = "Can't assemble device after creation"
664
      logger.Error(errorstring)
665
      raise errors.BlockDeviceError("%s, very unusual event - check the node"
666
                                    " daemon logs" % errorstring)
667
    device.SetSyncSpeed(constants.SYNC_SPEED)
668
    if on_primary or disk.OpenOnSecondary():
669
      device.Open(force=True)
670
    DevCacheManager.UpdateCache(device.dev_path, owner,
671
                                on_primary, disk.iv_name)
672

    
673
  device.SetInfo(info)
674

    
675
  physical_id = device.unique_id
676
  return physical_id
677

    
678

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

682
  This is intended to be called recursively.
683

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

    
705

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

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

711
  This function is called recursively.
712

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

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

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

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

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

    
751
  else:
752
    result = True
753
  return result
754

    
755

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

759
  This is a wrapper over _RecursiveAssembleBD.
760

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

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

    
771

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

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

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

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

    
796

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

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

    
813

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

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

    
838

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

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

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

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

    
858

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

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

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

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

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

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

    
882

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

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

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

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

    
900

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

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

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

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

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

    
937

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

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

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

    
952

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

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

960
  Returns:
961
    The base_dir the OS resides in
962

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

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

    
972
  return None
973

    
974

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

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

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

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

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

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

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

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

    
1014
  return api_version
1015

    
1016

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

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

1023
  Returns:
1024
    list of OS objects
1025

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

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

    
1046
  return result
1047

    
1048

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

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

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

1061
  """
1062

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

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

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

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

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

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

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

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

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

    
1097

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

    
1105

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

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

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

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

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

    
1141

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

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

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

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

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

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

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

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

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

    
1179
  comprcmd = "gzip"
1180

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

    
1185

    
1186

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

    
1190
  result = utils.RunCmd(command)
1191

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

    
1198
  return True
1199

    
1200

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

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

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

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

    
1215
  config = objects.SerializableConfigParser()
1216

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

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

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

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

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

    
1255
  return True
1256

    
1257

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

1261
  Args:
1262
    dest: directory containing the export
1263

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

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

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

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

    
1277
  return config
1278

    
1279

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

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

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

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

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

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

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

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

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

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

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

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

    
1335
  result = utils.RunCmd(command)
1336

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

    
1343
  return True
1344

    
1345

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

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

    
1355

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

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

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

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

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

    
1372
  return True
1373

    
1374

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

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

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

    
1406

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

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

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

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

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

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

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

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

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

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

    
1474
    return result == 0, output
1475

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

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

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

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

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

    
1515
    return rr
1516

    
1517

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

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

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

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

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

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

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

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

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