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