Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ df4c2628

History | View | Annotate | Download (52.1 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
import random
34

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

    
45

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

    
49

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

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

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

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

    
65
  return True
66

    
67

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

71
  This runs the master stop script.
72

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

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

    
81
  return True
82

    
83

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

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

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

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

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

    
110
  utils.AddAuthorizedKey(auth_keys, sshpub)
111

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

    
114
  return True
115

    
116

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

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

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

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

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

    
142
  # Return a reassuring string to the caller, and quit
143
  raise errors.QuitGanetiException(False, 'Shutdown scheduled')
144

    
145

    
146
def GetNodeInfo(vgname):
147
  """Gives back a hash with different informations about the node.
148

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

159
  """
160
  outputarray = {}
161
  vginfo = _GetVGInfo(vgname)
162
  outputarray['vg_size'] = vginfo['vg_size']
163
  outputarray['vg_free'] = vginfo['vg_free']
164

    
165
  hyper = hypervisor.GetHypervisor()
166
  hyp_info = hyper.GetNodeInfo()
167
  if hyp_info is not None:
168
    outputarray.update(hyp_info)
169

    
170
  f = open("/proc/sys/kernel/random/boot_id", 'r')
171
  try:
172
    outputarray["bootid"] = f.read(128).rstrip("\n")
173
  finally:
174
    f.close()
175

    
176
  return outputarray
177

    
178

    
179
def VerifyNode(what):
180
  """Verify the status of the local node.
181

182
  Args:
183
    what - a dictionary of things to check:
184
      'filelist' : list of files for which to compute checksums
185
      'nodelist' : list of nodes we should check communication with
186
      'hypervisor': run the hypervisor-specific verify
187

188
  Requested files on local node are checksummed and the result returned.
189

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

196
  """
197
  result = {}
198

    
199
  if 'hypervisor' in what:
200
    result['hypervisor'] = hypervisor.GetHypervisor().Verify()
201

    
202
  if 'filelist' in what:
203
    result['filelist'] = utils.FingerprintFiles(what['filelist'])
204

    
205
  if 'nodelist' in what:
206
    result['nodelist'] = {}
207
    random.shuffle(what['nodelist'])
208
    for node in what['nodelist']:
209
      success, message = _GetSshRunner().VerifyNodeHostname(node)
210
      if not success:
211
        result['nodelist'][node] = message
212
  if 'node-net-test' in what:
213
    result['node-net-test'] = {}
214
    my_name = utils.HostInfo().name
215
    my_pip = my_sip = None
216
    for name, pip, sip in what['node-net-test']:
217
      if name == my_name:
218
        my_pip = pip
219
        my_sip = sip
220
        break
221
    if not my_pip:
222
      result['node-net-test'][my_name] = ("Can't find my own"
223
                                          " primary/secondary IP"
224
                                          " in the node list")
225
    else:
226
      port = ssconf.SimpleStore().GetNodeDaemonPort()
227
      for name, pip, sip in what['node-net-test']:
228
        fail = []
229
        if not utils.TcpPing(pip, port, source=my_pip):
230
          fail.append("primary")
231
        if sip != pip:
232
          if not utils.TcpPing(sip, port, source=my_sip):
233
            fail.append("secondary")
234
        if fail:
235
          result['node-net-test'][name] = ("failure using the %s"
236
                                           " interface(s)" %
237
                                           " and ".join(fail))
238

    
239
  return result
240

    
241

    
242
def GetVolumeList(vg_name):
243
  """Compute list of logical volumes and their size.
244

245
  Returns:
246
    dictionary of all partions (key) with their size (in MiB), inactive
247
    and online status:
248
    {'test1': ('20.06', True, True)}
249

250
  """
251
  lvs = {}
252
  sep = '|'
253
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
254
                         "--separator=%s" % sep,
255
                         "-olv_name,lv_size,lv_attr", vg_name])
256
  if result.failed:
257
    logger.Error("Failed to list logical volumes, lvs output: %s" %
258
                 result.output)
259
    return result.output
260

    
261
  valid_line_re = re.compile("^ *([^|]+)\|([0-9.]+)\|([^|]{6})\|?$")
262
  for line in result.stdout.splitlines():
263
    line = line.strip()
264
    match = valid_line_re.match(line)
265
    if not match:
266
      logger.Error("Invalid line returned from lvs output: '%s'" % line)
267
      continue
268
    name, size, attr = match.groups()
269
    inactive = attr[4] == '-'
270
    online = attr[5] == 'o'
271
    lvs[name] = (size, inactive, online)
272

    
273
  return lvs
274

    
275

    
276
def ListVolumeGroups():
277
  """List the volume groups and their size.
278

279
  Returns:
280
    Dictionary with keys volume name and values the size of the volume
281

282
  """
283
  return utils.ListVolumeGroups()
284

    
285

    
286
def NodeVolumes():
287
  """List all volumes on this node.
288

289
  """
290
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
291
                         "--separator=|",
292
                         "--options=lv_name,lv_size,devices,vg_name"])
293
  if result.failed:
294
    logger.Error("Failed to list logical volumes, lvs output: %s" %
295
                 result.output)
296
    return {}
297

    
298
  def parse_dev(dev):
299
    if '(' in dev:
300
      return dev.split('(')[0]
301
    else:
302
      return dev
303

    
304
  def map_line(line):
305
    return {
306
      'name': line[0].strip(),
307
      'size': line[1].strip(),
308
      'dev': parse_dev(line[2].strip()),
309
      'vg': line[3].strip(),
310
    }
311

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

    
314

    
315
def BridgesExist(bridges_list):
316
  """Check if a list of bridges exist on the current node.
317

318
  Returns:
319
    True if all of them exist, false otherwise
320

321
  """
322
  for bridge in bridges_list:
323
    if not utils.BridgeExists(bridge):
324
      return False
325

    
326
  return True
327

    
328

    
329
def GetInstanceList():
330
  """Provides a list of instances.
331

332
  Returns:
333
    A list of all running instances on the current node
334
    - instance1.example.com
335
    - instance2.example.com
336

337
  """
338
  try:
339
    names = hypervisor.GetHypervisor().ListInstances()
340
  except errors.HypervisorError, err:
341
    logger.Error("error enumerating instances: %s" % str(err))
342
    raise
343

    
344
  return names
345

    
346

    
347
def GetInstanceInfo(instance):
348
  """Gives back the informations about an instance as a dictionary.
349

350
  Args:
351
    instance: name of the instance (ex. instance1.example.com)
352

353
  Returns:
354
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
355
    where
356
    memory: memory size of instance (int)
357
    state: xen state of instance (string)
358
    time: cpu time of instance (float)
359

360
  """
