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