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