361
  output = {}
362

    
363
  iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
364
  if iinfo is not None:
365
    output['memory'] = iinfo[2]
366
    output['state'] = iinfo[4]
367
    output['time'] = iinfo[5]
368

    
369
  return output
370

    
371

    
372
def GetAllInstancesInfo():
373
  """Gather data about all instances.
374

375
  This is the equivalent of `GetInstanceInfo()`, except that it
376
  computes data for all instances at once, thus being faster if one
377
  needs data about more than one instance.
378

379
  Returns: a dictionary of dictionaries, keys being the instance name,
380
    and with values:
381
    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
382
    where
383
    memory: memory size of instance (int)
384
    state: xen state of instance (string)
385
    time: cpu time of instance (float)
386
    vcpus: the number of cpus
387

388
  """
389
  output = {}
390

    
391
  iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
392
  if iinfo:
393
    for name, inst_id, memory, vcpus, state, times in iinfo:
394
      output[name] = {
395
        'memory': memory,
396
        'vcpus': vcpus,
397
        'state': state,
398
        'time': times,
399
        }
400

    
401
  return output
402

    
403

    
404
def AddOSToInstance(instance, os_disk, swap_disk):
405
  """Add an OS to an instance.
406

407
  Args:
408
    instance: the instance object
409
    os_disk: the instance-visible name of the os device
410
    swap_disk: the instance-visible name of the swap device
411

412
  """
413
  inst_os = OSFromDisk(instance.os)
414

    
415
  create_script = inst_os.create_script
416

    
417
  os_device = instance.FindDisk(os_disk)
418
  if os_device is None:
419
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
420
    return False
421

    
422
  swap_device = instance.FindDisk(swap_disk)
423
  if swap_device is None:
424
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
425
    return False
426

    
427
  real_os_dev = _RecursiveFindBD(os_device)
428
  if real_os_dev is None:
429
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
430
                                  str(os_device))
431
  real_os_dev.Open()
432

    
433
  real_swap_dev = _RecursiveFindBD(swap_device)
434
  if real_swap_dev is None:
435
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
436
                                  str(swap_device))
437
  real_swap_dev.Open()
438

    
439
  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
440
                                     instance.name, int(time.time()))
441
  if not os.path.exists(constants.LOG_OS_DIR):
442
    os.mkdir(constants.LOG_OS_DIR, 0750)
443

    
444
  command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
445
                                inst_os.path, create_script, instance.name,
446
                                real_os_dev.dev_path, real_swap_dev.dev_path,
447
                                logfile)
448

    
449
  result = utils.RunCmd(command)
450
  if result.failed:
451
    logger.Error("os create command '%s' returned error: %s, logfile: %s,"
452
                 " output: %s" %
453
                 (command, result.fail_reason, logfile, result.output))
454
    return False
455

    
456
  return True
457

    
458

    
459
def RunRenameInstance(instance, old_name, os_disk, swap_disk):
460
  """Run the OS rename script for an instance.
461

462
  Args:
463
    instance: the instance object
464
    old_name: the old name of the instance
465
    os_disk: the instance-visible name of the os device
466
    swap_disk: the instance-visible name of the swap device
467

468
  """
469
  inst_os = OSFromDisk(instance.os)
470

    
471
  script = inst_os.rename_script
472

    
473
  os_device = instance.FindDisk(os_disk)
474
  if os_device is None:
475
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
476
    return False
477

    
478
  swap_device = instance.FindDisk(swap_disk)
479
  if swap_device is None:
480
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
481
    return False
482

    
483
  real_os_dev = _RecursiveFindBD(os_device)
484
  if real_os_dev is None:
485
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
486
                                  str(os_device))
487
  real_os_dev.Open()
488

    
489
  real_swap_dev = _RecursiveFindBD(swap_device)
490
  if real_swap_dev is None:
491
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
492
                                  str(swap_device))
493
  real_swap_dev.Open()
494

    
495
  logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
496
                                           old_name,
497
                                           instance.name, int(time.time()))
498
  if not os.path.exists(constants.LOG_OS_DIR):
499
    os.mkdir(constants.LOG_OS_DIR, 0750)
500

    
501
  command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
502
                                inst_os.path, script, old_name, instance.name,
503
                                real_os_dev.dev_path, real_swap_dev.dev_path,
504
                                logfile)
505

    
506
  result = utils.RunCmd(command)
507

    
508
  if result.failed:
509
    logger.Error("os create command '%s' returned error: %s"
510
                 " output: %s" %
511
                 (command, result.fail_reason, result.output))
512
    return False
513

    
514
  return True
515

    
516

    
517
def _GetVGInfo(vg_name):
518
  """Get informations about the volume group.
519

520
  Args:
521
    vg_name: the volume group
522

523
  Returns:
524
    { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
525
    where
526
    vg_size is the total size of the volume group in MiB
527
    vg_free is the free size of the volume group in MiB
528
    pv_count are the number of physical disks in that vg
529

530
  If an error occurs during gathering of data, we return the same dict
531
  with keys all set to None.
532

533
  """
534
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
535

    
536
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
537
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
538

    
539
  if retval.failed:
540
    errmsg = "volume group %s not present" % vg_name
541
    logger.Error(errmsg)
542
    return retdic
543
  valarr = retval.stdout.strip().rstrip(':').split(':')
544
  if len(valarr) == 3:
545
    try:
546
      retdic = {
547
        "vg_size": int(round(float(valarr[0]), 0)),
548
        "vg_free": int(round(float(valarr[1]), 0)),
549
        "pv_count": int(valarr[2]),
550
        }
551
    except ValueError, err:
552
      logger.Error("Fail to parse vgs output: %s" % str(err))
553
  else:
554
    logger.Error("vgs output has the wrong number of fields (expected"
555
                 " three): %s" % str(valarr))
556
  return retdic
557

    
558

    
559
def _GatherBlockDevs(instance):
560
  """Set up an instance's block device(s).
561

562
  This is run on the primary node at instance startup. The block
563
  devices must be already assembled.
564

565
  """
566
  block_devices = []
567
  for disk in instance.disks:
568
    device = _RecursiveFindBD(disk)
569
    if device is None:
570
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
571
                                    str(disk))
572
    device.Open()
573
    block_devices.append((disk, device))
574
  return block_devices
575

    
576

    
577
def StartInstance(instance, extra_args):
578
  """Start an instance.
579

580
  Args:
581
    instance - name of instance to start.
582

583
  """
