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