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