Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 243cdbcc

History | View | Annotate | Download (49.3 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 _OSOndiskVersion(name, os_dir):
966
  """Compute and return the API version of a given OS.
967

968
  This function will try to read the API version of the os given by
969
  the 'name' parameter and residing in the 'os_dir' directory.
970

971
  Return value will be either an integer denoting the version or None in the
972
  case when this is not a valid OS name.
973

974
  """
975
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
976

    
977
  try:
978
    st = os.stat(api_file)
979
  except EnvironmentError, err:
980
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
981
                           " found (%s)" % _ErrnoOrStr(err))
982

    
983
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
984
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
985
                           " a regular file")
986

    
987
  try:
988
    f = open(api_file)
989
    try:
990
      api_version = f.read(256)
991
    finally:
992
      f.close()
993
  except EnvironmentError, err:
994
    raise errors.InvalidOS(name, os_dir, "error while reading the"
995
                           " API version (%s)" % _ErrnoOrStr(err))
996

    
997
  api_version = api_version.strip()
998
  try:
999
    api_version = int(api_version)
1000
  except (TypeError, ValueError), err:
1001
    raise errors.InvalidOS(name, os_dir,
1002
                           "API version is not integer (%s)" % str(err))
1003

    
1004
  return api_version
1005

    
1006

    
1007
def DiagnoseOS(top_dirs=None):
1008
  """Compute the validity for all OSes.
1009

1010
  Returns an OS object for each name in all the given top directories
1011
  (if not given defaults to constants.OS_SEARCH_PATH)
1012

1013
  Returns:
1014
    list of OS objects
1015

1016
  """
1017
  if top_dirs is None:
1018
    top_dirs = constants.OS_SEARCH_PATH
1019

    
1020
  result = []
1021
  for dir_name in top_dirs:
1022
    if os.path.isdir(dir_name):
1023
      try:
1024
        f_names = utils.ListVisibleFiles(dir_name)
1025
      except EnvironmentError, err:
1026
        logger.Error("Can't list the OS directory %s: %s" %
1027
                     (dir_name, str(err)))
1028
        break
1029
      for name in f_names:
1030
        try:
1031
          os_inst = OSFromDisk(name, base_dir=dir_name)
1032
          result.append(os_inst)
1033
        except errors.InvalidOS, err:
1034
          result.append(objects.OS.FromInvalidOS(err))
1035

    
1036
  return result
1037

    
1038

    
1039
def OSFromDisk(name, base_dir=None):
1040
  """Create an OS instance from disk.
1041

1042
  This function will return an OS instance if the given name is a
1043
  valid OS name. Otherwise, it will raise an appropriate
1044
  `errors.InvalidOS` exception, detailing why this is not a valid
1045
  OS.
1046

1047
  Args:
1048
    os_dir: Directory containing the OS scripts. Defaults to a search
1049
            in all the OS_SEARCH_PATH directories.
1050

1051
  """
1052

    
1053
  if base_dir is None:
1054
    os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1055
    if os_dir is None:
1056
      raise errors.InvalidOS(name, None, "OS dir not found in search path")
1057
  else:
1058
    os_dir = os.path.sep.join([base_dir, name])
1059

    
1060
  api_version = _OSOndiskVersion(name, os_dir)
1061

    
1062
  if api_version != constants.OS_API_VERSION:
1063
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1064
                           " (found %s want %s)"
1065
                           % (api_version, constants.OS_API_VERSION))
1066

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

    
1070
  for script in os_scripts:
1071
    os_scripts[script] = os.path.sep.join([os_dir, script])
1072

    
1073
    try:
1074
      st = os.stat(os_scripts[script])
1075
    except EnvironmentError, err:
1076
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1077
                             (script, _ErrnoOrStr(err)))
1078

    
1079
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1080
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1081
                             script)
1082

    
1083
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1084
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1085
                             script)
1086

    
1087

    
1088
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1089
                    create_script=os_scripts['create'],
1090
                    export_script=os_scripts['export'],
1091
                    import_script=os_scripts['import'],
1092
                    rename_script=os_scripts['rename'],
1093
                    api_version=api_version)
1094

    
1095

    
1096
def SnapshotBlockDevice(disk):
1097
  """Create a snapshot copy of a block device.
1098

1099
  This function is called recursively, and the snapshot is actually created
1100
  just for the leaf lvm backend device.
1101

1102
  Args:
1103
    disk: the disk to be snapshotted
1104

1105
  Returns:
1106
    a config entry for the actual lvm device snapshotted.
1107

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

    
1131

    
1132
def ExportSnapshot(disk, dest_node, instance):
1133
  """Export a block device snapshot to a remote node.
