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