Initial commit

This commit is contained in:
Filip Stedronsky 2021-07-13 18:55:29 +02:00
commit 0c68ff3ca0
4 changed files with 317 additions and 0 deletions

BIN
BCD Executable file

Binary file not shown.

18
README.md Normal file
View File

@ -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.

165
setup_win10.py Executable file
View File

@ -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)

134
unattend.xml.example Normal file
View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<!--
=====================
SPECIALIZE SETTINGS
=====================
During the specialize pass of Windows Setup, machine-specific information for the image is applied.
For example, you can configure network settings, international settings, and domain information.
The specialize pass is used in conjunction with the generalize pass.
The generalize pass is used to create a Windows reference image that can be used throughout an organization.
From this basic Windows reference image, you can add further customizations that apply to different divisions
within an organization or apply to different installations of Windows.
The specialize pass is used to apply these specific customizations.
-->
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" 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">
<ComputerName>testws1</ComputerName>
</component>
<component name="Microsoft-Windows-RemoteAssistance-Exe" 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">
<CreateEncryptedOnlyTickets>true</CreateEncryptedOnlyTickets>
<fAllowToGetHelp>true</fAllowToGetHelp>
</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>
<component name="Microsoft-Windows-Deployment" 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">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add">
<Description>Some preparation script</Description>
<Order>1</Order>
<Path>cmd.exe /c C:\my_script.cmd</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
<component name="Networking-MPSSVC-Svc" 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">
<DomainProfile_EnableFirewall>false</DomainProfile_EnableFirewall>
<PrivateProfile_EnableFirewall>false</PrivateProfile_EnableFirewall>
<PublicProfile_EnableFirewall>false</PublicProfile_EnableFirewall>
</component>
<component name="Microsoft-Windows-Embedded-EmbeddedLogon" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<NoLockScreen>1</NoLockScreen>
</component>
</settings>
<!--
=====================
AUDITSYSTEM SETTINGS
=====================
The auditSystem pass is an optional pass that enables you to add additional device drivers and applications to the image.
This results in fewer required images because a reference image can be created with a minimal set of drivers.
The image can be updated with additional drivers during the audit process.
You can then test and resolve any operating system issues related to malfunctioning or incorrectly installed devices on the image.
For example, you can install additional language packs, updates, or other applications, such as Microsoft Office.
See Reseal mode in oobeSystem.
-->
<!--
<settings pass="auditSystem">
</settings>
-->
<!--
=====================
AUDITUSER SETTINGS
=====================
The auditUser pass is similar to the auditSystem pass.
However, the auditUser pass processes these settings after users have logged on, not before they have logged on.
Like the auditSystem pass, the auditUser pass is used to test the functionality of the Windows Vista image.
See Reseal mode in oobeSystem.
-->
<!--
<settings pass="auditUser">
</settings>
-->
<!--
=====================
OOBESYSTEM SETTINGS
=====================
The oobeSystem pass configures settings that are applied during the first-boot experience for end users, also called Windows Welcome.
oobeSystem settings are processed before a user first logs into Windows.
Out-of-Box-Experience (OOBE) runs the first time the user starts a new computer.
OOBE runs before the Windows shell or any additional software runs, and performs a small set of tasks necessary to configure and run Windows.
-->
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" 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">
<TimeZone>Central Europe Standard Time</TimeZone>
<UserAccounts>
<AdministratorPassword>
<PlainText>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>