Fix some typos
[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 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   f = open("/proc/mounts", "r")
428   mountlines = f.readlines()
429   f.close()
430   mounts = {}
431   for line in mountlines:
432     device, mountpoint, fstype, rest = line.split(None, 3)
433     # fs type blacklist
434     if fstype in ["nfs", "nfs4", "autofs", "tmpfs", "proc", "sysfs"]:
435       continue
436     try:
437       dev = os.stat(mountpoint).st_dev
438     except OSError, err:
439       # this should be a fairly rare error, since we are blacklisting
440       # network filesystems; with this in mind, we'll ignore it,
441       # since the rereadpt check catches in-use filesystems,
442       # and this is used for disk information only
443       print >> sys.stderr, ("Can't stat mountpoint '%s': %s" %
444                             (mountpoint, err))
445       print >> sys.stderr, "Ignoring."
446       continue
447     mounts[dev] = mountpoint
448   return mounts
449
450
451 def DevInfo(name, dev, mountinfo):
452   """Computes miscellaneous information about a block device.
453
454   Args:
455     name: the device name, e.g. sda
456
457   Returns:
458     (mpath, whatvg, fileinfo), where
459     mpath is the mount path where this device is mounted or None
460     whatvg is the result of the ReadPV function
461     fileinfo is the output of file -bs on the device
462   """
463
464   if dev in mountinfo:
465     mpath = mountinfo[dev]
466   else:
467     mpath = None
468
469   whatvg = ReadPV(name)
470
471   result = ExecCommand("file -bs /dev/%s" % name)
472   if result.failed:
473     fileinfo = "<error: %s>" % result.stderr
474   fileinfo = result.stdout[:45]
475   return mpath, whatvg, fileinfo
476
477
478 def ShowDiskInfo(opts):
479   """Shows a nicely formatted block device list for this system.
480
481   This function shows the user a table with the information gathered
482   by the other functions defined, in order to help the user make a
483   choice about which disks should be allocated to our volume group.
484
485   """
486   mounts = GetMountInfo()
487   dlist = GetDiskList(opts)
488
489   print "------- Disk information -------"
490   print ("%5s %7s %4s %5s %-10s %s" %
491          ("Name", "Size[M]", "Used", "Mount", "LVM?", "Info"))
492
493   flatlist = []
494   # Flatten the [(disk, [partition,...]), ...] list
495   for name, size, dev, parts, inuse in dlist:
496     if inuse:
497       str_inuse = "yes"
498     else:
499       str_inuse = "no"
500     flatlist.append((name, size, dev, str_inuse))
501     for partname, partsize, partdev in parts:
502       flatlist.append((partname, partsize, partdev, ""))
503
504   for name, size, dev, in_use in flatlist:
505     mp, vgname, fileinfo = DevInfo(name, dev, mounts)
506     if mp is None:
507       mp = "-"
508     if vgname is None:
509       lvminfo = "-"
510     elif vgname == "":
511       lvminfo = "yes,free"
512     else:
513       lvminfo = "in %s" % vgname
514
515     if len(name) > 3:
516       # Indent partitions
517       name = " %s" % name
518     print ("%-5s %7.2f %-4s %-5s %-10s %s" %
519            (name, float(size) / 1024 / 1024, in_use, mp, lvminfo, fileinfo))
520
521
522 def CheckReread(name):
523   """Check to see if a block device is in use.
524
525   Uses blockdev to reread the partition table of a block device, and
526   thus compute the in-use status. See the discussion in GetDiskList
527   about the meaning of 'in use'.
528
529   Returns:
530     boolean, the in-use status of the device
531   """
532
533   for retries in range(3):
534     result = ExecCommand("blockdev --rereadpt /dev/%s" % name)
535     if not result.failed:
536       break
537     time.sleep(2)
538
539   return not result.failed
540
541
542 def WipeDisk(name):
543   """Wipes a block device.
544
545   This function wipes a block device, by clearing and re-reading the
546   partition table. If not successful, it writes back the old partition
547   data, and leaves the cleanup to the user.
548
549   Args:
550     the device name (e.g. sda)
551   """
552
553   if not CheckReread(name):
554     raise OperationalError("CRITICAL: disk %s you selected seems to be in"
555                            " use. ABORTING!" % name)
556
557   fd = os.open("/dev/%s" % name, os.O_RDWR | os.O_SYNC)
558   olddata = os.read(fd, 512)
559   if len(olddata) != 512:
560     raise OperationalError("CRITICAL: Can't read partition table information"
561                            " from /dev/%s (needed 512 bytes, got %d" %
562                            (name, len(olddata)))
563   newdata = "\0" * 512
564   os.lseek(fd, 0, 0)
565   bytes_written = os.write(fd, newdata)
566   os.close(fd)
567   if bytes_written != 512:
568     raise OperationalError("CRITICAL: Can't write partition table information"
569                            " to /dev/%s (tried to write 512 bytes, written"
570                            " %d. I don't know how to cleanup. Sorry." %
571                            (name, bytes_written))
572
573   if not CheckReread(name):
574     fd = os.open("/dev/%s" % name, os.O_RDWR | os.O_SYNC)
575     os.write(fd, olddata)
576     os.close(fd)
577     raise OperationalError("CRITICAL: disk %s which I have just wiped cannot"
578                            " reread partition table. Most likely, it is"
579                            " in use. You have to clean after this yourself."
580                            " I tried to restore the old partition table,"
581                            " but I cannot guarantee nothing has broken." %
582                            name)
583
584
585 def PartitionDisk(name):
586   """Partitions a disk.
587
588   This function creates a single partition spanning the entire disk,
589   by means of fdisk.
590
591   Args:
592     the device name, e.g. sda
593   """
594   result = ExecCommand(
595     'echo ,,8e, | sfdisk /dev/%s' % name)
596   if result.failed:
597     raise OperationalError("CRITICAL: disk %s which I have just partitioned"
598                            " cannot reread its partition table, or there"
599                            " is some other sfdisk error. Likely, it is in"
600                            " use. You have to clean this yourself. Error"
601                            " message from sfdisk: %s" %
602                            (name, result.output))
603
604
605 def CreatePVOnDisk(name):
606   """Creates a physical volume on a block device.
607
608   This function creates a physical volume on a block device, overriding
609   all warnings. So it can wipe existing PVs and PVs which are in a VG.
610
611   Args:
612     the device name, e.g. sda
613
614   """
615   result = ExecCommand("pvcreate -yff /dev/%s1 " % name)
616   if result.failed:
617     raise OperationalError("I cannot create a physical volume on"
618                            " partition /dev/%s1. Error message: %s."
619                            " Please clean up yourself." %
620                            (name, result.output))
621
622
623 def CreateVG(vgname, disks):
624   """Creates the volume group.
625
626   This function creates a volume group named `vgname` on the disks
627   given as parameters. The physical extent size is set to 64MB.
628
629   Args:
630     disks: a list of disk names, e.g. ['sda','sdb']
631
632   """
633   pnames = ["'/dev/%s1'" % disk for disk in disks]
634   result = ExecCommand("vgcreate -s 64MB '%s' %s" % (vgname, " ".join(pnames)))
635   if result.failed:
636     raise OperationalError("I cannot create the volume group %s from"
637                            " disks %s. Error message: %s. Please clean up"
638                            " yourself." %
639                            (vgname, " ".join(disks), result.output))
640
641
642 def ValidateDiskList(options):
643   """Validates or computes the disk list for create.
644
645   This function either computes the available disk list (if the user
646   gave --alldisks option), or validates the user-given disk list (by
647   using the --disks option) such that all given disks are present and
648   not in use.
649
650   Args:
651     the options returned from OptParser.parse_options
652
653   Returns:
654     a list of disk names, e.g. ['sda', 'sdb']
655   """
656
657   sysdisks = GetDiskList(options)
658   if not sysdisks:
659     raise PrereqError("no disks found (I looked for"
660                       " non-removable block devices).")
661   sysd_free = []
662   sysd_used = []
663   for name, size, dev, part, used in sysdisks:
664     if used:
665       sysd_used.append(name)
666     else:
667       sysd_free.append(name)
668
669   if not sysd_free:
670     raise PrereqError("no free disks found! (%d in-use disks)" %
671                       len(sysd_used))
672   if options.alldisks:
673     disklist = sysd_free
674   elif options.disks:
675     disklist = options.disks.split(",")
676     for name in disklist:
677       if name in sysd_used:
678         raise ParameterError("disk %s is in use, cannot wipe!" % name)
679       if name not in sysd_free:
680         raise ParameterError("cannot find disk %s!" % name)
681   else:
682     raise ParameterError("Please use either --alldisks or --disks!")
683
684   return disklist
685
686
687 def BootStrap():
688   """Actual main routine."""
689
690   CheckPrereq()
691
692   options, args = ParseOptions()
693   vgname = options.vgname
694   command = args.pop(0)
695   if command == "diskinfo":
696     ShowDiskInfo(options)
697     return
698   if command != "create":
699     Usage()
700
701   exists, lv_count, vg_size, vg_free = CheckVGExists(vgname)
702   if exists:
703     raise PrereqError("It seems volume group '%s' already exists:\n"
704                       "  LV count: %s, size: %s, free: %s." %
705                       (vgname, lv_count, vg_size, vg_free))
706
707
708   disklist = ValidateDiskList(options)
709
710   for disk in disklist:
711     WipeDisk(disk)
712     PartitionDisk(disk)
713   for disk in disklist:
714     CreatePVOnDisk(disk)
715   CreateVG(vgname, disklist)
716
717   status, lv_count, size, free = CheckVGExists(vgname)
718   if status:
719     print "Done! %s: size %s GiB, disks: %s" % (vgname, size,
720                                               ",".join(disklist))
721   else:
722     raise OperationalError("Although everything seemed ok, the volume"
723                            " group did not get created.")
724
725
726 def main():
727   """application entry point.
728
729   This is just a wrapper over BootStrap, to handle our own exceptions.
730   """
731
732   try:
733     BootStrap()
734   except PrereqError, err:
735     print >> sys.stderr, "The prerequisites for running this tool are not met."
736     print >> sys.stderr, ("Please make sure you followed all the steps in"
737                           " the build document.")
738     print >> sys.stderr, "Description: %s" % str(err)
739     sys.exit(1)
740   except SysconfigError, err:
741     print >> sys.stderr, ("This system's configuration seems wrong, at"
742                           " least is not what I expect.")
743     print >> sys.stderr, ("Please check that the installation didn't fail"
744                           " at some step.")
745     print >> sys.stderr, "Description: %s" % str(err)
746     sys.exit(1)
747   except ParameterError, err:
748     print >> sys.stderr, ("Some parameters you gave to the program or the"
749                           " invocation is wrong. ")
750     print >> sys.stderr, "Description: %s" % str(err)
751     Usage()
752   except OperationalError, err:
753     print >> sys.stderr, ("A serious error has happened while modifying"
754                           " the system's configuration.")
755     print >> sys.stderr, ("Please review the error message below and make"
756                           " sure you clean up yourself.")
757     print >> sys.stderr, ("It is most likely that the system configuration"
758                           " has been partially altered.")
759     print >> sys.stderr, str(err)
760     sys.exit(1)
761   except ProgrammingError, err:
762     print >> sys.stderr, ("Internal application error. Please signal this"
763                           " to xencluster-team.")
764     print >> sys.stderr, "Error description: %s" % str(err)
765     sys.exit(1)
766   except Error, err:
767     print >> sys.stderr, "Unhandled application error: %s" % err
768     sys.exit(1)
769   except (IOError, OSError), err:
770     print >> sys.stderr, "I/O error detected, please report."
771     print >> sys.stderr, "Description: %s" % str(err)
772     sys.exit(1)
773
774
775 if __name__ == "__main__":
776   main()