Coding style: execute black + isort and fix undefined 'path'
[grml2usb.git] / grml2usb
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 # pylint: disable-msg=C0302
4 """
5 grml2usb
6 ~~~~~~~~
7
8 This script installs a Grml system (either a running system or ISO[s]) to a USB device
9
10 :copyright: (c) 2009-2019 by Michael Prokop <mika@grml.org>
11 :license: GPL v2 or any later version
12 :bugreports: http://grml.org/bugs/
13
14 """
15
16
17 import datetime
18 import fileinput
19 import glob
20 import logging
21 import os
22 import os.path
23 import re
24 import shutil
25 import struct
26 import subprocess
27 import sys
28 import tempfile
29 import time
30 import uuid
31 from inspect import isclass, isroutine
32 from optparse import OptionParser
33
34 # The line following this line is patched by debian/rules and tarball.sh.
35 PROG_VERSION = "***UNKNOWN***"
36
37 # when running from inside git, try to report version information via git-describe
38 try:
39     git_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
40     with open(os.devnull, "w") as devnull:
41         PROG_VERSION = (
42             subprocess.check_output(
43                 ["git", "-C", git_dir, "describe", "--always", "--dirty"],
44                 stderr=devnull,
45             )
46             .strip()
47             .decode("utf-8", errors="replace")
48             + " (git)"
49         )
50 except Exception:
51     pass
52
53 # global variables
54 MOUNTED = set()  # register mountpoints
55 TMPFILES = set()  # register tmpfiles
56 DATESTAMP = time.mktime(
57     datetime.datetime.now().timetuple()
58 )  # unique identifier for syslinux.cfg
59 GRML_FLAVOURS = set()  # which flavours are being installed?
60 GRML_DEFAULT = None
61 UUID = None
62 SYSLINUX_LIBS = [
63     "/usr/lib/syslinux/modules/bios/",  # Debian
64     "/usr/lib/syslinux/bios/",  # Arch Linux
65 ]
66 GPT_HEADER = b"\x55\xaa\x45\x46\x49\x20\x50\x41\x52\x54"  # original GPT header
67 GRUB_INSTALL = None
68
69 RE_PARTITION = re.compile(r"([a-z/]*?)(\d+)$")
70 RE_P_PARTITION = re.compile(r"(.*?\d+)p(\d+)$")
71 RE_LOOP_DEVICE = re.compile(r"/dev/loop\d+$")
72
73
74 def syslinux_warning(option, opt, value, opt_parser):
75     """A helper function for printing a warning about deprecated option
76     """
77     # pylint: disable-msg=W0613
78     sys.stderr.write(
79         "Note: the --syslinux option is deprecated as syslinux "
80         "is grml2usb's default. Continuing anyway.\n"
81     )
82     setattr(opt_parser.values, option.dest, True)
83
84
85 # if grub option is set, unset syslinux option
86 def grub_option(option, opt, value, opt_parser):
87     """A helper function adjusting other option values
88     """
89     # pylint: disable-msg=W0613
90     setattr(opt_parser.values, option.dest, True)
91     setattr(opt_parser.values, "syslinux", False)
92
93
94 # cmdline parsing
95 USAGE = "Usage: %prog [options] <[ISO[s] | /run/live/medium]> </dev/sdX#>\n\
96 \n\
97 %prog installs Grml ISO[s] to an USB device to be able to boot from it.\n\
98 Make sure you have at least one Grml ISO or a running Grml system (/run/live/medium),\n\
99 grub or syslinux and root access.\n\
100 \n\
101 Run %prog --help for usage hints, further information via: man grml2usb"
102
103 # pylint: disable-msg=C0103
104 # pylint: disable-msg=W0603
105 parser = OptionParser(usage=USAGE)
106 parser.add_option(
107     "--bootoptions",
108     dest="bootoptions",
109     action="append",
110     type="string",
111     help="use specified bootoptions as default",
112 )
113 parser.add_option(
114     "--bootloader-only",
115     dest="bootloaderonly",
116     action="store_true",
117     help="do not copy files but just install a bootloader",
118 )
119 parser.add_option(
120     "--copy-only",
121     dest="copyonly",
122     action="store_true",
123     help="copy files only but do not install bootloader",
124 )
125 parser.add_option(
126     "--dry-run", dest="dryrun", action="store_true", help="avoid executing commands"
127 )
128 parser.add_option(
129     "--fat16",
130     dest="fat16",
131     action="store_true",
132     help="format specified partition with FAT16",
133 )
134 parser.add_option(
135     "--force",
136     dest="force",
137     action="store_true",
138     help="force any actions requiring manual interaction",
139 )
140 parser.add_option(
141     "--grub",
142     dest="grub",
143     action="callback",
144     callback=grub_option,
145     help="install grub bootloader instead of (default) syslinux",
146 )
147 parser.add_option(
148     "--grub-mbr",
149     dest="grubmbr",
150     action="store_true",
151     help="install grub into MBR instead of (default) PBR",
152 )
153 parser.add_option(
154     "--mbr-menu",
155     dest="mbrmenu",
156     action="store_true",
157     help="enable interactive boot menu in MBR",
158 )
159 parser.add_option(
160     "--quiet",
161     dest="quiet",
162     action="store_true",
163     help="do not output anything but just errors on console",
164 )
165 parser.add_option(
166     "--remove-bootoption",
167     dest="removeoption",
168     action="append",
169     help="regex for removing existing bootoptions",
170 )
171 parser.add_option(
172     "--rw-blockdev",
173     dest="rwblockdev",
174     action="store_true",
175     help="enforce read-write mode on involved block devices",
176 )
177 parser.add_option(
178     "--skip-addons",
179     dest="skipaddons",
180     action="store_true",
181     help="do not install /boot/addons/ files",
182 )
183 parser.add_option(
184     "--skip-bootflag",
185     dest="skipbootflag",
186     action="store_true",
187     help="do not try to check whether the destination has the bootflag set",
188 )
189 parser.add_option(
190     "--skip-grub-config",
191     dest="skipgrubconfig",
192     action="store_true",
193     help="skip generation of grub configuration files",
194 )
195 parser.add_option(
196     "--skip-mbr",
197     dest="skipmbr",
198     action="store_true",
199     help="do not install a master boot record (MBR) on the device",
200 )
201 parser.add_option(
202     "--skip-syslinux-config",
203     dest="skipsyslinuxconfig",
204     action="store_true",
205     help="skip generation of syslinux configuration files",
206 )
207 parser.add_option(
208     "--skip-usb-check",
209     dest="skipusbcheck",
210     action="store_true",
211     help="skip check to verify whether given device is removable",
212 )
213 parser.add_option(
214     "--syslinux",
215     dest="syslinux",
216     action="callback",
217     default=True,
218     callback=syslinux_warning,
219     help="install syslinux bootloader (deprecated as it's the default)",
220 )
221 parser.add_option(
222     "--syslinux-mbr",
223     dest="syslinuxmbr",
224     action="store_true",
225     help="install syslinux master boot record (MBR) instead of default",
226 )
227 parser.add_option(
228     "--syslinux-libs",
229     dest="syslinuxlibs",
230     action="append",
231     default=[],
232     help="syslinux modules directory path",
233 )
234 parser.add_option(
235     "--tmpdir",
236     dest="tmpdir",
237     default="/tmp",
238     help="directory to be used for temporary files",
239 )
240 parser.add_option(
241     "--verbose", dest="verbose", action="store_true", help="enable verbose mode"
242 )
243 parser.add_option(
244     "-v",
245     "--version",
246     dest="version",
247     action="store_true",
248     help="display version and exit",
249 )
250 (options, args) = parser.parse_args()
251
252
253 GRML2USB_BASE = "/usr/share/grml2usb"
254 if not os.path.isdir(GRML2USB_BASE):
255     GRML2USB_BASE = os.path.dirname(os.path.realpath(__file__))
256
257
258 class HodorException(Exception):
259     """Throw exception if the exact error is not known and not fatal.
260
261     @Exception: message"""
262
263
264 class CriticalException(Exception):
265     """Throw critical exception if the exact error is not known but fatal.
266
267     @Exception: message"""
268
269
270 class VerifyException(Exception):
271     """Throw critical exception if there is an fatal error when verifying something.
272
273     @Exception: message"""
274
275
276 # The following two functions help to operate on strings as
277 # array (list) of bytes (octets). In Python 3000, the bytes
278 # datatype will need to be used. This is intended for using
279 # with manipulation of files on the octet level, like shell
280 # arrays, e.g. in MBR creation.
281
282
283 def array2string(*a):
284     """Convert a list of integers [0;255] to a string."""
285     return struct.pack("%sB" % len(a), *a)
286
287
288 def string2array(s):
289     """Convert a (bytes) string into a list of integers."""
290     return struct.unpack("%sB" % len(s), s)
291
292
293 def cleanup():
294     """Cleanup function to make sure there aren't any mounted devices left behind.
295     """
296
297     def del_failed(fn, path, exc):
298         logging.warn("Deletion of %s failed in temporary folder" % path)
299
300     logging.info("Cleaning up before exiting...")
301     proc = subprocess.Popen(["sync"])
302     proc.wait()
303
304     for device in MOUNTED.copy():
305         try:
306             unmount(device, "")
307             logging.debug("Unmounted %s" % device)
308         except Exception:
309             logging.debug("RuntimeError while umount %s, ignoring" % device)
310
311     for tmppath in TMPFILES.copy():
312         try:
313             if os.path.isdir(tmppath) and not os.path.islink(tmppath):
314                 # symbolic links to directories are ignored
315                 # without the check it will throw an OSError
316                 shutil.rmtree(tmppath, onerror=del_failed)
317                 logging.debug("temporary directory %s deleted" % tmppath)
318                 unregister_tmpfile(tmppath)
319             elif os.path.isfile:
320                 os.unlink(tmppath)
321                 logging.debug("temporary file %s deleted" % tmppath)
322                 unregister_tmpfile(tmppath)
323         except Exception:
324             msg = "RuntimeError while removing temporary %s, ignoring"
325             logging.debug(msg % tmppath)
326
327
328 def register_tmpfile(path):
329     """
330     register tmpfile
331     """
332
333     TMPFILES.add(path)
334
335
336 def unregister_tmpfile(path):
337     """
338     remove registered tmpfile
339     """
340
341     try:
342         TMPFILES.remove(path)
343     except KeyError:
344         pass
345
346
347 def register_mountpoint(target):
348     """register specified target in a set() for handling clean exiting
349
350     @target: destination target of mountpoint
351     """
352
353     MOUNTED.add(target)
354
355
356 def unregister_mountpoint(target):
357     """unregister specified target in a set() for handling clean exiting
358
359     @target: destination target of mountpoint
360     """
361
362     if target in MOUNTED:
363         MOUNTED.remove(target)
364
365
366 def get_function_name(obj):
367     """Helper function for use in execute() to retrieve name of a function
368
369     @obj: the function object
370     """
371     if not (isroutine(obj) or isclass(obj)):
372         obj = type(obj)
373     return obj.__module__ + "." + obj.__name__
374
375
376 def set_rw(device):
377     if not options.rwblockdev:
378         return
379
380     logging.debug("executing: blockdev --setrw %s", device)
381     proc = subprocess.Popen(["blockdev", "--setrw", device])
382     proc.wait()
383     if proc.returncode != 0:
384         raise Exception("error executing blockdev on %s" % device)
385
386
387 def execute(f, *exec_arguments):
388     """Wrapper for executing a command. Either really executes
389     the command (default) or when using --dry-run commandline option
390     just displays what would be executed."""
391     # usage: execute(subprocess.Popen, (["ls", "-la"]))
392     if options.dryrun:
393         # pylint: disable-msg=W0141
394         logging.debug(
395             "dry-run only: %s(%s)",
396             get_function_name(f),
397             ", ".join(map(repr, exec_arguments)),
398         )
399     else:
400         # pylint: disable-msg=W0142
401         return f(*exec_arguments)
402
403
404 def is_exe(fpath):
405     """Check whether a given file can be executed
406
407     @fpath: full path to file
408     @return:"""
409     return os.path.exists(fpath) and os.access(fpath, os.X_OK)
410
411
412 def which(program):
413     """Check whether a given program is available in PATH
414
415     @program: name of executable"""
416     fpath = os.path.split(program)[0]
417     if fpath:
418         if is_exe(program):
419             return program
420     else:
421         for path in os.environ["PATH"].split(os.pathsep):
422             exe_file = os.path.join(path, program)
423             if is_exe(exe_file):
424                 return exe_file
425
426     return None
427
428
429 def get_defaults_file(iso_mount, flavour, name):
430     """get the default file for syslinux
431     """
432     bootloader_dirs = ["/boot/isolinux/", "/boot/syslinux/"]
433     for directory in bootloader_dirs:
434         for name in name, "%s_%s" % (get_flavour_filename(flavour), name):
435             if os.path.isfile(iso_mount + directory + name):
436                 return (directory, name)
437     return ("", "")
438
439
440 def search_file(
441     filename,
442     search_path="/bin" + os.pathsep + "/usr/bin",
443     lst_return=False,
444     required=False,
445 ):
446     """Given a search path, find file
447
448     @filename: name of file to search for
449     @search_path: path where searching for the specified filename
450     @lst_return: return list of matching files instead one file"""
451     paths = search_path.split(os.pathsep)
452     current_dir = ""  # make pylint happy :)
453     retval = []
454
455     def match_file(cwd):
456         """Helper function ffor testing if specified file exists in cwd
457
458         @cwd: current working directory
459         """
460         return os.path.exists(os.path.join(cwd, filename))
461
462     for path in paths:
463         current_dir = path
464         if match_file(current_dir):
465             retval.append(os.path.abspath(os.path.join(current_dir, filename)))
466             if not lst_return:
467                 break
468         # pylint: disable-msg=W0612
469         for current_dir, directories, files in os.walk(path):
470             if match_file(current_dir):
471                 retval.append(os.path.abspath(os.path.join(current_dir, filename)))
472                 if not lst_return:
473                     break
474
475     if required and not retval:
476         raise CriticalException(
477             "Required file %s not found in %s" % (filename, search_path)
478         )
479
480     if lst_return:
481         return retval
482     elif retval:
483         return retval[0]
484     else:
485         return None
486
487
488 def check_uid_root():
489     """Check for root permissions"""
490     if not os.geteuid() == 0:
491         raise CriticalException("please run this script with uid 0 (root).")
492
493
494 # for usage inside check_boot_flag
495 def get_partition_for_path(path):
496     import parted
497
498     boot_dev, x = get_device_from_partition(path)
499
500     d = parted.getDevice(boot_dev)
501     disk = parted.Disk(d)
502     return disk.getPartitionByPath(path)
503
504
505 def check_boot_flag(device):
506     boot_dev, x = get_device_from_partition(device)
507
508     try:
509         import parted
510
511         part = get_partition_for_path(device)
512         if part is None:
513             raise HodorException("parted could not find partition")
514         if part.getFlag(parted.PARTITION_BOOT):
515             logging.debug("bootflag is enabled on %s" % device)
516             return
517     except HodorException as e:
518         logging.info("%s, falling back to old bootflag detection", e)
519     except ImportError:
520         logging.debug("could not import parted, falling back to old bootflag detection")
521
522     with open(boot_dev, "rb") as image:
523         data = image.read(520)
524         bootcode = data[440:]
525         gpt_data = bootcode[70:80]
526
527         if gpt_data == GPT_HEADER:
528             logging.info("GPT detected, skipping bootflag check")
529         elif bootcode[6] == b"\x80":
530             logging.debug("bootflag is enabled")
531         else:
532             logging.debug("bootflag is NOT enabled")
533             raise VerifyException(
534                 "Device %s does not have the bootflag set. "
535                 "Please enable it to be able to boot." % device
536             )
537
538
539 def mkfs_fat16(device):
540     """Format specified device with VFAT/FAT16 filesystem.
541
542     @device: partition that should be formated"""
543
544     if options.dryrun:
545         logging.info("Would execute mkfs.vfat -F 16 %s now.", device)
546         return 0
547
548     logging.info("Formating partition with fat16 filesystem")
549     logging.debug("mkfs.vfat -F 16 %s", device)
550     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
551     proc.wait()
552     if proc.returncode != 0:
553         raise CriticalException("error executing mkfs.vfat")
554
555
556 def generate_isolinux_splash(grml_flavour):
557     """Generate bootsplash for isolinux/syslinux
558
559     @grml_flavour: name of grml flavour the configuration should be generated for"""
560
561     grml_name = grml_flavour
562
563     return """\
564 \ f17\f\18/boot/syslinux/logo.16
565
566 Some information and boot options available via keys F2 - F10. http://grml.org/
567 %(grml_name)s
568 """ % {
569         "grml_name": grml_name
570     }
571
572
573 def generate_main_syslinux_config(*arg):
574     """Generate main configuration for use in syslinux.cfg
575
576     @*arg: just for backward compatibility"""
577     # pylint: disable-msg=W0613
578     # remove warning about unused arg
579
580     return """\
581 label -
582 menu label Default boot modes:
583 menu disable
584 include defaults.cfg
585
586 menu end
587 menu separator
588
589 # flavours:
590 label -
591 menu label Additional boot entries for:
592 menu disable
593 include additional.cfg
594
595 menu separator
596 include options.cfg
597 include addons.cfg
598
599 label help
600   include promptname.cfg
601   config prompt.cfg
602   text help
603                                         Jump to old style isolinux prompt
604                                         featuring further information
605                                         regarding available boot options.
606   endtext
607
608
609 include hiddens.cfg
610 """
611
612
613 def generate_flavour_specific_syslinux_config(grml_flavour):
614     """Generate flavour specific configuration for use in syslinux.cfg
615
616     @grml_flavour: name of grml flavour the configuration should be generated for"""
617
618     return """\
619 menu begin grml %(grml_flavour)s
620     menu title %(display_name)s
621     label mainmenu
622     menu label ^Back to main menu...
623     menu exit
624     menu separator
625     # include config for boot parameters from disk
626     include %(grml_flavour)s_grml.cfg
627     menu hide
628 menu end
629 """ % {
630         "grml_flavour": grml_flavour,
631         "display_name": get_flavour_filename(grml_flavour),
632     }
633
634
635 def install_grub(device):
636     """Install grub on specified device.
637
638     @mntpoint: mountpoint of device where grub should install its files to
639     @device: partition where grub should be installed to"""
640
641     if options.dryrun:
642         logging.info(
643             "Would execute grub-install [--root-directory=mount_point] %s now.", device
644         )
645     else:
646         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
647         register_tmpfile(device_mountpoint)
648         try:
649             # If using --grub-mbr then make sure we install grub in MBR instead of PBR
650             if options.grubmbr:
651                 logging.debug("Using option --grub-mbr ...")
652                 grub_device, x = get_device_from_partition(device)
653             else:
654                 grub_device = device
655
656             set_rw(device)
657             mount(device, device_mountpoint, "")
658
659             logging.info("Installing grub as bootloader")
660             for opt in ["--", "--force"]:
661                 set_rw(device)
662                 set_rw(grub_device)
663                 logging.debug(
664                     "%s --recheck --no-floppy --target=i386-pc --root-directory=%s %s %s",
665                     GRUB_INSTALL,
666                     device_mountpoint,
667                     opt,
668                     grub_device,
669                 )
670                 proc = subprocess.Popen(
671                     [
672                         GRUB_INSTALL,
673                         "--recheck",
674                         "--no-floppy",
675                         "--target=i386-pc",
676                         "--root-directory=%s" % device_mountpoint,
677                         opt,
678                         grub_device,
679                     ],
680                     stdout=open(os.devnull, "r+"),
681                 )
682                 proc.wait()
683                 if proc.returncode == 0:
684                     break
685
686             if proc.returncode != 0:
687                 # raise Exception("error executing grub-install")
688                 logging.critical(
689                     "Fatal: error executing grub-install "
690                     "(please check the grml2usb FAQ or drop the --grub option)"
691                 )
692                 logging.critical(
693                     "Note:  if using grub2 consider using "
694                     "the --grub-mbr option as grub considers PBR problematic."
695                 )
696                 cleanup()
697                 sys.exit(1)
698         except CriticalException as error:
699             logging.critical("Fatal: %s", error)
700             cleanup()
701             sys.exit(1)
702         finally:
703             unmount(device_mountpoint, "")
704             os.rmdir(device_mountpoint)
705             unregister_tmpfile(device_mountpoint)
706
707
708 def install_syslinux(device):
709     """Install syslinux on specified device.
710
711     @device: partition where syslinux should be installed to"""
712
713     if options.dryrun:
714         logging.info("Would install syslinux as bootloader on %s", device)
715         return 0
716
717     set_rw(device)
718
719     # syslinux -d boot/isolinux /dev/sdb1
720     logging.info("Installing syslinux as bootloader")
721     logging.debug("syslinux -d boot/syslinux %s", device)
722     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
723     proc.wait()
724     if proc.returncode != 0:
725         raise CriticalException(
726             "Error executing syslinux (either try --fat16 or use grub?)"
727         )
728
729
730 def install_bootloader(device):
731     """Install bootloader on specified device.
732
733     @device: partition where bootloader should be installed to"""
734
735     # by default we use grub, so install syslinux only on request
736     if options.grub:
737         try:
738             install_grub(device)
739         except CriticalException as error:
740             logging.critical("Fatal: %s", error)
741             cleanup()
742             sys.exit(1)
743     else:
744         try:
745             install_syslinux(device)
746         except CriticalException as error:
747             logging.critical("Fatal: %s", error)
748             cleanup()
749             sys.exit(1)
750
751
752 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
753     """install 'mbr' master boot record (MBR) on a device
754
755     Retrieve the partition table from "device", install an MBR from the
756     "mbrtemplate" file, set the "partition" (0..3) active, and install the
757     result back to "device".
758
759     @mbrtemplate: default MBR file
760
761     @device: name of a file assumed to be a hard disc (or USB stick) image, or
762     something like "/dev/sdb"
763
764     @partition: must be a number between 0 and 3, inclusive
765
766     @mbrtemplate: must be a valid MBR file of at least 440 (or 439 if
767     ismirbsdmbr) bytes.
768
769     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
770     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
771     false then leave the mirbsdmbr specific flag set to FFh, set all
772     active flags to 0 and set the active flag of the partition to 80h.  Note:
773     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
774     the active flag."""
775
776     logging.info("Installing default MBR")
777
778     if not os.path.isfile(mbrtemplate):
779         logging.error(
780             "Error installing MBR (either try --syslinux-mbr or "
781             'install missing file "%s"?)',
782             mbrtemplate,
783         )
784         raise CriticalException("%s can not be read." % mbrtemplate)
785
786     if partition is not None and ((partition < 0) or (partition > 3)):
787         logging.warn("Cannot activate partition %d", partition)
788         partition = None
789
790     if ismirbsdmbr:
791         nmbrbytes = 439
792     else:
793         nmbrbytes = 440
794
795     tmpf = tempfile.NamedTemporaryFile()
796
797     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
798     proc = subprocess.Popen(
799         ["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
800         stderr=open(os.devnull, "r+"),
801     )
802     proc.wait()
803     if proc.returncode != 0:
804         raise Exception("error executing dd (first run)")
805
806     logging.debug(
807         "executing: dd if=%s of=%s bs=%s count=1 conv=notrunc",
808         mbrtemplate,
809         tmpf.name,
810         nmbrbytes,
811     )
812     proc = subprocess.Popen(
813         [
814             "dd",
815             "if=%s" % mbrtemplate,
816             "of=%s" % tmpf.name,
817             "bs=%s" % nmbrbytes,
818             "count=1",
819             "conv=notrunc",
820         ],
821         stderr=open(os.devnull, "r+"),
822     )
823     proc.wait()
824     if proc.returncode != 0:
825         raise Exception("error executing dd (second run)")
826
827     mbrcode = tmpf.file.read(512)
828     if len(mbrcode) < 512:
829         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
830
831     if partition is not None:
832         if ismirbsdmbr:
833             mbrcode = (
834                 mbrcode[0:439]
835                 + chr(partition).encode("latin-1")
836                 + mbrcode[440:510]
837                 + b"\x55\xAA"
838             )
839         else:
840             actives = [b"\x00", b"\x00", b"\x00", b"\x00"]
841             actives[partition] = b"\x80"
842             mbrcode = (
843                 mbrcode[0:446]
844                 + actives[0]
845                 + mbrcode[447:462]
846                 + actives[1]
847                 + mbrcode[463:478]
848                 + actives[2]
849                 + mbrcode[479:494]
850                 + actives[3]
851                 + mbrcode[495:510]
852                 + b"\x55\xAA"
853             )
854
855     tmpf.file.seek(0)
856     tmpf.file.truncate()
857     tmpf.file.write(mbrcode)
858     tmpf.file.close()
859
860     set_rw(device)
861
862     logging.debug(
863         "executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device
864     )
865     proc = subprocess.Popen(
866         [
867             "dd",
868             "if=%s" % tmpf.name,
869             "of=%s" % device,
870             "bs=512",
871             "count=1",
872             "conv=notrunc",
873         ],
874         stderr=open(os.devnull, "r+"),
875     )
876     proc.wait()
877     if proc.returncode != 0:
878         raise Exception("error executing dd (third run)")
879     del tmpf
880
881     # make sure we sync filesystems before returning
882     proc = subprocess.Popen(["sync"])
883     proc.wait()
884
885     set_rw(device)
886
887
888 def is_writeable(device):
889     """Check if the device is writeable for the current user
890
891     @device: partition where bootloader should be installed to"""
892
893     if not device:
894         return False
895
896     if not os.path.exists(device):
897         return False
898
899     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
900
901
902 def mount(source, target, mount_options):
903     """Mount specified source on given target
904
905     @source: name of device/ISO that should be mounted
906     @target: directory where the ISO should be mounted to
907     @options: mount specific options"""
908
909     # note: options.dryrun does not work here, as we have to
910     # locate files and identify the grml flavour
911
912     for x in open("/proc/mounts", "r").readlines():
913         if x.startswith(source):
914             raise CriticalException(
915                 (
916                     "Error executing mount: %s already mounted - "
917                     "please unmount before invoking grml2usb"
918                 ).format(source)
919             )
920
921     if os.path.isdir(source):
922         logging.debug("Source %s is not a device, therefore not mounting.", source)
923         return 0
924
925     logging.debug("mount %s %s %s", mount_options, source, target)
926     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
927     proc.wait()
928     if proc.returncode != 0:
929         raise CriticalException(
930             "Error executing mount (no filesystem on the partition?)"
931         )
932     else:
933         logging.debug("register_mountpoint(%s)", target)
934         register_mountpoint(target)
935
936
937 def unmount(target, unmount_options):
938     """Unmount specified target
939
940     @target: target where something is mounted on and which should be unmounted
941     @options: options for umount command"""
942
943     # make sure we unmount only already mounted targets
944     target_unmount = False
945     mounts = open("/proc/mounts").readlines()
946     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
947     for line in mounts:
948         if re.match(mountstring, line):
949             target_unmount = True
950
951     if not target_unmount:
952         logging.debug("%s not mounted anymore", target)
953     else:
954         logging.debug("umount %s %s", list(unmount_options), target)
955         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
956         proc.wait()
957         if proc.returncode != 0:
958             raise Exception("Error executing umount")
959         else:
960             logging.debug("unregister_mountpoint(%s)", target)
961             unregister_mountpoint(target)
962
963
964 def check_for_usbdevice(device):
965     """Check whether the specified device is a removable USB device
966
967     @device: device name, like /dev/sda1 or /dev/sda
968     """
969
970     usbdevice = re.match(r"/dev/(.*?)\d*$", device).group(1)
971     # newer systems:
972     usbdev = os.path.realpath("/sys/class/block/" + usbdevice + "/removable")
973     if not os.path.isfile(usbdev):
974         # Ubuntu with kernel 2.6.24 for example:
975         usbdev = os.path.realpath("/sys/block/" + usbdevice + "/removable")
976
977     if os.path.isfile(usbdev):
978         is_usb = open(usbdev).readline()
979         if is_usb.find("1"):
980             return 0
981
982     return 1
983
984
985 def check_for_fat(partition):
986     """Check whether specified partition is a valid VFAT/FAT16 filesystem
987
988     @partition: device name of partition"""
989
990     if not os.access(partition, os.R_OK):
991         raise CriticalException(
992             "Failed to read device %s"
993             " (wrong UID/permissions or device/directory not present?)" % partition
994         )
995
996     try:
997         filesystem = (
998             subprocess.check_output(
999                 ["/sbin/blkid", "-s", "TYPE", "-o", "value", partition]
1000             )
1001             .decode()
1002             .rstrip()
1003         )
1004
1005         if filesystem != "vfat":
1006             raise CriticalException(
1007                 "Partition %s does not contain a FAT16 filesystem. "
1008                 "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition)
1009             )
1010
1011     except OSError:
1012         raise CriticalException(
1013             "Sorry, /sbin/blkid not available (install util-linux?)"
1014         )
1015
1016
1017 def mkdir(directory):
1018     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
1019
1020     # just silently pass as it's just fine it the directory exists
1021     if not os.path.isdir(directory):
1022         try:
1023             os.makedirs(directory)
1024         # pylint: disable-msg=W0704
1025         except OSError:
1026             pass
1027
1028
1029 def exec_rsync(source, target):
1030     """Simple wrapper around rsync to install files
1031
1032     @source: source file/directory
1033     @target: target file/directory"""
1034     logging.debug("Source: %s / Target: %s", source, target)
1035     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
1036     proc.wait()
1037     if proc.returncode == 12:
1038         logging.critical("Fatal: No space left on device")
1039         cleanup()
1040         sys.exit(1)
1041
1042     if proc.returncode != 0:
1043         logging.critical("Fatal: could not install %s", source)
1044         cleanup()
1045         sys.exit(1)
1046
1047
1048 def write_uuid(target_file):
1049     """Generates an returns uuid and write it to the specified file
1050
1051     @target_file: filename to write the uuid to
1052     """
1053
1054     fileh = open(target_file, "w")
1055     uid = str(uuid.uuid4())
1056     fileh.write(uid)
1057     fileh.close()
1058     return uid
1059
1060
1061 def get_uuid(target):
1062     """Get the uuid of the specified target. Will generate an uuid if none exist.
1063
1064     @target: directory/mountpoint containing the grml layout
1065     """
1066
1067     conf_target = target + "/conf/"
1068     uuid_file_name = conf_target + "/bootid.txt"
1069     if os.path.isdir(conf_target):
1070         if os.path.isfile(uuid_file_name):
1071             uuid_file = open(uuid_file_name, "r")
1072             uid = uuid_file.readline().strip()
1073             uuid_file.close()
1074             return uid
1075         else:
1076             return write_uuid(uuid_file_name)
1077     else:
1078         execute(mkdir, conf_target)
1079         return write_uuid(uuid_file_name)
1080
1081
1082 def get_shortname(grml_flavour):
1083     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
1084     @grml_flavour: flavour name which shold be translated to shortname"""
1085
1086     return re.sub(r"[,._-]", "", grml_flavour)
1087
1088
1089 def copy_system_files(grml_flavour, iso_mount, target):
1090     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1091
1092     @grml_flavour: name of grml flavour the configuration should be generated for
1093     @iso_mount: path where a grml ISO is mounted on
1094     @target: path where grml's main files should be copied to"""
1095
1096     squashfs = search_file(grml_flavour + ".squashfs", iso_mount)
1097     if squashfs is None:
1098         logging.error("error locating squashfs file")
1099         raise CriticalException(
1100             "squashfs file not found, please check that your iso is not corrupt"
1101         )
1102     else:
1103         squashfs_target = target + "/live/" + grml_flavour + "/"
1104         execute(mkdir, squashfs_target)
1105     exec_rsync(squashfs, squashfs_target + grml_flavour + ".squashfs")
1106
1107     for prefix in grml_flavour + "/", "":
1108         filesystem_module = search_file(prefix + "filesystem.module", iso_mount)
1109         if filesystem_module:
1110             break
1111     if filesystem_module is None:
1112         logging.error("error locating filesystem.module file")
1113         raise CriticalException("filesystem.module not found")
1114     else:
1115         exec_rsync(filesystem_module, squashfs_target + "filesystem.module")
1116
1117     shortname = get_shortname(grml_flavour)
1118     if os.path.isdir(iso_mount + "/boot/" + shortname):
1119         exec_rsync(iso_mount + "/boot/" + shortname, target + "/boot")
1120     else:
1121         kernel = search_file("vmlinuz", iso_mount)
1122         if kernel is None:
1123             # compat for releases < 2011.12
1124             kernel = search_file("linux26", iso_mount)
1125
1126         if kernel is None:
1127             logging.error("error locating kernel file")
1128             raise CriticalException("Kernel not found")
1129
1130         source = os.path.dirname(kernel) + "/"
1131         dest = target + "/" + os.path.dirname(kernel).replace(iso_mount, "") + "/"
1132         execute(mkdir, dest)
1133         exec_rsync(source, dest)
1134
1135
1136 def update_grml_versions(iso_mount, target):
1137     """Update the grml version file on a cd
1138     Returns true if version was updated successfully,
1139     False if grml-version does not exist yet on the mountpoint
1140
1141     @iso_mount: string of the iso mount point
1142     @target: path of the target mount point
1143     """
1144     grml_target = target + "/grml/"
1145     target_grml_version_file = search_file("grml-version", grml_target)
1146     if target_grml_version_file:
1147         iso_grml_version_file = search_file("grml-version", iso_mount)
1148         if not iso_grml_version_file:
1149             logging.warn(
1150                 "Warning: %s could not be found - can not install it",
1151                 iso_grml_version_file,
1152             )
1153             return False
1154         try:
1155             # read the flavours from the iso image
1156             iso_versions = {}
1157             iso_file = open(iso_grml_version_file, "r")
1158             for line in iso_file:
1159                 iso_versions[get_flavour(line)] = line.strip()
1160
1161             # update the existing flavours on the target
1162             for line in fileinput.input([target_grml_version_file], inplace=1):
1163                 flavour = get_flavour(line)
1164                 if flavour in list(iso_versions.keys()):
1165                     print(iso_versions.pop(flavour))
1166                 else:
1167                     print(line.strip())
1168             fileinput.close()
1169
1170             target_file = open(target_grml_version_file, "a")
1171             # add the new flavours from the current iso
1172             for flavour in iso_versions:
1173                 target_file.write("%s\n" % iso_versions[flavour])
1174         except IOError:
1175             logging.warn("Warning: Could not write file")
1176         finally:
1177             iso_file.close()
1178             target_file.close()
1179         return True
1180     else:
1181         return False
1182
1183
1184 def copy_grml_files(grml_flavour, iso_mount, target):
1185     """copy some minor grml files to a given target
1186
1187     @grml_flavour: the current grml_flavour
1188     @iso_mount: path where a grml ISO is mounted on
1189     @target: path where grml's main files should be copied to"""
1190
1191     grml_target = target + "/grml/"
1192     execute(mkdir, grml_target)
1193
1194     grml_prefixe = ["GRML", "grml"]
1195     for prefix in grml_prefixe:
1196         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
1197         if os.path.exists(filename):
1198             exec_rsync(filename, grml_target)
1199             break
1200     else:
1201         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
1202
1203
1204 def copy_addons(iso_mount, target):
1205     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1206
1207     @iso_mount: path where a grml ISO is mounted on
1208     @target: path where grml's main files should be copied to"""
1209
1210     addons = target + "/boot/addons/"
1211     execute(mkdir, addons)
1212
1213     for addon_file in glob.glob(iso_mount + "/boot/addons/*"):
1214         filename = os.path.basename(addon_file)
1215         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1216         logging.debug("Copying addon file %s" % filename)
1217         exec_rsync(src_file, addons)
1218
1219
1220 def build_loopbackcfg(target):
1221     """Generate GRUB's loopback.cfg based on existing config files.
1222
1223     @target: target directory
1224     """
1225
1226     grub_dir = "/boot/grub/"
1227     mkdir(os.path.join(target, grub_dir))
1228
1229     f = open(target + grub_dir + "loopback.cfg", "w")
1230
1231     f.write("# grml2usb generated grub2 configuration file\n")
1232     f.write("source /boot/grub/header.cfg\n")
1233
1234     for defaults in glob.glob(
1235         target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"
1236     ):
1237         sourcefile = defaults.split(target + os.path.sep)[1]
1238         logging.debug("Found source file" + sourcefile)
1239         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1240
1241     for ops in glob.glob(
1242         target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"
1243     ):
1244         sourcefile = ops.split(target + os.path.sep)[1]
1245         logging.debug("Found source file" + sourcefile)
1246         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1247
1248     f.write("source /boot/grub/addons.cfg\n")
1249     f.write("source /boot/grub/footer.cfg\n")
1250     f.close()
1251
1252
1253 def glob_and_copy(filepattern, dst):
1254     """Glob on specified filepattern and copy the result to dst
1255
1256     @filepattern: globbing pattern
1257     @dst: target directory
1258     """
1259     for name in glob.glob(filepattern):
1260         copy_if_exist(name, dst)
1261
1262
1263 def search_and_copy(filename, search_path, dst):
1264     """Search for the specified filename at searchpath and copy it to dst
1265
1266     @filename: filename to look for
1267     @search_path: base search file
1268     @dst: destionation to copy the file to
1269     """
1270     file_location = search_file(filename, search_path)
1271     copy_if_exist(file_location, dst)
1272
1273
1274 def copy_if_exist(filename, dst):
1275     """Copy filename to dst if filename is set.
1276
1277     @filename: a filename
1278     @dst: dst file
1279     """
1280     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1281         exec_rsync(filename, dst)
1282
1283
1284 def copy_bootloader_files(iso_mount, target, grml_flavour):
1285     """Copy grml's bootloader files to a given target
1286
1287     @iso_mount: path where a grml ISO is mounted on
1288     @target: path where grml's main files should be copied to
1289     @grml_flavour: name of the current processed grml_flavour
1290     """
1291
1292     syslinux_target = target + "/boot/syslinux/"
1293     execute(mkdir, syslinux_target)
1294
1295     grub_target = target + "/boot/grub/"
1296     execute(mkdir, grub_target)
1297
1298     logo = search_file("logo.16", iso_mount, required=True)
1299     exec_rsync(logo, syslinux_target + "logo.16")
1300
1301     bootx64_efi = search_file("bootx64.efi", iso_mount)
1302     if bootx64_efi:
1303         mkdir(target + "/efi/boot/")
1304         exec_rsync(bootx64_efi, target + "/efi/boot/bootx64.efi")
1305
1306     efi_img = search_file("efi.img", iso_mount)
1307     if efi_img:
1308         mkdir(target + "/boot/")
1309         exec_rsync(efi_img, target + "/boot/efi.img")
1310         handle_secure_boot(target, efi_img)
1311
1312     for ffile in ["f%d" % number for number in range(1, 11)]:
1313         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1314
1315     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1316     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1317     if os.path.isfile(syslinux_target + "ldlinux.sys"):
1318         os.unlink(syslinux_target + "ldlinux.sys")
1319
1320     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1321     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1322
1323     if not source_dir:
1324         raise CriticalException(
1325             "file default.cfg could not be found.\n"
1326             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1327             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version."
1328         )
1329
1330     if not os.path.exists(iso_mount + "/boot/grub/footer.cfg"):
1331         logging.warning(
1332             "Warning: Grml releases older than 2011.12 support only one flavour in grub."
1333         )
1334
1335     for expr in (
1336         name,
1337         "distri.cfg",
1338         defaults_file,
1339         "grml.png",
1340         "hd.cfg",
1341         "isolinux.cfg",
1342         "isolinux.bin",
1343         "isoprompt.cfg",
1344         "options.cfg",
1345         "prompt.cfg",
1346         "vesamenu.cfg",
1347         "grml.png",
1348         "*.c32",
1349     ):
1350         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1351
1352     for modules_dir in options.syslinuxlibs + SYSLINUX_LIBS:
1353         if os.path.isdir(modules_dir):
1354             for filename in glob.glob1(syslinux_target, "*.c32"):
1355                 copy_if_exist(os.path.join(modules_dir, filename), syslinux_target)
1356             break
1357
1358     # copy the addons_*.cfg file to the new syslinux directory
1359     glob_and_copy(iso_mount + source_dir + "addon*.cfg", syslinux_target)
1360
1361     search_and_copy(
1362         "hidden.cfg", iso_mount + source_dir, syslinux_target + "new_" + "hidden.cfg"
1363     )
1364
1365     # copy all grub files from ISO
1366     glob_and_copy(iso_mount + "/boot/grub/*", grub_target)
1367
1368     # finally (after all GRUB files have been installed) build static loopback.cfg
1369     build_loopbackcfg(target)
1370
1371
1372 def install_iso_files(grml_flavour, iso_mount, device, target):
1373     """Copy files from ISO to given target
1374
1375     @grml_flavour: name of grml flavour the configuration should be generated for
1376     @iso_mount: path where a grml ISO is mounted on
1377     @device: device/partition where bootloader should be installed to
1378     @target: path where grml's main files should be copied to"""
1379
1380     global GRML_DEFAULT
1381     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1382     if options.dryrun:
1383         return 0
1384     elif not options.bootloaderonly:
1385         logging.info("Copying files. This might take a while....")
1386         try:
1387             copy_system_files(grml_flavour, iso_mount, target)
1388             copy_grml_files(grml_flavour, iso_mount, target)
1389         except CriticalException as error:
1390             logging.critical("Execution failed: %s", error)
1391             sys.exit(1)
1392
1393     if not options.skipaddons:
1394         if not search_file("addons", iso_mount):
1395             logging.info("Could not find addons, therefore not installing.")
1396         else:
1397             copy_addons(iso_mount, target)
1398
1399     if not options.copyonly:
1400         copy_bootloader_files(iso_mount, target, grml_flavour)
1401
1402         if not options.dryrun:
1403             handle_bootloader_config(grml_flavour, device, target)
1404
1405     # make sure we sync filesystems before returning
1406     proc = subprocess.Popen(["sync"])
1407     proc.wait()
1408
1409
1410 def get_device_from_partition(partition):
1411     device = partition
1412     partition_number = None
1413     if partition[-1].isdigit() and not RE_LOOP_DEVICE.match(partition):
1414         m = RE_P_PARTITION.match(partition) or RE_PARTITION.match(partition)
1415         if m:
1416             device = m.group(1)
1417             partition_number = int(m.group(2)) - 1
1418     return (device, partition_number)
1419
1420
1421 def get_flavour(flavour_str):
1422     """Returns the flavour of a grml version string
1423     """
1424     return re.match(r"[\w-]*", flavour_str).group()
1425
1426
1427 def identify_grml_flavour(mountpath):
1428     """Get name of grml flavour
1429
1430     @mountpath: path where the grml ISO is mounted to
1431     @return: name of grml-flavour"""
1432
1433     version_files = search_file("grml-version", mountpath, lst_return=True)
1434
1435     if not version_files:
1436         if mountpath.startswith("/run/live/medium"):
1437             logging.critical("Error: could not find grml-version file.")
1438             logging.critical(
1439                 "Looks like your system is running from RAM but required files are not available."
1440             )
1441             logging.critical(
1442                 "Please either boot without toram=... or use boot option toram instead of toram=..."
1443             )
1444         else:
1445             logging.critical("Error: could not find grml-version file.")
1446         cleanup()
1447         sys.exit(1)
1448
1449     flavours = []
1450     logging.debug("version_files = %s", version_files)
1451     for version_file in version_files:
1452         tmpfile = None
1453         try:
1454             tmpfile = open(version_file, "r")
1455             for line in tmpfile.readlines():
1456                 flavours.append(get_flavour(line))
1457         finally:
1458             if tmpfile:
1459                 tmpfile.close()
1460
1461     return flavours
1462
1463
1464 def get_bootoptions(grml_flavour):
1465     """Returns bootoptions for specific flavour
1466
1467     @grml_flavour: name of the grml_flavour
1468     """
1469     # do NOT write "None" in kernel cmdline
1470     if not options.bootoptions:
1471         bootopt = ""
1472     else:
1473         bootopt = " ".join(options.bootoptions)
1474     bootopt = bootopt.replace("%flavour", grml_flavour)
1475     return bootopt
1476
1477
1478 def handle_grub_config(grml_flavour, device, target):
1479     """Main handler for generating grub (v1 and v2) configuration
1480
1481     @grml_flavour: name of grml flavour the configuration should be generated for
1482     @device: device/partition where grub should be installed to
1483     @target: path of grub's configuration files"""
1484
1485     global UUID
1486
1487     logging.debug("Updating grub configuration")
1488
1489     grub_target = target + "/boot/grub/"
1490     secureboot_target = target + "/EFI/ubuntu/"
1491
1492     bootid_re = re.compile(r"bootid=[\w_-]+")
1493     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1494
1495     bootopt = get_bootoptions(grml_flavour)
1496
1497     remove_regexes = []
1498     option_re = re.compile(r"(.*/boot/.*(linux26|vmlinuz).*)")
1499
1500     if options.removeoption:
1501         for regex in options.removeoption:
1502             remove_regexes.append(re.compile(regex))
1503
1504     shortname = get_shortname(grml_flavour)
1505     for filename in glob.glob(grub_target + "*.cfg") + glob.glob(
1506         secureboot_target + "*.cfg"
1507     ):
1508         for line in fileinput.input(filename, inplace=1):
1509             line = line.rstrip("\r\n")
1510             if option_re.search(line):
1511                 line = bootid_re.sub("", line)
1512                 if shortname in filename:
1513                     line = live_media_path_re.sub("", line)
1514                     line = line.rstrip() + " live-media-path=/live/%s/ " % (
1515                         grml_flavour
1516                     )
1517                 if bootopt.strip():
1518                     line = line.replace(" {} ".format(bootopt.strip()), " ")
1519                     if line.endswith(bootopt):
1520                         line = line[: -len(bootopt)]
1521                 line = line.rstrip() + r" bootid=%s %s " % (UUID, bootopt)
1522                 for regex in remove_regexes:
1523                     line = regex.sub(" ", line)
1524             print(line)
1525         fileinput.close()
1526
1527
1528 def initial_syslinux_config(target):
1529     """Generates initial syslinux configuration
1530
1531     @target path of syslinux's configuration files"""
1532
1533     target = target + "/"
1534     filename = target + "grmlmain.cfg"
1535     if os.path.isfile(target + "grmlmain.cfg"):
1536         return
1537     data = open(filename, "w")
1538     data.write(generate_main_syslinux_config())
1539     data.close()
1540
1541     filename = target + "hiddens.cfg"
1542     data = open(filename, "w")
1543     data.write("include hidden.cfg\n")
1544     data.close()
1545
1546
1547 def add_entry_if_not_present(filename, entry):
1548     """Write entry into filename if entry is not already in the file
1549
1550     @filename: name of the file
1551     @entry: data to write to the file
1552     """
1553     data = open(filename, "a+")
1554     data.seek(0)
1555     for line in data:
1556         if line == entry:
1557             break
1558     else:
1559         data.write(entry)
1560
1561     data.close()
1562
1563
1564 def get_flavour_filename(flavour):
1565     """Generate a iso9960 save filename out of the specified flavour
1566
1567     @flavour: grml flavour
1568     """
1569     return flavour.replace("-", "_")
1570
1571
1572 def adjust_syslinux_bootoptions(src, flavour):
1573     """Adjust existing bootoptions of specified syslinux config to
1574     grml2usb specific ones, e.g. change the location of the kernel...
1575
1576     @src: config file to alter
1577     @flavour: grml flavour
1578     """
1579
1580     append_re = re.compile(r"^(\s*append.*/boot/.*)$", re.I)
1581     # flavour_re = re.compile("(label.*)(grml\w+)")
1582     default_re = re.compile(r"(default.cfg)")
1583     bootid_re = re.compile(r"bootid=[\w_-]+")
1584     live_media_path_re = re.compile(r"live-media-path=[\w_/-]+")
1585
1586     bootopt = get_bootoptions(flavour)
1587
1588     regexe = []
1589     option_re = None
1590     if options.removeoption:
1591         option_re = re.compile(r"/boot/.*/(initrd.gz|initrd.img)")
1592
1593         for regex in options.removeoption:
1594             regexe.append(re.compile(r"%s" % regex))
1595
1596     for line in fileinput.input(src, inplace=1):
1597         # line = flavour_re.sub(r'\1 %s-\2' % flavour, line)
1598         line = default_re.sub(r"%s-\1" % flavour, line)
1599         line = bootid_re.sub("", line)
1600         line = live_media_path_re.sub("", line)
1601         line = append_re.sub(r"\1 live-media-path=/live/%s/ " % flavour, line)
1602         line = append_re.sub(r"\1 boot=live %s " % bootopt, line)
1603         line = append_re.sub(r"\1 %s=%s " % ("bootid", UUID), line)
1604         if option_re and option_re.search(line):
1605             for regex in regexe:
1606                 line = regex.sub(" ", line)
1607         sys.stdout.write(line)
1608     fileinput.close()
1609
1610
1611 def adjust_labels(src, replacement):
1612     """Adjust the specified labels in the syslinux config file src with
1613     specified replacement
1614     """
1615     label_re = re.compile(r"^(\s*label\s*) ([a-zA-Z0-9_-]+)", re.I)
1616     for line in fileinput.input(src, inplace=1):
1617         line = label_re.sub(replacement, line)
1618         sys.stdout.write(line)
1619     fileinput.close()
1620
1621
1622 def add_syslinux_entry(filename, grml_flavour):
1623     """Add includes for a specific grml_flavour to the specified filename
1624
1625     @filename: syslinux config file
1626     @grml_flavour: grml flavour to add
1627     """
1628
1629     entry_filename = "option_%s.cfg" % grml_flavour
1630     entry = "include %s\n" % entry_filename
1631
1632     add_entry_if_not_present(filename, entry)
1633     path = os.path.dirname(filename)
1634
1635     data = open(path + "/" + entry_filename, "w")
1636     data.write(generate_flavour_specific_syslinux_config(grml_flavour))
1637     data.close()
1638
1639
1640 def modify_filenames(grml_flavour, target, filenames):
1641     """Replace the standard filenames with the new ones
1642
1643     @grml_flavour: grml-flavour strin
1644     @target: directory where the files are located
1645     @filenames: list of filenames to alter
1646     """
1647     grml_filename = get_flavour_filename(grml_flavour)
1648     for filename in filenames:
1649         old_filename = "%s/%s" % (target, filename)
1650         new_filename = "%s/%s_%s" % (target, grml_filename, filename)
1651         os.rename(old_filename, new_filename)
1652
1653
1654 def remove_default_entry(filename):
1655     """Remove the default entry from specified syslinux file
1656
1657     @filename: syslinux config file
1658     """
1659     default_re = re.compile(r"^(\s*menu\s*default\s*)$", re.I)
1660     for line in fileinput.input(filename, inplace=1):
1661         if default_re.match(line):
1662             continue
1663         sys.stdout.write(line)
1664     fileinput.close()
1665
1666
1667 def handle_syslinux_config(grml_flavour, target):
1668     """Main handler for generating syslinux configuration
1669
1670     @grml_flavour: name of grml flavour the configuration should be generated for
1671     @target: path of syslinux's configuration files"""
1672
1673     logging.debug("Generating syslinux configuration")
1674     syslinux_target = target + "/boot/syslinux/"
1675     # should be present via  copy_bootloader_files(), but make sure it exits:
1676     execute(mkdir, syslinux_target)
1677     syslinux_cfg = syslinux_target + "syslinux.cfg"
1678
1679     # install main configuration only *once*, no matter how many ISOs we have:
1680     syslinux_config_file = open(syslinux_cfg, "w")
1681     syslinux_config_file.write("timeout 300\n")
1682     syslinux_config_file.write("include vesamenu.cfg\n")
1683     syslinux_config_file.close()
1684
1685     prompt_name = open(syslinux_target + "promptname.cfg", "w")
1686     prompt_name.write("menu label S^yslinux prompt\n")
1687     prompt_name.close()
1688
1689     initial_syslinux_config(syslinux_target)
1690     flavour_filename = get_flavour_filename(grml_flavour)
1691
1692     if search_file("default.cfg", syslinux_target):
1693         modify_filenames(grml_flavour, syslinux_target, ["grml.cfg", "default.cfg"])
1694
1695     filename = search_file("new_hidden.cfg", syslinux_target)
1696
1697     # process hidden file
1698     if not search_file("hidden.cfg", syslinux_target):
1699         new_hidden = syslinux_target + "hidden.cfg"
1700         os.rename(filename, new_hidden)
1701         adjust_syslinux_bootoptions(new_hidden, grml_flavour)
1702     else:
1703         new_hidden_file = "%s/%s_hidden.cfg" % (syslinux_target, flavour_filename)
1704         os.rename(filename, new_hidden_file)
1705         adjust_labels(new_hidden_file, r"\1 %s-\2" % grml_flavour)
1706         adjust_syslinux_bootoptions(new_hidden_file, grml_flavour)
1707         entry = "include %s_hidden.cfg\n" % flavour_filename
1708         add_entry_if_not_present("%s/hiddens.cfg" % syslinux_target, entry)
1709
1710     new_default = "%s_default.cfg" % (flavour_filename)
1711     entry = "include %s\n" % new_default
1712     defaults_file = "%s/defaults.cfg" % syslinux_target
1713     new_default_with_path = "%s/%s" % (syslinux_target, new_default)
1714     new_grml_cfg = "%s/%s_grml.cfg" % (syslinux_target, flavour_filename)
1715
1716     if os.path.isfile(defaults_file):
1717
1718         # remove default menu entry in menu
1719         remove_default_entry(new_default_with_path)
1720
1721         # adjust all labels for additional isos
1722         adjust_labels(new_default_with_path, r"\1 %s" % grml_flavour)
1723         adjust_labels(new_grml_cfg, r"\1 %s-\2" % grml_flavour)
1724
1725     # always adjust bootoptions
1726     adjust_syslinux_bootoptions(new_default_with_path, grml_flavour)
1727     adjust_syslinux_bootoptions(new_grml_cfg, grml_flavour)
1728
1729     add_entry_if_not_present("%s/defaults.cfg" % syslinux_target, entry)
1730
1731     add_syslinux_entry("%s/additional.cfg" % syslinux_target, flavour_filename)
1732
1733
1734 def handle_secure_boot(target, efi_img):
1735     """Provide secure boot support by extracting files from /boot/efi.img
1736
1737     @target: path where grml's main files should be copied to
1738     @efi_img: path to the efi.img file that includes the files for secure boot
1739     """
1740
1741     mkdir(target + "/efi/boot/")
1742     efi_mountpoint = tempfile.mkdtemp(
1743         prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1744     )
1745     logging.debug("efi_mountpoint = %s" % efi_mountpoint)
1746     register_tmpfile(efi_mountpoint)
1747
1748     try:
1749         logging.debug(
1750             "mount(%s, %s, ['-o', 'ro', '-t', 'vfat']" % (efi_img, efi_mountpoint)
1751         )
1752         mount(efi_img, efi_mountpoint, ["-o", "ro", "-t", "vfat"])
1753     except CriticalException as error:
1754         logging.critical("Fatal: %s", error)
1755         sys.exit(1)
1756
1757     ubuntu_cfg = search_file("grub.cfg", efi_mountpoint + "/EFI/ubuntu")
1758     logging.debug("ubuntu_cfg = %s" % ubuntu_cfg)
1759     if not ubuntu_cfg:
1760         logging.info(
1761             "No /EFI/ubuntu/grub.cfg found inside EFI image, looks like Secure Boot support is missing."
1762         )
1763     else:
1764         mkdir(target + "/efi/ubuntu")
1765         logging.debug(
1766             "exec_rsync(%s, %s + '/efi/ubuntu/grub.cfg')" % (ubuntu_cfg, target)
1767         )
1768         exec_rsync(ubuntu_cfg, target + "/efi/ubuntu/grub.cfg")
1769
1770         logging.debug(
1771             "exec_rsync(%s + '/EFI/BOOT/grubx64.efi', %s + '/efi/boot/grubx64.efi')'"
1772             % (efi_mountpoint, target)
1773         )
1774         exec_rsync(
1775             efi_mountpoint + "/EFI/BOOT/grubx64.efi", target + "/efi/boot/grubx64.efi"
1776         )
1777
1778         # NOTE - we're overwriting /efi/boot/bootx64.efi from copy_bootloader_files here
1779         logging.debug(
1780             "exec_rsync(%s + '/EFI/BOOT/bootx64.efi', %s + '/efi/boot/bootx64.efi')'"
1781             % (efi_mountpoint, target)
1782         )
1783         exec_rsync(
1784             efi_mountpoint + "/EFI/BOOT/bootx64.efi", target + "/efi/boot/bootx64.efi"
1785         )
1786
1787     try:
1788         unmount(efi_mountpoint, "")
1789         logging.debug("Unmounted %s" % efi_mountpoint)
1790         os.rmdir(efi_mountpoint)
1791         logging.debug("Removed directory %s" % efi_mountpoint)
1792     except Exception:
1793         logging.critical("RuntimeError while umount %s" % efi_mountpoint)
1794         sys.exit(1)
1795
1796
1797 def handle_bootloader_config(grml_flavour, device, target):
1798     """Main handler for generating bootloader's configuration
1799
1800     @grml_flavour: name of grml flavour the configuration should be generated for
1801     @device: device/partition where bootloader should be installed to
1802     @target: path of bootloader's configuration files"""
1803
1804     global UUID
1805     UUID = get_uuid(target)
1806     if options.skipsyslinuxconfig:
1807         logging.info("Skipping generation of syslinux configuration as requested.")
1808     else:
1809         try:
1810             handle_syslinux_config(grml_flavour, target)
1811         except CriticalException as error:
1812             logging.critical("Fatal: %s", error)
1813             sys.exit(1)
1814
1815     if options.skipgrubconfig:
1816         logging.info("Skipping generation of grub configuration as requested.")
1817     else:
1818         try:
1819             handle_grub_config(grml_flavour, device, target)
1820         except CriticalException as error:
1821             logging.critical("Fatal: %s", error)
1822             sys.exit(1)
1823
1824
1825 def install(image, device):
1826     """Install a grml image to the specified device
1827
1828     @image: directory or is file
1829     @device: partition or directory to install the device
1830     """
1831     iso_mountpoint = image
1832     remove_image_mountpoint = False
1833     if os.path.isdir(image):
1834         if options.force or os.path.exists(os.path.join(image, "live")):
1835             logging.info("Using %s as install base", image)
1836         else:
1837             q = input(
1838                 "%s does not look like a Grml system. "
1839                 "Do you really want to use this image? y/N " % image
1840             )
1841             if q.lower() == "y":
1842                 logging.info("Using %s as install base", image)
1843             else:
1844                 logging.info("Skipping install base %s", image)
1845     else:
1846         logging.info("Using ISO %s", image)
1847         iso_mountpoint = tempfile.mkdtemp(
1848             prefix="grml2usb", dir=os.path.abspath(options.tmpdir)
1849         )
1850         register_tmpfile(iso_mountpoint)
1851         remove_image_mountpoint = True
1852         try:
1853             mount(image, iso_mountpoint, ["-o", "loop,ro", "-t", "iso9660"])
1854         except CriticalException as error:
1855             logging.critical("Fatal: %s", error)
1856             sys.exit(1)
1857
1858     try:
1859         install_grml(iso_mountpoint, device)
1860     finally:
1861         if remove_image_mountpoint:
1862             try:
1863                 remove_mountpoint(iso_mountpoint)
1864             except CriticalException:
1865                 cleanup()
1866                 raise
1867
1868
1869 def install_grml(mountpoint, device):
1870     """Main logic for copying files of the currently running Grml system.
1871
1872     @mountpoint: directory where currently running live system resides (usually /run/live/medium)
1873     @device: partition where the specified ISO should be installed to"""
1874
1875     device_mountpoint = device
1876     if os.path.isdir(device):
1877         logging.info("Specified device is a directory, therefore not mounting.")
1878         remove_device_mountpoint = False
1879     else:
1880         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
1881         register_tmpfile(device_mountpoint)
1882         remove_device_mountpoint = True
1883         try:
1884             check_for_fat(device)
1885             if not options.skipbootflag:
1886                 check_boot_flag(device)
1887
1888             set_rw(device)
1889             mount(device, device_mountpoint, ["-o", "utf8,iocharset=iso8859-1"])
1890         except CriticalException:
1891             mount(device, device_mountpoint, "")
1892     try:
1893         grml_flavours = identify_grml_flavour(mountpoint)
1894         for flavour in set(grml_flavours):
1895             if not flavour:
1896                 logging.warning("No valid flavour found, please check your iso")
1897             logging.info('Identified grml flavour "%s".', flavour)
1898             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1899             GRML_FLAVOURS.add(flavour)
1900     finally:
1901         if remove_device_mountpoint:
1902             remove_mountpoint(device_mountpoint)
1903
1904
1905 def remove_mountpoint(mountpoint):
1906     """remove a registered mountpoint
1907     """
1908
1909     try:
1910         unmount(mountpoint, "")
1911         if os.path.isdir(mountpoint):
1912             os.rmdir(mountpoint)
1913             unregister_tmpfile(mountpoint)
1914     except CriticalException:
1915         cleanup()
1916         raise
1917
1918
1919 def handle_mbr(device):
1920     """Main handler for installing master boot record (MBR)
1921
1922     @device: device where the MBR should be installed to"""
1923
1924     if options.dryrun:
1925         logging.info("Would install MBR")
1926         return 0
1927
1928     mbr_device, partition_number = get_device_from_partition(device)
1929     if partition_number is None:
1930         logging.warn("Could not detect partition number, not activating partition")
1931
1932     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1933     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1934     if mbr_device == "/dev/loop":
1935         mbr_device = device
1936         logging.info(
1937             "Detected loop device - using %s as MBR device therefore", mbr_device
1938         )
1939
1940     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1941     if options.syslinuxmbr:
1942         mbrcode = ""
1943         mbr_locations = (
1944             "/usr/lib/syslinux/mbr.bin",
1945             "/usr/lib/syslinux/bios/mbr.bin",
1946             "/usr/share/syslinux/mbr.bin",
1947         )
1948         for mbrpath in mbr_locations:
1949             if os.path.isfile(mbrpath):
1950                 mbrcode = mbrpath
1951                 break
1952
1953         if not mbrcode:
1954             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1955             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1956             raise CriticalException(
1957                 "syslinux MBR  can not be found at %s." % str_locations
1958             )
1959     elif options.mbrmenu:
1960         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1961
1962     try:
1963         install_mbr(mbrcode, mbr_device, partition_number, True)
1964     except (IOError, Exception) as error:
1965         logging.critical("Execution failed: %s", error)
1966         sys.exit(1)
1967
1968
1969 def handle_vfat(device):
1970     """Check for FAT specific settings and options
1971
1972     @device: device that should checked / formated"""
1973
1974     # make sure we have mkfs.vfat available
1975     if options.fat16:
1976         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1977             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1978             logging.critical("Please make sure to install dosfstools.")
1979             sys.exit(1)
1980
1981         if options.force:
1982             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1983         else:
1984             # make sure the user is aware of what he is doing
1985             f = input(
1986                 "Are you sure you want to format the specified partition with fat16? y/N "
1987             )
1988             if f == "y" or f == "Y":
1989                 logging.info(
1990                     "Note: you can skip this question using the option --force"
1991                 )
1992             else:
1993                 sys.exit(1)
1994         try:
1995             mkfs_fat16(device)
1996         except CriticalException as error:
1997             logging.critical("Execution failed: %s", error)
1998             sys.exit(1)
1999
2000     # check for vfat filesystem
2001     if device is not None and not os.path.isdir(device) and options.syslinux:
2002         try:
2003             check_for_fat(device)
2004         except CriticalException as error:
2005             logging.critical("Execution failed: %s", error)
2006             sys.exit(1)
2007
2008     if options.skipusbcheck:
2009         logging.info(
2010             "Not checking for removable USB device as requested via option --skip-usb-check."
2011         )
2012         return
2013
2014     if (
2015         not os.path.isdir(device)
2016         and not check_for_usbdevice(device)
2017         and not options.force
2018     ):
2019         print(
2020             "Warning: the specified device %s does not look like a removable usb device."
2021             % device
2022         )
2023         f = input("Do you really want to continue? y/N ")
2024         if f.lower() != "y":
2025             sys.exit(1)
2026
2027
2028 def handle_compat_warning(device):
2029     """Backwards compatible checks
2030
2031     @device: device that should be checked"""
2032
2033     # make sure we can replace old grml2usb script and warn user when using old way of life:
2034     if (
2035         device.startswith("/mnt/external")
2036         or device.startswith("/mnt/usb")
2037         and not options.force
2038     ):
2039         print("Warning: the semantics of grml2usb has changed.")
2040         print("Instead of using grml2usb /path/to/iso %s you might" % device)
2041         print("want to use grml2usb /path/to/iso /dev/... instead.")
2042         print("Please check out the grml2usb manpage for details.")
2043         f = input("Do you really want to continue? y/N ")
2044         if f.lower() != "y":
2045             sys.exit(1)
2046
2047
2048 def handle_logging():
2049     """Log handling and configuration"""
2050
2051     if options.verbose and options.quiet:
2052         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
2053
2054     FORMAT = "%(message)s"
2055     if options.verbose:
2056         FORMAT = "%(asctime)-15s %(message)s"
2057         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
2058     elif options.quiet:
2059         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
2060     else:
2061         logging.basicConfig(level=logging.INFO, format=FORMAT)
2062
2063
2064 def handle_bootloader(device):
2065     """wrapper for installing bootloader
2066
2067     @device: device where bootloader should be installed to"""
2068
2069     # Install bootloader only if not using the --copy-only option
2070     if options.copyonly:
2071         logging.info(
2072             "Not installing bootloader and its files as requested via option copyonly."
2073         )
2074     elif os.path.isdir(device):
2075         logging.info("Not installing bootloader as %s is a directory.", device)
2076     else:
2077         install_bootloader(device)
2078
2079
2080 def check_options(opts):
2081     """Check compatibility of provided user opts
2082
2083     @opts option dict from OptionParser
2084     """
2085     if opts.grubmbr and not opts.grub:
2086         raise CriticalException("--grub-mbr requires --grub option.")
2087
2088     if opts.copyonly and opts.grub:
2089         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
2090
2091
2092 def check_programs():
2093     """check if all needed programs are installed"""
2094     if options.grub:
2095         global GRUB_INSTALL
2096         GRUB_INSTALL = which("grub-install") or which("grub2-install")
2097         if not GRUB_INSTALL:
2098             logging.critical(
2099                 "Fatal: grub-install not available (please install the "
2100                 "grub package or drop the --grub option)"
2101             )
2102             sys.exit(1)
2103
2104     if options.syslinux:
2105         if not which("syslinux"):
2106             logging.critical(
2107                 "Fatal: syslinux not available (please install the "
2108                 "syslinux package or use the --grub option)"
2109             )
2110             sys.exit(1)
2111
2112     if not which("rsync"):
2113         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2114         sys.exit(1)
2115
2116
2117 def load_loop():
2118     """Runs modprobe loop and throws away its output"""
2119     if not which("modprobe"):
2120         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2121         logging.critical("Hint: is /sbin missing in PATH?")
2122         sys.exit(1)
2123
2124     proc = subprocess.Popen(
2125         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2126     )
2127     proc.communicate()
2128
2129
2130 def main():
2131     """Main invocation"""
2132
2133     try:
2134         if options.version:
2135             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2136             sys.exit(0)
2137
2138         if len(args) < 2:
2139             parser.error("invalid usage")
2140
2141         # log handling
2142         handle_logging()
2143
2144         # make sure we have the appropriate permissions
2145         check_uid_root()
2146
2147         check_options(options)
2148
2149         load_loop()
2150
2151         logging.info("Executing grml2usb version %s", PROG_VERSION)
2152
2153         if options.dryrun:
2154             logging.info("Running in simulation mode as requested via option dry-run.")
2155
2156         check_programs()
2157
2158         # specified arguments
2159         device = os.path.realpath(args[len(args) - 1])
2160         isos = args[0 : len(args) - 1]
2161
2162         if not os.path.isdir(device):
2163             if device[-1:].isdigit():
2164                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2165                     logging.warn(
2166                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2167                     )
2168
2169         # provide upgrade path
2170         handle_compat_warning(device)
2171
2172         # check for vfat partition
2173         handle_vfat(device)
2174
2175         # main operation (like installing files)
2176         for iso in isos:
2177             install(iso, device)
2178
2179         # install mbr
2180         is_superfloppy = not device[-1:].isdigit()
2181         if is_superfloppy:
2182             logging.info("Detected superfloppy format - not installing MBR")
2183
2184         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2185             handle_mbr(device)
2186
2187         handle_bootloader(device)
2188
2189         logging.info(
2190             "Note: grml flavour %s was installed as the default booting system.",
2191             GRML_DEFAULT,
2192         )
2193
2194         for flavour in GRML_FLAVOURS:
2195             logging.info(
2196                 "Note: you can boot flavour %s using '%s' on the commandline.",
2197                 flavour,
2198                 flavour,
2199             )
2200
2201         # finally be polite :)
2202         logging.info(
2203             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2204             PROG_VERSION,
2205         )
2206
2207     except Exception as error:
2208         logging.critical("Fatal: %s", str(error))
2209         if options.verbose:
2210             logging.exception("Exception:")
2211         sys.exit(1)
2212
2213
2214 if __name__ == "__main__":
2215     try:
2216         main()
2217     except KeyboardInterrupt:
2218         logging.info("Received KeyboardInterrupt")
2219         cleanup()
2220
2221 # END OF FILE ##################################################################
2222 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8