584
  running_instances = GetInstanceList()
585

    
586
  if instance.name in running_instances:
587
    return True
588

    
589
  block_devices = _GatherBlockDevs(instance)
590
  hyper = hypervisor.GetHypervisor()
591

    
592
  try:
593
    hyper.StartInstance(instance, block_devices, extra_args)
594
  except errors.HypervisorError, err:
595
    logger.Error("Failed to start instance: %s" % err)
596
    return False
597

    
598
  return True
599

    
600

    
601
def ShutdownInstance(instance):
602
  """Shut an instance down.
603

604
  Args:
605
    instance - name of instance to shutdown.
606

607
  """
608
  running_instances = GetInstanceList()
609

    
610
  if instance.name not in running_instances:
611
    return True
612

    
613
  hyper = hypervisor.GetHypervisor()
614
  try:
615
    hyper.StopInstance(instance)
616
  except errors.HypervisorError, err:
617
    logger.Error("Failed to stop instance: %s" % err)
618
    return False
619

    
620
  # test every 10secs for 2min
621
  shutdown_ok = False
622

    
623
  time.sleep(1)
624
  for dummy in range(11):
625
    if instance.name not in GetInstanceList():
626
      break
627
    time.sleep(10)
628
  else:
629
    # the shutdown did not succeed
630
    logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
631

    
632
    try:
633
      hyper.StopInstance(instance, force=True)
634
    except errors.HypervisorError, err:
635
      logger.Error("Failed to stop instance: %s" % err)
636
      return False
637

    
638
    time.sleep(1)
639
    if instance.name in GetInstanceList():
640
      logger.Error("could not shutdown instance '%s' even by destroy")
641
      return False
642

    
643
  return True
644

    
645

    
646
def RebootInstance(instance, reboot_type, extra_args):
647
  """Reboot an instance.
648

649
  Args:
650
    instance    - name of instance to reboot
651
    reboot_type - how to reboot [soft,hard,full]
652

653
  """
654
  running_instances = GetInstanceList()
655

    
656
  if instance.name not in running_instances:
657
    logger.Error("Cannot reboot instance that is not running")
658
    return False
659

    
660
  hyper = hypervisor.GetHypervisor()
661
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
662
    try:
663
      hyper.RebootInstance(instance)
664
    except errors.HypervisorError, err:
665
      logger.Error("Failed to soft reboot instance: %s" % err)
666
      return False
667
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
668
    try:
669
      ShutdownInstance(instance)
670
      StartInstance(instance, extra_args)
671
    except errors.HypervisorError, err:
672
      logger.Error("Failed to hard reboot instance: %s" % err)
673
      return False
674
  else:
675
    raise errors.ParameterError("reboot_type invalid")
676

    
677

    
678
  return True
679

    
680

    
681
def MigrateInstance(instance, target, live):
682
  """Migrates an instance to another node.
683

684
  """
685
  hyper = hypervisor.GetHypervisor()
686

    
687
  try:
688
    hyper.MigrateInstance(instance, target, live)
689
  except errors.HypervisorError, err:
690
    msg = "Failed to migrate instance: %s" % str(err)
691
    logger.Error(msg)
692
    return (False, msg)
693
  return (True, "Migration successfull")
694

    
695

    
696
def CreateBlockDevice(disk, size, owner, on_primary, info):
697
  """Creates a block device for an instance.
698

699
  Args:
700
   disk: a ganeti.objects.Disk object
701
   size: the size of the physical underlying device
702
   owner: a string with the name of the instance
703
   on_primary: a boolean indicating if it is the primary node or not
704
   info: string that will be sent to the physical device creation
705

706
  Returns:
707
    the new unique_id of the device (this can sometime be
708
    computed only after creation), or None. On secondary nodes,
709
    it's not required to return anything.
710

711
  """
712
  clist = []
713
  if disk.children:
714
    for child in disk.children:
715
      crdev = _RecursiveAssembleBD(child, owner, on_primary)
716
      if on_primary or disk.AssembleOnSecondary():
717
        # we need the children open in case the device itself has to
718
        # be assembled
719
        crdev.Open()
720
      clist.append(crdev)
721
  try:
722
    device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
723
    if device is not None:
724
      logger.Info("removing existing device %s" % disk)
725
      device.Remove()
726
  except errors.BlockDeviceError, err:
727
    pass
728

    
729
  device = bdev.Create(disk.dev_type, disk.physical_id,
730
                       clist, size)
731
  if device is None:
732
    raise ValueError("Can't create child device for %s, %s" %
733
                     (disk, size))
734
  if on_primary or disk.AssembleOnSecondary():
735
    if not device.Assemble():
736
      errorstring = "Can't assemble device after creation"
737
      logger.Error(errorstring)
738
      raise errors.BlockDeviceError("%s, very unusual event - check the node"
739
                                    " daemon logs" % errorstring)
740
    device.SetSyncSpeed(constants.SYNC_SPEED)
741
    if on_primary or disk.OpenOnSecondary():
742
      device.Open(force=True)
743
    DevCacheManager.UpdateCache(device.dev_path, owner,
744
                                on_primary, disk.iv_name)
745

    
746
  device.SetInfo(info)
747

    
748
  physical_id = device.unique_id
749
  return physical_id
750

    
751

    
752
def RemoveBlockDevice(disk):
753
  """Remove a block device.
754

755
  This is intended to be called recursively.
756

757
  """
758
  try:
759
    # since we are removing the device, allow a partial match
760
    # this allows removal of broken mirrors
761
    rdev = _RecursiveFindBD(disk, allow_partial=True)
762
  except errors.BlockDeviceError, err:
763
    # probably can't attach
764
    logger.Info("Can't attach to device %s in remove" % disk)
765
    rdev = None
766
  if rdev is not None:
767
    r_path = rdev.dev_path
768
    result = rdev.Remove()
769
    if result:
770
      DevCacheManager.RemoveCache(r_path)
771
  else:
772
    result = True
773
  if disk.children:
774
    for child in disk.children:
775
      result = result and RemoveBlockDevice(child)
776
  return result
777

    
778

    
779
def _RecursiveAssembleBD(disk, owner, as_primary):
780
  """Activate a block device for an instance.
781

782
  This is run on the primary and secondary nodes for an instance.
783

784
  This function is called recursively.
785

786
  Args:
787
    disk: a objects.Disk object
788
    as_primary: if we should make the block device read/write
789

790
  Returns:
791
    the assembled device or None (in case no device was assembled)
792

793
  If the assembly is not successful, an exception is raised.
794

795
  """
