a5ef78a79f4e443067060731047be5173164d33b
[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, part in self.image.g.inspect_get_drive_mappings(self.root):
126             if part == "%s%d" % (device, self.last_part_num):
127                 self.last_drive = drive
128             if part == 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.image.disable_guestfs()
305
306         vm = None
307         monitor = None
308         try:
309             self.out.output("Starting windows VM ...", False)
310             monitorfd, monitor = tempfile.mkstemp()
311             os.close(monitorfd)
312             vm = _VM(self.image.device, monitor, self.sysprep_params)
313             self.out.success("started (console on vnc display: %d)." %
314                              vm.display)
315
316             self.out.output("Waiting for OS to boot ...", False)
317             self._wait_vm_boot(vm, monitor, token)
318             self.out.success('done')
319
320             self.out.output("Checking connectivity to the VM ...", False)
321             self._check_connectivity()
322             self.out.success('done')
323
324             self.out.output("Disabling automatic logon ...", False)
325             self._disable_autologon()
326             self.out.success('done')
327
328             self.out.output('Preparing system for image creation:')
329
330             tasks = self.list_syspreps()
331             enabled = [task for task in tasks if task.enabled]
332             size = len(enabled)
333
334             # Make sure shrink runs in the end, before ms sysprep
335             enabled = [task for task in enabled if
336                        self.sysprep_info(task).name != 'shrink']
337
338             if len(enabled) != size:
339                 enabled.append(self.shrink)
340
341             # Make sure the ms sysprep is the last task to run if it is enabled
342             enabled = [task for task in enabled if
343                        self.sysprep_info(task).name != 'microsoft-sysprep']
344
345             ms_sysprep_enabled = False
346             if len(enabled) != size:
347                 enabled.append(self.microsoft_sysprep)
348                 ms_sysprep_enabled = True
349
350             cnt = 0
351             for task in enabled:
352                 cnt += 1
353                 self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
354                 task()
355                 setattr(task.im_func, 'executed', True)
356
357             self.out.output("Sending shut down command ...", False)
358             if not ms_sysprep_enabled:
359                 self._shutdown()
360             self.out.success("done")
361
362             self.out.output("Waiting for windows to shut down ...", False)
363             vm.wait(self.sysprep_params['shutdown_timeout'])
364             self.out.success("done")
365         finally:
366             if monitor is not None:
367                 os.unlink(monitor)
368
369             try:
370                 if vm is not None:
371                     self.out.output("Destroying windows VM ...", False)
372                     vm.destroy()
373                     self.out.success("done")
374             finally:
375                 self.image.enable_guestfs()
376
377                 self.mount(readonly=False)
378                 try:
379                     if disabled_uac:
380                         self._update_uac_remote_setting(0)
381
382                     self._update_firewalls(*firewall_states)
383                 finally:
384                     self.umount()
385
386     def _shutdown(self):
387         """Shuts down the windows VM"""
388         self._guest_exec(r'shutdown /s /t 5')
389
390     def _wait_vm_boot(self, vm, fname, msg):
391         """Wait until a message appears on a file or the vm process dies"""
392
393         for _ in range(self.sysprep_params['boot_timeout']):
394             time.sleep(1)
395             with open(fname) as f:
396                 for line in f:
397                     if line.startswith(msg):
398                         return True
399             if not vm.isalive():
400                 raise FatalError("Windows VM died unexpectedly!")
401
402         raise FatalError("Windows VM booting timed out!")
403
404     def _disable_autologon(self):
405         """Disable automatic logon on the windows image"""
406
407         winlogon = \
408             r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
409
410         self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
411         self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
412         self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
413
414     def _registry_file_path(self, regfile):
415         """Retrieves the case sensitive path to a registry file"""
416
417         systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
418         path = "%s/system32/config/%s" % (systemroot, regfile)
419         try:
420             path = self.image.g.case_sensitive_path(path)
421         except RuntimeError as error:
422             raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
423                              % (regfile, str(error)))
424         return path
425
426     def _enable_os_monitor(self):
427         """Add a script in the registry that will send a random string to the
428         first serial port when the windows image finishes booting.
429         """
430
431         token = "".join(random.choice(string.ascii_letters) for x in range(16))
432
433         path = self._registry_file_path('SOFTWARE')
434         softwarefd, software = tempfile.mkstemp()
435         try:
436             os.close(softwarefd)
437             self.image.g.download(path, software)
438
439             h = hivex.Hivex(software, write=True)
440
441             # Enable automatic logon.
442             # This is needed because we need to execute a script that we add in
443             # the RunOnce registry entry and those programs only get executed
444             # when a user logs on. There is a RunServicesOnce registry entry
445             # whose keys get executed in the background when the logon dialog
446             # box first appears, but they seem to only work with services and
447             # not arbitrary command line expressions :-(
448             #
449             # Instructions on how to turn on automatic logon in Windows can be
450             # found here: http://support.microsoft.com/kb/324737
451             #
452             # Warning: Registry change will not work if the “Logon Banner” is
453             # defined on the server either by a Group Policy object (GPO) or by
454             # a local policy.
455
456             winlogon = h.root()
457             for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
458                           'Winlogon'):
459                 winlogon = h.node_get_child(winlogon, child)
460
461             h.node_set_value(
462                 winlogon,
463                 {'key': 'DefaultUserName', 't': 1,
464                  'value': "Administrator".encode('utf-16le')})
465             h.node_set_value(
466                 winlogon,
467                 {'key': 'DefaultPassword', 't': 1,
468                  'value':  self.sysprep_params['password'].encode('utf-16le')})
469             h.node_set_value(
470                 winlogon,
471                 {'key': 'AutoAdminLogon', 't': 1,
472                  'value': "1".encode('utf-16le')})
473
474             key = h.root()
475             for child in ('Microsoft', 'Windows', 'CurrentVersion'):
476                 key = h.node_get_child(key, child)
477
478             runonce = h.node_get_child(key, "RunOnce")
479             if runonce is None:
480                 runonce = h.node_add_child(key, "RunOnce")
481
482             value = (
483                 r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
484                 r'-ExecutionPolicy RemoteSigned '
485                 r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
486                 r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
487                 r'$port.Close()}"').encode('utf-16le')
488
489             h.node_set_value(runonce,
490                              {'key': "BootMonitor", 't': 1, 'value': value})
491
492             value = (
493                 r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
494                 r'\policies\system /v LocalAccountTokenFilterPolicy'
495                 r' /t REG_DWORD /d 1 /f').encode('utf-16le')
496
497             h.node_set_value(runonce,
498                              {'key': "UpdateRegistry", 't': 1, 'value': value})
499
500             h.commit(None)
501
502             self.image.g.upload(software, path)
503         finally:
504             os.unlink(software)
505
506         return token
507
508     def _update_firewalls(self, domain, public, standard):
509         """Enables or disables the firewall for the Domain, the Public and the
510         Standard profile. Returns a triplete with the old values.
511
512         1 will enable a firewall and 0 will disable it
513         """
514
515         if domain not in (0, 1):
516             raise ValueError("Valid values for domain parameter are 0 and 1")
517
518         if public not in (0, 1):
519             raise ValueError("Valid values for public parameter are 0 and 1")
520
521         if standard not in (0, 1):
522             raise ValueError("Valid values for standard parameter are 0 and 1")
523
524         path = self._registry_file_path("SYSTEM")
525         systemfd, system = tempfile.mkstemp()
526         try:
527             os.close(systemfd)
528             self.image.g.download(path, system)
529
530             h = hivex.Hivex(system, write=True)
531
532             select = h.node_get_child(h.root(), 'Select')
533             current_value = h.node_get_value(select, 'Current')
534
535             # expecting a little endian dword
536             assert h.value_type(current_value)[1] == 4
537             current = "%03d" % h.value_dword(current_value)
538
539             firewall_policy = h.root()
540             for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
541                           'Parameters', 'FirewallPolicy'):
542                 firewall_policy = h.node_get_child(firewall_policy, child)
543
544             old_values = []
545             new_values = [domain, public, standard]
546             for profile in ('Domain', 'Public', 'Standard'):
547                 node = h.node_get_child(firewall_policy, '%sProfile' % profile)
548
549                 old_value = h.node_get_value(node, 'EnableFirewall')
550
551                 # expecting a little endian dword
552                 assert h.value_type(old_value)[1] == 4
553                 old_values.append(h.value_dword(old_value))
554
555                 h.node_set_value(
556                     node, {'key': 'EnableFirewall', 't': 4L,
557                            'value': struct.pack("<I", new_values.pop(0))})
558
559             h.commit(None)
560             self.image.g.upload(system, path)
561
562         finally:
563             os.unlink(system)
564
565         return old_values
566
567     def _update_uac_remote_setting(self, value):
568         """Updates the registry key value:
569         [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
570         \System]"LocalAccountTokenFilterPolicy"
571
572         value = 1 will disable the UAC remote restrictions
573         value = 0 will enable the UAC remote restrictions
574
575         For more info see here: http://support.microsoft.com/kb/951016
576
577         Returns:
578             True if the key is changed
579             False if the key is unchanged
580         """
581
582         if value not in (0, 1):
583             raise ValueError("Valid values for value parameter are 0 and 1")
584
585         path = self._registry_file_path('SOFTWARE')
586         softwarefd, software = tempfile.mkstemp()
587         try:
588             os.close(softwarefd)
589             self.image.g.download(path, software)
590
591             h = hivex.Hivex(software, write=True)
592
593             key = h.root()
594             for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
595                           'System'):
596                 key = h.node_get_child(key, child)
597
598             policy = None
599             for val in h.node_values(key):
600                 if h.value_key(val) == "LocalAccountTokenFilterPolicy":
601                     policy = val
602
603             if policy is not None:
604                 dword = h.value_dword(policy)
605                 if dword == value:
606                     return False
607             elif value == 0:
608                 return False
609
610             new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
611                          'value': struct.pack("<I", value)}
612
613             h.node_set_value(key, new_value)
614             h.commit(None)
615
616             self.image.g.upload(software, path)
617
618         finally:
619             os.unlink(software)
620
621         return True
622
623     def _do_collect_metadata(self):
624         """Collect metadata about the OS"""
625         super(Windows, self)._do_collect_metadata()
626         self.meta["USERS"] = " ".join(self._get_users())
627
628     def _get_users(self):
629         """Returns a list of users found in the images"""
630         samfd, sam = tempfile.mkstemp()
631         try:
632             os.close(samfd)
633             self.image.g.download(self._registry_file_path('SAM'), sam)
634
635             h = hivex.Hivex(sam)
636
637             # Navigate to /SAM/Domains/Account/Users
638             users_node = h.root()
639             for child in ('SAM', 'Domains', 'Account', 'Users'):
640                 users_node = h.node_get_child(users_node, child)
641
642             # Navigate to /SAM/Domains/Account/Users/Names
643             names_node = h.node_get_child(users_node, 'Names')
644
645             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\%RID%
646             # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\Names\%Username%
647             #
648             # The RID (relative identifier) of each user is stored as the type!
649             # (not the value) of the default key of the node under Names whose
650             # name is the user's username. Under the RID node, there in a F
651             # value that contains information about this user account.
652             #
653             # See sam.h of the chntpw project on how to translate the F value
654             # of an account in the registry. Bytes 56 & 57 are the account type
655             # and status flags. The first bit is the 'account disabled' bit
656             disabled = lambda f: int(f[56].encode('hex'), 16) & 0x01
657
658             users = []
659             for user_node in h.node_children(names_node):
660                 username = h.node_name(user_node)
661                 rid = h.value_type(h.node_get_value(user_node, ""))[0]
662                 # if RID is 500 (=0x1f4), the corresponding node name under
663                 # Users is '000001F4'
664                 key = ("%8.x" % rid).replace(' ', '0').upper()
665                 rid_node = h.node_get_child(users_node, key)
666                 f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
667
668                 if disabled(f_value):
669                     self.out.warn("Found disabled `%s' account!" % username)
670                     continue
671
672                 users.append(username)
673
674         finally:
675             os.unlink(sam)
676
677         # Filter out the guest account
678         return users
679
680     def _check_connectivity(self):
681         """Check if winexe works on the Windows VM"""
682
683         retries = self.sysprep_params['connection_retries']
684         # If the connection_retries parameter is set to 0 disable the
685         # connectivity check
686         if retries == 0:
687             return True
688
689         passwd = self.sysprep_params['password']
690         winexe = WinEXE('Administrator', passwd, 'localhost')
691         winexe.uninstall().debug(9)
692
693         for i in range(retries):
694             (stdout, stderr, rc) = winexe.run('cmd /C')
695             if rc == 0:
696                 return True
697             log = tempfile.NamedTemporaryFile(delete=False)
698             try:
699                 log.file.write(stdout)
700             finally:
701                 log.close()
702             self.out.output("failed! See: `%s' for the full output" % log.name)
703             if i < retries - 1:
704                 self.out.output("retrying ...", False)
705
706         raise FatalError("Connection to the Windows VM failed after %d retries"
707                          % retries)
708
709     def _guest_exec(self, command, fatal=True):
710         """Execute a command on a windows VM"""
711
712         passwd = self.sysprep_params['password']
713
714         winexe = WinEXE('Administrator', passwd, 'localhost')
715         winexe.runas('Administrator', passwd).uninstall()
716
717         try:
718             (stdout, stderr, rc) = winexe.run(command)
719         except WinexeTimeout:
720             FatalError("Command: `%s' timeout out." % command)
721
722         if rc != 0 and fatal:
723             reason = stderr if len(stderr) else stdout
724             self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
725                             (command, rc, reason))
726             raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
727                              (command, rc, reason))
728
729         return (stdout, stderr, rc)
730
731
732 class _VM(object):
733     """Windows Virtual Machine"""
734     def __init__(self, disk, serial, params):
735         """Create _VM instance
736
737             disk: VM's hard disk
738             serial: File to save the output of the serial port
739         """
740
741         self.disk = disk
742         self.serial = serial
743         self.params = params
744
745         def random_mac():
746             """creates a random mac address"""
747             mac = [0x00, 0x16, 0x3e,
748                    random.randint(0x00, 0x7f),
749                    random.randint(0x00, 0xff),
750                    random.randint(0x00, 0xff)]
751
752             return ':'.join(['%02x' % x for x in mac])
753
754         # Use ganeti's VNC port range for a random vnc port
755         self.display = random.randint(11000, 14999) - 5900
756
757         kvm = get_kvm_binary()
758
759         if kvm is None:
760             FatalError("Can't find the kvm binary")
761
762         args = [
763             kvm, '-smp', '1', '-m', '1024', '-drive',
764             'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
765             '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
766             '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
767             '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
768             '-monitor', 'stdio']
769
770         self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
771                                         stdout=subprocess.PIPE)
772
773     def isalive(self):
774         """Check if the VM is still alive"""
775         return self.process.poll() is None
776
777     def destroy(self):
778         """Destroy the VM"""
779
780         if not self.isalive():
781             return
782
783         def handler(signum, frame):
784             self.process.terminate()
785             time.sleep(1)
786             if self.isalive():
787                 self.process.kill()
788             self.process.wait()
789             raise FatalError("VM destroy timed-out")
790
791         signal.signal(signal.SIGALRM, handler)
792
793         signal.alarm(self.params['shutdown_timeout'])
794         self.process.communicate(input="system_powerdown\n")
795         signal.alarm(0)
796
797     def wait(self, timeout=0):
798         """Wait for the VM to terminate"""
799
800         def handler(signum, frame):
801             self.destroy()
802             raise FatalError("VM wait timed-out.")
803
804         signal.signal(signal.SIGALRM, handler)
805
806         signal.alarm(timeout)
807         stdout, stderr = self.process.communicate()
808         signal.alarm(0)
809
810         return (stdout, stderr, self.process.poll())
811
812 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :