Concepts
This section discusses the basic concepts underlying
yaird.
The main procedure of the program is this:
#
# go -- main program:
# write an image to destination in specified format.
# destination must not exist yet.
# templates define how actions in masterplan are expanded.
#
sub go ($$) {
my ($config, $destination) = @_;
my $masterPlan = Plan::makePlan ($config->{goals});
my $image = $masterPlan->expand ($config->{templates});
Pack::package ($image, $config->{format}, $destination);
}
What it does is simple:
given some goals, make a plan with a number of actions
that the generated image should execute;
transform the plan to a detailed description of the image;
build and pack the image.
About Goals
The generated initial boot image should achieve a number of goals
before handing over control to the root file system. There is a
configuration file that determines what these goals are; the
default list of goals is as follows:
GOALS
TEMPLATE prologue
INPUT
MODULE mousedev
MODULE evdev
MOUNTDIR "/" "/mnt"
TEMPLATE postlude
END GOALS
The complete list of goals that yaird
knows about is as follows:
TEMPLATE name
Add the contents of the named template to the image.
It is not possible to pass arguments to the template.
MODULE name
Add the named module to the image.
INPUT
Add modules for every keyboard device found on the
system to the image.
NETWORK
Add modules for every ethernet device found on the
system to the image.
MOUNTDIR fsdir mountPoint
Given a directory that occurs in /etc/fstab,
get the underlying block device and file system type
working, then mount it at mountPoint.
MOUNTDEV blockDevice mountPoint
Given a block device that occurs in /etc/fstab,
get the block device and corresponding file system type
working, then mount it at mountPoint.
It is not possible to express activating a block device
without mounting it somewhere.
It is likely that new types of goal will need to be introduced
to support features such as software suspend.
Making the Plan
The goals listed in the configuration file need to be translated
into actions to be taken by the generated image.
As an example, before mounting a file system, the modules containing
the implementation of the file system need to be loaded.
To refine the goal of loading a kernel module,
the ModProbe module invokes the
modprobe command to find any
prerequisite modules, skipping any modules that are blacklisted
or compiled into the kernel. Aliases are handled transparantly
by modprobe, module options are recorded to be included in the
initial image.
If the
modprobe command decides a module
needs an install command, an error is generated because we
cannot in general determine which executables the install
command would need to be on the initial boot image.
The KConfig module determines if loading a
module can be omitted because the module is hardcoded into the
kernel. As an example, it is aware of the fact that the module
ext3 is not needed if the new kernel configuration
contains CONFIG_EXT3_FS=y.
Having knowledge of the relation between module names and
kernel defines hardcoded into yaird
is hardly elegant. Perhaps it is possible to generate this
mapping based on the kernel Makefiles when building the
kernel, but that's too complex just now.
Only a few modules are known: yaird
looks for modules such as ext3 when that filesystem
is used, so it makes sense to check whether a missing module
is compiled in. On the other hand, hardware modules that are
compiled in never show up in modules.pcimap
and friends, so they remain completely outside the view of
yaird.
Before a device as listed in /etc/fstab can be
mounted, that device needs to be enabled. That device could be an
NFS mount, a loopback mount or it could be a block device.
The loopback case is not supported yet, but block devices are.
This support is based on a number of sources of information:
Scanning the /dev directory gives us the
relation between all block special files and major/minor
numbers.
Scanning the /sys/block directory gives us the
relation between all major/minor numbers and kernel names
such as dm-0 or sda1; it
also gives the relation between partitions and complete
devices.
If there is a symlink in a /sys/block
subdirectory to the /sys/devices
directory, it also gives is the relation between a block
device and the underlying hardware.
Based on the kernel name and partition relationships of the device,
we determine the steps needed to activate the device. As an example,
to activate sda1, we need to activate sda,
then create a block special file for sda1. As
another example, to activate dm-0, our first bet is
to check whether this is an LVM logical volume, and if so activate the
physical volumes underlying the volume group, and finally running
vgchange -a y.
Otherwise, it could be an encrypted device, for which we
generate different code.
Hardware Planning
Some devices, such as sdx or hdy, are
expected to have underlying hardware; as an example, sda
may be backed by pci0000:00/0000:00:1f.2/host0/0:0:0:0.
This represents a hardware path, in this case a controller on the PCI
bus that connects to a SCSI device. In order to use the device,
every component on the path needs to be activated, the component
closest to the CPU first.
Based on the pathname in /sys/devices and on
files within the directory for the component, we can determine
what kind of component we're dealing with, and how to find the
required modules.
Finding modules closely follows the methods used in the
hotplug package, and the
hotplug approach in turn is an almost
literal translation of the code that the kernel uses to find a
driver for a newly detected piece of hardware.
For components that talk some protocol over a bus, like SCSI or
IDE disks or CDROMs, this is a simple hard coded selection; as an
example, the ScsiDev module knows that a SCSI device
with a type file containing "5" is a CDROM,
and that sr-mod is the appropriate driver.
Devices such as PCI or USB devices cannot be classified into
a few simple categories. These devices have properties such
as "Vendor", "Device" and "Class" that are visible in sysfs.
The source code of kernel driver modules for these devices
contains a table listing which combination of properties mark a
device that the driver is prepared to handle. When the kernel
is compiled, these tables are summarised in a text file such
as modules.pcimap. Based on this table,
we find a driver module needed for the device and mark it for
inclusion on the image.
Multiple modules can match the same hardware: as an example,
usb-storage and ub both match an USB
stick. In such cases, we load all matching modules into the
kernel and leave it to kernel to decide who gets to manage the
device. There's one complication: some modules, such as
usb-core, match any device (probably to maintain some
administration of their own, or to provide an ultra-generic
interface), but do not actually provide access to the device.
Such devices are weeded out by the Blacklist module,
based on information in
/etc/hotplug/blacklist and
/etc/hotplug/blacklist.d.
It turns out that the "load modules for every component in the sysfs
path" approach is not always sufficient: sometimes you have to load
siblings as well. As an example, consider a combined EHCI/UHCI
USB controller on a single chip. The same ports can show up as EHCI
or UHCI devices, different PCI functions in the same PCI slot, with
different sysfs directories, depending on what kind of hardware is
connected. Purely following the sysfs path, we would only need to load
the EHCI driver, but it appears that on this kind of chip, EHCI devices
are not reliably detected unless the UHCI driver is loaded as well.
For this reason, we extend the algorithm with a rule: "for PCI devices,
load modules for every function in the PCI slot".
That's actually a bit much: it would load all of ALSA if you have a
combined ISA/IDE/USB/Multimedia chipset. So we limit the above
to those PCI functions that provide USB ports.
Plan Transformation
The plan generated in the first phase is a collection of general
intentions, stuff like 'load this module', but it does not
specify exactly what files must be placed on the image and what
lines are to be added to the initialisation scripts.
The module ActionList represents this plan with a
list of hashes; every hash contains at least 'action' and
'target', with other keys added to provide extra information
as needed. If two steps in the plan have identical action and
target, the last one is considered redundant and silently omitted.
This plan is transformed to an exact image description with
the help of templates.
These templates are read from a configuration file; for every
type of action they can contain:
files to be copied from the mother system to the
same location on the image;
directories to be created on the image; these do
not have to exist on the mother system;
trees to be copied recursively from the mother
system to the image;
script fragments: a few lines of code to be appended to
the named file on the image.
All of the above are fed through HTML-Template, with the hash
describing this action as parameters. In practice, this looks
like so:
#
# Template -- translating general intentions to exact image layout
#
TEMPLATE SET
TEMPLATE insmod
BEGIN
FILE "<TMPL_VAR NAME=target>"
FILE "/sbin/insmod"
# optionList may be undef
# and already is suitably escaped.
SCRIPT "/init"
BEGIN
!/sbin/insmod '<TMPL_VAR NAME=target>' \
! <TMPL_VAR NAME=optionList>
END SCRIPT
END TEMPLATE
# lots of other templates omitted ...
END TEMPLATE SET
There are a few attributes that are available to every template:
version
The kernel version we're generating an image for.
Useful if you want your image to include a complete copy
of /lib/modules/(version)/kernel.
appVersion
The version of yaird used to
build the image.
auxDir
The directory where yaird
keeps executables intended to go on the image, such as
run_init.
Currently, there are templates for Debian and for Fedora, plus
a template showing how to use the older initrd approach.
Image Generation
The detailed image description consists of a collection of names of
files, directories, symbolic links and block or character devices,
plus a number of lines of shell script. The image description does
not contain permission or ownership information: files always have
mode 444, executables and directories always 555, devices always
mode 600,
Having device files on the image is wrong: it will
break if the new kernel uses different device numbers. Mostly
this can be avoided by using the dev
files provided by sysfs, but there is a bootstrap problem:
the mount command needed to
access sysfs assumes /dev/null and
/dev/console are available.
and everything is owned by root.
The Image module contains the image description and
can write the image to a directory. It understands about symlinks:
if /sbin/vgscan is added to the image and it
happens to be a symlink to lvmiopversion, both
vgscan and lvmiopversion
will be added to the image. Shared libraries are supported
via the SharedLibraries module, as discussed in
. Invocations of other executables are not
recognised automatically: if lvmiopversion executes
/etc/lvm-200/vgscan, the latter needs to be
added explicitly to the image.
The copying of complete trees to the image is influenced by the
copying for executables: if there is a symlink in the tree, it's
target is also included on the image, but if the target is a
directory, its contents are not copied recursively. This
approach avoids loops in image generation.
Note that the target of a symlink must exist:
yaird refuses to copy dangling links.
Packing the Image
The final step is packing the image in a format that the
bootloader can process; this is handled by the module
Pack. The following formats are supported:
cpio
A zipped cpio file (new ASCII format), required for the
initramfs model as used in the templates for Debian and
Fedora.
directory
An unpacked directory, good for debugging or manually
creating odd formats.
cramfs
A cramfs filesystem, used for Debian initrd images.