Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 778b75bb

History | View | Annotate | Download (48.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Functions used by the node daemon"""
23

    
24

    
25
import os
26
import os.path
27
import shutil
28
import time
29
import stat
30
import errno
31
import re
32
import subprocess
33

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

    
44

    
45
def _GetSshRunner():
46
  return ssh.SshRunner()
47

    
48

    
49
def StartMaster():
50
  """Activate local node as master node.
51

52
  There are two needed steps for this:
53
    - run the master script
54
    - register the cron script
55

56
  """
57
  result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "start"])
58

    
59
  if result.failed:
60
    logger.Error("could not activate cluster interface with command %s,"
61
                 " error: '%s'" % (result.cmd, result.output))
62
    return False
63

    
64
  return True
65

    
66

    
67
def StopMaster():
68
  """Deactivate this node as master.
69

70
  This runs the master stop script.
71

72
  """
73
  result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "stop"])
74

    
75
  if result.failed:
76
    logger.Error("could not deactivate cluster interface with command %s,"
77
                 " error: '%s'" % (result.cmd, result.output))
78
    return False
79

    
80
  return True
81

    
82

    
83
def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
84
  """Joins this node to the cluster.
85

86
  This does the following:
87
      - updates the hostkeys of the machine (rsa and dsa)
88
      - adds the ssh private key to the user
89
      - adds the ssh public key to the users' authorized_keys file
90

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

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

    
106
  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
107
    utils.WriteFile(name, data=content, mode=0600)
108

    
109
  utils.AddAuthorizedKey(auth_keys, sshpub)
110

    
111
  utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
112

    
113
  return True
114

    
115

    
116
def LeaveCluster():
117
  """Cleans up the current node and prepares it to be removed from the cluster.
118

119
  """
120
  if os.path.isdir(constants.DATA_DIR):
121
    for rel_name in utils.ListVisibleFiles(constants.DATA_DIR):
122
      full_name = os.path.join(constants.DATA_DIR, rel_name)
123
      if os.path.isfile(full_name) and not os.path.islink(full_name):
124
        utils.RemoveFile(full_name)
125

    
126
  try:
127
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
128
  except errors.OpExecError, err:
129
    logger.Error("Error while processing ssh files: %s" % err)
130
    return
131

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

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

    
141

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

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

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

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

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

    
172
  return outputarray
173

    
174

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

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

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

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

192
  """
193
  result = {}
194

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

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

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

    
209

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

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

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

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

    
238
  return lvs
239

    
240

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

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

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

    
250

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

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

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

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

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

    
279

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

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

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

    
291
  return True
292

    
293

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

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

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

    
309
  return names
310

    
311

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

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

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

325
  """
326
  output = {}
327

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

    
334
  return output
335

    
336

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

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

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

353
  """
354
  output = {}
355

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

    
366
  return output
367

    
368

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

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

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

    
380
  create_script = inst_os.create_script
381

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

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

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

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

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

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

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

    
421
  return True
422

    
423

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

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

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

    
436
  script = inst_os.rename_script
437

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

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

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

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

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

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

    
471
  result = utils.RunCmd(command)
472

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

    
479
  return True
480

    
481

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

485
  Args:
486
    vg_name: the volume group
487

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

495
  If an error occurs during gathering of data, we return the same dict
496
  with keys all set to None.
497

498
  """
499
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
500

    
501
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
502
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
503

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

    
523

    
524
def _GatherBlockDevs(instance):
525
  """Set up an instance's block device(s).
526

527
  This is run on the primary node at instance startup. The block
528
  devices must be already assembled.
529

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

    
541

    
542
def StartInstance(instance, extra_args):
543
  """Start an instance.
544

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

548
  """
549
  running_instances = GetInstanceList()
550

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

    
554
  block_devices = _GatherBlockDevs(instance)
555
  hyper = hypervisor.GetHypervisor()
556

    
557
  try:
558
    hyper.StartInstance(instance, block_devices, extra_args)
559
  except errors.HypervisorError, err:
560
    logger.Error("Failed to start instance: %s" % err)
561
    return False
562

    
563
  return True
564

    
565

    
566
def ShutdownInstance(instance):
567
  """Shut an instance down.
568

569
  Args:
570
    instance - name of instance to shutdown.
571

572
  """
573
  running_instances = GetInstanceList()
574

    
575
  if instance.name not in running_instances:
576
    return True
577

    
578
  hyper = hypervisor.GetHypervisor()
579
  try:
580
    hyper.StopInstance(instance)
581
  except errors.HypervisorError, err:
582
    logger.Error("Failed to stop instance: %s" % err)
583
    return False
584

    
585
  # test every 10secs for 2min
586
  shutdown_ok = False
587

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

    
597
    try:
598
      hyper.StopInstance(instance, force=True)
599
    except errors.HypervisorError, err:
600
      logger.Error("Failed to stop instance: %s" % err)
601
      return False
602

    
603
    time.sleep(1)
604
    if instance.name in GetInstanceList():
605
      logger.Error("could not shutdown instance '%s' even by destroy")
606
      return False
607

    
608
  return True
609

    
610

    
611
def RebootInstance(instance, reboot_type, extra_args):
612
  """Reboot an instance.
613

614
  Args:
615
    instance    - name of instance to reboot
616
    reboot_type - how to reboot [soft,hard,full]
617

618
  """
619
  running_instances = GetInstanceList()
620

    
621
  if instance.name not in running_instances:
622
    logger.Error("Cannot reboot instance that is not running")
623
    return False
624

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

    
642

    
643
  return True
644

    
645

    
646
def CreateBlockDevice(disk, size, owner, on_primary, info):
647
  """Creates a block device for an instance.
648

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

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

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

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

    
696
  device.SetInfo(info)
697

    
698
  physical_id = device.unique_id
699
  return physical_id
700

    
701

    
702
def RemoveBlockDevice(disk):
703
  """Remove a block device.
704

705
  This is intended to be called recursively.
706

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

    
728

    
729
def _RecursiveAssembleBD(disk, owner, as_primary):
730
  """Activate a block device for an instance.
731

732
  This is run on the primary and secondary nodes for an instance.
733

734
  This function is called recursively.
735

736
  Args:
737
    disk: a objects.Disk object
738
    as_primary: if we should make the block device read/write
739

740
  Returns:
741
    the assembled device or None (in case no device was assembled)
742

743
  If the assembly is not successful, an exception is raised.
744

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

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

    
772
  else:
773
    result = True
774
  return result
775

    
776

    
777
def AssembleBlockDevice(disk, owner, as_primary):
778
  """Activate a block device for an instance.
779

780
  This is a wrapper over _RecursiveAssembleBD.
781

782
  Returns:
783
    a /dev path for primary nodes
784
    True for secondary nodes
785

786
  """
787
  result = _RecursiveAssembleBD(disk, owner, as_primary)
788
  if isinstance(result, bdev.BlockDev):
789
    result = result.dev_path
790
  return result
791

    
792

    
793
def ShutdownBlockDevice(disk):
794
  """Shut down a block device.
795

796
  First, if the device is assembled (can `Attach()`), then the device
797
  is shutdown. Then the children of the device are shutdown.
798

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

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

    
817

    
818
def MirrorAddChildren(parent_cdev, new_cdevs):
819
  """Extend a mirrored block device.
820

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

    
834

    
835
def MirrorRemoveChildren(parent_cdev, new_cdevs):
836
  """Shrink a mirrored block device.
837

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

    
859

    
860
def GetMirrorStatus(disks):
861
  """Get the mirroring status of a list of devices.
862

863
  Args:
864
    disks: list of `objects.Disk`
865

866
  Returns:
867
    list of (mirror_done, estimated_time) tuples, which
868
    are the result of bdev.BlockDevice.CombinedSyncStatus()
869

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

    
879

    
880
def _RecursiveFindBD(disk, allow_partial=False):
881
  """Check if a device is activated.
882

883
  If so, return informations about the real device.
884

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

891
  Returns:
892
    None if the device can't be found
893
    otherwise the device instance
894

895
  """
896
  children = []
897
  if disk.children:
898
    for chdisk in disk.children:
899
      children.append(_RecursiveFindBD(chdisk))
900

    
901
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
902

    
903

    
904
def FindBlockDevice(disk):
905
  """Check if a device is activated.
906

907
  If so, return informations about the real device.
908

909
  Args:
910
    disk: the objects.Disk instance
911
  Returns:
912
    None if the device can't be found
913
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
914

915
  """
916
  rbd = _RecursiveFindBD(disk)
917
  if rbd is None:
918
    return rbd
919
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
920

    
921

    
922
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
923
  """Write a file to the filesystem.
924

925
  This allows the master to overwrite(!) a file. It will only perform
926
  the operation if the file belongs to a list of configuration files.
927

928
  """
929
  if not os.path.isabs(file_name):
930
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
931
                 file_name)