1134

1135
  Args:
1136
    disk: the snapshot block device
1137
    dest_node: the node to send the image to
1138
    instance: instance being exported
1139

1140
  Returns:
1141
    True if successful, False otherwise.
1142

1143
  """
1144
  inst_os = OSFromDisk(instance.os)
1145
  export_script = inst_os.export_script
1146

    
1147
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1148
                                     instance.name, int(time.time()))
1149
  if not os.path.exists(constants.LOG_OS_DIR):
1150
    os.mkdir(constants.LOG_OS_DIR, 0750)
1151

    
1152
  real_os_dev = _RecursiveFindBD(disk)
1153
  if real_os_dev is None:
1154
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1155
                                  str(disk))
1156
  real_os_dev.Open()
1157

    
1158
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1159
  destfile = disk.physical_id[1]
1160

    
1161
  # the target command is built out of three individual commands,
1162
  # which are joined by pipes; we check each individual command for
1163
  # valid parameters
1164

    
1165
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1166
                               export_script, instance.name,
1167
                               real_os_dev.dev_path, logfile)
1168

    
1169
  comprcmd = "gzip"
1170

    
1171
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1172
                                destdir, destdir, destfile)
1173
  remotecmd = _GetSshRunner().BuildCmd(dest_node, constants.GANETI_RUNAS,
1174
                                       destcmd)
1175

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

    
1179
  result = utils.RunCmd(command)
1180

    
1181
  if result.failed:
1182
    logger.Error("os snapshot export command '%s' returned error: %s"
1183
                 " output: %s" %
1184
                 (command, result.fail_reason, result.output))
1185
    return False
1186

    
1187
  return True
1188

    
1189

    
1190
def FinalizeExport(instance, snap_disks):
1191
  """Write out the export configuration information.
1192

1193
  Args:
1194
    instance: instance configuration
1195
    snap_disks: snapshot block devices
1196

1197
  Returns:
1198
    False in case of error, True otherwise.
1199

1200
  """
1201
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1202
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1203

    
1204
  config = objects.SerializableConfigParser()
1205

    
1206
  config.add_section(constants.INISECT_EXP)
1207
  config.set(constants.INISECT_EXP, 'version', '0')
1208
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1209
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1210
  config.set(constants.INISECT_EXP, 'os', instance.os)
1211
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1212

    
1213
  config.add_section(constants.INISECT_INS)
1214
  config.set(constants.INISECT_INS, 'name', instance.name)
1215
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1216
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1217
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1218

    
1219
  nic_count = 0
1220
  for nic_count, nic in enumerate(instance.nics):
1221
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1222
               nic_count, '%s' % nic.mac)
1223
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1224
    config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count, '%s' % nic.bridge)
1225
  # TODO: redundant: on load can read nics until it doesn't exist
1226
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1227

    
1228
  disk_count = 0
1229
  for disk_count, disk in enumerate(snap_disks):
1230
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1231
               ('%s' % disk.iv_name))
1232
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1233
               ('%s' % disk.physical_id[1]))
1234
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1235
               ('%d' % disk.size))
1236
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1237

    
1238
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1239
  cfo = open(cff, 'w')
1240
  try:
1241
    config.write(cfo)
1242
  finally:
1243
    cfo.close()
1244

    
1245
  shutil.rmtree(finaldestdir, True)
1246
  shutil.move(destdir, finaldestdir)
1247

    
1248
  return True
1249

    
1250

    
1251
def ExportInfo(dest):
1252
  """Get export configuration information.
1253

1254
  Args:
1255
    dest: directory containing the export
1256

1257
  Returns:
1258
    A serializable config file containing the export info.
1259

1260
  """
1261
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1262

    
1263
  config = objects.SerializableConfigParser()
1264
  config.read(cff)
1265

    
1266
  if (not config.has_section(constants.INISECT_EXP) or
1267
      not config.has_section(constants.INISECT_INS)):
1268
    return None
1269

    
1270
  return config
1271

    
1272

    
1273
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1274
  """Import an os image into an instance.
1275

1276
  Args:
1277
    instance: the instance object
1278
    os_disk: the instance-visible name of the os device
1279
    swap_disk: the instance-visible name of the swap device
