Reduce chance of ssh failures in verify cluster
[ganeti-local] / lib / backend.py
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
143 def GetNodeInfo(vgname):
144   """Gives back a hash with different informations about the node.
145
146   Returns:
147     { 'vg_size' : xxx,  'vg_free' : xxx, 'memory_domain0': xxx,
148       'memory_free' : xxx, 'memory_total' : xxx }
149     where
150     vg_size is the size of the configured volume group in MiB
151     vg_free is the free size of the volume group in MiB
152     memory_dom0 is the memory allocated for domain0 in MiB
153     memory_free is the currently available (free) ram in MiB
154     memory_total is the total number of ram in MiB
155
156   """
157   outputarray = {}
158   vginfo = _GetVGInfo(vgname)
159   outputarray['vg_size'] = vginfo['vg_size']
160   outputarray['vg_free'] = vginfo['vg_free']
161
162   hyper = hypervisor.GetHypervisor()
163   hyp_info = hyper.GetNodeInfo()
164   if hyp_info is not None:
165     outputarray.update(hyp_info)
166
167   f = open("/proc/sys/kernel/random/boot_id", 'r')
168   try:
169     outputarray["bootid"] = f.read(128).rstrip("\n")
170   finally:
171     f.close()
172
173   return outputarray
174
175
176 def VerifyNode(what):
177   """Verify the status of the local node.
178
179   Args:
180     what - a dictionary of things to check:
181       'filelist' : list of files for which to compute checksums
182       'nodelist' : list of nodes we should check communication with
183       'hypervisor': run the hypervisor-specific verify
184
185   Requested files on local node are checksummed and the result returned.
186
187   The nodelist is traversed, with the following checks being made
188   for each node:
189   - known_hosts key correct
190   - correct resolving of node name (target node returns its own hostname
191     by ssh-execution of 'hostname', result compared against name in list.
192
193   """
194   result = {}
195
196   if 'hypervisor' in what:
197     result['hypervisor'] = hypervisor.GetHypervisor().Verify()
198
199   if 'filelist' in what:
200     result['filelist'] = utils.FingerprintFiles(what['filelist'])
201
202   if 'nodelist' in what:
203     result['nodelist'] = {}
204     random.shuffle(what['nodelist'])
205     for node in what['nodelist']:
206       success, message = _GetSshRunner().VerifyNodeHostname(node)
207       if not success:
208         result['nodelist'][node] = message
209   return result
210
211
212 def GetVolumeList(vg_name):
213   """Compute list of logical volumes and their size.
214
215   Returns:
216     dictionary of all partions (key) with their size (in MiB), inactive
217     and online status:
218     {'test1': ('20.06', True, True)}
219
220   """
221   lvs = {}
222   sep = '|'
223   result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
224                          "--separator=%s" % sep,
225                          "-olv_name,lv_size,lv_attr", vg_name])
226   if result.failed:
227     logger.Error("Failed to list logical volumes, lvs output: %s" %
228                  result.output)
229     return result.output
230
231   for line in result.stdout.splitlines():
232     line = line.strip().rstrip(sep)
233     name, size, attr = line.split(sep)
234     if len(attr) != 6:
235       attr = '------'
236     inactive = attr[4] == '-'
237     online = attr[5] == 'o'
238     lvs[name] = (size, inactive, online)
239
240   return lvs
241
242
243 def ListVolumeGroups():
244   """List the volume groups and their size.
245
246   Returns:
247     Dictionary with keys volume name and values the size of the volume
248
249   """
250   return utils.ListVolumeGroups()
251
252
253 def NodeVolumes():
254   """List all volumes on this node.
255
256   """
257   result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
258                          "--separator=|",
259                          "--options=lv_name,lv_size,devices,vg_name"])
260   if result.failed:
261     logger.Error("Failed to list logical volumes, lvs output: %s" %
262                  result.output)
263     return {}
264
265   def parse_dev(dev):
266     if '(' in dev:
267       return dev.split('(')[0]
268     else:
269       return dev
270
271   def map_line(line):
272     return {
273       'name': line[0].strip(),
274       'size': line[1].strip(),
275       'dev': parse_dev(line[2].strip()),
276       'vg': line[3].strip(),
277     }
278
279   return [map_line(line.split('|')) for line in result.stdout.splitlines()]
280
281
282 def BridgesExist(bridges_list):
283   """Check if a list of bridges exist on the current node.
284
285   Returns:
286     True if all of them exist, false otherwise
287
288   """
289   for bridge in bridges_list:
290     if not utils.BridgeExists(bridge):
291       return False
292
293   return True
294
295
296 def GetInstanceList():
297   """Provides a list of instances.
298
299   Returns:
300     A list of all running instances on the current node
301     - instance1.example.com
302     - instance2.example.com
303
304   """
305   try:
306     names = hypervisor.GetHypervisor().ListInstances()
307   except errors.HypervisorError, err:
308     logger.Error("error enumerating instances: %s" % str(err))
309     raise
310
311   return names
312
313
314 def GetInstanceInfo(instance):
315   """Gives back the informations about an instance as a dictionary.
316
317   Args:
318     instance: name of the instance (ex. instance1.example.com)
319
320   Returns:
321     { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
322     where
323     memory: memory size of instance (int)
324     state: xen state of instance (string)
325     time: cpu time of instance (float)
326
327   """
328   output = {}
329
330   iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
331   if iinfo is not None:
332     output['memory'] = iinfo[2]
333     output['state'] = iinfo[4]
334     output['time'] = iinfo[5]
335
336   return output
337
338
339 def GetAllInstancesInfo():
340   """Gather data about all instances.
341
342   This is the equivalent of `GetInstanceInfo()`, except that it
343   computes data for all instances at once, thus being faster if one
344   needs data about more than one instance.
345
346   Returns: a dictionary of dictionaries, keys being the instance name,
347     and with values:
348     { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
349     where
350     memory: memory size of instance (int)
351     state: xen state of instance (string)
352     time: cpu time of instance (float)
353     vcpus: the number of cpus
354
355   """
356   output = {}
357
358   iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
359   if iinfo:
360     for name, inst_id, memory, vcpus, state, times in iinfo:
361       output[name] = {
362         'memory': memory,
363         'vcpus': vcpus,
364         'state': state,
365         'time': times,
366         }
367
368   return output
369
370
371 def AddOSToInstance(instance, os_disk, swap_disk):
372   """Add an OS to an instance.
373
374   Args:
375     instance: the instance object
376     os_disk: the instance-visible name of the os device
377     swap_disk: the instance-visible name of the swap device
378
379   """
380   inst_os = OSFromDisk(instance.os)
381
382   create_script = inst_os.create_script
383
384   os_device = instance.FindDisk(os_disk)
385   if os_device is None:
386     logger.Error("Can't find this device-visible name '%s'" % os_disk)
387     return False
388
389   swap_device = instance.FindDisk(swap_disk)
390   if swap_device is None:
391     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
392     return False
393
394   real_os_dev = _RecursiveFindBD(os_device)
395   if real_os_dev is None:
396     raise errors.BlockDeviceError("Block device '%s' is not set up" %
397                                   str(os_device))
398   real_os_dev.Open()
399
400   real_swap_dev = _RecursiveFindBD(swap_device)
401   if real_swap_dev is None:
402     raise errors.BlockDeviceError("Block device '%s' is not set up" %
403                                   str(swap_device))
404   real_swap_dev.Open()
405
406   logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
407                                      instance.name, int(time.time()))
408   if not os.path.exists(constants.LOG_OS_DIR):
409     os.mkdir(constants.LOG_OS_DIR, 0750)
410
411   command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
412                                 inst_os.path, create_script, instance.name,
413                                 real_os_dev.dev_path, real_swap_dev.dev_path,
414                                 logfile)
415
416   result = utils.RunCmd(command)
417   if result.failed:
418     logger.Error("os create command '%s' returned error: %s, logfile: %s,"
419                  " output: %s" %
420                  (command, result.fail_reason, logfile, result.output))
421     return False
422
423   return True
424
425
426 def RunRenameInstance(instance, old_name, os_disk, swap_disk):
427   """Run the OS rename script for an instance.
428
429   Args:
430     instance: the instance object
431     old_name: the old name of the instance
432     os_disk: the instance-visible name of the os device
433     swap_disk: the instance-visible name of the swap device
434
435   """
436   inst_os = OSFromDisk(instance.os)
437
438   script = inst_os.rename_script
439
440   os_device = instance.FindDisk(os_disk)
441   if os_device is None:
442     logger.Error("Can't find this device-visible name '%s'" % os_disk)
443     return False
444
445   swap_device = instance.FindDisk(swap_disk)
446   if swap_device is None:
447     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
448     return False
449
450   real_os_dev = _RecursiveFindBD(os_device)
451   if real_os_dev is None:
452     raise errors.BlockDeviceError("Block device '%s' is not set up" %
453                                   str(os_device))
454   real_os_dev.Open()
455
456   real_swap_dev = _RecursiveFindBD(swap_device)
457   if real_swap_dev is None:
458     raise errors.BlockDeviceError("Block device '%s' is not set up" %
459                                   str(swap_device))
460   real_swap_dev.Open()
461
462   logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
463                                            old_name,
464                                            instance.name, int(time.time()))
465   if not os.path.exists(constants.LOG_OS_DIR):
466     os.mkdir(constants.LOG_OS_DIR, 0750)
467
468   command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
469                                 inst_os.path, script, old_name, instance.name,
470                                 real_os_dev.dev_path, real_swap_dev.dev_path,
471                                 logfile)
472
473   result = utils.RunCmd(command)
474
475   if result.failed:
476     logger.Error("os create command '%s' returned error: %s"
477                  " output: %s" %
478                  (command, result.fail_reason, result.output))
479     return False
480
481   return True
482
483
484 def _GetVGInfo(vg_name):
485   """Get informations about the volume group.
486
487   Args:
488     vg_name: the volume group
489
490   Returns:
491     { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
492     where
493     vg_size is the total size of the volume group in MiB
494     vg_free is the free size of the volume group in MiB
495     pv_count are the number of physical disks in that vg
496
497   If an error occurs during gathering of data, we return the same dict
498   with keys all set to None.
499
500   """
501   retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
502
503   retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
504                          "--nosuffix", "--units=m", "--separator=:", vg_name])
505
506   if retval.failed:
507     errmsg = "volume group %s not present" % vg_name
508     logger.Error(errmsg)
509     return retdic
510   valarr = retval.stdout.strip().rstrip(':').split(':')
511   if len(valarr) == 3:
512     try:
513       retdic = {
514         "vg_size": int(round(float(valarr[0]), 0)),
515         "vg_free": int(round(float(valarr[1]), 0)),
516         "pv_count": int(valarr[2]),
517         }
518     except ValueError, err:
519       logger.Error("Fail to parse vgs output: %s" % str(err))
520   else:
521     logger.Error("vgs output has the wrong number of fields (expected"
522                  " three): %s" % str(valarr))
523   return retdic
524
525
526 def _GatherBlockDevs(instance):
527   """Set up an instance's block device(s).
528
529   This is run on the primary node at instance startup. The block
530   devices must be already assembled.
531
532   """
533   block_devices = []
534   for disk in instance.disks:
535     device = _RecursiveFindBD(disk)
536     if device is None:
537       raise errors.BlockDeviceError("Block device '%s' is not set up." %
538                                     str(disk))
539     device.Open()
540     block_devices.append((disk, device))
541   return block_devices
542
543
544 def StartInstance(instance, extra_args):
545   """Start an instance.
546
547   Args:
548     instance - name of instance to start.
549
550   """
551   running_instances = GetInstanceList()
552
553   if instance.name in running_instances:
554     return True
555
556   block_devices = _GatherBlockDevs(instance)
557   hyper = hypervisor.GetHypervisor()
558
559   try:
560     hyper.StartInstance(instance, block_devices, extra_args)
561   except errors.HypervisorError, err:
562     logger.Error("Failed to start instance: %s" % err)
563     return False
564
565   return True
566
567
568 def ShutdownInstance(instance):
569   """Shut an instance down.
570
571   Args:
572     instance - name of instance to shutdown.
573
574   """
575   running_instances = GetInstanceList()
576
577   if instance.name not in running_instances:
578     return True
579
580   hyper = hypervisor.GetHypervisor()
581   try:
582     hyper.StopInstance(instance)
583   except errors.HypervisorError, err:
584     logger.Error("Failed to stop instance: %s" % err)
585     return False
586
587   # test every 10secs for 2min
588   shutdown_ok = False
589
590   time.sleep(1)
591   for dummy in range(11):
592     if instance.name not in GetInstanceList():
593       break
594     time.sleep(10)
595   else:
596     # the shutdown did not succeed
597     logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
598
599     try:
600       hyper.StopInstance(instance, force=True)
601     except errors.HypervisorError, err:
602       logger.Error("Failed to stop instance: %s" % err)
603       return False
604
605     time.sleep(1)
606     if instance.name in GetInstanceList():
607       logger.Error("could not shutdown instance '%s' even by destroy")
608       return False
609
610   return True
611
612
613 def RebootInstance(instance, reboot_type, extra_args):
614   """Reboot an instance.
615
616   Args:
617     instance    - name of instance to reboot
618     reboot_type - how to reboot [soft,hard,full]
619
620   """
621   running_instances = GetInstanceList()
622
623   if instance.name not in running_instances:
624     logger.Error("Cannot reboot instance that is not running")
625     return False
626
627   hyper = hypervisor.GetHypervisor()
628   if reboot_type == constants.INSTANCE_REBOOT_SOFT:
629     try:
630       hyper.RebootInstance(instance)
631     except errors.HypervisorError, err:
632       logger.Error("Failed to soft reboot instance: %s" % err)
633       return False
634   elif reboot_type == constants.INSTANCE_REBOOT_HARD:
635     try:
636       ShutdownInstance(instance)
637       StartInstance(instance, extra_args)
638     except errors.HypervisorError, err:
639       logger.Error("Failed to hard reboot instance: %s" % err)
640       return False
641   else:
642     raise errors.ParameterError("reboot_type invalid")
643
644
645   return True
646
647
648 def CreateBlockDevice(disk, size, owner, on_primary, info):
649   """Creates a block device for an instance.
650
651   Args:
652    disk: a ganeti.objects.Disk object
653    size: the size of the physical underlying device
654    owner: a string with the name of the instance
655    on_primary: a boolean indicating if it is the primary node or not
656    info: string that will be sent to the physical device creation
657
658   Returns:
659     the new unique_id of the device (this can sometime be
660     computed only after creation), or None. On secondary nodes,
661     it's not required to return anything.
662
663   """
664   clist = []
665   if disk.children:
666     for child in disk.children:
667       crdev = _RecursiveAssembleBD(child, owner, on_primary)
668       if on_primary or disk.AssembleOnSecondary():
669         # we need the children open in case the device itself has to
670         # be assembled
671         crdev.Open()
672       clist.append(crdev)
673   try:
674     device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
675     if device is not None:
676       logger.Info("removing existing device %s" % disk)
677       device.Remove()
678   except errors.BlockDeviceError, err:
679     pass
680
681   device = bdev.Create(disk.dev_type, disk.physical_id,
682                        clist, size)
683   if device is None:
684     raise ValueError("Can't create child device for %s, %s" %
685                      (disk, size))
686   if on_primary or disk.AssembleOnSecondary():
687     if not device.Assemble():
688       errorstring = "Can't assemble device after creation"
689       logger.Error(errorstring)
690       raise errors.BlockDeviceError("%s, very unusual event - check the node"
691                                     " daemon logs" % errorstring)
692     device.SetSyncSpeed(constants.SYNC_SPEED)
693     if on_primary or disk.OpenOnSecondary():
694       device.Open(force=True)
695     DevCacheManager.UpdateCache(device.dev_path, owner,
696                                 on_primary, disk.iv_name)
697
698   device.SetInfo(info)
699
700   physical_id = device.unique_id
701   return physical_id
702
703
704 def RemoveBlockDevice(disk):
705   """Remove a block device.
706
707   This is intended to be called recursively.
708
709   """
710   try:
711     # since we are removing the device, allow a partial match
712     # this allows removal of broken mirrors
713     rdev = _RecursiveFindBD(disk, allow_partial=True)
714   except errors.BlockDeviceError, err:
715     # probably can't attach
716     logger.Info("Can't attach to device %s in remove" % disk)
717     rdev = None
718   if rdev is not None:
719     r_path = rdev.dev_path
720     result = rdev.Remove()
721     if result:
722       DevCacheManager.RemoveCache(r_path)
723   else:
724     result = True
725   if disk.children:
726     for child in disk.children:
727       result = result and RemoveBlockDevice(child)
728   return result
729
730
731 def _RecursiveAssembleBD(disk, owner, as_primary):
732   """Activate a block device for an instance.
733
734   This is run on the primary and secondary nodes for an instance.
735
736   This function is called recursively.
737
738   Args:
739     disk: a objects.Disk object
740     as_primary: if we should make the block device read/write
741
742   Returns:
743     the assembled device or None (in case no device was assembled)
744
745   If the assembly is not successful, an exception is raised.
746
747   """
748   children = []
749   if disk.children:
750     mcn = disk.ChildrenNeeded()
751     if mcn == -1:
752       mcn = 0 # max number of Nones allowed
753     else:
754       mcn = len(disk.children) - mcn # max number of Nones
755     for chld_disk in disk.children:
756       try:
757         cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
758       except errors.BlockDeviceError, err:
759         if children.count(None) >= mcn:
760           raise
761         cdev = None
762         logger.Debug("Error in child activation: %s" % str(err))
763       children.append(cdev)
764
765   if as_primary or disk.AssembleOnSecondary():
766     r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
767     r_dev.SetSyncSpeed(constants.SYNC_SPEED)
768     result = r_dev
769     if as_primary or disk.OpenOnSecondary():
770       r_dev.Open()
771     DevCacheManager.UpdateCache(r_dev.dev_path, owner,
772                                 as_primary, disk.iv_name)
773
774   else:
775     result = True
776   return result
777
778
779 def AssembleBlockDevice(disk, owner, as_primary):
780   """Activate a block device for an instance.
781
782   This is a wrapper over _RecursiveAssembleBD.
783
784   Returns:
785     a /dev path for primary nodes
786     True for secondary nodes
787
788   """
789   result = _RecursiveAssembleBD(disk, owner, as_primary)
790   if isinstance(result, bdev.BlockDev):
791     result = result.dev_path
792   return result
793
794
795 def ShutdownBlockDevice(disk):
796   """Shut down a block device.
797
798   First, if the device is assembled (can `Attach()`), then the device
799   is shutdown. Then the children of the device are shutdown.
800
801   This function is called recursively. Note that we don't cache the
802   children or such, as oppossed to assemble, shutdown of different
803   devices doesn't require that the upper device was active.
804
805   """
806   r_dev = _RecursiveFindBD(disk)
807   if r_dev is not None:
808     r_path = r_dev.dev_path
809     result = r_dev.Shutdown()
810     if result:
811       DevCacheManager.RemoveCache(r_path)
812   else:
813     result = True
814   if disk.children:
815     for child in disk.children:
816       result = result and ShutdownBlockDevice(child)
817   return result
818
819
820 def MirrorAddChildren(parent_cdev, new_cdevs):
821   """Extend a mirrored block device.
822
823   """
824   parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
825   if parent_bdev is None:
826     logger.Error("Can't find parent device")
827     return False
828   new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
829   if new_bdevs.count(None) > 0:
830     logger.Error("Can't find new device(s) to add: %s:%s" %
831                  (new_bdevs, new_cdevs))
832     return False
833   parent_bdev.AddChildren(new_bdevs)
834   return True
835
836
837 def MirrorRemoveChildren(parent_cdev, new_cdevs):
838   """Shrink a mirrored block device.
839
840   """
841   parent_bdev = _RecursiveFindBD(parent_cdev)
842   if parent_bdev is None:
843     logger.Error("Can't find parent in remove children: %s" % parent_cdev)
844     return False
845   devs = []
846   for disk in new_cdevs:
847     rpath = disk.StaticDevPath()
848     if rpath is None:
849       bd = _RecursiveFindBD(disk)
850       if bd is None:
851         logger.Error("Can't find dynamic device %s while removing children" %
852                      disk)
853         return False
854       else:
855         devs.append(bd.dev_path)
856     else:
857       devs.append(rpath)
858   parent_bdev.RemoveChildren(devs)
859   return True
860
861
862 def GetMirrorStatus(disks):
863   """Get the mirroring status of a list of devices.
864
865   Args:
866     disks: list of `objects.Disk`
867
868   Returns:
869     list of (mirror_done, estimated_time) tuples, which
870     are the result of bdev.BlockDevice.CombinedSyncStatus()
871
872   """
873   stats = []
874   for dsk in disks:
875     rbd = _RecursiveFindBD(dsk)
876     if rbd is None:
877       raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
878     stats.append(rbd.CombinedSyncStatus())
879   return stats
880
881
882 def _RecursiveFindBD(disk, allow_partial=False):
883   """Check if a device is activated.
884
885   If so, return informations about the real device.
886
887   Args:
888     disk: the objects.Disk instance
889     allow_partial: don't abort the find if a child of the
890                    device can't be found; this is intended to be
891                    used when repairing mirrors
892
893   Returns:
894     None if the device can't be found
895     otherwise the device instance
896
897   """
898   children = []
899   if disk.children:
900     for chdisk in disk.children:
901       children.append(_RecursiveFindBD(chdisk))
902
903   return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
904
905
906 def FindBlockDevice(disk):
907   """Check if a device is activated.
908
909   If so, return informations about the real device.
910
911   Args:
912     disk: the objects.Disk instance
913   Returns:
914     None if the device can't be found
915     (device_path, major, minor, sync_percent, estimated_time, is_degraded)
916
917   """
918   rbd = _RecursiveFindBD(disk)
919   if rbd is None:
920     return rbd
921   return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
922
923
924 def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
925   """Write a file to the filesystem.
926
927   This allows the master to overwrite(!) a file. It will only perform
928   the operation if the file belongs to a list of configuration files.
929
930   """
931   if not os.path.isabs(file_name):
932     logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
933                  file_name)
934     return False
935
936   allowed_files = [
937     constants.CLUSTER_CONF_FILE,
938     constants.ETC_HOSTS,
939     constants.SSH_KNOWN_HOSTS_FILE,
940     ]
941   allowed_files.extend(ssconf.SimpleStore().GetFileList())
942   if file_name not in allowed_files:
943     logger.Error("Filename passed to UploadFile not in allowed"
944                  " upload targets: '%s'" % file_name)
945     return False
946
947   utils.WriteFile(file_name, data=data, mode=mode, uid=uid, gid=gid,
948                   atime=atime, mtime=mtime)
949   return True
950
951
952 def _ErrnoOrStr(err):
953   """Format an EnvironmentError exception.
954
955   If the `err` argument has an errno attribute, it will be looked up
956   and converted into a textual EXXXX description. Otherwise the string
957   representation of the error will be returned.
958
959   """
960   if hasattr(err, 'errno'):
961     detail = errno.errorcode[err.errno]
962   else:
963     detail = str(err)
964   return detail
965
966
967 def _OSOndiskVersion(name, os_dir):
968   """Compute and return the API version of a given OS.
969
970   This function will try to read the API version of the os given by
971   the 'name' parameter and residing in the 'os_dir' directory.
972
973   Return value will be either an integer denoting the version or None in the
974   case when this is not a valid OS name.
975
976   """
977   api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
978
979   try:
980     st = os.stat(api_file)
981   except EnvironmentError, err:
982     raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
983                            " found (%s)" % _ErrnoOrStr(err))
984
985   if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
986     raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
987                            " a regular file")
988
989   try:
990     f = open(api_file)
991     try:
992       api_version = f.read(256)
993     finally:
994       f.close()
995   except EnvironmentError, err:
996     raise errors.InvalidOS(name, os_dir, "error while reading the"
997                            " API version (%s)" % _ErrnoOrStr(err))
998
999   api_version = api_version.strip()
1000   try:
1001     api_version = int(api_version)
1002   except (TypeError, ValueError), err:
1003     raise errors.InvalidOS(name, os_dir,
1004                            "API version is not integer (%s)" % str(err))
1005
1006   return api_version
1007
1008
1009 def DiagnoseOS(top_dirs=None):
1010   """Compute the validity for all OSes.
1011
1012   Returns an OS object for each name in all the given top directories
1013   (if not given defaults to constants.OS_SEARCH_PATH)
1014
1015   Returns:
1016     list of OS objects
1017
1018   """
1019   if top_dirs is None:
1020     top_dirs = constants.OS_SEARCH_PATH
1021
1022   result = []
1023   for dir_name in top_dirs:
1024     if os.path.isdir(dir_name):
1025       try:
1026         f_names = utils.ListVisibleFiles(dir_name)
1027       except EnvironmentError, err:
1028         logger.Error("Can't list the OS directory %s: %s" %
1029                      (dir_name, str(err)))
1030         break
1031       for name in f_names:
1032         try:
1033           os_inst = OSFromDisk(name, base_dir=dir_name)
1034           result.append(os_inst)
1035         except errors.InvalidOS, err:
1036           result.append(objects.OS.FromInvalidOS(err))
1037
1038   return result
1039
1040
1041 def OSFromDisk(name, base_dir=None):
1042   """Create an OS instance from disk.
1043
1044   This function will return an OS instance if the given name is a
1045   valid OS name. Otherwise, it will raise an appropriate
1046   `errors.InvalidOS` exception, detailing why this is not a valid
1047   OS.
1048
1049   Args:
1050     os_dir: Directory containing the OS scripts. Defaults to a search
1051             in all the OS_SEARCH_PATH directories.
1052
1053   """
1054
1055   if base_dir is None:
1056     os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1057     if os_dir is None:
1058       raise errors.InvalidOS(name, None, "OS dir not found in search path")
1059   else:
1060     os_dir = os.path.sep.join([base_dir, name])
1061
1062   api_version = _OSOndiskVersion(name, os_dir)
1063
1064   if api_version != constants.OS_API_VERSION:
1065     raise errors.InvalidOS(name, os_dir, "API version mismatch"
1066                            " (found %s want %s)"
1067                            % (api_version, constants.OS_API_VERSION))
1068
1069   # OS Scripts dictionary, we will populate it with the actual script names
1070   os_scripts = {'create': '', 'export': '', 'import': '', 'rename': ''}
1071
1072   for script in os_scripts:
1073     os_scripts[script] = os.path.sep.join([os_dir, script])
1074
1075     try:
1076       st = os.stat(os_scripts[script])
1077     except EnvironmentError, err:
1078       raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1079                              (script, _ErrnoOrStr(err)))
1080
1081     if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1082       raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1083                              script)
1084
1085     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1086       raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1087                              script)
1088
1089
1090   return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1091                     create_script=os_scripts['create'],
1092                     export_script=os_scripts['export'],
1093                     import_script=os_scripts['import'],
1094                     rename_script=os_scripts['rename'],
1095                     api_version=api_version)
1096
1097
1098 def SnapshotBlockDevice(disk):
1099   """Create a snapshot copy of a block device.
1100
1101   This function is called recursively, and the snapshot is actually created
1102   just for the leaf lvm backend device.
1103
1104   Args:
1105     disk: the disk to be snapshotted
1106
1107   Returns:
1108     a config entry for the actual lvm device snapshotted.
1109
1110   """
1111   if disk.children:
1112     if len(disk.children) == 1:
1113       # only one child, let's recurse on it
1114       return SnapshotBlockDevice(disk.children[0])
1115     else:
1116       # more than one child, choose one that matches
1117       for child in disk.children:
1118         if child.size == disk.size:
1119           # return implies breaking the loop
1120           return SnapshotBlockDevice(child)
1121   elif disk.dev_type == constants.LD_LV:
1122     r_dev = _RecursiveFindBD(disk)
1123     if r_dev is not None:
1124       # let's stay on the safe side and ask for the full size, for now
1125       return r_dev.Snapshot(disk.size)
1126     else:
1127       return None
1128   else:
1129     raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1130                                  " '%s' of type '%s'" %
1131                                  (disk.unique_id, disk.dev_type))
1132
1133
1134 def ExportSnapshot(disk, dest_node, instance):
1135   """Export a block device snapshot to a remote node.
1136
1137   Args:
1138     disk: the snapshot block device
1139     dest_node: the node to send the image to
1140     instance: instance being exported
1141
1142   Returns:
1143     True if successful, False otherwise.
1144
1145   """
1146   inst_os = OSFromDisk(instance.os)
1147   export_script = inst_os.export_script
1148
1149   logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1150                                      instance.name, int(time.time()))
1151   if not os.path.exists(constants.LOG_OS_DIR):
1152     os.mkdir(constants.LOG_OS_DIR, 0750)
1153
1154   real_os_dev = _RecursiveFindBD(disk)
1155   if real_os_dev is None:
1156     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1157                                   str(disk))
1158   real_os_dev.Open()
1159
1160   destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1161   destfile = disk.physical_id[1]
1162
1163   # the target command is built out of three individual commands,
1164   # which are joined by pipes; we check each individual command for
1165   # valid parameters
1166
1167   expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1168                                export_script, instance.name,
1169                                real_os_dev.dev_path, logfile)
1170
1171   comprcmd = "gzip"
1172
1173   destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1174                                 destdir, destdir, destfile)
1175   remotecmd = _GetSshRunner().BuildCmd(dest_node, constants.GANETI_RUNAS,
1176                                        destcmd)
1177
1178   # all commands have been checked, so we're safe to combine them
1179   command = '|'.join([expcmd, comprcmd, utils.ShellQuoteArgs(remotecmd)])
1180
1181   result = utils.RunCmd(command)
1182
1183   if result.failed:
1184     logger.Error("os snapshot export command '%s' returned error: %s"
1185                  " output: %s" %
1186                  (command, result.fail_reason, result.output))
1187     return False
1188
1189   return True
1190
1191
1192 def FinalizeExport(instance, snap_disks):
1193   """Write out the export configuration information.
1194
1195   Args:
1196     instance: instance configuration
1197     snap_disks: snapshot block devices
1198
1199   Returns:
1200     False in case of error, True otherwise.
1201
1202   """
1203   destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1204   finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1205
1206   config = objects.SerializableConfigParser()
1207
1208   config.add_section(constants.INISECT_EXP)
1209   config.set(constants.INISECT_EXP, 'version', '0')
1210   config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1211   config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1212   config.set(constants.INISECT_EXP, 'os', instance.os)
1213   config.set(constants.INISECT_EXP, 'compression', 'gzip')
1214
1215   config.add_section(constants.INISECT_INS)
1216   config.set(constants.INISECT_INS, 'name', instance.name)
1217   config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1218   config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1219   config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1220
1221   nic_count = 0
1222   for nic_count, nic in enumerate(instance.nics):
1223     config.set(constants.INISECT_INS, 'nic%d_mac' %
1224                nic_count, '%s' % nic.mac)
1225     config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1226     config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count, '%s' % nic.bridge)
1227   # TODO: redundant: on load can read nics until it doesn't exist
1228   config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1229
1230   disk_count = 0
1231   for disk_count, disk in enumerate(snap_disks):
1232     config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1233                ('%s' % disk.iv_name))
1234     config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1235                ('%s' % disk.physical_id[1]))
1236     config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1237                ('%d' % disk.size))
1238   config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1239
1240   cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1241   cfo = open(cff, 'w')
1242   try:
1243     config.write(cfo)
1244   finally:
1245     cfo.close()
1246
1247   shutil.rmtree(finaldestdir, True)
1248   shutil.move(destdir, finaldestdir)
1249
1250   return True
1251
1252
1253 def ExportInfo(dest):
1254   """Get export configuration information.
1255
1256   Args:
1257     dest: directory containing the export
1258
1259   Returns:
1260     A serializable config file containing the export info.
1261
1262   """
1263   cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1264
1265   config = objects.SerializableConfigParser()
1266   config.read(cff)
1267
1268   if (not config.has_section(constants.INISECT_EXP) or
1269       not config.has_section(constants.INISECT_INS)):
1270     return None
1271
1272   return config
1273
1274
1275 def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1276   """Import an os image into an instance.
1277
1278   Args:
1279     instance: the instance object
1280     os_disk: the instance-visible name of the os device
1281     swap_disk: the instance-visible name of the swap device
1282     src_node: node holding the source image
1283     src_image: path to the source image on src_node
1284
1285   Returns:
1286     False in case of error, True otherwise.
1287
1288   """
1289   inst_os = OSFromDisk(instance.os)
1290   import_script = inst_os.import_script
1291
1292   os_device = instance.FindDisk(os_disk)
1293   if os_device is None:
1294     logger.Error("Can't find this device-visible name '%s'" % os_disk)
1295     return False
1296
1297   swap_device = instance.FindDisk(swap_disk)
1298   if swap_device is None:
1299     logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1300     return False
1301
1302   real_os_dev = _RecursiveFindBD(os_device)
1303   if real_os_dev is None:
1304     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1305                                   str(os_device))
1306   real_os_dev.Open()
1307
1308   real_swap_dev = _RecursiveFindBD(swap_device)
1309   if real_swap_dev is None:
1310     raise errors.BlockDeviceError("Block device '%s' is not set up" %
1311                                   str(swap_device))
1312   real_swap_dev.Open()
1313
1314   logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1315                                         instance.name, int(time.time()))
1316   if not os.path.exists(constants.LOG_OS_DIR):
1317     os.mkdir(constants.LOG_OS_DIR, 0750)
1318
1319   destcmd = utils.BuildShellCmd('cat %s', src_image)
1320   remotecmd = _GetSshRunner().BuildCmd(src_node, constants.GANETI_RUNAS,
1321                                        destcmd)
1322
1323   comprcmd = "gunzip"
1324   impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1325                                inst_os.path, import_script, instance.name,
1326                                real_os_dev.dev_path, real_swap_dev.dev_path,
1327                                logfile)
1328
1329   command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
1330
1331   result = utils.RunCmd(command)
1332
1333   if result.failed:
1334     logger.Error("os import command '%s' returned error: %s"
1335                  " output: %s" %
1336                  (command, result.fail_reason, result.output))
1337     return False
1338
1339   return True
1340
1341
1342 def ListExports():
1343   """Return a list of exports currently available on this machine.
1344
1345   """
1346   if os.path.isdir(constants.EXPORT_DIR):
1347     return utils.ListVisibleFiles(constants.EXPORT_DIR)
1348   else:
1349     return []
1350
1351
1352 def RemoveExport(export):
1353   """Remove an existing export from the node.
1354
1355   Args:
1356     export: the name of the export to remove
1357
1358   Returns:
1359     False in case of error, True otherwise.
1360
1361   """
1362   target = os.path.join(constants.EXPORT_DIR, export)
1363
1364   shutil.rmtree(target)
1365   # TODO: catch some of the relevant exceptions and provide a pretty
1366   # error message if rmtree fails.
1367
1368   return True
1369
1370
1371 def RenameBlockDevices(devlist):
1372   """Rename a list of block devices.
1373
1374   The devlist argument is a list of tuples (disk, new_logical,
1375   new_physical). The return value will be a combined boolean result
1376   (True only if all renames succeeded).
1377
1378   """
1379   result = True
1380   for disk, unique_id in devlist:
1381     dev = _RecursiveFindBD(disk)
1382     if dev is None:
1383       result = False
1384       continue
1385     try:
1386       old_rpath = dev.dev_path
1387       dev.Rename(unique_id)
1388       new_rpath = dev.dev_path
1389       if old_rpath != new_rpath:
1390         DevCacheManager.RemoveCache(old_rpath)
1391         # FIXME: we should add the new cache information here, like:
1392         # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1393         # but we don't have the owner here - maybe parse from existing
1394         # cache? for now, we only lose lvm data when we rename, which
1395         # is less critical than DRBD or MD
1396     except errors.BlockDeviceError, err:
1397       logger.Error("Can't rename device '%s' to '%s': %s" %
1398                    (dev, unique_id, err))
1399       result = False
1400   return result
1401
1402
1403 def _TransformFileStorageDir(file_storage_dir):
1404   """Checks whether given file_storage_dir is valid.
1405
1406   Checks wheter the given file_storage_dir is within the cluster-wide
1407   default file_storage_dir stored in SimpleStore. Only paths under that
1408   directory are allowed.
1409
1410   Args:
1411     file_storage_dir: string with path
1412   
1413   Returns:
1414     normalized file_storage_dir (string) if valid, None otherwise
1415
1416   """
1417   file_storage_dir = os.path.normpath(file_storage_dir)
1418   base_file_storage_dir = ssconf.SimpleStore().GetFileStorageDir()
1419   if (not os.path.commonprefix([file_storage_dir, base_file_storage_dir]) ==
1420       base_file_storage_dir):
1421     logger.Error("file storage directory '%s' is not under base file"
1422                  " storage directory '%s'" %
1423                  (file_storage_dir, base_file_storage_dir))
1424     return None
1425   return file_storage_dir
1426
1427
1428 def CreateFileStorageDir(file_storage_dir):
1429   """Create file storage directory.
1430
1431   Args:
1432     file_storage_dir: string containing the path
1433
1434   Returns:
1435     tuple with first element a boolean indicating wheter dir
1436     creation was successful or not
1437
1438   """
1439   file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1440   result = True,
1441   if not file_storage_dir:
1442     result = False,
1443   else:
1444     if os.path.exists(file_storage_dir):
1445       if not os.path.isdir(file_storage_dir):
1446         logger.Error("'%s' is not a directory" % file_storage_dir)
1447         result = False,
1448     else:
1449       try:
1450         os.makedirs(file_storage_dir, 0750)
1451       except OSError, err:
1452         logger.Error("Cannot create file storage directory '%s': %s" %
1453                      (file_storage_dir, err))
1454         result = False,
1455   return result
1456
1457
1458 def RemoveFileStorageDir(file_storage_dir):
1459   """Remove file storage directory.
1460
1461   Remove it only if it's empty. If not log an error and return.
1462
1463   Args:
1464     file_storage_dir: string containing the path
1465
1466   Returns:
1467     tuple with first element a boolean indicating wheter dir
1468     removal was successful or not
1469
1470   """
1471   file_storage_dir = _TransformFileStorageDir(file_storage_dir)
1472   result = True,
1473   if not file_storage_dir:
1474     result = False,
1475   else:
1476     if os.path.exists(file_storage_dir):
1477       if not os.path.isdir(file_storage_dir):
1478         logger.Error("'%s' is not a directory" % file_storage_dir)
1479         result = False,
1480       # deletes dir only if empty, otherwise we want to return False
1481       try:
1482         os.rmdir(file_storage_dir)
1483       except OSError, err:
1484         logger.Error("Cannot remove file storage directory '%s': %s" %
1485                      (file_storage_dir, err))
1486         result = False,
1487   return result
1488
1489
1490 def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
1491   """Rename the file storage directory.
1492
1493   Args:
1494     old_file_storage_dir: string containing the old path
1495     new_file_storage_dir: string containing the new path
1496
1497   Returns:
1498     tuple with first element a boolean indicating wheter dir
1499     rename was successful or not
1500
1501   """
1502   old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
1503   new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
1504   result = True,
1505   if not old_file_storage_dir or not new_file_storage_dir:
1506     result = False,
1507   else:
1508     if not os.path.exists(new_file_storage_dir):
1509       if os.path.isdir(old_file_storage_dir):
1510         try:
1511           os.rename(old_file_storage_dir, new_file_storage_dir)
1512         except OSError, err:
1513           logger.Error("Cannot rename '%s' to '%s': %s"
1514                        % (old_file_storage_dir, new_file_storage_dir, err))
1515           result =  False,
1516       else:
1517         logger.Error("'%s' is not a directory" % old_file_storage_dir)
1518         result = False,
1519     else:
1520       if os.path.exists(old_file_storage_dir):
1521         logger.Error("Cannot rename '%s' to '%s'. Both locations exist." %
1522                      old_file_storage_dir, new_file_storage_dir)
1523         result = False,
1524   return result
1525
1526
1527 class HooksRunner(object):
1528   """Hook runner.
1529
1530   This class is instantiated on the node side (ganeti-noded) and not on
1531   the master side.
1532
1533   """
1534   RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1535
1536   def __init__(self, hooks_base_dir=None):
1537     """Constructor for hooks runner.
1538
1539     Args:
1540       - hooks_base_dir: if not None, this overrides the
1541         constants.HOOKS_BASE_DIR (useful for unittests)
1542
1543     """
1544     if hooks_base_dir is None:
1545       hooks_base_dir = constants.HOOKS_BASE_DIR
1546     self._BASE_DIR = hooks_base_dir
1547
1548   @staticmethod
1549   def ExecHook(script, env):
1550     """Exec one hook script.
1551
1552     Args:
1553      - script: the full path to the script
1554      - env: the environment with which to exec the script
1555
1556     """
1557     # exec the process using subprocess and log the output
1558     fdstdin = None
1559     try:
1560       fdstdin = open("/dev/null", "r")
1561       child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1562                                stderr=subprocess.STDOUT, close_fds=True,
1563                                shell=False, cwd="/", env=env)
1564       output = ""
1565       try:
1566         output = child.stdout.read(4096)
1567         child.stdout.close()
1568       except EnvironmentError, err:
1569         output += "Hook script error: %s" % str(err)
1570
1571       while True:
1572         try:
1573           result = child.wait()
1574           break
1575         except EnvironmentError, err:
1576           if err.errno == errno.EINTR:
1577             continue
1578           raise
1579     finally:
1580       # try not to leak fds
1581       for fd in (fdstdin, ):
1582         if fd is not None:
1583           try:
1584             fd.close()
1585           except EnvironmentError, err:
1586             # just log the error
1587             #logger.Error("While closing fd %s: %s" % (fd, err))
1588             pass
1589
1590     return result == 0, output
1591
1592   def RunHooks(self, hpath, phase, env):
1593     """Run the scripts in the hooks directory.
1594
1595     This method will not be usually overriden by child opcodes.
1596
1597     """
1598     if phase == constants.HOOKS_PHASE_PRE:
1599       suffix = "pre"
1600     elif phase == constants.HOOKS_PHASE_POST:
1601       suffix = "post"
1602     else:
1603       raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1604     rr = []
1605
1606     subdir = "%s-%s.d" % (hpath, suffix)
1607     dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1608     try:
1609       dir_contents = utils.ListVisibleFiles(dir_name)
1610     except OSError, err:
1611       # must log
1612       return rr
1613
1614     # we use the standard python sort order,
1615     # so 00name is the recommended naming scheme
1616     dir_contents.sort()
1617     for relname in dir_contents:
1618       fname = os.path.join(dir_name, relname)
1619       if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1620           self.RE_MASK.match(relname) is not None):
1621         rrval = constants.HKR_SKIP
1622         output = ""
1623       else:
1624         result, output = self.ExecHook(fname, env)
1625         if not result:
1626           rrval = constants.HKR_FAIL
1627         else:
1628           rrval = constants.HKR_SUCCESS
1629       rr.append(("%s/%s" % (subdir, relname), rrval, output))
1630
1631     return rr
1632
1633
1634 class IAllocatorRunner(object):
1635   """IAllocator runner.
1636
1637   This class is instantiated on the node side (ganeti-noded) and not on
1638   the master side.
1639
1640   """
1641   def Run(self, name, idata):
1642     """Run an iallocator script.
1643
1644     Return value: tuple of:
1645        - run status (one of the IARUN_ constants)
1646        - stdout
1647        - stderr
1648        - fail reason (as from utils.RunResult)
1649
1650     """
1651     alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
1652                                   os.path.isfile)
1653     if alloc_script is None:
1654       return (constants.IARUN_NOTFOUND, None, None, None)
1655
1656     fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
1657     try:
1658       os.write(fd, idata)
1659       os.close(fd)
1660       result = utils.RunCmd([alloc_script, fin_name])
1661       if result.failed:
1662         return (constants.IARUN_FAILURE, result.stdout, result.stderr,
1663                 result.fail_reason)
1664     finally:
1665       os.unlink(fin_name)
1666
1667     return (constants.IARUN_SUCCESS, result.stdout, result.stderr, None)
1668
1669
1670 class DevCacheManager(object):
1671   """Simple class for managing a cache of block device information.
1672
1673   """
1674   _DEV_PREFIX = "/dev/"
1675   _ROOT_DIR = constants.BDEV_CACHE_DIR
1676
1677   @classmethod
1678   def _ConvertPath(cls, dev_path):
1679     """Converts a /dev/name path to the cache file name.
1680
1681     This replaces slashes with underscores and strips the /dev
1682     prefix. It then returns the full path to the cache file
1683
1684     """
1685     if dev_path.startswith(cls._DEV_PREFIX):
1686       dev_path = dev_path[len(cls._DEV_PREFIX):]
1687     dev_path = dev_path.replace("/", "_")
1688     fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1689     return fpath
1690
1691   @classmethod
1692   def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1693     """Updates the cache information for a given device.
1694
1695     """
1696     if dev_path is None:
1697       logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1698       return
1699     fpath = cls._ConvertPath(dev_path)
1700     if on_primary:
1701       state = "primary"
1702     else:
1703       state = "secondary"
1704     if iv_name is None:
1705       iv_name = "not_visible"
1706     fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1707     try:
1708       utils.WriteFile(fpath, data=fdata)
1709     except EnvironmentError, err:
1710       logger.Error("Can't update bdev cache for %s, error %s" %
1711                    (dev_path, str(err)))
1712
1713   @classmethod
1714   def RemoveCache(cls, dev_path):
1715     """Remove data for a dev_path.
1716
1717     """
1718     if dev_path is None:
1719       logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1720       return
1721     fpath = cls._ConvertPath(dev_path)
1722     try:
1723       utils.RemoveFile(fpath)
1724     except EnvironmentError, err:
1725       logger.Error("Can't update bdev cache for %s, error %s" %
1726                    (dev_path, str(err)))