Repository: osbuild/osbuild-composer
Pull request #93 (2019-11-12 12:43:47)

api/tests: print all differences between expected and actual values
After this change one can immediately see all the differences when developing
or running the tests.

ondrejbudai: Sorry, that commit was stupid. The new one should be good. This is just small non-important change but it makes my life easier. When I use these tests against lorax API and lorax returns different status code I don't get the diff for response body. In this case I need to fix the status code in the test and rerun it to get also the response body. After this commit I will get both status code and response body nicely printed next to each other.

Patch:
diff --git a/.travis.yml b/.travis.yml
index ba670d6a8..4c8a37cde 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,17 +15,14 @@ matrix:
 
     - language: python
       python: 3.7
-      env: PYTHONUNBUFFERED=1
       script:
         # ubuntu's rpm package sets dbpath to ~/.rpmdb, which makes rpm fail...
         - sudo sh -c 'mkdir /etc/rpm; echo "%_dbpath /var/lib/rpm" > /etc/rpm/macros'
-        - sudo env "PATH=$PATH" "OSBUILD_TEST_BUILD_PIPELINE=test/build-from-yum.json" test/run --image-info
-    # I know this looks stupid, but it actually enables the tests to run in parallel, so it takes less time
+        - sudo env "PATH=$PATH" "PYTHONUNBUFFERED=1" "OSBUILD_TEST_BUILD_PIPELINE=test/build-from-yum.json" test/run --image-info
     - language: python
       python: 3.7
-      env: PYTHONUNBUFFERED=1
       before_install: sudo apt-get install -y qemu-kvm
       script:
         # ubuntu's rpm package sets dbpath to ~/.rpmdb, which makes rpm fail...
         - sudo sh -c 'mkdir /etc/rpm; echo "%_dbpath /var/lib/rpm" > /etc/rpm/macros'
-        - sudo env "PATH=$PATH" "OSBUILD_TEST_BUILD_PIPELINE=test/build-from-yum.json" test/run --boot-test
+        - sudo env "PATH=$PATH" "PYTHONUNBUFFERED=1" "OSBUILD_TEST_BUILD_PIPELINE=test/build-from-yum.json" test/run --boot-test
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 504d52741..0088a8cf7 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -38,7 +38,9 @@ func main() {
 		Metalink: "https://mirrors.fedoraproject.org/metalink?repo=fedora-30&arch=x86_64",
 	}
 
-	packages, err := rpmmd.FetchPackageList([]rpmmd.RepoConfig{repo})
+	rpm := rpmmd.NewRPMMD()
+
+	packages, err := rpm.FetchPackageList([]rpmmd.RepoConfig{repo})
 	if err != nil {
 		panic(err)
 	}
