Merge remote-tracking branch 'origin/pr/35'
[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 boot flag 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     logging.info("Checking for boot flag")
509     try:
510         import parted
511
512         part = get_partition_for_path(device)
513         if part is None:
514             raise HodorException("parted could not find partition")
515         if part.getFlag(parted.PARTITION_BOOT):
516             logging.debug("boot flag is enabled on %s" % device)
517             return
518         else:
519             logging.debug("boot flag is NOT enabled on %s" % device)
520             raise VerifyException(
521                 "Device %s does not have the boot flag set. "
522                 "Please enable it to be able to boot." % device
523             )
524     except ImportError:
525         raise VerifyException(
526             "Could not import parted to verify boot flag on %s, please make sure python3-parted is installed."
527             % device
528         )
529
530
531 def mkfs_fat16(device):
532     """Format specified device with VFAT/FAT16 filesystem.
533
534     @device: partition that should be formated"""
535
536     if options.dryrun:
537         logging.info("Would execute mkfs.vfat -F 16 %s now.", device)
538         return 0
539
540     logging.info("Formating partition with fat16 filesystem")
541     logging.debug("mkfs.vfat -F 16 %s", device)
542     proc = subprocess.Popen(["mkfs.vfat", "-F", "16", device])
543     proc.wait()
544     if proc.returncode != 0:
545         raise CriticalException("error executing mkfs.vfat")
546
547
548 def generate_isolinux_splash(grml_flavour):
549     """Generate bootsplash for isolinux/syslinux
550
551     @grml_flavour: name of grml flavour the configuration should be generated for"""
552
553     grml_name = grml_flavour
554
555     return """\
556 \ f17\f\18/boot/syslinux/logo.16
557
558 Some information and boot options available via keys F2 - F10. http://grml.org/
559 %(grml_name)s
560 """ % {
561         "grml_name": grml_name
562     }
563
564
565 def generate_main_syslinux_config(*arg):
566     """Generate main configuration for use in syslinux.cfg
567
568     @*arg: just for backward compatibility"""
569     # pylint: disable-msg=W0613
570     # remove warning about unused arg
571
572     return """\
573 label -
574 menu label Default boot modes:
575 menu disable
576 include defaults.cfg
577
578 menu end
579 menu separator
580
581 # flavours:
582 label -
583 menu label Additional boot entries for:
584 menu disable
585 include additional.cfg
586
587 menu separator
588 include options.cfg
589 include addons.cfg
590
591 label help
592   include promptname.cfg
593   config prompt.cfg
594   text help
595                                         Jump to old style isolinux prompt
596                                         featuring further information
597                                         regarding available boot options.
598   endtext
599
600
601 include hiddens.cfg
602 """
603
604
605 def generate_flavour_specific_syslinux_config(grml_flavour):
606     """Generate flavour specific configuration for use in syslinux.cfg
607
608     @grml_flavour: name of grml flavour the configuration should be generated for"""
609
610     return """\
611 menu begin grml %(grml_flavour)s
612     menu title %(display_name)s
613     label mainmenu
614     menu label ^Back to main menu...
615     menu exit
616     menu separator
617     # include config for boot parameters from disk
618     include %(grml_flavour)s_grml.cfg
619     menu hide
620 menu end
621 """ % {
622         "grml_flavour": grml_flavour,
623         "display_name": get_flavour_filename(grml_flavour),
624     }
625
626
627 def install_grub(device):
628     """Install grub on specified device.
629
630     @mntpoint: mountpoint of device where grub should install its files to
631     @device: partition where grub should be installed to"""
632
633     if options.dryrun:
634         logging.info(
635             "Would execute grub-install [--root-directory=mount_point] %s now.", device
636         )
637     else:
638         device_mountpoint = tempfile.mkdtemp(prefix="grml2usb")
639         register_tmpfile(device_mountpoint)
640         try:
641             # If using --grub-mbr then make sure we install grub in MBR instead of PBR
642             if options.grubmbr:
643                 logging.debug("Using option --grub-mbr ...")
644                 grub_device, x = get_device_from_partition(device)
645             else:
646                 grub_device = device
647
648             set_rw(device)
649             mount(device, device_mountpoint, "")
650
651             logging.info("Installing grub as bootloader")
652             for opt in ["--", "--force"]:
653                 set_rw(device)
654                 set_rw(grub_device)
655                 logging.debug(
656                     "%s --recheck --no-floppy --target=i386-pc --root-directory=%s %s %s",
657                     GRUB_INSTALL,
658                     device_mountpoint,
659                     opt,
660                     grub_device,
661                 )
662                 proc = subprocess.Popen(
663                     [
664                         GRUB_INSTALL,
665                         "--recheck",
666                         "--no-floppy",
667                         "--target=i386-pc",
668                         "--root-directory=%s" % device_mountpoint,
669                         opt,
670                         grub_device,
671                     ],
672                     stdout=open(os.devnull, "r+"),
673                 )
674                 proc.wait()
675                 if proc.returncode == 0:
676                     break
677
678             if proc.returncode != 0:
679                 # raise Exception("error executing grub-install")
680                 logging.critical(
681                     "Fatal: error executing grub-install "
682                     "(please check the grml2usb FAQ or drop the --grub option)"
683                 )
684                 logging.critical(
685                     "Note:  if using grub2 consider using "
686                     "the --grub-mbr option as grub considers PBR problematic."
687                 )
688                 cleanup()
689                 sys.exit(1)
690         except CriticalException as error:
691             logging.critical("Fatal: %s", error)
692             cleanup()
693             sys.exit(1)
694         finally:
695             unmount(device_mountpoint, "")
696             os.rmdir(device_mountpoint)
697             unregister_tmpfile(device_mountpoint)
698
699
700 def install_syslinux(device):
701     """Install syslinux on specified device.
702
703     @device: partition where syslinux should be installed to"""
704
705     if options.dryrun:
706         logging.info("Would install syslinux as bootloader on %s", device)
707         return 0
708
709     set_rw(device)
710
711     # syslinux -d boot/isolinux /dev/sdb1
712     logging.info("Installing syslinux as bootloader")
713     logging.debug("syslinux -d boot/syslinux %s", device)
714     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
715     proc.wait()
716     if proc.returncode != 0:
717         raise CriticalException(
718             "Error executing syslinux (either try --fat16 or use grub?)"
719         )
720
721
722 def install_bootloader(device):
723     """Install bootloader on specified device.
724
725     @device: partition where bootloader should be installed to"""
726
727     # by default we use grub, so install syslinux only on request
728     if options.grub:
729         try:
730             install_grub(device)
731         except CriticalException as error:
732             logging.critical("Fatal: %s", error)
733             cleanup()
734             sys.exit(1)
735     else:
736         try:
737             install_syslinux(device)
738         except CriticalException as error:
739             logging.critical("Fatal: %s", error)
740             cleanup()
741             sys.exit(1)
742
743
744 def install_mbr(mbrtemplate, device, partition, ismirbsdmbr=True):
745     """install 'mbr' master boot record (MBR) on a device
746
747     Retrieve the partition table from "device", install an MBR from the
748     "mbrtemplate" file, set the "partition" (0..3) active, and install the
749     result back to "device".
750
751     @mbrtemplate: default MBR file (must be a valid MBR file of at least 440
752     (or 439 if ismirbsdmbr) bytes)
753
754     @device: name of a file assumed to be a hard disc (or USB stick) image, or
755     something like "/dev/sdb"
756
757     @partition: must be a number between 0 and 3, inclusive
758
759     @ismirbsdmbr: if true then ignore the active flag, set the mirbsdmbr
760     specific flag to 0/1/2/3 and set the MBR's default value accordingly. If
761     false then leave the mirbsdmbr specific flag set to FFh, set all
762     active flags to 0 and set the active flag of the partition to 80h.  Note:
763     behaviour of mirbsdmbr: if flag = 0/1/2/3 then use it, otherwise search for
764     the active flag."""
765
766     logging.info("Installing default MBR")
767
768     if not os.path.isfile(mbrtemplate):
769         logging.error(
770             "Error installing MBR (either try --syslinux-mbr or "
771             'install missing file "%s"?)',
772             mbrtemplate,
773         )
774         raise CriticalException("%s can not be read." % mbrtemplate)
775
776     if partition is not None and ((partition < 0) or (partition > 3)):
777         logging.warn("Cannot activate partition %d", partition)
778         partition = None
779
780     if ismirbsdmbr:
781         nmbrbytes = 439
782     else:
783         nmbrbytes = 440
784
785     tmpf = tempfile.NamedTemporaryFile()
786
787     logging.debug("executing: dd if='%s' of='%s' bs=512 count=1", device, tmpf.name)
788     proc = subprocess.Popen(
789         ["dd", "if=%s" % device, "of=%s" % tmpf.name, "bs=512", "count=1"],
790         stderr=open(os.devnull, "r+"),
791     )
792     proc.wait()
793     if proc.returncode != 0:
794         raise Exception("error executing dd (first run)")
795
796     logging.debug(
797         "executing: dd if=%s of=%s bs=%s count=1 conv=notrunc",
798         mbrtemplate,
799         tmpf.name,
800         nmbrbytes,
801     )
802     proc = subprocess.Popen(
803         [
804             "dd",
805             "if=%s" % mbrtemplate,
806             "of=%s" % tmpf.name,
807             "bs=%s" % nmbrbytes,
808             "count=1",
809             "conv=notrunc",
810         ],
811         stderr=open(os.devnull, "r+"),
812     )
813     proc.wait()
814     if proc.returncode != 0:
815         raise Exception("error executing dd (second run)")
816
817     mbrcode = tmpf.file.read(512)
818     if len(mbrcode) < 512:
819         raise EOFError("MBR size (%d) < 512" % len(mbrcode))
820
821     if partition is not None:
822         if ismirbsdmbr:
823             mbrcode = (
824                 mbrcode[0:439]
825                 + chr(partition).encode("latin-1")
826                 + mbrcode[440:510]
827                 + b"\x55\xAA"
828             )
829         else:
830             actives = [b"\x00", b"\x00", b"\x00", b"\x00"]
831             actives[partition] = b"\x80"
832             mbrcode = (
833                 mbrcode[0:446]
834                 + actives[0]
835                 + mbrcode[447:462]
836                 + actives[1]
837                 + mbrcode[463:478]
838                 + actives[2]
839                 + mbrcode[479:494]
840                 + actives[3]
841                 + mbrcode[495:510]
842                 + b"\x55\xAA"
843             )
844
845     tmpf.file.seek(0)
846     tmpf.file.truncate()
847     tmpf.file.write(mbrcode)
848     tmpf.file.close()
849
850     set_rw(device)
851
852     logging.debug(
853         "executing: dd if='%s' of='%s' bs=512 count=1 conv=notrunc", tmpf.name, device
854     )
855     proc = subprocess.Popen(
856         [
857             "dd",
858             "if=%s" % tmpf.name,
859             "of=%s" % device,
860             "bs=512",
861             "count=1",
862             "conv=notrunc",
863         ],
864         stderr=open(os.devnull, "r+"),
865     )
866     proc.wait()
867     if proc.returncode != 0:
868         raise Exception("error executing dd (third run)")
869     del tmpf
870
871     # make sure we sync filesystems before returning
872     logging.debug("executing: sync")
873     proc = subprocess.Popen(["sync"])
874     proc.wait()
875
876     logging.debug("Probing device via 'blockdev --rereadpt %s'", device)
877     proc = subprocess.Popen(["blockdev", "--rereadpt", device])
878     proc.wait()
879     if proc.returncode != 0:
880         raise Exception(
881             "Couldn't execute blockdev on '%s' (install util-linux?)", device
882         )
883
884     set_rw(device)
885
886
887 def is_writeable(device):
888     """Check if the device is writeable for the current user
889
890     @device: partition where bootloader should be installed to"""
891
892     if not device:
893         return False
894
895     if not os.path.exists(device):
896         return False
897
898     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
899
900
901 def mount(source, target, mount_options):
902     """Mount specified source on given target
903
904     @source: name of device/ISO that should be mounted
905     @target: directory where the ISO should be mounted to
906     @options: mount specific options"""
907
908     # note: options.dryrun does not work here, as we have to
909     # locate files and identify the grml flavour
910
911     for x in open("/proc/mounts", "r").readlines():
912         if x.startswith(source):
913             raise CriticalException(
914                 (
915                     "Error executing mount: %s already mounted - "
916                     "please unmount before invoking grml2usb"
917                 ).format(source)
918             )
919
920     if os.path.isdir(source):
921         logging.debug("Source %s is not a device, therefore not mounting.", source)
922         return 0
923
924     logging.debug("mount %s %s %s", mount_options, source, target)
925     proc = subprocess.Popen(["mount"] + list(mount_options) + [source, target])
926     proc.wait()
927     if proc.returncode != 0:
928         raise CriticalException(
929             "Error executing mount (no filesystem on the partition?)"
930         )
931     else:
932         logging.debug("register_mountpoint(%s)", target)
933         register_mountpoint(target)
934
935
936 def unmount(target, unmount_options):
937     """Unmount specified target
938
939     @target: target where something is mounted on and which should be unmounted
940     @options: options for umount command"""
941
942     # make sure we unmount only already mounted targets
943     target_unmount = False
944     mounts = open("/proc/mounts").readlines()
945     mountstring = re.compile(".*%s.*" % re.escape(os.path.realpath(target)))
946     for line in mounts:
947         if re.match(mountstring, line):
948             target_unmount = True
949
950     if not target_unmount:
951         logging.debug("%s not mounted anymore", target)
952     else:
953         logging.debug("umount %s %s", list(unmount_options), target)
954         proc = subprocess.Popen(["umount"] + list(unmount_options) + [target])
955         proc.wait()
956         if proc.returncode != 0:
957             raise Exception("Error executing umount")
958         else:
959             logging.debug("unregister_mountpoint(%s)", target)
960             unregister_mountpoint(target)
961
962
963 def check_for_usbdevice(device):
964     """Check whether the specified device is a removable USB device
965
966     @device: device name, like /dev/sda1 or /dev/sda
967     """
968
969     usbdevice = re.match(r"/dev/(.*?)\d*$", device).group(1)
970     # newer systems:
971     usbdev = os.path.realpath("/sys/class/block/" + usbdevice + "/removable")
972     if not os.path.isfile(usbdev):
973         # Ubuntu with kernel 2.6.24 for example:
974         usbdev = os.path.realpath("/sys/block/" + usbdevice + "/removable")
975
976     if os.path.isfile(usbdev):
977         is_usb = open(usbdev).readline()
978         if is_usb.find("1"):
979             return 0
980
981     return 1
982
983
984 def check_for_fat(partition):
985     """Check whether specified partition is a valid VFAT/FAT16 filesystem
986
987     @partition: device name of partition"""
988
989     if not os.access(partition, os.R_OK):
990         raise CriticalException(
991             "Failed to read device %s"
992             " (wrong UID/permissions or device/directory not present?)" % partition
993         )
994
995     try:
996         filesystem = (
997             subprocess.check_output(
998                 ["/sbin/blkid", "-s", "TYPE", "-o", "value", partition]
999             )
1000             .decode()
1001             .rstrip()
1002         )
1003
1004         if filesystem != "vfat":
1005             raise CriticalException(
1006                 "Partition %s does not contain a FAT16 filesystem. "
1007                 "(Use --fat16 or run mkfs.vfat %s)" % (partition, partition)
1008             )
1009
1010     except OSError:
1011         raise CriticalException(
1012             "Sorry, /sbin/blkid not available (install util-linux?)"
1013         )
1014
1015
1016 def mkdir(directory):
1017     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
1018
1019     # just silently pass as it's just fine it the directory exists
1020     if not os.path.isdir(directory):
1021         try:
1022             os.makedirs(directory)
1023         # pylint: disable-msg=W0704
1024         except OSError:
1025             pass
1026
1027
1028 def exec_rsync(source, target):
1029     """Simple wrapper around rsync to install files
1030
1031     @source: source file/directory
1032     @target: target file/directory"""
1033     logging.debug("Source: %s / Target: %s", source, target)
1034     proc = subprocess.Popen(["rsync", "-rlptDH", "--inplace", source, target])
1035     proc.wait()
1036     if proc.returncode == 12:
1037         logging.critical("Fatal: No space left on device")
1038         cleanup()
1039         sys.exit(1)
1040
1041     if proc.returncode != 0:
1042         logging.critical("Fatal: could not install %s", source)
1043         cleanup()
1044         sys.exit(1)
1045
1046
1047 def write_uuid(target_file):
1048     """Generates an returns uuid and write it to the specified file
1049
1050     @target_file: filename to write the uuid to
1051     """
1052
1053     fileh = open(target_file, "w")
1054     uid = str(uuid.uuid4())
1055     fileh.write(uid)
1056     fileh.close()
1057     return uid
1058
1059
1060 def get_uuid(target):
1061     """Get the uuid of the specified target. Will generate an uuid if none exist.
1062
1063     @target: directory/mountpoint containing the grml layout
1064     """
1065
1066     conf_target = target + "/conf/"
1067     uuid_file_name = conf_target + "/bootid.txt"
1068     if os.path.isdir(conf_target):
1069         if os.path.isfile(uuid_file_name):
1070             uuid_file = open(uuid_file_name, "r")
1071             uid = uuid_file.readline().strip()
1072             uuid_file.close()
1073             return uid
1074         else:
1075             return write_uuid(uuid_file_name)
1076     else:
1077         execute(mkdir, conf_target)
1078         return write_uuid(uuid_file_name)
1079
1080
1081 def get_shortname(grml_flavour):
1082     """Get shortname based from grml_flavour name. The rules applied are the same as in grml-live
1083     @grml_flavour: flavour name which shold be translated to shortname"""
1084
1085     return re.sub(r"[,._-]", "", grml_flavour)
1086
1087
1088 def copy_system_files(grml_flavour, iso_mount, target):
1089     """copy grml's main files (like squashfs, kernel and initrd) to a given target
1090
1091     @grml_flavour: name of grml flavour the configuration should be generated for
1092     @iso_mount: path where a grml ISO is mounted on
1093     @target: path where grml's main files should be copied to"""
1094
1095     squashfs = search_file(grml_flavour + ".squashfs", iso_mount)
1096     if squashfs is None:
1097         logging.error("error locating squashfs file")
1098         raise CriticalException(
1099             "squashfs file not found, please check that your iso is not corrupt"
1100         )
1101     else:
1102         squashfs_target = target + "/live/" + grml_flavour + "/"
1103         execute(mkdir, squashfs_target)
1104     exec_rsync(squashfs, squashfs_target + grml_flavour + ".squashfs")
1105
1106     for prefix in grml_flavour + "/", "":
1107         filesystem_module = search_file(prefix + "filesystem.module", iso_mount)
1108         if filesystem_module:
1109             break
1110     if filesystem_module is None:
1111         logging.error("error locating filesystem.module file")
1112         raise CriticalException("filesystem.module not found")
1113     else:
1114         exec_rsync(filesystem_module, squashfs_target + "filesystem.module")
1115
1116     shortname = get_shortname(grml_flavour)
1117     if os.path.isdir(iso_mount + "/boot/" + shortname):
1118         exec_rsync(iso_mount + "/boot/" + shortname, target + "/boot")
1119     else:
1120         kernel = search_file("vmlinuz", iso_mount)
1121         if kernel is None:
1122             # compat for releases < 2011.12
1123             kernel = search_file("linux26", iso_mount)
1124
1125         if kernel is None:
1126             logging.error("error locating kernel file")
1127             raise CriticalException("Kernel not found")
1128
1129         source = os.path.dirname(kernel) + "/"
1130         dest = target + "/" + os.path.dirname(kernel).replace(iso_mount, "") + "/"
1131         execute(mkdir, dest)
1132         exec_rsync(source, dest)
1133
1134
1135 def update_grml_versions(iso_mount, target):
1136     """Update the grml version file on a cd
1137     Returns true if version was updated successfully,
1138     False if grml-version does not exist yet on the mountpoint
1139
1140     @iso_mount: string of the iso mount point
1141     @target: path of the target mount point
1142     """
1143     grml_target = target + "/grml/"
1144     target_grml_version_file = search_file("grml-version", grml_target)
1145     if target_grml_version_file:
1146         iso_grml_version_file = search_file("grml-version", iso_mount)
1147         if not iso_grml_version_file:
1148             logging.warn(
1149                 "Warning: %s could not be found - can not install it",
1150                 iso_grml_version_file,
1151             )
1152             return False
1153         try:
1154             # read the flavours from the iso image
1155             iso_versions = {}
1156             iso_file = open(iso_grml_version_file, "r")
1157             for line in iso_file:
1158                 iso_versions[get_flavour(line)] = line.strip()
1159
1160             # update the existing flavours on the target
1161             for line in fileinput.input([target_grml_version_file], inplace=1):
1162                 flavour = get_flavour(line)
1163                 if flavour in list(iso_versions.keys()):
1164                     print(iso_versions.pop(flavour))
1165                 else:
1166                     print(line.strip())
1167             fileinput.close()
1168
1169             target_file = open(target_grml_version_file, "a")
1170             # add the new flavours from the current iso
1171             for flavour in iso_versions:
1172                 target_file.write("%s\n" % iso_versions[flavour])
1173         except IOError:
1174             logging.warn("Warning: Could not write file")
1175         finally:
1176             iso_file.close()
1177             target_file.close()
1178         return True
1179     else:
1180         return False
1181
1182
1183 def copy_grml_files(grml_flavour, iso_mount, target):
1184     """copy some minor grml files to a given target
1185
1186     @grml_flavour: the current grml_flavour
1187     @iso_mount: path where a grml ISO is mounted on
1188     @target: path where grml's main files should be copied to"""
1189
1190     grml_target = target + "/grml/"
1191     execute(mkdir, grml_target)
1192
1193     grml_prefixe = ["GRML", "grml"]
1194     for prefix in grml_prefixe:
1195         filename = "{0}/{1}/{2}".format(iso_mount, prefix, grml_flavour)
1196         if os.path.exists(filename):
1197             exec_rsync(filename, grml_target)
1198             break
1199     else:
1200         logging.warn("Warning: could not find flavour directory for %s ", grml_flavour)
1201
1202
1203 def copy_addons(iso_mount, target):
1204     """copy grml's addons files (like allinoneimg, bsd4grml,..) to a given target
1205
1206     @iso_mount: path where a grml ISO is mounted on
1207     @target: path where grml's main files should be copied to"""
1208
1209     addons = target + "/boot/addons/"
1210     execute(mkdir, addons)
1211
1212     for addon_file in glob.glob(iso_mount + "/boot/addons/*"):
1213         filename = os.path.basename(addon_file)
1214         src_file = iso_mount + "/boot/addons/" + os.path.basename(addon_file)
1215         logging.debug("Copying addon file %s" % filename)
1216         exec_rsync(src_file, addons)
1217
1218
1219 def build_loopbackcfg(target):
1220     """Generate GRUB's loopback.cfg based on existing config files.
1221
1222     @target: target directory
1223     """
1224
1225     grub_dir = "/boot/grub/"
1226     mkdir(os.path.join(target, grub_dir))
1227
1228     f = open(target + grub_dir + "loopback.cfg", "w")
1229
1230     f.write("# grml2usb generated grub2 configuration file\n")
1231     f.write("source /boot/grub/header.cfg\n")
1232
1233     for defaults in glob.glob(
1234         target + os.path.sep + grub_dir + os.path.sep + "*_default.cfg"
1235     ):
1236         sourcefile = defaults.split(target + os.path.sep)[1]
1237         logging.debug("Found source file" + sourcefile)
1238         os.path.isfile(defaults) and f.write("source " + sourcefile + "\n")
1239
1240     for ops in glob.glob(
1241         target + os.path.sep + grub_dir + os.path.sep + "*_options.cfg"
1242     ):
1243         sourcefile = ops.split(target + os.path.sep)[1]
1244         logging.debug("Found source file" + sourcefile)
1245         os.path.isfile(ops) and f.write("source " + sourcefile + "\n")
1246
1247     f.write("source /boot/grub/addons.cfg\n")
1248     f.write("source /boot/grub/footer.cfg\n")
1249     f.close()
1250
1251
1252 def glob_and_copy(filepattern, dst):
1253     """Glob on specified filepattern and copy the result to dst
1254
1255     @filepattern: globbing pattern
1256     @dst: target directory
1257     """
1258     for name in glob.glob(filepattern):
1259         copy_if_exist(name, dst)
1260
1261
1262 def search_and_copy(filename, search_path, dst):
1263     """Search for the specified filename at searchpath and copy it to dst
1264
1265     @filename: filename to look for
1266     @search_path: base search file
1267     @dst: destionation to copy the file to
1268     """
1269     file_location = search_file(filename, search_path)
1270     copy_if_exist(file_location, dst)
1271
1272
1273 def copy_if_exist(filename, dst):
1274     """Copy filename to dst if filename is set.
1275
1276     @filename: a filename
1277     @dst: dst file
1278     """
1279     if filename and (os.path.isfile(filename) or os.path.isdir(filename)):
1280         exec_rsync(filename, dst)
1281
1282
1283 def copy_bootloader_files(iso_mount, target, grml_flavour):
1284     """Copy grml's bootloader files to a given target
1285
1286     @iso_mount: path where a grml ISO is mounted on
1287     @target: path where grml's main files should be copied to
1288     @grml_flavour: name of the current processed grml_flavour
1289     """
1290
1291     syslinux_target = target + "/boot/syslinux/"
1292     execute(mkdir, syslinux_target)
1293
1294     grub_target = target + "/boot/grub/"
1295     execute(mkdir, grub_target)
1296
1297     logo = search_file("logo.16", iso_mount, required=True)
1298     exec_rsync(logo, syslinux_target + "logo.16")
1299
1300     bootx64_efi = search_file("bootx64.efi", iso_mount)
1301     if bootx64_efi:
1302         mkdir(target + "/efi/boot/")
1303         exec_rsync(bootx64_efi, target + "/efi/boot/bootx64.efi")
1304
1305     efi_img = search_file("efi.img", iso_mount)
1306     if efi_img:
1307         mkdir(target + "/boot/")
1308         exec_rsync(efi_img, target + "/boot/efi.img")
1309         handle_secure_boot(target, efi_img)
1310
1311     for ffile in ["f%d" % number for number in range(1, 11)]:
1312         search_and_copy(ffile, iso_mount, syslinux_target + ffile)
1313
1314     # avoid the "file is read only, overwrite anyway (y/n) ?" question
1315     # of mtools by syslinux ("mmove -D o -D O s:/ldlinux.sys $target_file")
1316     if os.path.isfile(syslinux_target + "ldlinux.sys"):
1317         os.unlink(syslinux_target + "ldlinux.sys")
1318
1319     (source_dir, name) = get_defaults_file(iso_mount, grml_flavour, "default.cfg")
1320     (source_dir, defaults_file) = get_defaults_file(iso_mount, grml_flavour, "grml.cfg")
1321
1322     if not source_dir:
1323         raise CriticalException(
1324             "file default.cfg could not be found.\n"
1325             "Note:  this grml2usb version requires an ISO generated by grml-live >=0.9.24 ...\n"
1326             "       ... either use grml releases >=2009.10 or switch to an older grml2usb version."
1327         )
1328
1329     if not os.path.exists(iso_mount + "/boot/grub/footer.cfg"):
1330         logging.warning(
1331             "Warning: Grml releases older than 2011.12 support only one flavour in grub."
1332         )
1333
1334     for expr in (
1335         name,
1336         "distri.cfg",
1337         defaults_file,
1338         "grml.png",
1339         "hd.cfg",
1340         "isolinux.cfg",
1341         "isolinux.bin",
1342         "isoprompt.cfg",
1343         "options.cfg",
1344         "prompt.cfg",
1345         "vesamenu.cfg",
1346         "grml.png",
1347         "*.c32",
1348     ):
1349         glob_and_copy(iso_mount + source_dir + expr, syslinux_target)
1350
1351     for modules_dir in options.syslinuxlibs + SYSLINUX_LIBS:
1352         if os.path.isdir(modules_dir):
1353             for filename in glob.glob1(syslinux_target, "*.c32"):
1354                 copy_if_exist(os.path.join(modules_dir, filename), syslinux_target)
1355             break
1356
1357     # copy the addons_*.cfg file to the new syslinux directory
1358     glob_and_copy(iso_mount + source_dir + "addon*.cfg", syslinux_target)
1359
1360     search_and_copy(
1361         "hidden.cfg", iso_mount + source_dir, syslinux_target + "new_" + "hidden.cfg"
1362     )
1363
1364     # copy all grub files from ISO
1365     glob_and_copy(iso_mount + "/boot/grub/*", grub_target)
1366
1367     # finally (after all GRUB files have been installed) build static loopback.cfg
1368     build_loopbackcfg(target)
1369
1370
1371 def install_iso_files(grml_flavour, iso_mount, device, target):
1372     """Copy files from ISO to given target
1373
1374     @grml_flavour: name of grml flavour the configuration should be generated for
1375     @iso_mount: path where a grml ISO is mounted on
1376     @device: device/partition where bootloader should be installed to
1377     @target: path where grml's main files should be copied to"""
1378
1379     global GRML_DEFAULT
1380     GRML_DEFAULT = GRML_DEFAULT or grml_flavour
1381     if options.dryrun:
1382         return 0
1383     elif not options.bootloaderonly:
1384         logging.info("Copying files. This might take a while....")
1385         try:
1386             copy_system_files(grml_flavour, iso_mount, target)
1387             copy_grml_files(grml_flavour, iso_mount, target)
1388         except CriticalException as error:
1389             logging.critical("Execution failed: %s", error)
1390             sys.exit(1)
1391
1392     if not options.skipaddons:
1393         if not search_file("addons", iso_mount):
1394             logging.info("Could not find addons, therefore not installing.")
1395         else:
1396             copy_addons(iso_mount, target)
1397
1398     if not options.copyonly:
1399         copy_bootloader_files(iso_mount, target, grml_flavour)
1400
1401         if not options.dryrun:
1402             handle_bootloader_config(grml_flavour, device, target)
1403
1404     # make sure we sync filesystems before returning
1405     logging.info("Synching data (this might take a while)")
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             set_rw(device)
1885             mount(device, device_mountpoint, ["-o", "utf8,iocharset=iso8859-1"])
1886         except CriticalException:
1887             mount(device, device_mountpoint, "")
1888     try:
1889         grml_flavours = identify_grml_flavour(mountpoint)
1890         for flavour in set(grml_flavours):
1891             if not flavour:
1892                 logging.warning("No valid flavour found, please check your iso")
1893             logging.info('Identified grml flavour "%s".', flavour)
1894             install_iso_files(flavour, mountpoint, device, device_mountpoint)
1895             GRML_FLAVOURS.add(flavour)
1896     finally:
1897         if remove_device_mountpoint:
1898             remove_mountpoint(device_mountpoint)
1899
1900
1901 def remove_mountpoint(mountpoint):
1902     """remove a registered mountpoint
1903     """
1904
1905     try:
1906         unmount(mountpoint, "")
1907         if os.path.isdir(mountpoint):
1908             os.rmdir(mountpoint)
1909             unregister_tmpfile(mountpoint)
1910     except CriticalException:
1911         cleanup()
1912         raise
1913
1914
1915 def handle_mbr(device):
1916     """Main handler for installing master boot record (MBR)
1917
1918     @device: device where the MBR should be installed to"""
1919
1920     if options.dryrun:
1921         logging.info("Would install MBR")
1922         return 0
1923
1924     mbr_device, partition_number = get_device_from_partition(device)
1925     if partition_number is None:
1926         logging.warn("Could not detect partition number, not activating partition")
1927
1928     # if we get e.g. /dev/loop1 as device we don't want to put the MBR
1929     # into /dev/loop of course, therefore use /dev/loop1 as mbr_device
1930     if mbr_device == "/dev/loop":
1931         mbr_device = device
1932         logging.info(
1933             "Detected loop device - using %s as MBR device therefore", mbr_device
1934         )
1935
1936     mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1937     if options.syslinuxmbr:
1938         mbrcode = ""
1939         mbr_locations = (
1940             "/usr/lib/syslinux/mbr.bin",
1941             "/usr/lib/syslinux/bios/mbr.bin",
1942             "/usr/share/syslinux/mbr.bin",
1943         )
1944         for mbrpath in mbr_locations:
1945             if os.path.isfile(mbrpath):
1946                 mbrcode = mbrpath
1947                 break
1948
1949         if not mbrcode:
1950             str_locations = " or ".join(['"%s"' % l for l in mbr_locations])
1951             logging.error("Cannot find syslinux MBR, install it at %s)", str_locations)
1952             raise CriticalException(
1953                 "syslinux MBR  can not be found at %s." % str_locations
1954             )
1955     elif options.mbrmenu:
1956         mbrcode = GRML2USB_BASE + "/mbr/mbrldr"
1957
1958     try:
1959         install_mbr(mbrcode, mbr_device, partition_number, True)
1960     except (IOError, Exception) as error:
1961         logging.critical("Execution failed: %s", error)
1962         sys.exit(1)
1963
1964
1965 def handle_vfat(device):
1966     """Check for FAT specific settings and options
1967
1968     @device: device that should checked / formated"""
1969
1970     # make sure we have mkfs.vfat available
1971     if options.fat16:
1972         if not which("mkfs.vfat") and not options.copyonly and not options.dryrun:
1973             logging.critical("Sorry, mkfs.vfat not available. Exiting.")
1974             logging.critical("Please make sure to install dosfstools.")
1975             sys.exit(1)
1976
1977         if options.force:
1978             print("Forcing mkfs.fat16 on %s as requested via option --force." % device)
1979         else:
1980             # make sure the user is aware of what he is doing
1981             f = input(
1982                 "Are you sure you want to format the specified partition with fat16? y/N "
1983             )
1984             if f == "y" or f == "Y":
1985                 logging.info(
1986                     "Note: you can skip this question using the option --force"
1987                 )
1988             else:
1989                 sys.exit(1)
1990         try:
1991             mkfs_fat16(device)
1992         except CriticalException as error:
1993             logging.critical("Execution failed: %s", error)
1994             sys.exit(1)
1995
1996     # check for vfat filesystem
1997     if device is not None and not os.path.isdir(device) and options.syslinux:
1998         try:
1999             check_for_fat(device)
2000         except CriticalException as error:
2001             logging.critical("Execution failed: %s", error)
2002             sys.exit(1)
2003
2004     if options.skipusbcheck:
2005         logging.info(
2006             "Not checking for removable USB device as requested via option --skip-usb-check."
2007         )
2008         return
2009
2010     if (
2011         not os.path.isdir(device)
2012         and not check_for_usbdevice(device)
2013         and not options.force
2014     ):
2015         print(
2016             "Warning: the specified device %s does not look like a removable usb device."
2017             % device
2018         )
2019         f = input("Do you really want to continue? y/N ")
2020         if f.lower() != "y":
2021             sys.exit(1)
2022
2023
2024 def handle_compat_warning(device):
2025     """Backwards compatible checks
2026
2027     @device: device that should be checked"""
2028
2029     # make sure we can replace old grml2usb script and warn user when using old way of life:
2030     if (
2031         device.startswith("/mnt/external")
2032         or device.startswith("/mnt/usb")
2033         and not options.force
2034     ):
2035         print("Warning: the semantics of grml2usb has changed.")
2036         print("Instead of using grml2usb /path/to/iso %s you might" % device)
2037         print("want to use grml2usb /path/to/iso /dev/... instead.")
2038         print("Please check out the grml2usb manpage for details.")
2039         f = input("Do you really want to continue? y/N ")
2040         if f.lower() != "y":
2041             sys.exit(1)
2042
2043
2044 def handle_logging():
2045     """Log handling and configuration"""
2046
2047     if options.verbose and options.quiet:
2048         parser.error("please use either verbose (--verbose) or quiet (--quiet) option")
2049
2050     FORMAT = "%(message)s"
2051     if options.verbose:
2052         FORMAT = "%(asctime)-15s %(message)s"
2053         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
2054     elif options.quiet:
2055         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
2056     else:
2057         logging.basicConfig(level=logging.INFO, format=FORMAT)
2058
2059
2060 def handle_bootloader(device):
2061     """wrapper for installing bootloader
2062
2063     @device: device where bootloader should be installed to"""
2064
2065     # Install bootloader only if not using the --copy-only option
2066     if options.copyonly:
2067         logging.info(
2068             "Not installing bootloader and its files as requested via option copyonly."
2069         )
2070     elif os.path.isdir(device):
2071         logging.info("Not installing bootloader as %s is a directory.", device)
2072     else:
2073         install_bootloader(device)
2074
2075
2076 def check_options(opts):
2077     """Check compatibility of provided user opts
2078
2079     @opts option dict from OptionParser
2080     """
2081     if opts.grubmbr and not opts.grub:
2082         raise CriticalException("--grub-mbr requires --grub option.")
2083
2084     if opts.copyonly and opts.grub:
2085         raise CriticalException("Cannot use --copy-only and --grub at the same time.")
2086
2087
2088 def check_programs():
2089     """check if all needed programs are installed"""
2090     if options.grub:
2091         global GRUB_INSTALL
2092         GRUB_INSTALL = which("grub-install") or which("grub2-install")
2093         if not GRUB_INSTALL:
2094             logging.critical(
2095                 "Fatal: grub-install not available (please install the "
2096                 "grub package or drop the --grub option)"
2097             )
2098             sys.exit(1)
2099
2100     if options.syslinux:
2101         if not which("syslinux"):
2102             logging.critical(
2103                 "Fatal: syslinux not available (please install the "
2104                 "syslinux package or use the --grub option)"
2105             )
2106             sys.exit(1)
2107
2108     if not which("rsync"):
2109         logging.critical("Fatal: rsync not available, can not continue - sorry.")
2110         sys.exit(1)
2111
2112
2113 def load_loop():
2114     """Runs modprobe loop and throws away its output"""
2115     if not which("modprobe"):
2116         logging.critical("Fatal: modprobe not available, can not continue - sorry.")
2117         logging.critical("Hint: is /sbin missing in PATH?")
2118         sys.exit(1)
2119
2120     proc = subprocess.Popen(
2121         ["modprobe", "loop"], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2122     )
2123     proc.communicate()
2124
2125
2126 def main():
2127     """Main invocation"""
2128
2129     try:
2130         if options.version:
2131             print(os.path.basename(sys.argv[0]) + " " + PROG_VERSION)
2132             sys.exit(0)
2133
2134         if len(args) < 2:
2135             parser.error("invalid usage")
2136
2137         # log handling
2138         handle_logging()
2139
2140         # make sure we have the appropriate permissions
2141         check_uid_root()
2142
2143         check_options(options)
2144
2145         load_loop()
2146
2147         logging.info("Executing grml2usb version %s", PROG_VERSION)
2148
2149         if options.dryrun:
2150             logging.info("Running in simulation mode as requested via option dry-run.")
2151
2152         check_programs()
2153
2154         # specified arguments
2155         device = os.path.realpath(args[len(args) - 1])
2156         isos = args[0 : len(args) - 1]
2157
2158         if not os.path.isdir(device):
2159             if device[-1:].isdigit():
2160                 if int(device[-1:]) > 4 or device[-2:].isdigit():
2161                     logging.warn(
2162                         "Warning: installing on partition number >4, booting *might* fail depending on your system."
2163                     )
2164
2165         # provide upgrade path
2166         handle_compat_warning(device)
2167
2168         if not options.skipbootflag:
2169             check_boot_flag(device)
2170
2171         # check for vfat partition
2172         handle_vfat(device)
2173
2174         # main operation (like installing files)
2175         for iso in isos:
2176             install(iso, device)
2177
2178         # install mbr
2179         is_superfloppy = not device[-1:].isdigit()
2180         if is_superfloppy:
2181             logging.info("Detected superfloppy format - not installing MBR")
2182
2183         if not options.skipmbr and not os.path.isdir(device) and not is_superfloppy:
2184             handle_mbr(device)
2185
2186         handle_bootloader(device)
2187
2188         logging.info(
2189             "Note: grml flavour %s was installed as the default booting system.",
2190             GRML_DEFAULT,
2191         )
2192
2193         for flavour in GRML_FLAVOURS:
2194             logging.info(
2195                 "Note: you can boot flavour %s using '%s' on the commandline.",
2196                 flavour,
2197                 flavour,
2198             )
2199
2200         # finally be polite :)
2201         logging.info(
2202             "Finished execution of grml2usb (%s). Have fun with your Grml system.",
2203             PROG_VERSION,
2204         )
2205
2206     except Exception as error:
2207         logging.critical("Fatal: %s", str(error))
2208         if options.verbose:
2209             logging.exception("Exception:")
2210         sys.exit(1)
2211
2212
2213 if __name__ == "__main__":
2214     try:
2215         main()
2216     except KeyboardInterrupt:
2217         logging.info("Received KeyboardInterrupt")
2218         cleanup()
2219
2220 # END OF FILE ##################################################################
2221 # vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8