1280
    src_node: node holding the source image
1281
    src_image: path to the source image on src_node
1282

1283
  Returns:
1284
    False in case of error, True otherwise.
1285

1286
  """
1287
  inst_os = OSFromDisk(instance.os)
1288
  import_script = inst_os.import_script
1289

    
1290
  os_device = instance.FindDisk(os_disk)
1291
  if os_device is None:
1292
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1293
    return False
1294

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

    
1300
  real_os_dev = _RecursiveFindBD(os_device)
1301
  if real_os_dev is None:
1302
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1303
                                  str(os_device))
1304
  real_os_dev.Open()
1305

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

    
1312
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1313
                                        instance.name, int(time.time()))
1314
  if not os.path.exists(constants.LOG_OS_DIR):
1315
    os.mkdir(constants.LOG_OS_DIR, 0750)
1316

    
1317
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1318
  remotecmd = _GetSshRunner().BuildCmd(src_node, constants.GANETI_RUNAS,
1319
                                       destcmd)
1320

    
1321
  comprcmd = "gunzip"
1322
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1323
                               inst_os.path, import_script, instance.name,
1324
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1325
                               logfile)
1326

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

    
1329
  result = utils.RunCmd(command)
1330

    
1331
  if result.failed:
1332
    logger.Error("os import command '%s' returned error: %s"
1333
                 " output: %s" %
1334
                 (command, result.fail_reason, result.output))
1335
    return False
1336

    
1337
  return True
1338

    
1339

    
1340
def ListExports():
1341
  """Return a list of exports currently available on this machine.
1342

1343
  """
1344
  if os.path.isdir(constants.EXPORT_DIR):
1345
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1346
  else:
1347
    return []
1348

    
1349

    
1350
def RemoveExport(export):
1351
  """Remove an existing export from the node.
1352

1353
  Args:
1354
    export: the name of the export to remove
1355

1356
  Returns:
1357
    False in case of error, True otherwise.
1358

1359
  """
1360
  target = os.path.join(constants.EXPORT_DIR, export)
1361

    
1362
  shutil.rmtree(target)
1363
  # TODO: catch some of the relevant exceptions and provide a pretty
1364
  # error message if rmtree fails.
1365

    
1366
  return True
1367

    
1368

    
1369
def RenameBlockDevices(devlist):
1370
  """Rename a list of block devices.
1371

1372
  The devlist argument is a list of tuples (disk, new_logical,
1373
  new_physical). The return value will be a combined boolean result
1374
  (True only if all renames succeeded).
1375

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

    
1400

    
1401
def _TransformFileStorageDir(file_storage_dir):
1402
  """Checks whether given file_storage_dir is valid.
1403

1404
  Checks wheter the given file_storage_dir is within the cluster-wide
1405
  default file_storage_dir stored in SimpleStore. Only paths under that
1406
  directory are allowed.
1407

1408
  Args:
1409
    file_storage_dir: string with path
1410
  
1411
  Returns:
1412
    normalized file_storage_dir (string) if valid, None otherwise
1413

1414
  """
1415
  file_storage_dir = os.path.normpath(file_storage_dir)
1416
  base_file_storage_dir = ssconf.SimpleStore().GetFileStorageDir()
1417
  if (not os.path.commonprefix([file_storage_dir, base_file_storage_dir]) ==
1418
      base_file_storage_dir):
1419
    logger.Error("file storage directory '%s' is not under base file"
1420
                 " storage directory '%s'" %
1421
                 (file_storage_dir, base_file_storage_dir))
1422
    return None
1423
  return file_storage_dir
1424

    
1425

    
1426
def CreateFileStorageDir(file_storage_dir):
1427
  """Create file storage directory.
1428

1429
  Args:
1430
    file_storage_dir: string containing the path
1431

1432
  Returns:
1433
    tuple with first element a boolean indicating wheter dir
1434
    creation was successful or not
1435

1436
  """
1437
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1438
  result = True,
1439
  if not file_storage_dir:
1440
    result = False,
1441
  else:
1442
    if os.path.exists(file_storage_dir):
1443
      if not os.path.isdir(file_storage_dir):
1444
        logger.Error("'%s' is not a directory" % file_storage_dir)
1445
        result = False,
1446
    else:
1447
      try:
1448
        os.makedirs(file_storage_dir, 0750)
1449
      except OSError, err:
1450
        logger.Error("Cannot create file storage directory '%s': %s" %
1451
                     (file_storage_dir, err))
1452
        result = False,
1453
  return result
1454

    
1455

    
1456
def RemoveFileStorageDir(file_storage_dir):
1457
  """Remove file storage directory.
1458

1459
  Remove it only if it's empty. If not log an error and return.
1460

1461
  Args:
1462
    file_storage_dir: string containing the path
1463

1464
  Returns:
1465
    tuple with first element a boolean indicating wheter dir
1466
    removal was successful or not
1467

1468
  """
1469
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1470
  result = True,
1471
  if not file_storage_dir:
1472
    result = False,
1473
  else:
1474
    if os.path.exists(file_storage_dir):
1475
      if not os.path.isdir(file_storage_dir):
1476
        logger.Error("'%s' is not a directory" % file_storage_dir)
1477
        result = False,
1478
      # deletes dir only if empty, otherwise we want to return False
1479
      try:
1480
        os.rmdir(file_storage_dir)
1481
      except OSError, err:
1482
        logger.Error("Cannot remove file storage directory '%s': %s" %
1483
                     (file_storage_dir, err))
1484
        result = False,
1485
  return result
1486

    
1487

    
1488
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
1489
  """Rename the file storage directory.
1490

1491
  Args:
1492
    old_file_storage_dir: string containing the old path
1493
    new_file_storage_dir: string containing the new path
1494

1495
  Returns:
1496
    tuple with first element a boolean indicating wheter dir
1497
    rename was successful or not
1498

1499
  """
1500
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
1501
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
1502
  result = True,
1503
  if not old_file_storage_dir or not new_file_storage_dir:
1504
    result = False,
1505
  else:
1506
    if not os.path.exists(new_file_storage_dir):
1507
      if os.path.isdir(old_file_storage_dir):
1508
        try:
1509
          os.rename(old_file_storage_dir, new_file_storage_dir)
1510
        except OSError, err:
1511
          logger.Error("Cannot rename '%s' to '%s': %s"
1512
                       % (old_file_storage_dir, new_file_storage_dir, err))
1513
          result =  False,
1514
      else:
1515
        logger.Error("'%s' is not a directory" % old_file_storage_dir)
1516
        result = False,
1517
    else:
1518
      if os.path.exists(old_file_storage_dir):
1519
        logger.Error("Cannot rename '%s' to '%s'. Both locations exist." %
1520
                     old_file_storage_dir, new_file_storage_dir)
1521
        result = False,
1522
  return result
1523

    
1524

    
1525
class HooksRunner(object):
1526
  """Hook runner.
1527

1528
  This class is instantiated on the node side (ganeti-noded) and not on
1529
  the master side.
1530

1531
  """
1532
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1533

    
1534
  def __init__(self, hooks_base_dir=None):
1535
    """Constructor for hooks runner.
1536

1537
    Args:
1538
      - hooks_base_dir: if not None, this overrides the
1539
        constants.HOOKS_BASE_DIR (useful for unittests)
1540

1541
    """
1542
    if hooks_base_dir is None:
1543
      hooks_base_dir = constants.HOOKS_BASE_DIR
1544
    self._BASE_DIR = hooks_base_dir
1545

    
1546
  @staticmethod
1547
  def ExecHook(script, env):
1548
    """Exec one hook script.
1549

1550
    Args:
1551
     - script: the full path to the script
1552
     - env: the environment with which to exec the script
1553

1554
    """
1555
    # exec the process using subprocess and log the output
1556
    fdstdin = None
1557
    try:
1558
      fdstdin = open("/dev/null", "r")
1559
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1560
                               stderr=subprocess.STDOUT, close_fds=True,
1561
                               shell=False, cwd="/", env=env)
1562
      output = ""
1563
      try:
1564
        output = child.stdout.read(4096)
1565
        child.stdout.close()
1566
      except EnvironmentError, err:
1567
        output += "Hook script error: %s" % str(err)
1568

    
1569
      while True:
1570
        try:
1571
          result = child.wait()
1572
          break
1573
        except EnvironmentError, err:
1574
          if err.errno == errno.EINTR:
1575
            continue
1576
          raise
1577
    finally:
1578
      # try not to leak fds
1579
      for fd in (fdstdin, ):
1580
        if fd is not None:
1581
          try:
1582
            fd.close()
1583
          except EnvironmentError, err:
1584
            # just log the error
1585
            #logger.Error("While closing fd %s: %s" % (fd, err))
