From 01ecf1bea3752a19e9f4d42ac8997978ab53d8ec Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Mon, 2 Jun 2025 22:03:19 +0000 Subject: [PATCH 1/6] update --- docs/imagecustomizer/api/cosi.md | 264 +++++++++++------- .../pkg/imagecustomizerlib/cosicommon.go | 70 ++++- .../pkg/imagecustomizerlib/cosimetadata.go | 20 +- 3 files changed, 231 insertions(+), 123 deletions(-) diff --git a/docs/imagecustomizer/api/cosi.md b/docs/imagecustomizer/api/cosi.md index 722d231e12..702bb52bb3 100644 --- a/docs/imagecustomizer/api/cosi.md +++ b/docs/imagecustomizer/api/cosi.md @@ -14,101 +14,110 @@ The COSI file MUST be an uncompressed tarball. The file extension SHOULD be `.co The tarball MUST contain the following files: - `metadata.json`: A JSON file that contains the metadata of the COSI file. -- Partition image files in the folder `images`: The actual partition images - that will be used to install the OS. +- Filesystem image files in the folder `images/`: The actual filesystem images + that Trident will use to install the OS. -If the tarball contains other files, readers MUST ignore them. A writer SHOULD NOT -add any other files to the tarball. +To allow for future extensions, the tarball MAY contain other files, but Trident +MUST ignore them. The tarball SHOULD NOT contain any extra files that will not +be used by Trident. -The tarball MUST NOT have a common root directory. The `metadata.json` file and the -`images` directory MUST be in the root directory of the tarball. +### Layout -The metadata file SHOULD be placed at the beginning of the tarball to allow for -quick access to the metadata without needing to traverse the entire tarball. +The tarball MUST NOT have a common root directory. The metadata file MUST be at +the root of the tarball. If it were extracted with a standard `tar` invocation, +the metadata file would be placed in the current directory. -## Partition Image Files +The metadata file SHOULD, be placed at the beginning of the tarball to allow for +quick access to the metadata without having to traverse the entire tarball. -The partition image files MUST be raw partition images that are compressed using ZSTD -compression. +### Partition Image Files -All partition image files MUST be in the `images` directory or one of its -subdirectories. +The partition image files are the actual images that Trident will use to install +the OS. These MUST be raw partition images. -## Metadata JSON File +The image files SHOULD be compressed. They SHOULD use ZSTD compression. Trident +only supports ZSTD-compressed images at the time of writing (2024-09-25), but +that could change in the future. Not using ZSTD-compressed images will result in +Trident failing to install the OS. -The metadata file MUST be named `metadata.json` and MUST be a valid JSON file. +They MUST be located in a directory called `images/` inside the tarball. They +MAY be placed in subdirectories of `images/` to organize them. Trident MUST be +able to handle images in subdirectories. -## Metadata JSON Schema +### Metadata JSON File -### Root Object +The metadata file MUST be named `metadata.json` and MUST be at the root of the +tarball. The metadata file MUST be a valid JSON file. + +#### Schema + +##### Root Object The metadata file MUST contain a JSON object with the following fields: -| Field | Type | Required | Description | -| ------------ | -------------------------------------- | -------- | ------------------------------------------------------ | -| `version` | string `MAJOR.MINOR` | Yes | The version of the metadata schema. MUST be `1.0`. | -| `osArch` | [OsArchitecture](#osarchitecture-enum) | Yes | The CPU architecture of the OS. | -| `osRelease` | string | Yes | The contents of OS's `/etc/os-release` file. | -| `images` | [Image](#image-object)[] | Yes | Metadata of partition images that contain filesystems. | -| `osPackages` | [OsPackage](#ospackage-object)[] | No | The list of packages installed in the OS. | -| `id` | UUID (string, case insensitive) | No | A unique identifier for the COSI file. | - -If the object contains other fields, readers MUST ignore them. A writer SHOULD NOT -add any other files to the object. - -### `Image` Object - -| Field | Type | Required | Description | -| ------------ | ------------------------------------ | -------- | ----------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the image file in the tarball. | -| `mountPoint` | string | Yes | The mount point of the partition. | -| `fsType` | string | Yes | The filesystem type of the partition. [1] | -| `fsUuid` | string | Yes | The UUID of the filesystem. | -| `partType` | UUID (string, case insensitive) | Yes | The GPT partition type. [2] [3] [4] | -| `verity` | [VerityConfig](#verityconfig-object) | No | The verity metadata of the partition. | +| Field | Type | Added in | Required | Description | +| ------------ | -------------------------------------- | -------- | --------------- | ------------------------------------------------ | +| `version` | string `MAJOR.MINOR` | 1.0 | Yes (since 1.0) | The version of the metadata schema. | +| `osArch` | [OsArchitecture](#osarchitecture-enum) | 1.0 | Yes (since 1.0) | The architecture of the OS. | +| `osRelease` | string | 1.0 | Yes (since 1.0) | The contents of `/etc/os-release` verbatim. | +| `images` | [Filesystem](#filesystem-object)[] | 1.0 | Yes (since 1.0) | Filesystem metadata. | +| `osPackages` | [OsPackage](#ospackage-object)[] | 1.0 | Yes (since 1.1) | The list of packages installed in the OS. | +| `id` | UUID (string, case insensitive) | 1.0 | No | A unique identifier for the COSI file. | + +If the object contains other fields, readers MUST ignore them. A writer SHOULD +NOT add any other files to the object. + +##### `Filesystem` Object + +This object carries information about a filesystem and the partition it comes +from in a virtual disk. + +| Field | Type | Added in | Required | Description | +| ------------ | ------------------------------------ | -------- | ---------------- | ----------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the image file in the tarball. | +| `mountPoint` | string | 1.0 | Yes (since 1.0) | The mount point of the filesystem. | +| `fsType` | string | 1.0 | Yes (since 1.0) | The filesystem's type. [1] | +| `fsUuid` | string | 1.0 | Yes (since 1.0) | The UUID of the filesystem. [2] | +| `partType` | UUID (string, case insensitive) | 1.0 | Yes (since 1.0) | The GPT partition type. [3] [4] [5] | +| `verity` | [VerityConfig](#verityconfig-object) | 1.0 | Conditionally[6] | The verity metadata of the filesystem. | _Notes:_ -- **[1]** It MUST use the name recognized by the Linux kernel. For example, `ext4` for +- **[1]** It MUST use the name recognized by the kernel. For example, `ext4` for ext4 filesystems, `vfat` for FAT32 filesystems, etc. - -- **[2]** It MUST be a UUID defined by the [Discoverable - Partition Specification +- **[2]** It MUST be unique across all filesystems in the COSI tarball. + Additionally, volumes in an A/B volume pair MUST have unique filesystem UUIDs. +- **[3]** It MUST be a UUID defined by the [Discoverable Partition Specification (DPS)](https://uapi-group.org/specifications/specs/discoverable_partitions_specification/) when the applicable type exists in the DPS. Other partition types MAY be used for types not defined in DPS (e.g. Windows partitions). - -- **[3]** The EFI Sytem Partition (ESP) MUST be identified with the UUID +- **[4]** The EFI Sytem Partition (ESP) MUST be identified with the UUID established by the DPS: `c12a7328-f81f-11d2-ba4b-00a0c93ec93b`. - -- **[4]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic +- **[5]** Should default to `0fc63daf-8483-4772-8e79-3d69d8477de4` (Generic Linux Data) if the partition type cannot be determined. +- **[6]** The `verity` field MUST be specified if the OS is configured to open this + filesystem with `dm-verity`. Otherwise, it MUST be omitted OR set to `null`. -### `VerityConfig` Object +##### `VerityConfig` Object The `VerityConfig` object contains information required to set up a verity -device on top of a data partition. +device on top of a data device. -| Field | Type | Required | Description | -| ---------- | ------------------------------ | -------- | -------------------------------------------------------- | -| `image` | [ImageFile](#imagefile-object) | Yes | Details of the hash partition image file in the tarball. | -| `roothash` | string | Yes | Verity root hash. | +| Field | Type | Added in | Required | Description | +| ---------- | ------------------------------ | -------- | --------------- | -------------------------------------------------------- | +| `image` | [ImageFile](#imagefile-object) | 1.0 | Yes (since 1.0) | Details of the hash partition image file in the tarball. | +| `roothash` | string | 1.0 | Yes (since 1.0) | Verity root hash. | -### `ImageFile` Object - -| Field | Type | Required | Description | -| ------------------ | ------ | -------- | ----------------------------------------------------------------------------------------- | -| `path` | string | Yes | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | -| `compressedSize` | number | Yes | Size of the compressed image in bytes. | -| `uncompressedSize` | number | Yes | Size of the raw uncompressed image in bytes. | -| `sha384` | string | No[5] | SHA-384 hash of the compressed hash image. | - -_Notes:_ +##### `ImageFile` Object -- **[5]** The `sha384` field is optional, but it is RECOMMENDED to include it for - integrity verification. +| Field | Type | Added in | Required | Description | +| ------------------ | ------ | -------- | --------------- | ----------------------------------------------------------------------------------------- | +| `path` | string | 1.0 | Yes (since 1.0) | Absolute path of the compressed image file inside the tarball. MUST start with `images/`. | +| `compressedSize` | number | 1.0 | Yes (since 1.0) | Size of the compressed image in bytes. | +| `uncompressedSize` | number | 1.0 | Yes (since 1.0) | Size of the raw uncompressed image in bytes. | +| `sha384` | string | 1.0 | Yes (since 1.1) | SHA-384 hash of the compressed hash image. | -### `OsArchitecture` Enum +##### `OsArchitecture` Enum The `osArch` field in the root object MUST be a string that represents the architecture of the OS. The following table lists the valid values for the @@ -119,28 +128,29 @@ architecture of the OS. The following table lists the valid values for the | `x86_64` | AMD64 or Intel 64-bit architecture. | | `arm64` | ARM 64-bit architecture. | -### `OsPackage` Object +_Note:_ The `osArch` field uses the names reported by `uname -m` for consistency. +The `osArch` field is case-insensitive. -When present, the `osPackages` field in the root object MUST contain an array of -`OsPackage` objects. Each object represents a package installed in the OS. +##### `OsPackage` Object -A reader MAY use this field to determine if the OS is missing any packages that are -required for how the user intends to use the OS image. +The `osPackages` field in the root object MUST contain an array of `OsPackage` +objects. Each object represents a package installed in the OS. -| Field | Type | Required | Description | -| --------- | ------ | -------- | ------------------------------------- | -| `name` | string | Yes | The name of the package. | -| `version` | string | Yes | The version of the package installed. | -| `release` | string | No | The release number of the package. | -| `arch` | string | No | The CPU architecture of the package. | +| Field | Type | Added in | Required | Description | +| --------- | ------ | -------- | --------------- | ------------------------------------- | +| `name` | string | 1.0 | Yes (since 1.0) | The name of the package. | +| `version` | string | 1.0 | Yes (since 1.0) | The version of the package installed. | +| `release` | string | 1.0 | Yes (since 1.1) | The release of the package. | +| `arch` | string | 1.0 | Yes (since 1.1) | The architecture of the package. | -### Samples -#### Simple Image +#### Samples + +##### Simple Image ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -169,15 +179,36 @@ required for how the user intends to use the OS image. "verity": null } ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", + "osPackages": [ + { + "name": "bash", + "version": "5.1.8", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "coreutils", + "version": "8.32", + "release": "1.azl3", + "arch": "x86_64" + }, + { + "name": "systemd", + "version": "255", + "release": "20.azl3", + "arch": "x86_64" + }, + // More packages... + ] } ``` -#### Verity Image +##### Verity Image with UKI ```json { - "version": "1.0", + "version": "1.1", "images": [ { "image": { @@ -202,31 +233,13 @@ required for how the user intends to use the OS image. }, // More images... ], - "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n" -} -``` - -#### Packages - -```json -{ - "version": "1.0", - "images": [ - // Images... - ], - "osRelease": "", + "osRelease": "NAME=\"Microsoft Azure Linux\"\nVERSION=\"3.0.20240824\"\nID=azurelinux\nVERSION_ID=\"3.0\"\nPRETTY_NAME=\"Microsoft Azure Linux 3.0\"\nANSI_COLOR=\"1;34\"\nHOME_URL=\"https://aka.ms/azurelinux\"\nBUG_REPORT_URL=\"https://aka.ms/azurelinux\"\nSUPPORT_URL=\"https://aka.ms/azurelinux\"\n", "osPackages": [ - { - "name": "bash", - "version": "5.1.8" - }, - { - "name": "coreutils", - "version": "8.32" - }, { "name": "systemd", - "version": "255" + "version": "255", + "release": "20.azl3", + "arch": "x86_64" }, // More packages... ] @@ -240,12 +253,25 @@ required for how the user intends to use the OS image. - Tar is simple and ubiquitous. It is easy to create and extract tarballs on virtually any platform. There are native libraries for virtually every programming language to handle tarballs, including Rust and Go. +- Tar is a super simple tape format. It is just a stream of files with metadata + at the beginning. This makes it easy to read and write. **Why an uncompressed tarball?** -- This allows the metadata file to be easily read without needing to decompress and - extract the entire tarball. Also, compressing the tarball doesn't provide any - meaningful size reductions since the partition images are all compressed individually. +- The images SHOULD be compressed, and other than that the file should be pretty + light-weight. Compressing the entire tarball does not yield a significant size + reduction, if at all. This also allows us to read the metadata without having + to extract the entire tarball. + +**Why not ZIP?** + +- ZIP is more complex than tar. It has more features, notably an index at the + end of the file. However, to compute the hash of the file, we need to read it + through, anyway, so we can index the file as we read it. Even in cases where + we don't need to compute the hash, to take full advantage of the index, we + would need to implement our own ZIP reader. +- ZSTD support in ZIP is not very + widespread. **Why not use a custom format?** @@ -253,3 +279,25 @@ required for how the user intends to use the OS image. cases, specifically network streaming. However, the complexity of creating and maintaining a custom format outweighs the benefits. Tar is simple and good enough for our needs. + +**Why not use VHD or VHDX?** + +- VHD and VHDX are complex formats that are not designed for our use case. They + are designed to be used as virtual disks, not as a simple container for + partition images. They are also not as portable as tarballs. +- They do not have a standard way to store metadata. The spec does include some + empty space reserved for future expansion, but using it would require us to + implement our own fork of the VHD/VHDX spec. + + **What about a VHD+Metadata?** + + - Putting the metadata in a separate file would defeat the purpose of having a + single file. + +**What other formats were considered?** + +- We considered using a custom format, but the complexity of creating and + maintaining a custom format outweighs the benefits. +- SquashFS was considered, but it would only change the container around the + filesystems images. When considering only the container, there was no real + practical benefit to using SquashFS over Tar. diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index 54f666dfc9..5bce82fa21 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -10,16 +10,18 @@ import ( "path" "path/filepath" "runtime" + "strings" "github.com/microsoft/azurelinux/toolkit/tools/imagegen/diskutils" "github.com/microsoft/azurelinux/toolkit/tools/internal/logger" "github.com/microsoft/azurelinux/toolkit/tools/internal/safeloopback" + "github.com/microsoft/azurelinux/toolkit/tools/internal/shell" ) type ImageBuildData struct { Source string KnownInfo outputPartitionMetadata - Metadata *Image + Metadata *FileSystem VeritySource string } @@ -47,8 +49,20 @@ func convertToCosi(ic *ImageCustomizerParameters) error { defer os.Remove(path.Join(outputDir, partition.PartitionFilename)) } + buildDir := filepath.Join(outputDir, "build") + existingImageConnection, _, _, err := connectToExistingImage(ic.rawImageFile, buildDir, "imageroot", true) + if err != nil { + return err + } + defer existingImageConnection.Close() + + osPackages, err := getAllPackagesFromChroot(existingImageConnection) + if err != nil { + return err + } + err = buildCosiFile(outputDir, ic.outputImageFile, partitionMetadataOutput, ic.verityMetadata, - ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease) + ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease, osPackages) if err != nil { return fmt.Errorf("failed to build COSI file:\n%w", err) } @@ -65,7 +79,7 @@ func convertToCosi(ic *ImageCustomizerParameters) error { func buildCosiFile(sourceDir string, outputFile string, partitions []outputPartitionMetadata, verityMetadata []verityDeviceMetadata, partUuidToFstabEntry map[string]diskutils.FstabEntry, - imageUuidStr string, osRelease string, + imageUuidStr string, osRelease string, osPackages []OsPackage, ) error { // Pre-compute a map for quick lookup of partition metadata by UUID partUuidToMetadata := make(map[string]outputPartitionMetadata) @@ -93,7 +107,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti continue } - metadataImage := Image{ + metadataImage := FileSystem{ Image: ImageFile{ Path: path.Join("images", partition.PartitionFilename), UncompressedSize: partition.UncompressedSize, @@ -146,11 +160,12 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti } metadata := MetadataJson{ - Version: "1.0", - OsArch: getArchitectureForCosi(), - Id: imageUuidStr, - Images: make([]Image, len(imageData)), - OsRelease: osRelease, + Version: "1.0", + OsArch: getArchitectureForCosi(), + Id: imageUuidStr, + Images: make([]FileSystem, len(imageData)), + OsRelease: osRelease, + OsPackages: osPackages, } // Copy updated metadata @@ -303,3 +318,40 @@ func getArchitectureForCosi() string { } return runtime.GOARCH } + +func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, error) { + var packages []OsPackage + + err := imageConnection.Chroot().UnsafeRun(func() error { + // Run rpm query inside chroot + out, _, err := shell.Execute( + "rpm", "-qa", "--queryformat", "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n", + ) + if err != nil { + return fmt.Errorf("failed to list installed RPMs:\n%w", err) + } + + lines := strings.Split(strings.TrimSpace(out), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 4 { + logger.Log.Infof("Skipping malformed RPM line while parsing osPackages for COSI: %q", line) + continue + } + pkg := OsPackage{ + Name: parts[0], + Version: parts[1], + Release: parts[2], + Arch: parts[3], + } + packages = append(packages, pkg) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to get packages from chroot:\n%w", err) + } + + return packages, nil +} diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go index d1a4e95f15..aaee48c83d 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go @@ -1,14 +1,15 @@ package imagecustomizerlib type MetadataJson struct { - Version string `json:"version"` - OsArch string `json:"osArch"` - Images []Image `json:"images"` - OsRelease string `json:"osRelease"` - Id string `json:"id"` + Version string `json:"version"` + OsArch string `json:"osArch"` + Images []FileSystem `json:"images"` + OsRelease string `json:"osRelease"` + Id string `json:"id"` + OsPackages []OsPackage `json:"osPackages,omitempty"` } -type Image struct { +type FileSystem struct { Image ImageFile `json:"image"` MountPoint string `json:"mountPoint"` FsType string `json:"fsType"` @@ -28,3 +29,10 @@ type ImageFile struct { UncompressedSize uint64 `json:"uncompressedSize"` Sha384 string `json:"sha384"` } + +type OsPackage struct { + Name string `json:"name"` + Version string `json:"version"` + Release string `json:"release,omitempty"` + Arch string `json:"arch,omitempty"` +} From 13d5851451f4dbf02601d35cbce2d1a52805739b Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Mon, 2 Jun 2025 22:11:41 +0000 Subject: [PATCH 2/6] update --- toolkit/tools/pkg/imagecustomizerlib/cosicommon.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index 5bce82fa21..a7cd4e0921 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -74,6 +74,11 @@ func convertToCosi(ic *ImageCustomizerParameters) error { return err } + err = existingImageConnection.CleanClose() + if err != nil { + return err + } + return nil } @@ -160,7 +165,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti } metadata := MetadataJson{ - Version: "1.0", + Version: "1.1", OsArch: getArchitectureForCosi(), Id: imageUuidStr, Images: make([]FileSystem, len(imageData)), @@ -323,7 +328,6 @@ func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, er var packages []OsPackage err := imageConnection.Chroot().UnsafeRun(func() error { - // Run rpm query inside chroot out, _, err := shell.Execute( "rpm", "-qa", "--queryformat", "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n", ) @@ -335,7 +339,7 @@ func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, er for _, line := range lines { parts := strings.Fields(line) if len(parts) != 4 { - logger.Log.Infof("Skipping malformed RPM line while parsing osPackages for COSI: %q", line) + logger.Log.Infof("Skipping malformed RPM line while parsing installed RPMs for COSI: %q", line) continue } pkg := OsPackage{ From 6aa0f02c436a22427ef4a029b8830c82b4c548f6 Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Mon, 2 Jun 2025 23:54:51 +0000 Subject: [PATCH 3/6] update --- docs/imagecustomizer/api/cosi.md | 7 +++++++ toolkit/tools/pkg/imagecustomizerlib/cosicommon.go | 4 ++-- .../tools/pkg/imagecustomizerlib/cosimetadata.go | 14 +++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/imagecustomizer/api/cosi.md b/docs/imagecustomizer/api/cosi.md index 702bb52bb3..f4fc32e3f1 100644 --- a/docs/imagecustomizer/api/cosi.md +++ b/docs/imagecustomizer/api/cosi.md @@ -5,6 +5,13 @@ nav_order: 3 # Composable Operating System Image (COSI) Specification +## Revision Summary + +| Revision | Spec Date | +|----------|-------------| +| 1.1 | TBD | +| 1.0 | 2024-10-09 | + ## COSI File Format The COSI file MUST be an uncompressed tarball. The file extension SHOULD be `.cosi`. diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index a7cd4e0921..aaef884edd 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -137,7 +137,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti return fmt.Errorf("missing metadata for hash partition UUID:\n%s", verity.hashPartUuid) } - metadataImage.Verity = &Verity{ + metadataImage.Verity = &VerityConfig{ Roothash: verity.rootHash, Image: ImageFile{ Path: path.Join("images", hashPartition.PartitionFilename), @@ -301,7 +301,7 @@ func populateMetadata(data *ImageBuildData) error { return nil } -func populateVerityMetadata(source string, verity *Verity) error { +func populateVerityMetadata(source string, verity *VerityConfig) error { if source == "" && verity == nil { return nil } diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go index aaee48c83d..95c307caee 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go @@ -10,15 +10,15 @@ type MetadataJson struct { } type FileSystem struct { - Image ImageFile `json:"image"` - MountPoint string `json:"mountPoint"` - FsType string `json:"fsType"` - FsUuid string `json:"fsUuid"` - PartType string `json:"partType"` - Verity *Verity `json:"verity"` + Image ImageFile `json:"image"` + MountPoint string `json:"mountPoint"` + FsType string `json:"fsType"` + FsUuid string `json:"fsUuid"` + PartType string `json:"partType"` + Verity *VerityConfig `json:"verity"` } -type Verity struct { +type VerityConfig struct { Image ImageFile `json:"image"` Roothash string `json:"roothash"` } From 5b654a42b9dcf9d6bca19aaff972d443058d4909 Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Tue, 3 Jun 2025 18:49:59 +0000 Subject: [PATCH 4/6] address feedback --- toolkit/tools/pkg/imagecustomizerlib/cosicommon.go | 4 ++-- toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index aaef884edd..e32eb260a6 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -165,7 +165,7 @@ func buildCosiFile(sourceDir string, outputFile string, partitions []outputParti } metadata := MetadataJson{ - Version: "1.1", + Version: "1.0", OsArch: getArchitectureForCosi(), Id: imageUuidStr, Images: make([]FileSystem, len(imageData)), @@ -339,7 +339,7 @@ func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, er for _, line := range lines { parts := strings.Fields(line) if len(parts) != 4 { - logger.Log.Infof("Skipping malformed RPM line while parsing installed RPMs for COSI: %q", line) + logger.Log.Warnf("Skipping malformed RPM line while parsing installed RPMs for COSI: %q", line) continue } pkg := OsPackage{ diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go index 95c307caee..75a5ba3d5c 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosimetadata.go @@ -6,7 +6,7 @@ type MetadataJson struct { Images []FileSystem `json:"images"` OsRelease string `json:"osRelease"` Id string `json:"id"` - OsPackages []OsPackage `json:"osPackages,omitempty"` + OsPackages []OsPackage `json:"osPackages"` } type FileSystem struct { @@ -33,6 +33,6 @@ type ImageFile struct { type OsPackage struct { Name string `json:"name"` Version string `json:"version"` - Release string `json:"release,omitempty"` - Arch string `json:"arch,omitempty"` + Release string `json:"release"` + Arch string `json:"arch"` } From cbd74dd3580632e3bbfbbd10d3d1d9aaa4cdfb1f Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Fri, 6 Jun 2025 22:29:01 +0000 Subject: [PATCH 5/6] address comments --- docs/imagecustomizer/api/cosi.md | 30 ++++------ .../pkg/imagecustomizerlib/cosicommon.go | 60 +++++++------------ .../customizepartitionsfilecopy.go | 2 +- .../pkg/imagecustomizerlib/imagecustomizer.go | 20 ++++--- .../pkg/imagecustomizerlib/imageutils.go | 12 +++- .../imagecustomizerlib/liveosisobuilder.go | 2 +- 6 files changed, 54 insertions(+), 72 deletions(-) diff --git a/docs/imagecustomizer/api/cosi.md b/docs/imagecustomizer/api/cosi.md index f4fc32e3f1..985c5a876c 100644 --- a/docs/imagecustomizer/api/cosi.md +++ b/docs/imagecustomizer/api/cosi.md @@ -22,11 +22,7 @@ The tarball MUST contain the following files: - `metadata.json`: A JSON file that contains the metadata of the COSI file. - Filesystem image files in the folder `images/`: The actual filesystem images - that Trident will use to install the OS. - -To allow for future extensions, the tarball MAY contain other files, but Trident -MUST ignore them. The tarball SHOULD NOT contain any extra files that will not -be used by Trident. + that will be used to install the OS. ### Layout @@ -34,22 +30,19 @@ The tarball MUST NOT have a common root directory. The metadata file MUST be at the root of the tarball. If it were extracted with a standard `tar` invocation, the metadata file would be placed in the current directory. -The metadata file SHOULD, be placed at the beginning of the tarball to allow for +The metadata file SHOULD be placed at the beginning of the tarball to allow for quick access to the metadata without having to traverse the entire tarball. ### Partition Image Files -The partition image files are the actual images that Trident will use to install +The partition image files are the actual images that reader will use to install the OS. These MUST be raw partition images. -The image files SHOULD be compressed. They SHOULD use ZSTD compression. Trident -only supports ZSTD-compressed images at the time of writing (2024-09-25), but -that could change in the future. Not using ZSTD-compressed images will result in -Trident failing to install the OS. +The partition image files MUST be raw partition images that are compressed using ZSTD +compression. -They MUST be located in a directory called `images/` inside the tarball. They -MAY be placed in subdirectories of `images/` to organize them. Trident MUST be -able to handle images in subdirectories. +All partition image files MUST be in the `images` directory or one of its +subdirectories. ### Metadata JSON File @@ -90,7 +83,7 @@ from in a virtual disk. _Notes:_ -- **[1]** It MUST use the name recognized by the kernel. For example, `ext4` for +- **[1]** It MUST use the name recognized by the Linux kernel. For example, `ext4` for ext4 filesystems, `vfat` for FAT32 filesystems, etc. - **[2]** It MUST be unique across all filesystems in the COSI tarball. Additionally, volumes in an A/B volume pair MUST have unique filesystem UUIDs. @@ -265,10 +258,9 @@ objects. Each object represents a package installed in the OS. **Why an uncompressed tarball?** -- The images SHOULD be compressed, and other than that the file should be pretty - light-weight. Compressing the entire tarball does not yield a significant size - reduction, if at all. This also allows us to read the metadata without having - to extract the entire tarball. +- This allows the metadata file to be easily read without needing to decompress and + extract the entire tarball. Also, compressing the tarball doesn't provide any + meaningful size reductions since the partition images are all compressed individually. **Why not ZIP?** diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index e32eb260a6..2f0e26b998 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -49,20 +49,8 @@ func convertToCosi(ic *ImageCustomizerParameters) error { defer os.Remove(path.Join(outputDir, partition.PartitionFilename)) } - buildDir := filepath.Join(outputDir, "build") - existingImageConnection, _, _, err := connectToExistingImage(ic.rawImageFile, buildDir, "imageroot", true) - if err != nil { - return err - } - defer existingImageConnection.Close() - - osPackages, err := getAllPackagesFromChroot(existingImageConnection) - if err != nil { - return err - } - err = buildCosiFile(outputDir, ic.outputImageFile, partitionMetadataOutput, ic.verityMetadata, - ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease, osPackages) + ic.partUuidToFstabEntry, ic.imageUuidStr, ic.osRelease, ic.osPackages) if err != nil { return fmt.Errorf("failed to build COSI file:\n%w", err) } @@ -74,11 +62,6 @@ func convertToCosi(ic *ImageCustomizerParameters) error { return err } - err = existingImageConnection.CleanClose() - if err != nil { - return err - } - return nil } @@ -325,37 +308,36 @@ func getArchitectureForCosi() string { } func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, error) { - var packages []OsPackage - + var out string err := imageConnection.Chroot().UnsafeRun(func() error { - out, _, err := shell.Execute( + var err error + out, _, err = shell.Execute( "rpm", "-qa", "--queryformat", "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n", ) if err != nil { return fmt.Errorf("failed to list installed RPMs:\n%w", err) } - - lines := strings.Split(strings.TrimSpace(out), "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) != 4 { - logger.Log.Warnf("Skipping malformed RPM line while parsing installed RPMs for COSI: %q", line) - continue - } - pkg := OsPackage{ - Name: parts[0], - Version: parts[1], - Release: parts[2], - Arch: parts[3], - } - packages = append(packages, pkg) - } return nil }) - if err != nil { - return nil, fmt.Errorf("failed to get packages from chroot:\n%w", err) + return nil, fmt.Errorf("failed to get RPM output from chroot:\n%w", err) + } + + lines := strings.Split(strings.TrimSpace(out), "\n") + var packages []OsPackage + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 4 { + return nil, fmt.Errorf("malformed RPM line encountered while parsing installed RPMs for COSI: %q", line) + } + packages = append(packages, OsPackage{ + Name: parts[0], + Version: parts[1], + Release: parts[2], + Arch: parts[3], + }) } return packages, nil + } diff --git a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go index 11c4c1885c..696d3294d9 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go +++ b/toolkit/tools/pkg/imagecustomizerlib/customizepartitionsfilecopy.go @@ -16,7 +16,7 @@ import ( func customizePartitionsUsingFileCopy(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, buildImageFile string, newBuildImageFile string, ) (map[string]string, error) { - existingImageConnection, _, _, err := connectToExistingImage(buildImageFile, buildDir, "imageroot", false) + existingImageConnection, _, _, _, err := connectToExistingImage(buildImageFile, buildDir, "imageroot", false) if err != nil { return nil, err } diff --git a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go index 4c91923db7..ef9292f708 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imagecustomizer.go @@ -84,6 +84,7 @@ type ImageCustomizerParameters struct { partUuidToFstabEntry map[string]diskutils.FstabEntry osRelease string + osPackages []OsPackage } type verityDeviceMetadata struct { @@ -457,7 +458,7 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { } // Customize the raw image file. - partUuidToFstabEntry, baseImageVerityMetadata, osRelease, err := customizeImageHelper(ic.buildDirAbs, ic.configPath, + partUuidToFstabEntry, baseImageVerityMetadata, osRelease, osPackages, err := customizeImageHelper(ic.buildDirAbs, ic.configPath, ic.config, ic.rawImageFile, ic.rpmsSources, ic.useBaseImageRpmRepos, partitionsCustomized, ic.imageUuidStr, ic.packageSnapshotTime) if err != nil { return err @@ -475,6 +476,7 @@ func customizeOSContents(ic *ImageCustomizerParameters) error { ic.partUuidToFstabEntry = partUuidToFstabEntry ic.baseImageVerityMetadata = baseImageVerityMetadata ic.osRelease = osRelease + ic.osPackages = osPackages // For COSI, always shrink the filesystems. shrinkPartitions := ic.outputImageFormat == imagecustomizerapi.ImageFormatTypeCosi @@ -829,20 +831,20 @@ func validateOutput(baseConfigPath string, output imagecustomizerapi.Output, out func customizeImageHelper(buildDir string, baseConfigPath string, config *imagecustomizerapi.Config, rawImageFile string, rpmsSources []string, useBaseImageRpmRepos bool, partitionsCustomized bool, imageUuidStr string, packageSnapshotTime string, -) (map[string]diskutils.FstabEntry, []verityDeviceMetadata, string, error) { +) (map[string]diskutils.FstabEntry, []verityDeviceMetadata, string, []OsPackage, error) { logger.Log.Debugf("Customizing OS") - imageConnection, partUuidToFstabEntry, baseImageVerityMetadata, err := connectToExistingImage(rawImageFile, + imageConnection, partUuidToFstabEntry, baseImageVerityMetadata, osPackages, err := connectToExistingImage(rawImageFile, buildDir, "imageroot", true) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } defer imageConnection.Close() // Extract OS release info from rootfs for COSI osRelease, err := extractOSRelease(imageConnection) if err != nil { - return nil, nil, "", fmt.Errorf("failed to extract OS release from rootfs partition:\n%w", err) + return nil, nil, "", nil, fmt.Errorf("failed to extract OS release from rootfs partition:\n%w", err) } imageConnection.Chroot().UnsafeRun(func() error { @@ -854,7 +856,7 @@ func customizeImageHelper(buildDir string, baseConfigPath string, config *imagec err = validateVerityMountPaths(imageConnection, config, partUuidToFstabEntry) if err != nil { - return nil, nil, "", fmt.Errorf("verity validation failed:\n%w", err) + return nil, nil, "", nil, fmt.Errorf("verity validation failed:\n%w", err) } // Do the actual customizations. @@ -866,15 +868,15 @@ func customizeImageHelper(buildDir string, baseConfigPath string, config *imagec warnOnLowFreeSpace(buildDir, imageConnection) if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } err = imageConnection.CleanClose() if err != nil { - return nil, nil, "", err + return nil, nil, "", nil, err } - return partUuidToFstabEntry, baseImageVerityMetadata, osRelease, nil + return partUuidToFstabEntry, baseImageVerityMetadata, osRelease, osPackages, nil } func shrinkFilesystemsHelper(buildImageFile string) error { diff --git a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go index 01cfe483be..832b4f2c56 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/imageutils.go +++ b/toolkit/tools/pkg/imagecustomizerlib/imageutils.go @@ -21,16 +21,22 @@ import ( type installOSFunc func(imageChroot *safechroot.Chroot) error func connectToExistingImage(imageFilePath string, buildDir string, chrootDirName string, includeDefaultMounts bool, -) (*ImageConnection, map[string]diskutils.FstabEntry, []verityDeviceMetadata, error) { +) (*ImageConnection, map[string]diskutils.FstabEntry, []verityDeviceMetadata, []OsPackage, error) { imageConnection := NewImageConnection() partUuidToMountPath, verityMetadata, err := connectToExistingImageHelper(imageConnection, imageFilePath, buildDir, chrootDirName, includeDefaultMounts) if err != nil { imageConnection.Close() - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return imageConnection, partUuidToMountPath, verityMetadata, nil + + packages, err := getAllPackagesFromChroot(imageConnection) + if err != nil { + return nil, nil, nil, nil, err + } + + return imageConnection, partUuidToMountPath, verityMetadata, packages, nil } func connectToExistingImageHelper(imageConnection *ImageConnection, imageFilePath string, diff --git a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go index 9795f429f7..89bcb4ffdf 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go +++ b/toolkit/tools/pkg/imagecustomizerlib/liveosisobuilder.go @@ -65,7 +65,7 @@ func createLiveOSIsoImage(buildDir, baseConfigPath string, inputArtifactsStore * }() logger.Log.Debugf("Connecting to raw image (%s)", rawImageFile) - rawImageConnection, _, _, err := connectToExistingImage(rawImageFile, isoBuildDir, "readonly-rootfs-mount", false /*includeDefaultMounts*/) + rawImageConnection, _, _, _, err := connectToExistingImage(rawImageFile, isoBuildDir, "readonly-rootfs-mount", false /*includeDefaultMounts*/) if err != nil { return err } From d9f0bc9c9fabca524b15e06c5b20fb5b5c9a25e3 Mon Sep 17 00:00:00 2001 From: Elaine Zhao Date: Mon, 9 Jun 2025 17:36:00 +0000 Subject: [PATCH 6/6] address comment --- toolkit/tools/pkg/imagecustomizerlib/cosicommon.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go index 2f0e26b998..fd512d1dcd 100644 --- a/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go +++ b/toolkit/tools/pkg/imagecustomizerlib/cosicommon.go @@ -314,10 +314,7 @@ func getAllPackagesFromChroot(imageConnection *ImageConnection) ([]OsPackage, er out, _, err = shell.Execute( "rpm", "-qa", "--queryformat", "%{NAME} %{VERSION} %{RELEASE} %{ARCH}\n", ) - if err != nil { - return fmt.Errorf("failed to list installed RPMs:\n%w", err) - } - return nil + return err }) if err != nil { return nil, fmt.Errorf("failed to get RPM output from chroot:\n%w", err)