Add decorators defining sysprep params
[snf-image-creator] / image_creator / os_type / windows.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright 2012 GRNET S.A. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8 #
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12 #
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 #
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 """This module hosts OS-specific code common for the various Microsoft
37 Windows OSs."""
38
39 from image_creator.os_type import OSBase, sysprep, add_sysprep_param
40 from image_creator.util import FatalError, check_guestfs_version, get_command
41 from image_creator.winexe import WinEXE, WinexeTimeout
42
43 import hivex
44 import tempfile
45 import os
46 import signal
47 import time
48 import random
49 import string
50 import subprocess
51 import struct
52
53 BOOT_TIMEOUT = 300
54 SHUTDOWN_TIMEOUT = 120
55 CONNECTION_RETRIES = 5
56
57 # For more info see: http://technet.microsoft.com/en-us/library/jj612867.aspx
58 KMS_CLIENT_SETUP_KEYS = {
59     "Windows 8 Professional": "NG4HW-VH26C-733KW-K6F98-J8CK4",
60     "Windows 8 Professional N": "XCVCF-2NXM9-723PB-MHCB7-2RYQQ",
61     "Windows 8 Enterprise": "32JNW-9KQ84-P47T8-D8GGY-CWCK7",
62     "Windows 8 Enterprise N": "JMNMF-RHW7P-DMY6X-RF3DR-X2BQT",
63     "Windows Server 2012 Core": "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
64     "Windows Server 2012 Core N": "8N2M2-HWPGY-7PGT9-HGDD8-GVGGY",
65     "Windows Server 2012 Core Single Language":
66     "2WN2H-YGCQR-KFX6K-CD6TF-84YXQ",
67     "Windows Server 2012 Core Country Specific":
68     "4K36P-JN4VD-GDC6V-KDT89-DYFKP",
69     "Windows Server 2012 Server Standard": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
70     "Windows Server 2012 Standard Core": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
71     "Windows Server 2012 MultiPoint Standard": "HM7DN-YVMH3-46JC3-XYTG7-CYQJJ",
72     "Windows Server 2012 MultiPoint Premium": "XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G",
73     "Windows Server 2012 Datacenter": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
74     "Windows Server 2012 Datacenter Core": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
75     "Windows 7 Professional": "FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4",
76     "Windows 7 Professional N": "MRPKT-YTG23-K7D7T-X2JMM-QY7MG",
77     "Windows 7 Professional E": "W82YF-2Q76Y-63HXB-FGJG9-GF7QX",
78     "Windows 7 Enterprise": "33PXH-7Y6KF-2VJC9-XBBR8-HVTHH",
79     "Windows 7 Enterprise N": "YDRBP-3D83W-TY26F-D46B2-XCKRJ",
80     "Windows 7 Enterprise E": "C29WB-22CC8-VJ326-GHFJW-H9DH4",
81     "Windows Server 2008 R2 Web": "6TPJF-RBVHG-WBW2R-86QPH-6RTM4",
82     "Windows Server 2008 R2 HPC edition": "TT8MH-CG224-D3D7Q-498W2-9QCTX",
83     "Windows Server 2008 R2 Standard": "YC6KT-GKW9T-YTKYR-T4X34-R7VHC",
84     "Windows Server 2008 R2 Enterprise": "489J6-VHDMP-X63PK-3K798-CPX3Y",
85     "Windows Server 2008 R2 Datacenter": "74YFP-3QFB3-KQT8W-PMXWJ-7M648",
86     "Windows Server 2008 R2 for Itanium-based Systems":
87     "GT63C-RJFQ3-4GMB6-BRFB9-CB83V",
88     "Windows Vista Business": "YFKBB-PQJJV-G996G-VWGXY-2V3X8",
89     "Windows Vista Business N": "HMBQG-8H2RH-C77VX-27R82-VMQBT",
90     "Windows Vista Enterprise": "VKK3X-68KWM-X2YGT-QR4M6-4BWMV",
91     "Windows Vista Enterprise N": "VTC42-BM838-43QHV-84HX6-XJXKV",
92     "Windows Web Server 2008": "WYR28-R7TFJ-3X2YQ-YCY4H-M249D",
93     "Windows Server 2008 Standard": "TM24T-X9RMF-VWXK6-X8JC9-BFGM2",
94     "Windows Server 2008 Standard without Hyper-V":
95     "W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ",
96     "Windows Server 2008 Enterprise":
97     "YQGMW-MPWTJ-34KDK-48M3W-X4Q6V",
98     "Windows Server 2008 Enterprise without Hyper-V":
99     "39BXF-X8Q23-P2WWT-38T2F-G3FPG",
100     "Windows Server 2008 HPC": "RCTX3-KWVHP-BR6TB-RB6DM-6X7HP",
101     "Windows Server 2008 Datacenter": "7M67G-PC374-GR742-YH8V4-TCBY3",
102     "Windows Server 2008 Datacenter without Hyper-V":
103     "22XQ2-VRXRG-P8D42-K34TD-G3QQC",
104     "Windows Server 2008 for Itanium-Based Systems":
105     "4DWFP-JF3DJ-B7DTH-78FJB-PDRHK"}
106
107
108 class Windows(OSBase):
109     """OS class for Windows"""
110
111     @add_sysprep_param('password', 'Image Administrator Password', 20)
112     def __init__(self, image, **kargs):
113         super(Windows, self).__init__(image, **kargs)
114
115         device = self.g.part_to_dev(self.root)
116
117         self.last_part_num = self.g.part_list(device)[-1]['part_num']
118         self.last_drive = None
119         self.system_drive = None
120
121         for drive, partition in self.g.inspect_get_drive_mappings(self.root):
122             if partition == "%s%d" % (device, self.last_part_num):
123                 self.last_drive = drive
124             if partition == self.root:
125                 self.system_drive = drive
126
127         assert self.system_drive
128
129         self.product_name = self.g.inspect_get_product_name(self.root)
130
131     @sysprep('Disabling IPv6 privacy extensions')
132     def disable_ipv6_privacy_extensions(self):
133         """Disable IPv6 privacy extensions"""
134
135         self._guest_exec('netsh interface ipv6 set global '
136                          'randomizeidentifiers=disabled store=persistent')
137
138     @sysprep('Disabling Teredo interface')
139     def disable_teredo(self):
140         """Disable Teredo interface"""
141
142         self._guest_exec('netsh interface teredo set state disabled')
143
144     @sysprep('Disabling ISATAP Adapters')
145     def disable_isatap(self):
146         """Disable ISATAP Adapters"""
147
148         self._guest_exec('netsh interface isa set state disabled')
149
150     @sysprep('Enabling ping responses')
151     def enable_pings(self):
152         """Enable ping responses"""
153
154         self._guest_exec('netsh firewall set icmpsetting 8')
155
156     @sysprep('Disabling hibernation support')
157     def disable_hibernation(self):
158         """Disable hibernation support and remove the hibernation file"""
159
160         self._guest_exec(r'powercfg.exe /hibernate off')
161
162     @sysprep('Setting the system clock to UTC')
163     def utc(self):
164         """Set the hardware clock to UTC"""
165
166         path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
167         self._guest_exec(
168             r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
169
170     @sysprep('Clearing the event logs')
171     def clear_logs(self):
172         """Clear all the event logs"""
173
174         self._guest_exec(
175             r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
176
177     @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
178     def microsoft_sysprep(self):
179         """Run the Microsoft System Preparation Tool. This will remove
180         system-specific data and will make the image ready to be deployed.
181         After this no other task may run.
182         """
183
184         self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
185                          r'/quiet /generalize /oobe /shutdown')
186         self.syspreped = True
187
188     @sysprep('Converting the image into a KMS client', enabled=False)
189     def kms_client_setup(self):
190         """Install the appropriate KMS client setup key to the image to convert
191         it to a KMS client. Computers that are running volume licensing
192         editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
193         2008 R2, Windows Vista, and Windows Server 2008 are, by default, KMS
194         clients with no additional configuration needed.
195         """
196         try:
197             setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
198         except KeyError:
199             self.out.warn(
200                 "Don't know the KMS client setup key for product: `%s'" %
201                 self.product_name)
202             return
203
204         self._guest_exec(
205             "cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
206
207     @sysprep('Shrinking the last filesystem')
208     def shrink(self):
209         """Shrink the last filesystem. Make sure the filesystem is defragged"""
210
211         # Query for the maximum number of reclaimable bytes
212         cmd = (
213             r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
214             r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
215             'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
216             r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
217             r'ECHO EXIT >> %SCRIPT% & ' +
218             r'DISKPART /S %SCRIPT% & ' +
219             r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
220             r'DEL /Q %SCRIPT%"')
221
222         stdout, stderr, rc = self._guest_exec(cmd)
223
224         querymax = None
225         for line in stdout.splitlines():
226             # diskpart will return something like this:
227             #
228             #   The maximum number of reclaimable bytes is: xxxx MB
229             #
230             if line.find('reclaimable') >= 0:
231                 querymax = line.split(':')[1].split()[0].strip()
232                 assert querymax.isdigit(), \
233                     "Number of reclaimable bytes not a number"
234
235         if querymax is None:
236             FatalError("Error in shrinking! "
237                        "Couldn't find the max number of reclaimable bytes!")
238
239         querymax = int(querymax)
240         # From ntfsresize:
241         # Practically the smallest shrunken size generally is at around
242         # "used space" + (20-200 MB). Please also take into account that
243         # Windows might need about 50-100 MB free space left to boot safely.
244         # I'll give 100MB extra space just to be sure
245         querymax -= 100
246
247         if querymax < 0:
248             self.out.warn("Not enought available space to shrink the image!")
249             return
250
251         self.out.output("\tReclaiming %dMB ..." % querymax)
252
253         cmd = (
254             r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
255             r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
256             'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
257             'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
258             r'ECHO EXIT >> %SCRIPT% & ' +
259             r'DISKPART /S %SCRIPT% & ' +
260             r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
261             r'DEL /Q %SCRIPT%"')
262
263         stdout, stderr, rc = self._guest_exec(cmd)
264
265         for line in stdout.splitlines():
266             if line.find('shrunk') >= 0:
267                 self.out.output(line)
268
269     def do_sysprep(self):
270         """Prepare system for image creation."""
271
272         if getattr(self, 'syspreped', False):
273             raise FatalError("Image is already syspreped!")
274
275         txt = "System preparation parameter: `%s' is needed but missing!"
276         for param in self.needed_sysprep_params:
277             if param not in self.sysprep_params:
278                 raise FatalError(txt % param)
279
280         self.mount(readonly=False)
281         try:
282             disabled_uac = self._update_uac_remote_setting(1)
283             token = self._enable_os_monitor()
284
285             # disable the firewalls
286             firewall_states = self._update_firewalls(0, 0, 0)
287
288             # Delete the pagefile. It will be recreated when the system boots
289             systemroot = self.g.inspect_get_windows_systemroot(self.root)
290             pagefile = "%s/pagefile.sys" % systemroot
291             self.g.rm_rf(self.g.case_sensitive_path(pagefile))
292
293         finally:
294             self.umount()
295
296         self.out.output("Shutting down helper VM ...", False)
297         self.g.sync()
298         # guestfs_shutdown which is the prefered way to shutdown the backend
299         # process was introduced in version 1.19.16
300         if check_guestfs_version(self.g, 1, 19, 16) >= 0:
301             ret = self.g.shutdown()
302         else:
303             ret = self.g.kill_subprocess()
304
305         self.out.success('done')
306
307         vm = None
308         monitor = None
309         try:
310             self.out.output("Starting windows VM ...", False)
311             monitorfd, monitor = tempfile.mkstemp()
312             os.close(monitorfd)
313             vm = _VM(self.image.device, monitor)
314             self.out.success("started (console on vnc display: %d)." %
315                              vm.display)
316
317             self.out.output("Waiting for OS to boot ...", False)
318             self._wait_vm_boot(vm, monitor, token)
319             self.out.success('done')
320
321             self.out.output("Checking connectivity to the VM ...", False)
322             self._check_connectivity()
323             self.out.success('done')
324
325             self.out.output("Disabling automatic logon ...", False)
326             self._disable_autologon()
327             self.out.success('done')
328
329             self.out.output('Preparing system for image creation:')
330
331             tasks = self.list_syspreps()
332             enabled = filter(lambda x: x.enabled, tasks)
333             size = len(enabled)
334
335             # Make sure shrink runs in the end, before ms sysprep
336             enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
337                              enabled)
338
339             shrink_enabled = False
340             if len(enabled) != size:
341                 enabled.append(self.shrink)
342                 shrink_enabled = True
343
344             # Make sure the ms sysprep is the last task to run if it is enabled
345             enabled = filter(
346                 lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
347                 enabled)
348
349             ms_sysprep_enabled = False
350             if len(enabled) != size:
351                 enabled.append(self.microsoft_sysprep)
352                 ms_sysprep_enabled = True
353
354             cnt = 0
355             for task in enabled:
356                 cnt += 1
357                 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
358                 task()
359                 setattr(task.im_func, 'executed', True)
360
361             self.out.output("Sending shut down command ...", False)
362             if not ms_sysprep_enabled:
363                 self._shutdown()
364             self.out.success("done")
365
366             self.out.output("Waiting for windows to shut down ...", False)
367             vm.wait(SHUTDOWN_TIMEOUT)
368             self.out.success("done")
369         finally:
370             if monitor is not None:
371                 os.unlink(monitor)
372
373             try:
374                 if vm is not None:
375                     self.out.output("Destroying windows VM ...", False)
376                     vm.destroy()
377                     self.out.success("done")
378             finally:
379                 self.out.output("Relaunching helper VM (may take a while) ...",
380                                 False)
381                 self.g.launch()
382                 self.out.success('done')
383
384                 self.mount(readonly=False)
385                 try:
386                     if disabled_uac:
387                         self._update_uac_remote_setting(0)
388
389                     self._update_firewalls(*firewall_states)
390                 finally:
391                     self.umount()
392
393     def _shutdown(self):
394         """Shuts down the windows VM"""
395         self._guest_exec(r'shutdown /s /t 5')
396
397     def _wait_vm_boot(self, vm, fname, msg):
398         """Wait until a message appears on a file or the vm process dies"""
399
400         for i in range(BOOT_TIMEOUT):
401             time.sleep(1)
402             with open(fname) as f:
403                 for line in f:
404                     if line.startswith(msg):
405                         return True
406             if not vm.isalive():
407                 raise FatalError("Windows VM died unexpectedly!")
408
409         raise FatalError("Windows VM booting timed out!")
410
411     def _disable_autologon(self):
412         """Disable automatic logon on the windows image"""
413
414         winlogon = \
415             r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
416
417         self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
418         self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
419         self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
420
421     def _registry_file_path(self, regfile):
422         """Retrieves the case sensitive path to a registry file"""
423
424         systemroot = self.g.inspect_get_windows_systemroot(self.root)
425         path = "%s/system32/config/%s" % (systemroot, regfile)
426         try:
427             path = self.g.case_sensitive_path(path)
428         except RuntimeError as e:
429             raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
430                              % (regfile, str(e)))
431         return path
432
433     def _enable_os_monitor(self):
434         """Add a script in the registry that will send a random string to the
435         first serial port when the windows image finishes booting.
436         """
437
438         token = "".join(random.choice(string.ascii_letters) for x in range(16))
439
440         path = self._registry_file_path('SOFTWARE')
441         softwarefd, software = tempfile.mkstemp()
442         try:
443             os.close(softwarefd)
444             self.g.download(path, software)
445
446             h = hivex.Hivex(software, write=True)
447
448             # Enable automatic logon.
449             # This is needed because we need to execute a script that we add in
450             # the RunOnce registry entry and those programs only get executed
451             # when a user logs on. There is a RunServicesOnce registry entry
452             # whose keys get executed in the background when the logon dialog
453             # box first appears, but they seem to only work with services and
454             # not arbitrary command line expressions :-(
455             #
456             # Instructions on how to turn on automatic logon in Windows can be
457             # found here: http://support.microsoft.com/kb/324737
458             #
459             # Warning: Registry change will not work if the “Logon Banner” is
460             # defined on the server either by a Group Policy object (GPO) or by
461             # a local policy.
462
463             winlogon = h.root()
464             for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
465                           'Winlogon'):
466                 winlogon = h.node_get_child(winlogon, child)
467
468             h.node_set_value(
469                 winlogon,
470                 {'key': 'DefaultUserName', 't': 1,
471                  'value': "Administrator".encode('utf-16le')})
472             h.node_set_value(
473                 winlogon,
474                 {'key': 'DefaultPassword', 't': 1,
475                  'value':  self.sysprep_params['password'].encode('utf-16le')})
476             h.node_set_value(
477                 winlogon,
478                 {'key': 'AutoAdminLogon', 't': 1,
479                  'value': "1".encode('utf-16le')})
480
481             key = h.root()
482             for child in ('Microsoft', 'Windows', 'CurrentVersion'):
483                 key = h.node_get_child(key, child)
484
485             runonce = h.node_get_child(key, "RunOnce")
486             if runonce is None:
487                 runonce = h.node_add_child(key, "RunOnce")
488
489             value = (
490                 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
491                 r'-ExecutionPolicy RemoteSigned '
492                 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
493                 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
494                 r'$port.Close()}"').encode('utf-16le')
495
496             h.node_set_value(runonce,
497                              {'key': "BootMonitor", 't': 1, 'value': value})
498
499             value = (
500                 r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
501                 r'\policies\system /v LocalAccountTokenFilterPolicy'
502                 r' /t REG_DWORD /d 1 /f').encode('utf-16le')
503
504             h.node_set_value(runonce,
505                              {'key': "UpdateRegistry", 't': 1, 'value': value})
506
507             h.commit(None)
508
509             self.g.upload(software, path)
510         finally:
511             os.unlink(software)
512
513         return token
514
515     def _update_firewalls(self, domain, public, standard):
516         """Enables or disables the firewall for the Domain, the Public and the
517         Standard profile. Returns a triplete with the old values.
518
519         1 will enable a firewall and 0 will disable it
520         """
521
522         if domain not in (0, 1):
523             raise ValueError("Valid values for domain parameter are 0 and 1")
524
525         if public not in (0, 1):
526             raise ValueError("Valid values for public parameter are 0 and 1")
527
528         if standard not in (0, 1):
529             raise ValueError("Valid values for standard parameter are 0 and 1")
530
531         path = self._registry_file_path("SYSTEM")
532         systemfd, system = tempfile.mkstemp()
533         try:
534             os.close(systemfd)
535             self.g.download(path, system)
536
537             h = hivex.Hivex(system, write=True)
538
539             select = h.node_get_child(h.root(), 'Select')
540             current_value = h.node_get_value(select, 'Current')
541
542             # expecting a little endian dword
543             assert h.value_type(current_value)[1] == 4
544             current = "%03d" % h.value_dword(current_value)
545
546             firewall_policy = h.root()
547             for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
548                           'Parameters', 'FirewallPolicy'):
549                 firewall_policy = h.node_get_child(firewall_policy, child)
550
551             old_values = []
552             new_values = [domain, public, standard]
553             for profile in ('Domain', 'Public', 'Standard'):
554                 node = h.node_get_child(firewall_policy, '%sProfile' % profile)
555
556                 old_value = h.node_get_value(node, 'EnableFirewall')
557
558                 # expecting a little endian dword
559                 assert h.value_type(old_value)[1] == 4
560                 old_values.append(h.value_dword(old_value))
561
562                 h.node_set_value(
563                     node, {'key': 'EnableFirewall', 't': 4L,
564                            'value': struct.pack("<I", new_values.pop(0))})
565
566             h.commit(None)
567             self.g.upload(system, path)
568
569         finally:
570             os.unlink(system)
571
572         return old_values
573
574     def _update_uac_remote_setting(self, value):
575         """Updates the registry key value:
576         [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
577         \System]"LocalAccountTokenFilterPolicy"
578
579         value = 1 will disable the UAC remote restrictions
580         value = 0 will enable the UAC remote restrictions
581
582         For more info see here: http://support.microsoft.com/kb/951016
583
584         Returns:
585             True if the key is changed
586             False if the key is unchanged
587         """
588
589         if value not in (0, 1):
590             raise ValueError("Valid values for value parameter are 0 and 1")
591
592         path = self._registry_file_path('SOFTWARE')
593         softwarefd, software = tempfile.mkstemp()
594         try:
595             os.close(softwarefd)
596             self.g.download(path, software)
597
598             h = hivex.Hivex(software, write=True)
599
600             key = h.root()
601             for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
602                           'System'):
603                 key = h.node_get_child(key, child)
604
605             policy = None
606             for val in h.node_values(key):
607                 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
608                     policy = val
609
610             if policy is not None:
611                 dword = h.value_dword(policy)
612                 if dword == value:
613                     return False
614             elif value == 0:
615                 return False
616
617             new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
618                          'value': struct.pack("<I", value)}
619
620             h.node_set_value(key, new_value)
621             h.commit(None)
622
623             self.g.upload(software, path)
624
625         finally:
626             os.unlink(software)
627
628         return True
629
630     def _do_collect_metadata(self):
631         """Collect metadata about the OS"""
632         super(Windows, self)._do_collect_metadata()
633         self.meta["USERS"] = " ".join(self._get_users())
634
635     def _get_users(self):
636         """Returns a list of users found in the images"""
637         path = self._registry_file_path('SAM')
638         samfd, sam = tempfile.mkstemp()
639         try:
640             os.close(samfd)
641             self.g.download(path, sam)
642
643             h = hivex.Hivex(sam)
644
645             # Navigate to /SAM/Domains/Account/Users
646             users_node = h.root()
647             for child in ('SAM', 'Domains', 'Account', 'Users'):
648                 users_node = h.node_get_child(users_node, child)
649
650             # Navigate to /SAM/Domains/Account/Users/Names
651             names_node = h.node_get_child(users_node, 'Names')
652
653             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\%RID%
654             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\Names\%Username%
655             #
656             # The RID (relative identifier) of each user is stored as the type!
657             # (not the value) of the default key of the node under Names whose
658             # name is the user's username. Under the RID node, there in a F
659             # value that contains information about this user account.
660             #
661             # See sam.h of the chntpw project on how to translate the F value
662             # of an account in the registry. Bytes 56 & 57 are the account type
663             # and status flags. The first bit is the 'account disabled' bit
664             disabled = lambda f: int(f[56].encode('hex'), 16) & 0x01
665
666             users = []
667             for user_node in h.node_children(names_node):
668                 username = h.node_name(user_node)
669                 rid = h.value_type(h.node_get_value(user_node, ""))[0]
670                 # if RID is 500 (=0x1f4), the corresponding node name under
671                 # Users is '000001F4'
672                 key = ("%8.x" % rid).replace(' ', '0').upper()
673                 rid_node = h.node_get_child(users_node, key)
674                 f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
675
676                 if disabled(f_value):
677                     self.out.warn("Found disabled `%s' account!" % username)
678                     continue
679
680                 users.append(username)
681
682         finally:
683             os.unlink(sam)
684
685         # Filter out the guest account
686         return users
687
688     def _check_connectivity(self):
689         """Check if winexe works on the Windows VM"""
690
691         passwd = self.sysprep_params['password']
692         winexe = WinEXE('Administrator', passwd, 'localhost')
693         winexe.uninstall().debug(9)
694
695         for i in range(CONNECTION_RETRIES):
696             (stdout, stderr, rc) = winexe.run('cmd /C')
697             if rc == 0:
698                 return True
699             log = tempfile.NamedTemporaryFile(delete=False)
700             try:
701                 log.file.write(stdout)
702             finally:
703                 log.close()
704             self.out.output("failed! See: `%' for the full output" % log.name)
705             if i < CONNECTION_RETRIES - 1:
706                 self.out.output("Retrying ...", False)
707         raise FatalError("Connection to the VM failed after %d retries" %
708                          CONNECTION_RETRIES)
709
710     def _guest_exec(self, command, fatal=True):
711         """Execute a command on a windows VM"""
712
713         passwd = self.sysprep_params['password']
714
715         winexe = WinEXE('Administrator', passwd, 'localhost')
716         winexe.runas('Administrator', passwd).uninstall()
717
718         try:
719             (stdout, stderr, rc) = winexe.run(command)
720         except WinexeTimeout:
721             FatalError("Command: `%s' timeout out." % command)
722
723         if rc != 0 and fatal:
724             reason = stderr if len(stderr) else stdout
725             self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
726                             (command, rc, reason))
727             raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
728                              (command, rc, reason))
729
730         return (stdout, stderr, rc)
731
732
733 class _VM(object):
734     """Windows Virtual Machine"""
735     def __init__(self, disk, serial):
736         """Create _VM instance
737
738             disk: VM's hard disk
739             serial: File to save the output of the serial port
740         """
741
742         self.disk = disk
743         self.serial = serial
744
745         def random_mac():
746             mac = [0x00, 0x16, 0x3e,
747                    random.randint(0x00, 0x7f),
748                    random.randint(0x00, 0xff),
749                    random.randint(0x00, 0xff)]
750
751             return ':'.join(map(lambda x: "%02x" % x, mac))
752
753         # Use ganeti's VNC port range for a random vnc port
754         self.display = random.randint(11000, 14999) - 5900
755
756         args = [
757             'kvm', '-smp', '1', '-m', '1024', '-drive',
758             'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
759             '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
760             '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
761             '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
762             '-monitor', 'stdio']
763
764         self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
765                                         stdout=subprocess.PIPE)
766
767     def isalive(self):
768         """Check if the VM is still alive"""
769         return self.process.poll() is None
770
771     def destroy(self):
772         """Destroy the VM"""
773
774         if not self.isalive():
775             return
776
777         def handler(signum, frame):
778             self.process.terminate()
779             time.sleep(1)
780             if self.isalive():
781                 self.process.kill()
782             self.process.wait()
783             self.out.output("timed-out")
784             raise FatalError("VM destroy timed-out")
785
786         signal.signal(signal.SIGALRM, handler)
787
788         signal.alarm(SHUTDOWN_TIMEOUT)
789         self.process.communicate(input="system_powerdown\n")
790         signal.alarm(0)
791
792     def wait(self, timeout=0):
793         """Wait for the VM to terminate"""
794
795         def handler(signum, frame):
796             self.destroy()
797             raise FatalError("VM wait timed-out.")
798
799         signal.signal(signal.SIGALRM, handler)
800
801         signal.alarm(timeout)
802         stdout, stderr = self.process.communicate()
803         signal.alarm(0)
804
805         return (stdout, stderr, self.process.poll())
806
807 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :