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