5110e306e94c1a2a45f957434ca45167e82851e7
[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
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', None, 'Image Administrator Password')
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         self.syspreped = False
131
132     @sysprep('Disabling IPv6 privacy extensions')
133     def disable_ipv6_privacy_extensions(self):
134         """Disable IPv6 privacy extensions"""
135
136         self._guest_exec('netsh interface ipv6 set global '
137                          'randomizeidentifiers=disabled store=persistent')
138
139     @sysprep('Disabling Teredo interface')
140     def disable_teredo(self):
141         """Disable Teredo interface"""
142
143         self._guest_exec('netsh interface teredo set state disabled')
144
145     @sysprep('Disabling ISATAP Adapters')
146     def disable_isatap(self):
147         """Disable ISATAP Adapters"""
148
149         self._guest_exec('netsh interface isa set state disabled')
150
151     @sysprep('Enabling ping responses')
152     def enable_pings(self):
153         """Enable ping responses"""
154
155         self._guest_exec('netsh firewall set icmpsetting 8')
156
157     @sysprep('Disabling hibernation support')
158     def disable_hibernation(self):
159         """Disable hibernation support and remove the hibernation file"""
160
161         self._guest_exec(r'powercfg.exe /hibernate off')
162
163     @sysprep('Setting the system clock to UTC')
164     def utc(self):
165         """Set the hardware clock to UTC"""
166
167         path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
168         self._guest_exec(
169             r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
170
171     @sysprep('Clearing the event logs')
172     def clear_logs(self):
173         """Clear all the event logs"""
174
175         self._guest_exec(
176             r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
177
178     @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
179     def microsoft_sysprep(self):
180         """Run the Microsoft System Preparation Tool. This will remove
181         system-specific data and will make the image ready to be deployed.
182         After this no other task may run.
183         """
184
185         self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
186                          r'/quiet /generalize /oobe /shutdown')
187         self.syspreped = True
188
189     @sysprep('Converting the image into a KMS client', enabled=False)
190     def kms_client_setup(self):
191         """Install the appropriate KMS client setup key to the image to convert
192         it to a KMS client. Computers that are running volume licensing
193         editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
194         2008 R2, Windows Vista, and Windows Server 2008 are, by default, KMS
195         clients with no additional configuration needed.
196         """
197         try:
198             setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
199         except KeyError:
200             self.out.warn(
201                 "Don't know the KMS client setup key for product: `%s'" %
202                 self.product_name)
203             return
204
205         self._guest_exec(
206             r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
207
208     @sysprep('Shrinking the last filesystem')
209     def shrink(self):
210         """Shrink the last filesystem. Make sure the filesystem is defragged"""
211
212         # Query for the maximum number of reclaimable bytes
213         cmd = (
214             r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
215             r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
216             'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
217             r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
218             r'ECHO EXIT >> %SCRIPT% & ' +
219             r'DISKPART /S %SCRIPT% & ' +
220             r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
221             r'DEL /Q %SCRIPT%"')
222
223         stdout, stderr, rc = self._guest_exec(cmd)
224
225         querymax = None
226         for line in stdout.splitlines():
227             # diskpart will return something like this:
228             #
229             #   The maximum number of reclaimable bytes is: xxxx MB
230             #
231             if line.find('reclaimable') >= 0:
232                 querymax = line.split(':')[1].split()[0].strip()
233                 assert querymax.isdigit(), \
234                     "Number of reclaimable bytes not a number"
235
236         if querymax is None:
237             FatalError("Error in shrinking! "
238                        "Couldn't find the max number of reclaimable bytes!")
239
240         querymax = int(querymax)
241         # From ntfsresize:
242         # Practically the smallest shrunken size generally is at around
243         # "used space" + (20-200 MB). Please also take into account that
244         # Windows might need about 50-100 MB free space left to boot safely.
245         # I'll give 100MB extra space just to be sure
246         querymax -= 100
247
248         if querymax < 0:
249             self.out.warn("Not enought available space to shrink the image!")
250             return
251
252         self.out.output("\tReclaiming %dMB ..." % querymax)
253
254         cmd = (
255             r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
256             r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
257             'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
258             'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
259             r'ECHO EXIT >> %SCRIPT% & ' +
260             r'DISKPART /S %SCRIPT% & ' +
261             r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
262             r'DEL /Q %SCRIPT%"')
263
264         stdout, stderr, rc = self._guest_exec(cmd)
265
266         for line in stdout.splitlines():
267             if line.find('shrunk') >= 0:
268                 self.out.output(line)
269
270     def do_sysprep(self):
271         """Prepare system for image creation."""
272
273         if getattr(self, 'syspreped', False):
274             raise FatalError("Image is already syspreped!")
275
276         txt = "System preparation parameter: `%s' is needed but missing!"
277         for name, param in self.needed_sysprep_params.items():
278             if name not in self.sysprep_params:
279                 raise FatalError(txt % param)
280
281         self.mount(readonly=False)
282         try:
283             disabled_uac = self._update_uac_remote_setting(1)
284             token = self._enable_os_monitor()
285
286             # disable the firewalls
287             firewall_states = self._update_firewalls(0, 0, 0)
288
289             # Delete the pagefile. It will be recreated when the system boots
290             systemroot = self.g.inspect_get_windows_systemroot(self.root)
291             pagefile = "%s/pagefile.sys" % systemroot
292             self.g.rm_rf(self.g.case_sensitive_path(pagefile))
293
294         finally:
295             self.umount()
296
297         self.out.output("Shutting down helper VM ...", False)
298         self.g.sync()
299         # guestfs_shutdown which is the prefered way to shutdown the backend
300         # process was introduced in version 1.19.16
301         if check_guestfs_version(self.g, 1, 19, 16) >= 0:
302             self.g.shutdown()
303         else:
304             self.g.kill_subprocess()
305
306         self.out.success('done')
307
308         vm = None
309         monitor = None
310         try:
311             self.out.output("Starting windows VM ...", False)
312             monitorfd, monitor = tempfile.mkstemp()
313             os.close(monitorfd)
314             vm = _VM(self.image.device, monitor)
315             self.out.success("started (console on vnc display: %d)." %
316                              vm.display)
317
318             self.out.output("Waiting for OS to boot ...", False)
319             self._wait_vm_boot(vm, monitor, token)
320             self.out.success('done')
321
322             self.out.output("Checking connectivity to the VM ...", False)
323             self._check_connectivity()
324             self.out.success('done')
325
326             self.out.output("Disabling automatic logon ...", False)
327             self._disable_autologon()
328             self.out.success('done')
329
330             self.out.output('Preparing system for image creation:')
331
332             tasks = self.list_syspreps()
333             enabled = [task for task in tasks if task.enabled]
334             size = len(enabled)
335
336             # Make sure shrink runs in the end, before ms sysprep
337             enabled = [task for task in enabled if
338                        self.sysprep_info(task).name != 'shrink']
339
340             if len(enabled) != size:
341                 enabled.append(self.shrink)
342
343             # Make sure the ms sysprep is the last task to run if it is enabled
344             enabled = [task for task in enabled if
345                        self.sysprep_info(task).name != 'microsoft-sysprep']
346
347             ms_sysprep_enabled = False
348             if len(enabled) != size:
349                 enabled.append(self.microsoft_sysprep)
350                 ms_sysprep_enabled = True
351
352             cnt = 0
353             for task in enabled:
354                 cnt += 1
355                 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
356                 task()
357                 setattr(task.im_func, 'executed', True)
358
359             self.out.output("Sending shut down command ...", False)
360             if not ms_sysprep_enabled:
361                 self._shutdown()
362             self.out.success("done")
363
364             self.out.output("Waiting for windows to shut down ...", False)
365             vm.wait(SHUTDOWN_TIMEOUT)
366             self.out.success("done")
367         finally:
368             if monitor is not None:
369                 os.unlink(monitor)
370
371             try:
372                 if vm is not None:
373                     self.out.output("Destroying windows VM ...", False)
374                     vm.destroy()
375                     self.out.success("done")
376             finally:
377                 self.out.output("Relaunching helper VM (may take a while) ...",
378                                 False)
379                 self.g.launch()
380                 self.out.success('done')
381
382                 self.mount(readonly=False)
383                 try:
384                     if disabled_uac:
385                         self._update_uac_remote_setting(0)
386
387                     self._update_firewalls(*firewall_states)
388                 finally:
389                     self.umount()
390
391     def _shutdown(self):
392         """Shuts down the windows VM"""
393         self._guest_exec(r'shutdown /s /t 5')
394
395     def _wait_vm_boot(self, vm, fname, msg):
396         """Wait until a message appears on a file or the vm process dies"""
397
398         for _ in range(BOOT_TIMEOUT):
399             time.sleep(1)
400             with open(fname) as f:
401                 for line in f:
402                     if line.startswith(msg):
403                         return True
404             if not vm.isalive():
405                 raise FatalError("Windows VM died unexpectedly!")
406
407         raise FatalError("Windows VM booting timed out!")
408
409     def _disable_autologon(self):
410         """Disable automatic logon on the windows image"""
411
412         winlogon = \
413             r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
414
415         self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
416         self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
417         self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
418
419     def _registry_file_path(self, regfile):
420         """Retrieves the case sensitive path to a registry file"""
421
422         systemroot = self.g.inspect_get_windows_systemroot(self.root)
423         path = "%s/system32/config/%s" % (systemroot, regfile)
424         try:
425             path = self.g.case_sensitive_path(path)
426         except RuntimeError as error:
427             raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
428                              % (regfile, str(error)))
429         return path
430
431     def _enable_os_monitor(self):
432         """Add a script in the registry that will send a random string to the
433         first serial port when the windows image finishes booting.
434         """
435
436         token = "".join(random.choice(string.ascii_letters) for x in range(16))
437
438         path = self._registry_file_path('SOFTWARE')
439         softwarefd, software = tempfile.mkstemp()
440         try:
441             os.close(softwarefd)
442             self.g.download(path, software)
443
444             h = hivex.Hivex(software, write=True)
445
446             # Enable automatic logon.
447             # This is needed because we need to execute a script that we add in
448             # the RunOnce registry entry and those programs only get executed
449             # when a user logs on. There is a RunServicesOnce registry entry
450             # whose keys get executed in the background when the logon dialog
451             # box first appears, but they seem to only work with services and
452             # not arbitrary command line expressions :-(
453             #
454             # Instructions on how to turn on automatic logon in Windows can be
455             # found here: http://support.microsoft.com/kb/324737
456             #
457             # Warning: Registry change will not work if the “Logon Banner” is
458             # defined on the server either by a Group Policy object (GPO) or by
459             # a local policy.
460
461             winlogon = h.root()
462             for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
463                           'Winlogon'):
464                 winlogon = h.node_get_child(winlogon, child)
465
466             h.node_set_value(
467                 winlogon,
468                 {'key': 'DefaultUserName', 't': 1,
469                  'value': "Administrator".encode('utf-16le')})
470             h.node_set_value(
471                 winlogon,
472                 {'key': 'DefaultPassword', 't': 1,
473                  'value':  self.sysprep_params['password'].encode('utf-16le')})
474             h.node_set_value(
475                 winlogon,
476                 {'key': 'AutoAdminLogon', 't': 1,
477                  'value': "1".encode('utf-16le')})
478
479             key = h.root()
480             for child in ('Microsoft', 'Windows', 'CurrentVersion'):
481                 key = h.node_get_child(key, child)
482
483             runonce = h.node_get_child(key, "RunOnce")
484             if runonce is None:
485                 runonce = h.node_add_child(key, "RunOnce")
486
487             value = (
488                 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
489                 r'-ExecutionPolicy RemoteSigned '
490                 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
491                 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
492                 r'$port.Close()}"').encode('utf-16le')
493
494             h.node_set_value(runonce,
495                              {'key': "BootMonitor", 't': 1, 'value': value})
496
497             value = (
498                 r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
499                 r'\policies\system /v LocalAccountTokenFilterPolicy'
500                 r' /t REG_DWORD /d 1 /f').encode('utf-16le')
501
502             h.node_set_value(runonce,
503                              {'key': "UpdateRegistry", 't': 1, 'value': value})
504
505             h.commit(None)
506
507             self.g.upload(software, path)
508         finally:
509             os.unlink(software)
510
511         return token
512
513     def _update_firewalls(self, domain, public, standard):
514         """Enables or disables the firewall for the Domain, the Public and the
515         Standard profile. Returns a triplete with the old values.
516
517         1 will enable a firewall and 0 will disable it
518         """
519
520         if domain not in (0, 1):
521             raise ValueError("Valid values for domain parameter are 0 and 1")
522
523         if public not in (0, 1):
524             raise ValueError("Valid values for public parameter are 0 and 1")
525
526         if standard not in (0, 1):
527             raise ValueError("Valid values for standard parameter are 0 and 1")
528
529         path = self._registry_file_path("SYSTEM")
530         systemfd, system = tempfile.mkstemp()
531         try:
532             os.close(systemfd)
533             self.g.download(path, system)
534
535             h = hivex.Hivex(system, write=True)
536
537             select = h.node_get_child(h.root(), 'Select')
538             current_value = h.node_get_value(select, 'Current')
539
540             # expecting a little endian dword
541             assert h.value_type(current_value)[1] == 4
542             current = "%03d" % h.value_dword(current_value)
543
544             firewall_policy = h.root()
545             for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
546                           'Parameters', 'FirewallPolicy'):
547                 firewall_policy = h.node_get_child(firewall_policy, child)
548
549             old_values = []
550             new_values = [domain, public, standard]
551             for profile in ('Domain', 'Public', 'Standard'):
552                 node = h.node_get_child(firewall_policy, '%sProfile' % profile)
553
554                 old_value = h.node_get_value(node, 'EnableFirewall')
555
556                 # expecting a little endian dword
557                 assert h.value_type(old_value)[1] == 4
558                 old_values.append(h.value_dword(old_value))
559
560                 h.node_set_value(
561                     node, {'key': 'EnableFirewall', 't': 4L,
562                            'value': struct.pack("<I", new_values.pop(0))})
563
564             h.commit(None)
565             self.g.upload(system, path)
566
567         finally:
568             os.unlink(system)
569
570         return old_values
571
572     def _update_uac_remote_setting(self, value):
573         """Updates the registry key value:
574         [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
575         \System]"LocalAccountTokenFilterPolicy"
576
577         value = 1 will disable the UAC remote restrictions
578         value = 0 will enable the UAC remote restrictions
579
580         For more info see here: http://support.microsoft.com/kb/951016
581
582         Returns:
583             True if the key is changed
584             False if the key is unchanged
585         """
586
587         if value not in (0, 1):
588             raise ValueError("Valid values for value parameter are 0 and 1")
589
590         path = self._registry_file_path('SOFTWARE')
591         softwarefd, software = tempfile.mkstemp()
592         try:
593             os.close(softwarefd)
594             self.g.download(path, software)
595
596             h = hivex.Hivex(software, write=True)
597
598             key = h.root()
599             for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
600                           'System'):
601                 key = h.node_get_child(key, child)
602
603             policy = None
604             for val in h.node_values(key):
605                 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
606                     policy = val
607
608             if policy is not None:
609                 dword = h.value_dword(policy)
610                 if dword == value:
611                     return False
612             elif value == 0:
613                 return False
614
615             new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
616                          'value': struct.pack("<I", value)}
617
618             h.node_set_value(key, new_value)
619             h.commit(None)
620
621             self.g.upload(software, path)
622
623         finally:
624             os.unlink(software)
625
626         return True
627
628     def _do_collect_metadata(self):
629         """Collect metadata about the OS"""
630         super(Windows, self)._do_collect_metadata()
631         self.meta["USERS"] = " ".join(self._get_users())
632
633     def _get_users(self):
634         """Returns a list of users found in the images"""
635         samfd, sam = tempfile.mkstemp()
636         try:
637             os.close(samfd)
638             self.g.download(self._registry_file_path('SAM'), sam)
639
640             h = hivex.Hivex(sam)
641
642             # Navigate to /SAM/Domains/Account/Users
643             users_node = h.root()
644             for child in ('SAM', 'Domains', 'Account', 'Users'):
645                 users_node = h.node_get_child(users_node, child)
646
647             # Navigate to /SAM/Domains/Account/Users/Names
648             names_node = h.node_get_child(users_node, 'Names')
649
650             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\%RID%
651             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\Names\%Username%
652             #
653             # The RID (relative identifier) of each user is stored as the type!
654             # (not the value) of the default key of the node under Names whose
655             # name is the user's username. Under the RID node, there in a F
656             # value that contains information about this user account.
657             #
658             # See sam.h of the chntpw project on how to translate the F value
659             # of an account in the registry. Bytes 56 & 57 are the account type
660             # and status flags. The first bit is the 'account disabled' bit
661             disabled = lambda f: int(f[56].encode('hex'), 16) & 0x01
662
663             users = []
664             for user_node in h.node_children(names_node):
665                 username = h.node_name(user_node)
666                 rid = h.value_type(h.node_get_value(user_node, ""))[0]
667                 # if RID is 500 (=0x1f4), the corresponding node name under
668                 # Users is '000001F4'
669                 key = ("%8.x" % rid).replace(' ', '0').upper()
670                 rid_node = h.node_get_child(users_node, key)
671                 f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
672
673                 if disabled(f_value):
674                     self.out.warn("Found disabled `%s' account!" % username)
675                     continue
676
677                 users.append(username)
678
679         finally:
680             os.unlink(sam)
681
682         # Filter out the guest account
683         return users
684
685     def _check_connectivity(self):
686         """Check if winexe works on the Windows VM"""
687
688         passwd = self.sysprep_params['password']
689         winexe = WinEXE('Administrator', passwd, 'localhost')
690         winexe.uninstall().debug(9)
691
692         for i in range(CONNECTION_RETRIES):
693             (stdout, stderr, rc) = winexe.run('cmd /C')
694             if rc == 0:
695                 return True
696             log = tempfile.NamedTemporaryFile(delete=False)
697             try:
698                 log.file.write(stdout)
699             finally:
700                 log.close()
701             self.out.output("failed! See: `%s' for the full output" % log.name)
702             if i < CONNECTION_RETRIES - 1:
703                 self.out.output("Retrying ...", False)
704         raise FatalError("Connection to the VM failed after %d retries" %
705                          CONNECTION_RETRIES)
706
707     def _guest_exec(self, command, fatal=True):
708         """Execute a command on a windows VM"""
709
710         passwd = self.sysprep_params['password']
711
712         winexe = WinEXE('Administrator', passwd, 'localhost')
713         winexe.runas('Administrator', passwd).uninstall()
714
715         try:
716             (stdout, stderr, rc) = winexe.run(command)
717         except WinexeTimeout:
718             FatalError("Command: `%s' timeout out." % command)
719
720         if rc != 0 and fatal:
721             reason = stderr if len(stderr) else stdout
722             self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
723                             (command, rc, reason))
724             raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
725                              (command, rc, reason))
726
727         return (stdout, stderr, rc)
728
729
730 class _VM(object):
731     """Windows Virtual Machine"""
732     def __init__(self, disk, serial):
733         """Create _VM instance
734
735             disk: VM's hard disk
736             serial: File to save the output of the serial port
737         """
738
739         self.disk = disk
740         self.serial = serial
741
742         def random_mac():
743             """creates a random mac address"""
744             mac = [0x00, 0x16, 0x3e,
745                    random.randint(0x00, 0x7f),
746                    random.randint(0x00, 0xff),
747                    random.randint(0x00, 0xff)]
748
749             return ':'.join(['%02x' % x for x in mac])
750
751         # Use ganeti's VNC port range for a random vnc port
752         self.display = random.randint(11000, 14999) - 5900
753
754         args = [
755             'kvm', '-smp', '1', '-m', '1024', '-drive',
756             'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
757             '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
758             '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
759             '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
760             '-monitor', 'stdio']
761
762         self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
763                                         stdout=subprocess.PIPE)
764
765     def isalive(self):
766         """Check if the VM is still alive"""
767         return self.process.poll() is None
768
769     def destroy(self):
770         """Destroy the VM"""
771
772         if not self.isalive():
773             return
774
775         def handler(signum, frame):
776             self.process.terminate()
777             time.sleep(1)
778             if self.isalive():
779                 self.process.kill()
780             self.process.wait()
781             raise FatalError("VM destroy timed-out")
782
783         signal.signal(signal.SIGALRM, handler)
784
785         signal.alarm(SHUTDOWN_TIMEOUT)
786         self.process.communicate(input="system_powerdown\n")
787         signal.alarm(0)
788
789     def wait(self, timeout=0):
790         """Wait for the VM to terminate"""
791
792         def handler(signum, frame):
793             self.destroy()
794             raise FatalError("VM wait timed-out.")
795
796         signal.signal(signal.SIGALRM, handler)
797
798         signal.alarm(timeout)
799         stdout, stderr = self.process.communicate()
800         signal.alarm(0)
801
802         return (stdout, stderr, self.process.poll())
803
804 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :