428 lines
18 KiB
Markdown
428 lines
18 KiB
Markdown
|
# Driver Update Disks Developer Documentation
|
||
|
|
||
|
_v1.0, Mon Feb 15 2016_
|
||
|
|
||
|
This document describes implementation details of how Driver Update Disks
|
||
|
(usually abbreviated DUD or DD) are loaded and used by the installer, as a
|
||
|
reference for developers working on the code.
|
||
|
|
||
|
**_NOTE: This is not a specification. Filenames, paths, and procedures may
|
||
|
change without warning. Avoid contact with eyes and skin and avoid inhaling
|
||
|
fumes. Keep out of reach of children and customers. Any resemblance to real
|
||
|
code, living or dead is purely coincidental._**
|
||
|
|
||
|
Most of the important code is in `driver_updates.py`, which uses the utilities
|
||
|
from `utils/dd` to actually handle driver RPMs. There's also support code
|
||
|
sprinkled through this directory and in the installer itself, which will be
|
||
|
described below. But first...
|
||
|
|
||
|
# A Quick Overview
|
||
|
|
||
|
## What is a DUD?
|
||
|
|
||
|
A DUD (Driver Update Disk) is a disk (or disk image - typically a `.iso` file)
|
||
|
that contains some _metadata_ and one or more _repos_ which contain
|
||
|
_driver RPMs_ and/or _installer enhancement RPMs_.
|
||
|
|
||
|
## What are they for?
|
||
|
|
||
|
These are used to add kernel modules and/or userspace programs to the installer
|
||
|
*before* the installer's second stage is loaded. This is most commonly needed
|
||
|
when trying to install onto a system whose disk (or network) controllers are
|
||
|
not supported by the existing installer.
|
||
|
|
||
|
## How does it work?
|
||
|
|
||
|
Here's a short summary; please see below for important technical details of
|
||
|
each part of the process.
|
||
|
|
||
|
1. **Boot argument**: `inst.dd=<driverdisk>` specifies where to look for
|
||
|
driver disks.
|
||
|
2. **Automatic DUD handling**: If a disk partition labeled `OEMDRV` is found
|
||
|
during startup (before `udev` settles) it's assumed to be a driver disk.
|
||
|
3. **Interactive DUD selection**: The user may pass the argument `inst.dd`
|
||
|
by itself to request interactive driver disk selection via a text menu.
|
||
|
The user can choose a device to examine, pick an `.iso` image to load from
|
||
|
(if multiple images are present), and choose individual drivers to load.
|
||
|
4. **Driver RPM selection**: `dd_list` is used to list the contents of a DUD
|
||
|
repo; it will only list RPMs that have one of these headers, matching the current kernel/installer version:
|
||
|
* `Provides: kernel-modules <version-expr>`
|
||
|
* `Provides: installer-enhancement <version-expr>`
|
||
|
5. **Driver extraction**: For all matching RPMs, `dd_extract` extracts
|
||
|
kernel modules and firmware (if the `kernel-modules` header was present)
|
||
|
and binaries and libraries (if `installer-enhancement` was present)
|
||
|
into the initrd and installer environment.
|
||
|
6. **Driver loading**: After processing each DUD, `driver_updates.py` runs
|
||
|
`depmod` and tries to manually `modprobe` all extracted drivers.
|
||
|
7. **Repo copying and package listing**: `driver_updates.py` saves
|
||
|
a list of the extracted package names and a copy of each DUD repo used into
|
||
|
the installer environment. When `anaconda` runs, it enables any DUD repos
|
||
|
it finds and adds all the named packages to the install transaction.
|
||
|
|
||
|
# Overall design notes
|
||
|
|
||
|
Here are a few things to keep in mind about the overall design of the DUD code.
|
||
|
|
||
|
1. **The real action happens in the dracut initqueue**
|
||
|
|
||
|
We need both `udev` and `dracut` to be processing events so that other
|
||
|
dependent events are handled as usual.
|
||
|
|
||
|
It's tempting to think that we should just handle DUDs *before* the
|
||
|
initqueue mainloop so that the devices will magically already be present
|
||
|
when we start the initqueue, as if we had started with them loaded.
|
||
|
Except then you need to start running `udevadm trigger` and `udevadm settle`
|
||
|
yourself to make the DUD devices appear, and then handle them manually, and
|
||
|
now you've gone and re-written the dracut mainloop.
|
||
|
|
||
|
Too clever is stupid. Just use initqueue.
|
||
|
|
||
|
*Any long-running action that's going to be performed inside
|
||
|
one of the dracut scriptlets should be using `/sbin/initqueue` to add items
|
||
|
to the initqueue instead of executing the code itself in-place.*
|
||
|
|
||
|
(See, for example, the way `driver-updates-genrules.sh` handles DUDs that
|
||
|
are already present inside the initramfs.)
|
||
|
|
||
|
2. **Let `dracut` handle devices and downloads**
|
||
|
|
||
|
`dracut`'s main purpose in life is to bring up and initialize the devices
|
||
|
needed to start the system (or, in this case, the installer image).
|
||
|
|
||
|
Dracut's far better at handling disks and image mounting than Python is,
|
||
|
and in general we want upstream dracut to be responsible for handling as
|
||
|
much as possible of the bootup process. (That's kind of its job, after all.)
|
||
|
|
||
|
*Anything that directly handles probing devices, downloading images,
|
||
|
calling udev, etc. should be done in dracut scriptlets, not `driver_updates.py`.*
|
||
|
|
||
|
(The current invocation of `udevadm` that's in there should be moved to a
|
||
|
dracut scriptlet as soon as we can get away with changing it.)
|
||
|
|
||
|
3. **Let `dd_list`/`dd_extract` handle driver RPMs**
|
||
|
|
||
|
Neither `dracut` nor `driver_updates.py` should be messing with RPM headers,
|
||
|
unpacking individual RPMs, etc.
|
||
|
|
||
|
*Use `dd_list` to list the driver RPMs that are available in a repo, and
|
||
|
use `dd_extract` to extract their contents. Don't mess with RPMs directly.*
|
||
|
|
||
|
4. **Use as little Python as possible in initrd**
|
||
|
|
||
|
If you know Python better than dracut or C, it might be tempting to solve
|
||
|
problems by hacking on the Python parts of the DUD code.
|
||
|
This leads to redundant code - and more code for us to maintain.
|
||
|
|
||
|
`driver_updates.py`'s purpose is basically just to mount disks or disk
|
||
|
images and find valid DUD repos therein (either automatically or
|
||
|
interactively), and to save needed data to places where the installer
|
||
|
can find it later.
|
||
|
|
||
|
*Any other tasks should be handled in dracut scriptlets,
|
||
|
the C utilities, or the main installer program.*
|
||
|
|
||
|
(If you must add Python code, avoid pulling in new libraries. It
|
||
|
makes building images much more fragile and bloats the initramfs.)
|
||
|
|
||
|
# Boot arguments
|
||
|
|
||
|
The `inst.dd` (or `dd`) boot argument is used to specify the location of a
|
||
|
DUD to use, or to start interactive mode. It has three forms:
|
||
|
|
||
|
1. Network URLs
|
||
|
* example: `inst.dd=http://host.fake.domain/path/to/dd.iso`
|
||
|
* Target should be a `.iso` (or other disk image) or driver `.rpm`
|
||
|
* Target will be downloaded and then handled appropriately
|
||
|
2. Block devices
|
||
|
* examples: `inst.dd=cdrom:/dev/cdrom`, `inst.dd=hd:LABEL=DRIVERZ`
|
||
|
* The `hd:` or `cdrom:` prefix is stripped
|
||
|
* `TAG=VALUE` pairs are looked up using `blkid`
|
||
|
3. Interactive mode
|
||
|
* `inst.dd` (or `dd`)
|
||
|
* Brings up text-based menu:
|
||
|
1. Choose from a list of disk devices
|
||
|
2. (if device contains multiple `.iso` images) Choose an `.iso`
|
||
|
3. Choose from a list of drivers found in the DUD
|
||
|
|
||
|
If one (or more) of these are specified, dracut will wait until all specified
|
||
|
`inst.dd` items have been handled before starting the main installer image.
|
||
|
|
||
|
# Automatic DUD handling (`OEMDRV`)
|
||
|
|
||
|
If no `inst.dd` boot argument is specified, dracut watches for a readable
|
||
|
disk partition labeled `OEMDRV`. If one appears before dracut finishes waiting
|
||
|
for required devices, it is automatically handled like a normal DUD.
|
||
|
|
||
|
## NOTE: device autodetection limitations
|
||
|
|
||
|
Once the kernel's device probing finishes, `udev` waits for and processes
|
||
|
responses from devices. Once every response has been handled and no more new
|
||
|
responses arrive for a short period of time (no less than 500ms), dracut assumes
|
||
|
all devices have been found and declares the system "settled".
|
||
|
|
||
|
If there's no response from any `OEMDRV` device by then, the installer starts
|
||
|
normally.
|
||
|
|
||
|
_If the `OEMDRV` device is present but too slow to be autodetected, the user
|
||
|
can boot with an argument like `inst.dd=hd:LABEL=OEMDRV` to indicate that
|
||
|
dracut should expect an `OEMDRV` device and not start the installer until it
|
||
|
appears._
|
||
|
|
||
|
# DUD filesystem layout
|
||
|
|
||
|
A driver updates disk may contain one or more driver repos, which are usually
|
||
|
laid out as follows:
|
||
|
|
||
|
/
|
||
|
|rhdd3 - DD marker, contains the DD's description string
|
||
|
/rpms
|
||
|
| /i686 - contains RPMs for this arch and acts as package repo
|
||
|
| /x86_64
|
||
|
| /ppc64
|
||
|
| /... - any other architecture the DD provides drivers for
|
||
|
|
||
|
In other words, a DUD is a directory containing:
|
||
|
1. a file named `rhdd3` (which contains the description for that DUD), and
|
||
|
2. a directory named `rpms/`, under which there is at least one directory
|
||
|
which matches the output of `uname -m`. Under there are RPMs that contain
|
||
|
the special DUD headers (see the next section for more info).
|
||
|
|
||
|
By convention, the "repos" under `rpms/` usually have yum metadata, but this is
|
||
|
not required or enforced by the tools. If there's no metadata present,
|
||
|
anaconda will run `createrepo` (or whatever the backend requires to make
|
||
|
that directory usable).
|
||
|
|
||
|
# Driver Updates RPM Headers
|
||
|
|
||
|
When `dd_list` examines a driver disk, it checks the `Provides:` headers of
|
||
|
each RPM to see if it provides either `kernel-modules` or
|
||
|
`installer-enhancement`. RPMs that contain kernel modules or firmware must
|
||
|
provide `kernel-modules`, and RPMs that contain libraries or binaries must
|
||
|
provide `installer-enhancement`.
|
||
|
|
||
|
## RPM header version matching
|
||
|
|
||
|
Usually the packages will have a header like one of
|
||
|
these:
|
||
|
|
||
|
Provides: kernel-modules >= 3.6.9
|
||
|
Provides: installer-enhancement >= 19
|
||
|
|
||
|
The given version comparison expression is compared against either the running
|
||
|
kernel or the installer version; if the kernel or installer matches the
|
||
|
expression, then the RPM is considered valid for use inside this installer
|
||
|
environment, and it will be listed by `dd_list`. Otherwise, `dd_list` (and,
|
||
|
therefore, the rest of the DUD code) will ignore that RPM.
|
||
|
|
||
|
Note that RPM operations are handled entirely by `dd_list` and `dd_extract`.
|
||
|
|
||
|
### `installer-enhancement` versioning
|
||
|
|
||
|
The DUD code does _not_ use the actual running installer version; instead it
|
||
|
hardcodes the value `19`. I think (at some point) there _were_ DUDs in
|
||
|
the wild that required this fact, and so it's just been left as-is.
|
||
|
|
||
|
If the Anaconda Add-on API changes, this version should *definitely* be changed.
|
||
|
|
||
|
# `dracut` component details
|
||
|
|
||
|
This section describes all the various bits of driver update support code that
|
||
|
live in `dracut` and how they get called or call each other.
|
||
|
|
||
|
## cmdline: `parse-anaconda-dd.sh`
|
||
|
|
||
|
If any `inst.dd=...` (or `dd=...`) arguments are found, this scriptlet parses
|
||
|
their values and writes appropriate values to `/tmp/dd_disk`, `/tmp/dd_net`, or
|
||
|
`/tmp/dd_interactive`, as follows:
|
||
|
|
||
|
1. Values that start with `http:`, `https:`, `ftp:`, or `nfs:` are assumed to
|
||
|
be network URLs and get written to `/tmp/dd_net`.
|
||
|
2. Values of the form `hd:<dev>`, `cdrom:<dev>`, `file:<path>`, or `path:<path>`
|
||
|
will have the `<dev>` or `<path>` part written to `/tmp/dd_disk`.
|
||
|
3. Any other value is assumed to be a disk device, and therefore also gets
|
||
|
written to `/tmp/dd_disk`.
|
||
|
4. If a plain `dd` or `inst.dd` boot argument (without a value) is found, the
|
||
|
word "menu" is written to `/tmp/dd_interactive`.
|
||
|
|
||
|
Finally, the contents of all three files are combined to create `/tmp/dd_todo`.
|
||
|
|
||
|
## pre-trigger: `driver-updates-genrules.sh`
|
||
|
|
||
|
This sets up the udev rules and dracut scriptlets that will handle any DUDs
|
||
|
that appear. Specifically:
|
||
|
|
||
|
1. If `/tmp/dd_todo` exists, call `anaconda-lib.sh:wait_for_dd()` so that
|
||
|
dracut will not start the installer until `/tmp/dd.done` exists.
|
||
|
2. If `/tmp/dd_interactive` exists, add an initqueue item to start
|
||
|
`driver-updates@.service` once the initqueue starts.
|
||
|
3. If `/tmp/dd_interactive` does *not* exist, add a udev rule to automatically
|
||
|
handle any `OEMDRV` devices that appear.
|
||
|
4. For each item specified in `/tmp/dd_disk`, either:
|
||
|
* create a udev rule that will execute `driver-updates --disk` when that
|
||
|
disk appears, or
|
||
|
* if the item is a DUD image that already exists in the initrd: add an
|
||
|
initqueue item to run `driver-updates --disk` once the initqueue starts.
|
||
|
5. Set up a `initqueue/finished` scriptlet to force dracut to stay in the
|
||
|
initqueue until `initqueue/settled` runs at least once.
|
||
|
|
||
|
## initqueue/online: `fetch-driver-net.sh`
|
||
|
|
||
|
This script runs every time a network device comes online and tries to handle
|
||
|
each image URL listed in in `/tmp/dd_net`, as follows:
|
||
|
|
||
|
1. If the URL is listed in `/tmp/dd_net.done`, it is skipped
|
||
|
2. The URL is fetched using dracut's `fetch_url` function
|
||
|
3. If the image is successfully downloaded:
|
||
|
* append URL to `/tmp/dd_net.done`
|
||
|
* run `driver-updates --net URL IMAGE`
|
||
|
4. Otherwise: if the fetch failed, warn the user and exit
|
||
|
|
||
|
## `parse-kickstart`
|
||
|
|
||
|
When `parse-kickstart` handles a `driverdisk` command it emits the appropriate
|
||
|
`inst.dd=` argument, which will then get parsed and handled as usual when
|
||
|
the `run_kickstart` function does its thing (see `anaconda-lib.sh` for
|
||
|
details on how `run_kickstart` works).
|
||
|
|
||
|
There's two valid forms for the `driverdisk` command - here's how those get
|
||
|
handled by `parse-kickstart`:
|
||
|
|
||
|
| kickstart command | `parse-kickstart` output
|
||
|
|--------------------------------|--------------------------
|
||
|
| `driverdisk <partition>` | `inst.dd=hd:<partition>`
|
||
|
| `driverdisk --source=<url>` | `inst.dd=<url>`
|
||
|
|
||
|
Check the [kickstart documentation] for more info.
|
||
|
|
||
|
[kickstart documentation]: https://github.com/rhinstaller/pykickstart/blob/master/docs/kickstart-docs.rst#driverdisk
|
||
|
|
||
|
## initqueue: `driver-updates@.service`
|
||
|
|
||
|
This service launches the interactive driver update menu.
|
||
|
It handles hiding the `plymouth` splash screen, quieting kernel messages,
|
||
|
and connecting the menu to the tty so that the user can select drivers.
|
||
|
|
||
|
This gets started from the initqueue by `driver-updates-genrules.sh`,
|
||
|
if interactive mode was requested.
|
||
|
|
||
|
## initqueue: `driver_updates.py`
|
||
|
|
||
|
Handles a single DUD request: extract and load drivers, save the DUD repo(s)
|
||
|
for later use, and keep track of which requests have been handled. (Also
|
||
|
handles the interactive mode - more on that below.)
|
||
|
|
||
|
**NOTE:** inside the initrd environment this file is named
|
||
|
`/bin/driver-updates`. (See `module-setup.sh` if you want to know more about
|
||
|
what files get put where inside the initrd.)
|
||
|
|
||
|
### Invocation
|
||
|
|
||
|
For disk devices, `driver-updates-genrules.sh` sets up a udev rule that will
|
||
|
run `/sbin/initqueue` to add an initqueue job which will run `driver-updates`.
|
||
|
|
||
|
For initrd-embedded images, `driver-updates-genrules.sh` skips udev and runs
|
||
|
`/sbin/initqueue` itself to add the job to the initqueue.
|
||
|
|
||
|
For network images, `fetch-driver-net.sh` (in the `initqueue/online` hook) runs
|
||
|
`driver-updates` itself.
|
||
|
|
||
|
### Arguments
|
||
|
|
||
|
* `driver-updates --disk PART DEVNODE`
|
||
|
* `driver-updates --net URL LOCALFILE`
|
||
|
|
||
|
In both cases, the first argument is the string provided by the user - either
|
||
|
the partition specifier (like `LABEL=DRIVERS`) or the URL. This string will
|
||
|
also be in `/tmp/dd_net` or `/tmp/dd_disk`, and `/tmp/dd_todo`.
|
||
|
|
||
|
The second argument is the actual image or device node to be mounted and
|
||
|
processed. Dracut is responsible for downloading images or finding the disk
|
||
|
device and passing that to `driver-updates` here.
|
||
|
|
||
|
In the case of `inst.dd=file:/dd.img`, both items will be `/dd.img`.
|
||
|
|
||
|
### Driver Updates Disk Handling
|
||
|
|
||
|
Here, roughly, is how `driver-updates` handles a DUD:
|
||
|
|
||
|
1. Mount the disk (the `DEVNODE` or `LOCALFILE` argument above)
|
||
|
2. For all valid `rhdd3` repos found, list the RPMs using `dd_list`
|
||
|
* A valid repo is a directory containing a file named `rhdd3` and a
|
||
|
subdirectory named `rpms/$ARCH`.
|
||
|
(See [DUD filesystem layout](#dud-filesystem-layout), above)
|
||
|
3. For all matching RPMs found, `dd_extract` the RPM into `/updates`
|
||
|
* `/updates` is overlaid onto the installer image when we switch there
|
||
|
4. If the package contains drivers, write the name of the package to
|
||
|
`/run/install/dd_packages`
|
||
|
5. If the current repo had any driver RPMs, copy it to
|
||
|
`/run/install/DD-1` (`DD-2`, `DD-3`, etc.)
|
||
|
6. Load all the drivers that were extracted:
|
||
|
1. Copy the drivers and firmware to the `updates/` dirs:
|
||
|
* `/lib/modules/$(uname -r)/updates/`
|
||
|
* `/lib/firmware/updates/`
|
||
|
2. Run `depmod -a` and then `modprobe -a <module names>`
|
||
|
4. Append the `PART` or `URL` argument string to `/tmp/dd_finished`
|
||
|
5. If every item in `/tmp/dd_todo` is now also in `/tmp/dd_finished`,
|
||
|
create `/tmp/dd.done` so dracut knows it can exit the initqueue.
|
||
|
|
||
|
### Bash helper scripts called from driver_updates.py
|
||
|
|
||
|
* anaconda-ifdown
|
||
|
* This script sets the interface down and removes all flags in dracut for
|
||
|
future re-setting. This is useful for replacing existing network drivers.
|
||
|
|
||
|
* find-net-intfs-by-driver
|
||
|
* Find all network interfaces which depend on the given driver (command
|
||
|
line argument), then return a list of the network interfaces.
|
||
|
|
||
|
## pre-pivot: `anaconda-depmod.sh`
|
||
|
|
||
|
If any drivers were installed or downloaded, run depmod on `$NEWROOT` so we can
|
||
|
load the drivers later, if needed.
|
||
|
|
||
|
(To determine whether drivers were installed it simply checks to see if
|
||
|
`/run/install/DD-1` exists.)
|
||
|
|
||
|
# Testing
|
||
|
|
||
|
Testing the DUD code is a little tricky, since it's pretty hardware dependent.
|
||
|
Here are some tools and resources that should help if you're trying to test the
|
||
|
DUD code.
|
||
|
|
||
|
## Integration tests & unit tests
|
||
|
|
||
|
There are unit tests for `driver_updates.py` in
|
||
|
`tests/dracut_tests/test_driver_updates.py`. Please do add test cases for
|
||
|
any bugs that are found or new methods that get added.
|
||
|
|
||
|
There are also some tests for `dd_list` and `dd_extract`, in
|
||
|
`tests/dd_tests/dd_test.py`. These tests ensure that the utilities behave the
|
||
|
way that `driver_updates.py` expects them to.
|
||
|
|
||
|
(These tests run as part of the normal suite of tests that run during
|
||
|
`make check`.)
|
||
|
|
||
|
Finally, the [kickstart-tests] repo has at least one functional/integration
|
||
|
test for the `driverdisk` command. It also contains a helper program,
|
||
|
`mkdud.py`, which can be used to generate (fake) DUD images for test purposes.
|
||
|
|
||
|
[kickstart-tests]: https://github.com/rhinstaller/kickstart-tests
|
||
|
|
||
|
## Real-world DUD images
|
||
|
|
||
|
Here's a Red Hat Knowledgebase article that links to some Red Hat-provided
|
||
|
images: https://access.redhat.com/articles/64322 (Sadly, you have to log in to
|
||
|
download them)
|
||
|
|
||
|
# References
|
||
|
|
||
|
Old driverdisc docs from the RHEL7 [anaconda source]:
|
||
|
|
||
|
* [docs/driverdisc.rst]: describes the driver disc format and motivations for
|
||
|
its design.
|
||
|
* [dracut/README-dd]: developer documentation on the dd code in dracut.
|
||
|
|
||
|
[anaconda source]: https://github.com/rhinstaller/anaconda
|
||
|
[docs/driverdisc.rst]: https://github.com/rhinstaller/anaconda/blob/rhel7-branch/docs/driverdisc.rst
|
||
|
[dracut/README-dd]: https://github.com/rhinstaller/anaconda/blob/rhel7-branch/dracut/README-dd
|