932
    return False
933

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

    
945
  utils.WriteFile(file_name, data=data, mode=mode, uid=uid, gid=gid,
946
                  atime=atime, mtime=mtime)
947
  return True
948

    
949

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

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

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

    
964

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

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

972
  Returns:
973
    The base_dir the OS resides in
974

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

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

    
984
  return None
985

    
986

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

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

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

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

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

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

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

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

    
1026
  return api_version
1027

    
1028

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

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

1035
  Returns:
1036
    list of OS objects
1037

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

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

    
1058
  return result
1059

    
1060

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

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

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

1073
  """
1074

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

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

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

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

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

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

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

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

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

    
1109

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

    
1117

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

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

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

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

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

    
1153

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

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

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

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

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

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

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

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

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

    
1191
  comprcmd = "gzip"
1192

    
1193
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1194
                                destdir, destdir, destfile)
1195
  remotecmd = _GetSshRunner().BuildCmd(dest_node, constants.GANETI_RUNAS,
1196
                                       destcmd)
1197

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

    
1201
  result = utils.RunCmd(command)
1202

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

    
1209
  return True
1210

    
1211

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

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

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

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

    
1226
  config = objects.SerializableConfigParser()
1227

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

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

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

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

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

    
1267
  return True
1268

    
1269

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

1273
  Args:
1274
    dest: directory containing the export
1275

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

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

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

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

    
1289
  return config
1290

    
1291

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

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

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

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

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

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

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

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

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

    
1336
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1337
  remotecmd = _GetSshRunner().BuildCmd(src_node, constants.GANETI_RUNAS,
1338
                                       destcmd)
1339

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

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

    
1348
  result = utils.RunCmd(command)
1349

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

    
1356
  return True
1357

    
1358

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

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

    
1368

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

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

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

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

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

    
1385
  return True
1386

    
1387

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

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

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

    
1419

    
1420
def _TransformFileStorageDir(file_storage_dir):
1421
  """Checks whether given file_storage_dir is valid.
