4 # Copyright (C) 2006, 2007 Google Inc.
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.
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.
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
22 """Functions used by the node daemon"""
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
47 """Activate local node as master node.
49 There are two needed steps for this:
50 - run the master script
51 - register the cron script
54 result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "start"])
57 logger.Error("could not activate cluster interface with command %s,"
58 " error: '%s'" % (result.cmd, result.output))
65 """Deactivate this node as master.
68 - run the master stop script
69 - remove link to master cron script.
72 result = utils.RunCmd([constants.MASTER_SCRIPT, "-d", "stop"])
75 logger.Error("could not deactivate cluster interface with command %s,"
76 " error: '%s'" % (result.cmd, result.output))
82 def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
83 """Joins this node to the cluster.
85 This does the following:
86 - updates the hostkeys of the machine (rsa and dsa)
87 - adds the ssh private key to the user
88 - adds the ssh public key to the users' authorized_keys file
91 sshd_keys = [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
92 (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
93 (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
94 (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
95 for name, content, mode in sshd_keys:
96 utils.WriteFile(name, data=content, mode=mode)
99 priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
101 except errors.OpExecError, err:
102 logger.Error("Error while processing user ssh files: %s" % err)
105 for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
106 utils.WriteFile(name, data=content, mode=0600)
108 utils.AddAuthorizedKey(auth_keys, sshpub)
110 utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
116 """Cleans up the current node and prepares it to be removed from the cluster.
119 if os.path.isdir(constants.DATA_DIR):
120 for rel_name in utils.ListVisibleFiles(constants.DATA_DIR):
121 full_name = os.path.join(constants.DATA_DIR, rel_name)
122 if os.path.isfile(full_name) and not os.path.islink(full_name):
123 utils.RemoveFile(full_name)
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)
132 f = open(pub_key, 'r')
134 utils.RemoveAuthorizedKey(auth_keys, f.read(8192))
138 utils.RemoveFile(priv_key)
139 utils.RemoveFile(pub_key)
142 def GetNodeInfo(vgname):
143 """Gives back a hash with different informations about the node.
146 { 'vg_size' : xxx, 'vg_free' : xxx, 'memory_domain0': xxx,
147 'memory_free' : xxx, 'memory_total' : xxx }
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
157 vginfo = _GetVGInfo(vgname)
158 outputarray['vg_size'] = vginfo['vg_size']
159 outputarray['vg_free'] = vginfo['vg_free']
161 hyper = hypervisor.GetHypervisor()
162 hyp_info = hyper.GetNodeInfo()
163 if hyp_info is not None:
164 outputarray.update(hyp_info)
166 f = open("/proc/sys/kernel/random/boot_id", 'r')
168 outputarray["bootid"] = f.read(128).rstrip("\n")
175 def VerifyNode(what):
176 """Verify the status of the local node.
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
184 Requested files on local node are checksummed and the result returned.
186 The nodelist is traversed, with the following checks being made
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.
195 if 'hypervisor' in what:
196 result['hypervisor'] = hypervisor.GetHypervisor().Verify()
198 if 'filelist' in what:
199 result['filelist'] = utils.FingerprintFiles(what['filelist'])
201 if 'nodelist' in what:
202 result['nodelist'] = {}
203 for node in what['nodelist']:
204 success, message = ssh.VerifyNodeHostname(node)
206 result['nodelist'][node] = message
210 def GetVolumeList(vg_name):
211 """Compute list of logical volumes and their size.
214 dictionary of all partions (key) with their size:
218 result = utils.RunCmd(["lvs", "--noheadings", "--units=m",
219 "-oname,size", vg_name])
221 logger.Error("Failed to list logical volumes, lvs output: %s" %
225 lvlist = [line.split() for line in result.output.splitlines()]
229 def ListVolumeGroups():
230 """List the volume groups and their size.
233 Dictionary with keys volume name and values the size of the volume
236 return utils.ListVolumeGroups()
240 """List all volumes on this node.
243 result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
245 "--options=lv_name,lv_size,devices,vg_name"])
247 logger.Error("Failed to list logical volumes, lvs output: %s" %
253 return dev.split('(')[0]
259 'name': line[0].strip(),
260 'size': line[1].strip(),
261 'dev': parse_dev(line[2].strip()),
262 'vg': line[3].strip(),
265 return [map_line(line.split('|')) for line in result.output.splitlines()]
268 def BridgesExist(bridges_list):
269 """Check if a list of bridges exist on the current node.
272 True if all of them exist, false otherwise
275 for bridge in bridges_list:
276 if not utils.BridgeExists(bridge):
282 def GetInstanceList():
283 """Provides a list of instances.
286 A list of all running instances on the current node
287 - instance1.example.com
288 - instance2.example.com
292 names = hypervisor.GetHypervisor().ListInstances()
293 except errors.HypervisorError, err:
294 logger.Error("error enumerating instances: %s" % str(err))
300 def GetInstanceInfo(instance):
301 """Gives back the informations about an instance as a dictionary.
304 instance: name of the instance (ex. instance1.example.com)
307 { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
309 memory: memory size of instance (int)
310 state: xen state of instance (string)
311 time: cpu time of instance (float)
316 iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
317 if iinfo is not None:
318 output['memory'] = iinfo[2]
319 output['state'] = iinfo[4]
320 output['time'] = iinfo[5]
325 def GetAllInstancesInfo():
326 """Gather data about all instances.
328 This is the equivalent of `GetInstanceInfo()`, except that it
329 computes data for all instances at once, thus being faster if one
330 needs data about more than one instance.
332 Returns: a dictionary of dictionaries, keys being the instance name,
334 { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
336 memory: memory size of instance (int)
337 state: xen state of instance (string)
338 time: cpu time of instance (float)
339 vcpus: the number of cpus
344 iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
346 for name, inst_id, memory, vcpus, state, times in iinfo:
357 def AddOSToInstance(instance, os_disk, swap_disk):
358 """Add an OS to an instance.
361 instance: the instance object
362 os_disk: the instance-visible name of the os device
363 swap_disk: the instance-visible name of the swap device
366 inst_os = OSFromDisk(instance.os)
368 create_script = inst_os.create_script
370 os_device = instance.FindDisk(os_disk)
371 if os_device is None:
372 logger.Error("Can't find this device-visible name '%s'" % os_disk)
375 swap_device = instance.FindDisk(swap_disk)
376 if swap_device is None:
377 logger.Error("Can't find this device-visible name '%s'" % swap_disk)
380 real_os_dev = _RecursiveFindBD(os_device)
381 if real_os_dev is None:
382 raise errors.BlockDeviceError("Block device '%s' is not set up" %
386 real_swap_dev = _RecursiveFindBD(swap_device)
387 if real_swap_dev is None:
388 raise errors.BlockDeviceError("Block device '%s' is not set up" %
392 logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
393 instance.name, int(time.time()))
394 if not os.path.exists(constants.LOG_OS_DIR):
395 os.mkdir(constants.LOG_OS_DIR, 0750)
397 command = utils.BuildShellCmd("cd %s && %s -i %s -b %s -s %s &>%s",
398 inst_os.path, create_script, instance.name,
399 real_os_dev.dev_path, real_swap_dev.dev_path,
402 result = utils.RunCmd(command)
405 logger.Error("os create command '%s' returned error: %s"
407 (command, result.fail_reason, result.output))
413 def RunRenameInstance(instance, old_name, os_disk, swap_disk):
414 """Run the OS rename script for an instance.
417 instance: the instance object
418 old_name: the old name of the instance
419 os_disk: the instance-visible name of the os device
420 swap_disk: the instance-visible name of the swap device
423 inst_os = OSFromDisk(instance.os)
425 script = inst_os.rename_script
427 os_device = instance.FindDisk(os_disk)
428 if os_device is None:
429 logger.Error("Can't find this device-visible name '%s'" % os_disk)
432 swap_device = instance.FindDisk(swap_disk)
433 if swap_device is None:
434 logger.Error("Can't find this device-visible name '%s'" % swap_disk)
437 real_os_dev = _RecursiveFindBD(os_device)
438 if real_os_dev is None:
439 raise errors.BlockDeviceError("Block device '%s' is not set up" %
443 real_swap_dev = _RecursiveFindBD(swap_device)
444 if real_swap_dev is None:
445 raise errors.BlockDeviceError("Block device '%s' is not set up" %
449 logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
451 instance.name, int(time.time()))
452 if not os.path.exists(constants.LOG_OS_DIR):
453 os.mkdir(constants.LOG_OS_DIR, 0750)
455 command = utils.BuildShellCmd("cd %s && %s -o %s -n %s -b %s -s %s &>%s",
456 inst_os.path, script, old_name, instance.name,
457 real_os_dev.dev_path, real_swap_dev.dev_path,
460 result = utils.RunCmd(command)
463 logger.Error("os create command '%s' returned error: %s"
465 (command, result.fail_reason, result.output))
471 def _GetVGInfo(vg_name):
472 """Get informations about the volume group.
475 vg_name: the volume group
478 { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
480 vg_size is the total size of the volume group in MiB
481 vg_free is the free size of the volume group in MiB
482 pv_count are the number of physical disks in that vg
485 retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
486 "--nosuffix", "--units=m", "--separator=:", vg_name])
489 errmsg = "volume group %s not present" % vg_name
491 raise errors.LVMError(errmsg)
492 valarr = retval.stdout.strip().split(':')
494 "vg_size": int(round(float(valarr[0]), 0)),
495 "vg_free": int(round(float(valarr[1]), 0)),
496 "pv_count": int(valarr[2]),
501 def _GatherBlockDevs(instance):
502 """Set up an instance's block device(s).
504 This is run on the primary node at instance startup. The block
505 devices must be already assembled.
509 for disk in instance.disks:
510 device = _RecursiveFindBD(disk)
512 raise errors.BlockDeviceError("Block device '%s' is not set up." %
515 block_devices.append((disk, device))
519 def StartInstance(instance, extra_args):
520 """Start an instance.
523 instance - name of instance to start.
526 running_instances = GetInstanceList()
528 if instance.name in running_instances:
531 block_devices = _GatherBlockDevs(instance)
532 hyper = hypervisor.GetHypervisor()
535 hyper.StartInstance(instance, block_devices, extra_args)
536 except errors.HypervisorError, err:
537 logger.Error("Failed to start instance: %s" % err)
543 def ShutdownInstance(instance):
544 """Shut an instance down.
547 instance - name of instance to shutdown.
550 running_instances = GetInstanceList()
552 if instance.name not in running_instances:
555 hyper = hypervisor.GetHypervisor()
557 hyper.StopInstance(instance)
558 except errors.HypervisorError, err:
559 logger.Error("Failed to stop instance: %s" % err)
562 # test every 10secs for 2min
566 for dummy in range(11):
567 if instance.name not in GetInstanceList():
571 # the shutdown did not succeed
572 logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
575 hyper.StopInstance(instance, force=True)
576 except errors.HypervisorError, err:
577 logger.Error("Failed to stop instance: %s" % err)
581 if instance.name in GetInstanceList():
582 logger.Error("could not shutdown instance '%s' even by destroy")
588 def RebootInstance(instance, reboot_type, extra_args):
589 """Reboot an instance.
592 instance - name of instance to reboot
593 reboot_type - how to reboot [soft,hard,full]
596 running_instances = GetInstanceList()
598 if instance.name not in running_instances:
599 logger.Error("Cannot reboot instance that is not running")
602 hyper = hypervisor.GetHypervisor()
603 if reboot_type == constants.INSTANCE_REBOOT_SOFT:
605 hyper.RebootInstance(instance)
606 except errors.HypervisorError, err:
607 logger.Error("Failed to soft reboot instance: %s" % err)
609 elif reboot_type == constants.INSTANCE_REBOOT_HARD:
611 ShutdownInstance(instance)
612 StartInstance(instance, extra_args)
613 except errors.HypervisorError, err:
614 logger.Error("Failed to hard reboot instance: %s" % err)
617 raise errors.ParameterError("reboot_type invalid")
623 def CreateBlockDevice(disk, size, owner, on_primary, info):
624 """Creates a block device for an instance.
627 bdev: a ganeti.objects.Disk object
628 size: the size of the physical underlying devices
629 do_open: if the device should be `Assemble()`-d and
630 `Open()`-ed after creation
633 the new unique_id of the device (this can sometime be
634 computed only after creation), or None. On secondary nodes,
635 it's not required to return anything.
640 for child in disk.children:
641 crdev = _RecursiveAssembleBD(child, owner, on_primary)
642 if on_primary or disk.AssembleOnSecondary():
643 # we need the children open in case the device itself has to
650 device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
651 if device is not None:
652 logger.Info("removing existing device %s" % disk)
654 except errors.BlockDeviceError, err:
657 device = bdev.Create(disk.dev_type, disk.physical_id,
660 raise ValueError("Can't create child device for %s, %s" %
662 if on_primary or disk.AssembleOnSecondary():
663 if not device.Assemble():
664 raise errors.BlockDeviceError("Can't assemble device after creation,"
665 " very unusual event - check the node"
667 device.SetSyncSpeed(constants.SYNC_SPEED)
668 if on_primary or disk.OpenOnSecondary():
669 device.Open(force=True)
670 DevCacheManager.UpdateCache(device.dev_path, owner,
671 on_primary, disk.iv_name)
675 physical_id = device.unique_id
679 def RemoveBlockDevice(disk):
680 """Remove a block device.
682 This is intended to be called recursively.
686 # since we are removing the device, allow a partial match
687 # this allows removal of broken mirrors
688 rdev = _RecursiveFindBD(disk, allow_partial=True)
689 except errors.BlockDeviceError, err:
690 # probably can't attach
691 logger.Info("Can't attach to device %s in remove" % disk)
694 r_path = rdev.dev_path
695 result = rdev.Remove()
697 DevCacheManager.RemoveCache(r_path)
701 for child in disk.children:
702 result = result and RemoveBlockDevice(child)
706 def _RecursiveAssembleBD(disk, owner, as_primary):
707 """Activate a block device for an instance.
709 This is run on the primary and secondary nodes for an instance.
711 This function is called recursively.
714 disk: a objects.Disk object
715 as_primary: if we should make the block device read/write
718 the assembled device or None (in case no device was assembled)
720 If the assembly is not successful, an exception is raised.
725 mcn = disk.ChildrenNeeded()
727 mcn = 0 # max number of Nones allowed
729 mcn = len(disk.children) - mcn # max number of Nones
730 for chld_disk in disk.children:
732 cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
733 except errors.BlockDeviceError, err:
734 if children.count(None) > mcn:
737 logger.Debug("Error in child activation: %s" % str(err))
738 children.append(cdev)
740 if as_primary or disk.AssembleOnSecondary():
741 r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
742 r_dev.SetSyncSpeed(constants.SYNC_SPEED)
744 if as_primary or disk.OpenOnSecondary():
748 DevCacheManager.UpdateCache(r_dev.dev_path, owner,
749 as_primary, disk.iv_name)
756 def AssembleBlockDevice(disk, owner, as_primary):
757 """Activate a block device for an instance.
759 This is a wrapper over _RecursiveAssembleBD.
762 a /dev path for primary nodes
763 True for secondary nodes
766 result = _RecursiveAssembleBD(disk, owner, as_primary)
767 if isinstance(result, bdev.BlockDev):
768 result = result.dev_path
772 def ShutdownBlockDevice(disk):
773 """Shut down a block device.
775 First, if the device is assembled (can `Attach()`), then the device
776 is shutdown. Then the children of the device are shutdown.
778 This function is called recursively. Note that we don't cache the
779 children or such, as oppossed to assemble, shutdown of different
780 devices doesn't require that the upper device was active.
783 r_dev = _RecursiveFindBD(disk)
784 if r_dev is not None:
785 r_path = r_dev.dev_path
786 result = r_dev.Shutdown()
788 DevCacheManager.RemoveCache(r_path)
792 for child in disk.children:
793 result = result and ShutdownBlockDevice(child)
797 def MirrorAddChildren(parent_cdev, new_cdevs):
798 """Extend a mirrored block device.
801 parent_bdev = _RecursiveFindBD(parent_cdev, allow_partial=True)
802 if parent_bdev is None:
803 logger.Error("Can't find parent device")
805 new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
806 if new_bdevs.count(None) > 0:
807 logger.Error("Can't find new device(s) to add: %s:%s" %
808 (new_bdevs, new_cdevs))
810 parent_bdev.AddChildren(new_bdevs)
814 def MirrorRemoveChildren(parent_cdev, new_cdevs):
815 """Shrink a mirrored block device.
818 parent_bdev = _RecursiveFindBD(parent_cdev)
819 if parent_bdev is None:
820 logger.Error("Can't find parent in remove children: %s" % parent_cdev)
823 for disk in new_cdevs:
824 rpath = disk.StaticDevPath()
826 bd = _RecursiveFindBD(disk)
828 logger.Error("Can't find dynamic device %s while removing children" %
832 devs.append(bd.dev_path)
835 parent_bdev.RemoveChildren(devs)
839 def GetMirrorStatus(disks):
840 """Get the mirroring status of a list of devices.
843 disks: list of `objects.Disk`
846 list of (mirror_done, estimated_time) tuples, which
847 are the result of bdev.BlockDevice.CombinedSyncStatus()
852 rbd = _RecursiveFindBD(dsk)
854 raise errors.BlockDeviceError("Can't find device %s" % str(dsk))
855 stats.append(rbd.CombinedSyncStatus())
859 def _RecursiveFindBD(disk, allow_partial=False):
860 """Check if a device is activated.
862 If so, return informations about the real device.
865 disk: the objects.Disk instance
866 allow_partial: don't abort the find if a child of the
867 device can't be found; this is intended to be
868 used when repairing mirrors
871 None if the device can't be found
872 otherwise the device instance
877 for chdisk in disk.children:
878 children.append(_RecursiveFindBD(chdisk))
880 return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
883 def FindBlockDevice(disk):
884 """Check if a device is activated.
886 If so, return informations about the real device.
889 disk: the objects.Disk instance
891 None if the device can't be found
892 (device_path, major, minor, sync_percent, estimated_time, is_degraded)
895 rbd = _RecursiveFindBD(disk)
898 sync_p, est_t, is_degr = rbd.GetSyncStatus()
899 return rbd.dev_path, rbd.major, rbd.minor, sync_p, est_t, is_degr
902 def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
903 """Write a file to the filesystem.
905 This allows the master to overwrite(!) a file. It will only perform
906 the operation if the file belongs to a list of configuration files.
909 if not os.path.isabs(file_name):
910 logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
914 allowed_files = [constants.CLUSTER_CONF_FILE, "/etc/hosts",
915 constants.SSH_KNOWN_HOSTS_FILE]
916 allowed_files.extend(ssconf.SimpleStore().GetFileList())
917 if file_name not in allowed_files:
918 logger.Error("Filename passed to UploadFile not in allowed"
919 " upload targets: '%s'" % file_name)
922 dir_name, small_name = os.path.split(file_name)
923 fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
924 # here we need to make sure we remove the temp file, if any error
927 os.chown(new_name, uid, gid)
928 os.chmod(new_name, mode)
931 os.utime(new_name, (atime, mtime))
932 os.rename(new_name, file_name)
935 utils.RemoveFile(new_name)
939 def _ErrnoOrStr(err):
940 """Format an EnvironmentError exception.
942 If the `err` argument has an errno attribute, it will be looked up
943 and converted into a textual EXXXX description. Otherwise the string
944 representation of the error will be returned.
947 if hasattr(err, 'errno'):
948 detail = errno.errorcode[err.errno]
954 def _OSSearch(name, search_path=None):
955 """Search for OSes with the given name in the search_path.
958 name: The name of the OS to look for
959 search_path: List of dirs to search (defaults to constants.OS_SEARCH_PATH)
962 The base_dir the OS resides in
965 if search_path is None:
966 search_path = constants.OS_SEARCH_PATH
968 for dir_name in search_path:
969 t_os_dir = os.path.sep.join([dir_name, name])
970 if os.path.isdir(t_os_dir):
976 def _OSOndiskVersion(name, os_dir):
977 """Compute and return the API version of a given OS.
979 This function will try to read the API version of the os given by
980 the 'name' parameter and residing in the 'os_dir' directory.
982 Return value will be either an integer denoting the version or None in the
983 case when this is not a valid OS name.
986 api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
989 st = os.stat(api_file)
990 except EnvironmentError, err:
991 raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file not"
992 " found (%s)" % _ErrnoOrStr(err))
994 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
995 raise errors.InvalidOS(name, os_dir, "'ganeti_api_version' file is not"
1001 api_version = f.read(256)
1004 except EnvironmentError, err:
1005 raise errors.InvalidOS(name, os_dir, "error while reading the"
1006 " API version (%s)" % _ErrnoOrStr(err))
1008 api_version = api_version.strip()
1010 api_version = int(api_version)
1011 except (TypeError, ValueError), err:
1012 raise errors.InvalidOS(name, os_dir,
1013 "API version is not integer (%s)" % str(err))
1018 def DiagnoseOS(top_dirs=None):
1019 """Compute the validity for all OSes.
1021 Returns an OS object for each name in all the given top directories
1022 (if not given defaults to constants.OS_SEARCH_PATH)
1028 if top_dirs is None:
1029 top_dirs = constants.OS_SEARCH_PATH
1032 for dir_name in top_dirs:
1033 if os.path.isdir(dir_name):
1035 f_names = utils.ListVisibleFiles(dir_name)
1036 except EnvironmentError, err:
1037 logger.Error("Can't list the OS directory %s: %s" %
1038 (dir_name, str(err)))
1040 for name in f_names:
1042 os_inst = OSFromDisk(name, base_dir=dir_name)
1043 result.append(os_inst)
1044 except errors.InvalidOS, err:
1045 result.append(objects.OS.FromInvalidOS(err))
1050 def OSFromDisk(name, base_dir=None):
1051 """Create an OS instance from disk.
1053 This function will return an OS instance if the given name is a
1054 valid OS name. Otherwise, it will raise an appropriate
1055 `errors.InvalidOS` exception, detailing why this is not a valid
1059 os_dir: Directory containing the OS scripts. Defaults to a search
1060 in all the OS_SEARCH_PATH directories.
1064 if base_dir is None:
1065 base_dir = _OSSearch(name)
1067 if base_dir is None:
1068 raise errors.InvalidOS(name, None, "OS dir not found in search path")
1070 os_dir = os.path.sep.join([base_dir, name])
1071 api_version = _OSOndiskVersion(name, os_dir)
1073 if api_version != constants.OS_API_VERSION:
1074 raise errors.InvalidOS(name, os_dir, "API version mismatch"
1075 " (found %s want %s)"
1076 % (api_version, constants.OS_API_VERSION))
1078 # OS Scripts dictionary, we will populate it with the actual script names
1079 os_scripts = {'create': '', 'export': '', 'import': '', 'rename': ''}
1081 for script in os_scripts:
1082 os_scripts[script] = os.path.sep.join([os_dir, script])
1085 st = os.stat(os_scripts[script])
1086 except EnvironmentError, err:
1087 raise errors.InvalidOS(name, os_dir, "'%s' script missing (%s)" %
1088 (script, _ErrnoOrStr(err)))
1090 if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1091 raise errors.InvalidOS(name, os_dir, "'%s' script not executable" %
1094 if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1095 raise errors.InvalidOS(name, os_dir, "'%s' is not a regular file" %
1099 return objects.OS(name=name, path=os_dir, status=constants.OS_VALID_STATUS,
1100 create_script=os_scripts['create'],
1101 export_script=os_scripts['export'],
1102 import_script=os_scripts['import'],
1103 rename_script=os_scripts['rename'],
1104 api_version=api_version)
1107 def SnapshotBlockDevice(disk):
1108 """Create a snapshot copy of a block device.
1110 This function is called recursively, and the snapshot is actually created
1111 just for the leaf lvm backend device.
1114 disk: the disk to be snapshotted
1117 a config entry for the actual lvm device snapshotted.
1121 if len(disk.children) == 1:
1122 # only one child, let's recurse on it
1123 return SnapshotBlockDevice(disk.children[0])
1125 # more than one child, choose one that matches
1126 for child in disk.children:
1127 if child.size == disk.size:
1128 # return implies breaking the loop
1129 return SnapshotBlockDevice(child)
1130 elif disk.dev_type == constants.LD_LV:
1131 r_dev = _RecursiveFindBD(disk)
1132 if r_dev is not None:
1133 # let's stay on the safe side and ask for the full size, for now
1134 return r_dev.Snapshot(disk.size)
1138 raise errors.ProgrammerError("Cannot snapshot non-lvm block device"
1139 "'%s' of type '%s'" %
1140 (disk.unique_id, disk.dev_type))
1143 def ExportSnapshot(disk, dest_node, instance):
1144 """Export a block device snapshot to a remote node.
1147 disk: the snapshot block device
1148 dest_node: the node to send the image to
1149 instance: instance being exported
1152 True if successful, False otherwise.
1155 inst_os = OSFromDisk(instance.os)
1156 export_script = inst_os.export_script
1158 logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1159 instance.name, int(time.time()))
1160 if not os.path.exists(constants.LOG_OS_DIR):
1161 os.mkdir(constants.LOG_OS_DIR, 0750)
1163 real_os_dev = _RecursiveFindBD(disk)
1164 if real_os_dev is None:
1165 raise errors.BlockDeviceError("Block device '%s' is not set up" %
1169 destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1170 destfile = disk.physical_id[1]
1172 # the target command is built out of three individual commands,
1173 # which are joined by pipes; we check each individual command for
1176 expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
1177 export_script, instance.name,
1178 real_os_dev.dev_path, logfile)
1182 destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1183 destdir, destdir, destfile)
1184 remotecmd = ssh.BuildSSHCmd(dest_node, constants.GANETI_RUNAS, destcmd)
1188 # all commands have been checked, so we're safe to combine them
1189 command = '|'.join([expcmd, comprcmd, utils.ShellQuoteArgs(remotecmd)])
1191 result = utils.RunCmd(command)
1194 logger.Error("os snapshot export command '%s' returned error: %s"
1196 (command, result.fail_reason, result.output))
1202 def FinalizeExport(instance, snap_disks):
1203 """Write out the export configuration information.
1206 instance: instance configuration
1207 snap_disks: snapshot block devices
1210 False in case of error, True otherwise.
1213 destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1214 finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1216 config = objects.SerializableConfigParser()
1218 config.add_section(constants.INISECT_EXP)
1219 config.set(constants.INISECT_EXP, 'version', '0')
1220 config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1221 config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1222 config.set(constants.INISECT_EXP, 'os', instance.os)
1223 config.set(constants.INISECT_EXP, 'compression', 'gzip')
1225 config.add_section(constants.INISECT_INS)
1226 config.set(constants.INISECT_INS, 'name', instance.name)
1227 config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
1228 config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
1229 config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1230 for nic_count, nic in enumerate(instance.nics):
1231 config.set(constants.INISECT_INS, 'nic%d_mac' %
1232 nic_count, '%s' % nic.mac)
1233 config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1234 # TODO: redundant: on load can read nics until it doesn't exist
1235 config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
1237 for disk_count, disk in enumerate(snap_disks):
1238 config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1239 ('%s' % disk.iv_name))
1240 config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1241 ('%s' % disk.physical_id[1]))
1242 config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1244 config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
1246 cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
1247 cfo = open(cff, 'w')
1253 shutil.rmtree(finaldestdir, True)
1254 shutil.move(destdir, finaldestdir)
1259 def ExportInfo(dest):
1260 """Get export configuration information.
1263 dest: directory containing the export
1266 A serializable config file containing the export info.
1269 cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1271 config = objects.SerializableConfigParser()
1274 if (not config.has_section(constants.INISECT_EXP) or
1275 not config.has_section(constants.INISECT_INS)):
1281 def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
1282 """Import an os image into an instance.
1285 instance: the instance object
1286 os_disk: the instance-visible name of the os device
1287 swap_disk: the instance-visible name of the swap device
1288 src_node: node holding the source image
1289 src_image: path to the source image on src_node
1292 False in case of error, True otherwise.
1295 inst_os = OSFromDisk(instance.os)
1296 import_script = inst_os.import_script
1298 os_device = instance.FindDisk(os_disk)
1299 if os_device is None:
1300 logger.Error("Can't find this device-visible name '%s'" % os_disk)
1303 swap_device = instance.FindDisk(swap_disk)
1304 if swap_device is None:
1305 logger.Error("Can't find this device-visible name '%s'" % swap_disk)
1308 real_os_dev = _RecursiveFindBD(os_device)
1309 if real_os_dev is None:
1310 raise errors.BlockDeviceError("Block device '%s' is not set up" %
1314 real_swap_dev = _RecursiveFindBD(swap_device)
1315 if real_swap_dev is None:
1316 raise errors.BlockDeviceError("Block device '%s' is not set up" %
1318 real_swap_dev.Open()
1320 logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1321 instance.name, int(time.time()))
1322 if not os.path.exists(constants.LOG_OS_DIR):
1323 os.mkdir(constants.LOG_OS_DIR, 0750)
1325 destcmd = utils.BuildShellCmd('cat %s', src_image)
1326 remotecmd = ssh.BuildSSHCmd(src_node, constants.GANETI_RUNAS, destcmd)
1329 impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
1330 inst_os.path, import_script, instance.name,
1331 real_os_dev.dev_path, real_swap_dev.dev_path,
1334 command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
1336 result = utils.RunCmd(command)
1339 logger.Error("os import command '%s' returned error: %s"
1341 (command, result.fail_reason, result.output))
1348 """Return a list of exports currently available on this machine.
1351 if os.path.isdir(constants.EXPORT_DIR):
1352 return utils.ListVisibleFiles(constants.EXPORT_DIR)
1357 def RemoveExport(export):
1358 """Remove an existing export from the node.
1361 export: the name of the export to remove
1364 False in case of error, True otherwise.
1367 target = os.path.join(constants.EXPORT_DIR, export)
1369 shutil.rmtree(target)
1370 # TODO: catch some of the relevant exceptions and provide a pretty
1371 # error message if rmtree fails.
1376 def RenameBlockDevices(devlist):
1377 """Rename a list of block devices.
1379 The devlist argument is a list of tuples (disk, new_logical,
1380 new_physical). The return value will be a combined boolean result
1381 (True only if all renames succeeded).
1385 for disk, unique_id in devlist:
1386 dev = _RecursiveFindBD(disk)
1391 old_rpath = dev.dev_path
1392 dev.Rename(unique_id)
1393 new_rpath = dev.dev_path
1394 if old_rpath != new_rpath:
1395 DevCacheManager.RemoveCache(old_rpath)
1396 # FIXME: we should add the new cache information here, like:
1397 # DevCacheManager.UpdateCache(new_rpath, owner, ...)
1398 # but we don't have the owner here - maybe parse from existing
1399 # cache? for now, we only lose lvm data when we rename, which
1400 # is less critical than DRBD or MD
1401 except errors.BlockDeviceError, err:
1402 logger.Error("Can't rename device '%s' to '%s': %s" %
1403 (dev, unique_id, err))
1408 class HooksRunner(object):
1411 This class is instantiated on the node side (ganeti-noded) and not on
1415 RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
1417 def __init__(self, hooks_base_dir=None):
1418 """Constructor for hooks runner.
1421 - hooks_base_dir: if not None, this overrides the
1422 constants.HOOKS_BASE_DIR (useful for unittests)
1423 - logs_base_dir: if not None, this overrides the
1424 constants.LOG_HOOKS_DIR (useful for unittests)
1425 - logging: enable or disable logging of script output
1428 if hooks_base_dir is None:
1429 hooks_base_dir = constants.HOOKS_BASE_DIR
1430 self._BASE_DIR = hooks_base_dir
1433 def ExecHook(script, env):
1434 """Exec one hook script.
1438 - script: the full path to the script
1439 - env: the environment with which to exec the script
1442 # exec the process using subprocess and log the output
1445 fdstdin = open("/dev/null", "r")
1446 child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
1447 stderr=subprocess.STDOUT, close_fds=True,
1448 shell=False, cwd="/",env=env)
1451 output = child.stdout.read(4096)
1452 child.stdout.close()
1453 except EnvironmentError, err:
1454 output += "Hook script error: %s" % str(err)
1458 result = child.wait()
1460 except EnvironmentError, err:
1461 if err.errno == errno.EINTR:
1465 # try not to leak fds
1466 for fd in (fdstdin, ):
1470 except EnvironmentError, err:
1471 # just log the error
1472 #logger.Error("While closing fd %s: %s" % (fd, err))
1475 return result == 0, output
1477 def RunHooks(self, hpath, phase, env):
1478 """Run the scripts in the hooks directory.
1480 This method will not be usually overriden by child opcodes.
1483 if phase == constants.HOOKS_PHASE_PRE:
1485 elif phase == constants.HOOKS_PHASE_POST:
1488 raise errors.ProgrammerError("Unknown hooks phase: '%s'" % phase)
1491 subdir = "%s-%s.d" % (hpath, suffix)
1492 dir_name = "%s/%s" % (self._BASE_DIR, subdir)
1494 dir_contents = utils.ListVisibleFiles(dir_name)
1495 except OSError, err:
1499 # we use the standard python sort order,
1500 # so 00name is the recommended naming scheme
1502 for relname in dir_contents:
1503 fname = os.path.join(dir_name, relname)
1504 if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
1505 self.RE_MASK.match(relname) is not None):
1506 rrval = constants.HKR_SKIP
1509 result, output = self.ExecHook(fname, env)
1511 rrval = constants.HKR_FAIL
1513 rrval = constants.HKR_SUCCESS
1514 rr.append(("%s/%s" % (subdir, relname), rrval, output))
1519 class DevCacheManager(object):
1520 """Simple class for managing a chache of block device information.
1523 _DEV_PREFIX = "/dev/"
1524 _ROOT_DIR = constants.BDEV_CACHE_DIR
1527 def _ConvertPath(cls, dev_path):
1528 """Converts a /dev/name path to the cache file name.
1530 This replaces slashes with underscores and strips the /dev
1531 prefix. It then returns the full path to the cache file
1534 if dev_path.startswith(cls._DEV_PREFIX):
1535 dev_path = dev_path[len(cls._DEV_PREFIX):]
1536 dev_path = dev_path.replace("/", "_")
1537 fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
1541 def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
1542 """Updates the cache information for a given device.
1545 if dev_path is None:
1546 logger.Error("DevCacheManager.UpdateCache got a None dev_path")
1548 fpath = cls._ConvertPath(dev_path)
1554 iv_name = "not_visible"
1555 fdata = "%s %s %s\n" % (str(owner), state, iv_name)
1557 utils.WriteFile(fpath, data=fdata)
1558 except EnvironmentError, err:
1559 logger.Error("Can't update bdev cache for %s, error %s" %
1560 (dev_path, str(err)))
1563 def RemoveCache(cls, dev_path):
1564 """Remove data for a dev_path.
1567 if dev_path is None:
1568 logger.Error("DevCacheManager.RemoveCache got a None dev_path")
1570 fpath = cls._ConvertPath(dev_path)
1572 utils.RemoveFile(fpath)
1573 except EnvironmentError, err:
1574 logger.Error("Can't update bdev cache for %s, error %s" %
1575 (dev_path, str(err)))