Merge branch 'next' into branch-2.1
[ganeti-local] / tools / lvmstrap
1 #!/usr/bin/python
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 """Program which configures LVM on the Ganeti nodes.
23
24 This program wipes disks and creates a volume group on top of them. It
25 can also show disk information to help you decide which disks you want
26 to wipe.
27
28 The error handling is done by raising our own exceptions from most of
29 the functions; these exceptions then handled globally in the main()
30 function. The exceptions that each function can raise are not
31 documented individually, since almost every error path ends in a
32 raise.
33
34 Another two exceptions that are handled globally are IOError and
35 OSError. The idea behind this is, since we run as root, we should
36 usually not get these errors, but if we do it's most probably a system
37 error, so they should be handled and the user instructed to report
38 them.
39 """
40
41
42 import os
43 import sys
44 import optparse
45 import time
46
47 from ganeti.utils import RunCmd, ReadFile
48 from ganeti import constants
49
50 USAGE = ("\tlvmstrap diskinfo\n"
51          "\tlvmstrap [--vgname=NAME] [--allow-removable]"
52          " { --alldisks | --disks DISKLIST }"
53          " create")
54
55 verbose_flag = False
56
57
58 class Error(Exception):
59   """Generic exception"""
60   pass
61
62
63 class ProgrammingError(Error):
64   """Exception denoting invalid assumptions in programming.
65
66   This should catch sysfs tree changes, or otherwise incorrect
67   assumptions about the contents of the /sys/block/... directories.
68   """
69   pass
70
71
72 class SysconfigError(Error):
73   """Exception denoting invalid system configuration.
74
75   If the system configuration is somehow wrong (e.g. /dev files
76   missing, or having mismatched major/minor numbers relative to
77   /sys/block devices), this exception will be raised.
78
79   This should usually mean that the installation of the Xen node
80   failed in some steps.
81   """
82   pass
83
84
85 class PrereqError(Error):
86   """Exception denoting invalid prerequisites.
87
88   If the node does not meet the requirements for cluster membership, this
89   exception will be raised. Things like wrong kernel version, or no
90   free disks, etc. belong here.
91
92   This should usually mean that the build steps for the Xen node were
93   not followed correctly.
94   """
95   pass
96
97
98 class OperationalError(Error):
99   """Exception denoting actual errors.
100
101   Errors during the bootstrapping are signaled using this exception.
102   """
103   pass
104
105
106 class ParameterError(Error):
107   """Exception denoting invalid input from user.
108
109   Wrong disks given as parameters will be signaled using this
110   exception.
111   """
112   pass
113
114
115 def Usage():
116   """Shows program usage information and exits the program."""
117
118   print >> sys.stderr, "Usage:"
119   print >> sys.stderr, USAGE
120   sys.exit(2)
121
122
123 def ParseOptions():
124   """Parses the command line options.
125
126   In case of command line errors, it will show the usage and exit the
127   program.
128
129   Returns:
130     (options, args), as returned by OptionParser.parse_args
131   """
132   global verbose_flag
133
134   parser = optparse.OptionParser(usage="\n%s" % USAGE,
135                                  version="%%prog (ganeti) %s" %
136                                  constants.RELEASE_VERSION)
137
138   parser.add_option("--alldisks", dest="alldisks",
139                     help="erase ALL disks", action="store_true",
140                     default=False)
141   parser.add_option("-d", "--disks", dest="disks",
142                     help="Choose disks (e.g. hda,hdg)",
143                     metavar="DISKLIST")
144   parser.add_option("-v", "--verbose",
145                     action="store_true", dest="verbose", default=False,
146                     help="print command execution messages to stdout")
147   parser.add_option("-r", "--allow-removable",
148                     action="store_true", dest="removable_ok", default=False,
149                     help="allow and use removable devices too")
150   parser.add_option("-g", "--vg-name", type="string",
151                     dest="vgname", default="xenvg", metavar="NAME",
152                     help="the volume group to be created [default: xenvg]")
153
154
155   options, args = parser.parse_args()
156   if len(args) != 1:
157     Usage()
158
159   verbose_flag = options.verbose
160
161   return options, args
162
163
164 def ExecCommand(command):
165   """Executes a command.
166
167   This is just a wrapper around commands.getstatusoutput, with the
168   difference that if the command line argument -v has been given, it
169   will print the command line and the command output on stdout.
170
171   Args:
172     the command line
173   Returns:
174     (status, output) where status is the exit status and output the
175       stdout and stderr of the command together
176   """
177
178   if verbose_flag:
179     print command
180   result = RunCmd(command)
181   if verbose_flag:
182     print result.output
183   return result
184
185
186 def CheckPrereq():
187   """Check the prerequisites of this program.
188
189   It check that it runs on Linux 2.6, and that /sys is mounted and the
190   fact that /sys/block is a directory.
191   """
192
193   if os.getuid() != 0:
194     raise PrereqError("This tool runs as root only. Really.")
195
196   osname, nodename, release, version, arch = os.uname()
197   if osname != 'Linux':
198     raise PrereqError("This tool only runs on Linux"
199                       " (detected OS: %s)." % osname)
200
201   if not release.startswith("2.6."):
202     raise PrereqError("Wrong major kernel version (detected %s, needs"
203                       " 2.6.*)" % release)
204
205   if not os.path.ismount("/sys"):
206     raise PrereqError("Can't find a filesystem mounted at /sys."
207                       " Please mount /sys.")
208
209   if not os.path.isdir("/sys/block"):
210     raise SysconfigError("Can't find /sys/block directory. Has the"
211                          " layout of /sys changed?")
212
213   if not os.path.ismount("/proc"):
214     raise PrereqError("Can't find a filesystem mounted at /proc."
215                       " Please mount /proc.")
216
217   if not os.path.exists("/proc/mounts"):
218     raise SysconfigError("Can't find /proc/mounts")
219
220
221 def CheckVGExists(vgname):
222   """Checks to see if a volume group exists.
223
224   Args:
225     vgname: the volume group name
226
227   Returns:
228     a four-tuple (exists, lv_count, vg_size, vg_free), where:
229       exists: True if the volume exists, otherwise False; if False,
230         all other members of the tuple are None
231       lv_count: The number of logical volumes in the volume group
232       vg_size: The total size of the volume group (in gibibytes)
233       vg_free: The available space in the volume group
234   """
235
236   result = ExecCommand("vgs --nohead -o lv_count,vg_size,vg_free"
237                        " --nosuffix --units g"
238                        " --ignorelockingfailure %s" % vgname)
239   if not result.failed:
240     try:
241       lv_count, vg_size, vg_free = result.stdout.strip().split()
242     except ValueError:
243       # This means the output of vgdisplay can't be parsed
244       raise PrereqError("cannot parse output of vgs (%s)" % result.stdout)
245   else:
246     lv_count = vg_size = vg_free = None
247
248   return not result.failed, lv_count, vg_size, vg_free
249
250
251 def CheckSysDev(name, devnum):
252   """Checks consistency between /sys and /dev trees.
253
254   In /sys/block/<name>/dev and /sys/block/<name>/<part>/dev are the
255   kernel-known device numbers. The /dev/<name> block/char devices are
256   created by userspace and thus could differ from the kernel
257   view. This function checks the consistency between the device number
258   read from /sys and the actual device number in /dev.
259
260   Note that since the system could be using udev which removes and
261   recreates the device nodes on partition table rescan, we need to do
262   some retries here. Since we only do a stat, we can afford to do many
263   short retries.
264
265   Args:
266    name: the device name, e.g. 'sda'
267    devnum: the device number, e.g. 0x803 (2051 in decimal) for sda3
268
269   Returns:
270     None; failure of the check is signaled by raising a
271       SysconfigError exception
272   """
273
274   path = "/dev/%s" % name
275   for retries in range(40):
276     if os.path.exists(path):
277       break
278     time.sleep(0.250)
279   else:
280     raise SysconfigError("the device file %s does not exist, but the block"
281                          " device exists in the /sys/block tree" % path)
282   rdev = os.stat(path).st_rdev
283   if devnum != rdev:
284     raise SysconfigError("For device %s, the major:minor in /dev is %04x"
285                          " while the major:minor in sysfs is %s" %
286                          (path, rdev, devnum))
287
288
289 def ReadDev(syspath):
290   """Reads the device number from a sysfs path.
291
292   The device number is given in sysfs under a block device directory
293   in a file named 'dev' which contains major:minor (in ASCII). This
294   function reads that file and converts the major:minor pair to a dev
295   number.
296
297   Args:
298     syspath: the path to a block device dir in sysfs, e.g. /sys/block/sda
299
300   Returns:
301     the device number
302   """
303
304   if not os.path.exists("%s/dev" % syspath):
305     raise ProgrammingError("Invalid path passed to ReadDev: %s" % syspath)
306   f = open("%s/dev" % syspath)
307   data = f.read().strip()
308   f.close()
309   major, minor = data.split(":", 1)
310   major = int(major)
311   minor = int(minor)
312   dev = os.makedev(major, minor)
313   return dev
314
315
316 def ReadSize(syspath):
317   """Reads the size from a sysfs path.
318
319   The size is given in sysfs under a block device directory in a file
320   named 'size' which contains the number of sectors (in ASCII). This
321   function reads that file and converts the number in sectors to the
322   size in bytes.
323
324   Args:
325     syspath: the path to a block device dir in sysfs, e.g. /sys/block/sda
326
327   Returns:
328     the device size in bytes
329   """
330
331   if not os.path.exists("%s/size" % syspath):
332     raise ProgrammingError("Invalid path passed to ReadSize: %s" % syspath)
333   f = open("%s/size" % syspath)
334   data = f.read().strip()
335   f.close()
336   size = 512L * int(data)
337   return size
338
339
340 def ReadPV(name):
341   """Reads physical volume information.
342
343   This function tries to see if a block device is a physical volume.
344
345   Args:
346     dev: the device name (e.g. sda)
347   Returns:
348     The name of the volume group to which this PV belongs, or
349     "" if this PV is not in use, or
350     None if this is not a PV
351   """
352
353   result = ExecCommand("pvdisplay -c /dev/%s" % name)
354   if result.failed:
355     return None
356   vgname = result.stdout.strip().split(":")[1]
357   return vgname
358
359
360 def GetDiskList(opts):
361   """Computes the block device list for this system.
362
363   This function examines the /sys/block tree and using information
364   therein, computes the status of the block device.
365
366   Returns:
367     [(name, size, dev, partitions, inuse), ...]
368   where:
369     name is the block device name (e.g. sda)
370     size the size in bytes
371     dev  the device number (e.g. 8704 for hdg)
372     partitions is [(name, size, dev), ...] mirroring the disk list data
373     inuse is a boolean showing the in-use status of the disk, computed as the
374       possibility of re-reading the partition table (the meaning of the
375       operation varies with the kernel version, but is usually accurate;
376       a mounted disk/partition or swap-area or PV with active LVs on it
377       is busy)
378   """
379
380   dlist = []
381   for name in os.listdir("/sys/block"):
382     if (not name.startswith("hd") and
383         not name.startswith("sd") and
384         not name.startswith("ubd")):
385       continue
386
387     size = ReadSize("/sys/block/%s" % name)
388
389     f = open("/sys/block/%s/removable" % name)
390     removable = int(f.read().strip())
391     f.close()
392
393     if removable and not opts.removable_ok:
394       continue
395
396     dev = ReadDev("/sys/block/%s" % name)
397     CheckSysDev(name, dev)
398     inuse = not CheckReread(name)
399     # Enumerate partitions of the block device
400     partitions = []
401     for partname in os.listdir("/sys/block/%s" % name):
402       if not partname.startswith(name):
403         continue
404       partdev = ReadDev("/sys/block/%s/%s" % (name, partname))
405       partsize = ReadSize("/sys/block/%s/%s" % (name, partname))
406       CheckSysDev(partname, partdev)
407       partitions.append((partname, partsize, partdev))
408     partitions.sort()
409     dlist.append((name, size, dev, partitions, inuse))
410   dlist.sort()
411   return dlist
412
413
414 def GetMountInfo():
415   """Reads /proc/mounts and computes the mountpoint-devnum mapping.
416
417   This function reads /proc/mounts, finds the mounted filesystems
418   (excepting a hard-coded blacklist of network and virtual
419   filesystems) and does a stat on these mountpoints. The st_dev number
420   of the results is memorised for later matching against the
421   /sys/block devices.
422
423   Returns:
424    a mountpoint: device number dictionary
425   """
426
427   mountlines = ReadFile("/proc/mounts").splitlines()
428   mounts = {}
429   for line in mountlines:
430     device, mountpoint, fstype, rest = line.split(None, 3)
431     # fs type blacklist
432     if fstype in ["nfs", "nfs4", "autofs", "tmpfs", "proc", "sysfs"]:
433       continue
434     try:
435       dev = os.stat(mountpoint).st_dev
436     except OSError, err:
437       # this should be a fairly rare error, since we are blacklisting
438       # network filesystems; with this in mind, we'll ignore it,
439       # since the rereadpt check catches in-use filesystems,
440       # and this is used for disk information only
441       print >> sys.stderr, ("Can't stat mountpoint '%s': %s" %
442                             (mountpoint, err))
443       print >> sys.stderr, "Ignoring."
444       continue
445     mounts[dev] = mountpoint
446   return mounts
447
448
449 def DevInfo(name, dev, mountinfo):
450   """Computes miscellaneous information about a block device.
451
452   Args:
453     name: the device name, e.g. sda
454
455   Returns:
456     (mpath, whatvg, fileinfo), where
457     mpath is the mount path where this device is mounted or None
458     whatvg is the result of the ReadPV function
459     fileinfo is the output of file -bs on the device
460   """
461
462   if dev in mountinfo:
463     mpath = mountinfo[dev]
464   else:
465     mpath = None
466
467   whatvg = ReadPV(name)
468
469   result = ExecCommand("file -bs /dev/%s" % name)
470   if result.failed:
471     fileinfo = "<error: %s>" % result.stderr
472   fileinfo = result.stdout[:45]
473   return mpath, whatvg, fileinfo
474
475
476 def ShowDiskInfo(opts):
477   """Shows a nicely formatted block device list for this system.
478
479   This function shows the user a table with the information gathered
480   by the other functions defined, in order to help the user make a
481   choice about which disks should be allocated to our volume group.
482
483   """
484   mounts = GetMountInfo()
485   dlist = GetDiskList(opts)
486
487   print "------- Disk information -------"
488   print ("%5s %7s %4s %5s %-10s %s" %
489          ("Name", "Size[M]", "Used", "Mount", "LVM?", "Info"))
490
491   flatlist = []
492   # Flatten the [(disk, [partition,...]), ...] list
493   for name, size, dev, parts, inuse in dlist:
494     if inuse:
495       str_inuse = "yes"
496     else:
497       str_inuse = "no"
498     flatlist.append((name, size, dev, str_inuse))
499     for partname, partsize, partdev in parts:
500       flatlist.append((partname, partsize, partdev, ""))
501
502   for name, size, dev, in_use in flatlist:
503     mp, vgname, fileinfo = DevInfo(name, dev, mounts)
504     if mp is None:
505       mp = "-"
506     if vgname is None:
507       lvminfo = "-"
508     elif vgname == "":
509       lvminfo = "yes,free"
510     else:
511       lvminfo = "in %s" % vgname
512
513     if len(name) > 3:
514       # Indent partitions
515       name = " %s" % name
516     print ("%-5s %7.2f %-4s %-5s %-10s %s" %
517            (name, float(size) / 1024 / 1024, in_use, mp, lvminfo, fileinfo))
518
519
520 def CheckReread(name):
521   """Check to see if a block device is in use.
522
523   Uses blockdev to reread the partition table of a block device, and
524   thus compute the in-use status. See the discussion in GetDiskList
525   about the meaning of 'in use'.
526
527   Returns:
528     boolean, the in-use status of the device
529   """
530
531   for retries in range(3):
532     result = ExecCommand("blockdev --rereadpt /dev/%s" % name)
533     if not result.failed:
534       break
535     time.sleep(2)
536
537   return not result.failed
538
539
540 def WipeDisk(name):
541   """Wipes a block device.
542
543   This function wipes a block device, by clearing and re-reading the
544   partition table. If not successful, it writes back the old partition
545   data, and leaves the cleanup to the user.
546
547   Args:
548     the device name (e.g. sda)
549   """
550
551   if not CheckReread(name):
552     raise OperationalError("CRITICAL: disk %s you selected seems to be in"
553                            " use. ABORTING!" % name)
554
555   fd = os.open("/dev/%s" % name, os.O_RDWR | os.O_SYNC)
556   olddata = os.read(fd, 512)
557   if len(olddata) != 512:
558     raise OperationalError("CRITICAL: Can't read partition table information"
559                            " from /dev/%s (needed 512 bytes, got %d" %
560                            (name, len(olddata)))
561   newdata = "\0" * 512
562   os.lseek(fd, 0, 0)
563   bytes_written = os.write(fd, newdata)
564   os.close(fd)
565   if bytes_written != 512:
566     raise OperationalError("CRITICAL: Can't write partition table information"
567                            " to /dev/%s (tried to write 512 bytes, written"
568                            " %d. I don't know how to cleanup. Sorry." %
569                            (name, bytes_written))
570
571   if not CheckReread(name):
572     fd = os.open("/dev/%s" % name, os.O_RDWR | os.O_SYNC)
573     os.write(fd, olddata)
574     os.close(fd)
575     raise OperationalError("CRITICAL: disk %s which I have just wiped cannot"
576                            " reread partition table. Most likely, it is"
577                            " in use. You have to clean after this yourself."
578                            " I tried to restore the old partition table,"
579                            " but I cannot guarantee nothing has broken." %
580                            name)
581
582
583 def PartitionDisk(name):
584   """Partitions a disk.
585
586   This function creates a single partition spanning the entire disk,
587   by means of fdisk.
588
589   Args:
590     the device name, e.g. sda
591   """
592   result = ExecCommand(
593     'echo ,,8e, | sfdisk /dev/%s' % name)
594   if result.failed:
595     raise OperationalError("CRITICAL: disk %s which I have just partitioned"
596                            " cannot reread its partition table, or there"
597                            " is some other sfdisk error. Likely, it is in"
598                            " use. You have to clean this yourself. Error"
599                            " message from sfdisk: %s" %
600                            (name, result.output))
601
602
603 def CreatePVOnDisk(name):
604   """Creates a physical volume on a block device.
605
606   This function creates a physical volume on a block device, overriding
607   all warnings. So it can wipe existing PVs and PVs which are in a VG.
608
609   Args:
610     the device name, e.g. sda
611
612   """
613   result = ExecCommand("pvcreate -yff /dev/%s1 " % name)
614   if result.failed:
615     raise OperationalError("I cannot create a physical volume on"
616                            " partition /dev/%s1. Error message: %s."
617                            " Please clean up yourself." %
618                            (name, result.output))
619
620
621 def CreateVG(vgname, disks):
622   """Creates the volume group.
623
624   This function creates a volume group named `vgname` on the disks
625   given as parameters. The physical extent size is set to 64MB.
626
627   Args:
628     disks: a list of disk names, e.g. ['sda','sdb']
629
630   """
631   pnames = ["'/dev/%s1'" % disk for disk in disks]
632   result = ExecCommand("vgcreate -s 64MB '%s' %s" % (vgname, " ".join(pnames)))
633   if result.failed:
634     raise OperationalError("I cannot create the volume group %s from"
635                            " disks %s. Error message: %s. Please clean up"
636                            " yourself." %
637                            (vgname, " ".join(disks), result.output))
638
639
640 def ValidateDiskList(options):
641   """Validates or computes the disk list for create.
642
643   This function either computes the available disk list (if the user
644   gave --alldisks option), or validates the user-given disk list (by
645   using the --disks option) such that all given disks are present and
646   not in use.
647
648   Args:
649     the options returned from OptParser.parse_options
650
651   Returns:
652     a list of disk names, e.g. ['sda', 'sdb']
653   """
654
655   sysdisks = GetDiskList(options)
656   if not sysdisks:
657     raise PrereqError("no disks found (I looked for"
658                       " non-removable block devices).")
659   sysd_free = []
660   sysd_used = []
661   for name, size, dev, part, used in sysdisks:
662     if used:
663       sysd_used.append(name)
664     else:
665       sysd_free.append(name)
666
667   if not sysd_free:
668     raise PrereqError("no free disks found! (%d in-use disks)" %
669                       len(sysd_used))
670   if options.alldisks:
671     disklist = sysd_free
672   elif options.disks:
673     disklist = options.disks.split(",")
674     for name in disklist:
675       if name in sysd_used:
676         raise ParameterError("disk %s is in use, cannot wipe!" % name)
677       if name not in sysd_free:
678         raise ParameterError("cannot find disk %s!" % name)
679   else:
680     raise ParameterError("Please use either --alldisks or --disks!")
681
682   return disklist
683
684
685 def BootStrap():
686   """Actual main routine."""
687
688   CheckPrereq()
689
690   options, args = ParseOptions()
691   vgname = options.vgname
692   command = args.pop(0)
693   if command == "diskinfo":
694     ShowDiskInfo(options)
695     return
696   if command != "create":
697     Usage()
698
699   exists, lv_count, vg_size, vg_free = CheckVGExists(vgname)
700   if exists:
701     raise PrereqError("It seems volume group '%s' already exists:\n"
702                       "  LV count: %s, size: %s, free: %s." %
703                       (vgname, lv_count, vg_size, vg_free))
704
705
706   disklist = ValidateDiskList(options)
707
708   for disk in disklist:
709     WipeDisk(disk)
710     PartitionDisk(disk)
711   for disk in disklist:
712     CreatePVOnDisk(disk)
713   CreateVG(vgname, disklist)
714
715   status, lv_count, size, free = CheckVGExists(vgname)
716   if status:
717     print "Done! %s: size %s GiB, disks: %s" % (vgname, size,
718                                               ",".join(disklist))
719   else:
720     raise OperationalError("Although everything seemed ok, the volume"
721                            " group did not get created.")
722
723
724 def main():
725   """application entry point.
726
727   This is just a wrapper over BootStrap, to handle our own exceptions.
728   """
729
730   try:
731     BootStrap()
732   except PrereqError, err:
733     print >> sys.stderr, "The prerequisites for running this tool are not met."
734     print >> sys.stderr, ("Please make sure you followed all the steps in"
735                           " the build document.")
736     print >> sys.stderr, "Description: %s" % str(err)
737     sys.exit(1)
738   except SysconfigError, err:
739     print >> sys.stderr, ("This system's configuration seems wrong, at"
740                           " least is not what I expect.")
741     print >> sys.stderr, ("Please check that the installation didn't fail"
742                           " at some step.")
743     print >> sys.stderr, "Description: %s" % str(err)
744     sys.exit(1)
745   except ParameterError, err:
746     print >> sys.stderr, ("Some parameters you gave to the program or the"
747                           " invocation is wrong. ")
748     print >> sys.stderr, "Description: %s" % str(err)
749     Usage()
750   except OperationalError, err:
751     print >> sys.stderr, ("A serious error has happened while modifying"
752                           " the system's configuration.")
753     print >> sys.stderr, ("Please review the error message below and make"
754                           " sure you clean up yourself.")
755     print >> sys.stderr, ("It is most likely that the system configuration"
756                           " has been partially altered.")
757     print >> sys.stderr, str(err)
758     sys.exit(1)
759   except ProgrammingError, err:
760     print >> sys.stderr, ("Internal application error. Please signal this"
761                           " to xencluster-team.")
762     print >> sys.stderr, "Error description: %s" % str(err)
763     sys.exit(1)
764   except Error, err:
765     print >> sys.stderr, "Unhandled application error: %s" % err
766     sys.exit(1)
767   except (IOError, OSError), err:
768     print >> sys.stderr, "I/O error detected, please report."
769     print >> sys.stderr, "Description: %s" % str(err)
770     sys.exit(1)
771
772
773 if __name__ == "__main__":
774   main()