1422

1423
  Checks wheter the given file_storage_dir is within the cluster-wide
1424
  default file_storage_dir stored in SimpleStore. Only paths under that
1425
  directory are allowed.
1426

1427
  Args:
1428
    file_storage_dir: string with path
1429
  
1430
  Returns:
1431
    normalized file_storage_dir (string) if valid, None otherwise
1432

1433
  """
1434
  file_storage_dir = os.path.normpath(file_storage_dir)
1435
  base_file_storage_dir = ssconf.SimpleStore().GetFileStorageDir()
1436
  if (not os.path.commonprefix([file_storage_dir, base_file_storage_dir]) ==
1437
      base_file_storage_dir):
1438
    logger.Error("file storage directory '%s' is not under base file"
1439
                 " storage directory '%s'" %
1440
                 (file_storage_dir, base_file_storage_dir))
1441
    return None
1442
  return file_storage_dir
1443

    
1444

    
1445
def CreateFileStorageDir(file_storage_dir):
1446
  """Create file storage directory.
1447

1448
  Args:
1449
    file_storage_dir: string containing the path
1450

1451
  Returns:
1452
    tuple with first element a boolean indicating wheter dir
1453
    creation was successful or not
1454

1455
  """
1456
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1457
  result = True,
1458
  if not file_storage_dir:
1459
    result = False,
1460
  else:
1461
    if os.path.exists(file_storage_dir):
1462
      if not os.path.isdir(file_storage_dir):
1463
        logger.Error("'%s' is not a directory" % file_storage_dir)
1464
        result = False,
1465
    else:
1466
      try:
1467
        os.makedirs(file_storage_dir, 0750)
1468
      except OSError, err:
1469
        logger.Error("Cannot create file storage directory '%s': %s" %
1470
                     (file_storage_dir, err))
1471
        result = False,
1472
  return result
1473

    
1474

    
1475
def RemoveFileStorageDir(file_storage_dir):
1476
  """Remove file storage directory.
