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