DISCLAIMER. English language used here only for compatibility (ASCII only), so any suggestions about my bad grammar (and not only it) will be greatly appreciated.

среда, 18 июля 2012 г.

Multiboot with Grub 2. Do it the hard way ;-)

Upd1. Various small fixes.
Upd2. Change layout.

Table of content:
    A. Notes about grub-install.
    B. Notes about update-grub.
    C. Other notes.
    D. Multiboot schemes.
    E. Scripts and config examples.


A. grub-install {{{

I'll refer to following variable names from grub-install:
    - $bootdir (/boot by default) is specified by --boot-directory option and
      used only for determining $grubdir.
    - $grubdir is $bootdir/grub.
    - $grubd_drive is drive (in grub notation), where $grubdir resides, and
      $grub_partition is corresponding partition (in grub notation).
    - $install_device is system device (e.g. /dev/sda), where grub will be
      installed.
    - $install_drive is drive (in grub notation) of $install_device.

and assume following layout in the examples below:

    # sfdisk -l /dev/sda
    ..
       Device Boot Start     End   #cyls    #blocks   Id  System
    ..
    /dev/sda2       3648+   3713-     66-    524288   83  Linux
    ..
    # grep /proc/mounts  -esda2
    /dev/sda2 /boot ext2 rw,relatime 0 0

    # pvs
    PV         VG            Fmt  Attr PSize   PFree 
    /dev/sda3  shilvana_sys  lvm2 a-    60.00g  6.00g
    ..
    # grep /proc/mounts  -esys-root
    /dev/mapper/shilvana_sys-root / ext4 rw,nodev,relatime,barrier=1,journal_checksum,data=ordered 0 0

Then grub-install will do:
    - create (or recreate, if requested) device map file.
    - copy *.mod, *.img and other image files into $grubdir.
    - create environment file ($grubdir/grubenv).
    - determine modules needed to access $grubdir:
 - disk_module,
 - fs_module,
 - partmap_module,
 - abstraction_module (e.g. lvm), if any.
    - determine prefix - an absolute filename (including drive in grub
      notation) to $grubdir. It is used by insmod command for finding modules
      to load.

      If $grubdir has no abstraction (e.g. lvm) and {{{
        - if $grubdir resides on $install_drive ($grub_drive equals to
          $install_drive), grub-install sets prefix to $grub_drive 's
          partition (without drive speicifcation) followed by relative (to
          $grub_drive 's fs root) path to $grubdir. In other words,
          grub-install assumes, that partition number (but not bios drive
          number!) is reliable enough, and because $grubdir is on the same
          drive grub will boot from, there is no need to specify drive itself.

     # grub-install --boot-directory /boot/test_install /dev/sda
     Stopping before actual write
     Grub dir and device: '/boot/test_install/grub' - '/dev/sda2'
     Modules to access grub device (disk, fs, partmap, abstraction): ' biosdisk ext2  part_msdos '
     Grub drive, partition and uuid (for cross-disk install): '(hd0)' - 'msdos2' - ''
     Install device and drive: '/dev/sda' - '(hd0)'
     Prefix drive: '(,msdos2)'
     Want to execute next (no actual run):
     /usr/bin/grub-mkimage -d /usr/lib/grub/i386-pc -O i386-pc --output=/boot/test_install/grub/core.img --prefix=(,msdos2)/test_install/grub biosdisk ext2 part_msdos
     /usr/sbin/grub-setup --directory=/boot/test_install/grub --device-map=/boot/test_install/grub/device.map /dev/sda
     Installation finished. No error reported.

        - if drives are not equal (cross disk install - $grubdir is not on
          $install_drive), grub-install embeds config, which finds $grubdir by
          uuid and then sets prefix. Embedded config is in $grubdir/load.cfg
          file. In other words, grub-install does not trust to bios drive
          numbers and uses uuids to search. Note, also, that, when installing
          to MBR gap, install device should contain valid mbr and partition
          table, and partition should have Id set (e.g. to 83). Otherwise,
          such

     /usr/sbin/grub-setup: error: unable to identify a filesystem in hd1; safety check can't be performed.

   or such

     /usr/sbin/grub-setup: error: embedding is not possible, but this is required for cross-disk install.

   errors are possible. Here is correct cross-disk install log:

     # grub-install --boot-directory /boot/test_install /dev/sdb
     Grub cross-disk install
     Stopping before actual write
     Grub dir and device: '/boot/test_install/grub' - '/dev/sda2'
     Modules to access grub device (disk, fs, partmap, abstraction): ' biosdisk ext2  part_msdos  search_fs_uuid'
     Grub drive, partition and uuid (for cross-disk install): '(hd0)' - 'msdos2' - '4d2154ef-97ef-4513-94bf-2d6580056abf'
     Install device and drive: '/dev/sdb' - '(hd1)'
     Prefix drive: ''
     Want to execute next (no actual run):
     /usr/bin/grub-mkimage -c /boot/test_install/grub/load.cfg -d /usr/lib/grub/i386-pc -O i386-pc --output=/boot/test_install/grub/core.img --prefix=/test_install/grub biosdisk ext2 part_msdos search_fs_uuid
     /usr/sbin/grub-setup --directory=/boot/test_install/grub --device-map=/boot/test_install/grub/device.map /dev/sdb
     Installation finished. No error reported.
     # cat /boot/test_install/grub/load.cfg 
     search.fs_uuid 4d2154ef-97ef-4513-94bf-2d6580056abf root 
     set prefix=($root)/test_install/grub
      }}}
      If $grubdir has abstraction (e.g. lvm), {{{
      grub-install in either of the above cases (single-disk or cross-disk
      install) just sets prefix to the appropriate $grub_drive. In other
      words, grub-install entrusts to the abstraction layer to uniquely
      identify correct drive. For single-disk case:

     # grub-install --boot-directory /root/grub/test_install /dev/sda
     Stopping before actual write
     Grub dir and device: '/root/grub/test_install/grub' - '/dev/mapper/shilvana_sys-root'
     Modules to access grub device (disk, fs, partmap, abstraction): ' biosdisk ext2  part_msdos lvm '
     Grub drive, partition and uuid (for cross-disk install): '' - '' - ''
     Install device and drive: '/dev/sda' - ''
     Prefix drive: '(shilvana_sys-root)'
     Want to execute next (no actual run):
     /usr/bin/grub-mkimage -d /usr/lib/grub/i386-pc -O i386-pc --output=/root/grub/test_install/grub/core.img --prefix=(shilvana_sys-root)/root/grub/test_install/grub biosdisk ext2 part_msdos lvm
     /usr/sbin/grub-setup --directory=/root/grub/test_install/grub --device-map=/root/grub/test_install/grub/device.map /dev/sda
     Installation finished. No error reported.

      and for cross-disk case

     # grub-install --boot-directory /root/grub/test_install /dev/sdb
     Stopping before actual write
     Grub dir and device: '/root/grub/test_install/grub' - '/dev/mapper/shilvana_sys-root'
     Modules to access grub device (disk, fs, partmap, abstraction): ' biosdisk ext2  part_msdos lvm '
     Grub drive, partition and uuid (for cross-disk install): '' - '' - ''
     Install device and drive: '/dev/sdb' - ''
     Prefix drive: '(shilvana_sys-root)'
     Want to execute next (no actual run):
     /usr/bin/grub-mkimage -d /usr/lib/grub/i386-pc -O i386-pc --output=/root/grub/test_install/grub/core.img --prefix=(shilvana_sys-root)/root/grub/test_install/grub biosdisk ext2 part_msdos lvm
     /usr/sbin/grub-setup --directory=/root/grub/test_install/grub --device-map=/root/grub/test_install/grub/device.map /dev/sdb
     Installation finished. No error reported.
      }}}   

    - but grub-install does NOT generate any grub.cfg.

Here are some notes about grub's $root and $prefix variables (available in
grub shell during boot):
    - $root contain only drive name (in grub notation).
    - $prefix is an absolute filename of $grubdir (drive name followed by path
      relative to drive's fs root).
    - $grubdir (referred by $prefix) contains all data required by core.img to
      work properly and its location is hardcoded (?) (during grub-mkimage?)
      into the core.img.
    - Grub modules loaded from $prefix folder (i.e. $grubdir), not from $root.
      So, you can reset $root to something else and insmod will still works.
      Hence, when you're in grub rescue mode (grub can't find $grubdir and
      load normal module), you should first fix '$prefix' value, then you
      should be able to load normal module (and should load it). And then run
      normal command, and then (may be) set '$root' to something more
      convenient.
    - grub-install sets up only core.img functionality. It does not care about
      configuration (grub.cfg) in any way - configuration is grub-mkconfig
      duty.  So, grub-install does all you need to get normal grub environment
      (grub normal shell, not rescue shell) at the boot (in other words, to
      load normal module and run normal command), but it knows nothing about
      local OSes and how to boot them.

}}}
B. update-grub {{{

I'll use following terms:
    - grubdir is directory, where all grub modules and other stuff have
      installed by grub-install. It is usually /boot/grub, but may be set to
      'DIR/grub' using '--boot-directory DIR' option of grub-install.

update-grub just calls grub-mkconfig:

    # cat $(which update-grub)
    #!/bin/sh
    set -e
    exec grub-mkconfig -o /boot/grub/grub.cfg "$@"

grub-mkconfig will do
    - Check device map file (device.map) and create, if it does not exist.
    - Execute all scripts (with 'x' bit set) from /etc/grub.d. These scripts
      may access some other files from grubdir.  E.g. /etc/grub.d/00_header
      need video.lst, when generating load_video() function (if neither of
      video backends have been specified in GRUB_VIDEO_BACKEND from
      /etc/default/grub). Also 00_header tries to open localedir, and may try
      to open serial.mod, and may be more.. who knows?
    - Overwrite grub.cfg with new one.

Here is inotify log of update-grub run:

    # cp -a /mnt/boot/grub/* -t /boot/grub/
    # inotifywait -mr -e open  /boot/grub/ 2>&1 | tee grubdir_inotify.log
    # uniq update-grub_inotify.log 
    Setting up watches.  Beware: since -r was given, this may take a while!
    Watches established.
    /boot/grub/ OPEN device.map
    /boot/grub/ OPEN grub.cfg.new
    /boot/grub/ OPEN video.lst
    /boot/grub/ OPEN device.map
    /boot/grub/ OPEN,ISDIR 
    /boot/grub/ OPEN device.map
    /boot/grub/ OPEN grub.cfg.new

With different configuration (/etc/default/grub) and different scripts in
/etc/grub.d, files required for correct update-grub (grub-mkconfig) run may
differ. In other words, i can safely assume only, that "entire grubdir is
required for update-grub run".  This is important point, when designing proper
multiboot config.

}}}
C. Other notes {{{

Other notes about grub2.

Terminal output. {{{

Only two terminal outputs are suitable for doing something in grub's cmd:
console and vga_text. gfxterm even on low resolutions is very slow. Try

    grub> set pager=1
    grub> cat /some_not_too_little_file

and hold down the key (to scroll down by one line). And it most likely will
beep soon. As i understand, this means, that it can't refresh screen as fast
as input received (input buffer overflow?). So, to display boot menu gfxterm
may be suitable, but for working in grub's cmd - absolutely not.

List vbe modes

    grub> vbeinfo

test them

    grub> vbetest

switch to gfxterm

    grub> insmod vbe
    grub> insmod gfxterm
    grub> set gfxmode='640x480x32' # vbe to which switch to
    grub> terminal_output gfxterm

switch to other terminal output (e.g. console)

    grub> terminal_output console

}}}
When installing grub to MBR gap: {{{
    - boot.img is first sector (mbr sector itself).
    - diskboot.img is 2nd scetor (offset 0x200 bytes).
    - core.img contains diskboot.img as its first sector (so core.img starts
      from offset 0x200 as well) and continues up to.. well, further in the
      free space before first partition.

}}}
Some variables from default scripts in /etc/grub.d {{{

    - 'prev_saved_entry' environment variable (in grub's config part generated
      by 00_header script) is set by grub-reboot shell script. It is used to
      save previous 'saved_entry' value before overwriting it with new one.
      This is needed, because grub-reboot sets new 'saved_entry' for one
      (next) boot only, and grub.cfg should restore previous one after
      (during) next boot.
    - When OS root device is inside lvm, grub-mkconfig will not use UUIDs for
      'root=' kernel parameter (this is desired behavior, just confirm it).
      Code, which checks this (as well as GRUB_DISABLE_LINUX_UUID variable
      from /etc/default/grub), is in 10_linux script.

}}}

}}}
D. Multiboot schemes {{{

I'll use following terms:
    - grubdir is directory, where all grub modules and other stuff have
      installed by grub-install. It is usually /boot/grub, but may be set to
      'DIR/grub' using '--boot-directory DIR' option of grub-install.

Here suggested two schemes for booting several Linux systems with grub2. They
are designed to satisfy following requirments:
    - update-grub should work in all Linux systems and does not break
      anything.
    - No incorrect and useless menuentries, which often generated by
      /etc/grub.d/30_os-prober script.
    - New linux system should not ruin boot, if update-grub will accidently
      run on it with default config.
    - OS kernel and initrd should be stored on corresponding OS root
      partition. This is OS-dependent data and i don't want to store it in
      shared location (like shared boot partition). Also, if OS will be moved
      to some other computer, kernel and initrd will still be there.

Generally, there is two approaches to this problem:
    - One main config (grub.cfg), which have created and updated by hand (or
      some other method, but not by update-grub), and many OS-specific
      configs, which have generated and updated by update-grub from
      corresponding OS.  Main config should find and load OS-specific ones.
    - One merged config. Merge should occur during generation or update by
      update-grub from any OS.

Because each OS may run update-grub, second approach requires each OS to have
specific merge script in the /etc/grub.d, and if it is not there (for newly
installed OS), update-grub will overwrite grub.cfg making all other systems
unbootable. Hence, i'm not considering second approach further, and choosing
first one.

Main grub config should be stored on the shared boot partition, but
OS-specific ones may be stored on either shared boot partititon as well or on
OS root partition. I can't definitely say, that OS-specific grub config
belongs to OS or to grub. On the one hand, OS-specific config is generated
using OS-specific scripts from /etc/grub.d, and, hence, belongs to OS. But, on
the other hand, grub-mkconfig and scripts from /etc/grub.d may read files from
grubdir and make some choices depending on their content (and they actually
will), hence, OS-specific config depends on particular grub installation and
belongs to grub. In other words, OS-specific grub.cfg depends on both OS
configuration and bootloader features available.

So, i implement two layouts for both points of view on OS-specific grub
config.

1. OS-specific grub config stored on the OS root partition. {{{

Requirments:
    - grubdir should be accessible under /boot/grub, because grub-mkconfig
      hardcodes this path. (grub-mkconfig and some scripts from /etc/grub.d
      may need access to grubdir to check some files)
    - OS-specific grub.cfg should be stored in /boot/grub on OS root partition
      (not the same, as grubdir's partition). Note, that according to the
      definition at the beginning /boot/grub on the OS root is _not_ grubdir,
      because grub-install does not install anything into it. In other words,
      it's just a folder, and may be changed to any other.
    - main grub.cfg should be stored in grubdir on shared boot partition.

Mounts (order is significant!):
    /mnt/boot           - shared boot partition.
    /mnt/os_boot_grub   - bind of /boot/grub from OS root partition
                          (where to store OS-specific grub.cfg).
    /boot/grub          - bind of grubdir (/mnt/boot/grub). I.e. this is
                          grubdir, where update-grub expects to see it.

Notes:
    - I can't use symlink to grubdir: /boot/grub -> /mnt/boot/grub, because in
      this case /boot/grub directory on the OS root, where i want to store
      OS-specific grub config, will not exist at all.
    - Using symlinks, like /boot/grub/smth -> /mnt/boot/grub/smth, to every
      file (and folder) in /mnt/boot/grub is not a good idea, because, when
      content of grubdir have changed, i need to update such symlinks in every
      OS's /boot/grub (or similar folder). So, first of all i need to find all
      OSes root devices and corresponding folders on them.  This is not
      reliable, because i may either miss some OSes or different folders
      (instead of /boot/grub) in some OSes.
    - I bind only grubdir (.../grub directory on the shared boot partition),
      but not the whole shared boot partition, because i need to store kernel
      image and initrd, which usually placed in /boot, on the OS root
      partition.  Hence, /boot should still be on the OS root partition.

There is one thing left: update-grub tells grub-mkconfig to write result to
/boot/grub/grub.cfg, but since /boot/grub is bind of grubdir, this is not
appropriate place for OS-specific grub.cfg (it overwrites main grub.cfg, if
would be written there). OS-specific grub.cfg should be written to
/mnt/os_boot_grub/grub.cfg, but this requires change in update-grub script.  I
need to divert update-grub to other file, and put changed version in its
place.

Disadvantages of this implementation:
    - dpkg-divert (distibution-specific feature).
    - Because OS-specific grub.cfg depends on bootloader features configured
      (*.mods available, etc), placing it on the OS root partition may be not
      appropriate: if you move OS to another computer, which most likely will
      have bootloader (suppose still grub2) configured (by grub-install)
      differently, old grub.cfg may not work.

Here is complete walkthrough {{{

1. Divert update-grub.

    # dpkg-divert --add --rename --divert /usr/sbin/update-grub.distrib /usr/sbin/update-grub
    Adding 'local diversion of /usr/sbin/update-grub to /usr/sbin/update-grub.distrib'
    # ls -l /usr/sbin/update-grub*
    lrwxrwxrwx 1 root root 11 Jun 30 21:39 /usr/sbin/update-grub2 -> update-grub
    -rwxr-xr-x 1 root root 64 Jun 15 13:13 /usr/sbin/update-grub.distrib
    # dpkg-divert --list | grep grub
    local diversion of /usr/sbin/update-grub to /usr/sbin/update-grub.distrib

2. Create new update-grub, which writes result to /mnt/os_boot_grub/grub.cfg .

    # cat /usr/sbin/update-grub
    #!/bin/sh
    set -e
    exec grub-mkconfig -o /mnt/os_boot_grub/grub.cfg "$@"

3. Mount all properly.

    # grep /etc/fstab  -eboot
    LABEL=common_boot       /mnt/boot               ext2        defaults    0 2
    /boot/grub              /mnt/os_boot_grub       none        bind        0 0
    /mnt/boot/grub          /boot/grub              none        bind        0 0

4. Install grub, and tell grub-install where grubdir should be

    # grub-install --boot-directory /mnt/boot/ /dev/sda
    Installation finished. No error reported.

5. Generate (or update) OS-specific grub config.

    # update-grub
    Generating grub.cfg ...
    Found background image: /usr/share/images/desktop-base/desktop-grub.png
    Found linux image: /boot/vmlinuz-2.6.32-5-686-bigmem
    Found initrd image: /boot/initrd.img-2.6.32-5-686-bigmem
    Found Windows Vista (loader) on /dev/sda1
    Found Debian GNU/Linux (wheezy/sid) on /dev/mapper/shilvana_sys-testing_root
    done

And then you can find OS-specific grub.cfg

    # ls -l /mnt/os_boot_grub/
    total 4
    -r--r--r-- 1 root root 3512 Jul  5 15:58 grub.cfg

and can ensure, that it is on the correct device (OS root partition)

    # mountpoint -d /mnt/os_boot_grub/
    254:0
    # mountpoint -d /
    254:0

or, if you unmount binds,

    # umount /boot/grub /mnt/os_boot_grub 
    # ls -l /boot/
    total 6900
    -rw-r----- 1 root root  111601 Jul  3 15:19 config-2.6.32-5-686-bigmem
    drwxr-x--- 2 root root    4096 Jul  5 16:04 grub
    -rw-r--r-- 1 root root 3234993 Jul  5 16:04 initrd.img-2.6.32-5-686-bigmem
    -rw-r----- 1 root root 1326610 Jul  3 15:19 System.map-2.6.32-5-686-bigmem
    -rw-r----- 1 root root 2367872 Jul  3 15:19 vmlinuz-2.6.32-5-686-bigmem
    # ls -l /boot/grub/
    total 4
    -r--r--r-- 1 root root 3512 Jul  5 16:04 grub.cfg

6. Copy main grub.cfg to grubdir (/mnt/boot/grub). It should find OS root
   devices (using 'search --label' or 'search --fs-uuid') and load
   corresponding grub.cfg-s from them (using configfile).

}}}

}}}
2. OS-specific grub config stored in subfolder on the shared boot partition. {{{

Requirments:
    - grubdir should be accessible under /boot/grub, because grub-mkconfig
      hardcodes this path. (grub-mkconfig and some scripts from /etc/grub.d
      may need access to grubdir to check some files)
    - OS-specific grub.cfg should be stored in subfolder on shared boot
      partition (grubdir's partition).
    - main grub.cfg should be stored in grubdir on shared boot partition.

Structure of shared boot partition (mounted on /mnt/boot):
    /mnt/boot/common/grub   - grubdir .
    /mnt/boot/OS_name/grub  - location (folder) of OS-sepcific grub.cfg.

Mounts:
    /mnt/boot       - shared boot partition.
    /boot/grub      - bind of OS-specific grub.cfg location (folder)
                      (/mnt/boot/OS_name/grub).

And to make grubdir content accessible under /boot/grub (where update-grub
expects to see it), i should hardlink all files, _except_ grub.cfg, from
common/grub into all OS-specific locations 'OS_name/grub' on shared boot
partition.

Disadvantages of this implementation:
    - If you mess up with hardlinks, you can easily overwrite other OSes
      grub.cfg-s or main grub.cfg and end up with unbootable system. So, it's
      better to use script for updating hardlinks.
    - Because OS-specific grub.cfg is on the shared boot partition, if you
      move OS to another computer, you will not have any grub.cfg for this OS
      (even not working now, but which can give you some hints how to boot
      OS).

Here is complete walkthrough: {{{
1. Create grubdir and OS-specific grub.cfg locations on the shared boot
   partition

    # mkdir -p common stable_linux/grub testing_linux/grub

2. Install grub, and tell grub-install where grubdir should be

    # grub-install --boot-directory /mnt/boot/common /dev/sda
    Installation finished. No error reported.

3. Hardlink all files, except grub.cfg, from grubdir (common/grub) to
   OS-specific locations:

    # shopt -s extglob
    # cp -al common/grub/!(*.cfg) -t stable_linux/grub/

and check, that all is right (i have several grub.cfg-s in grubdir, so i
exclude them all during hardlinking):

    # find common/grub/ -links 1 
    common/grub/grub.cfg
    common/grub/grub_load_from_folder.cfg
    common/grub/grub2.cfg

Alternatively, you can use script for hardlinking (this is recommended).
Note, that grub-install recreates (removes and creates again) files in
grubdir, hence, all hardlinks must be recreated after each grub-install run.

4. Mount OS-specific grub.cfg location under /boot/grub (fstab):

    LABEL=common_boot               /mnt/boot       ext2    defaults  0 2
    /mnt/boot/stable_linux/grub     /boot/grub      none    bind      0 0

5. Generate (or update) OS-specific grub config.

    # update-grub
    Generating grub.cfg ...
    Found background image: /usr/share/images/desktop-base/desktop-grub.png
    Found linux image: /boot/vmlinuz-2.6.32-5-686-bigmem
    Found initrd image: /boot/initrd.img-2.6.32-5-686-bigmem
    Found Windows Vista (loader) on /dev/sda1
    Found Debian GNU/Linux (wheezy/sid) on /dev/mapper/shilvana_sys-testing_root
    done

and check, that all is right

    # ls -l  stable_linux/grub/grub.cfg 
    -r--r--r-- 1 root root 3525 Jul 16 18:42 stable_linux/grub/grub.cfg

6. Copy main grub.cfg to grubdir (/mnt/boot/common/grub). It should find
   OS-specific grub.cfg-s and load them (using configfile).

}}}

}}}

}}}
E. Examples. {{{

Here are some (working) examples.

E1. Example of main grub.cfg {{{

E1A. Functions {{{

function reset_os_args {
    # NOTE: This function must be called at the end of menuentry. Otherwise,
    # further selected menuentries (if any) may have mixed args (some from
    # previously selected menuentries).
    set OS_name='Undefined'
    # NOTE: Empty label matches with device without label, but until you use
    # search_os_root() this is not the problem, because it will not call
    # search with empty label. What about empty uuid? It matches with fs not
    # supporting uuids?
    set OS_root_uuid=''                     # required
    set OS_root_label=''                    # required
    set OS_hints=''
    set OS_boot_config='/boot/grub/grub.cfg'
    set OS_chainloader='+0'
    set OS_root_not_found='0'
    set OS_boot_not_found='0'
}

function search_os_root {
    echo "Searching for '$OS_name' root device.."
    set OS_root_not_found='0'
    if [ -n "$OS_root_uuid" ]; then
        if search --no-floppy --set=root --fs-uuid "$OS_root_uuid" $OS_hints; then
            return 0
        fi
        echo "UUID '$OS_root_uuid' not found. Press Enter."
    elif [ -n "$OS_root_label" ]; then
        if search --no-floppy --set=root --label "$OS_root_label" $OS_hints; then
            return 0
        fi
        echo "Label '$OS_root_label' not found. Press Enter."
    else
        echo "No label or uuid specified. Press Enter."
    fi
    read
    set OS_root_not_found='1'
    return 1
}

function load_os_boot_config {
    echo "Loading '$OS_name' boot config.."
    set OS_boot_not_found='0'
    if [ -n "$OS_boot_config" ]; then
        if [ -f "$OS_boot_config" ]; then
            save_main_default
            configfile "$OS_boot_config"
            return 0
        fi
        echo "Boot config ('$OS_boot_config') not found. Press Enter."
    else
        echo "Boot config not specified, use default (/boot/grub/grub.cfg)."
        if [ -f "/boot/grub/grub.cfg" ]; then
            save_main_default
            configfile "/boot/grub/grub.cfg"
            return 0
        fi
        echo "Default boot config (/boot/grub/grub.cfg) not found. Press Enter."
    fi
    read
    set OS_boot_not_found='1'
    return 1
}

function load_os {
    # Search for OS root, then load os boot config.
    if search_os_root; then
        if load_os_boot_config; then
            set root="$old_root"    # I'm here, if i exit from nested grub.cfg.
            set main_saved_entry="$old_main_saved_entry"
            # Saved (in grubenv) 'main_saved_entry' value is still not reset,
            # but it should be overwritten, when other menuentry will be
            # chosen. Also loading of other config does not affect 'default'
            # value in this config, and it is still '$old_main_saved_entry'.
            return 0
        fi
        set root="$old_root"
    fi
    return 1
}

function chainload_os {
    # Search for OS root, then chainload OS.
    if search_os_root; then
        echo "Chain-loading to '${OS_name}'.."
        chainloader "$OS_chainloader"
        save_main_default
        boot            # I shouldn't exit the function, if all is ok.
        set root="$old_root"
        set main_saved_entry="$old_main_saved_entry"
    fi
    return 1
}

function try_label_then_uuid {
    # Execute command (passed as first positional parameter) first with label
    # set (and uuid empty) and, if it failes, with uuid set (and label empty).
    # Actaully, now retry will happen only, if root device search failed.
    # Arguments:
    #   1 - command to execute.
    set t="$OS_root_uuid"
    set OS_root_uuid=''
    "$1"
    set ret="$?"
    set OS_root_uuid="$t"
    if [ "$OS_root_not_found" = '1' ]; then
        # Retry only, if root have not found. Otherwise - success or missed
        # boot config - do nothing.
        set t="$OS_root_label"
        set OS_root_label=''
        echo "Loading '$OS_name' by label failed, try by uuid.."
        "$1"
        set ret="$?"
        set OS_root_label="$t"
    fi
    return "$ret"
}

function try_uuid_then_label {
    # Arguments:
    #   1 - command to execute.
    set t="$OS_root_label"
    set OS_root_label=''
    "$1"
    set ret="$?"
    set OS_root_label="$t"
    if [ "$OS_root_not_found" = '1' ]; then
        set t="$OS_root_uuid"
        set OS_root_uuid=''
        echo "Loading '$OS_name' by uuid failed, try by label.."
        "$1"
        set ret="$?"
        set OS_root_uuid="$t"
    fi
    return "$ret"
}

function save_main_default {
    set main_saved_entry="$chosen"
    save_env main_saved_entry
    # In case loaded configfile also saves default, this (now chosen)
    # menuentry will be included in saved value (will look like 'this>that'),
    # but that config has no menuentry 'this', it has only 'that', and, hence,
    # saved menuentry path will be incorrect. So, i need to make it think,
    # that 'that' menuentry is topmost. Note, that if i set 'chosen' to empty
    # value, grub will _not_ believe, that 'that' menuentry is topmost, hence
    # resulted menuentry path will look like '>that' and still won't work.
    unset chosen
}

# Set variables to defaults.
reset_os_args

}}}
E1B. Menuentries for storing OS-specific grub config on the OS root {{{

# FIXME: Theme.
# FIXME: Timeout.
# FIXME: In the both boot schemes i store linux kernel and initrd on the OS
# root partition (in /boot folder), hence, /boot will be always on the same
# device as /, but _not_ on the same as /boot/grub (in the both boot schemes
# shared boot partition (or some folder on it) mounted here). grub-mkconfig
# probes device under /boot and sets GRUB_DEVICE_BOOT and
# GRUB_DEVICE_BOOT_UUID variables, which have exported to all /etc/grub.d
# scripts. I'm not sure whether these variables should contain device, which
# holds kernel image and initrd, or device, which holds grubdir (/boot/grub).
# For the last case GRUB_DEVICE_BOOT will be set incorrectly. I can't confirm
# neither any of the intended usages of these variables nor any side-effects
# of possible incorrect value.  Though, the way 10_linux script uses
# GRUB_DEVICE_BOOT variable _may_ mean, that it (variable) should hold kernel
# image's and initrd's device. So, this is just a reminder of possible failure
# ;)

insmod lvm
insmod part_msdos
insmod ext2

if [ -s $prefix/grubenv ]; then
  load_env
fi
# Comment line below, if you don't want to set as default (for main grub
# config) last used entry.
set default="$main_saved_entry"

set old_main_saved_entry="$main_saved_entry"
set old_root="$root"
# NOTE: It's advised to explicitly set all os arguments to prevent some of
# them left unchanged from previous menuentries. Also, it is required to call
# reset_os_args at the end of each menuentry (though, code does not check
# this).
menuentry "Stable Linux" {
    set OS_name='Stable Linux'
    set OS_root_uuid='eda990a3-434f-4646-a578-2f7743b7f269'
    set OS_root_label='stable_root'
    set OS_hints='--hint (shilvana_sys-stable_root)'
    set OS_boot_config='/boot/grub/grub.cfg'
    try_label_then_uuid 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Testing Linux" {
    set OS_name='Testing Linux'
    set OS_root_uuid='d80f0166-c472-11e1-b606-00215c4d0de1'
    set OS_root_label='testing_root'
    set OS_hints='--hint (shilvana_sys-testing_root)'
    set OS_boot_config='/boot/grub/grub.cfg'
    try_label_then_uuid 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Windows" {
    insmod ntfs
    set OS_name='Windows'
    set OS_root_uuid='C0FEC280FEC26DEA'
    set OS_root_label='wvista'
    set OS_hints='--hint (hd0,msdos1)'
    set OS_chainloader='+1'
    try_label_then_uuid 'chainload_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback label to uuid" {
    set OS_name='Test fallback'
    set OS_root_uuid='d80f0166-c472-11e1-b606-00215c4d0de1'
    set OS_root_label='test_fallback'
    set OS_hints=''
    set OS_boot_config=''
    try_label_then_uuid 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback uuid to label" {
    set OS_name='Test fallback'
    set OS_root_uuid='d80f0166-c472-11e1-b606-01215c4d0ce1'
    set OS_root_label='testing_root'
    set OS_hints=''
    set OS_boot_config='/boot/grub/grub.cfg'
    try_uuid_then_label 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback both empty" {
    set OS_name='Test fallback'
    set OS_root_uuid=''
    set OS_root_label=''
    set OS_hints=''
    set OS_boot_config='/boot/grub/grub.cfg'
    try_uuid_then_label 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

}}}
E1C. Menuentries for storing OS-specific grub config in subfolder on the shared boot {{{

# FIXME: Theme.
# FIXME: Timeout.
# FIXME: In the both boot schemes i store linux kernel and initrd on the OS
# root partition (in /boot folder), hence, /boot will be always on the same
# device as /, but _not_ on the same as /boot/grub (in the both boot schemes
# shared boot partition (or some folder on it) mounted here). grub-mkconfig
# probes device under /boot and sets GRUB_DEVICE_BOOT and
# GRUB_DEVICE_BOOT_UUID variables, which have exported to all /etc/grub.d
# scripts. I'm not sure whether these variables should contain device, which
# holds kernel image and initrd, or device, which holds grubdir (/boot/grub).
# For the last case GRUB_DEVICE_BOOT will be set incorrectly. I can't confirm
# neither any of the intended usages of these variables nor any side-effects
# of possible incorrect value.  Though, the way 10_linux script uses
# GRUB_DEVICE_BOOT variable _may_ mean, that it (variable) should hold kernel
# image's and initrd's device. So, this is just a reminder of possible failure
# ;)

insmod lvm
insmod part_msdos
insmod ext2

if [ -s $prefix/grubenv ]; then
  load_env
fi
# Comment line below, if you don't want to set as default (for main grub
# config) last used entry.
set default="$main_saved_entry"

set old_main_saved_entry="$main_saved_entry"
set old_root="$root"
# NOTE: It's advised to explicitly set all os arguments to prevent some of
# them left unchanged from previous menuentries. Also, it is required to call
# reset_os_args at the end of each menuentry (though, code does not check
# this).
menuentry "Stable Linux" {
    set OS_name='Stable Linux'
    set OS_root_uuid=''
    set OS_root_label=''
    set OS_hints=''
    set OS_boot_config='/stable_linux/grub/grub.cfg'
    load_os_boot_config
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Testing Linux" {
    set OS_name='Testing Linux'
    set OS_root_uuid=''
    set OS_root_label=''
    set OS_hints=''
    set OS_boot_config='/testing_linux/grub/grub.cfg'
    load_os_boot_config
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Windows" {
    insmod ntfs
    set OS_name='Windows'
    set OS_root_uuid='C0FEC280FEC26DEA'
    set OS_root_label='wvista'
    set OS_hints='--hint (hd0,msdos1)'
    set OS_chainloader='+1'
    try_label_then_uuid 'chainload_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback label to uuid" {
    set OS_name='Test fallback'
    set OS_root_uuid='d80f0166-c472-11e1-b606-00215c4d0de1'
    set OS_root_label='test_fallback'
    set OS_hints=''
    set OS_boot_config=''
    try_label_then_uuid 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback uuid to label" {
    set OS_name='Test fallback'
    set OS_root_uuid='d80f0166-c472-11e1-b606-01215c4d0ce1'
    set OS_root_label='testing_root'
    set OS_hints=''
    set OS_boot_config='/boot/grub/grub.cfg'
    try_uuid_then_label 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

menuentry "Test fallback both empty" {
    set OS_name='Test fallback'
    set OS_root_uuid=''
    set OS_root_label=''
    set OS_hints=''
    set OS_boot_config='/boot/grub/grub.cfg'
    try_uuid_then_label 'load_os'
    echo "Ret = $?"
    read
    reset_os_args
}

}}}

In order to make working main grub.cfg you should concatenate functions with
corresponding (to chosen boot scheme) menuentries flavour.  You may use such
script to do this (adjust pathes)

#!/bin/sh

set -ef
project_path='/home/sgf/Documents/boot_usb'
src_path='src/main_grub_config'
bin_path='bin'

cd "$project_path"
cat "./$src_path/grub_main_lib.cfg" \
    "./$src_path/grub_main.cfg"     \
    >| "./$bin_path/grub_main.cfg"

}}}
E2. Example of script for installing grub hardlinks {{{

#!/bin/sh

set -euf

OIFS="$IFS"
newline='
'
bkp_dirs=''
repl=''

# Adjust these pathes, if necessary.
boot_path='/mnt/boot'         # Where shared boot partition mounted.
grubdir_relpath='common/grub'   # Relative (to $boot_path) path to grubdir.
# find's regexp (posix-basic) for matching grub config files. These files will
# not be hardlinked from grubdir.
os_config_pattern='grub*.cfg'

grubdir="$boot_path/$grubdir_relpath"
os_config_dirs="$(find -P "$boot_path"  -path "$grubdir" -prune \
                                        -o -path '*/grub' -print)"
echo "Update hardlinks in OS boot directories."
echo "Shared boot partition mounted on '$boot_path'."
echo "Main grub directory (full path) is '$grubdir'."
echo "Found OS boot directories are:"
echo "$os_config_dirs"
read -p 'Proceed? (y/any key): ' repl
if [ "$repl" != 'y' ]; then
    echo "..exit"
    exit 0
fi

IFS="$newline"
for os_config_dir in $os_config_dirs; do
    echo "Updating '$os_config_dir'.."
    echo "Moving old files out of the way.."
    bkp_dir="$(mktemp -d "$os_config_dir".XXXX)"
    # I'll use `cp && rm` for moving files, because i want to check every file
    # for match with "$os_config_pattern", and, hence, if current file is in
    # subdir under '$os_config_dir', i should recreate this subdir(s) under
    # backup location before moving file, but mv hasn't option '--parents'.
    # For determining correctly subdir(s), which should be recreated by
    # '--parents', `cp` must run in the source top dir.
    cd "$os_config_dir"
    find -P . -mindepth 1 \
                -not -type d -not -iname "$os_config_pattern" \
                -exec sh -euf -c "
                            cp -Pl --parents -t \"$bkp_dir\" \"\$@\" \
                            && rm -f \"\$@\"" \
                         sh {} \+
    echo "..done"
    echo "Hardlinking.."
    # I'm in grubdir now.
    cd "$grubdir"
    find -P . -mindepth 1 \
                -not -type d -and -not -iname "$os_config_pattern" \
                -exec cp -Pl --parents -t "$os_config_dir" {} \+
    echo "..done"
    bkp_dirs="${bkp_dirs}${bkp_dir}${newline}"
done
IFS="$OIFS"

cat <<EOF
grub hardlinks have been updated.  If you want to restore previous
os_config_dir use

    cp -RTl --remove-destination backup_dir os_config_dir

for restoring (without '--remove-destination' you'll overwrite grubdir files
and will need to reinstall grub (by grub-install) after that) and then

    find backup_dir -depth -links '+1' -delete

for deleting backup.  Otherwise, you may safely remove backup directories.
Here they are:
EOF
echo "$bkp_dirs"

}}}

}}}

Комментариев нет:

Отправить комментарий