[lxc-devel] [distrobuilder/master] Support building Windows VM images

monstermunchkin on Github lxc-bot at linuxcontainers.org
Wed Oct 14 12:21:39 UTC 2020


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 310 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20201014/ef599c0c/attachment.bin>
-------------- next part --------------
From 5994aadde58bd4bc7a36f6ad72e811f03fd65bfb Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 13:24:47 +0200
Subject: [PATCH 01/10] sources: Add Windows source

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 sources/source.go  |  2 ++
 sources/windows.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+)
 create mode 100644 sources/windows.go

diff --git a/sources/source.go b/sources/source.go
index fce32c5..cf33b34 100644
--- a/sources/source.go
+++ b/sources/source.go
@@ -44,6 +44,8 @@ func Get(name string) Downloader {
 		return NewVoidLinuxHTTP()
 	case "funtoo-http":
 		return NewFuntooHTTP()
+	case "windows":
+		return NewWindows()
 	}
 
 	return nil
diff --git a/sources/windows.go b/sources/windows.go
new file mode 100644
index 0000000..b2c619d
--- /dev/null
+++ b/sources/windows.go
@@ -0,0 +1,86 @@
+package sources
+
+import (
+	"bytes"
+	"fmt"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	lxd "github.com/lxc/lxd/shared"
+	"github.com/pkg/errors"
+
+	"github.com/lxc/distrobuilder/shared"
+)
+
+// Windows represents the Windows OS
+type Windows struct{}
+
+// NewWindows creates a new Windows instance.
+func NewWindows() *Windows {
+	return &Windows{}
+}
+
+// Run unpacks a Windows VHD file.
+func (s *Windows) Run(definition shared.Definition, rootfsDir string) error {
+	// URL
+	u, err := url.Parse(definition.Source.URL)
+	if err != nil {
+		return errors.Wrap(err, "Failed to parse URL")
+	}
+
+	if u.Scheme != "file" {
+		return fmt.Errorf("Scheme %q is not supported", u.Scheme)
+	}
+
+	rawFilePath := fmt.Sprintf("%s.raw", u.Path)
+
+	if !lxd.PathExists(rawFilePath) {
+		// Convert the vhd image to raw.
+		err = shared.RunCommand("qemu-img", "convert", "-O", "raw", u.Path, rawFilePath)
+		if err != nil {
+			return errors.Wrap(err, "Failed to convert image")
+		}
+	}
+
+	// Figure out the offset
+	var buf bytes.Buffer
+
+	err = lxd.RunCommandWithFds(nil, &buf, "fdisk", "-l", "-o", "Start", rawFilePath)
+	if err != nil {
+		return errors.Wrap(err, "Failed to list partitions")
+	}
+
+	output := strings.Split(buf.String(), "\n")
+	offsetStr := strings.TrimSpace(output[len(output)-2])
+
+	offset, err := strconv.Atoi(offsetStr)
+	if err != nil {
+		return errors.Wrap(err, "Failed to read offset")
+	}
+
+	roRootDir := filepath.Join(os.TempDir(), "distrobuilder", "rootfs.ro")
+
+	err = os.MkdirAll(roRootDir, 0755)
+	if err != nil {
+		return errors.Wrap(err, "Failed to create directory")
+	}
+	defer os.RemoveAll(filepath.Join(os.TempDir(), "distrobuilder"))
+
+	// Mount the partition read-only since we don't want to accidently modify it.
+	err = shared.RunCommand("mount", "-o", fmt.Sprintf("ro,loop,offset=%d", offset*512),
+		rawFilePath, roRootDir)
+	if err != nil {
+		return errors.Wrap(err, "Failed to mount partition read-only")
+	}
+
+	// Copy the read-only rootfs to the real rootfs.
+	err = shared.RunCommand("rsync", "-qa", roRootDir+"/", rootfsDir)
+	if err != nil {
+		return errors.Wrap(err, "Failed to copy rootfs")
+	}
+
+	return nil
+}