1477

1478
  Remove it only if it's empty. If not log an error and return.
1479

1480
  Args:
1481
    file_storage_dir: string containing the path
1482

1483
  Returns:
1484
    tuple with first element a boolean indicating wheter dir
1485
    removal was successful or not
1486

1487
  """
1488
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1489
  result = True,
1490
  if not file_storage_dir:
1491
    result = False,
1492
  else:
1493
    if os.path.exists(file_storage_dir):
1494
      if not os.path.isdir(file_storage_dir):
1495
        logger.Error("'%s' is not a directory" % file_storage_dir)
1496
        result = False,
1497
      # deletes dir only if empty, otherwise we want to return False
1498
      try:
1499
        os.rmdir(file_storage_dir)
1500
      except OSError, err:
1501
        logger.Error("Cannot remove file storage directory '%s': %s" %
1502
                     (file_storage_dir, err))
1503
        result = False,
1504
  return result
1505

    
1506

    
1507
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
1508
  """Rename the file storage directory.
1509

1510
  Args:
1511
    old_file_storage_dir: string containing the old path
1512
    new_file_storage_dir: string containing the new path
1513

1514
  Returns:
1515
    tuple with first element a boolean indicating wheter dir
1516
    rename was successful or not
1517

1518
  """
1519
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
1520
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
1521
  result = True,
1522
  if not old_file_storage_dir or not new_file_storage_dir:
1523
    result = False,
1524
  else:
1525
    if not os.path.exists(new_file_storage_dir):
1526
      if os.path.isdir(old_file_storage_dir):
1527
        try:
1528
          os.rename(old_file_storage_dir, new_file_storage_dir)
1529
        except OSError, err:
1530
          logger.Error("Cannot rename '%s' to '%s': %s"
1531
                       % (old_file_storage_dir, new_file_storage_dir, err))
1532
          result =  False,
1533
      else:
1534
        logger.Error("'%s' is not a directory" % old_file_storage_dir)
1535
        result = False,
1536
    else:
1537
      if os.path.exists(old_file_storage_dir):
1538
        logger.Error("Cannot rename '%s' to '%s'. Both locations exist." %
1539
                     old_file_storage_dir, new_file_storage_dir)
1540
        result = False,
1541
  return result
1542

    
1543

    
1544
class HooksRunner(object):
1545
  """Hook runner.
1546

1547
  This class is instantiated on the node side (ganeti-noded) and not on
1548
  the master side.
1549

1550
  """
1551
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1552

    
1553
  def __init__(self, hooks_base_dir=None):
1554
    """Constructor for hooks runner.
1555

1556
    Args:
1557
      - hooks_base_dir: if not None, this overrides the
1558
        constants.HOOKS_BASE_DIR (useful for unittests)
1559
      - logs_base_dir: if not None, this overrides the
1560
        constants.LOG_HOOKS_DIR (useful for unittests)
1561
      - logging: enable or disable logging of script output
1562

1563
    """
1564
    if hooks_base_dir is None:
1565
      hooks_base_dir = constants.HOOKS_BASE_DIR
1566
    self._BASE_DIR = hooks_base_dir
1567

    
1568
  @staticmethod
1569
  def ExecHook(script, env):
1570
    """Exec one hook script.
1571

1572
    Args:
1573
     - phase: the phase
1574
     - script: the full path to the script
1575
     - env: the environment with which to exec the script
1576

