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