#!/usr/bin/env python # # media_common.py # # (c) Copyright 2009 - 2010 Michael Towers (larch42 at googlemail dot com) # # This file is part of the larch project. # # larch is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # larch is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with larch; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # #---------------------------------------------------------------------------- # 2010.07.14 """This code is used by both iso generator and partition creator modules. The larch medium creation stage first uses the Medium class to prepare the bootloader directories. build_iso and build_partition can also be used to rebuild media starting from existing larch media, e.g. for conversions from CD to usb-stick and vice-versa. Even a switch of bootloader is possible as the bootloader configs for both GRUB and syslinux/isolinux are normally included on the medium. If not using a larch installation directory, the bootloader and/or mkisofs must be supported by the host (according to medium and bootloader). """ import os from config import * from backend import * class Medium: """This class represents a boot medium image. It checks the options for conflicts. If necessary (no -S option provided on the command line) it converts a larchified system to a boot medium image, by preparing the bootloader directories and adding customization stuff from the profile, but it does not write to any medium. """ def __init__(self, options): self.options = options if options.setdir: try: wd = os.path.realpath(options.setdir) os.chdir(wd) except: errout(_("Couldn't switch directory to '%s'") % wd) self.iso = False self.installation_dir = get_installation_dir() self.installation0 = (self.installation_dir if self.installation_dir != '/' else '') # Check options if options.chroot and options.nochroot: errout(_("Option -C conflicts with -c")) if options.source: # Using a specified source image if options.profile: errout(_("Option -S conflicts with -p")) # Use chroot? self.chrootpath = self.installation0 if options.chroot else '' else: # Using the larch build system # Use chroot? self.chrootpath = '' if options.nochroot else self.installation0 # Mount point for source, if necessary self.mps = self.chrootpath + SOURCEMOUNT runcmd('rm -rf %s' % self.mps) runcmd('mkdir -p %s' % self.mps) # Create a temporary work area self.build = self.chrootpath + BUILD0 runcmd('rm -rf %s' % self.build) runcmd('mkdir -p %s' % self.build) if options.source: # Location of medium self.medium_dir = self._get_source(options.source, options.syslinux, self.chrootpath, test=options.testmedium) else: # Location for the live medium image self.medium_dir = self.installation0 + CHROOT_DIR_MEDIUM self._check_larchimage(self.medium_dir) self.profile_dir = get_profile() self._bootdir(options.syslinux) self._customizelarchdir() # The boot directory needs to be outside of the medium directory # for some of the next steps runcmd('cp -a %s/boot %s' % (self.medium_dir, self.build)) def unmount(self): if self.medium_dir == self.mps: unmount(self.mps) if self.iso: if self.chrootpath: unmount(self.chrootpath + ISOBUILDMNT) # Change owner of iso to match current directory cwdstat = os.stat(os.getcwd()) os.lchown(self.iso, cwdstat.st_uid, cwdstat.st_gid) def get_device_label(self, device): l = runcmd('blkid -c /dev/null -o value -s LABEL %s' % device)[1][0] if self.options.detection not in detection_methods.split('|'): errout(_("Invalid detection method (-d option)")) return l def chroot_medium_dir(self): """Get the medium path relative within the (possible) chroot. """ return self.medium_dir[len(self.chrootpath):] def iso_path(self): """If building an iso within a chroot the build directory must be bind-mounted into the accessible area. """ self.iso = self.options.isofile.replace('/', '_') basedir = os.getcwd() if self.chrootpath: runcmd('mkdir -p %s' % (self.chrootpath + ISOBUILDMNT)) if not mount(basedir, self.chrootpath + ISOBUILDMNT, '--bind'): errout(_("Couldn't bind-mount current directory")) basedir = ISOBUILDMNT return os.path.join(basedir, self.iso) def _check_larchimage(self, mediumdir): testfile = mediumdir + '/larch/system.sqf' if not os.path.isfile(testfile): errout(_("File '%s' doesn't exist, '%s' is not a larch medium") % (testfile, mediumdir)) def _get_source(self, source, syslinux, ipath='', test=False): source = os.path.realpath(source) testcode = 64 if test else 0 # For various tests, below # Is source an iso? if os.path.isfile(source): # Mount it if mount(source, self.mps, '-o loop'): source = self.mps else: errout(_("Couldn't mount '%s'. Not an iso?") % source) # Is source a device? elif source.startswith('/dev/'): mp = _get_mountpoint(source) if mp: source = mp elif mount(source, self.mps): source = self.mps else: errout(_("Couldn't mount '%s'") % source) # Simple check that it is really a larch image self._check_larchimage(source) # Check bootloader support if not (os.path.isdir(source + '/boot/isolinux') or os.path.isdir(source + '/boot/syslinux')): if test or syslinux: errout(_("Source doesn't support syslinux"), 0) testcode += 8 if not os.path.isdir(source + '/boot/grub'): if test or not syslinux: errout(_("Source doesn't support GRUB"), 0) testcode += 16 if testcode: if os.path.isfile(source + 'larch/nosave'): testcode += 2 unmount() sys.exit(testcode) return None # Is source within the chroot directory? if ipath and not source.startswith(ipath + '/'): if mount(source, self.mps, '--bind'): source = self.mps else: errout(_("Couldn't bind-mount '%s'") % source) # Copy boot files runcmd('cp -a %s/boot %s' % (source, self.build)) # Remove old config files runcmd('rm -f %s/boot/syslinux/syslinux.cfg' % self.build) runcmd('rm -f %s/boot/isolinux/isolinux.cfg' % self.build) runcmd('rm -f %s/boot/grub/menu.lst' % self.build) runcmd('rm -f %s/boot/isolinux/isolinux.bin' % self.build) runcmd('rm -f %s/boot/isolinux/isolinux.boot' % self.build) # Rename the syslinux boot directory if necessary if os.path.isdir(self.build + '/boot/syslinux'): runcmd('mv %s/boot/syslinux %s/boot/isolinux' % (self.build, self.build)) return source def _bootdir(self, syslinux): """Prepare boot directories for the various bootloaders. The bootloader configuration files are not generated yet, as these depend on the medium. """ comment("Fetch kernel and initramfs") if not runcmd('cp -r %s/boot %s' % (self.medium_dir, self.build))[0]: errout(_("No kernel and/or initramfs")) comment("Preparing bootloader directories") # Only include the directories if the corresponding bootloader # package ('grub' and/or 'syslinux') is installed grub = os.path.isfile(self.installation0 + GRUBDIR + '/stage2_eltorito') isolinux = os.path.isfile(self.installation0 + SYSLINUXDIR + '/isolinux.bin') # Check bootloader support if syslinux: if not isolinux: errout(_("Installation doesn't support syslinux")) elif not grub: errout(_("Installation doesn't support GRUB")) # Bootloader independent files are provided in larch at # cd-root/boot0. The contents of this directory are placed in the # medium's 'boot' directory. # Individual files can be added or substituted by # supplying them in the profile at cd-root/boot. # It is also possible to completely replace the basic boot directory # by having cd-root/boot0 in the profile - then the default # larch version will not be used. source0 = '%s/cd-root/boot0' % self.profile_dir if not os.path.isdir(source0): source0 = '%s/cd-root/boot0' % base_dir runcmd('bash -c "cp -r %s/* %s/boot"' % (source0, self.build)) # Copy any additional profile stuff psource = '%s/cd-root/boot' % self.profile_dir if os.path.isdir(psource): runcmd('cp -rf %s/* %s' % (psource, self.build)) # Get the boot options file bootlines = self.profile_dir + '/bootlines' if not os.path.isfile(bootlines): bootlines = base_dir + '/data/bootlines' runcmd('cp -f %s %s/boot' % (bootlines, self.build)) # The idea is that a basic boot directory for each boot-loader is # provided in larch at cd-root/{grub0,isolinux0}. These are copied # to the medium as 'boot/grub' and 'boot/isolinux'. Individual files # can be added or substituted by supplying them in the profile at # cd-root/{grub,isolinux}. # It is also possible to completely replace the basic boot directory # by having cd-root/{grub0,isolinux0} in the profile - then the default # larch versions will not be used. for ok, boot_dir in ((grub, 'grub'), (isolinux, 'isolinux')): if ok: # Copy bootloader specific stuff source0 = '%s/cd-root/%s0' % (self.profile_dir, boot_dir) if not os.path.isdir(source0): source0 = '%s/cd-root/%s0' % (base_dir, boot_dir) runcmd('cp -rf %s %s/boot/%s' % (source0, self.build, boot_dir)) # Copy any additional profile stuff psource = '%s/cd-root/%s' % (self.profile_dir, boot_dir) if os.path.isdir(psource): runcmd('cp -rf %s %s/boot' % (psource, self.build)) # Copy the grub boot files to the medium's grub directory # and rename base config file if grub: runcmd('bash -c "cp %s/* %s/boot/grub"' % (self.installation0 + GRUBDIR, self.build)) runcmd('mv %s/boot/grub/menu.lst %s/boot/grub/menu.lst_0' % (self.build, self.build)) # Copy mbr.bin, vesamenu.c32 and isolinux.bin (renamed) # to the boot directory and rename base config file if isolinux: runcmd('cp %s/mbr.bin %s/boot/isolinux' % (self.installation0 + SYSLINUXDIR, self.build)) runcmd('cp %s/vesamenu.c32 %s/boot/isolinux' % (self.installation0 + SYSLINUXDIR, self.build)) runcmd('cp %s/isolinux.bin %s/boot/isolinux/isolinux.bin0' % (self.installation0 + SYSLINUXDIR, self.build)) runcmd(('mv %s/boot/isolinux/isolinux.cfg' ' %s/boot/isolinux/isolinux.cfg_0') % (self.build, self.build)) def _customizelarchdir(self): # Replace any existing larch/copy directory runcmd('rm -rf %s/larch/copy' % self.medium_dir) if os.path.isdir(self.profile_dir + '/cd-root/larch/copy'): runcmd('cp -r %s/cd-root/larch/copy %s/larch' % (self.profile_dir, self.medium_dir)) # Replace any existing larch/extra directory runcmd('rm -rf %s/larch/extra' % self.medium_dir) if os.path.isdir(self.profile_dir + '/cd-root/larch/extra'): runcmd('cp -r %s/cd-root/larch/extra %s/larch' % (self.profile_dir, self.medium_dir)) # Handle existence of larch/nosave runcmd('rm -f %s/larch/nosave' % self.medium_dir) if os.path.isfile(self.profile_dir + '/nosave'): runcmd('cp %s/nosave %s/larch' % (self.profile_dir, self.medium_dir)) def _get_mountpoint(path): ok, lines = runcmd('cat /etc/mtab') for l in lines: ll = l.split() if (len(ll) > 4) and (ll[1] == path): return ll[0] return '' def check_label(l, n): if isinstance(l, unicode): l = l.encode('utf8') if len(l) > n: if query_yn(_("The volume label is too long. Use the default (%s)?") % LABEL): return LABEL else: errout(_("Cancelled")) return l def add_larchboot(idir): writefile("The presence of the file 'larch/larchboot' enables\n" "booting the device in 'search' mode.\n", idir + '/larch/larchboot') def bootconfig(medium, syslinux, label='', device='', detection=''): """Convert and complete the bootlines file. """ # - add boot partition to options if detection == 'uuid': bootp = ('uuid=' + runcmd('blkid -c /dev/null -o value -s UUID %s' % device)[1][0].strip()) elif detection == 'label': if not label: errout(_("Can't boot to label - device has no label")) bootp = 'label=' + label elif detection == 'partition': bootp = 'root=' + device else: bootp = '' # - convert bootfiles to the correct format, # inserting necessary info bootconf = medium + '/boot/bootlines' if not os.path.isfile(bootconf): errout(_("Boot configuration file '%s' not found") % bootconf) fhi = open(bootconf) insert = '' i = 0 block = '' title = '' opts = '' for line in fhi: line = line.strip() if not line: if title: i += 1 # A block is ready if syslinux: # isolinux/syslinux block += 'label %02d\n' % i block += 'MENU LABEL %s\n' % title block += 'kernel /boot/larch.kernel\n' block += ('append initrd=/boot/larch.img %s %s\n' % (bootp, opts)) else: # GRUB block += 'title %s\n' % title block += ('kernel /boot/larch.kernel %s %s\n' % (bootp, opts)) block += 'initrd /boot/larch.img\n' if i > 1: insert += '\n' insert += block block = '' title = '' opts = '' elif line.startswith('comment:'): block += '#%s\n' % (line.split(':', 1)[1]) elif line.startswith('title:'): title = line.split(':', 1)[1].lstrip() elif line.startswith('options:'): opts = line.split(':', 1)[1].lstrip() fhi.close() # - insert the resulting string into the bootloader config file if syslinux: boot_dir = ('isolinux' if os.path.isdir(medium + '/boot/isolinux') else 'syslinux') configfile0 = 'isolinux.cfg_0' configfile = boot_dir + '.cfg' else: boot_dir = 'grub' configfile0 = 'menu.lst_0' configfile = 'menu.lst' configpath = '%s/boot/%s/%s' % (medium, boot_dir, configfile0) if not os.path.isfile(configpath): errout(_("Base configuration file (%s) not found") % configpath) fhi = open(configpath) fho = open('%s/boot/%s/%s' % (medium, boot_dir, configfile), 'w') for line in fhi: if line.startswith("###LARCH"): fho.write(insert) else: fho.write(line) fhi.close() fho.close()