796
  children = []
797
  if disk.children:
798
    mcn = disk.ChildrenNeeded()
799
    if mcn == -1:
800
      mcn = 0 # max number of Nones allowed
801
    else:
802
      mcn = len(disk.children) - mcn # max number of Nones
803
    for chld_disk in disk.children:
804
      try:
805
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
806
      except errors.BlockDeviceError, err:
807
        if children.count(None) >= mcn:
808
          raise
809
        cdev = None
810
        logger.Debug("Error in child activation: %s" % str(err))
811
      children.append(cdev)
812

    
813
  if as_primary or disk.AssembleOnSecondary():
814
    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
815
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
816
    result = r_dev
817
    if as_primary or disk.OpenOnSecondary():
818
      r_dev.Open()
819
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
820
                                as_primary, disk.iv_name)
821

    
822
  else:
823
    result = True
824
  return result
825

    
826

    
827
def AssembleBlockDevice(disk, owner, as_primary):
828
  """Activate a block device for an instance.
829

830
  This is a wrapper over _RecursiveAssembleBD.
831

832
  Returns:
833
    a /dev path for primary nodes
834
    True for secondary nodes
835

836
  """
837
  result = _RecursiveAssembleBD(disk, owner, as_primary)
838
  if isinstance(result, bdev.BlockDev):
839
    result = result.dev_path
840
  return result
841

    
842

    
843
def ShutdownBlockDevice(disk):
844
  """Shut down a block device.
845

846
  First, if the device is assembled (can `Attach()`), then the device
847
  is shutdown. Then the children of the device are shutdown.
848

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

853
  """
854
  r_dev = _RecursiveFindBD(disk)
855
  if r_dev is not None:
856
    r_path = r_dev.dev_path
857
    result = r_dev.Shutdown()
858
    if result:
859
      DevCacheManager.RemoveCache(r_path)
860
  else:
861
    result = True
862
  if disk.children:
863
    for child in disk.children:
864
      result = result and ShutdownBlockDevice(child)
865
  return result
866

    
867

    
868
def MirrorAddChildren(parent_cdev, new_cdevs):
869
  """Extend a mirrored block device.
870

871
  """
872
  parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
873
  if parent_bdev is None:
874
    logger.Error("Can't find parent device")
875
    return False
876
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
877
  if new_bdevs.count(None) > 0:
878
    logger.Error("Can't find new device(s) to add: %s:%s" %
879
                 (new_bdevs, new_cdevs))
880
    return False
881
  parent_bdev.AddChildren(new_bdevs)
882
  return True
883

    
884

    
885
def MirrorRemoveChildren(parent_cdev, new_cdevs):
886
  """Shrink a mirrored block device.
887

888
  """
889
  parent_bdev = _RecursiveFindBD(parent_cdev)
890
  if parent_bdev is None:
891
    logger.Error("Can't find parent in remove children: %s" % parent_cdev)
892
    return False
893
  devs = []
894
  for disk in new_cdevs:
895
    rpath = disk.StaticDevPath()
896
    if rpath is None:
897
      bd = _RecursiveFindBD(disk)
898
      if bd is None:
899
        logger.Error("Can't find dynamic device %s while removing children" %
900
                     disk)
901
        return False
902
      else:
903
        devs.append(bd.dev_path)
904
    else:
905
      devs.append(rpath)
906
  parent_bdev.RemoveChildren(devs)
907
  return True
908

    
909

    
910
def GetMirrorStatus(disks):
911
  """Get the mirroring status of a list of devices.
912

913
  Args:
914
    disks: list of `objects.Disk`
915

916
  Returns:
917
    list of (mirror_done, estimated_time) tuples, which
918
    are the result of bdev.BlockDevice.CombinedSyncStatus()
919

920
  """
921
  stats = []
922
  for dsk in disks:
923
    rbd = _RecursiveFindBD(dsk)
924
    if rbd is None:
925
      raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
926
    stats.append(rbd.CombinedSyncStatus())
927
  return stats
928

    
929

    
930
def _RecursiveFindBD(disk, allow_partial=False):
931
  """Check if a device is activated.
932

933
  If so, return informations about the real device.
934

935
  Args:
936
    disk: the objects.Disk instance
937
    allow_partial: don't abort the find if a child of the
938
                   device can't be found; this is intended to be
939
                   used when repairing mirrors
940

941
  Returns:
942
    None if the device can't be found
943
    otherwise the device instance
944

945
  """
946
  children = []
947
  if disk.children:
948
    for chdisk in disk.children:
949
      children.append(_RecursiveFindBD(chdisk))
950

    
951
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
952

    
953

    
954
def FindBlockDevice(disk):
955
  """Check if a device is activated.
956

957
  If so, return informations about the real device.
958

959
  Args:
960
    disk: the objects.Disk instance
961
  Returns:
962
    None if the device can't be found
963
    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
964

965
  """
966
  rbd = _RecursiveFindBD(disk)
967
  if rbd is None:
968
    return rbd
969
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
970

    
971

    
972
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
973
  """Write a file to the filesystem.
974

975
  This allows the master to overwrite(!) a file. It will only perform
976
  the operation if the file belongs to a list of configuration files.
977

978
  """
979
  if not os.path.isabs(file_name):
980
    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
981
                 file_name)
982
    return False
983

    
984
  allowed_files = [
985
    constants.CLUSTER_CONF_FILE,
986
    constants.ETC_HOSTS,
987
    constants.SSH_KNOWN_HOSTS_FILE,
988
    constants.VNC_PASSWORD_FILE,
989
    ]
990
  allowed_files.extend(ssconf.SimpleStore().GetFileList())
991
  if file_name not in allowed_files:
992
    logger.Error("Filename passed to UploadFile not in allowed"
993
                 " upload targets: '%s'" % file_name)
994
    return False
995

    
996
  utils.WriteFile(file_name, data=data, mode=mode, uid=uid, gid=gid,
997
                  atime=atime, mtime=mtime)
998
  return True
999

    
1000

    
1001
def _ErrnoOrStr(err):
1002
  """Format an EnvironmentError exception.
1003

1004
  If the `err` argument has an errno attribute, it will be looked up
1005
  and converted into a textual EXXXX description. Otherwise the string
1006
  representation of the error will be returned.
1007

1008
  """
1009
  if hasattr(err, 'errno'):
1010
    detail = errno.errorcode[err.errno]
1011
  else:
1012
    detail = str(err)
1013
  return detail
