├── .gitignore ├── LICENSE ├── README.md ├── kiwi ├── BCD.py ├── __init__.py ├── install.py ├── interface.py ├── mount.py └── wimlib.py ├── screenshots ├── editions.png ├── extraction.png ├── menu.png └── sources.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joseph Kogut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | KiWI is a lightweight, graphical, curses-based terminal program for 3 | deploying Windows installations over networks. It's recommended 4 | that it be run in a diskless PXE system on the target machine, with 5 | the installation sources being accessed over the network. 6 | 7 | You can find instructions on how to do this here: 8 | https://wiki.archlinux.org/index.php/Diskless_system 9 | 10 | KiWI was inspired by the Archboot setup wizard. 11 | 12 | ## Why? 13 | 14 | The standard Windows setup is based on Windows PE. It's slow to boot, slow to install, has relatively high requirements for its given task, and it's intentionally crippled in some areas. Namely, the standard Windows installer will not deploy an image to anything but a fixed storage device, and installation sources must be located on the media. Furthermore, certain setup options are unable to be changed by the user, such as custom partitioning options, and filesystem options. 15 | 16 | Windows Deployment Services removes some of these limitations, and imposes some of its own. It allows for Windows setup to be network booted, but this functionality requires Windows Server, and the setup is still PE based. Configuration can also be tricky. 17 | 18 | Conversely, KiWI is easy to setup, fast to boot (~15 seconds with PXELINUX over GbE with a BTRFS formatted NBD root), fast to install (2-5 minutes, depending on RAM capacity, processor, installation source and target IO throughput), and easily configurable. 19 | 20 | KiWI can find and install from bare WIMs available over NFS, SMB, SCP, block devices, or local paths. KiWI supports installing Windows in both MBR/BIOS and GPT/UEFI configurations. 21 | 22 | ## Limitations 23 | As described in kiwi/BCD.py, the Boot Configuration Data (BCD) store is a mostly undocumented binary file. The way KiWI currently makes Windows deployments bootable is by using a BCD store that was created by Windows, then changing the disk signature of the new install to match the premade BCD. In the future, this should be replaced with a custom BCD creation tool. 24 | 25 | ### Requirements 26 | * python3 27 | * python-dialog 28 | * parted 29 | * gptfdisk 30 | * ntfsprogs 31 | * ntfs-3g 32 | * dosfstools 33 | * ms-sys 34 | * wimlib 35 | 36 | #### Optional: 37 | * nfs-utils 38 | * nbd 39 | * sshfs 40 | 41 | ## Installation 42 | python setup.py install 43 | 44 | ## Usage 45 | python -m kiwi.install 46 | 47 | ## Troubleshooting 48 | * Windows 10 images appear to be stuck at 0% on "Creating files", what gives? 49 | 50 | There's a bug in older versions of libntfs-3g that causes wimlib to fail to parse the security descriptors in the image. Installing the ntfs-3g-fuse package from the Arch User Repository seems to fix the issue for now. 51 | 52 | ## Screenshots 53 | ![Main Menu](/screenshots/menu.png?raw=true) 54 | ![Sources](/screenshots/sources.png?raw=true) 55 | ![Sources](/screenshots/editions.png?raw=true) 56 | ![Sources](/screenshots/extraction.png?raw=true) 57 | -------------------------------------------------------------------------------- /kiwi/BCD.py: -------------------------------------------------------------------------------- 1 | """ The Boot Configuration Data (BCD) file is a mostly undocumented binary 2 | file used by the Windows Boot Manager to configure the boot process. Among 3 | other things, it specifies the boot drive disk signature, and system partition 4 | offset, or for GPT formatted disks, the disk and partition UUIDs. 5 | 6 | Generally speaking, the boot and/or system partitions have a unique, randomly 7 | generated signature or UUID that's written to the BCD by the Windows installer. 8 | Because of the lack of documentation Surrounding the BCD, we simply store the 9 | correct configuration here, Gzipped and base64 encoded. """ 10 | 11 | import gzip 12 | import base64 13 | def write_bcd(bcd, path): 14 | data = gzip.decompress(base64.b64decode(bcd)) 15 | filp = open(path, 'wb') 16 | filp.write(data) 17 | filp.close() 18 | 19 | bios_bcd = """ 20 | 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""" 99 | 100 | uefi_bcd = """ 101 | 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 161 | NEuzNEuzNMv/Svk3vPkX4wBQAAA=""" 162 | -------------------------------------------------------------------------------- /kiwi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakogut/KiWI/3cbbb24166c2682b33fbc1ea40823a253fcdf585/kiwi/__init__.py -------------------------------------------------------------------------------- /kiwi/install.py: -------------------------------------------------------------------------------- 1 | import locale 2 | from dialog import Dialog 3 | 4 | locale.setlocale(locale.LC_ALL, '') 5 | 6 | import sys, os 7 | import glob 8 | import re 9 | import subprocess 10 | from time import sleep 11 | import shutil 12 | import configparser 13 | 14 | import logging 15 | import logging.handlers 16 | 17 | from .interface import * 18 | from .mount import * 19 | from .wimlib import wiminfo 20 | 21 | logger = logging.getLogger() 22 | 23 | import urllib.request 24 | from urllib.error import HTTPError, URLError 25 | import socket 26 | 27 | config_url = 'http://10.10.200.1/linux/kiwi/kiwi.conf' 28 | config_timeout=0.1 29 | 30 | class FailedInstallStep(Exception): pass 31 | 32 | class WindowsInstallApp(object): 33 | def __init__(self, config=None): 34 | self.logger = logging.getLogger(__name__) 35 | self.config = config 36 | 37 | self.uefi = False 38 | 39 | self.boot_part = '' 40 | self.system_part = '' 41 | self.image_path = '' 42 | self.image_index = '' 43 | 44 | self.boot_dir = '/mnt/boot' 45 | self.system_dir = '/mnt/system' 46 | 47 | self.mbr_disk_signature = '4D34B30F' 48 | self.gpt_disk_signature = '572BD0E9-D39E-422C-82E6-F37157C3535D' 49 | self.boot_partuuid = '8d03c7bb-6b0c-4223-aaa1-f20bf521cd6e' 50 | self.system_partuuid = '57092450-f142-4749-b540-f2ec0a183b7b' 51 | 52 | self.cluster_size = 4096 53 | self.fs_compression = False 54 | self.quick_format = True 55 | 56 | self.d = Dialog(dialog='dialog') 57 | self.d.set_background_title('KiWI: Killer Windows Installer') 58 | 59 | self.source_dir = '/mnt/source/' 60 | 61 | advanced_items = [ 62 | ('Filesystem options', MenuItem(self.fs_options)), 63 | ] 64 | 65 | advanced_submenu = Menu(self.d, advanced_items, title='Advanced Options') 66 | 67 | main_menu_items = [ 68 | ('Configure Networking', MenuItem(self.configure_network)), 69 | ('Prepare Storage Device', MenuItem(self.auto_prepare)), 70 | ('Select Installation Source', MenuItem(self.prepare_source)), 71 | ('Install OS', MenuItem(self.install_os)), 72 | #('Install Bootloader', self.install_bootloader), 73 | ('Reboot', MenuItem(self.reboot)), 74 | ('---', MenuItem(separator=True)), 75 | ('Advanced Options', advanced_submenu), 76 | ] 77 | 78 | self.running = True 79 | self.main_menu = StatefulMenu(self.d, main_menu_items, title='Main Menu') 80 | while self.running: self.main_menu.run(ret=self.exit()) 81 | 82 | def sync(self): 83 | self.d.infobox('Syncing buffered data\n\nDo NOT reboot!', width=30) 84 | subprocess.check_call(['sync']) 85 | 86 | def reboot(self): 87 | self.sync() 88 | subprocess.check_call(['reboot']) 89 | 90 | def fs_options(self): 91 | choices = [ 92 | ('Quick Format', '', 'quick_format'), 93 | ('NTFS Compression', '', 'fs_compression'), 94 | ('Force GPT/EFI', '', 'uefi'), 95 | ] 96 | 97 | code, selected = self.d.checklist('Filesystem Options', choices=[ 98 | (choice[0], choice[1], getattr(self, choice[2])) for choice in choices], 99 | cancel_label='Back') 100 | 101 | if code != self.d.OK: return 102 | 103 | for item in choices: 104 | tag = item[0] 105 | var_name = item[2] 106 | 107 | if tag in selected: setattr(self, var_name, True) 108 | else: setattr(self, var_name, False) 109 | 110 | def test_network(self): 111 | return True if subprocess.call( 112 | ['ping', '-c 2', '-i 0.2', '8.8.8.8'], 113 | stdout=subprocess.PIPE) == 0 else False 114 | 115 | def configure_network(self): 116 | if not self.test_network(): 117 | rc = subprocess.call('nmtui', shell=True) 118 | else: 119 | self.d.msgbox('Network Configuration Successful', width=40, title='Network Status') 120 | self.main_menu.advance() 121 | 122 | def detect_blockdevs(self): 123 | devices = [] 124 | p = subprocess.run(['lsblk', '-Ppd'], stdout=subprocess.PIPE) 125 | for line in p.stdout.decode('UTF-8').split('\n'): 126 | dev = {} 127 | for p in line.split(): 128 | pair = p.split('=') 129 | dev[pair[0]] = pair[1][1:-1] 130 | 131 | # We don't need read-only devices 132 | if 'RO' not in dev or dev['RO'] == '1': continue 133 | devices.append(dev) 134 | 135 | self.d.msgbox('Detected Devices:\n\n' + '\n'.join( 136 | [' '.join([dev['NAME'], dev['SIZE']]) for dev in devices])) 137 | 138 | self.devices = devices 139 | 140 | def select_disk(self): 141 | self.detect_blockdevs() 142 | 143 | entries = [tuple([device['NAME'], '-']) for device in self.devices] + [('OTHER', '+')] 144 | code, tag = self.d.menu('Choose an installation drive', choices=entries) 145 | if code != self.d.OK: raise FailedInstallStep 146 | 147 | if tag == 'OTHER': 148 | code, tag = self.d.inputbox('Enter a path to a block device') 149 | if code != self.d.OK: 150 | raise FailedInstallStep 151 | 152 | if not os.path.isfile(tag): 153 | raise FailedInstallStep 154 | 155 | import stat 156 | mode = os.stat(tag).st_mode 157 | if not stat.S_ISBLK(mode): 158 | raise FailedInstallStep 159 | 160 | code, confirmation = self.d.inputbox('This will erase ALL data on %s' % tag + \ 161 | '\n\nType \'YES\' to continue', width=40, height=15) 162 | if code != self.d.OK or confirmation != 'YES': raise FailedInstallStep 163 | 164 | self.install_drive = tag 165 | self.logger.info('Block device {} selected for installation'.format(self.install_drive)) 166 | 167 | def supports_uefi(self): 168 | p = subprocess.Popen(['efivar', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 169 | uefi = True if p.returncode == 0 else False 170 | return uefi 171 | 172 | def auto_prepare(self): 173 | try: 174 | self.select_disk() 175 | except FailedInstallStep: 176 | self.d.msgbox('Disk selection failed. Please retry the step to prepare the storage device.', width=40) 177 | raise 178 | 179 | for dir in [self.system_dir, self.boot_dir]: 180 | if mountpoint(dir): unmount(dir) 181 | 182 | partitions = glob.glob(self.install_drive + '+.') 183 | for part in partitions: 184 | logger.debug('Unmounting partition {}'.format(part)) 185 | try: unmount(part) 186 | except subprocess.CalledProcessError: pass 187 | 188 | self.auto_partition() 189 | self.auto_format() 190 | 191 | self.main_menu.advance() 192 | 193 | def auto_partition(self): 194 | if self.uefi is False: 195 | self.uefi = self.supports_uefi() 196 | else: uefi_forced = True 197 | 198 | if self.uefi and not uefi_forced: 199 | self.logger.info('Detected machine booted with UEFI, using GPT') 200 | elif self.uefi and uefi_forced: 201 | self.logger.info('UEFI install forced, using GPT') 202 | else: self.logger.info('UEFI not supported, creating DOS partition table') 203 | 204 | partition_table = 'msdos' if not self.uefi else 'gpt' 205 | 206 | try: 207 | subprocess.check_call(['parted', '-s', self.install_drive, 208 | 'mklabel', partition_table]) 209 | if self.uefi: 210 | subprocess.check_call(['parted', '--align', 'optimal', 211 | '-s', self.install_drive, '--', 212 | 'mkpart', 'ESP', 'fat32', '0%s', '512', 213 | 'mkpart', 'Windows', 'NTFS', '512', '100%', 214 | 'set', '1', 'esp', 'on']) 215 | else: 216 | subprocess.check_call(['parted', '-s', self.install_drive, '--', 217 | 'mkpart', 'primary', 'NTFS', '2048s', '-1s', 218 | 'set', '1', 'boot', 'on']) 219 | 220 | except subprocess.CalledProcessError: 221 | self.d.msgbox('Partitioning/formatting failed. Please retry.', width=40) 222 | raise FailedInstallStep 223 | 224 | if self.uefi: 225 | self.boot_part = self.install_drive + '1' 226 | self.system_part = self.install_drive + '2' 227 | else: 228 | self.system_part = self.install_drive + '1' 229 | 230 | def auto_format(self): 231 | call = ['mkfs.ntfs'] 232 | 233 | call.append('-c') 234 | call.append(str(self.cluster_size)) 235 | 236 | if self.fs_compression: call.append('-C') 237 | if self.quick_format: call.append('-Q') 238 | call.append(self.system_part) 239 | 240 | try: 241 | subprocess.check_call(call, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 242 | 243 | if self.uefi: subprocess.check_call(['mkfs.msdos', '-F32', self.boot_part], 244 | stdout = subprocess.PIPE, stderr=subprocess.PIPE) 245 | except subprocess.CalledProcessError: 246 | raise FailedInstallStep 247 | 248 | self.d.infobox('Formatting drive...') 249 | 250 | self.logger.info('Sucessfully formatted installation drive') 251 | 252 | def prepare_source(self): 253 | if not self.test_network(): 254 | self.configure_network() 255 | 256 | source_items = [ 257 | ('Network Filesystem (NFS)', MenuItem(self.prepare_nfs_source)), 258 | ('Windows Share (SMB/CIFS)', MenuItem(self.prepare_smb_source)), 259 | #('Network Block Device (NBD)', MenuItem()), 260 | ('SCP/SFTP (SSH)', MenuItem(self.prepare_sshfs_source)), 261 | ('Block Device (USB, CD/DVD, etc.)', self.prepare_blk_source), 262 | ('---', MenuItem(separator=True)), 263 | ('OTHER (Path)', MenuItem(self.prepare_fs_source)), 264 | ] 265 | 266 | try: 267 | Menu(self.d, source_items, 'Select Installation Source', ret=None).run() 268 | self.select_source() 269 | except FailedInstallStep: raise 270 | except subprocess.CalledProcessError: 271 | self.d.msgbox('Mount Failed. Please retry the installation source selection step') 272 | 273 | def prepare_nfs_source(self): 274 | code, path = self.d.inputbox('Enter an NFS server or share', 275 | init=self.config.get('source', 'default_nfs', fallback=''), width=40) 276 | 277 | mount(path, self.source_dir, force=True, mkdir=True) 278 | 279 | def prepare_smb_source(self): 280 | code, path = self.d.inputbox( 281 | 'Enter an SMB share path in the format \'user@//server/share\'', width=40) 282 | 283 | user, passwd, cred = '', '', '' 284 | if '@' in path: 285 | user, path = path.split('@') 286 | code, passwd = self.d.passwordbox( 287 | 'Enter the share password, if applicable', width=40) 288 | 289 | cred = 'password={},'.format(passwd) 290 | if user: cred += 'username={},'.format(user) 291 | 292 | mount(path, self.source_dir, options=cred, force=True, mkdir=True, fs_type='cifs') 293 | 294 | def prepare_fs_source(self): 295 | code, path = self.d.inputbox('Enter a UNIX path', width=40) 296 | mount(path, self.source_dir, force=True, mkdir=True, bind=True) 297 | 298 | def prepare_sshfs_source(self): 299 | code, path = self.d.inputbox('Enter an SSHFS path, in the format user@server:/', width=40) 300 | code, passwd = self.d.passwordbox('Enter the password', width=40) 301 | 302 | try: os.makedirs(self.source_dir) 303 | except FileExistsError: pass 304 | 305 | if mountpoint(self.source_dir): unmount(self.source_dir) 306 | 307 | disable_hostkey_check = ['-o', 'StrictHostKeyChecking=no'] 308 | call = ['sshfs', path, self.source_dir, '-o', 'password_stdin'] 309 | call += disable_hostkey_check 310 | 311 | p = subprocess.Popen(call, stdin=subprocess.PIPE, stdout=open('/dev/null', 'w')) 312 | p.communicate(input=passwd.encode('UTF-8')) 313 | if p.returncode != 0: raise subprocess.CalledProcessError 314 | 315 | def prepare_blk_source(self): 316 | code, path = self.d.inputbox('Enter a block device path', width=40) 317 | mount(path, self.source_dir, force=True, mkdir=True) 318 | 319 | def select_source(self): 320 | discovered_wims = glob.glob(self.source_dir + '**/*.wim', recursive=True) 321 | discovered_wims += glob.glob(self.source_dir + '**/.esd', recursive=True) 322 | 323 | #discovered_isos = glob.glob(self.source_dir + '**/.iso', recursive=True) 324 | 325 | if not discovered_wims: # or discovered_isos: 326 | self.d.msgbox('Failed to locate install sources. Check your media, and try again.', width=40) 327 | raise FailedInstallStep 328 | 329 | entries = [tuple([wim, '-']) for wim in discovered_wims] 330 | code, tag = self.d.menu('Choose a WIM', choices=entries) 331 | if code == self.d.OK: self.image_path = tag 332 | else: raise FailedInstallStep 333 | 334 | try: 335 | entries = [ 336 | tuple([ 337 | image['Index'], 338 | # Not every WIM has 'Display Name' defined 339 | image.get('Display Name') or image.get('Description') + ' ' + 340 | image.get('Architecture') 341 | ]) for image in wiminfo(self.image_path)] 342 | except subprocess.CalledProcessError: 343 | self.d.msgbox('Image is invalid or corrupt. Please retry the installation source step.', width=40) 344 | raise FailedInstallStep 345 | 346 | code, tag = self.d.menu('Choose an image', choices=entries, width=40) 347 | if code == self.d.OK: self.image_index = tag 348 | else: raise FailedInstallStep 349 | 350 | self.main_menu.advance() 351 | 352 | def install_os(self): 353 | if not self.system_part: 354 | self.auto_prepare() 355 | self.main_menu.position -= 1 356 | 357 | if not (self.image_path and self.image_index): 358 | self.prepare_source() 359 | self.main_menu.position -= 1 360 | 361 | self.extract_wim(self.image_path, self.image_index, self.system_part) 362 | self.sync() 363 | 364 | self.install_bootloader() 365 | self.main_menu.advance() 366 | 367 | def extract_wim(self, wimfile, imageid, target): 368 | r, w = os.pipe() 369 | process = subprocess.Popen(['wimlib-imagex', 'apply', wimfile, imageid, target], 370 | stdout=w, stderr=w) 371 | 372 | filp = os.fdopen(r) 373 | 374 | self.logger.info('Applying WIM...') 375 | 376 | while True: 377 | line = filp.readline() 378 | self.logger.debug('Discarding line from WIM STDOUT: {}'.format(line)) 379 | if 'Creating files' in line: break 380 | 381 | for stage in ['Creating files', 'Extracting file data']: 382 | self.d.gauge_start(text=stage, width=80, percent=0) 383 | 384 | while(True): 385 | line = filp.readline() 386 | self.logger.debug('Wim extraction STDOUT: {}'.format(line)) 387 | if stage not in line: continue 388 | pct = re.search(r'\d+%', line).group(0)[:-1] 389 | 390 | if pct: 391 | self.d.gauge_update(int(pct)) 392 | if pct == '100': break 393 | 394 | exit_code = self.d.gauge_stop() 395 | 396 | def ntfs_hide(self, path): 397 | subprocess.check_call(['setfattr', '-h', '-v', '0x02000000', 398 | '-n', 'system.ntfs_attrib', path]) 399 | 400 | def install_bootloader(self): 401 | from . import BCD 402 | mount(self.system_part, self.system_dir, mkdir=True) 403 | 404 | if not self.uefi: 405 | self.write_mbr() 406 | 407 | shutil.copytree( 408 | os.path.join(self.system_dir, 'Windows/Boot/PCAT'), 409 | os.path.join(self.system_dir, 'Boot')) 410 | 411 | shutil.copy2( 412 | os.path.join(self.system_dir, 'Boot/bootmgr'), self.system_dir) 413 | 414 | for file in ['Boot', 'bootmgr']: 415 | self.ntfs_hide(os.path.join(self.system_dir, file)) 416 | 417 | BCD.write_bcd(BCD.bios_bcd, os.path.join(self.system_dir, 'Boot/BCD')) 418 | else: 419 | mount(self.boot_part, self.boot_dir, mkdir=True) 420 | subprocess.check_call(['sgdisk', self.install_drive, 421 | '-U', self.gpt_disk_signature, 422 | '-u 1:' + self.boot_partuuid, 423 | '-u 2:' + self.system_partuuid]) 424 | 425 | for dir in ['Boot', 'Microsoft']: 426 | os.makedirs(os.path.join(self.boot_dir, 'EFI/' + dir)) 427 | 428 | shutil.copytree( 429 | os.path.join(self.system_dir, 'Windows/Boot/EFI'), 430 | os.path.join(self.boot_dir, 'EFI/Microsoft/Boot')) 431 | 432 | shutil.copyfile( 433 | os.path.join(self.boot_dir, 'EFI/Microsoft/Boot/bootmgfw.efi'), 434 | os.path.join(self.boot_dir, 'EFI/Boot/bootx64.efi')) 435 | 436 | BCD.write_bcd(BCD.uefi_bcd, 437 | os.path.join(self.boot_dir, 'EFI/Microsoft/Boot/BCD')) 438 | 439 | def write_mbr(self): 440 | subprocess.check_call(['ms-sys', '-S', self.mbr_disk_signature, '-7', self.install_drive], 441 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 442 | 443 | self.logger.info('MBR written to {}'.format(self.install_drive)) 444 | 445 | def exit(self): 446 | self.running = False 447 | 448 | def handle_exception(exc_type, exc_value, exc_traceback): 449 | if issubclass(exc_type, KeyboardInterrupt): 450 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 451 | return 452 | 453 | logger.critical('Unhandled exception', exc_info=(exc_type, exc_value, exc_traceback)) 454 | 455 | import sys 456 | sys.excepthook = handle_exception 457 | 458 | if __name__ == '__main__': 459 | logger = logging.getLogger() 460 | logger.setLevel(logging.INFO) 461 | 462 | fh = logging.FileHandler('/tmp/kiwi-install.log') 463 | logger.addHandler(fh) 464 | 465 | configdata = None 466 | 467 | try: 468 | configdata = urllib.request.urlopen(config_url, timeout=config_timeout).read().decode('UTF-8') 469 | except (HTTPError, URLError): 470 | logger.warning('Unable to fetch config file from URL {}'.format(config_url)) 471 | except socket.timeout: 472 | logger.warning('Socket timed out while trying to fetch config') 473 | 474 | config = configparser.ConfigParser() 475 | config.read_string(configdata) 476 | 477 | app = WindowsInstallApp(config) 478 | -------------------------------------------------------------------------------- /kiwi/interface.py: -------------------------------------------------------------------------------- 1 | from .install import FailedInstallStep 2 | 3 | separator_tag = '*' 4 | 5 | class MenuItem(object): 6 | def __init__(self, func=None, separator=False): 7 | self.function = func 8 | self.separator = separator 9 | 10 | # Wrapper for child.function() that creates a call stack 11 | def run(self, ret=None): 12 | try: 13 | if self.function: self.function() 14 | except: pass 15 | 16 | if ret: ret() 17 | 18 | class Menu(MenuItem): 19 | def __init__(self, dialog, items, title, ret='default'): 20 | self.d = dialog 21 | 22 | if ret == 'default': self.ret = self.run 23 | else: self.ret = None 24 | 25 | super().__init__(func=self.function) 26 | 27 | self.entries = [] 28 | self.dispatch_table = {} 29 | tag = 1 30 | 31 | self.title = title 32 | 33 | for entry, item in items: 34 | if isinstance(item, MenuItem) and item.separator is True: 35 | self.entries.append(tuple([separator_tag, entry])) 36 | else: 37 | self.entries.append(tuple([str(tag), entry])) 38 | self.dispatch_table[str(tag)] = item 39 | tag += 1 40 | 41 | def function(self): 42 | code, tag = self.d.menu(self.title, choices=self.entries) 43 | if code == self.d.OK: self._dispatch(tag) 44 | 45 | def _dispatch(self, tag): 46 | if tag == separator_tag: self.run() 47 | elif tag in self.dispatch_table: 48 | func = self.dispatch_table[tag] 49 | if isinstance(func, MenuItem): 50 | func.run(ret=self.ret) 51 | else: func() 52 | 53 | class StatefulMenu(Menu): 54 | def __init__(self, dialog, items, title, position=0): 55 | super().__init__(dialog, items, title) 56 | self.position = position 57 | 58 | def advance(self): 59 | self.position += 1 60 | 61 | def function(self): 62 | code, tag = self.d.menu(self.title, choices=self.entries, 63 | default_item=self.entries[self.position][0]) 64 | 65 | if code == self.d.OK: 66 | if tag != separator_tag: self.position = int(tag) - 1 67 | self._dispatch(tag) 68 | -------------------------------------------------------------------------------- /kiwi/mount.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger() 3 | 4 | import subprocess 5 | 6 | def mountpoint(path): 7 | try: 8 | subprocess.check_call(['mountpoint', path], 9 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 10 | except subprocess.CalledProcessError: 11 | return False 12 | 13 | return True 14 | 15 | def unmount(path): 16 | subprocess.check_call(['umount', path], stdout=subprocess.PIPE, 17 | stderr=subprocess.PIPE) 18 | 19 | def mount(src, dst, options='', **kwargs): 20 | if kwargs.get('mkdir'): subprocess.check_call(['mkdir', '-p', dst]) 21 | 22 | if mountpoint(dst): 23 | logger.warning('Destination %s is already a mountpoint' % dst) 24 | if kwargs.get('force'): unmount(dst) 25 | else: return 26 | 27 | call = ['mount', src, dst] 28 | 29 | if kwargs.get('type'): call += ['-t', fs_type] 30 | 31 | if kwargs.get('bind'): options += ',bind' 32 | if kwargs.get('ro'): options += ',ro' 33 | 34 | if options: 35 | call.append('-o') 36 | call.append(options) 37 | 38 | subprocess.check_call(call, stdout=subprocess.PIPE, 39 | stderr=subprocess.PIPE) 40 | 41 | -------------------------------------------------------------------------------- /kiwi/wimlib.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | 3 | WIMLIB_IMAGEX_PATH = '/usr/bin/wimlib-imagex' 4 | 5 | def wiminfo(wim_path): 6 | images = [] 7 | 8 | index = 1 9 | while True: 10 | cmd = [WIMLIB_IMAGEX_PATH, 'info', wim_path, str(index)] 11 | p = Popen(cmd, stdout=PIPE, stderr=PIPE) 12 | output, err = p.communicate() 13 | if p.returncode != 0: break 14 | 15 | properties = {} 16 | image_info = output.decode('UTF-8').split('\n') 17 | for line in image_info: 18 | if ':' not in line: continue 19 | property, value = [token.strip() for token in line.split(':', maxsplit=1)] 20 | properties[property] = value 21 | 22 | images.append(properties) 23 | index += 1 24 | 25 | return images 26 | 27 | if __name__ == '__main__': 28 | print(wiminfo('/mnt/nfs/home/nfs/win7_x64_sp1.wim')) 29 | -------------------------------------------------------------------------------- /screenshots/editions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakogut/KiWI/3cbbb24166c2682b33fbc1ea40823a253fcdf585/screenshots/editions.png -------------------------------------------------------------------------------- /screenshots/extraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakogut/KiWI/3cbbb24166c2682b33fbc1ea40823a253fcdf585/screenshots/extraction.png -------------------------------------------------------------------------------- /screenshots/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakogut/KiWI/3cbbb24166c2682b33fbc1ea40823a253fcdf585/screenshots/menu.png -------------------------------------------------------------------------------- /screenshots/sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakogut/KiWI/3cbbb24166c2682b33fbc1ea40823a253fcdf585/screenshots/sources.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = 'KiWI', 5 | description = 'Killer Windows Installer - An alternative to WDS', 6 | license = 'MIT', 7 | version = '1', 8 | author = 'Joseph Kogut', 9 | author_email = 'joseph.kogut@gmail.com', 10 | url = 'http://github.com/jakogut/kiwi.git', 11 | packages = ['kiwi'], 12 | install_requires = [ 13 | 'pythondialog >= 3.3.0' 14 | ] 15 | ) 16 | --------------------------------------------------------------------------------