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