Repository: osbuild/osbuild-composer
Pull request #1235 (2021-02-17 19:12:35)

weldr: add url parametr to ostree image options
This introduces an URL parameter to the OSTree image options. This is going to be required for when we introduce the installer image type. For now, we just use it as a convenience to make the parent commit optional in the ostree commit image types.

This will be used by https://github.com/osbuild/cockpit-composer/pull/1232 and https://github.com/weldr/lorax/pull/1115.

Testing is still missing for one of these commits, as well as release notes, so I'm making this a WIP.

cc @jkozol, @bcl, @gicmo and @achilleas-k 

This pull request includes:

- [x] adequate testing for the new functionality or fixed issue
- [x] adequate documentation informing people about the change


teg: OpenStack failed, but that is unrelated to this PR. Everything else passed.
henrywang: Thanks @teg. I run test locally with rhel 8.4 nightly repo, things looks great! BTW, I'll add `rt-kernel` test in another PR.
teg: OpenStack still failing, and one test failing due to seemingly hardware error in PSI:
```
$ dmesg --level ERR
[ 1437.697530] blk_update_request: I/O error, dev vda, sector 47447184 op 0x0:(READ) flags 0x80700 phys_seg 5 prio class 0
[ 1437.725188] blk_update_request: I/O error, dev vda, sector 47447424 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
[ 1437.742487] blk_update_request: I/O error, dev vda, sector 47447424 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
```

Patch:
diff --git a/.gitignore b/.gitignore
index 534086142..3efe28a99 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
 __pycache__
 
 .vscode
+.DS_Store
 
+/containers/config/
 /bin
 /rpmbuild
