From 0c68ff3ca0a784304d74865d8e74049552710c20 Mon Sep 17 00:00:00 2001
From: Filip Stedronsky
Date: Tue, 13 Jul 2021 18:55:29 +0200
Subject: [PATCH] Initial commit
---
BCD | Bin 0 -> 20480 bytes
README.md | 18 +++++
setup_win10.py | 165 +++++++++++++++++++++++++++++++++++++++++++
unattend.xml.example | 134 +++++++++++++++++++++++++++++++++++
4 files changed, 317 insertions(+)
create mode 100755 BCD
create mode 100644 README.md
create mode 100755 setup_win10.py
create mode 100644 unattend.xml.example
diff --git a/BCD b/BCD
new file mode 100755
index 0000000000000000000000000000000000000000..29a0c3a1844949b53a3df0e2e7041de0abb815b6
GIT binary patch
literal 20480
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
literal 0
HcmV?d00001
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3dde954
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# Deploy Windows 10 from Linux
+
+This is a simple Python script that installs Windows 10 to a target disk from a running
+Linux system (i.e., without booting from Windows installation ISO and without using Windows PE).
+
+## Use cases
+
+ * Mass-install Windows workstations from a PXE-booted Linux environment.
+ (Here it may be useful to convert install.wim to a pipable WIM file
+ and then you can stream it e.g. using HTTP from a server).
+ * Provision VMs with Windows 10 with a single command, without any
+ intermediate steps with mounting ISOs, changing boot order and the like.
+
+## Limitations
+
+ * Currently supports only BIOS boot, not UEFI. But this should be easy
+ to implement.
+
diff --git a/setup_win10.py b/setup_win10.py
new file mode 100755
index 0000000..6eb4f9b
--- /dev/null
+++ b/setup_win10.py
@@ -0,0 +1,165 @@
+#!/usr/bin/python3
+
+import sys,os,shutil
+import time
+import string
+import clize
+from clize import ArgumentError, Parameter
+import argparse
+from contextlib import *
+from pathlib import Path
+import subprocess
+import tempfile
+import parted
+
+def is_part(pth):
+ pth = Path(pth)
+ if not pth.is_block_device(): raise RuntimeError("Not a block device, cannot determine partition-ness")
+ sys_path = Path("/sys/class/block") / pth.name
+ if not sys_path.exists(): raise RuntimeError("{sys_path} does not exist (for {pth})")
+ return (sys_path / 'partition').exists()
+
+
+
+@contextmanager
+def with_device(pth):
+ pth = Path(pth)
+ if pth.is_file():
+ r = subprocess.run(['losetup', '--show', '-f', '-P', pth], check=True, capture_output=True)
+ dev = Path(r.stdout.decode('ascii').strip())
+ if not dev.is_block_device():
+ raise RuntimeError(f"Cannot find loop device {dev}")
+ try:
+ yield dev
+ finally:
+ subprocess.run(['losetup', '-d', dev])
+ elif pth.is_block_device():
+ pass
+
+def ci_lookup(base, *comps, creating=False, parents=False):
+ """Lookup path components case-insensitively"""
+ cur = Path(base)
+ for idx, comp in enumerate(comps):
+ cands = [ item for item in cur.iterdir() if item.name.lower() == comp.lower() ]
+ if not cands:
+ if creating and idx == len(comps) - 1:
+ cur = cur / comp
+ break
+ elif parents and idx < len(comps) - 1:
+ cur = cur / comp
+ cur.mkdir()
+ continue
+ else:
+ raise FileNotFoundError(f"'{comp}' not found case-insensitively in '{cur}'")
+ elif len(cands) > 1:
+ raise RuntimeError(f"Multiple case-insensitive candidates for '{comp}' in '{cur}': {cands}")
+ else:
+ cur = cands[0]
+ return cur
+
+
+@contextmanager
+def with_iso(iso):
+ with ExitStack() as es:
+ dir = Path(tempfile.mkdtemp(prefix="win10_iso_"))
+ es.callback(lambda: dir.rmdir())
+ subprocess.run(['mount', '-o', 'loop,ro', '-t', 'udf', str(iso), str(dir)], check=True)
+ es.callback(lambda: subprocess.run(['umount', dir]))
+ wim = ci_lookup(dir, 'sources', 'install.wim')
+ yield wim
+
+@contextmanager
+def with_mounted(part):
+ part = Path(part)
+ with ExitStack() as es:
+ dir = Path(tempfile.mkdtemp(prefix=f"ntfs_{part.name}_"))
+ es.callback(lambda: dir.rmdir())
+ subprocess.run(['ntfs-3g', str(part), dir], check=True)
+ es.callback(lambda: subprocess.run(['umount', dir]))
+ yield dir
+
+
+def create_partitions(dev):
+ with open(dev, 'r+b') as fh:
+ fh.write(bytearray(4096)) # clear MBR and other metadata
+
+ device = parted.Device(str(dev))
+ disk = parted.freshDisk(device, 'msdos')
+ geometry = parted.Geometry(device=device, start=2048,
+ length=device.getLength() - 2048)
+ filesystem = parted.FileSystem(type='ntfs', geometry=geometry)
+ partition = parted.Partition(disk=disk, type=parted.PARTITION_NORMAL,
+ fs=filesystem, geometry=geometry)
+ disk.addPartition(partition=partition,
+ constraint=device.optimalAlignedConstraint)
+ partition.setFlag(parted.PARTITION_BOOT)
+ disk.commit()
+
+
+def part_path(dev, partno):
+ dev = Path(dev)
+ return dev.parent / f"{dev.name}{'p' if dev.name[-1] in string.digits else ''}{partno}"
+
+
+def format_part(part):
+ cmd = ['mkntfs', '-vv', '-f', '-S', '63', '-H', '255', '--partition-start', '2048', str(part)]
+ subprocess.run(cmd, check=True)
+
+
+def apply_wim(part, wim, image_name):
+ subprocess.run(['wimapply', str(wim), str(image_name), str(part)], check=True)
+
+def setup_vbr(part):
+ subprocess.run(['ms-sys', '-f', '--ntfs', str(part)], check=True)
+
+def setup_mbr(disk):
+ subprocess.run(['ms-sys', '-f', '--mbr7', str(disk)], check=True)
+
+def copy_boot_files(dir):
+ shutil.copy(ci_lookup(dir, 'Windows', 'Boot', 'PCAT', 'bootmgr'), ci_lookup(dir, 'bootmgr', creating=True))
+ boot_dir = ci_lookup(dir, 'Boot', creating=True)
+ boot_dir.mkdir(exist_ok=True)
+ shutil.copy(Path(__file__).parent / 'BCD', ci_lookup(boot_dir, 'BCD', creating=True))
+
+
+def setup_part(part, wim, image_name, *, unattend=None, postproc=None):
+ format_part(part)
+ setup_vbr(part)
+ apply_wim(part, wim, image_name)
+ with with_mounted(part) as dir:
+ copy_boot_files(dir)
+ if unattend:
+ trg = ci_lookup(dir, 'Windows', 'Panther', 'unattend.xml', creating=True, parents=True)
+ print(f"Copying unattend file: {unattend} -> {trg}")
+ shutil.copy(unattend, trg)
+ if postproc:
+ if '/' not in postproc: postproc = f"./{postproc}"
+ subprocess.run([str(postproc), dir])
+
+
+def exactly_one(*a):
+ return sum( bool(x) for x in a ) == 1
+
+def main(*, disk=None, part=None, wim=None, iso=None, image_name=None, unattend=None, postproc=None):
+ if not exactly_one(disk, part):
+ raise ArgumentError("You must specify exactly one of 'disk', 'part'")
+ if not exactly_one(wim, iso):
+ raise ArgumentError("You must specify exactly one of 'wim', 'iso'")
+ with ExitStack() as es:
+ if iso:
+ wim = es.enter_context(with_iso(iso))
+ if disk:
+ create_partitions(disk)
+ with with_device(disk) as dev:
+ create_partitions(dev)
+ setup_mbr(dev)
+ part = part_path(dev, 1)
+ setup_part(part, wim, image_name, unattend=unattend, postproc=postproc)
+ else:
+ setup_part(part, unattend=unattend, postproc=postproc)
+
+if __name__ == '__main__':
+ clize.run(main)
+
+
+
diff --git a/unattend.xml.example b/unattend.xml.example
new file mode 100644
index 0000000..bc96a66
--- /dev/null
+++ b/unattend.xml.example
@@ -0,0 +1,134 @@
+
+
+
+
+
+ testws1
+
+
+
+
+ true
+ true
+
+
+ 0409:00000409
+ en-US
+ en-US
+ en-US
+ en-US
+
+
+
+
+ Some preparation script
+ 1
+ cmd.exe /c C:\my_script.cmd
+
+
+
+
+ false
+ false
+ false
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ Central Europe Standard Time
+
+
+ true
+ password
+
+
+
+
+ true
+ password
+
+ admin
+ Administrators
+ admin
+ admin
+
+
+
+
+
+
+ true
+ 3
+ true
+ true
+ true
+ true
+ true
+ Work
+ true
+
+
+
+
+ 0409:00000409
+ en-US
+ en-US
+ en-US
+ en-US
+
+
+
+