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