1014

    
1015

    
1016
def _OSOndiskVersion(name, os_dir):
1017
  """Compute and return the API version of a given OS.
1018

1019
  This function will try to read the API version of the os given by
1020
  the 'name' parameter and residing in the 'os_dir' directory.
1021

1022
  Return value will be either an integer denoting the version or None in the
1023
  case when this is not a valid OS name.
1024

1025
  """
1026
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
1027

    
1028
  try:
1029
    st = os.stat(api_file)
1030
  except EnvironmentError, err:
1031
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
1032
                           " found (%s)" % _ErrnoOrStr(err))
1033

    
1034
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1035
    raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
1036
                           " a regular file")
1037

    
1038
  try:
1039
    f = open(api_file)
1040
    try:
1041
      api_version = f.read(256)
1042
    finally:
1043
      f.close()
1044
  except EnvironmentError, err:
1045
    raise errors.InvalidOS(name, os_dir, "error while reading the"
1046
                           " API version (%s)" % _ErrnoOrStr(err))
1047

    
1048
  api_version = api_version.strip()
1049
  try:
1050
    api_version = int(api_version)
1051
  except (TypeError, ValueError), err:
1052
    raise errors.InvalidOS(name, os_dir,
1053
                           "API version is not integer (%s)" % str(err))
1054

    
1055
  return api_version
1056

    
1057

    
1058
def DiagnoseOS(top_dirs=None):
1059
  """Compute the validity for all OSes.
1060

1061
  Returns an OS object for each name in all the given top directories
1062
  (if not given defaults to constants.OS_SEARCH_PATH)
1063

1064
  Returns:
1065
    list of OS objects
1066

1067
  """
1068
  if top_dirs is None:
1069
    top_dirs = constants.OS_SEARCH_PATH
1070

    
1071
  result = []
1072
  for dir_name in top_dirs:
1073
    if os.path.isdir(dir_name):
1074
      try:
1075
        f_names = utils.ListVisibleFiles(dir_name)
1076
      except EnvironmentError, err:
1077
        logger.Error("Can't list the OS directory %s: %s" %
1078
                     (dir_name, str(err)))
1079
        break
1080
      for name in f_names:
1081
        try:
1082
          os_inst = OSFromDisk(name, base_dir=dir_name)
1083
          result.append(os_inst)
1084
        except errors.InvalidOS, err:
1085
          result.append(objects.OS.FromInvalidOS(err))
1086

    
1087
  return result
1088

    
1089

    
1090
def OSFromDisk(name, base_dir=None):
1091
  """Create an OS instance from disk.
1092

1093
  This function will return an OS instance if the given name is a
1094
  valid OS name. Otherwise, it will raise an appropriate
1095
  `errors.InvalidOS` exception, detailing why this is not a valid
1096
  OS.
1097

1098
  Args:
1099
    os_dir: Directory containing the OS scripts. Defaults to a search
1100
            in all the OS_SEARCH_PATH directories.
1101

1102
  """
1103

    
1104
  if base_dir is None:
1105
    os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1106
    if os_dir is None:
1107
      raise errors.InvalidOS(name, None, "OS dir not found in search path")
1108
  else:
1109
    os_dir = os.path.sep.join([base_dir, name])
1110

    
1111
  api_version = _OSOndiskVersion(name, os_dir)
1112

    
1113
  if api_version != constants.OS_API_VERSION:
1114
    raise errors.InvalidOS(name, os_dir, "API version mismatch"
1115
                           " (found %s want %s)"
1116
                           % (api_version, constants.OS_API_VERSION))
1117

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

    
1121
  for script in os_scripts:
1122
    os_scripts[script] = os.path.sep.join([os_dir, script])
1123

    
1124
    try:
1125
      st = os.stat(os_scripts[script])
1126
    except EnvironmentError, err:
1127
      raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1128
                             (script, _ErrnoOrStr(err)))
1129

    
1130
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1131
      raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1132
                             script)
1133

    
1134
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1135
      raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1136
                             script)
1137

    
1138

    
1139
  return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1140
                    create_script=os_scripts['create'],
1141
                    export_script=os_scripts['export'],
1142
                    import_script=os_scripts['import'],
1143
                    rename_script=os_scripts['rename'],
1144
                    api_version=api_version)
1145

    
1146

    
1147
def GrowBlockDevice(disk, amount):
1148
  """Grow a stack of block devices.
1149

1150
  This function is called recursively, with the childrens being the
1151
  first one resize.
1152

1153
  Args:
1154
    disk: the disk to be grown
1155

1156
  Returns: a tuple of (status, result), with:
1157
    status: the result (true/false) of the operation
1158
    result: the error message if the operation failed, otherwise not used
1159

1160
  """
1161
  r_dev = _RecursiveFindBD(disk)
1162
  if r_dev is None:
1163
    return False, "Cannot find block device %s" % (disk,)
1164

    
1165
  try:
1166
    r_dev.Grow(amount)
1167
  except errors.BlockDeviceError, err:
1168
    return False, str(err)
1169

    
1170
  return True, None
1171

    
1172

    
1173
def SnapshotBlockDevice(disk):
1174
  """Create a snapshot copy of a block device.
1175

1176
  This function is called recursively, and the snapshot is actually created
1177
  just for the leaf lvm backend device.
1178

1179
  Args:
1180
    disk: the disk to be snapshotted
1181

1182
  Returns:
1183
    a config entry for the actual lvm device snapshotted.
1184

1185
  """
1186
  if disk.children:
1187
    if len(disk.children) == 1:
1188
      # only one child, let's recurse on it
1189
      return SnapshotBlockDevice(disk.children[0])
1190
    else:
1191
      # more than one child, choose one that matches
1192
      for child in disk.children:
1193
        if child.size == disk.size:
1194
          # return implies breaking the loop
1195
          return SnapshotBlockDevice(child)
1196
  elif disk.dev_type == constants.LD_LV:
1197
    r_dev = _RecursiveFindBD(disk)
1198
    if r_dev is not None:
1199
      # let's stay on the safe side and ask for the full size, for now
1200
      return r_dev.Snapshot(disk.size)
1201
    else:
1202
      return None
1203
  else:
1204
    raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1205
                                 " '%s' of type '%s'" %
1206
                                 (disk.unique_id, disk.dev_type))
1207

    
1208

    
1209
def ExportSnapshot(disk, dest_node, instance):
1210
  """Export a block device snapshot to a remote node.
1211

1212
  Args:
1213
    disk: the snapshot block device
1214
    dest_node: the node to send the image to
1215
    instance: instance being exported
1216

1217
  Returns:
1218
    True if successful, False otherwise.
1219

1220
  """
