Initial commit
This commit is contained in:
commit
0c68ff3ca0
18
README.md
Normal file
18
README.md
Normal 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
165
setup_win10.py
Executable 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
134
unattend.xml.example
Normal 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>
|
Loading…
Reference in New Issue
Block a user