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