1586
            pass
1587

    
1588
    return result == 0, output
1589

    
1590
  def RunHooks(self, hpath, phase, env):
1591
    """Run the scripts in the hooks directory.
1592

1593
    This method will not be usually overriden by child opcodes.
1594

1595
    """
1596
    if phase == constants.HOOKS_PHASE_PRE:
1597
      suffix = "pre"
1598
    elif phase == constants.HOOKS_PHASE_POST:
1599
      suffix = "post"
1600
    else:
1601
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1602
    rr = []
1603

    
1604
    subdir = "%s-%s.d" % (hpath, suffix)
1605
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1606
    try:
1607
      dir_contents = utils.ListVisibleFiles(dir_name)
1608
    except OSError, err:
1609
      # must log
1610
      return rr
1611

    
1612
    # we use the standard python sort order,
1613
    # so 00name is the recommended naming scheme
1614
    dir_contents.sort()
1615
    for relname in dir_contents:
1616
      fname = os.path.join(dir_name, relname)
1617
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1618
          self.RE_MASK.match(relname) is not None):
1619
        rrval = constants.HKR_SKIP
1620
        output = ""
1621
      else:
1622
        result, output = self.ExecHook(fname, env)
1623
        if not result:
1624
          rrval = constants.HKR_FAIL
1625
        else:
1626
          rrval = constants.HKR_SUCCESS
1627
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1628

    
1629
    return rr
1630

    
1631

    
1632
class IAllocatorRunner(object):
1633
  """IAllocator runner.
1634

1635
  This class is instantiated on the node side (ganeti-noded) and not on
1636
  the master side.
1637

1638
  """
1639
  def Run(self, name, idata):
1640
    """Run an iallocator script.
1641

1642
    Return value: tuple of:
1643
       - run status (one of the IARUN_ constants)
1644
       - stdout
1645
       - stderr
1646
       - fail reason (as from utils.RunResult)
1647

1648
    """
1649
    alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
1650
                                  os.path.isfile)
1651
    if alloc_script is None:
1652
      return (constants.IARUN_NOTFOUND, None, None, None)
1653

    
1654
    fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
1655
    try:
1656
      os.write(fd, idata)
1657
      os.close(fd)
1658
      result = utils.RunCmd([alloc_script, fin_name])
1659
      if result.failed:
1660
        return (constants.IARUN_FAILURE, result.stdout, result.stderr,
1661
                result.fail_reason)
1662
    finally:
1663
      os.unlink(fin_name)
1664

    
1665
    return (constants.IARUN_SUCCESS, result.stdout, result.stderr, None)
1666

    
1667

    
1668
class DevCacheManager(object):
1669
  """Simple class for managing a cache of block device information.
1670

1671
  """
1672
  _DEV_PREFIX = "/dev/"
1673
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1674

    
1675
  @classmethod
1676
  def _ConvertPath(cls, dev_path):
1677
    """Converts a /dev/name path to the cache file name.
1678

1679
    This replaces slashes with underscores and strips the /dev
1680
    prefix. It then returns the full path to the cache file
1681

1682
    """
1683
    if dev_path.startswith(cls._DEV_PREFIX):
1684
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1685
    dev_path = dev_path.replace("/", "_")
1686
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1687
    return fpath
1688

    
1689
  @classmethod
1690
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1691
    """Updates the cache information for a given device.
1692

1693
    """
1694
    if dev_path is None:
1695
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1696
      return
1697
    fpath = cls._ConvertPath(dev_path)
1698
    if on_primary:
1699
      state = "primary"
1700
    else:
1701
      state = "secondary"
1702
    if iv_name is None:
1703
      iv_name = "not_visible"
1704
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1705
    try:
1706
      utils.WriteFile(fpath, data=fdata)
1707
    except EnvironmentError, err:
1708
      logger.Error("Can't update bdev cache for %s, error %s" %
1709
                   (dev_path, str(err)))
1710

    
1711
  @classmethod
1712
  def RemoveCache(cls, dev_path):
1713
    """Remove data for a dev_path.
1714

1715
    """
1716
    if dev_path is None:
1717
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1718
      return
1719
    fpath = cls._ConvertPath(dev_path)
1720
    try:
1721
      utils.RemoveFile(fpath)
1722
    except EnvironmentError, err:
1723
      logger.Error("Can't update bdev cache for %s, error %s" %
1724
                   (dev_path, str(err)))