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