1221
  inst_os = OSFromDisk(instance.os)
1222
  export_script = inst_os.export_script
1223

    
1224
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1225
                                     instance.name, int(time.time()))
1226
  if not os.path.exists(constants.LOG_OS_DIR):
1227
    os.mkdir(constants.LOG_OS_DIR, 0750)
1228

    
1229
  real_os_dev = _RecursiveFindBD(disk)
1230
  if real_os_dev is None:
1231
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1232
                                  str(disk))
1233
  real_os_dev.Open()
1234

    
1235
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1236
  destfile = disk.physical_id[1]
1237

    
1238
  # the target command is built out of three individual commands,
1239
  # which are joined by pipes; we check each individual command for
1240
  # valid parameters
1241

    
1242
  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1243
                               export_script, instance.name,
1244
                               real_os_dev.dev_path, logfile)
1245

    
1246
  comprcmd = "gzip"
1247

    
1248
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1249
                                destdir, destdir, destfile)
1250
  remotecmd = _GetSshRunner().BuildCmd(dest_node, constants.GANETI_RUNAS,
1251
                                       destcmd)
1252

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

    
1256
  result = utils.RunCmd(command)
1257

    
1258
  if result.failed:
1259
    logger.Error("os snapshot export command '%s' returned error: %s"
1260
                 " output: %s" %
1261
                 (command, result.fail_reason, result.output))
1262
    return False
1263

    
1264
  return True
1265

    
1266

    
1267
def FinalizeExport(instance, snap_disks):
1268
  """Write out the export configuration information.
1269

1270
  Args:
1271
    instance: instance configuration
1272
    snap_disks: snapshot block devices
1273

1274
  Returns:
1275
    False in case of error, True otherwise.
1276

1277
  """
1278
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1279
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1280

    
1281
  config = objects.SerializableConfigParser()
1282

    
1283
  config.add_section(constants.INISECT_EXP)
1284
  config.set(constants.INISECT_EXP, 'version', '0')
1285
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1286
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1287
  config.set(constants.INISECT_EXP, 'os', instance.os)
1288
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1289

    
1290
  config.add_section(constants.INISECT_INS)
1291
  config.set(constants.INISECT_INS, 'name', instance.name)
1292
  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1293
  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1294
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1295

    
1296
  nic_count = 0
1297
  for nic_count, nic in enumerate(instance.nics):
1298
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1299
               nic_count, '%s' % nic.mac)
1300
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1301
    config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count, '%s' % nic.bridge)
1302
  # TODO: redundant: on load can read nics until it doesn't exist
1303
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1304

    
1305
  disk_count = 0
1306
  for disk_count, disk in enumerate(snap_disks):
1307
    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1308
               ('%s' % disk.iv_name))
1309
    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1310
               ('%s' % disk.physical_id[1]))
1311
    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1312
               ('%d' % disk.size))
1313
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1314

    
1315
  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1316
  cfo = open(cff, 'w')
1317
  try:
1318
    config.write(cfo)
1319
  finally:
1320
    cfo.close()
1321

    
1322
  shutil.rmtree(finaldestdir, True)
1323
  shutil.move(destdir, finaldestdir)
1324

    
1325
  return True
1326

    
1327

    
1328
def ExportInfo(dest):
1329
  """Get export configuration information.
1330

1331
  Args:
1332
    dest: directory containing the export
1333

1334
  Returns:
1335
    A serializable config file containing the export info.
1336

1337
  """
1338
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1339

    
1340
  config = objects.SerializableConfigParser()
1341
  config.read(cff)
1342

    
1343
  if (not config.has_section(constants.INISECT_EXP) or
1344
      not config.has_section(constants.INISECT_INS)):
1345
    return None
1346

    
1347
  return config
1348

    
1349

    
1350
def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1351
  """Import an os image into an instance.
1352

1353
  Args:
1354
    instance: the instance object
1355
    os_disk: the instance-visible name of the os device
1356
    swap_disk: the instance-visible name of the swap device
1357
    src_node: node holding the source image
1358
    src_image: path to the source image on src_node
1359

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

1363
  """
1364
  inst_os = OSFromDisk(instance.os)
1365
  import_script = inst_os.import_script
1366

    
1367
  os_device = instance.FindDisk(os_disk)
1368
  if os_device is None:
1369
    logger.Error("Can't find this device-visible name '%s'" % os_disk)
1370
    return False
1371

    
1372
  swap_device = instance.FindDisk(swap_disk)
1373
  if swap_device is None:
1374
    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1375
    return False
1376

    
1377
  real_os_dev = _RecursiveFindBD(os_device)
1378
  if real_os_dev is None:
1379
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1380
                                  str(os_device))
1381
  real_os_dev.Open()
1382

    
1383
  real_swap_dev = _RecursiveFindBD(swap_device)
1384
  if real_swap_dev is None:
1385
    raise errors.BlockDeviceError("Block device '%s' is not set up" %
1386
                                  str(swap_device))
1387
  real_swap_dev.Open()
1388

    
1389
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1390
                                        instance.name, int(time.time()))
1391
  if not os.path.exists(constants.LOG_OS_DIR):
1392
    os.mkdir(constants.LOG_OS_DIR, 0750)
1393

    
1394
  destcmd = utils.BuildShellCmd('cat %s', src_image)
1395
  remotecmd = _GetSshRunner().BuildCmd(src_node, constants.GANETI_RUNAS,
1396
                                       destcmd)
1397

    
1398
  comprcmd = "gunzip"
1399
  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1400
                               inst_os.path, import_script, instance.name,
1401
                               real_os_dev.dev_path, real_swap_dev.dev_path,
1402
                               logfile)
1403

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

    
1406
  result = utils.RunCmd(command)
1407

    
1408
  if result.failed:
1409
    logger.Error("os import command '%s' returned error: %s"
1410
                 " output: %s" %
1411
                 (command, result.fail_reason, result.output))
1412
    return False
1413

    
1414
  return True
1415

    
1416

    
1417
def ListExports():
1418
  """Return a list of exports currently available on this machine.
1419

1420
  """
1421
  if os.path.isdir(constants.EXPORT_DIR):
1422
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1423
  else:
1424
    return []
1425

    
1426

    
1427
def RemoveExport(export):
1428
  """Remove an existing export from the node.
1429

1430
  Args:
1431
    export: the name of the export to remove
1432

1433
  Returns:
1434
    False in case of error, True otherwise.
1435

1436
  """
