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