diff --git a/cmd/osbuild-worker/jobimpl-osbuild-koji.go b/cmd/osbuild-worker/jobimpl-osbuild-koji.go
index f7d2d26fb..545546bfa 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild-koji.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild-koji.go
@@ -18,6 +18,7 @@ import (
 
 type OSBuildKojiJobImpl struct {
 	Store       string
+	Output      string
 	KojiServers map[string]koji.GSSAPICredentials
 }
 
@@ -54,7 +55,7 @@ func (impl *OSBuildKojiJobImpl) kojiUpload(file *os.File, server, directory, fil
 }
 
 func (impl *OSBuildKojiJobImpl) Run(job worker.Job) error {
-	outputDirectory, err := ioutil.TempDir("/var/tmp", "osbuild-worker-*")
+	outputDirectory, err := ioutil.TempDir(impl.Output, job.Id().String()+"-*")
 	if err != nil {
 		return fmt.Errorf("error creating temporary output directory: %v", err)
 	}
diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go
index 512e559df..c6f0e1e85 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild.go
@@ -25,6 +25,7 @@ import (
 
 type OSBuildJobImpl struct {
 	Store       string
+	Output      string
 	KojiServers map[string]koji.GSSAPICredentials
 }
 
@@ -62,7 +63,7 @@ func osbuildStagesToRPMs(stages []osbuild.StageResult) []koji.RPM {
 }
 
 func (impl *OSBuildJobImpl) Run(job worker.Job) error {
-	outputDirectory, err := ioutil.TempDir("/var/tmp", "osbuild-worker-*")
+	outputDirectory, err := ioutil.TempDir(impl.Output, job.Id().String()+"-*")
 	if err != nil {
 		return fmt.Errorf("error creating temporary output directory: %v", err)
 	}
@@ -154,7 +155,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 				Datastore:  options.Datastore,
 			}
 
-			tempDirectory, err := ioutil.TempDir("/var/tmp", "osbuild-worker-vmware-*")
+			tempDirectory, err := ioutil.TempDir(impl.Output, job.Id().String()+"-vmware-*")
 			if err != nil {
 				r = append(r, err)
 				continue
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index 578f77c7c..62907ce81 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -117,6 +117,8 @@ func main() {
 		log.Fatal("CACHE_DIRECTORY is not set. Is the service file missing CacheDirectory=?")
 	}
 	store := path.Join(cacheDirectory, "osbuild-store")
+	output := path.Join(cacheDirectory, "output")
+	_ = os.Mkdir(output, os.ModeDir)
 
 	kojiServers := make(map[string]koji.GSSAPICredentials)
 	for server, creds := range config.KojiServers {
@@ -152,10 +154,12 @@ func main() {
 	jobImpls := map[string]JobImplementation{
 		"osbuild": &OSBuildJobImpl{
 			Store:       store,
+			Output:      output,
 			KojiServers: kojiServers,
 		},
 		"osbuild-koji": &OSBuildKojiJobImpl{
 			Store:       store,
+			Output:      output,
 			KojiServers: kojiServers,
 		},
 		"koji-init": &KojiInitJobImpl{
diff --git a/docs/news/unreleased/weldr-ostree-url.md b/docs/news/unreleased/weldr-ostree-url.md
new file mode 100644
index 000000000..5bbc7ad50
--- /dev/null
+++ b/docs/news/unreleased/weldr-ostree-url.md
@@ -0,0 +1,40 @@
+# Weldr API: Allow parent OSTree commit to be read from repository
+
+The weldr API for building OSTree based images is extended to optionally take an `url` parameter instead of the current `parent`.
+
+The `parent` parameter contains the OSTree commit SHA of the parent commit when building an update commit. Obtaining this is cumbersome, so instead the `url` of the repository containing the desired parent commit can be specified. In this case, composer will take the current `HEAD` of the given `ref` as the parent.
+
+At most one of `parent` and `url` can be specified in a given compose request.
+
+Before:
+
+    curl --silent \
+        --header "Content-Type: application/json" \
+        --unix-socket /run/weldr/api.socket \
+        http://localhost/api/v1/compose \
+        --data "{ \
+            \"blueprint_name\": \"foo\", \
+            \"compose_type\": \"rhel-edge-commit\", \
+            \"ostree\": {\ \
+                \"parent\": \"b8a69e5c79be5830bb272356809a52b1660d2013c26f6973d549d0a312a8d21a\", \
+                \"ref\": \"fedora/stable/x86_64/iot\" \
+            } \
+        }"
+
+After:
+
+    curl --silent \
+        --header "Content-Type: application/json" \
+        --unix-socket /run/weldr/api.socket \
+        http://localhost/api/v1/compose \
+        --data "{ \
+            \"blueprint_name\": \"foo\", \
+            \"compose_type\": \"rhel-edge-commit\", \
+            \"ostree\": {\ \
+                \"url\": \"https://d2ju0wfl996cmc.cloudfront.net/\", \
+                \"ref\": \"fedora/stable/x86_64/iot\" \
+            } \
+        }"
+
+Relevant PRs:
+https://github.com/osbuild/osbuild-composer/pull/1235
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 606f29e19..3946ac97f 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -65,6 +65,9 @@ type ImageType interface {
 	// Retrns the MIME-type for the image type.
 	MIMEType() string
 
+	// Returns the default OSTree ref for the image type.
+	OSTreeRef() string
+
 	// Returns the proper image size for a given output format. If the input size
 	// is 0 the default value for the format will be returned.
 	Size(size uint64) uint64
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index 64ef7a396..22b642c6d 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -18,6 +18,7 @@ import (
 
 const name = "fedora-32"
 const modulePlatformID = "platform:f32"
+const ostreeRef = "fedora/32/%s/iot"
 
 type distribution struct {
 	arches        map[string]architecture
@@ -146,6 +147,13 @@ func (t *imageType) MIMEType() string {
 	return t.mimeType
 }
 
+func (t *imageType) OSTreeRef() string {
+	if t.rpmOstree {
+		return fmt.Sprintf(ostreeRef, t.arch.name)
+	}
+	return ""
+}
+
 func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
@@ -513,13 +521,9 @@ func qemuAssembler(format string, filename string, uefi bool, imageOptions distr
 }
 
 func ostreeCommitAssembler(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-	ref := options.OSTree.Ref
-	if ref == "" {
-		ref = fmt.Sprintf("fedora/32/%s/iot", arch.Name())
-	}
 	return osbuild.NewOSTreeCommitAssembler(
 		&osbuild.OSTreeCommitAssemblerOptions{
-			Ref:    ref,
+			Ref:    options.OSTree.Ref,
 			Parent: options.OSTree.Parent,
 			Tar: osbuild.OSTreeCommitAssemblerTarOptions{
 				Filename: "commit.tar",
diff --git a/internal/distro/fedora33/distro.go b/internal/distro/fedora33/distro.go
index c37e66f57..f7cd66e5b 100644
--- a/internal/distro/fedora33/distro.go
+++ b/internal/distro/fedora33/distro.go
@@ -18,6 +18,7 @@ import (
 
 const name = "fedora-33"
 const modulePlatformID = "platform:f33"
+const ostreeRef = "fedora/33/%s/iot"
 
 type distribution struct {
 	arches        map[string]architecture
@@ -146,6 +147,13 @@ func (t *imageType) MIMEType() string {
 	return t.mimeType
 }
 
+func (t *imageType) OSTreeRef() string {
+	if t.rpmOstree {
+		return fmt.Sprintf(ostreeRef, t.arch.name)
+	}
+	return ""
+}
+
 func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
@@ -525,13 +533,9 @@ func qemuAssembler(format string, filename string, uefi bool, imageOptions distr
 }
 
 func ostreeCommitAssembler(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-	ref := options.OSTree.Ref
-	if ref == "" {
-		ref = fmt.Sprintf("fedora/33/%s/iot", arch.Name())
-	}
 	return osbuild.NewOSTreeCommitAssembler(
 		&osbuild.OSTreeCommitAssemblerOptions{
-			Ref:    ref,
+			Ref:    options.OSTree.Ref,
 			Parent: options.OSTree.Parent,
 			Tar: osbuild.OSTreeCommitAssemblerTarOptions{
 				Filename: "commit.tar",
diff --git a/internal/distro/rhel8/distro.go b/internal/distro/rhel8/distro.go
index 1b91312ee..b019c5131 100644
--- a/internal/distro/rhel8/distro.go
+++ b/internal/distro/rhel8/distro.go
@@ -18,6 +18,7 @@ import (
 
 const name = "rhel-8"
 const modulePlatformID = "platform:el8"
+const ostreeRef = "rhel/8/%s/edge"
 
 type distribution struct {
 	arches        map[string]architecture
@@ -148,6 +149,13 @@ func (t *imageType) MIMEType() string {
 	return t.mimeType
 }
 
+func (t *imageType) OSTreeRef() string {
+	if t.rpmOstree {
+		return fmt.Sprintf(ostreeRef, t.arch.name)
+	}
+	return ""
+}
+
 func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
@@ -611,13 +619,9 @@ func tarAssembler(filename, compression string) *osbuild.Assembler {
 }
 
 func ostreeCommitAssembler(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-	ref := options.OSTree.Ref
-	if ref == "" {
-		ref = fmt.Sprintf("rhel/8/%s/edge", arch.Name())
-	}
 	return osbuild.NewOSTreeCommitAssembler(
 		&osbuild.OSTreeCommitAssemblerOptions{
-			Ref:    ref,
+			Ref:    options.OSTree.Ref,
 			Parent: options.OSTree.Parent,
 			Tar: osbuild.OSTreeCommitAssemblerTarOptions{
 				Filename: "commit.tar",
diff --git a/internal/distro/rhel84/distro.go b/internal/distro/rhel84/distro.go
index b13b2823e..0f706f2e8 100644
--- a/internal/distro/rhel84/distro.go
+++ b/internal/distro/rhel84/distro.go
@@ -22,6 +22,7 @@ import (
 const name = "rhel-84"
 const centosName = "centos-8"
 const modulePlatformID = "platform:el8"
+const ostreeRef = "rhel/8/%s/edge"
 
 type distribution struct {
 	arches        map[string]architecture
@@ -161,6 +162,13 @@ func (t *imageType) MIMEType() string {
 	return t.mimeType
 }
 
+func (t *imageType) OSTreeRef() string {
+	if t.rpmOstree {
+		return fmt.Sprintf(ostreeRef, t.arch.name)
+	}
+	return ""
+}
+
 func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
@@ -722,13 +730,9 @@ func tarAssembler(filename, compression string) *osbuild.Assembler {
 }
 
 func ostreeCommitAssembler(options distro.ImageOptions, arch distro.Arch) *osbuild.Assembler {
-	ref := options.OSTree.Ref
-	if ref == "" {
-		ref = fmt.Sprintf("rhel/8/%s/edge", arch.Name())
-	}
 	return osbuild.NewOSTreeCommitAssembler(
 		&osbuild.OSTreeCommitAssemblerOptions{
-			Ref:    ref,
+			Ref:    options.OSTree.Ref,
 			Parent: options.OSTree.Parent,
 			Tar: osbuild.OSTreeCommitAssemblerTarOptions{
 				Filename: "commit.tar",
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 9bcbf18e0..dd17dfcab 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -4,6 +4,7 @@ import (
 	"archive/tar"
 	"bytes"
 	"crypto/rand"
+	"encoding/hex"
 	"encoding/json"
 	errors_package "errors"
 	"fmt"
@@ -1798,6 +1799,36 @@ func (api *API) blueprintsTagHandler(writer http.ResponseWriter, request *http.R
 	statusResponseOK(writer)
 }
 
+func ostreeResolveRef(location, ref string) (string, error) {
+	u, err := url.Parse(location)
+	if err != nil {
+		return "", err
+	}
+	u, err = u.Parse("refs/heads/")
+	if err != nil {
+		return "", err
+	}
+	u, err = u.Parse(ref)
+	if err != nil {
+		return "", err
+	}
+	resp, err := http.Get(u.String())
+	if err != nil {
+		return "", err
+	}
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	parent := strings.TrimSpace(string(body))
+	// Check that this is at least a hex string.
+	_, err = hex.DecodeString(parent)
+	if err != nil {
+		return "", err
+	}
+	return parent, nil
+}
+
 // Schedule new compose by first translating the appropriate blueprint into a pipeline and then
 // pushing it into the channel for waiting builds.
 func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1806,6 +1837,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	}
 
 	type OSTreeRequest struct {
+		URL    string `json:"url"`
 		Ref    string `json:"ref"`
 		Parent string `json:"parent"`
 	}
@@ -1856,6 +1888,11 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
+	// set default ostree ref, if one not provided
+	if cr.OSTree.Ref == "" {
+		cr.OSTree.Ref = imageType.OSTreeRef()
+	}
+
 	if !verifyStringsWithRegex(writer, []string{cr.BlueprintName}, ValidBlueprintName) {
 		return
 	}
@@ -1878,6 +1915,31 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
+	// Fetch parent ostree commit from ref + url if commit is not
+	// provided. The parameter name "parent" is perhaps slightly misleading
+	// as it represent whatever commit sha the image type requires, not
+	// strictly speaking just the parent commit.
+	if cr.OSTree.Ref != "" && cr.OSTree.URL != "" {
+		if cr.OSTree.Parent != "" {
+			errors := responseError{
+				ID:  "OSTreeOptionsError",
+				Msg: "Supply at most one of Parent and URL",
+			}
+			statusResponseError(writer, http.StatusBadRequest, errors)
+			return
+		}
+		parent, err := ostreeResolveRef(cr.OSTree.URL, cr.OSTree.Ref)
+		if err != nil {
+			errors := responseError{
+				ID:  "OSTreeCommitError",
+				Msg: err.Error(),
+			}
+			statusResponseError(writer, http.StatusBadRequest, errors)
+			return
+		}
+		cr.OSTree.Parent = parent
+	}
+
 	packages, buildPackages, err := api.depsolveBlueprint(bp, imageType)
 	if err != nil {
 		errors := responseError{
diff --git a/schutzbot/Jenkinsfile b/schutzbot/Jenkinsfile
index e332ad7ce..7c7ca7bef 100644
--- a/schutzbot/Jenkinsfile
+++ b/schutzbot/Jenkinsfile
@@ -301,6 +301,23 @@ pipeline {
                         }
                     }
                 }
+                stage('F32 New OSTree') {
+                    when {
+                        expression {
+                            return env.BUILD_CAUSE != 'cron';
+                        }
+                    }
+
+                    agent { label "f32cloudbase && psi && x86_64" }
+                    steps {
+                        run_tests('ostree-ng')
+                    }
+                    post {
+                        always {
+                            preserve_logs('fedora32-ostree-ng')
+                        }
+                    }
+                }
                 stage('F32: koji-osbuild') {
                     when {
                         expression {
@@ -406,6 +423,23 @@ pipeline {
                         }
                     }
                 }
+                stage('F33 New OSTree') {
+                    when {
+                        expression {
+                            return env.BUILD_CAUSE != 'cron';
+                        }
+                    }
+
+                    agent { label "f33cloudbase && psi && x86_64" }
+                    steps {
+                        run_tests('ostree-ng')
+                    }
+                    post {
+                        always {
+                            preserve_logs('fedora33-ostree-ng')
+                        }
+                    }
+                }
                 stage('F33 aarch64 Base') {
                     when {
                         expression {
@@ -547,6 +581,23 @@ pipeline {
                         }
                     }
                 }
+                stage('EL8 New OSTree') {
+                    when {
+                        expression {
+                            return env.BUILD_CAUSE != 'cron';
+                        }
+                    }
+
+                    agent { label "rhel8cloudbase && psi && x86_64" }
+                    steps {
+                        run_tests('ostree-ng')
+                    }
+                    post {
+                        always {
+                            preserve_logs('rhel8-ostree-ng')
+                        }
+                    }
+                }
                 stage('EL8: koji-osbuild') {
                     when {
                         expression {
@@ -631,6 +682,17 @@ pipeline {
                         }
                     }
                 }
+                stage('EL8.4 New OSTree') {
+                    agent { label "rhel84cloudbase && psi && x86_64" }
+                    steps {
+                        run_tests('ostree-ng')
+                    }
+                    post {
+                        always {
+                            preserve_logs('rhel84-ostree-ng')
+                        }
+                    }
+                }
                 stage('CS8 Base') {
                     when {
                         expression {
@@ -792,6 +854,13 @@ void run_tests(test_type) {
         )
     }
 
+    if (test_type == 'ostree-ng') {
+        sh (
+            label: "New OSTree tests",
+            script: "/usr/libexec/tests/osbuild-composer/ostree-ng.sh"
+        )
+    }
+
     if (test_type == 'integration') {
         // Run Koji tests.
         sh (


Test patch:
diff --git a/internal/distro/distro_test_common/distro_test_common.go b/internal/distro/distro_test_common/distro_test_common.go
index 81c904a76..5dc932839 100644
--- a/internal/distro/distro_test_common/distro_test_common.go
+++ b/internal/distro/distro_test_common/distro_test_common.go
@@ -88,6 +88,9 @@ func TestDistro_Manifest(t *testing.T, pipelinePath string, prefix string, distr
 			got, err := imageType.Manifest(tt.ComposeRequest.Blueprint.Customizations,
 				distro.ImageOptions{
 					Size: imageType.Size(0),
+					OSTree: distro.OSTreeImageOptions{
+						Ref: imageType.OSTreeRef(),
+					},
 				},
 				repos,
 				tt.RpmMD.Packages,
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index b262aacd3..293852e43 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -79,6 +79,10 @@ func (t *imageType) MIMEType() string {
 	return "application/x-test"
 }
 
+func (t *imageType) OSTreeRef() string {
+	return ""
+}
+
 func (t *imageType) Size(size uint64) uint64 {
 	return size
 }
diff --git a/internal/distro/test_distro/distro.go b/internal/distro/test_distro/distro.go
index a0c610944..3221aa5b4 100644
--- a/internal/distro/test_distro/distro.go
+++ b/internal/distro/test_distro/distro.go
@@ -63,6 +63,10 @@ func (t *TestImageType) MIMEType() string {
 	return "application/x-test"
 }
 
+func (t *TestImageType) OSTreeRef() string {
+	return ""
+}
+
 func (t *TestImageType) Size(size uint64) uint64 {
 	return 0
 }
diff --git a/test/cases/ostree-ng.sh b/test/cases/ostree-ng.sh
new file mode 100755
index 000000000..018abbf48
--- /dev/null
+++ b/test/cases/ostree-ng.sh
@@ -0,0 +1,477 @@
+#!/bin/bash
+set -euo pipefail
+
+OSBUILD_COMPOSER_TEST_DATA=/usr/share/tests/osbuild-composer/
+URL=http://192.168.100.1/repo/
+
+# Get OS data.
+source /etc/os-release
+ARCH=$(uname -m)
+
+# Provision the software under tet.
+/usr/libexec/osbuild-composer-test/provision.sh
+
+# Set os-variant and boot location used by virt-install.
+case "${ID}-${VERSION_ID}" in
+    "fedora-32")
+        IMAGE_TYPE=fedora-iot-commit
+        OSTREE_REF="fedora/32/${ARCH}/iot"
+        OS_VARIANT="fedora32"
+        BOOT_LOCATION="https://mirrors.rit.edu/fedora/fedora/linux/releases/32/Everything/x86_64/os/";;
+    "fedora-33")
+        IMAGE_TYPE=fedora-iot-commit
+        OSTREE_REF="fedora/33/${ARCH}/iot"
+        OS_VARIANT="fedora33"
+        BOOT_LOCATION="https://mirrors.rit.edu/fedora/fedora/linux/releases/33/Everything/x86_64/os/";;
+    "rhel-8.3")
+        IMAGE_TYPE=rhel-edge-commit
+        OSTREE_REF="rhel/8/${ARCH}/edge"
+        OS_VARIANT="rhel8.3"
+        # When 8.3 was released, it wasn't available on all RH internal
+        # mirrors, therefore the Boston mirror is hardcoded.
+        BOOT_LOCATION="http://download.eng.bos.redhat.com/released/rhel-8/RHEL-8/8.3.0/BaseOS/x86_64/os/";;
+    "rhel-8.4")
+        IMAGE_TYPE=rhel-edge-commit
+        OSTREE_REF="rhel/8/${ARCH}/edge"
+        OS_VARIANT="rhel8-unknown"
+        BOOT_LOCATION="http://download.devel.redhat.com/nightly/rhel-8/RHEL-8/latest-RHEL-8.4/compose/BaseOS/x86_64/os/";;
+    *)
+        echo "unsupported distro: ${ID}-${VERSION_ID}"
+        exit 1;;
+esac
+
+
+# Colorful output.
+function greenprint {
+    echo -e "\033[1;32m${1}\033[0m"
+}
+
+# Start libvirtd and test it.
+greenprint "🚀 Starting libvirt daemon"
+sudo systemctl start libvirtd
+sudo virsh list --all > /dev/null
+
+# Set a customized dnsmasq configuration for libvirt so we always get the
+# same address on bootup.
+sudo tee /tmp/integration.xml > /dev/null << EOF
+<network>
+  <name>integration</name>
+  <uuid>1c8fe98c-b53a-4ca4-bbdb-deb0f26b3579</uuid>
+  <forward mode='nat'>
+    <nat>
+      <port start='1024' end='65535'/>
+    </nat>
+  </forward>
+  <bridge name='integration' stp='on' delay='0'/>
+  <mac address='52:54:00:36:46:ef'/>
+  <ip address='192.168.100.1' netmask='255.255.255.0'>
+    <dhcp>
+      <range start='192.168.100.2' end='192.168.100.254'/>
+      <host mac='34:49:22:B0:83:30' name='vm' ip='192.168.100.50'/>
+    </dhcp>
+  </ip>
+</network>
+EOF
+if ! sudo virsh net-info integration > /dev/null 2>&1; then
+    sudo virsh net-define /tmp/integration.xml
+    sudo virsh net-start integration
+fi
+
+# Allow anyone in the wheel group to talk to libvirt.
+greenprint "🚪 Allowing users in wheel group to talk to libvirt"
+WHEEL_GROUP=wheel
+if [[ $ID == rhel ]]; then
+    WHEEL_GROUP=adm
+fi
+sudo tee /etc/polkit-1/rules.d/50-libvirt.rules > /dev/null << EOF
+polkit.addRule(function(action, subject) {
+    if (action.id == "org.libvirt.unix.manage" &&
+        subject.isInGroup("${WHEEL_GROUP}")) {
+            return polkit.Result.YES;
+    }
+});
+EOF
+
+# Set up variables.
+TEST_UUID=$(uuidgen)
+IMAGE_KEY="osbuild-composer-ostree-test-${TEST_UUID}"
+GUEST_ADDRESS=192.168.100.50
+
+# Set up temporary files.
+TEMPDIR=$(mktemp -d)
+BLUEPRINT_FILE=${TEMPDIR}/blueprint.toml
+KS_FILE=${TEMPDIR}/ks.cfg
+COMPOSE_START=${TEMPDIR}/compose-start-${IMAGE_KEY}.json
+COMPOSE_INFO=${TEMPDIR}/compose-info-${IMAGE_KEY}.json
+
+# SSH setup.
+SSH_OPTIONS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5)
+SSH_KEY=${OSBUILD_COMPOSER_TEST_DATA}keyring/id_rsa
+
+# Get the compose log.
+get_compose_log () {
+    COMPOSE_ID=$1
+    LOG_FILE=${WORKSPACE}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.log
+
+    # Download the logs.
+    sudo composer-cli compose log "$COMPOSE_ID" | tee "$LOG_FILE" > /dev/null
+}
+
+# Get the compose metadata.
+get_compose_metadata () {
+    COMPOSE_ID=$1
+    METADATA_FILE=${WORKSPACE}/osbuild-${ID}-${VERSION_ID}-${COMPOSE_ID}.json
+
+    # Download the metadata.
+    sudo composer-cli compose metadata "$COMPOSE_ID" > /dev/null
+
+    # Find the tarball and extract it.
+    TARBALL=$(basename "$(find . -maxdepth 1 -type f -name "*-metadata.tar")")
+    tar -xf "$TARBALL" -C "${TEMPDIR}"
+    rm -f "$TARBALL"
+
+    # Move the JSON file into place.
+    cat "${TEMPDIR}"/"${COMPOSE_ID}".json | jq -M '.' | tee "$METADATA_FILE" > /dev/null
+}
+
+# Build ostree image.
+build_image() {
+    blueprint_file=$1
+    blueprint_name=$2
+
+    # Prepare the blueprint for the compose.
+    greenprint "📋 Preparing blueprint"
+    sudo composer-cli blueprints push "$blueprint_file"
+    sudo composer-cli blueprints depsolve "$blueprint_name"
+
+    # Get worker unit file so we can watch the journal.
+    WORKER_UNIT=$(sudo systemctl list-units | grep -o -E "osbuild.*worker.*\.service")
+    sudo journalctl -af -n 1 -u "${WORKER_UNIT}" &
+    WORKER_JOURNAL_PID=$!
+
+    # Start the compose.
+    greenprint "🚀 Starting compose"
+    if [[ $blueprint_name == upgrade ]]; then
+        sudo curl --silent --header "Content-Type: application/json" --unix-socket /run/weldr/api.socket http://localhost/api/v1/compose --data "{
+            \"blueprint_name\": \"$blueprint_name\",
+            \"compose_type\": \"$IMAGE_TYPE\",
+            \"ostree\": {
+                \"url\": \"$URL\",
+                \"ref\": \"$OSTREE_REF\"
+            }
+        }" | tee "$COMPOSE_START"
+    else
+        sudo curl --silent --header "Content-Type: application/json" --unix-socket /run/weldr/api.socket http://localhost/api/v1/compose --data "{
+            \"blueprint_name\": \"$blueprint_name\",
+            \"compose_type\": \"$IMAGE_TYPE\"
+        }" | tee "$COMPOSE_START"
+    fi
+    COMPOSE_ID=$(jq -r '.build_id' "$COMPOSE_START")
+
+    # Wait for the compose to finish.
+    greenprint "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
+    while true; do
+        sudo composer-cli --json compose info "${COMPOSE_ID}" | tee "$COMPOSE_INFO" > /dev/null
+        COMPOSE_STATUS=$(jq -r '.queue_status' "$COMPOSE_INFO")
+
+        # Is the compose finished?
+        if [[ $COMPOSE_STATUS != RUNNING ]] && [[ $COMPOSE_STATUS != WAITING ]]; then
+            break
+        fi
+
+        # Wait 30 seconds and try again.
+        sleep 5
+    done
+
+    # Capture the compose logs from osbuild.
+    greenprint "💬 Getting compose log and metadata"
+    get_compose_log "$COMPOSE_ID"
+    get_compose_metadata "$COMPOSE_ID"
+
+    # Did the compose finish with success?
+    if [[ $COMPOSE_STATUS != FINISHED ]]; then
+        echo "Something went wrong with the compose. 😢"
+        exit 1
+    fi
+
+    # Stop watching the worker journal.
+    sudo kill ${WORKER_JOURNAL_PID}
+}
+
+# Wait for the ssh server up to be.
+wait_for_ssh_up () {
+    SSH_STATUS=$(sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@"${1}" '/bin/bash -c "echo -n READY"')
+    if [[ $SSH_STATUS == READY ]]; then
+        echo 1
+    else
+        echo 0
+    fi
+}
+
+# Clean up our mess.
+clean_up () {
+    greenprint "🧼 Cleaning up"
+    sudo virsh destroy "${IMAGE_KEY}"
+    if [[ $ARCH == aarch64 ]]; then
+        sudo virsh undefine "${IMAGE_KEY}" --nvram
+    else
+        sudo virsh undefine "${IMAGE_KEY}"
+    fi
+    # Remove qcow2 file.
+    sudo rm -f "$LIBVIRT_IMAGE_PATH"
+    # Remove extracted upgrade image-tar.
+    sudo rm -rf "$UPGRADE_PATH"
+    # Remove "remote" repo.
+    sudo rm -rf "${HTTPD_PATH}"/{repo,compose.json}
+    # Remomve tmp dir.
+    sudo rm -rf "$TEMPDIR"
+    # Stop httpd
+    sudo systemctl disable httpd --now
+}
+
+# Test result checking
+check_result () {
+    greenprint "Checking for test result"
+    if [[ $RESULTS == 1 ]]; then
+        greenprint "💚 Success"
+    else
+        greenprint "❌ Failed"
+        clean_up
+        exit 1
+    fi
+}
+
+##################################################
+##
+## ostree image/commit installation
+##
+##################################################
+
+# Write a blueprint for ostree image.
+tee "$BLUEPRINT_FILE" > /dev/null << EOF
+name = "ostree"
+description = "A base ostree image"
+version = "0.0.1"
+modules = []
+groups = []
+
+[[packages]]
+name = "python36"
+version = "*"
+EOF
+
+# Build installation image.
+build_image "$BLUEPRINT_FILE" ostree
+
+# Start httpd to serve ostree repo.
+greenprint "🚀 Starting httpd daemon"
+sudo systemctl start httpd
+
+# Download the image and extract tar into web server root folder.
+greenprint "📥 Downloading and extracting the image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+IMAGE_FILENAME="${COMPOSE_ID}-commit.tar"
+HTTPD_PATH="/var/www/html"
+sudo tar -xf "${IMAGE_FILENAME}" -C ${HTTPD_PATH}
+sudo rm -f "$IMAGE_FILENAME"
+
+# Clean compose and blueprints.
+greenprint "Clean up osbuild-composer"
+sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
+sudo composer-cli blueprints delete ostree > /dev/null
+
+# Ensure SELinux is happy with our new images.
+greenprint "👿 Running restorecon on image directory"
+sudo restorecon -Rv /var/lib/libvirt/images/
+
+# Create qcow2 file for virt install.
+greenprint "Create qcow2 file for virt install"
+LIBVIRT_IMAGE_PATH=/var/lib/libvirt/images/${IMAGE_KEY}.qcow2
+sudo qemu-img create -f qcow2 "${LIBVIRT_IMAGE_PATH}" 20G
+
+# Write kickstart file for ostree image installation.
+greenprint "Generate kickstart file"
+tee "$KS_FILE" > /dev/null << STOPHERE
+text
+lang en_US.UTF-8
+keyboard us
+timezone --utc Etc/UTC
+
+selinux --enforcing
+rootpw --lock --iscrypted locked
+user --name=admin --groups=wheel --iscrypted --password=\$6\$1LgwKw9aOoAi/Zy9\$Pn3ErY1E8/yEanJ98evqKEW.DZp24HTuqXPJl6GYCm8uuobAmwxLv7rGCvTRZhxtcYdmC0.XnYRSR9Sh6de3p0
+sshkey --username=admin "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+
+bootloader --timeout=1 --append="net.ifnames=0 modprobe.blacklist=vc4"
+
+network --bootproto=dhcp --device=link --activate --onboot=on
+
+zerombr
+clearpart --all --initlabel --disklabel=msdos
+autopart --nohome --noswap --type=plain
+ostreesetup --nogpg --osname=${IMAGE_TYPE} --remote=${IMAGE_TYPE} --url=${URL} --ref=${OSTREE_REF}
+poweroff
+
+%post --log=/var/log/anaconda/post-install.log --erroronfail
+
+# no sudo password for user admin
+echo -e 'admin\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
+
+# Remove any persistent NIC rules generated by udev
+rm -vf /etc/udev/rules.d/*persistent-net*.rules
+# And ensure that we will do DHCP on eth0 on startup
+cat > /etc/sysconfig/network-scripts/ifcfg-eth0 << EOF
+DEVICE="eth0"
+BOOTPROTO="dhcp"
+ONBOOT="yes"
+TYPE="Ethernet"
+PERSISTENT_DHCLIENT="yes"
+EOF
+
+echo "Packages within this iot or edge image:"
+echo "-----------------------------------------------------------------------"
+rpm -qa | sort
+echo "-----------------------------------------------------------------------"
+# Note that running rpm recreates the rpm db files which aren't needed/wanted
+rm -f /var/lib/rpm/__db*
+
+echo "Zeroing out empty space."
+# This forces the filesystem to reclaim space from deleted files
+dd bs=1M if=/dev/zero of=/var/tmp/zeros || :
+rm -f /var/tmp/zeros
+echo "(Don't worry -- that out-of-space error was expected.)"
+
+%end
+STOPHERE
+
+# Install ostree image via anaconda.
+greenprint "Install ostree image via anaconda"
+sudo virt-install  --initrd-inject="${KS_FILE}" \
+                   --extra-args="ks=file:/ks.cfg console=ttyS0,115200" \
+                   --name="${IMAGE_KEY}"\
+                   --disk path="${LIBVIRT_IMAGE_PATH}",format=qcow2 \
+                   --ram 3072 \
+                   --vcpus 2 \
+                   --network network=integration,mac=34:49:22:B0:83:30 \
+                   --os-type linux \
+                   --os-variant ${OS_VARIANT} \
+                   --location ${BOOT_LOCATION} \
+                   --nographics \
+                   --noautoconsole \
+                   --wait=-1 \
+                   --noreboot
+
+# Start VM.
+greenprint "Start VM"
+sudo virsh start "${IMAGE_KEY}"
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+for LOOP_COUNTER in $(seq 0 30); do
+    RESULTS="$(wait_for_ssh_up $GUEST_ADDRESS)"
+    if [[ $RESULTS == 1 ]]; then
+        echo "SSH is ready now! 🥳"
+        break
+    fi
+    sleep 10
+done
+
+# Check image installation result
+check_result
+
+##################################################
+##
+## ostree image/commit upgrade
+##
+##################################################
+
+# Write a blueprint for ostree image.
+tee "$BLUEPRINT_FILE" > /dev/null << EOF
+name = "upgrade"
+description = "An upgrade ostree image"
+version = "0.0.2"
+modules = []
+groups = []
+
+[[packages]]
+name = "python36"
+version = "*"
+
+[[packages]]
+name = "wget"
+version = "*"
+EOF
+
+# Build upgrade image.
+build_image "$BLUEPRINT_FILE" upgrade
+
+# Download the image and extract tar into web server root folder.
+greenprint "📥 Downloading and extracting the image"
+sudo composer-cli compose image "${COMPOSE_ID}" > /dev/null
+IMAGE_FILENAME="${COMPOSE_ID}-commit.tar"
+UPGRADE_PATH="$(pwd)/upgrade"
+mkdir -p "$UPGRADE_PATH"
+sudo tar -xf "$IMAGE_FILENAME" -C "$UPGRADE_PATH"
+sudo rm -f "$IMAGE_FILENAME"
+
+# Clean compose and blueprints.
+greenprint "Clean up osbuild-composer again"
+sudo composer-cli compose delete "${COMPOSE_ID}" > /dev/null
+sudo composer-cli blueprints delete upgrade > /dev/null
+
+# Introduce new ostree commit into repo.
+greenprint "Introduce new ostree commit into repo"
+sudo ostree pull-local --repo "${HTTPD_PATH}/repo" "${UPGRADE_PATH}/repo" "$OSTREE_REF"
+sudo ostree summary --update --repo "${HTTPD_PATH}/repo"
+
+# Ensure SELinux is happy with all objects files.
+greenprint "👿 Running restorecon on web server root folder"
+sudo restorecon -Rv "${HTTPD_PATH}/repo" > /dev/null
+
+# Get ostree commit value.
+greenprint "Get ostree image commit value"
+UPGRADE_HASH=$(jq -r '."ostree-commit"' < "${UPGRADE_PATH}"/compose.json)
+
+# Upgrade image/commit.
+greenprint "Upgrade ostree image/commit"
+sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@${GUEST_ADDRESS} 'sudo rpm-ostree upgrade'
+sudo ssh "${SSH_OPTIONS[@]}" -i "${SSH_KEY}" admin@${GUEST_ADDRESS} 'nohup sudo systemctl reboot &>/dev/null & exit'
+
+# Sleep 10 seconds here to make sure vm restarted already
+sleep 10
+
+# Check for ssh ready to go.
+greenprint "🛃 Checking for SSH is ready to go"
+# shellcheck disable=SC2034  # Unused variables left for readability
+for LOOP_COUNTER in $(seq 0 30); do
+    RESULTS="$(wait_for_ssh_up $GUEST_ADDRESS)"
+    if [[ $RESULTS == 1 ]]; then
+        echo "SSH is ready now! 🥳"
+        break
+    fi
+    sleep 10
+done
+
+# Check ostree upgrade result
+check_result
+
+# Add instance IP address into /etc/ansible/hosts
+sudo tee "${TEMPDIR}"/inventory > /dev/null << EOF
+[ostree_guest]
+${GUEST_ADDRESS}
+
+[ostree_guest:vars]
+ansible_python_interpreter=/usr/bin/python3
+ansible_user=admin
+ansible_private_key_file=${SSH_KEY}
+ansible_ssh_common_args="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+EOF
+
+# Test IoT/Edge OS
+sudo ansible-playbook -v -i "${TEMPDIR}"/inventory -e image_type=${IMAGE_TYPE} -e ostree_commit="${UPGRADE_HASH}" /usr/share/tests/osbuild-composer/ansible/check_ostree.yaml || RESULTS=0
+check_result
+
+# Final success clean up
+clean_up
+
+exit 0