1437
  target = os.path.join(constants.EXPORT_DIR, export)
1438

    
1439
  shutil.rmtree(target)
1440
  # TODO: catch some of the relevant exceptions and provide a pretty
1441
  # error message if rmtree fails.
1442

    
1443
  return True
1444

    
1445

    
1446
def RenameBlockDevices(devlist):
1447
  """Rename a list of block devices.
1448

1449
  The devlist argument is a list of tuples (disk, new_logical,
1450
  new_physical). The return value will be a combined boolean result
1451
  (True only if all renames succeeded).
1452

1453
  """
1454
  result = True
1455
  for disk, unique_id in devlist:
1456
    dev = _RecursiveFindBD(disk)
1457
    if dev is None:
1458
      result = False
1459
      continue
1460
    try:
1461
      old_rpath = dev.dev_path
1462
      dev.Rename(unique_id)
1463
      new_rpath = dev.dev_path
1464
      if old_rpath != new_rpath:
1465
        DevCacheManager.RemoveCache(old_rpath)
1466
        # FIXME: we should add the new cache information here, like:
1467
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1468
        # but we don't have the owner here - maybe parse from existing
1469
        # cache? for now, we only lose lvm data when we rename, which
1470
        # is less critical than DRBD or MD
1471
    except errors.BlockDeviceError, err:
1472
      logger.Error("Can't rename device '%s' to '%s': %s" %
1473
                   (dev, unique_id, err))
1474
      result = False
1475
  return result
1476

    
1477

    
1478
def _TransformFileStorageDir(file_storage_dir):
1479
  """Checks whether given file_storage_dir is valid.
1480

1481
  Checks wheter the given file_storage_dir is within the cluster-wide
1482
  default file_storage_dir stored in SimpleStore. Only paths under that
1483
  directory are allowed.
1484

1485
  Args:
1486
    file_storage_dir: string with path
1487

1488
  Returns:
1489
    normalized file_storage_dir (string) if valid, None otherwise
1490

1491
  """
1492
  file_storage_dir = os.path.normpath(file_storage_dir)
1493
  base_file_storage_dir = ssconf.SimpleStore().GetFileStorageDir()
1494
  if (not os.path.commonprefix([file_storage_dir, base_file_storage_dir]) ==
1495
      base_file_storage_dir):
1496
    logger.Error("file storage directory '%s' is not under base file"
1497
                 " storage directory '%s'" %
1498
                 (file_storage_dir, base_file_storage_dir))
1499
    return None
1500
  return file_storage_dir
1501

    
1502

    
1503
def CreateFileStorageDir(file_storage_dir):
1504
  """Create file storage directory.
1505

1506
  Args:
1507
    file_storage_dir: string containing the path
1508

1509
  Returns:
1510
    tuple with first element a boolean indicating wheter dir
1511
    creation was successful or not
1512

1513
  """
1514
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1515
  result = True,
1516
  if not file_storage_dir:
1517
    result = False,
1518
  else:
1519
    if os.path.exists(file_storage_dir):
1520
      if not os.path.isdir(file_storage_dir):
1521
        logger.Error("'%s' is not a directory" % file_storage_dir)
1522
        result = False,
1523
    else:
1524
      try:
1525
        os.makedirs(file_storage_dir, 0750)
1526
      except OSError, err:
1527
        logger.Error("Cannot create file storage directory '%s': %s" %
1528
                     (file_storage_dir, err))
1529
        result = False,
1530
  return result
1531

    
1532

    
1533
def RemoveFileStorageDir(file_storage_dir):
1534
  """Remove file storage directory.
1535

1536
  Remove it only if it's empty. If not log an error and return.
1537

1538
  Args:
1539
    file_storage_dir: string containing the path
1540

1541
  Returns:
1542
    tuple with first element a boolean indicating wheter dir
1543
    removal was successful or not
1544

1545
  """
1546
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1547
  result = True,
1548
  if not file_storage_dir:
1549
    result = False,
1550
  else:
1551
    if os.path.exists(file_storage_dir):
1552
      if not os.path.isdir(file_storage_dir):
1553
        logger.Error("'%s' is not a directory" % file_storage_dir)
1554
        result = False,
1555
      # deletes dir only if empty, otherwise we want to return False
1556
      try:
1557
        os.rmdir(file_storage_dir)
1558
      except OSError, err:
1559
        logger.Error("Cannot remove file storage directory '%s': %s" %
1560
                     (file_storage_dir, err))
1561
        result = False,
1562
  return result
1563

    
1564

    
1565
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
1566
  """Rename the file storage directory.
1567

1568
  Args:
1569
    old_file_storage_dir: string containing the old path
1570
    new_file_storage_dir: string containing the new path
1571

1572
  Returns:
1573
    tuple with first element a boolean indicating wheter dir
1574
    rename was successful or not
1575

1576
  """
1577
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
1578
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
1579
  result = True,
1580
  if not old_file_storage_dir or not new_file_storage_dir:
1581
    result = False,
1582
  else:
1583
    if not os.path.exists(new_file_storage_dir):
1584
      if os.path.isdir(old_file_storage_dir):
1585
        try:
1586
          os.rename(old_file_storage_dir, new_file_storage_dir)
1587
        except OSError, err:
1588
          logger.Error("Cannot rename '%s' to '%s': %s"
1589
                       % (old_file_storage_dir, new_file_storage_dir, err))
1590
          result =  False,
1591
      else:
1592
        logger.Error("'%s' is not a directory" % old_file_storage_dir)
1593
        result = False,
1594
    else:
1595
      if os.path.exists(old_file_storage_dir):
1596
        logger.Error("Cannot rename '%s' to '%s'. Both locations exist." %
1597
                     old_file_storage_dir, new_file_storage_dir)
1598
        result = False,
1599
  return result
1600

    
1601

    
1602
def CloseBlockDevices(disks):
1603
  """Closes the given block devices.
1604

1605
  This means they will be switched to secondary mode (in case of DRBD).
1606

1607
  """
1608
  bdevs = []
1609
  for cf in disks:
1610
    rd = _RecursiveFindBD(cf)
1611
    if rd is None:
1612
      return (False, "Can't find device %s" % cf)
1613
    bdevs.append(rd)
1614

    
1615
  msg = []
1616
  for rd in bdevs:
1617
    try:
1618
      rd.Close()
1619
    except errors.BlockDeviceError, err:
1620
      msg.append(str(err))
1621
  if msg:
1622
    return (False, "Can't make devices secondary: %s" % ",".join(msg))