From 4e082ef4b1505926441647065c667acd629c7b0e Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 13:25:36 +0200
Subject: [PATCH 02/10] shared/definition: Accept Windows as a source

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 shared/definition.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/shared/definition.go b/shared/definition.go
index 5512cfe..1989795 100644
--- a/shared/definition.go
+++ b/shared/definition.go
@@ -336,6 +336,7 @@ func (d *Definition) Validate() error {
 		"plamolinux-http",
 		"voidlinux-http",
 		"funtoo-http",
+		"windows",
 	}
 	if !shared.StringInSlice(strings.TrimSpace(d.Source.Downloader), validDownloaders) {
 		return fmt.Errorf("source.downloader must be one of %v", validDownloaders)

From 38feac9ce9829761840fd1004295b391bed7b8d7 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 13:34:59 +0200
Subject: [PATCH 03/10] shared/definition: Ignore package manager when using
 Windows

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 shared/definition.go | 80 +++++++++++++++++++++++---------------------
 1 file changed, 41 insertions(+), 39 deletions(-)

diff --git a/shared/definition.go b/shared/definition.go
index 1989795..dccdde7 100644
--- a/shared/definition.go
+++ b/shared/definition.go
@@ -342,51 +342,53 @@ func (d *Definition) Validate() error {
 		return fmt.Errorf("source.downloader must be one of %v", validDownloaders)
 	}
 
-	if d.Packages.Manager != "" {
-		validManagers := []string{
-			"apk",
-			"apt",
-			"dnf",
-			"egoportage",
-			"opkg",
-			"pacman",
-			"portage",
-			"yum",
-			"equo",
-			"xbps",
-			"zypper",
-			"luet",
-		}
-		if !shared.StringInSlice(strings.TrimSpace(d.Packages.Manager), validManagers) {
-			return fmt.Errorf("packages.manager must be one of %v", validManagers)
-		}
+	if d.Source.Downloader != "windows" {
+		if d.Packages.Manager != "" {
+			validManagers := []string{
+				"apk",
+				"apt",
+				"dnf",
+				"egoportage",
+				"opkg",
+				"pacman",
+				"portage",
+				"yum",
+				"equo",
+				"xbps",
+				"zypper",
+				"luet",
+			}
+			if !shared.StringInSlice(strings.TrimSpace(d.Packages.Manager), validManagers) {
+				return fmt.Errorf("packages.manager must be one of %v", validManagers)
+			}
 
-		if d.Packages.CustomManager != nil {
-			return fmt.Errorf("cannot have both packages.manager and packages.custom-manager set")
-		}
-	} else {
-		if d.Packages.CustomManager == nil {
-			return fmt.Errorf("packages.manager or packages.custom-manager needs to be set")
-		}
+			if d.Packages.CustomManager != nil {
+				return fmt.Errorf("cannot have both packages.manager and packages.custom-manager set")
+			}
+		} else {
+			if d.Packages.CustomManager == nil {
+				return fmt.Errorf("packages.manager or packages.custom-manager needs to be set")
+			}
 
-		if d.Packages.CustomManager.Clean.Command == "" {
-			return fmt.Errorf("packages.custom-manager requires a clean command")
-		}
+			if d.Packages.CustomManager.Clean.Command == "" {
+				return fmt.Errorf("packages.custom-manager requires a clean command")
+			}
 
-		if d.Packages.CustomManager.Install.Command == "" {
-			return fmt.Errorf("packages.custom-manager requires an install command")
-		}
+			if d.Packages.CustomManager.Install.Command == "" {
+				return fmt.Errorf("packages.custom-manager requires an install command")
+			}
 
-		if d.Packages.CustomManager.Remove.Command == "" {
-			return fmt.Errorf("packages.custom-manager requires a remove command")
-		}
+			if d.Packages.CustomManager.Remove.Command == "" {
+				return fmt.Errorf("packages.custom-manager requires a remove command")
+			}
 
-		if d.Packages.CustomManager.Refresh.Command == "" {
-			return fmt.Errorf("packages.custom-manager requires a refresh command")
-		}
+			if d.Packages.CustomManager.Refresh.Command == "" {
+				return fmt.Errorf("packages.custom-manager requires a refresh command")
+			}
 
-		if d.Packages.CustomManager.Update.Command == "" {
-			return fmt.Errorf("packages.custom-manager requires an update command")
+			if d.Packages.CustomManager.Update.Command == "" {
+				return fmt.Errorf("packages.custom-manager requires an update command")
+			}
 		}
 	}
 

From 79691c4b107572f93982441f67e0c41c9f0fd9f0 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 13:48:17 +0200
Subject: [PATCH 04/10] distrobuilder: Rename VM functions

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/main_lxd.go | 17 +++------
 distrobuilder/vm.go       | 80 +++++++++++++++++++--------------------
 2 files changed, 46 insertions(+), 51 deletions(-)

diff --git a/distrobuilder/main_lxd.go b/distrobuilder/main_lxd.go
index 2446ee6..2e1d67f 100644
--- a/distrobuilder/main_lxd.go
+++ b/distrobuilder/main_lxd.go
@@ -239,25 +239,20 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 		}
 		defer vm.umountImage()
 
-		err = vm.createRootFS()
+		err = vm.createFilesystems()
 		if err != nil {
-			return errors.Wrap(err, "Failed to create root filesystem")
+			return errors.Wrap(err, "Failed to create filesystems")
 		}
 
-		err = vm.mountRootPartition()
+		err = vm.mountRootFilesystem()
 		if err != nil {
-			return errors.Wrap(err, "failed to mount root partion")
+			return errors.Wrap(err, "Failed to mount root filesystem")
 		}
 		defer lxd.RunCommand("umount", "-R", vmDir)
 
-		err = vm.createUEFIFS()
+		err = vm.mountUEFIFilesystem()
 		if err != nil {
-			return errors.Wrap(err, "Failed to create UEFI filesystem")
-		}
-
-		err = vm.mountUEFIPartition()
-		if err != nil {
-			return errors.Wrap(err, "Failed to mount UEFI partition")
+			return errors.Wrap(err, "Failed to mount UEFI filesystem")
 		}
 
 		// We cannot use LXD's rsync package as that uses the --delete flag which
diff --git a/distrobuilder/vm.go b/distrobuilder/vm.go
index f6ba300..7939cfa 100644
--- a/distrobuilder/vm.go
+++ b/distrobuilder/vm.go
@@ -194,41 +194,6 @@ func (v *vm) umountImage() error {
 	return nil
 }
 
-func (v *vm) createRootFS() error {
-	if v.loopDevice == "" {
-		return fmt.Errorf("Disk image not mounted")
-	}
-
-	switch v.rootFS {
-	case "btrfs":
-		err := shared.RunCommand("mkfs.btrfs", "-f", "-L", "rootfs", v.getRootfsDevFile())
-		if err != nil {
-			return err
-		}
-
-		// Create the root subvolume as well
-		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir)
-		if err != nil {
-			return err
-		}
-		defer shared.RunCommand("umount", v.rootfsDir)
-
-		return shared.RunCommand("btrfs", "subvolume", "create", fmt.Sprintf("%s/@", v.rootfsDir))
-	case "ext4":
-		return shared.RunCommand("mkfs.ext4", "-F", "-b", "4096", "-i 8192", "-m", "0", "-L", "rootfs", "-E", "resize=536870912", v.getRootfsDevFile())
-	}
-
-	return nil
-}
-
-func (v *vm) createUEFIFS() error {
-	if v.loopDevice == "" {
-		return fmt.Errorf("Disk image not mounted")
-	}
-
-	return shared.RunCommand("mkfs.vfat", "-F", "32", "-n", "UEFI", v.getUEFIDevFile())
-}
-
 func (v *vm) getRootfsPartitionUUID() (string, error) {
 	if v.loopDevice == "" {
 		return "", fmt.Errorf("Disk image not mounted")
@@ -255,23 +220,58 @@ func (v *vm) getUEFIPartitionUUID() (string, error) {
 	return strings.TrimSpace(stdout), nil
 }
 
-func (v *vm) mountRootPartition() error {
+func (v *vm) createFilesystems() error {
 	if v.loopDevice == "" {
 		return fmt.Errorf("Disk image not mounted")
 	}
 
+	var err error
+
+	// Create root filesystem
 	switch v.rootFS {
 	case "btrfs":
-		return shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir, "-o", "defaults,subvol=/@")
+		err := shared.RunCommand("mkfs.btrfs", "-f", "-L", "rootfs", v.getRootfsDevFile())
+		if err != nil {
+			return err
+		}
+
+		// Create the root subvolume as well
+		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir)
+		if err != nil {
+			return err
+		}
+		defer shared.RunCommand("umount", v.rootfsDir)
+
+		err = shared.RunCommand("btrfs", "subvolume", "create", fmt.Sprintf("%s/@", v.rootfsDir))
 	case "ext4":
-		return shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir)
+		err = shared.RunCommand("mkfs.ext4", "-F", "-b", "4096", "-i 8192", "-m", "0", "-L", "rootfs", "-E", "resize=536870912", v.getRootfsDevFile())
+	}
+	if err != nil {
+		return err
+	}
+
+	// Create UEFI filesystem
+	return shared.RunCommand("mkfs.vfat", "-F", "32", "-n", "UEFI", v.getUEFIDevFile())
+}
 
+func (v *vm) mountRootFilesystem() error {
+	if v.loopDevice == "" {
+		return fmt.Errorf("Disk image not mounted")
 	}
 
-	return nil
+	var err error
+
+	switch v.rootFS {
+	case "btrfs":
+		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir, "-o", "defaults,subvol=/@")
+	case "ext4":
+		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir)
+	}
+
+	return err
 }
 
