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</PlainText> + <Value>password</Value> + </AdministratorPassword> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Password> + <PlainText>true</PlainText> + <Value>password</Value> + </Password> + <Description>admin</Description> + <Group>Administrators</Group> + <Name>admin</Name> + <DisplayName>admin</DisplayName> + </LocalAccount> + </LocalAccounts> + + </UserAccounts> + + <OOBE> + <HideEULAPage>true</HideEULAPage> + <ProtectYourPC>3</ProtectYourPC> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideLocalAccountScreen>true</HideLocalAccountScreen> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <SkipMachineOOBE>true</SkipMachineOOBE> + <SkipUserOOBE>true</SkipUserOOBE> + <NetworkLocation>Work</NetworkLocation> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + </OOBE> + + </component> + <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <InputLocale>0409:00000409</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UILanguageFallback>en-US</UILanguageFallback> + <UserLocale>en-US</UserLocale> + </component> + + </settings> +</unattend>