1623
  else:
1624
    return (True, "All devices secondary")
1625

    
1626

    
1627
class HooksRunner(object):
1628
  """Hook runner.
1629

1630
  This class is instantiated on the node side (ganeti-noded) and not on
1631
  the master side.
1632

1633
  """
1634
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1635

    
1636
  def __init__(self, hooks_base_dir=None):
1637
    """Constructor for hooks runner.
1638

1639
    Args:
1640
      - hooks_base_dir: if not None, this overrides the
1641
        constants.HOOKS_BASE_DIR (useful for unittests)
1642

1643
    """
1644
    if hooks_base_dir is None:
1645
      hooks_base_dir = constants.HOOKS_BASE_DIR
1646
    self._BASE_DIR = hooks_base_dir
1647

    
1648
  @staticmethod
1649
  def ExecHook(script, env):
1650
    """Exec one hook script.
1651

1652
    Args:
1653
     - script: the full path to the script
1654
     - env: the environment with which to exec the script
1655

1656
    """
1657
    # exec the process using subprocess and log the output
1658
    fdstdin = None
1659
    try:
1660
      fdstdin = open("/dev/null", "r")
1661
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1662
                               stderr=subprocess.STDOUT, close_fds=True,
1663
                               shell=False, cwd="/", env=env)
1664
      output = ""
1665
      try:
1666
        output = child.stdout.read(4096)
1667
        child.stdout.close()
1668
      except EnvironmentError, err:
1669
        output += "Hook script error: %s" % str(err)
1670

    
1671
      while True:
1672
        try:
1673
          result = child.wait()
1674
          break
1675
        except EnvironmentError, err:
1676
          if err.errno == errno.EINTR:
1677
            continue
1678
          raise
1679
    finally:
1680
      # try not to leak fds
1681
      for fd in (fdstdin, ):
1682
        if fd is not None:
1683
          try:
1684
            fd.close()
1685
          except EnvironmentError, err:
1686
            # just log the error
1687
            #logger.Error("While closing fd %s: %s" % (fd, err))
1688
            pass
1689

    
1690
    return result == 0, output
1691

    
1692
  def RunHooks(self, hpath, phase, env):
1693
    """Run the scripts in the hooks directory.
1694

1695
    This method will not be usually overriden by child opcodes.
1696

1697
    """
1698
    if phase == constants.HOOKS_PHASE_PRE:
1699
      suffix = "pre"
1700
    elif phase == constants.HOOKS_PHASE_POST:
1701
      suffix = "post"
1702
    else:
1703
      raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1704
    rr = []
1705

    
1706
    subdir = "%s-%s.d" % (hpath, suffix)
1707
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1708
    try:
1709
      dir_contents = utils.ListVisibleFiles(dir_name)
1710
    except OSError, err:
1711
      # must log
1712
      return rr
1713

    
1714
    # we use the standard python sort order,
1715
    # so 00name is the recommended naming scheme
1716
    dir_contents.sort()
1717
    for relname in dir_contents:
1718
      fname = os.path.join(dir_name, relname)
1719
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1720
          self.RE_MASK.match(relname) is not None):
1721
        rrval = constants.HKR_SKIP
1722
        output = ""
1723
      else:
1724
        result, output = self.ExecHook(fname, env)
1725
        if not result:
1726
          rrval = constants.HKR_FAIL
1727
        else:
1728
          rrval = constants.HKR_SUCCESS
1729
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
1730

    
1731
    return rr
1732

    
1733

    
1734
class IAllocatorRunner(object):
1735
  """IAllocator runner.
1736

1737
  This class is instantiated on the node side (ganeti-noded) and not on
1738
  the master side.
1739

1740
  """
1741
  def Run(self, name, idata):
1742
    """Run an iallocator script.
1743

1744
    Return value: tuple of:
1745
       - run status (one of the IARUN_ constants)
1746
       - stdout
1747
       - stderr
1748
       - fail reason (as from utils.RunResult)
1749

1750
    """
1751
    alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
1752
                                  os.path.isfile)
1753
    if alloc_script is None:
1754
      return (constants.IARUN_NOTFOUND, None, None, None)
1755

    
1756
    fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
1757
    try:
1758
      os.write(fd, idata)
1759
      os.close(fd)
1760
      result = utils.RunCmd([alloc_script, fin_name])
1761
      if result.failed:
1762
        return (constants.IARUN_FAILURE, result.stdout, result.stderr,
1763
                result.fail_reason)
1764
    finally:
1765
      os.unlink(fin_name)
1766

    
1767
    return (constants.IARUN_SUCCESS, result.stdout, result.stderr, None)
1768

    
1769

    
1770
class DevCacheManager(object):
1771
  """Simple class for managing a cache of block device information.
1772

1773
  """
1774
  _DEV_PREFIX = "/dev/"
1775
  _ROOT_DIR = constants.BDEV_CACHE_DIR
1776

    
1777
  @classmethod
1778
  def _ConvertPath(cls, dev_path):
1779
    """Converts a /dev/name path to the cache file name.
1780

1781
    This replaces slashes with underscores and strips the /dev
1782
    prefix. It then returns the full path to the cache file
1783

1784
    """
1785
    if dev_path.startswith(cls._DEV_PREFIX):
1786
      dev_path = dev_path[len(cls._DEV_PREFIX):]
1787
    dev_path = dev_path.replace("/", "_")
1788
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1789
    return fpath
1790

    
1791
  @classmethod
1792
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1793
    """Updates the cache information for a given device.
1794

1795
    """
1796
    if dev_path is None:
1797
      logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1798
      return
1799
    fpath = cls._ConvertPath(dev_path)
1800
    if on_primary:
1801
      state = "primary"
1802
    else:
1803
      state = "secondary"
1804
    if iv_name is None:
1805
      iv_name = "not_visible"
1806
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1807
    try:
1808
      utils.WriteFile(fpath, data=fdata)
1809
    except EnvironmentError, err:
1810
      logger.Error("Can't update bdev cache for %s, error %s" %
1811
                   (dev_path, str(err)))
1812

    
1813
  @classmethod
1814
  def RemoveCache(cls, dev_path):
1815
    """Remove data for a dev_path.
1816

1817
    """
1818
    if dev_path is None:
1819
      logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1820
      return
1821
    fpath = cls._ConvertPath(dev_path)
1822
    try:
1823
      utils.RemoveFile(fpath)
1824
    except EnvironmentError, err:
1825
      logger.Error("Can't update bdev cache for %s, error %s" %
1826
                   (dev_path, str(err)))