@@ -56,7 +58,7 @@ func main() {
 	store := store.New(&stateFile)
 
 	jobAPI := jobqueue.New(logger, store)
-	weldrAPI := weldr.New(repo, packages, logger, store)
+	weldrAPI := weldr.New(rpm, repo, packages, logger, store)
 
 	go jobAPI.Serve(jobListener)
 	weldrAPI.Serve(weldrListener)
diff --git a/dnf-json b/dnf-json
index 7e043b317..c0f0c103e 100644
--- a/dnf-json
+++ b/dnf-json
@@ -5,6 +5,8 @@ import dnf
 import json
 import sys
 
+DNF_ERROR_EXIT_CODE = 10
+
 
 def timestamp_to_rfc3339(timestamp):
     d = datetime.datetime.utcfromtimestamp(package.buildtime)
@@ -39,6 +41,11 @@ def create_base(repos):
     return base
 
 
+def exit_with_dnf_error(kind: str, reason: str):
+    json.dump({"kind": kind, "reason": reason}, sys.stdout)
+    sys.exit(DNF_ERROR_EXIT_CODE)
+
+
 call = json.load(sys.stdin)
 command = call["command"]
 arguments = call.get("arguments", {})
@@ -63,8 +70,18 @@ if command == "dump":
 
 elif command == "depsolve":
     base = create_base(arguments.get("repos", {}))
-    base.install_specs(arguments["package-specs"])
-    base.resolve()
+    errors = []
+
+    try:
+        base.install_specs(arguments["package-specs"])
+    except dnf.exceptions.MarkingErrors as e:
+        exit_with_dnf_error("MarkingErrors", f"Error occurred when marking packages for installation: {e}")
+
+    try:
+        base.resolve()
+    except dnf.exceptions.DepsolveError as e:
+        exit_with_dnf_error("DepsolveError", f"There was a problem depsolving {arguments['package-specs']}: {e}")
+
     packages = []
     for package in base.transaction.install_set:
         packages.append({
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index 1c82b25a1..f3968969e 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -11,8 +11,8 @@ As such, it is a drop-in replacement.
 }
 
 Name:           %{goname}
-Release:        1%{?dist}
-Summary:        An image building service based on osbuild.
+Release:        2%{?dist}
+Summary:        An image building service based on osbuild
 
 # Upstream license specification: Apache-2.0
 License:        ASL 2.0
@@ -67,14 +67,13 @@ install -m 0755 -vd                                         %{buildroot}%{_local
 %files
 %license LICENSE
 %doc README.md
-%{_libexecdir}/osbuild-composer
-%{_libexecdir}/osbuild-composer/osbuild-composer
-%{_libexecdir}/osbuild-composer/osbuild-worker
-%{_libexecdir}/osbuild-composer/dnf-json
+%{_libexecdir}/osbuild-composer/
 %{_unitdir}/*.{service,socket}
 %{_sysusersdir}/osbuild-composer.conf
 
 %changelog
+* Wed Nov 13 15:14:00 CEST 2019 Ondrej Budai <obudai@redhat.com> - 2-2
+- Fix specfile according to packaging guidelines.
 * Mon Nov 11 13:23:00 CEST 2019 Tom Gundersen <teg@jklm.no> - 2-1
 - First release.
 
diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go
index 3d1016666..a7f9a0477 100644
--- a/internal/blueprint/blueprint.go
+++ b/internal/blueprint/blueprint.go
@@ -22,6 +22,14 @@ type Blueprint struct {
 	Customizations *Customizations `json:"customizations,omitempty"`
 }
 
+type Change struct {
+	Commit    string    `json:"commit"`
+	Message   string    `json:"message"`
+	Revision  *string   `json:"revision"`
+	Timestamp string    `json:"timestamp"`
+	Blueprint Blueprint `json:"-"`
+}
+
 // A Package specifies an RPM package.
 type Package struct {
 	Name    string `json:"name"`
@@ -42,10 +50,10 @@ func (b *Blueprint) GetKernelCustomization() *KernelCustomization {
 }
 
 func (p Package) ToNameVersion() string {
-       // Omit version to prevent all packages with prefix of name to be installed
-       if p.Version == "*" {
-               return p.Name
-       }
+	// Omit version to prevent all packages with prefix of name to be installed
+	if p.Version == "*" {
+		return p.Name
+	}
 
-       return p.Name + "-" + p.Version
+	return p.Name + "-" + p.Version
 }
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
new file mode 100644
index 000000000..706db07d7
--- /dev/null
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -0,0 +1,58 @@
+package rpmmd_mock
+
+import (
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
+var basePackageList = fetchPackageList{
+	rpmmd.PackageList{
+		{Name: "package1"},
+		{Name: "package2"},
+	},
+	nil,
+}
+
+var BaseFixture = Fixture{
+	basePackageList,
+	depsolve{
+		[]rpmmd.PackageSpec{
+			{
+				Name:    "libgpg-error",
+				Epoch:   0,
+				Version: "1.33",
+				Release: "2.fc30",
+				Arch:    "x86_64",
+			},
+			{
+				Name:    "libsemanage",
+				Epoch:   0,
+				Version: "2.9",
+				Release: "1.fc30",
+				Arch:    "x86_64",
+			},
+		},
+		nil,
+	},
+}
+
+var NonExistingPackage = Fixture{
+	basePackageList,
+	depsolve{
+		nil,
+		&rpmmd.DNFError{
+			Kind:   "MarkingErrors",
+			Reason: "Error occurred when marking packages for installation: Problems in request:\nmissing packages: fash",
+		},
+	},
+}
+
+var BadDepsolve = Fixture{
+	basePackageList,
+	depsolve{
+		nil,
+		&rpmmd.DNFError{
+			Kind:   "DepsolveError",
+			Reason: "There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch",
+		},
+	},
+}
diff --git a/internal/mocks/rpmmd/rpmmd_mock.go b/internal/mocks/rpmmd/rpmmd_mock.go
new file mode 100644
index 000000000..a5b08e04a
--- /dev/null
+++ b/internal/mocks/rpmmd/rpmmd_mock.go
@@ -0,0 +1,33 @@
+package rpmmd_mock
+
+import "github.com/osbuild/osbuild-composer/internal/rpmmd"
+
+type fetchPackageList struct {
+	ret rpmmd.PackageList
+	err error
+}
+type depsolve struct {
+	ret []rpmmd.PackageSpec
+	err error
+}
+
+type Fixture struct {
+	fetchPackageList
+	depsolve
+}
+
+type rpmmdMock struct {
+	Fixture Fixture
+}
+
+func NewRPMMDMock(fixture Fixture) rpmmd.RPMMD {
+	return &rpmmdMock{Fixture: fixture}
+}
+
+func (r *rpmmdMock) FetchPackageList(repos []rpmmd.RepoConfig) (rpmmd.PackageList, error) {
+	return r.Fixture.fetchPackageList.ret, r.Fixture.fetchPackageList.err
+}
+
+func (r *rpmmdMock) Depsolve(specs []string, repos []rpmmd.RepoConfig) ([]rpmmd.PackageSpec, error) {
+	return r.Fixture.depsolve.ret, r.Fixture.depsolve.err
+}
diff --git a/internal/pipeline/script_stage.go b/internal/pipeline/script_stage.go
new file mode 100644
index 000000000..a6b194a07
--- /dev/null
+++ b/internal/pipeline/script_stage.go
@@ -0,0 +1,24 @@
+package pipeline
+
+// The ScriptStageOptions specifies a custom script to run in the image
+type ScriptStageOptions struct {
+	Script string `json:"script"`
+}
+
+func (ScriptStageOptions) isStageOptions() {}
+
+// NewScriptStageOptions creates a new script stage options object, with
+// the mandatory fields set.
+func NewScriptStageOptions(script string) *ScriptStageOptions {
+	return &ScriptStageOptions{
+		Script: script,
+	}
+}
+
+// NewScriptStage creates a new Script Stage object.
+func NewScriptStage(options *ScriptStageOptions) *Stage {
+	return &Stage{
+		Name:    "org.osbuild.script",
+		Options: options,
+	}
+}
diff --git a/internal/pipeline/stage.go b/internal/pipeline/stage.go
index 099a5bd4e..3f6e2c56d 100644
--- a/internal/pipeline/stage.go
+++ b/internal/pipeline/stage.go
@@ -64,6 +64,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error {
 		options = new(FirewallStageOptions)
 	case "org.osbuild.systemd":
 		options = new(SystemdStageOptions)
+	case "org.osbuild.script":
+		options = new(ScriptStageOptions)
 	default:
 		return errors.New("unexpected stage name")
 	}
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 1ec88979f..291924c91 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -2,6 +2,8 @@ package rpmmd
 
 import (
 	"encoding/json"
+	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"sort"
@@ -39,6 +41,20 @@ type PackageSpec struct {
 	Arch    string `json:"arch,omitempty"`
 }
 
+type RPMMD interface {
+	FetchPackageList(repos []RepoConfig) (PackageList, error)
+	Depsolve(specs []string, repos []RepoConfig) ([]PackageSpec, error)
+}
+
+type DNFError struct {
+	Kind   string `json:"kind"`
+	Reason string `json:"reason"`
+}
+
+func (err *DNFError) Error() string {
+	return fmt.Sprintf("DNF error occured: %s: %s", err.Kind, err.Reason)
+}
+
 func runDNF(command string, arguments interface{}, result interface{}) error {
 	var call = struct {
 		Command   string      `json:"command"`
@@ -72,24 +88,52 @@ func runDNF(command string, arguments interface{}, result interface{}) error {
 	}
 	stdin.Close()
 
-	err = json.NewDecoder(stdout).Decode(result)
+	output, err := ioutil.ReadAll(stdout)
 	if err != nil {
 		return err
 	}
 
-	return cmd.Wait()
+	err = cmd.Wait()
+
+	const DnfErrorExitCode = 10
+	if runError, ok := err.(*exec.ExitError); ok && runError.ExitCode() == DnfErrorExitCode {
+		var dnfError DNFError
+		err = json.Unmarshal(output, &dnfError)
+		if err != nil {
+			return err
+		}
+
+		return &dnfError
+	}
+
+	err = json.Unmarshal(output, result)
+
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
-func FetchPackageList(repos []RepoConfig) (PackageList, error) {
+type rpmmdImpl struct{}
+
+func NewRPMMD() RPMMD {
+	return &rpmmdImpl{}
+}
+
+func (*rpmmdImpl) FetchPackageList(repos []RepoConfig) (PackageList, error) {
 	var arguments = struct {
 		Repos []RepoConfig `json:"repos"`
 	}{repos}
 	var packages PackageList
 	err := runDNF("dump", arguments, &packages)
+	sort.Slice(packages, func(i, j int) bool {
+		return packages[i].Name < packages[j].Name
+	})
 	return packages, err
 }
 
-func Depsolve(specs []string, repos []RepoConfig) ([]PackageSpec, error) {
+func (*rpmmdImpl) Depsolve(specs []string, repos []RepoConfig) ([]PackageSpec, error) {
 	var arguments = struct {
 		PackageSpecs []string     `json:"package-specs"`
 		Repos        []RepoConfig `json:"repos"`
diff --git a/internal/store/store.go b/internal/store/store.go
index 6816be987..aefa11ce1 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -3,6 +3,8 @@
 package store
 
 import (
+	"crypto/sha1"
+	"encoding/hex"
 	"encoding/json"
 	"io/ioutil"
 	"log"
@@ -25,10 +27,11 @@ import (
 // A Store contains all the persistent state of osbuild-composer, and is serialized
 // on every change, and deserialized on start.
 type Store struct {
-	Blueprints map[string]blueprint.Blueprint `json:"blueprints"`
-	Workspace  map[string]blueprint.Blueprint `json:"workspace"`
-	Composes   map[uuid.UUID]Compose          `json:"composes"`
-	Sources    map[string]SourceConfig        `json:"sources"`
+	Blueprints        map[string]blueprint.Blueprint         `json:"blueprints"`
+	Workspace         map[string]blueprint.Blueprint         `json:"workspace"`
+	Composes          map[uuid.UUID]Compose                  `json:"composes"`
+	Sources           map[string]SourceConfig                `json:"sources"`
+	BlueprintsChanges map[string]map[string]blueprint.Change `json:"changes"`
 
 	mu           sync.RWMutex // protects all fields
 	pendingJobs  chan Job
@@ -142,6 +145,9 @@ func New(stateFile *string) *Store {
 	if s.Sources == nil {
 		s.Sources = make(map[string]SourceConfig)
 	}
+	if s.BlueprintsChanges == nil {
+		s.BlueprintsChanges = make(map[string]map[string]blueprint.Change)
+	}
 	s.pendingJobs = make(chan Job, 200)
 
 	return &s
@@ -362,9 +368,42 @@ func (s *Store) GetBlueprintCommitted(name string, bp *blueprint.Blueprint) bool
 	return true
 }
 
+func (s *Store) GetBlueprintChanges(name string) []blueprint.Change {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	var changes []blueprint.Change
+
+	for _, change := range s.BlueprintsChanges[name] {
+		changes = append(changes, change)
+	}
+
+	return changes
+}
+
 func (s *Store) PushBlueprint(bp blueprint.Blueprint) {
 	s.change(func() error {
+		hash := sha1.New()
+		// Hash timestamp to create unique hash
+		hash.Write([]byte(time.Now().String()))
+		// Get hash as a byte slice
+		commitBytes := hash.Sum(nil)
+		// Get hash as a hex string
+		commit := hex.EncodeToString(commitBytes)
+		message := "Recipe " + bp.Name + ", version " + bp.Version + " saved."
+		timestamp := time.Now().Format("2006-01-02T15:04:05Z")
+		change := blueprint.Change{
+			Commit:    commit,
+			Message:   message,
+			Timestamp: timestamp,
+			Blueprint: bp,
+		}
+
 		delete(s.Workspace, bp.Name)
+		if s.BlueprintsChanges[bp.Name] == nil {
+			s.BlueprintsChanges[bp.Name] = make(map[string]blueprint.Change)
+		}
+		s.BlueprintsChanges[bp.Name][commit] = change
 		s.Blueprints[bp.Name] = bp
 		return nil
 	})
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 204a9b97f..82551a99f 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -24,6 +24,7 @@ import (
 type API struct {
 	store *store.Store
 
+	rpmmd    rpmmd.RPMMD
 	repo     rpmmd.RepoConfig
 	packages rpmmd.PackageList
 
@@ -31,11 +32,12 @@ type API struct {
 	router *httprouter.Router
 }
 
-func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger, store *store.Store) *API {
+func New(rpmmd rpmmd.RPMMD, repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger, store *store.Store) *API {
 	// This needs to be shared with the worker API so that they can communicate with each other
 	// builds := make(chan queue.Build, 200)
 	api := &API{
 		store:    store,
+		rpmmd:    rpmmd,
 		repo:     repo,
 		packages: packages,
 		logger:   logger,
@@ -53,6 +55,8 @@ func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger,
 	api.router.POST("/api/v0/projects/source/new", api.sourceNewHandler)
 	api.router.DELETE("/api/v0/projects/source/delete/*source", api.sourceDeleteHandler)
 
+	api.router.GET("/api/v0/projects/depsolve/:projects", api.projectsDepsolveHandler)
+
 	api.router.GET("/api/v0/modules/list", api.modulesListAllHandler)
 	api.router.GET("/api/v0/modules/list/:modules", api.modulesListHandler)
 
@@ -64,6 +68,7 @@ func New(repo rpmmd.RepoConfig, packages rpmmd.PackageList, logger *log.Logger,
 	api.router.GET("/api/v0/blueprints/info/*blueprints", api.blueprintsInfoHandler)
 	api.router.GET("/api/v0/blueprints/depsolve/*blueprints", api.blueprintsDepsolveHandler)
 	api.router.GET("/api/v0/blueprints/diff/:blueprint/:from/:to", api.blueprintsDiffHandler)
+	api.router.GET("/api/v0/blueprints/changes/*blueprints", api.blueprintsChangesHandler)
 	api.router.POST("/api/v0/blueprints/new", api.blueprintsNewHandler)
 	api.router.POST("/api/v0/blueprints/workspace", api.blueprintsWorkspaceHandler)
 	api.router.DELETE("/api/v0/blueprints/delete/:blueprint", api.blueprintDeleteHandler)
@@ -466,7 +471,7 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 		}
 
 		if modulesRequested {
-			project.Dependencies, _ = rpmmd.Depsolve([]string{pkg.Name}, []rpmmd.RepoConfig{api.repo})
+			project.Dependencies, _ = api.rpmmd.Depsolve([]string{pkg.Name}, []rpmmd.RepoConfig{api.repo})
 		}
 
 		projects = append(projects, project)
@@ -479,6 +484,29 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 	}
 }
 
+func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
+	type reply struct {
+		Projects []rpmmd.PackageSpec `json:"projects"`
+	}
+
+	names := strings.Split(params.ByName("projects"), ",")
+
+	packages, err := api.rpmmd.Depsolve(names, []rpmmd.RepoConfig{api.repo})
+
+	if err != nil {
+		errors := responseError{
+			ID:  "PROJECTS_ERROR",
+			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	json.NewEncoder(writer).Encode(reply{
+		Projects: packages,
+	})
+}
+
 func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
 	type reply struct {
 		Total      uint     `json:"total"`
@@ -609,7 +637,7 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 				specs[i] += "-*-*.*"
 			}
 		}
-		dependencies, _ := rpmmd.Depsolve(specs, []rpmmd.RepoConfig{api.repo})
+		dependencies, _ := api.rpmmd.Depsolve(specs, []rpmmd.RepoConfig{api.repo})
 
 		blueprints = append(blueprints, entry{blueprint, dependencies})
 	}
@@ -706,6 +734,73 @@ func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.
 	json.NewEncoder(writer).Encode(reply{diffs})
 }
 
+func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
+	type change struct {
+		Changes []blueprint.Change `json:"changes"`
+		Name    string             `json:"name"`
+		Total   int                `json:"total"`
+	}
+
+	type reply struct {
+		BlueprintsChanges []change        `json:"blueprints"`
+		Errors            []responseError `json:"errors"`
+		Limit             uint            `json:"limit"`
+		Offset            uint            `json:"offset"`
+	}
+
+	names := strings.Split(params.ByName("blueprints"), ",")
+	if names[0] == "/" {
+		errors := responseError{
+			Code: http.StatusNotFound,
+			ID:   "HTTPError",
+			Msg:  "Not Found",
+		}
+		statusResponseError(writer, http.StatusNotFound, errors)
+		return
+	}
+
+	offset, limit, err := parseOffsetAndLimit(request.URL.Query())
+	if err != nil {
+		errors := responseError{
+			ID:  "BadLimitOrOffset",
+			Msg: fmt.Sprintf("BadRequest: %s", err.Error()),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	allChanges := []change{}
+	errors := []responseError{}
+	for i, name := range names {
+		// remove leading / from first name
+		if i == 0 {
+			name = name[1:]
+		}
+		bpChanges := api.store.GetBlueprintChanges(name)
+		if bpChanges != nil {
+			change := change{
+				Changes: bpChanges,
+				Name:    name,
+				Total:   len(bpChanges),
+			}
+			allChanges = append(allChanges, change)
+		} else {
+			error := responseError{
+				ID:  "UnknownBlueprint",
+				Msg: name,
+			}
+			errors = append(errors, error)
+		}
+	}
+
+	json.NewEncoder(writer).Encode(reply{
+		BlueprintsChanges: allChanges,
+		Errors:            errors,
+		Offset:            offset,
+		Limit:             limit,
+	})
+}
+
 func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
 	contentType := request.Header["Content-Type"]
 	if len(contentType) != 1 || contentType[0] != "application/json" {


Test patch:
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 230683ab1..d829aebe0 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -5,14 +5,18 @@ import (
 	"context"
 	"encoding/json"
 	"io/ioutil"
+	"math/rand"
 	"net"
 	"net/http"
 	"net/http/httptest"
 	"os"
 	"reflect"
+	"strconv"
 	"strings"
 	"testing"
+	"time"
 
+	"github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
@@ -24,11 +28,6 @@ var repo = rpmmd.RepoConfig{
 	BaseURL: "http://example.com/test/os",
 }
 
-var packages = rpmmd.PackageList{
-	{Name: "package1"},
-	{Name: "package2"},
-}
-
 func externalRequest(method, path, body string) *http.Response {
 	client := http.Client{
 		Transport: &http.Transport{
@@ -116,7 +115,6 @@ func testRoute(t *testing.T, api *weldr.API, external bool, method, path, body s
 
 	if resp.StatusCode != expectedStatus {
 		t.Errorf("%s: expected status %v, but got %v", path, expectedStatus, resp.StatusCode)
-		return
 	}
 
 	replyJSON, err := ioutil.ReadAll(resp.Body)
@@ -158,6 +156,18 @@ func testRoute(t *testing.T, api *weldr.API, external bool, method, path, body s
 	}
 }
 
+func createWeldrAPI(fixture rpmmd_mock.Fixture) (*weldr.API, *store.Store) {
+	s := store.New(nil)
+	rpm := rpmmd_mock.NewRPMMDMock(fixture)
+	packageList, err := rpm.FetchPackageList([]rpmmd.RepoConfig{repo})
+
+	if err != nil {
+		panic(err)
+	}
+
+	return weldr.New(rpm, repo, packageList, nil, s), s
+}
+
 func TestBasic(t *testing.T) {
 	var cases = []struct {
 		Path           string
@@ -194,7 +204,7 @@ func TestBasic(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
@@ -211,7 +221,7 @@ func TestBlueprintsNew(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
 }
@@ -228,7 +238,7 @@ func TestBlueprintsWorkspace(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
@@ -249,7 +259,7 @@ func TestBlueprintsInfo(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test1","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test2","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test2","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
@@ -271,7 +281,7 @@ func TestBlueprintsDiff(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
@@ -291,13 +301,28 @@ func TestBlueprintsDelete(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 		sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
 	}
 }
 
+func TestBlueprintsChanges(t *testing.T) {
+	api := weldr.New(repo, packages, nil, store.New(nil))
+	rand.Seed(time.Now().UnixNano())
+	id := strconv.Itoa(rand.Int())
+	ignoreFields := []string{"commit", "timestamp"}
+
+	sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/failing"+id, ``, http.StatusOK, `{"blueprints":[],"errors":[{"id":"UnknownBlueprint","msg":"failing`+id+`"}],"limit":20,"offset":0}`, ignoreFields...)
+	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":1}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
+	sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
+	sendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"`+id+`","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
+	testRoute(t, api, true, "GET", "/api/v0/blueprints/changes/"+id, ``, http.StatusOK, `{"blueprints":[{"changes":[{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""},{"commit":"","message":"Recipe `+id+`, version 0.0.0 saved.","revision":null,"timestamp":""}],"name":"`+id+`","total":2}],"errors":[],"limit":20,"offset":0}`, ignoreFields...)
+	sendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/"+id, ``)
+}
+
 func TestCompose(t *testing.T) {
 	var cases = []struct {
 		External       bool
@@ -313,7 +338,7 @@ func TestCompose(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, c.External, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		testRoute(t, api, c.External, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
 		sendHTTP(api, c.External, "DELETE", "/api/v0/blueprints/delete/test", ``)
@@ -337,8 +362,7 @@ func TestComposeQueue(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		s := store.New(nil)
-		api := weldr.New(repo, packages, nil, s)
+		api, s := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, false, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
 		// create job and leave it waiting
 		sendHTTP(api, false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`)
@@ -372,7 +396,7 @@ func TestSourcesNew(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 		sendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 	}
@@ -391,9 +415,27 @@ func TestSourcesDelete(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := weldr.New(repo, packages, nil, store.New(nil))
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
 		sendHTTP(api, true, "POST", "/api/v0/projects/source/new", `{"name": "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`)
 		testRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 		sendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
 	}
 }
+
+func TestProjectsDepsolve(t *testing.T) {
+	var cases = []struct {
+		Fixture        rpmmd_mock.Fixture
+		Path           string
+		ExpectedStatus int
+		ExpectedJSON   string
+	}{
+		{rpmmd_mock.NonExistingPackage, "/api/v0/projects/depsolve/fash", http.StatusBadRequest, `{"status":false,"errors":[{"id":"PROJECTS_ERROR","msg":"BadRequest: DNF error occured: MarkingErrors: Error occurred when marking packages for installation: Problems in request:\nmissing packages: fash"}]}`},
+		{rpmmd_mock.BaseFixture, "/api/v0/projects/depsolve/fish", http.StatusOK, `{"projects":[{"name":"libgpg-error","epoch":0,"version":"1.33","release":"2.fc30","arch":"x86_64"},{"name":"libsemanage","epoch":0,"version":"2.9","release":"1.fc30","arch":"x86_64"}]}`},
+		{rpmmd_mock.BadDepsolve, "/api/v0/projects/depsolve/go2rpm", http.StatusBadRequest, `{"status":false,"errors":[{"id":"PROJECTS_ERROR","msg":"BadRequest: DNF error occured: DepsolveError: There was a problem depsolving ['go2rpm']: \n Problem: conflicting requests\n  - nothing provides askalono-cli needed by go2rpm-1-4.fc31.noarch"}]}`},
+	}
+
+	for _, c := range cases {
+		api, _ := createWeldrAPI(c.Fixture)
+		testRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
+	}
+}
diff --git a/test/cases/ami_local_boot.json b/test/cases/ami_local_boot.json
new file mode 100644
index 000000000..67b41964e
--- /dev/null
+++ b/test/cases/ami_local_boot.json
@@ -0,0 +1,172 @@
+{
+  "boot-test": {
+    "output-format": "ami",
+    "filename": "image.ami",
+    "boot-type": "qemu",
+    "blueprint": {
+      "name": "ami-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "@Core",
+            "chrony",
+            "kernel",
+            "selinux-policy-targeted",
+            "grub2-pc",
+            "langpacks-en",
+            "libxcrypt-compat",
+            "xfsprogs",
+            "cloud-init",
+            "checkpolicy",
+            "net-tools"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.fstab",
+        "options": {
+          "filesystems": [
+            {
+              "uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+              "vfs_type": "ext4",
+              "path": "/",
+              "options": "defaults",
+              "freq": 1,
+              "passno": 1
+            }
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.qemu",
+      "options": {
+        "format": "qcow2",
+        "filename": "image.ami",
+        "ptuuid": "0x14fc63d2",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/cases/disk_local_boot.json b/test/cases/disk_local_boot.json
new file mode 100644
index 000000000..77f9a99f1
--- /dev/null
+++ b/test/cases/disk_local_boot.json
@@ -0,0 +1,173 @@
+{
+  "boot-test": {
+    "output-format": "partitioned-disk",
+    "filename": "disk.img",
+    "boot-type": "nspawn",
+    "blueprint": {
+      "name": "partitioned-disk-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "@core",
+            "chrony",
+            "firewalld",
+            "grub2-pc",
+            "kernel",
+            "langpacks-en",
+            "selinux-policy-targeted"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.fstab",
+        "options": {
+          "filesystems": [
+            {
+              "uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+              "vfs_type": "ext4",
+              "path": "/",
+              "options": "defaults",
+              "freq": 1,
+              "passno": 1
+            }
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.script",
+        "options": {
+          "script": "sed -i 's|#Port 22|Port 1022|' /etc/ssh/sshd_config; sed -i 's|#ListenAddress 0.0.0.0|ListenAddress 127.0.0.1|' /etc/ssh/sshd_config"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ],
+          "disabled_services": [
+            "auditd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.qemu",
+      "options": {
+        "format": "raw",
+        "filename": "disk.img",
+        "ptuuid": "0x14fc63d2",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
diff --git a/test/cases/ext4_local_boot.json b/test/cases/ext4_local_boot.json
new file mode 100644
index 000000000..348a8833c
--- /dev/null
+++ b/test/cases/ext4_local_boot.json
@@ -0,0 +1,156 @@
+{
+  "boot-test": {
+    "output-format": "ext4",
+    "filename": "filesystem.img",
+    "boot-type": "nspawn",
+    "blueprint": {
+      "name": "ext4-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "policycoreutils",
+            "selinux-policy-targeted",
+            "kernel",
+            "firewalld",
+            "chrony",
+            "langpacks-en",
+            "openssh-server"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.script",
+        "options": {
+          "script": "sed -i 's|#Port 22|Port 1022|' /etc/ssh/sshd_config; sed -i 's|#ListenAddress 0.0.0.0|ListenAddress 127.0.0.1|' /etc/ssh/sshd_config"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ],
+          "disabled_services": [
+            "auditd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.rawfs",
+      "options": {
+        "filename": "filesystem.img",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
diff --git a/test/cases/openstack_local_boot.json b/test/cases/openstack_local_boot.json
new file mode 100644
index 000000000..175d149e2
--- /dev/null
+++ b/test/cases/openstack_local_boot.json
@@ -0,0 +1,168 @@
+{
+  "boot-test": {
+    "output-format": "openstack",
+    "filename": "image.qcow2",
+    "boot-type": "qemu",
+    "blueprint": {
+      "name": "openstack-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "@Core",
+            "chrony",
+            "kernel",
+            "selinux-policy-targeted",
+            "grub2-pc",
+            "spice-vdagent",
+            "qemu-guest-agent",
+            "xen-libs",
+            "langpacks-en",
+            "cloud-init",
+            "libdrm"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.fstab",
+        "options": {
+          "filesystems": [
+            {
+              "uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+              "vfs_type": "ext4",
+              "path": "/",
+              "options": "defaults",
+              "freq": 1,
+              "passno": 1
+            }
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.qemu",
+      "options": {
+        "format": "qcow2",
+        "filename": "image.qcow2",
+        "ptuuid": "0x14fc63d2",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
diff --git a/test/cases/qcow2_local_boot.json b/test/cases/qcow2_local_boot.json
index bff7008ed..139191860 100644
--- a/test/cases/qcow2_local_boot.json
+++ b/test/cases/qcow2_local_boot.json
@@ -1,7 +1,8 @@
 {
-  "metadata": {
+  "boot-test": {
     "output-format": "qcow2",
     "filename": "image.qcow2",
+    "boot-type": "qemu",
     "blueprint": {
       "name": "qcow2-boot-test",
       "description": "Image for boot test",
diff --git a/test/cases/tar_local_boot.json b/test/cases/tar_local_boot.json
new file mode 100644
index 000000000..beb37b81c
--- /dev/null
+++ b/test/cases/tar_local_boot.json
@@ -0,0 +1,154 @@
+{
+  "boot-test": {
+    "output-format": "tar",
+    "filename": "root.tar.xz",
+    "boot-type": "nspawn-extract",
+    "blueprint": {
+      "name": "tar-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "policycoreutils",
+            "selinux-policy-targeted",
+            "kernel",
+            "firewalld",
+            "chrony",
+            "langpacks-en",
+            "openssh-server"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.script",
+        "options": {
+          "script": "sed -i 's|#Port 22|Port 1022|' /etc/ssh/sshd_config; sed -i 's|#ListenAddress 0.0.0.0|ListenAddress 127.0.0.1|' /etc/ssh/sshd_config"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ],
+          "disabled_services": [
+            "auditd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.tar",
+      "options": {
+        "filename": "root.tar.xz"
+      }
+    }
+  }
+}
diff --git a/test/cases/vhd_local_boot.json b/test/cases/vhd_local_boot.json
new file mode 100644
index 000000000..33dfce2f9
--- /dev/null
+++ b/test/cases/vhd_local_boot.json
@@ -0,0 +1,167 @@
+{
+  "boot-test": {
+    "output-format": "vhd",
+    "filename": "image.vhd",
+    "boot-type": "qemu",
+    "blueprint": {
+      "name": "vhd-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "@Core",
+            "chrony",
+            "kernel",
+            "selinux-policy-targeted",
+            "grub2-pc",
+            "langpacks-en",
+            "net-tools",
+            "ntfsprogs",
+            "WALinuxAgent",
+            "libxcrypt-compat"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.fstab",
+        "options": {
+          "filesystems": [
+            {
+              "uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+              "vfs_type": "ext4",
+              "path": "/",
+              "options": "defaults",
+              "freq": 1,
+              "passno": 1
+            }
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.qemu",
+      "options": {
+        "format": "qcow2",
+        "filename": "image.vhd",
+        "ptuuid": "0x14fc63d2",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/cases/vmdk_local_boot.json b/test/cases/vmdk_local_boot.json
new file mode 100644
index 000000000..cc31a7b25
--- /dev/null
+++ b/test/cases/vmdk_local_boot.json
@@ -0,0 +1,165 @@
+{
+  "boot-test": {
+    "output-format": "vmdk",
+    "filename": "disk.vmdk",
+    "boot-type": "qemu",
+    "blueprint": {
+      "name": "vmdk-boot-test",
+      "description": "Image for boot test",
+      "packages": [],
+      "modules": [],
+      "groups": [],
+      "customizations": {
+        "user": [
+          {
+            "name": "redhat",
+            "password": "$y$j9T$hMKTMCO/s27TcOMRH9eUN1$6G0Jrc20QiptW6P3fwm3LKDd/4IIVSr1N2u611UwNgB",
+            "groups": [
+              "wheel"
+            ]
+          }
+        ],
+        "sshkey": [
+          {
+            "user": "redhat",
+            "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+          }
+        ]
+      },
+      "services": {
+        "enabled": [
+          "sshd"
+        ]
+      }
+    }
+  },
+  "pipeline": {
+    "build": {
+      "stages": [
+        {
+          "name": "org.osbuild.dnf",
+          "options": {
+            "repos": [
+              {
+                "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+                "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+                "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+              }
+            ],
+            "packages": [
+              "dnf",
+              "e2fsprogs",
+              "policycoreutils",
+              "qemu-img",
+              "systemd",
+              "grub2-pc",
+              "tar"
+            ],
+            "releasever": "30",
+            "basearch": "x86_64"
+          }
+        }
+      ]
+    },
+    "stages": [
+      {
+        "name": "org.osbuild.dnf",
+        "options": {
+          "repos": [
+            {
+              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+              "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n",
+              "checksum": "sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"
+            }
+          ],
+          "packages": [
+            "@core",
+            "chrony",
+            "firewalld",
+            "grub2-pc",
+            "kernel",
+            "langpacks-en",
+            "open-vm-tools",
+            "selinux-policy-targeted"
+          ],
+          "exclude_packages": [
+            "dracut-config-rescue"
+          ],
+          "releasever": "30",
+          "basearch": "x86_64"
+        }
+      },
+      {
+        "name": "org.osbuild.locale",
+        "options": {
+          "language": "en_US"
+        }
+      },
+      {
+        "name": "org.osbuild.fstab",
+        "options": {
+          "filesystems": [
+            {
+              "uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+              "vfs_type": "ext4",
+              "path": "/",
+              "options": "defaults",
+              "freq": 1,
+              "passno": 1
+            }
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.grub2",
+        "options": {
+          "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+          "boot_fs_uuid": "00000000-0000-0000-0000-000000000000",
+          "kernel_opts": "ro biosdevname=0 net.ifnames=0"
+        }
+      },
+      {
+        "name": "org.osbuild.systemd",
+        "options": {
+          "enabled_services": [
+            "sshd"
+          ]
+        }
+      },
+      {
+        "name": "org.osbuild.users",
+        "options": {
+          "users": {
+            "redhat": {
+              "groups": [
+                "wheel"
+              ],
+              "password": "$6$IR7O7z56ouB/OInP$.hscD6dQqPQGwMuQ.idumixSHI/JEyaUfiCAHVSpGO/iNLEvvVZVOQL23zBzQbc2.yJ25xAZD75H0tXqKJpEE/",
+              "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC61wMCjOSHwbVb4VfVyl5sn497qW4PsdQ7Ty7aD6wDNZ/QjjULkDV/yW5WjDlDQ7UqFH0Sr7vywjqDizUAqK7zM5FsUKsUXWHWwg/ehKg8j9xKcMv11AkFoUoujtfAujnKODkk58XSA9whPr7qcw3vPrmog680pnMSzf9LC7J6kXfs6lkoKfBh9VnlxusCrw2yg0qI1fHAZBLPx7mW6+me71QZsS6sVz8v8KXyrXsKTdnF50FjzHcK9HXDBtSJS5wA3fkcRYymJe0o6WMWNdgSRVpoSiWaHHmFgdMUJaYoCfhXzyl7LtNb3Q+Sveg+tJK7JaRXBLMUllOlJ6ll5Hod root@localhost"
+            }
+          }
+        }
+      },
+      {
+        "name": "org.osbuild.selinux",
+        "options": {
+          "file_contexts": "etc/selinux/targeted/contexts/files/file_contexts"
+        }
+      },
+      {
+        "name": "org.osbuild.fix-bls",
+        "options": {}
+      }
+    ],
+    "assembler": {
+      "name": "org.osbuild.qemu",
+      "options": {
+        "format": "vmdk",
+        "filename": "disk.vmdk",
+        "ptuuid": "0x14fc63d2",
+        "root_fs_uuid": "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
+        "size": 3221225472
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/run b/test/run
index b4cdce7e4..2003d51f6 100755
--- a/test/run
+++ b/test/run
@@ -13,7 +13,7 @@ import shutil
 import urllib.request
 
 
-from typing import Dict, Any
+from typing import Dict, Any, Union, Tuple
 
 TEST_DIR = os.path.dirname(__file__)
 
@@ -73,6 +73,30 @@ def qemu_boot_image(image_file):
         vm.kill()
 
 
+@contextlib.contextmanager
+def nspawn_boot_container(image_file):
+    cmd = ["systemd-nspawn", "--boot", "-M", "boottest", "--image", image_file]
+    print(f"running nspawn command: {' '.join(cmd)}")
+    container = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    try:
+        yield None
+    finally:
+        container.kill()
+
+
+@contextlib.contextmanager
+def nspawn_extract_container(image_file):
+    with tempfile.TemporaryDirectory() as dir:
+        subprocess.run(["tar", "xf", image_file], cwd=dir)
+        cmd = ["systemd-nspawn", "--boot", "-M", "boottest", "--directory", "."]
+        print(f"running nspawn command: {' '.join(cmd)}")
+        container = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dir)
+        try:
+            yield None
+        finally:
+            container.kill()
+
+
 def run_osbuild(pipeline, store):
     osbuild_cmd = ["python3", "-m", "osbuild", "--json", "--libdir", ".", "--store", store, "-"]
 
@@ -92,6 +116,10 @@ def run_osbuild(pipeline, store):
 
 def run_test(case, store):
     try:
+        if "compose" not in case:
+            print("skipping this test case")
+            return True
+
         if "pipeline" in case:
             _, output_id = run_osbuild(case["pipeline"], store)
             filename = os.path.join(store, "refs", output_id, case["compose"]["filename"])
@@ -109,12 +137,15 @@ def run_test(case, store):
     return True
 
 
-def get_local_boot_test_case(image_type: str) -> (str, Dict[Any, Any]):
-    with open(f"test/cases/{image_type}_local_boot.json", "r") as fd:
+def get_local_boot_test_case(fname: str) -> Union[Tuple[str, str, Dict[Any, Any]], None]:
+    with open(fname, "r") as fd:
         test_case_dict = json.load(fd)
 
+    if "boot-test" not in test_case_dict:
+        return None
+
     pipeline_dict = test_case_dict["pipeline"]
-    return test_case_dict["metadata"]["filename"], pipeline_dict
+    return test_case_dict["boot-test"]["boot-type"], test_case_dict["boot-test"]["filename"], pipeline_dict
 
 
 def run_ssh_test(private_key):
@@ -126,12 +157,21 @@ def run_ssh_test(private_key):
            "systemctl is-system-running"]
     for _ in range(40):
         try:
-            sp = subprocess.run(cmd, timeout=120, check=True, stdout=subprocess.PIPE)
+            # Run the process with check=False because it returns non-zero return code for "degraded"
+            sp = subprocess.run(cmd, timeout=120, check=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+            if sp.returncode == 255:
+                raise subprocess.CalledProcessError(sp.returncode, cmd)
             output = sp.stdout.decode('utf-8').strip()
             print(output)
             if output == "running":
                 print("ssh test success")
                 return 0
+            elif output == "degraded":
+                print("ssh test passed, but the system is degraded")
+                return 0
+            else:
+                print(f"ssh test failed, system status is: {output}")
+                return 1
         except subprocess.TimeoutExpired:
             print("ssh timeout expired")
         except subprocess.CalledProcessError as e:
@@ -151,17 +191,37 @@ def main():
 
     # Run local boot test
     if arg.boot_test is not None:
+        failed = False
         with osbuild_test_store() as store:
             with create_ssh_keys() as keydir:
-                for test_case in arg.boot_test if arg.boot_test != [] else ["qcow2"]:
-                    image_fname, pl_dict = get_local_boot_test_case(test_case)
-                    print("starting osbuild")
-                    _, output_id = run_osbuild(pl_dict, store)
-                    print("osbuild success")
-                    with qemu_boot_image(f"{store}/refs/{output_id}/{image_fname}"):
-                        ret = run_ssh_test(f"{keydir}/id_rsa")
-
-        return ret
+                if arg.boot_test != []:
+                    test_cases = list(map(lambda x: f"{TEST_DIR}/cases/{x}_local_boot.json", arg.boot_test))
+                else:
+                    test_cases = glob.glob(f"{TEST_DIR}/cases/*.json")
+
+                for test_case in test_cases:
+                    test_case_tuple = get_local_boot_test_case(test_case)
+                    if test_case_tuple:
+                        test_type, image_fname, pl_dict = test_case_tuple
+                        print("starting osbuild")
+                        _, output_id = run_osbuild(pl_dict, store)
+                        print("osbuild success")
+                        if test_type == "qemu":
+                            with qemu_boot_image(f"{store}/refs/{output_id}/{image_fname}"):
+                                if run_ssh_test(f"{keydir}/id_rsa") == 1:
+                                    failed = True
+                        elif test_type == "nspawn":
+                            with nspawn_boot_container(f"{store}/refs/{output_id}/{image_fname}"):
+                                if run_ssh_test(f"{keydir}/id_rsa") == 1:
+                                    failed = True
+                        elif test_type == "nspawn-extract":
+                            with nspawn_extract_container(f"{store}/refs/{output_id}/{image_fname}"):
+                                if run_ssh_test(f"{keydir}/id_rsa") == 1:
+                                    failed = True
+                        else:
+                            print("unknown test type")
+
+        return 1 if failed else 0
 
     failed = False
     with osbuild_test_store() as store: