diff --git a/HACKING.md b/HACKING.md index f441406fb4..3ada33c2bc 100644 --- a/HACKING.md +++ b/HACKING.md @@ -82,10 +82,88 @@ cache and if these changes are made as root, it can cause issues when running other go commands in the future as a regular user. Instead, it is recommended to first build the binary and then run it as root: ``` -go build -o build ./cmd/build -sudo ./build ... +go build -o bin/build ./cmd/build +sudo ./bin/build ... ``` +#### Booting images + +You can boot an image in its target environment by using the appropriate +command from `cmd/`. _Currently, only AWS is supported._ + +For example, to boot an AMI or EC2 image, you can use the `./cmd/boot-aws` +command with the `setup` subcommand: +```bash +go run ./cmd/boot-aws setup \ + --access-key-id "${AWS_ACCESS_KEY_ID}" \ + --secret-access-key "${AWS_SECRET_ACCESS_KEY}" \ + --region "${AWS_REGION}" \ + --bucket "${AWS_BUCKET}" \ + --ami-name "${IMAGE_NAME}" \ + --s3-key "${IMAGE_KEY}" \ + --username "${USERNAME}" \ + --arch "${IMAGE_ARCHITECTURE}" \ + --ssh-pubkey "${PATH_TO_SSH_PUBLIC_KEY}" \ + --ssh-privkey "${PATH_TO_SSH_PRIVATE_KEY}" \ + --resourcefile ./aws-test-resources.json \ + ${PATH_TO_IMAGE_FILE} +``` +where: +- `${AWS_ACCESS_KEY_ID}` and `${AWS_SECRET_ACCESS_KEY}` are the AWS credentials, +- `${AWS_REGION}` is the AWS region to use, +- `${AWS_BUCKET}` is an S3 bucket (that must already exist), +- `${IMAGE_NAME}` is the name to use for registering the AMI, +- `${IMAGE_KEY}` is the key (filename) to use for the file in S3, +- `${USERNAME}` is the username to set up on the instance, +- `${IMAGE_ARCHITECTURE}` is the hardware architecture of the image being + uploaded and booted, +- `${PATH_TO_SSH_PUBLIC_KEY}` and `${PATH_TO_SSH_PRIVATE_KEY}` point to an + public/private SSH key pair. + +This command will upload the image to S3, register the image as an AMI, create +a security group configured to allow SSH access, and launch an instance from +the AMI. It will then wait until the instance is ready and print its public IP +address. It will also use the public ssh key and provided username to configure +cloud-init to create a user and set the ssh key on first boot. + +The IDs of all created resources are stored in the file specified by the +`--resourcefile` flag. This can be used to tear down all the resources created +by the `setup` subcommand: +```bash +go run ./cmd/boot-aws teardown \ + --access-key-id "${AWS_ACCESS_KEY_ID}" \ + --secret-access-key "${AWS_SECRET_ACCESS_KEY}" \ + --region "${AWS_REGION}" \ + --bucket "${AWS_BUCKET}" \ + --name "${IMAGE_NAME}" \ + --key "${IMAGE_KEY}" \ + --username "${USERNAME}" \ + --arch "${IMAGE_ARCHITECTURE}" \ + --ssh-pubkey "${PATH_TO_SSH_PUBLIC_KEY}" \ + --ssh-privkey "${PATH_TO_SSH_PRIVATE_KEY}" \ + --resourcefile ./aws-test-resources.json +``` + +Alternatively, a setup-test-teardown procedure can be run in a single command using the `run` subcommand: +```bash +go run ./cmd/boot-aws run \ + --access-key-id "${AWS_ACCESS_KEY_ID}" \ + --secret-access-key "${AWS_SECRET_ACCESS_KEY}" \ + --region "${AWS_REGION}" \ + --bucket "${AWS_BUCKET}" \ + --ami-name "${IMAGE_NAME}" \ + --s3-key "${IMAGE_KEY}" \ + --username "${USERNAME}" \ + --arch "${IMAGE_ARCHITECTURE}" \ + --ssh-pubkey "${PATH_TO_SSH_PUBLIC_KEY}" \ + --ssh-privkey "${PATH_TO_SSH_PRIVATE_KEY}" \ + ${PATH_TO_IMAGE_FILE} ${PATH_TO_SCRIPT} +``` + +This will perform the same steps as the `setup` subcommand, then upload the +script specified by `${PATH_TO_SCRIPT}` to the instance, run it, and then +perform the same actions as the `teardown` subcommand. + #### Listing available image type configurations The `cmd/list-images` utility simply lists all available combinations of diff --git a/cmd/boot-aws/main.go b/cmd/boot-aws/main.go index d127521f79..40c180efd6 100644 --- a/cmd/boot-aws/main.go +++ b/cmd/boot-aws/main.go @@ -107,7 +107,7 @@ func scpFile(ip, user, key, hostsfile, source, dest string) error { func keyscan(ip, filepath string) error { var keys []byte - maxTries := 10 + maxTries := 30 // wait for at least 5 mins var keyscanErr error for try := 0; try < maxTries; try++ { keys, _, keyscanErr = run("ssh-keyscan", ip) @@ -159,12 +159,12 @@ func doSetup(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resour if err != nil { return err } - sshKey, err := flags.GetString("ssh-key") + sshPubKey, err := flags.GetString("ssh-pubkey") if err != nil { return err } - userData, err := createUserData(username, sshKey) + userData, err := createUserData(username, sshPubKey) if err != nil { return fmt.Errorf("createUserData(): %s", err.Error()) } @@ -173,7 +173,7 @@ func doSetup(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resour if err != nil { return err } - keyName, err := flags.GetString("key") + keyName, err := flags.GetString("s3-key") if err != nil { return err } @@ -185,13 +185,6 @@ func doSetup(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resour fmt.Printf("file uploaded to %s\n", aws.StringValue(&uploadOutput.Location)) - var share []string - if shareWith, err := flags.GetString("account-id"); shareWith != "" { - share = append(share, shareWith) - } else if err != nil { - return err - } - var bootModePtr *string if bootMode, err := flags.GetString("boot-mode"); bootMode != "" { bootModePtr = &bootMode @@ -199,7 +192,7 @@ func doSetup(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resour return err } - imageName, err := flags.GetString("name") + imageName, err := flags.GetString("ami-name") if err != nil { return err } @@ -209,7 +202,7 @@ func doSetup(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resour return err } - ami, snapshot, err := a.Register(imageName, bucketName, keyName, share, arch, bootModePtr) + ami, snapshot, err := a.Register(imageName, bucketName, keyName, nil, arch, bootModePtr) if err != nil { return fmt.Errorf("Register(): %s", err.Error()) } @@ -366,7 +359,7 @@ func teardown(cmd *cobra.Command, args []string) { } func doRunExec(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *resources) error { - sshKey, err := flags.GetString("ssh-key") + privKey, err := flags.GetString("ssh-privkey") if err != nil { return err } @@ -391,11 +384,8 @@ func doRunExec(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *reso return err } - // TODO: remove this assumption and add a private key flag - key := strings.TrimSuffix(sshKey, ".pub") - // ssh into the remote machine and exit immediately to check connection - if err := sshRun(ip, username, key, hostsfile, "exit"); err != nil { + if err := sshRun(ip, username, privKey, hostsfile, "exit"); err != nil { return err } @@ -403,12 +393,12 @@ func doRunExec(a *awscloud.AWS, filename string, flags *pflag.FlagSet, res *reso destination := filepath.Base(filename) // copy the executable - if err := scpFile(ip, username, key, hostsfile, filename, destination); err != nil { + if err := scpFile(ip, username, privKey, hostsfile, filename, destination); err != nil { return err } // run the executable - return sshRun(ip, username, key, hostsfile, fmt.Sprintf("./%s", destination)) + return sshRun(ip, username, privKey, hostsfile, fmt.Sprintf("./%s", destination)) } func runExec(cmd *cobra.Command, args []string) { @@ -454,13 +444,13 @@ func setupCLI() *cobra.Command { rootFlags.String("session-token", "", "session token") rootFlags.String("region", "", "target region") rootFlags.String("bucket", "", "target S3 bucket name") - rootFlags.String("key", "", "target S3 key name") - rootFlags.String("name", "", "AMI name") - rootFlags.String("account-id", "", "account id to share image with") + rootFlags.String("s3-key", "", "target S3 key name") + rootFlags.String("ami-name", "", "AMI name") rootFlags.String("arch", "", "arch (x86_64 or aarch64)") rootFlags.String("boot-mode", "", "boot mode (legacy-bios, uefi, uefi-preferred)") rootFlags.String("username", "", "name of the user to create on the system") - rootFlags.String("ssh-key", "", "path to user's public ssh key") + rootFlags.String("ssh-pubkey", "", "path to user's public ssh key") + rootFlags.String("ssh-privkey", "", "path to user's private ssh key") exitCheck(rootCmd.MarkPersistentFlagRequired("access-key-id")) exitCheck(rootCmd.MarkPersistentFlagRequired("secret-access-key")) @@ -468,19 +458,20 @@ func setupCLI() *cobra.Command { exitCheck(rootCmd.MarkPersistentFlagRequired("bucket")) // TODO: make it optional and use UUID if not specified - exitCheck(rootCmd.MarkPersistentFlagRequired("key")) + exitCheck(rootCmd.MarkPersistentFlagRequired("s3-key")) // TODO: make it optional and use UUID if not specified - exitCheck(rootCmd.MarkPersistentFlagRequired("name")) + exitCheck(rootCmd.MarkPersistentFlagRequired("ami-name")) exitCheck(rootCmd.MarkPersistentFlagRequired("arch")) // TODO: make it optional and use a default exitCheck(rootCmd.MarkPersistentFlagRequired("username")) - // TODO: make ssh key optional for 'run' and if not specified generate a - // temporary key pair - exitCheck(rootCmd.MarkPersistentFlagRequired("ssh-key")) + // TODO: make ssh key pair optional for 'run' and if not specified generate + // a temporary key pair + exitCheck(rootCmd.MarkPersistentFlagRequired("ssh-privkey")) + exitCheck(rootCmd.MarkPersistentFlagRequired("ssh-pubkey")) setupCmd := &cobra.Command{ Use: "setup [--resourcefile ] ", diff --git a/pkg/distro/rhel8/edge.go b/pkg/distro/rhel8/edge.go index f19ffd2fce..bbf6fa7d13 100644 --- a/pkg/distro/rhel8/edge.go +++ b/pkg/distro/rhel8/edge.go @@ -65,12 +65,13 @@ func edgeRawImgType() imageType { filename: "image.raw.xz", compression: "xz", mimeType: "application/xz", + image: edgeRawImage, packageSets: nil, defaultSize: 10 * common.GibiByte, rpmOstree: true, bootable: true, bootISO: false, - image: edgeRawImage, + kernelOptions: "modprobe.blacklist=vc4", buildPipelines: []string{"build"}, payloadPipelines: []string{"ostree-deployment", "image", "xz"}, exports: []string{"xz"}, diff --git a/pkg/distro/rhel9/ami.go b/pkg/distro/rhel9/ami.go index d2ec6809cb..d863bcf1cb 100644 --- a/pkg/distro/rhel9/ami.go +++ b/pkg/distro/rhel9/ami.go @@ -8,6 +8,7 @@ import ( "github.com/osbuild/images/pkg/subscription" ) +// TODO: move these to the EC2 environment const amiKernelOptions = "console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295" var ( diff --git a/pkg/distro/rhel9/edge.go b/pkg/distro/rhel9/edge.go index c619510037..e53066a400 100644 --- a/pkg/distro/rhel9/edge.go +++ b/pkg/distro/rhel9/edge.go @@ -69,6 +69,7 @@ var ( defaultImageConfig: &distro.ImageConfig{ Locale: common.ToPtr("en_US.UTF-8"), }, + kernelOptions: "modprobe.blacklist=vc4", defaultSize: 10 * common.GibiByte, rpmOstree: true, bootable: true, @@ -145,6 +146,7 @@ var ( defaultImageConfig: &distro.ImageConfig{ Locale: common.ToPtr("en_US.UTF-8"), }, + kernelOptions: amiKernelOptions + " modprobe.blacklist=vc4", defaultSize: 10 * common.GibiByte, rpmOstree: true, bootable: true, @@ -165,6 +167,7 @@ var ( defaultImageConfig: &distro.ImageConfig{ Locale: common.ToPtr("en_US.UTF-8"), }, + kernelOptions: "modprobe.blacklist=vc4", defaultSize: 10 * common.GibiByte, rpmOstree: true, bootable: true, diff --git a/pkg/distro/rhel9/images.go b/pkg/distro/rhel9/images.go index 7e7c4a5249..c978ae022e 100644 --- a/pkg/distro/rhel9/images.go +++ b/pkg/distro/rhel9/images.go @@ -405,7 +405,12 @@ func edgeRawImage(workload workload.Workload, img.Users = users.UsersFromBP(customizations.GetUsers()) img.Groups = users.GroupsFromBP(customizations.GetGroups()) - img.KernelOptionsAppend = []string{"modprobe.blacklist=vc4"} + // The kernel options defined on the image type are usually handled in + // osCustomiztions() but ostree images don't use OSCustomizations, so we + // handle them here separately. + if t.kernelOptions != "" { + img.KernelOptionsAppend = append(img.KernelOptionsAppend, t.kernelOptions) + } img.Keyboard = "us" img.Locale = "C.UTF-8" if !common.VersionLessThan(t.arch.distro.osVersion, "9.2") || !t.arch.distro.isRHEL() { diff --git a/test/README.md b/test/README.md index 07d04278bd..ac92219eac 100644 --- a/test/README.md +++ b/test/README.md @@ -4,7 +4,8 @@ - `./cmd/build` takes a config file as argument to build an image. For example: ``` -sudo go run ./cmd/build -output ./buildtest -rpmmd /tmp/rpmmd -distro fedora-38 -image qcow2 -config test/configs/embed-containers.json +go build -o bin/build ./cmd/build +sudo ./bin/build -output ./buildtest -rpmmd /tmp/rpmmd -distro fedora-38 -image qcow2 -config test/configs/embed-containers.json ``` will build a Fedora 38 qcow2 image using the configuration specified in the file `embed-containers.json` @@ -29,6 +30,7 @@ configure-generators | | (Dynamic: For each modified image type and config) | |-- Build --- | + | (Dynamic: For each distro/arch) |-- generate-ostree-build-configs-- | | (Dynamic: For each modified image type and config) @@ -54,16 +56,18 @@ The config generator: - Downloads the test build cache. - Filters out any manifest with an ID that exists in the build cache. - It also filters out any manifest that depends on an ostree commit because these can't be built without an ostree repository to pull from. -- For each remaining manifest, creates a build job which runs the `./test/scripts/build-image.sh` script for a given distro, image type, and config file. +- For each remaining manifest, creates a job which builds, boots (if applicable), and uploads the results to the build cache for a given distro, image type, and config file. + - `./test/scripts/build-image` builds the image using osbuild. + - `./test/scripts/boot-image` boots the image in the appropriate cloud or virtual environment (if supported). + - `./test/scripts/upload-results` uploads the results (manifest, image file, and build info) to the CI S3 bucket, so that rebuilds of the same manifest ID can be skipped. - For ostree container image types (`iot-container` and `edge-container`), it also adds a call to the `./tools/ci/push-container.sh` script to push the container to the GitLab registry. The name and tag for each container is `:` (see [Definitions](#definitions) below). - - If no builds are needed, it generates a `NullConfig`, which is a simple shell runner that exits successfully. This is required because the child pipeline config cannot be empty. - +- If no builds are needed, it generates a `NullConfig`, which is a simple shell runner that exits successfully. This is required because the child pipeline config cannot be empty. #### 2. Dynamic build job Each build job runs in parallel. For each image that is successfully built, a file is added to the test build cache under the following path: ``` -//.json +///info.json ``` Each file in the cache stores information relevant to the build, @@ -76,7 +80,8 @@ in the form "config": "", "manifest-checksum": "", "obuild-version": "", - "commit": "" + "commit": "", + "pr": "" } ``` @@ -91,7 +96,8 @@ for example: "config": "all-customizations", "manifest-checksum": "8c0ce3987d78fe6f3307494cd57ceed861de61c3b04786d6a7f570faacbdb5df", "obuild-version": "osbuild 89", - "commit": "52ecfdf1eb345e09c6a6edf4a8d3dd5c8079c51c" + "commit": "52ecfdf1eb345e09c6a6edf4a8d3dd5c8079c51c", + "pr": 42 } ``` @@ -111,8 +117,11 @@ The config generator: - Note that this manifest generation step uses the `-skip-noconfig` flag, which means that any image type not defined in the map is skipped. - Downloads the test build cache. - Filters out any manifest with an ID that exists in the build cache. -- For each remaining manifest, creates a build job which runs the ostree container that was used to generate the manifest and runs `./test/scripts/build-image.sh` script for a given distro, image type, and config file. - - If no builds are needed, it generates a `NullConfig`, which is a simple shell runner that exits successfully. This is required because the child pipeline config cannot be empty. +- For each remaining manifest, creates a job which builds, boots (if applicable), and uploads the results to the build cache for a given distro, image type, and config file. + - `./test/scripts/build-image` builds the image using osbuild. + - `./test/scripts/boot-image` boots the image in the appropriate cloud or virtual environment (if supported). + - `./test/scripts/upload-results` uploads the results (manifest, image file, and build info) to the CI S3 bucket, so that rebuilds of the same manifest ID can be skipped. +- If no builds are needed, it generates a `NullConfig`, which is a simple shell runner that exits successfully. This is required because the child pipeline config cannot be empty. #### 4. Dynamic ostree build job diff --git a/test/config-map.json b/test/config-map.json index ef5495ea96..d21820b58e 100644 --- a/test/config-map.json +++ b/test/config-map.json @@ -1,23 +1,54 @@ { "./configs/all-customizations.json": { - "image-types": [ - "qcow2" - ], "distros": [ "rhel-9*", "rhel-8*", "centos*", "fedora*" + ], + "image-types": [ + "qcow2" + ] + }, + "./configs/all-with-oscap.json": { + "distros": [ + "rhel-91", + "rhel-92", + "rhel-87", + "rhel-88", + "rhel-89", + "centos*", + "fedora*" + ], + "image-types": [ + "qcow2" + ] + }, + "./configs/disable-lm_sensors.json": { + "distros": [ + "rhel-84" + ], + "image-types": [ + "ec2-sap" + ] + }, + "./configs/edge-ostree-pull-device.json": { + "image-types": [ + "edge-simplified-installer" ] }, "./configs/edge-ostree-pull-empty.json": { "image-types": [ - "edge-ami", "edge-installer", "edge-raw-image", "edge-vsphere" ] }, + "./configs/edge-ostree-pull-user.json": { + "image-types": [ + "edge-ami" + ] + }, "./configs/embed-containers-2.json": { "image-types": [ "edge-container" @@ -39,7 +70,6 @@ "container", "ec2", "ec2-ha", - "ec2-sap", "edge-container", "gce", "gce-rhui", @@ -62,27 +92,42 @@ "iot-container" ] }, + "./configs/iot-ostree-pull-device.json": { + "image-types": [ + "iot-simplified-installer" + ] + }, "./configs/iot-ostree-pull-empty.json": { "image-types": [ - "iot-ami", "iot-installer", "iot-qcow2-image", "iot-raw-image" ] }, + "./configs/iot-ostree-pull-user.json": { + "image-types": [ + "iot-ami" + ] + }, "./configs/kernel-debug.json": { "image-types": [ "iot-commit" ] }, - "./configs/edge-ostree-pull-device.json": { + "./configs/minimal-pkgs.json": { + "distros": [ + "fedora*" + ], "image-types": [ - "edge-simplified-installer" + "qcow2" ] }, - "./configs/iot-ostree-pull-device.json": { + "./configs/minimal.json": { + "distros": [ + "fedora*" + ], "image-types": [ - "iot-simplified-installer" + "qcow2" ] }, "./configs/ostree.json": { @@ -92,34 +137,31 @@ "iot-container" ] }, - "./configs/all-with-oscap.json": { - "image-types": [ - "qcow2" - ], + "./configs/ec2-sap-empty.json": { "distros": [ - "rhel-91", - "rhel-92", + "centos-8", + "centos-9", + "fedora-37", + "fedora-38", + "fedora-39", + "fedora-40", + "rhel-7", + "rhel-8", + "rhel-810", + "rhel-85", + "rhel-86", "rhel-87", "rhel-88", "rhel-89", - "centos*", - "fedora*" - ] - }, - "./configs/minimal.json": { - "image-types": [ - "qcow2" + "rhel-9", + "rhel-90", + "rhel-91", + "rhel-92", + "rhel-93", + "rhel-94" ], - "distros": [ - "fedora*" - ] - }, - "./configs/minimal-pkgs.json": { "image-types": [ - "qcow2" - ], - "distros": [ - "fedora*" + "ec2-sap" ] } } diff --git a/test/configs/disable-lm_sensors.json b/test/configs/disable-lm_sensors.json new file mode 100644 index 0000000000..611899b7d9 --- /dev/null +++ b/test/configs/disable-lm_sensors.json @@ -0,0 +1,12 @@ +{ + "name": "disable-lm_sensors", + "blueprint": { + "customizations": { + "services": { + "disabled": [ + "lm_sensors.service" + ] + } + } + } +} diff --git a/test/configs/ec2-sap-empty.json b/test/configs/ec2-sap-empty.json new file mode 100644 index 0000000000..28a2bf0375 --- /dev/null +++ b/test/configs/ec2-sap-empty.json @@ -0,0 +1,4 @@ +{ + "name": "ec2-sap-empty", + "blueprint": {} +} diff --git a/test/configs/edge-ostree-pull-user.json b/test/configs/edge-ostree-pull-user.json new file mode 100644 index 0000000000..fc0dec5292 --- /dev/null +++ b/test/configs/edge-ostree-pull-user.json @@ -0,0 +1,23 @@ +{ + "name": "edge-ostree-pull-user", + "blueprint": { + "customizations": { + "user": [ + { + "groups": [ + "wheel" + ], + "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDyeUyVWvuSSOdrikiDWgNoCz0oYP4Ir68jJecn+XYc github.com/osbuild/images", + "name": "osbuild" + } + ] + } + }, + "depends": { + "config": "empty.json", + "image-type": "edge-container" + }, + "ostree": { + "url": "http://example.com/repo" + } +} diff --git a/test/configs/iot-ostree-pull-user.json b/test/configs/iot-ostree-pull-user.json new file mode 100644 index 0000000000..6d2272c10e --- /dev/null +++ b/test/configs/iot-ostree-pull-user.json @@ -0,0 +1,23 @@ +{ + "name": "iot-ostree-pull-user", + "blueprint": { + "customizations": { + "user": [ + { + "groups": [ + "wheel" + ], + "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDyeUyVWvuSSOdrikiDWgNoCz0oYP4Ir68jJecn+XYc github.com/osbuild/images", + "name": "osbuild" + } + ] + } + }, + "depends": { + "config": "empty.json", + "image-type": "iot-container" + }, + "ostree": { + "url": "http://example.com/repo" + } +} diff --git a/test/configs/user-customizations.json b/test/configs/user-customizations.json index ee192a9b37..03f1340e53 100644 --- a/test/configs/user-customizations.json +++ b/test/configs/user-customizations.json @@ -4,8 +4,8 @@ "customizations": { "user": [ { - "name": "redhat", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost" + "name": "osbuild", + "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDyeUyVWvuSSOdrikiDWgNoCz0oYP4Ir68jJecn+XYc github.com/osbuild/images" } ] } diff --git a/test/scripts/base-host-check.sh b/test/scripts/base-host-check.sh index 0e71011474..c8f6b7d6e1 100755 --- a/test/scripts/base-host-check.sh +++ b/test/scripts/base-host-check.sh @@ -10,7 +10,7 @@ running_wait() { # reported, instead the command will block until a later state (such as # running or degraded) is reached. while true; do - state=$(sudo systemctl is-system-running) + state=$(systemctl is-system-running) echo "${state}" # keep iterating on initializing and starting diff --git a/test/scripts/boot-image b/test/scripts/boot-image index 8a261ee164..8483e539ae 100755 --- a/test/scripts/boot-image +++ b/test/scripts/boot-image @@ -8,6 +8,8 @@ import sys import uuid from tempfile import TemporaryDirectory +import imgtestlib as testlib + def runcmd(cmd, stdin=None): job = sp.run(cmd) @@ -28,29 +30,64 @@ def get_aws_config(): @contextlib.contextmanager def create_ssh_key(): with TemporaryDirectory() as tmpdir: - # create an ssh key pair with empty password keypath = os.path.join(tmpdir, "testkey") - cmd = ["ssh-keygen", "-N", "", "-f", keypath] - runcmd(cmd) + if ci_priv_key := os.environ.get("CI_PRIV_SSH_KEY"): + # running in CI: use key from env + with open(keypath, "w") as keyfile: + keyfile.write(ci_priv_key + "\n") + os.chmod(keypath, 0o600) + + # get public key from priv key and write it out + cmd = ["ssh-keygen", "-y", "-f", keypath] + out, _ = testlib.runcmd(cmd) + pubkey = out.decode() + with open(keypath + ".pub", "w") as pubkeyfile: + pubkeyfile.write(pubkey) + else: + # create an ssh key pair with empty password + cmd = ["ssh-keygen", "-N", "", "-f", keypath] + runcmd(cmd) + yield keypath, keypath + ".pub" +@contextlib.contextmanager +def ensure_uncompressed(filepath): + """ + If the file at the given path is compressed, decompress it and return the new file path. + """ + base, ext = os.path.splitext(filepath) + if ext == ".xz": + print(f"Uncompressing {filepath}") + # needs to run as root to set perms and ownership on uncompressed file + runcmd(["sudo", "unxz", "--verbose", "--keep", filepath]) + yield base + # cleanup when done so the uncompressed file doesn't get uploaded to the build cache + os.unlink(base) + + else: + # we only do xz for now so it must be raw: return as is and hope for the best + yield filepath + + def boot_ami(distro, arch, image_type, image_path): aws_config = get_aws_config() - with create_ssh_key() as (privkey, pubkey): - image_name = f"image-boot-test-{distro}-{arch}-{image_type}-" + str(uuid.uuid4()) - cmd = ["go", "run", "./cmd/boot-aws", "run", - "--access-key-id", aws_config["key_id"], - "--secret-access-key", aws_config["secret_key"], - "--region", aws_config["region"], - "--bucket", aws_config["bucket"], - "--arch", arch, - "--name", image_name, - "--key", f"images/boot/{image_name}", - "--username", "osbuild", - "--ssh-key", pubkey, - image_path, "test/scripts/base-host-check.sh"] - runcmd(cmd) + with ensure_uncompressed(image_path) as raw_image_path: + with create_ssh_key() as (privkey, pubkey): + image_name = f"image-boot-test-{distro}-{arch}-{image_type}-" + str(uuid.uuid4()) + cmd = ["go", "run", "./cmd/boot-aws", "run", + "--access-key-id", aws_config["key_id"], + "--secret-access-key", aws_config["secret_key"], + "--region", aws_config["region"], + "--bucket", aws_config["bucket"], + "--arch", arch, + "--ami-name", image_name, + "--s3-key", f"images/boot/{image_name}", + "--username", "osbuild", + "--ssh-privkey", privkey, + "--ssh-pubkey", pubkey, + raw_image_path, "test/scripts/base-host-check.sh"] + runcmd(cmd) def find_image_file(build_path: str) -> str: @@ -94,7 +131,7 @@ def main(): print(f"Testing image at {image_path}") match image_type: - case "ami": + case "ami" | "ec2" | "ec2-ha" | "ec2-sap" | "edge-ami": boot_ami(distro, arch, image_type, image_path) case _: # skip diff --git a/test/scripts/generate-build-config b/test/scripts/generate-build-config index 61466fb34c..d6e81e5812 100755 --- a/test/scripts/generate-build-config +++ b/test/scripts/generate-build-config @@ -17,7 +17,7 @@ build/{distro}/{arch}/{image_type}/{config_name}: - sudo ./schutzbot/setup-osbuild-repo - sudo dnf install -y go gpgme-devel gcc osbuild osbuild-luks2 osbuild-lvm2 osbuild-ostree osbuild-selinux - s3cmd + s3cmd xz - ./test/scripts/build-image "{distro}" "{image_type}" "{config}" - ./test/scripts/boot-image "{distro}" "{arch}" "{image_type}" "{image_path}" - ./test/scripts/upload-results "{distro}" "{image_type}" "{config}" diff --git a/test/scripts/generate-ostree-build-config b/test/scripts/generate-ostree-build-config index 9d092785f4..0cc927eefc 100755 --- a/test/scripts/generate-ostree-build-config +++ b/test/scripts/generate-ostree-build-config @@ -13,9 +13,10 @@ build/{distro}/{arch}/{image_type}/{config_name}: - sudo ./schutzbot/setup-osbuild-repo - sudo dnf install -y go gpgme-devel gcc osbuild osbuild-luks2 osbuild-lvm2 osbuild-ostree osbuild-selinux - s3cmd podman + s3cmd podman xz - {start_container} - ./test/scripts/build-image "{distro}" "{image_type}" "{config}" + - ./test/scripts/boot-image "{distro}" "{arch}" "{image_type}" "{image_path}" - ./test/scripts/upload-results "{distro}" "{image_type}" "{config}" extends: .terraform variables: @@ -282,6 +283,8 @@ def generate_configs(build_requests, pull_configs, container_configs, pipeline_f config_name = config["name"] + build_name = testlib.gen_build_name(distro, arch, image_type, config_name) + image_path = f"./build/{build_name}" # generate script line to pull and start container container = container_configs[config_name] @@ -297,7 +300,8 @@ def generate_configs(build_requests, pull_configs, container_configs, pipeline_f pipeline_file.write(JOB_TEMPLATE.format(distro=distro, arch=arch, image_type=image_type, config_name=config_name, config=build_config_path, start_container=container_cmd, - internal="true" if "rhel" in distro else "false")) + internal="true" if "rhel" in distro else "false", + image_path=image_path)) print("✅ DONE!") diff --git a/test/scripts/imgtestlib.py b/test/scripts/imgtestlib.py index c937d97d09..96af144b7c 100644 --- a/test/scripts/imgtestlib.py +++ b/test/scripts/imgtestlib.py @@ -23,7 +23,11 @@ # image types that can be boot tested CAN_BOOT_TEST = [ - "ami" + "ami", + "ec2", + "ec2-ha", + "ec2-sap", + "edge-ami", ]