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