Skip to content

Commit 8245797

Browse files
authored
[mic][iso] generate PXE-bootable ISO images. (#10595)
1 parent 5894457 commit 8245797

29 files changed

Lines changed: 1074 additions & 124 deletions

toolkit/tools/imagecustomizer/docs/cli.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ existing RPM repo (such as packages.microsoft.com). Using a cloned repo with
106106
Disable the base image's installed RPM repos as a source of RPMs during package
107107
installation.
108108

109+
## --output-pxe-artifacts-dir
110+
111+
Create a folder containing the artifacts to be used for PXE booting.
112+
113+
For an overview of Azure Linux Image Customizer support for PXE, see the
114+
[PXE support page](./pxe.md).
115+
109116
## --log-level=LEVEL
110117

111118
Default: `info`

toolkit/tools/imagecustomizer/docs/configuration.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ The Azure Linux Image Customizer is configured using a YAML (or JSON) file.
7272
22. If the output format is set to `iso`, copy additional iso media files.
7373
([iso](#iso-type))
7474

75+
23. If [--output-pxe-artifacts-dir](./cli.md#output-pxe-artifacts-dir) is specified,
76+
then export the ISO image contents to the specified folder.
77+
7578
### /etc/resolv.conf
7679

7780
The `/etc/resolv.conf` file is overridden during customization so that the package
@@ -151,6 +154,9 @@ os:
151154
- [permissions](#permissions-string)
152155
- [kernelCommandLine](#iso-kernelcommandline)
153156
- [extraCommandLine](#extracommandline-string)
157+
- [pxe](#pxe-type)
158+
- [isoImageBaseUrl](#isoimagebaseurl-string)
159+
- [isoImageFileUrl](#isoimagefileurl-string)
154160
- [os type](#os-type)
155161
- [resetBootLoaderType](#resetbootloadertype-string)
156162
- [hostname](#hostname-string)
@@ -274,7 +280,11 @@ os:
274280
275281
### iso [[iso](#iso-type)]
276282
277-
Specifies the configuration for the generated ISO media.
283+
Optionally specifies the configuration for the generated ISO media.
284+
285+
### pxe [[pxe](#pxe-type)]
286+
287+
Optionally specifies the PXE-specific configuration for the generated OS artifacts.
278288
279289
### os [[os](#os-type)]
280290
@@ -316,6 +326,76 @@ Must be a multiple of 1 MiB.
316326

317327
The partitions to provision on the disk.
318328

329+
## pxe type
330+
331+
Specifies the PXE-specific configuration for the generated OS artifacts.
332+
333+
### isoImageBaseUrl [string]
334+
335+
Specifies the base URL for the ISO image to download at boot time. The Azure
336+
Linux Image Customizer will append the output image name to the specified base
337+
URL to form the full URL for downloading the image. The output image name is
338+
specified on the command-line using the `--output-image file` argument (see the
339+
[command-line interface](./cli.md) document for more details).
340+
341+
This can be useful if the ISO image name changes with each build and the
342+
script deploying the artifacts to the PXE server does not update grub.cfg with
343+
the ISO image name.
344+
345+
For example,
346+
347+
- If the user has the following content in the configuration file:
348+
349+
```yaml
350+
pxe:
351+
isoImageBaseUrl: http://hostname-or-ip/iso-publish-path
352+
```
353+
354+
- and specifies the following on the command line:
355+
356+
```bash
357+
sudo imagecustomizer \
358+
--image-file "./input/azure-linux.vhdx" \
359+
--config-file "./input/customization-config.yaml" \
360+
--rpm-source "./input/rpms" \
361+
--build-dir "./build" \
362+
--output-image-format "iso" \
363+
--output-image-file "./build/output/output.iso" \
364+
--output-pxe-artifacts-dir "./build/output/pxe-artifacts"
365+
```
366+
367+
- then, during PXE booting, the ISO image will be downloaded from:
368+
369+
```bash
370+
http://hostname-or-ip/iso-publish-path/output.iso
371+
```
372+
373+
This field is mutually exclusive with `isoImageFileUrl`.
374+
375+
For an overview of Azure Linux Image Customizer support for PXE, see the
376+
[PXE support page](./pxe.md).
377+
378+
### isoImageFileUrl [string]
379+
380+
Specifies the URL of the ISO image to download at boot time.
381+
The ISO image must be a LiveOS ISO image generated by the Azure Linux Image
382+
Customizer. The booting process will pivot to the root file system embedded
383+
in the ISO image after downloading it.
384+
385+
PXE Configuration Example:
386+
387+
- ```yaml
388+
pxe:
389+
isoImageFileUrl: http://hostname-or-ip/iso-publish-path/my-liveos.iso
390+
```
391+
392+
The supported download protocols are: nfs, http, https, ftp, torent, tftp.
393+
394+
This field is mutually exclusive with `isoImageBaseUrl`.
395+
396+
For an overview of Azure Linux Image Customizer support for PXE, see the
397+
[PXE support page](./pxe.md).
398+
319399
## iso type
320400

321401
Specifies the configuration for the generated ISO media.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Azure Linux Image Customizer PXE Support
2+
3+
## PXE Overview
4+
5+
Booting a host with an OS served over the network is one of the most popular
6+
methods for booting baremetal hosts. It requires no physical access to individual
7+
hosts and also centralizes the deployment configuration to a single server.
8+
9+
One way of enabling such setup is using the PXE (Preboot eXecution Environment)
10+
Boot protocol. The user can setup a server with all the OS artifacts, a DHCP
11+
endpoint, and a tftp connection endpoint. When a client machine is powered on,
12+
and its firmware will look for a DHCP server on the same network and find the
13+
one configured by the user.
14+
15+
The DHCP server will serve information about the tftp endpoint to the client,
16+
and the client firmware can then proceed with retrieving the OS artifacts over
17+
tftp, then loading them into memory, and finally handing control over to the
18+
loaded OS.
19+
20+
The tftp protocol expects certain artifacts to be present on the server:
21+
22+
- the boot loader (the shim and something like grub).
23+
- the boot loader configuration (like grub.cfg).
24+
- the kernel image.
25+
- the initrd image.
26+
27+
Once retrieved, the boot loader is run. Then the boot loader reads the
28+
boot loader configuration and then transfers control over to the kernel image
29+
with the retrieved initrd image as its file system.
30+
31+
The initrd image is customized to perform the next set of tasks now that an
32+
OS is running. The tasks can range from just running some local scripts all
33+
the way to installing another OS.
34+
35+
## LiveOS ISOs and PXE Support
36+
37+
A LiveOS ISO image is a bootable ISO image that runs all the necessary
38+
components from memory (i.e. does not need to install anything to the host
39+
persistent storage).
40+
41+
The necessary components can be either embedded into the initrd image itself
42+
or embedded into a separate 'rootfs' image (to allow much smaller
43+
initrd images). If separate, then, the initrd image must be configured with an
44+
agent that will look for the rootfs image, and transition control over to the
45+
rootfs at boot time.
46+
47+
Dracut provides the `dmsquash-live` module which managed this transition from
48+
the initrd image over to the rootfs image.
49+
50+
The **Azure Linux Image Customizer** produces such LiveOS ISO images. A typical
51+
image holds the following artifacts:
52+
53+
- the boot loader (the shim and something like grub).
54+
- the boot loader configuration.
55+
- the kernel image.
56+
- the initrd image.
57+
- the rootfs image.
58+
- other user defined artifacts (optional).
59+
60+
Note that the first 4 artifacts are what is necessary to get an OS kernel up
61+
and running in a network boot scenario. What remains for a successful booting
62+
of a LiveOS over the network is to make the rootfs image available for the final
63+
transition (during the initrd phase).
64+
65+
Dracut enables that entire flow through the use of the `livenet` module - where
66+
it inspects the `root=live:liveos-iso-url` kernel parameter from the boot loader
67+
config file, and if it recognizes the `liveos-iso-url` protocol, it downloads
68+
the ISO, and then proceeds to pivot to the embedded rootfs image.
69+
70+
The user can customize the rootfs using the Azure Linux Image Customizer as
71+
usual. In case of additional artifacts that need downloading, the user can
72+
install a daemon on the rootfs which will run when control is transferred to
73+
the rootfs image and download any additional items.
74+
75+
## Creating and Deploying PXE Boot Artifacts
76+
77+
The Azure Linux Image Customizer produces LiveOS ISO images that are also PXE
78+
bootable. So, the user can simply create an ISO image as usual, and the output
79+
can be taken and deployed to a PXE server.
80+
81+
To make the deployment of the generated artifacts easier for the user, the
82+
Azure Linux Image Customizer offers the following configurations:
83+
84+
- In the input configuration, there is a `pxe` node under which the user can
85+
configure PXE related properties - like the URL of the LiveOS ISO image to
86+
download (note that this image is the same image being built).
87+
See the [Azure Linux Image Customizer configuration](./configuration.md#pxe-type)
88+
page for more information.
89+
- When invoking the Azure Linux Image Customizer, the user can also elect to
90+
export the artifacts to a local folder.
91+
See the [Azure Linux Image Customizer command line](./cli.md#output-pxe-artifacts-dir)
92+
page for more information.
93+
94+
Below is a list of required artifacts and where on the PXE server they should
95+
be deployed:
96+
97+
```
98+
ISO media layout artifacts local folder target on PXE server
99+
----------------------- ------------------------ ------------------------------
100+
|- efi | <tftp-server-root>
101+
|- boot | |
102+
|- bootx64.efi |- bootx64.efi |- bootx64.efi
103+
|- grubx64.efi |- grubx64.efi |- grubx64.efi
104+
|- boot |- boot |- boot
105+
|- grub2 |- grub2 |- grub2
106+
|- grub-pxe.cfg |- grub.cfg |- grub.cfg
107+
|- grubenv |- grubenv |- grubenv
108+
|- grub.cfg
109+
|- vmlinuz |- vmlinuz |- vmlinuz
110+
|- initrd.img |- initrd.img |- initrd.img
111+
112+
<yyyy-server-root>
113+
|- other-user-artifacts |- other-user-artifacts |- other-user-artifacts
114+
|- <liveos>.iso |- <liveos>.iso
115+
```
116+
117+
Notes:
118+
119+
- Note that the `/boot/grub2/grub.cfg` file in the ISO media is not used for
120+
PXE booting. Instead, the `/boot/grub2/grub-pxe.cfg` gets renamed to `grub.cfg`
121+
and is used instead.
122+
- `yyyy` can be any protocol supported by Dracut's `livenet` module (i.e
123+
tftp, http, etc).
124+
- The ISO image file location under the server root is customizable -
125+
but it must be such that its URL matches what is specified in the grub.cfg
126+
`root=live:<URL>`.
127+
- While the core OS artifacts (the bootloader, its configuration, the kernel,
128+
initrd image, and rootfs image) will be downloaded and used automatically,
129+
the user will need to independently implement a way to download any
130+
additional artifacts. For example, the user can implement a daemon (and place
131+
it on the root file system) that will reach out and download the additional
132+
artifacts when it is up and running. The daemon can be configured with where
133+
to download the artifacts from, and what to do with them.

toolkit/tools/imagecustomizer/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var (
2727
rpmSources = app.Flag("rpm-source", "Path to a RPM repo config file or a directory containing RPMs.").Strings()
2828
disableBaseImageRpmRepos = app.Flag("disable-base-image-rpm-repos", "Disable the base image's RPM repos as an RPM source").Bool()
2929
enableShrinkFilesystems = app.Flag("shrink-filesystems", "Enable shrinking of filesystems to minimum size. Supports ext2, ext3, ext4 filesystem types.").Bool()
30+
outputPXEArtifactsDir = app.Flag("output-pxe-artifacts-dir", "Create a directory with customized image PXE booting artifacts. '--output-image-format' must be set to 'iso'.").String()
3031
logFlags = exe.SetupLogFlags(app)
3132
profFlags = exe.SetupProfileFlags(app)
3233
timestampFile = app.Flag("timestamp-file", "File that stores timestamps for this program.").String()
@@ -70,7 +71,8 @@ func customizeImage() error {
7071
var err error
7172

7273
err = imagecustomizerlib.CustomizeImageWithConfigFile(*buildDir, *configFile, *imageFile,
73-
*rpmSources, *outputImageFile, *outputImageFormat, *outputSplitPartitionsFormat, !*disableBaseImageRpmRepos, *enableShrinkFilesystems)
74+
*rpmSources, *outputImageFile, *outputImageFormat, *outputSplitPartitionsFormat, *outputPXEArtifactsDir,
75+
!*disableBaseImageRpmRepos, *enableShrinkFilesystems)
7476
if err != nil {
7577
return err
7678
}

toolkit/tools/imagecustomizerapi/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import "fmt"
88
type Config struct {
99
Storage Storage `yaml:"storage"`
1010
Iso *Iso `yaml:"iso"`
11+
Pxe *Pxe `yaml:"pxe"`
1112
OS *OS `yaml:"os"`
1213
Scripts Scripts `yaml:"scripts"`
1314
}
@@ -27,6 +28,13 @@ func (c *Config) IsValid() (err error) {
2728
}
2829
}
2930

31+
if c.Pxe != nil {
32+
err = c.Pxe.IsValid()
33+
if err != nil {
34+
return fmt.Errorf("invalid 'pxe' field:\n%w", err)
35+
}
36+
}
37+
3038
hasResetBootLoader := false
3139
if c.OS != nil {
3240
err = c.OS.IsValid()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package imagecustomizerapi
5+
6+
import (
7+
"fmt"
8+
"net/url"
9+
"strings"
10+
)
11+
12+
var PxeIsoDownloadProtocols = []string{"ftp://", "http://", "https://", "nfs://", "tftp://"}
13+
14+
// Iso defines how the generated iso media should be configured.
15+
type Pxe struct {
16+
IsoImageBaseUrl string `yaml:"isoImageBaseUrl"`
17+
IsoImageFileUrl string `yaml:"isoImageFileUrl"`
18+
}
19+
20+
func IsValidPxeUrl(urlString string) error {
21+
if urlString == "" {
22+
return nil
23+
}
24+
25+
_, err := url.Parse(urlString)
26+
if err != nil {
27+
return fmt.Errorf("invalid URL value (%s):\n%w", urlString, err)
28+
}
29+
30+
protocolFound := false
31+
for _, protocol := range PxeIsoDownloadProtocols {
32+
if strings.HasPrefix(urlString, protocol) {
33+
protocolFound = true
34+
break
35+
}
36+
}
37+
if !protocolFound {
38+
return fmt.Errorf("unsupported iso image URL protocol in (%s). One of (%v) is expected.", urlString, PxeIsoDownloadProtocols)
39+
}
40+
41+
return nil
42+
}
43+
44+
func (p *Pxe) IsValid() error {
45+
if p.IsoImageBaseUrl != "" && p.IsoImageFileUrl != "" {
46+
return fmt.Errorf("cannot specify both 'isoImageBaseUrl' and 'isoImageFileUrl' at the same time.")
47+
}
48+
err := IsValidPxeUrl(p.IsoImageBaseUrl)
49+
if err != nil {
50+
return fmt.Errorf("invalid 'isoImageBaseUrl' field value (%s):\n%w", p.IsoImageBaseUrl, err)
51+
}
52+
err = IsValidPxeUrl(p.IsoImageFileUrl)
53+
if err != nil {
54+
return fmt.Errorf("invalid 'isoImageFileUrl' field value (%s):\n%w", p.IsoImageFileUrl, err)
55+
}
56+
return nil
57+
}

toolkit/tools/pkg/imagecustomizerlib/customizefiles_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestCustomizeImageAdditionalFiles(t *testing.T) {
8080

8181
// Customize image.
8282
err := CustomizeImageWithConfigFile(buildDir, configFile, baseImage, nil, outImageFilePath, "raw", "",
83-
false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
83+
"" /*outputPXEArtifactsDir*/, false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
8484
if !assert.NoError(t, err) {
8585
return
8686
}
@@ -126,7 +126,7 @@ func TestCustomizeImageAdditionalFilesInfiniteFile(t *testing.T) {
126126

127127
// Customize image.
128128
err := CustomizeImageWithConfigFile(buildDir, configFile, baseImage, nil, outImageFilePath, "raw", "",
129-
false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
129+
"" /*outputPXEArtifactsDir*/, false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
130130
assert.ErrorContains(t, err, "failed to copy (/dev/zero)")
131131
assert.ErrorContains(t, err, "No space left on device")
132132
}
@@ -202,7 +202,7 @@ func TestCustomizeImageAdditionalDirs(t *testing.T) {
202202

203203
// Customize image.
204204
err := CustomizeImageWithConfigFile(buildDir, configFile, baseImage, nil, outImageFilePath, "raw", "",
205-
false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
205+
"" /*outputPXEArtifactsDir*/, false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
206206
if !assert.NoError(t, err) {
207207
return
208208
}
@@ -258,7 +258,7 @@ func TestCustomizeImageAdditionalDirsInfiniteFile(t *testing.T) {
258258

259259
// Customize image.
260260
err = CustomizeImage(buildDir, testTmpDir, &config, baseImage, nil, outImageFilePath, "raw", "",
261-
false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
261+
"" /*outputPXEArtifactsDir*/, false /*useBaseImageRpmRepos*/, false /*enableShrinkFilesystems*/)
262262
assert.ErrorContains(t, err, "failed to copy directory")
263263
assert.ErrorContains(t, err, "failed to copy file")
264264
assert.ErrorContains(t, err, "No space left on device")

0 commit comments

Comments
 (0)