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