1577
    """
1578
    # exec the process using subprocess and log the output
1579
    fdstdin = None
1580
    try:
1581
      fdstdin = open("/dev/null", "r")
1582
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1583
                               stderr=subprocess.STDOUT, close_fds=True,
1584
                               shell=False, cwd="/", env=env)
1585
      output = ""
1586
      try:
1587
        output = child.stdout.read(4096)
1588
        child.stdout.close()
1589
      except EnvironmentError, err:
1590
        output += "Hook script error: %s" % str(err)
1591

    
1592
      while True:
1593
        try:
1594
          result = child.wait()
1595
          break
1596
        except EnvironmentError, err:
1597
          if err.errno == errno.EINTR:
1598
            continue
1599
          raise
1600
    finally:
1601
      # try not to leak fds
1602
      for fd in (fdstdin, ):
1603
        if fd is not None:
1604
          try:
1605
            fd.close()
1606
          except EnvironmentError, err:
1607
            # just log the error
1608
            #logger.Error("While closing fd %s: %s" % (fd, err))
1609
            pass
1610

    
1611
    return result == 0, output
1612

    
1613
  def RunHooks(self, hpath, phase, env):
1614
    """Run the scripts in the hooks directory.
1615

1616
    This method will not be usually overriden by child opcodes.
1617

1618
    """
1619
    if phase == constants.HOOKS_PHASE_PRE:
1620
      suffix = "pre"
1621
    elif phase == constants.HOOKS_PHASE_POST:
1622
      suffix = "post"
1623
    else:
1624
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1625
    rr = []
1626

    
1627
    subdir = "%s-%s.d" % (hpath, suffix)
1628
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1629
    try:
1630
      dir_contents = utils.ListVisibleFiles(dir_name)
1631
    except OSError, err:
1632
      # must log
1633
      return rr
1634

    
1635
    # we use the standard python sort order,
1636
    # so 00name is the recommended naming scheme
1637
    dir_contents.sort()
1638
    for relname in dir_contents:
1639
      fname = os.path.join(dir_name, relname)
1640
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1641
          self.RE_MASK.match(relname) is not None):
1642
        rrval = constants.HKR_SKIP
1643
        output = ""
1644
      else:
1645
        result, output = self.ExecHook(fname, env)
1646
        if not result:
1647
          rrval = constants.HKR_FAIL
1648
        else:
1649
          rrval = constants.HKR_SUCCESS
1650
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1651

    
1652
    return rr
1653

    
1654

    
1655
class DevCacheManager(object):
1656
  """Simple class for managing a cache of block device information.
1657

1658
  """
1659
  _DEV_PREFIX = "/dev/"
1660
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1661

    
1662
  @classmethod
1663
  def _ConvertPath(cls, dev_path):
1664
    """Converts a /dev/name path to the cache file name.
1665

1666
    This replaces slashes with underscores and strips the /dev
1667
    prefix. It then returns the full path to the cache file
1668

1669
    """
1670
    if dev_path.startswith(cls._DEV_PREFIX):
1671
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1672
    dev_path = dev_path.replace("/", "_")
1673
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1674
    return fpath
1675

    
1676
  @classmethod
1677
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1678
    """Updates the cache information for a given device.
1679

1680
    """
1681
    if dev_path is None:
1682
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1683
      return
1684
    fpath = cls._ConvertPath(dev_path)
1685
    if on_primary:
1686
      state = "primary"
1687
    else:
1688
      state = "secondary"
1689
    if iv_name is None:
1690
      iv_name = "not_visible"
1691
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1692
    try:
1693
      utils.WriteFile(fpath, data=fdata)
1694
    except EnvironmentError, err:
1695
      logger.Error("Can't update bdev cache for %s, error %s" %
1696
                   (dev_path, str(err)))
1697

    
1698
  @classmethod
1699
  def RemoveCache(cls, dev_path):
1700
    """Remove data for a dev_path.
1701

1702
    """
1703
    if dev_path is None:
1704
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1705
      return
1706
    fpath = cls._ConvertPath(dev_path)
1707
    try:
1708
      utils.RemoveFile(fpath)
1709
    except EnvironmentError, err:
1710
      logger.Error("Can't update bdev cache for %s, error %s" %
1711
                   (dev_path, str(err)))