The following pull request was submitted through Github.
It can be accessed and reviewed at: https://github.com/lxc/distrobuilder/pull/368

This e-mail was sent by the LXC bot, direct replies will not reach the author
unless they happen to be subscribed to this list.

=== Description (from pull-request) ===

From 5994aadde58bd4bc7a36f6ad72e811f03fd65bfb Mon Sep 17 00:00:00 2001
From: Thomas Hipp <thomas.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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.h...@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)
        }
 
_______________________________________________
lxc-devel mailing list
lxc-devel@lists.linuxcontainers.org
http://lists.linuxcontainers.org/listinfo/lxc-devel

Reply via email to