-func (v *vm) mountUEFIPartition() error {
+func (v *vm) mountUEFIFilesystem() error {
 	if v.loopDevice == "" {
 		return fmt.Errorf("Disk image not mounted")
 	}

From fd261a161341397895e96e4b6025d8e81b35c40f Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 13:55:09 +0200
Subject: [PATCH 05/10] distrobuilder/vm: Add OS type

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/vm.go | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/distrobuilder/vm.go b/distrobuilder/vm.go
index 7939cfa..18b666d 100644
--- a/distrobuilder/vm.go
+++ b/distrobuilder/vm.go
@@ -14,6 +14,16 @@ import (
 	"github.com/lxc/distrobuilder/shared"
 )
 
+// OS represents the operating system.
+type OS int
+
+const (
+	// OSLinux represents the Linux operating system.
+	OSLinux OS = iota
+	// OSWindows represents the Windows operating system.
+	OSWindows
+)
+
 type vm struct {
 	imageFile  string
 	loopDevice string

From 4baf9d48a12cb0bcb4464899d5f631a4283e148e Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 14:04:21 +0200
Subject: [PATCH 06/10] distrobuilder: Add OS arg to newVM

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/main_lxd.go |  9 ++++++++-
 distrobuilder/vm.go       | 21 +++++++++++++++++----
 2 files changed, 25 insertions(+), 5 deletions(-)

diff --git a/distrobuilder/main_lxd.go b/distrobuilder/main_lxd.go
index 2e1d67f..e4e9313 100644
--- a/distrobuilder/main_lxd.go
+++ b/distrobuilder/main_lxd.go
@@ -202,6 +202,13 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 	var mounts []shared.ChrootMount
 	var vmDir string
 	var vm *vm
+	var targetOS OS
+
+	if c.global.definition.Source.Downloader == "windows" {
+		targetOS = OSWindows
+	} else {
+		targetOS = OSLinux
+	}
 
 	if c.flagVM {
 		vmDir = filepath.Join(c.global.flagCacheDir, "vm")
@@ -218,7 +225,7 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 
 		imgFile := filepath.Join(c.global.flagCacheDir, imgFilename)
 
-		vm, err = newVM(imgFile, vmDir, c.global.definition.Targets.LXD.VM.Filesystem, c.global.definition.Targets.LXD.VM.Size)
+		vm, err = newVM(imgFile, vmDir, c.global.definition.Targets.LXD.VM.Filesystem, c.global.definition.Targets.LXD.VM.Size, targetOS)
 		if err != nil {
 			return errors.Wrap(err, "Failed to instanciate VM")
 		}
diff --git a/distrobuilder/vm.go b/distrobuilder/vm.go
index 18b666d..b601238 100644
--- a/distrobuilder/vm.go
+++ b/distrobuilder/vm.go
@@ -30,14 +30,27 @@ type vm struct {
 	rootFS     string
 	rootfsDir  string
 	size       uint64
+	os         OS
 }
 
-func newVM(imageFile, rootfsDir, fs string, size uint64) (*vm, error) {
+func newVM(imageFile, rootfsDir, fs string, size uint64, os OS) (*vm, error) {
 	if fs == "" {
-		fs = "ext4"
+		if os == OSLinux {
+			fs = "ext4"
+		} else {
+			fs = "ntfs"
+		}
+	}
+
+	var supportedFs []string
+
+	if os == OSLinux {
+		supportedFs = []string{"btrfs", "ext4"}
+	} else {
+		supportedFs = []string{"ntfs"}
 	}
 
-	if !lxd.StringInSlice(fs, []string{"btrfs", "ext4"}) {
+	if !lxd.StringInSlice(fs, supportedFs) {
 		return nil, fmt.Errorf("Unsupported fs: %s", fs)
 	}
 
@@ -45,7 +58,7 @@ func newVM(imageFile, rootfsDir, fs string, size uint64) (*vm, error) {
 		size = 4294967296
 	}
 
-	return &vm{imageFile: imageFile, rootfsDir: rootfsDir, rootFS: fs, size: size}, nil
+	return &vm{imageFile: imageFile, rootfsDir: rootfsDir, rootFS: fs, size: size, os: os}, nil
 }
 
 func (v *vm) getLoopDev() string {

From b19477349558a389191de4925dfd4b4d837d217e Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 14:09:17 +0200
Subject: [PATCH 07/10] distrobuilder/vm: Add function getDiskUUID

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/vm.go | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/distrobuilder/vm.go b/distrobuilder/vm.go
index b601238..15049c6 100644
--- a/distrobuilder/vm.go
+++ b/distrobuilder/vm.go
@@ -308,3 +308,16 @@ func (v *vm) mountUEFIFilesystem() error {
 
 	return shared.RunCommand("mount", v.getUEFIDevFile(), mountpoint)
 }
+
+func (v *vm) getDiskUUID() (string, error) {
+	if v.loopDevice == "" {
+		return "", fmt.Errorf("Disk image not mounted")
+	}
+
+	stdout, err := lxd.RunCommand("blkid", "-s", "PTUUID", "-o", "value", v.loopDevice)
+	if err != nil {
+		return "", err
+	}
+
+	return strings.TrimSpace(stdout), nil
+}

From a6b2724fa74e84274b3c125507c9852b69f717ae Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 14:09:48 +0200
Subject: [PATCH 08/10] distrobuilder/chroot: Add function replaceUUID

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/chroot.go | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/distrobuilder/chroot.go b/distrobuilder/chroot.go
index 242f87e..90df838 100644
--- a/distrobuilder/chroot.go
+++ b/distrobuilder/chroot.go
@@ -1,11 +1,14 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
 
+	"github.com/google/uuid"
 	"github.com/pkg/errors"
 	"golang.org/x/sys/unix"
 
@@ -199,3 +202,26 @@ func getOverlay(cacheDir, sourceDir string) (func(), string, error) {
 
 	return cleanup, overlayDir, nil
 }
+
+func replaceUUID(path string, old uuid.UUID, new uuid.UUID) error {
+	f := func(u uuid.UUID) []byte {
+		b, err := u.MarshalBinary()
+		if err != nil {
+			return nil
+		}
+
+		out := []byte{b[3], b[2], b[1], b[0], b[5], b[4], b[7], b[6]}
+		out = append(out, b[8:]...)
+
+		return out
+	}
+
+	read, err := ioutil.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	write := bytes.ReplaceAll(read, f(old), f(new))
+
+	return ioutil.WriteFile(path, write, 0644)
+}

From 395c359a7393553629beeb62b20a88c29d2a16ce Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 14:13:00 +0200
Subject: [PATCH 09/10] shared/definition: Add VM specific targets

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 shared/definition.go | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/shared/definition.go b/shared/definition.go
index dccdde7..f83425c 100644
--- a/shared/definition.go
+++ b/shared/definition.go
@@ -153,10 +153,20 @@ type DefinitionTargetLXC struct {
 	Config        []DefinitionTargetLXCConfig `yaml:"config,omitempty"`
 }
 
+// DefinitionTargetLXDVMUUID represents the old partition UUIDs which are to be replaced by the newly generated ones.
+type DefinitionTargetLXDVMUUID struct {
+	EFI  string `yaml:"efi,omitempty"`
+	Data string `yaml:"data,omitempty"`
+	Disk string `yaml:"disk,omitempty"`
+}
+
 // DefinitionTargetLXDVM represents LXD VM specific options.
 type DefinitionTargetLXDVM struct {
-	Size       uint64 `yaml:"size,omitempty"`
-	Filesystem string `yaml:"filesystem,omitempty"`
+	Size       uint64                    `yaml:"size,omitempty"`
+	Filesystem string                    `yaml:"filesystem,omitempty"`
+	BCD        string                    `yaml:"bcd,omitempty"`
+	MSR        string                    `yaml:"msr,omitempty"`
+	UUID       DefinitionTargetLXDVMUUID `yaml:"uuid,omitempty"`
 }
 
 // DefinitionTargetLXD represents LXD specific options.

From d832a657e564d3aa66d8351b9fa6b5c154faab74 Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.hipp at canonical.com>
Date: Wed, 14 Oct 2020 14:17:37 +0200
Subject: [PATCH 10/10] distrobuilder: Support building Windows VM images

Signed-off-by: Thomas Hipp <thomas.hipp at canonical.com>
---
 distrobuilder/main.go     |  27 ++++++++
 distrobuilder/main_lxd.go | 140 +++++++++++++++++++++++++++++++++-----
 distrobuilder/vm.go       |  70 +++++++++++++++----
 3 files changed, 205 insertions(+), 32 deletions(-)

diff --git a/distrobuilder/main.go b/distrobuilder/main.go
index bc6ac88..b9f78ad 100644
--- a/distrobuilder/main.go
+++ b/distrobuilder/main.go
@@ -223,6 +223,28 @@ func (c *cmdGlobal) preRunBuild(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	// Sanity checks for Windows VMs.
+	if c.definition.Source.Downloader == "windows" {
+		subcommand := cmd.CalledAs()
+
+		if strings.Contains(subcommand, "lxc") {
+			return fmt.Errorf("Windows is not supported by LXC")
+		}
+
+		if subcommand == "build-lxd" || subcommand == "pack-lxd" {
+			ok, err := cmd.Flags().GetBool("vm")
+			if err != nil {
+				return err
+			}
+
+			if !ok {
+				return fmt.Errorf("LXD only supports Windows VMs")
+			}
+
+			c.definition.Targets.Type = "vm"
+		}
+	}
+
 	// Create cache directory if we also plan on creating LXC or LXD images
 	if !isRunningBuildDir {
 		err = os.MkdirAll(c.flagCacheDir, 0755)
@@ -251,6 +273,11 @@ func (c *cmdGlobal) preRunBuild(cmd *cobra.Command, args []string) error {
 		return errors.Wrap(err, "Error while downloading source")
 	}
 
+	// Return here as we cannot use chroot for Windows.
+	if c.definition.Source.Downloader == "windows" {
+		return nil
+	}
+
 	// Setup the mounts and chroot into the rootfs
 	exitChroot, err := shared.SetupChroot(c.sourceDir, c.definition.Environment, nil)
 	if err != nil {
diff --git a/distrobuilder/main_lxd.go b/distrobuilder/main_lxd.go
index e4e9313..b2a8cb5 100644
--- a/distrobuilder/main_lxd.go
+++ b/distrobuilder/main_lxd.go
@@ -2,10 +2,12 @@ package main
 
 import (
 	"fmt"
+	"net/url"
 	"os"
 	"os/exec"
 	"path/filepath"
 
+	"github.com/google/uuid"
 	lxd "github.com/lxc/lxd/shared"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -113,6 +115,11 @@ func (c *cmdLXD) commandPack() *cobra.Command {
 }
 
 func (c *cmdLXD) runPack(cmd *cobra.Command, args []string, overlayDir string) error {
+	// Return here as we cannot use chroot in Windows.
+	if c.global.definition.Source.Downloader == "windows" {
+		return nil
+	}
+
 	// Setup the mounts and chroot into the rootfs
 	exitChroot, err := shared.SetupChroot(overlayDir, c.global.definition.Environment, nil)
 	if err != nil {
@@ -255,13 +262,110 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 		if err != nil {
 			return errors.Wrap(err, "Failed to mount root filesystem")
 		}
-		defer lxd.RunCommand("umount", "-R", vmDir)
+		defer shared.RunCommand("umount", "-R", vmDir)
 
 		err = vm.mountUEFIFilesystem()
 		if err != nil {
 			return errors.Wrap(err, "Failed to mount UEFI filesystem")
 		}
 
+		// Copy EFI files and BCD to EFI partition, and populate MSR partition.
+		if targetOS == OSWindows {
+			// This is just a temporary mountpoint which will be removed later.
+			targetEFIDir := filepath.Join(vmDir, "boot", "efi")
+			targetEFIMicrosoftBootDir := filepath.Join(targetEFIDir, "EFI", "Microsoft", "Boot")
+			targetEFIBootDir := filepath.Join(targetEFIDir, "EFI", "Boot")
+
+			err = os.MkdirAll(targetEFIMicrosoftBootDir, 0755)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to create %s", targetEFIMicrosoftBootDir)
+			}
+
+			err = os.MkdirAll(targetEFIBootDir, 0755)
+			if err != nil {
+				return errors.Wrapf(err, "Failed to create %s", targetEFIBootDir)
+			}
+
+			// Copy EFI directory to boot partition
+			err = shared.RunCommand("rsync", "-a", "-HA", "--sparse", "--devices", "--checksum", "--numeric-ids", filepath.Join(overlayDir, "Windows", "Boot", "EFI")+"/", targetEFIMicrosoftBootDir)
+			if err != nil {
+				return errors.Wrap(err, "Failed to copy EFI data")
+			}
+
+			// Copy bootx64.efi file
+			err = shared.RunCommand("rsync", "-a", "-HA", "--sparse", "--devices", "--checksum", "--numeric-ids", filepath.Join(targetEFIMicrosoftBootDir, "bootmgfw.efi"), filepath.Join(targetEFIBootDir, "bootx64.efi"))
+			if err != nil {
+				return errors.Wrap(err, "Failed to copy bootx64.efi")
+			}
+
+			// Copy BCD file
+			bcdFile, err := url.Parse(c.global.definition.Targets.LXD.VM.BCD)
+			if err != nil {
+				return errors.Wrap(err, "Failed to get BCD file")
+			}
+
+			if bcdFile.Scheme == "file" {
+				err = shared.RunCommand("rsync", "-a", "-HA", "--sparse", "--devices", "--checksum", "--numeric-ids", bcdFile.Path, filepath.Join(targetEFIMicrosoftBootDir, "BCD"))
+				if err != nil {
+					return errors.Wrap(err, "Failed to copy BCD file")
+				}
+			}
+
+			// Fix UUIDs in the BCD file
+			efiPartUUID, err := vm.getUEFIPartitionUUID()
+			if err != nil {
+				return errors.Wrap(err, "Failed to get part UUID of EFI partition")
+			}
+
+			dataPartUUID, err := vm.getRootfsPartitionUUID()
+			if err != nil {
+				return errors.Wrap(err, "Failed to get part UUID of rootfs partition")
+			}
+
+			diskUUID, err := vm.getDiskUUID()
+			if err != nil {
+				return errors.Wrap(err, "Failed to get disk UUID")
+			}
+
+			err = replaceUUID(filepath.Join(targetEFIMicrosoftBootDir, "BCD"), uuid.MustParse(c.global.definition.Targets.LXD.VM.UUID.EFI), uuid.MustParse(efiPartUUID))
+			if err != nil {
+				return errors.Wrap(err, "Failed to replace EFI part UUID")
+			}
+
+			err = replaceUUID(filepath.Join(targetEFIMicrosoftBootDir, "BCD"), uuid.MustParse(c.global.definition.Targets.LXD.VM.UUID.Data), uuid.MustParse(dataPartUUID))
+			if err != nil {
+				return errors.Wrap(err, "Failed to replace rootfs part UUID")
+			}
+
+			err = replaceUUID(filepath.Join(targetEFIMicrosoftBootDir, "BCD"), uuid.MustParse(c.global.definition.Targets.LXD.VM.UUID.Disk), uuid.MustParse(diskUUID))
+			if err != nil {
+				return errors.Wrap(err, "Failed to replace rootfs part UUID")
+			}
+
+			err = shared.RunCommand("umount", "-R", targetEFIDir)
+			if err != nil {
+				return errors.Wrap(err, "Failed to unmount UEFI filesystem")
+			}
+
+			err = os.Remove(targetEFIDir)
+			if err != nil {
+				return errors.Wrap(err, "Failed to remove EFI mountpoint")
+			}
+
+			// Copy MSR (Microsoft Reserved Partition)
+			msrFile, err := url.Parse(c.global.definition.Targets.LXD.VM.MSR)
+			if err != nil {
+				return errors.Wrap(err, "Failed to get MSR file")
+			}
+
+			if msrFile.Scheme == "file" {
+				err = shared.RunCommand("dd", fmt.Sprintf("if=%s", msrFile.Path), fmt.Sprintf("of=%s", vm.getMSRDevFile()))
+				if err != nil {
+					return errors.Wrap(err, "Failed to copy MSR")
+				}
+			}
+		}
+
 		// We cannot use LXD's rsync package as that uses the --delete flag which
 		// causes an issue due to the boot/efi directory being present.
 		err = shared.RunCommand("rsync", "-a", "-HA", "--sparse", "--devices", "--checksum", "--numeric-ids", overlayDir+"/", vmDir)
@@ -298,26 +402,28 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 		}
 	}
 
-	exitChroot, err := shared.SetupChroot(rootfsDir,
-		c.global.definition.Environment, mounts)
-	if err != nil {
-		return errors.Wrap(err, "Failed to chroot")
-	}
-
-	// Run post files hook
-	for _, action := range c.global.definition.GetRunnableActions("post-files", imageTargets) {
-		err := shared.RunScript(action.Action)
+	if targetOS == OSLinux {
+		exitChroot, err := shared.SetupChroot(rootfsDir,
+			c.global.definition.Environment, mounts)
 		if err != nil {
-			exitChroot()
-			return errors.Wrap(err, "Failed to run post-files")
+			return errors.Wrap(err, "Failed to chroot")
+		}
+
+		// Run post files hook
+		for _, action := range c.global.definition.GetRunnableActions("post-files", imageTargets) {
+			err := shared.RunScript(action.Action)
+			if err != nil {
+				exitChroot()
+				return errors.Wrap(err, "Failed to run post-files")
+			}
 		}
-	}
 
-	exitChroot()
+		exitChroot()
+	}
 
 	// Unmount VM directory and loop device before creating the image.
 	if c.flagVM {
-		_, err := lxd.RunCommand("umount", "-R", vmDir)
+		err := shared.RunCommand("umount", "-R", vmDir)
 		if err != nil {
 			return err
 		}
@@ -328,7 +434,7 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 		}
 	}
 
-	err = img.Build(c.flagType == "unified", c.flagCompression, c.flagVM)
+	err := img.Build(c.flagType == "unified", c.flagCompression, c.flagVM)
 	if err != nil {
 		return errors.Wrap(err, "Failed to create LXD image")
 	}
@@ -337,7 +443,7 @@ func (c *cmdLXD) run(cmd *cobra.Command, args []string, overlayDir string) error
 }
 
 func (c *cmdLXD) checkVMDependencies() error {
-	dependencies := []string{"btrfs", "mkfs.ext4", "mkfs.vfat", "qemu-img", "rsync", "sgdisk"}
+	dependencies := []string{"btrfs", "mkfs.ext4", "mkfs.ntfs", "mkfs.vfat", "qemu-img", "rsync", "sgdisk"}
 
 	for _, dep := range dependencies {
 		_, err := exec.LookPath(dep)
diff --git a/distrobuilder/vm.go b/distrobuilder/vm.go
index 15049c6..827c8f4 100644
--- a/distrobuilder/vm.go
+++ b/distrobuilder/vm.go
@@ -70,7 +70,15 @@ func (v *vm) getRootfsDevFile() string {
 		return ""
 	}
 
-	return fmt.Sprintf("%sp2", v.loopDevice)
+	var partNum int
+
+	if v.os == OSLinux {
+		partNum = 2
+	} else {
+		partNum = 3
+	}
+
+	return fmt.Sprintf("%sp%d", v.loopDevice, partNum)
 }
 
 func (v *vm) getUEFIDevFile() string {
@@ -81,6 +89,18 @@ func (v *vm) getUEFIDevFile() string {
 	return fmt.Sprintf("%sp1", v.loopDevice)
 }
 
+func (v *vm) getMSRDevFile() string {
+	if v.loopDevice == "" {
+		return ""
+	}
+
+	if v.os != OSWindows {
+		return ""
+	}
+
+	return fmt.Sprintf("%sp2", v.loopDevice)
+}
+
 func (v *vm) createEmptyDiskImage() error {
 	f, err := os.Create(v.imageFile)
 	if err != nil {
@@ -104,8 +124,20 @@ func (v *vm) createEmptyDiskImage() error {
 func (v *vm) createPartitions() error {
 	args := [][]string{
 		{"--zap-all"},
-		{"--new=1::+100M", "-t 1:EF00"},
-		{"--new=2::", "-t 2:8300"},
+		// EFI system partition (ESP)
+		{"--new=1::+100M", "-t 1:C12A7328-F81F-11D2-BA4B-00A0C93EC93B"},
+	}
+
+	if v.os == OSLinux {
+		// Linux partition
+		args = append(args, []string{"--new=2::", "-t 2:0FC63DAF-8483-4772-8E79-3D69D8477DE4"})
+	} else {
+		args = append(args,
+			// Microsoft Reserved Partition (MSR)
+			[]string{"--new=2::+128M", "-t 2:E3C9E316-0B5C-4DB8-817D-F92DF00215AE"},
+			/// Microsoft basic data partition
+			[]string{"--new=3::", "-t 3:EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"},
+		)
 	}
 
 	for _, cmd := range args {
@@ -163,7 +195,15 @@ func (v *vm) mountImage() error {
 	}
 
 	if !lxd.PathExists(v.getRootfsDevFile()) {
-		fields := strings.Split(deviceNumbers[2], ":")
+		var idx int
+
+		if v.os == OSLinux {
+			idx = 2
+		} else {
+			idx = 3
+		}
+
+		fields := strings.Split(deviceNumbers[idx], ":")
 
 		major, err := strconv.Atoi(fields[0])
 		if err != nil {
@@ -197,18 +237,15 @@ func (v *vm) umountImage() error {
 		return err
 	}
 
-	// Make sure that p1 and p2 are also removed.
-	if lxd.PathExists(v.getUEFIDevFile()) {
-		err := os.Remove(v.getUEFIDevFile())
-		if err != nil {
-			return err
-		}
-	}
+	// Make sure that all partitions are removed.
+	for i := 1; i <= 3; i++ {
+		partition := fmt.Sprintf("%sp%d", v.loopDevice, i)
 
-	if lxd.PathExists(v.getRootfsDevFile()) {
-		err := os.Remove(v.getRootfsDevFile())
-		if err != nil {
-			return err
+		if lxd.PathExists(partition) {
+			err := os.Remove(partition)
+			if err != nil {
+				return err
+			}
 		}
 	}
 
@@ -268,6 +305,8 @@ func (v *vm) createFilesystems() error {
 		err = shared.RunCommand("btrfs", "subvolume", "create", fmt.Sprintf("%s/@", v.rootfsDir))
 	case "ext4":
 		err = shared.RunCommand("mkfs.ext4", "-F", "-b", "4096", "-i 8192", "-m", "0", "-L", "rootfs", "-E", "resize=536870912", v.getRootfsDevFile())
+	case "ntfs":
+		err = shared.RunCommand("mkfs.ntfs", "-f", "-L", "rootfs", v.getRootfsDevFile())
 	}
 	if err != nil {
 		return err
@@ -288,6 +327,7 @@ func (v *vm) mountRootFilesystem() error {
 	case "btrfs":
 		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir, "-o", "defaults,subvol=/@")
 	case "ext4":
+	case "ntfs":
 		err = shared.RunCommand("mount", v.getRootfsDevFile(), v.rootfsDir)
 	}
 


More information about the lxc-devel mailing list