Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 9b49a63b

History | View | Annotate | Download (31.4 kB)

1
# -*- coding: utf-8 -*-
2
#
3
# Copyright 2012 GRNET S.A. All rights reserved.
4
#
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
#
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
#
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
#
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
#
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35

    
36
"""This module hosts OS-specific code common for the various Microsoft
37
Windows OSs."""
38

    
39
from image_creator.os_type import OSBase, sysprep, add_sysprep_param
40
from image_creator.util import FatalError, get_kvm_binary
41
from image_creator.winexe import WinEXE, WinexeTimeout
42

    
43
import hivex
44
import tempfile
45
import os
46
import signal
47
import time
48
import random
49
import string
50
import subprocess
51
import struct
52

    
53
# For more info see: http://technet.microsoft.com/en-us/library/jj612867.aspx
54
KMS_CLIENT_SETUP_KEYS = {
55
    "Windows 8 Professional": "NG4HW-VH26C-733KW-K6F98-J8CK4",
56
    "Windows 8 Professional N": "XCVCF-2NXM9-723PB-MHCB7-2RYQQ",
57
    "Windows 8 Enterprise": "32JNW-9KQ84-P47T8-D8GGY-CWCK7",
58
    "Windows 8 Enterprise N": "JMNMF-RHW7P-DMY6X-RF3DR-X2BQT",
59
    "Windows Server 2012 Core": "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
60
    "Windows Server 2012 Core N": "8N2M2-HWPGY-7PGT9-HGDD8-GVGGY",
61
    "Windows Server 2012 Core Single Language":
62
    "2WN2H-YGCQR-KFX6K-CD6TF-84YXQ",
63
    "Windows Server 2012 Core Country Specific":
64
    "4K36P-JN4VD-GDC6V-KDT89-DYFKP",
65
    "Windows Server 2012 Server Standard": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
66
    "Windows Server 2012 Standard Core": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
67
    "Windows Server 2012 MultiPoint Standard": "HM7DN-YVMH3-46JC3-XYTG7-CYQJJ",
68
    "Windows Server 2012 MultiPoint Premium": "XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G",
69
    "Windows Server 2012 Datacenter": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
70
    "Windows Server 2012 Datacenter Core": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
71
    "Windows 7 Professional": "FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4",
72
    "Windows 7 Professional N": "MRPKT-YTG23-K7D7T-X2JMM-QY7MG",
73
    "Windows 7 Professional E": "W82YF-2Q76Y-63HXB-FGJG9-GF7QX",
74
    "Windows 7 Enterprise": "33PXH-7Y6KF-2VJC9-XBBR8-HVTHH",
75
    "Windows 7 Enterprise N": "YDRBP-3D83W-TY26F-D46B2-XCKRJ",
76
    "Windows 7 Enterprise E": "C29WB-22CC8-VJ326-GHFJW-H9DH4",
77
    "Windows Server 2008 R2 Web": "6TPJF-RBVHG-WBW2R-86QPH-6RTM4",
78
    "Windows Server 2008 R2 HPC edition": "TT8MH-CG224-D3D7Q-498W2-9QCTX",
79
    "Windows Server 2008 R2 Standard": "YC6KT-GKW9T-YTKYR-T4X34-R7VHC",
80
    "Windows Server 2008 R2 Enterprise": "489J6-VHDMP-X63PK-3K798-CPX3Y",
81
    "Windows Server 2008 R2 Datacenter": "74YFP-3QFB3-KQT8W-PMXWJ-7M648",
82
    "Windows Server 2008 R2 for Itanium-based Systems":
83
    "GT63C-RJFQ3-4GMB6-BRFB9-CB83V",
84
    "Windows Vista Business": "YFKBB-PQJJV-G996G-VWGXY-2V3X8",
85
    "Windows Vista Business N": "HMBQG-8H2RH-C77VX-27R82-VMQBT",
86
    "Windows Vista Enterprise": "VKK3X-68KWM-X2YGT-QR4M6-4BWMV",
87
    "Windows Vista Enterprise N": "VTC42-BM838-43QHV-84HX6-XJXKV",
88
    "Windows Web Server 2008": "WYR28-R7TFJ-3X2YQ-YCY4H-M249D",
89
    "Windows Server 2008 Standard": "TM24T-X9RMF-VWXK6-X8JC9-BFGM2",
90
    "Windows Server 2008 Standard without Hyper-V":
91
    "W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ",
92
    "Windows Server 2008 Enterprise":
93
    "YQGMW-MPWTJ-34KDK-48M3W-X4Q6V",
94
    "Windows Server 2008 Enterprise without Hyper-V":
95
    "39BXF-X8Q23-P2WWT-38T2F-G3FPG",
96
    "Windows Server 2008 HPC": "RCTX3-KWVHP-BR6TB-RB6DM-6X7HP",
97
    "Windows Server 2008 Datacenter": "7M67G-PC374-GR742-YH8V4-TCBY3",
98
    "Windows Server 2008 Datacenter without Hyper-V":
99
    "22XQ2-VRXRG-P8D42-K34TD-G3QQC",
100
    "Windows Server 2008 for Itanium-Based Systems":
101
    "4DWFP-JF3DJ-B7DTH-78FJB-PDRHK"}
102

    
103
_POSINT = lambda x: type(x) == int and x >= 0
104

    
105

    
106
class Windows(OSBase):
107
    """OS class for Windows"""
108
    @add_sysprep_param(
109
        'shutdown_timeout', int, 120, "Shutdown Timeout (seconds)", _POSINT)
110
    @add_sysprep_param(
111
        'boot_timeout', int, 300, "Boot Timeout (seconds)", _POSINT)
112
    @add_sysprep_param(
113
        'connection_retries', int, 5, "Connection Retries", _POSINT)
114
    @add_sysprep_param('password', str, None, 'Image Administrator Password')
115
    def __init__(self, image, **kargs):
116
        super(Windows, self).__init__(image, **kargs)
117

    
118
        # The commit with the following message was added in
119
        # libguestfs 1.17.18 and was backported in version 1.16.11:
120
        #
121
        # When a Windows guest doesn't have a HKLM\SYSTEM\MountedDevices node,
122
        # inspection fails.  However inspection should not completely fail just
123
        # because we cannot get the drive letter mapping from a guest.
124
        #
125
        # Since Microsoft Sysprep removes the aforementioned key, image
126
        # creation for windows can only be supported if the installed guestfs
127
        # version is 1.17.18 or higher
128
        if self.image.check_guestfs_version(1, 17, 18) < 0 and \
129
                (self.image.check_guestfs_version(1, 17, 0) >= 0 or
130
                 self.image.check_guestfs_version(1, 16, 11) < 0):
131
            raise FatalError(
132
                'For windows support libguestfs 1.16.11 or above is required')
133

    
134
        device = self.image.g.part_to_dev(self.root)
135

    
136
        self.last_part_num = self.image.g.part_list(device)[-1]['part_num']
137
        self.last_drive = None
138
        self.system_drive = None
139

    
140
        for drive, part in self.image.g.inspect_get_drive_mappings(self.root):
141
            if part == "%s%d" % (device, self.last_part_num):
142
                self.last_drive = drive
143
            if part == self.root:
144
                self.system_drive = drive
145

    
146
        assert self.system_drive
147

    
148
        self.product_name = self.image.g.inspect_get_product_name(self.root)
149
        self.syspreped = False
150

    
151
    @sysprep('Disabling IPv6 privacy extensions')
152
    def disable_ipv6_privacy_extensions(self):
153
        """Disable IPv6 privacy extensions"""
154

    
155
        self._guest_exec('netsh interface ipv6 set global '
156
                         'randomizeidentifiers=disabled store=persistent')
157

    
158
    @sysprep('Disabling Teredo interface')
159
    def disable_teredo(self):
160
        """Disable Teredo interface"""
161

    
162
        self._guest_exec('netsh interface teredo set state disabled')
163

    
164
    @sysprep('Disabling ISATAP Adapters')
165
    def disable_isatap(self):
166
        """Disable ISATAP Adapters"""
167

    
168
        self._guest_exec('netsh interface isa set state disabled')
169

    
170
    @sysprep('Enabling ping responses')
171
    def enable_pings(self):
172
        """Enable ping responses"""
173

    
174
        self._guest_exec('netsh firewall set icmpsetting 8')
175

    
176
    @sysprep('Disabling hibernation support')
177
    def disable_hibernation(self):
178
        """Disable hibernation support and remove the hibernation file"""
179

    
180
        self._guest_exec(r'powercfg.exe /hibernate off')
181

    
182
    @sysprep('Setting the system clock to UTC')
183
    def utc(self):
184
        """Set the hardware clock to UTC"""
185

    
186
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
187
        self._guest_exec(
188
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
189

    
190
    @sysprep('Clearing the event logs')
191
    def clear_logs(self):
192
        """Clear all the event logs"""
193

    
194
        self._guest_exec(
195
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
196

    
197
    @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
198
    def microsoft_sysprep(self):
199
        """Run the Microsoft System Preparation Tool. This will remove
200
        system-specific data and will make the image ready to be deployed.
201
        After this no other task may run.
202
        """
203

    
204
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
205
                         r'/quiet /generalize /oobe /shutdown')
206
        self.syspreped = True
207

    
208
    @sysprep('Converting the image into a KMS client', enabled=False)
209
    def kms_client_setup(self):
210
        """Install the appropriate KMS client setup key to the image to convert
211
        it to a KMS client. Computers that are running volume licensing
212
        editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
213
        2008 R2, Windows Vista, and Windows Server 2008 are by default KMS
214
        clients with no additional configuration needed.
215
        """
216
        try:
217
            setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
218
        except KeyError:
219
            self.out.warn(
220
                "Don't know the KMS client setup key for product: `%s'" %
221
                self.product_name)
222
            return
223

    
224
        self._guest_exec(
225
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
226

    
227
    @sysprep('Shrinking the last filesystem')
228
    def shrink(self):
229
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
230

    
231
        # Query for the maximum number of reclaimable bytes
232
        cmd = (
233
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
234
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
235
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
236
            r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
237
            r'ECHO EXIT >> %SCRIPT% & ' +
238
            r'DISKPART /S %SCRIPT% & ' +
239
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
240
            r'DEL /Q %SCRIPT%"')
241

    
242
        stdout, stderr, rc = self._guest_exec(cmd)
243

    
244
        querymax = None
245
        for line in stdout.splitlines():
246
            # diskpart will return something like this:
247
            #
248
            #   The maximum number of reclaimable bytes is: xxxx MB
249
            #
250
            if line.find('reclaimable') >= 0:
251
                querymax = line.split(':')[1].split()[0].strip()
252
                assert querymax.isdigit(), \
253
                    "Number of reclaimable bytes not a number"
254

    
255
        if querymax is None:
256
            FatalError("Error in shrinking! "
257
                       "Couldn't find the max number of reclaimable bytes!")
258

    
259
        querymax = int(querymax)
260
        # From ntfsresize:
261
        # Practically the smallest shrunken size generally is at around
262
        # "used space" + (20-200 MB). Please also take into account that
263
        # Windows might need about 50-100 MB free space left to boot safely.
264
        # I'll give 100MB extra space just to be sure
265
        querymax -= 100
266

    
267
        if querymax < 0:
268
            self.out.warn("Not enough available space to shrink the image!")
269
            return
270

    
271
        self.out.output("\tReclaiming %dMB ..." % querymax)
272

    
273
        cmd = (
274
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
275
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
276
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
277
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
278
            r'ECHO EXIT >> %SCRIPT% & ' +
279
            r'DISKPART /S %SCRIPT% & ' +
280
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
281
            r'DEL /Q %SCRIPT%"')
282

    
283
        stdout, stderr, rc = self._guest_exec(cmd, False)
284

    
285
        if rc != 0:
286
            FatalError("Shrinking failed. Please make sure the media is "
287
                       "defraged with a command like this: "
288
                       "`Defrag.exe /U /X /W'")
289
        for line in stdout.splitlines():
290
            if line.find('shrunk') >= 0:
291
                self.out.output(line)
292

    
293
    def do_sysprep(self):
294
        """Prepare system for image creation."""
295

    
296
        if getattr(self, 'syspreped', False):
297
            raise FatalError("Image is already syspreped!")
298

    
299
        txt = "System preparation parameter: `%s' is needed but missing!"
300
        for name, param in self.needed_sysprep_params.items():
301
            if name not in self.sysprep_params:
302
                raise FatalError(txt % name)
303

    
304
        self.mount(readonly=False)
305
        try:
306
            disabled_uac = self._update_uac_remote_setting(1)
307
            token = self._enable_os_monitor()
308

    
309
            # disable the firewalls
310
            firewall_states = self._update_firewalls(0, 0, 0)
311

    
312
            # Delete the pagefile. It will be recreated when the system boots
313
            systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
314
            try:
315
                pagefile = "%s/pagefile.sys" % systemroot
316
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
317
            except RuntimeError:
318
                pass
319

    
320
        finally:
321
            self.umount()
322

    
323
        self.image.disable_guestfs()
324

    
325
        vm = None
326
        monitor = None
327
        try:
328
            self.out.output("Starting windows VM ...", False)
329
            monitorfd, monitor = tempfile.mkstemp()
330
            os.close(monitorfd)
331
            vm = _VM(self.image.device, monitor, self.sysprep_params)
332
            self.out.success("started (console on vnc display: %d)." %
333
                             vm.display)
334

    
335
            self.out.output("Waiting for OS to boot ...", False)
336
            self._wait_vm_boot(vm, monitor, token)
337
            self.out.success('done')
338

    
339
            self.out.output("Checking connectivity to the VM ...", False)
340
            self._check_connectivity()
341
            self.out.success('done')
342

    
343
            self.out.output("Disabling automatic logon ...", False)
344
            self._disable_autologon()
345
            self.out.success('done')
346

    
347
            self.out.output('Preparing system for image creation:')
348

    
349
            tasks = self.list_syspreps()
350
            enabled = [task for task in tasks if task.enabled]
351
            size = len(enabled)
352

    
353
            # Make sure shrink runs in the end, before ms sysprep
354
            enabled = [task for task in enabled if
355
                       self.sysprep_info(task).name != 'shrink']
356

    
357
            if len(enabled) != size:
358
                enabled.append(self.shrink)
359

    
360
            # Make sure the ms sysprep is the last task to run if it is enabled
361
            enabled = [task for task in enabled if
362
                       self.sysprep_info(task).name != 'microsoft-sysprep']
363

    
364
            ms_sysprep_enabled = False
365
            if len(enabled) != size:
366
                enabled.append(self.microsoft_sysprep)
367
                ms_sysprep_enabled = True
368

    
369
            cnt = 0
370
            for task in enabled:
371
                cnt += 1
372
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
373
                task()
374
                setattr(task.im_func, 'executed', True)
375

    
376
            self.out.output("Sending shut down command ...", False)
377
            if not ms_sysprep_enabled:
378
                self._shutdown()
379
            self.out.success("done")
380

    
381
            self.out.output("Waiting for windows to shut down ...", False)
382
            vm.wait(self.sysprep_params['shutdown_timeout'])
383
            self.out.success("done")
384
        finally:
385
            if monitor is not None:
386
                os.unlink(monitor)
387

    
388
            try:
389
                if vm is not None:
390
                    self.out.output("Destroying windows VM ...", False)
391
                    vm.destroy()
392
                    self.out.success("done")
393
            finally:
394
                self.image.enable_guestfs()
395

    
396
                self.mount(readonly=False)
397
                try:
398
                    if disabled_uac:
399
                        self._update_uac_remote_setting(0)
400

    
401
                    self._update_firewalls(*firewall_states)
402
                finally:
403
                    self.umount()
404

    
405
    def _shutdown(self):
406
        """Shuts down the windows VM"""
407
        self._guest_exec(r'shutdown /s /t 5')
408

    
409
    def _wait_vm_boot(self, vm, fname, msg):
410
        """Wait until a message appears on a file or the vm process dies"""
411

    
412
        for _ in range(self.sysprep_params['boot_timeout']):
413
            time.sleep(1)
414
            with open(fname) as f:
415
                for line in f:
416
                    if line.startswith(msg):
417
                        return True
418
            if not vm.isalive():
419
                raise FatalError("Windows VM died unexpectedly!")
420

    
421
        raise FatalError("Windows VM booting timed out!")
422

    
423
    def _disable_autologon(self):
424
        """Disable automatic logon on the windows image"""
425

    
426
        winlogon = \
427
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
428

    
429
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
430
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
431
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
432

    
433
    def _registry_file_path(self, regfile):
434
        """Retrieves the case sensitive path to a registry file"""
435

    
436
        systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
437
        path = "%s/system32/config/%s" % (systemroot, regfile)
438
        try:
439
            path = self.image.g.case_sensitive_path(path)
440
        except RuntimeError as error:
441
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
442
                             % (regfile, str(error)))
443
        return path
444

    
445
    def _enable_os_monitor(self):
446
        """Add a script in the registry that will send a random string to the
447
        first serial port when the windows image finishes booting.
448
        """
449

    
450
        token = "".join(random.choice(string.ascii_letters) for x in range(16))
451

    
452
        path = self._registry_file_path('SOFTWARE')
453
        softwarefd, software = tempfile.mkstemp()
454
        try:
455
            os.close(softwarefd)
456
            self.image.g.download(path, software)
457

    
458
            h = hivex.Hivex(software, write=True)
459

    
460
            # Enable automatic logon.
461
            # This is needed because we need to execute a script that we add in
462
            # the RunOnce registry entry and those programs only get executed
463
            # when a user logs on. There is a RunServicesOnce registry entry
464
            # whose keys get executed in the background when the logon dialog
465
            # box first appears, but they seem to only work with services and
466
            # not arbitrary command line expressions :-(
467
            #
468
            # Instructions on how to turn on automatic logon in Windows can be
469
            # found here: http://support.microsoft.com/kb/324737
470
            #
471
            # Warning: Registry change will not work if the “Logon Banner” is
472
            # defined on the server either by a Group Policy object (GPO) or by
473
            # a local policy.
474

    
475
            winlogon = h.root()
476
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
477
                          'Winlogon'):
478
                winlogon = h.node_get_child(winlogon, child)
479

    
480
            h.node_set_value(
481
                winlogon,
482
                {'key': 'DefaultUserName', 't': 1,
483
                 'value': "Administrator".encode('utf-16le')})
484
            h.node_set_value(
485
                winlogon,
486
                {'key': 'DefaultPassword', 't': 1,
487
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
488
            h.node_set_value(
489
                winlogon,
490
                {'key': 'AutoAdminLogon', 't': 1,
491
                 'value': "1".encode('utf-16le')})
492

    
493
            key = h.root()
494
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
495
                key = h.node_get_child(key, child)
496

    
497
            runonce = h.node_get_child(key, "RunOnce")
498
            if runonce is None:
499
                runonce = h.node_add_child(key, "RunOnce")
500

    
501
            value = (
502
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
503
                r'-ExecutionPolicy RemoteSigned '
504
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
505
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
506
                r'$port.Close()}"').encode('utf-16le')
507

    
508
            h.node_set_value(runonce,
509
                             {'key': "BootMonitor", 't': 1, 'value': value})
510

    
511
            value = (
512
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
513
                r'\policies\system /v LocalAccountTokenFilterPolicy'
514
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
515

    
516
            h.node_set_value(runonce,
517
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
518

    
519
            h.commit(None)
520

    
521
            self.image.g.upload(software, path)
522
        finally:
523
            os.unlink(software)
524

    
525
        return token
526

    
527
    def _update_firewalls(self, domain, public, standard):
528
        """Enables or disables the firewall for the Domain, the Public and the
529
        Standard profile. Returns a triplete with the old values.
530

531
        1 will enable a firewall and 0 will disable it
532
        """
533

    
534
        if domain not in (0, 1):
535
            raise ValueError("Valid values for domain parameter are 0 and 1")
536

    
537
        if public not in (0, 1):
538
            raise ValueError("Valid values for public parameter are 0 and 1")
539

    
540
        if standard not in (0, 1):
541
            raise ValueError("Valid values for standard parameter are 0 and 1")
542

    
543
        path = self._registry_file_path("SYSTEM")
544
        systemfd, system = tempfile.mkstemp()
545
        try:
546
            os.close(systemfd)
547
            self.image.g.download(path, system)
548

    
549
            h = hivex.Hivex(system, write=True)
550

    
551
            select = h.node_get_child(h.root(), 'Select')
552
            current_value = h.node_get_value(select, 'Current')
553

    
554
            # expecting a little endian dword
555
            assert h.value_type(current_value)[1] == 4
556
            current = "%03d" % h.value_dword(current_value)
557

    
558
            firewall_policy = h.root()
559
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
560
                          'Parameters', 'FirewallPolicy'):
561
                firewall_policy = h.node_get_child(firewall_policy, child)
562

    
563
            old_values = []
564
            new_values = [domain, public, standard]
565
            for profile in ('Domain', 'Public', 'Standard'):
566
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
567

    
568
                old_value = h.node_get_value(node, 'EnableFirewall')
569

    
570
                # expecting a little endian dword
571
                assert h.value_type(old_value)[1] == 4
572
                old_values.append(h.value_dword(old_value))
573

    
574
                h.node_set_value(
575
                    node, {'key': 'EnableFirewall', 't': 4L,
576
                           'value': struct.pack("<I", new_values.pop(0))})
577

    
578
            h.commit(None)
579
            self.image.g.upload(system, path)
580

    
581
        finally:
582
            os.unlink(system)
583

    
584
        return old_values
585

    
586
    def _update_uac_remote_setting(self, value):
587
        """Updates the registry key value:
588
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
589
        \System]"LocalAccountTokenFilterPolicy"
590

591
        value = 1 will disable the UAC remote restrictions
592
        value = 0 will enable the UAC remote restrictions
593

594
        For more info see here: http://support.microsoft.com/kb/951016
595

596
        Returns:
597
            True if the key is changed
598
            False if the key is unchanged
599
        """
600

    
601
        if value not in (0, 1):
602
            raise ValueError("Valid values for value parameter are 0 and 1")
603

    
604
        path = self._registry_file_path('SOFTWARE')
605
        softwarefd, software = tempfile.mkstemp()
606
        try:
607
            os.close(softwarefd)
608
            self.image.g.download(path, software)
609

    
610
            h = hivex.Hivex(software, write=True)
611

    
612
            key = h.root()
613
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
614
                          'System'):
615
                key = h.node_get_child(key, child)
616

    
617
            policy = None
618
            for val in h.node_values(key):
619
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
620
                    policy = val
621

    
622
            if policy is not None:
623
                dword = h.value_dword(policy)
624
                if dword == value:
625
                    return False
626
            elif value == 0:
627
                return False
628

    
629
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
630
                         'value': struct.pack("<I", value)}
631

    
632
            h.node_set_value(key, new_value)
633
            h.commit(None)
634

    
635
            self.image.g.upload(software, path)
636

    
637
        finally:
638
            os.unlink(software)
639

    
640
        return True
641

    
642
    def _do_collect_metadata(self):
643
        """Collect metadata about the OS"""
644
        super(Windows, self)._do_collect_metadata()
645
        self.meta["USERS"] = " ".join(self._get_users())
646

    
647
    def _get_users(self):
648
        """Returns a list of users found in the images"""
649
        samfd, sam = tempfile.mkstemp()
650
        try:
651
            os.close(samfd)
652
            self.image.g.download(self._registry_file_path('SAM'), sam)
653

    
654
            h = hivex.Hivex(sam)
655

    
656
            # Navigate to /SAM/Domains/Account/Users
657
            users_node = h.root()
658
            for child in ('SAM', 'Domains', 'Account', 'Users'):
659
                users_node = h.node_get_child(users_node, child)
660

    
661
            # Navigate to /SAM/Domains/Account/Users/Names
662
            names_node = h.node_get_child(users_node, 'Names')
663

    
664
            # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\%RID%
665
            # HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\Users\Names\%Username%
666
            #
667
            # The RID (relative identifier) of each user is stored as the type!
668
            # (not the value) of the default key of the node under Names whose
669
            # name is the user's username. Under the RID node, there in a F
670
            # value that contains information about this user account.
671
            #
672
            # See sam.h of the chntpw project on how to translate the F value
673
            # of an account in the registry. Bytes 56 & 57 are the account type
674
            # and status flags. The first bit is the 'account disabled' bit
675
            disabled = lambda f: int(f[56].encode('hex'), 16) & 0x01
676

    
677
            users = []
678
            for user_node in h.node_children(names_node):
679
                username = h.node_name(user_node)
680
                rid = h.value_type(h.node_get_value(user_node, ""))[0]
681
                # if RID is 500 (=0x1f4), the corresponding node name under
682
                # Users is '000001F4'
683
                key = ("%8.x" % rid).replace(' ', '0').upper()
684
                rid_node = h.node_get_child(users_node, key)
685
                f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
686

    
687
                if disabled(f_value):
688
                    self.out.warn("Found disabled `%s' account!" % username)
689
                    continue
690

    
691
                users.append(username)
692

    
693
        finally:
694
            os.unlink(sam)
695

    
696
        # Filter out the guest account
697
        return users
698

    
699
    def _check_connectivity(self):
700
        """Check if winexe works on the Windows VM"""
701

    
702
        retries = self.sysprep_params['connection_retries']
703
        # If the connection_retries parameter is set to 0 disable the
704
        # connectivity check
705
        if retries == 0:
706
            return True
707

    
708
        passwd = self.sysprep_params['password']
709
        winexe = WinEXE('Administrator', passwd, 'localhost')
710
        winexe.uninstall().debug(9)
711

    
712
        for i in range(retries):
713
            (stdout, stderr, rc) = winexe.run('cmd /C')
714
            if rc == 0:
715
                return True
716
            log = tempfile.NamedTemporaryFile(delete=False)
717
            try:
718
                log.file.write(stdout)
719
            finally:
720
                log.close()
721
            self.out.output("failed! See: `%s' for the full output" % log.name)
722
            if i < retries - 1:
723
                self.out.output("retrying ...", False)
724

    
725
        raise FatalError("Connection to the Windows VM failed after %d retries"
726
                         % retries)
727

    
728
    def _guest_exec(self, command, fatal=True):
729
        """Execute a command on a windows VM"""
730

    
731
        passwd = self.sysprep_params['password']
732

    
733
        winexe = WinEXE('Administrator', passwd, 'localhost')
734
        winexe.runas('Administrator', passwd).uninstall()
735

    
736
        try:
737
            (stdout, stderr, rc) = winexe.run(command)
738
        except WinexeTimeout:
739
            FatalError("Command: `%s' timeout out." % command)
740

    
741
        if rc != 0 and fatal:
742
            reason = stderr if len(stderr) else stdout
743
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
744
                            (command, rc, reason))
745
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
746
                             (command, rc, reason))
747

    
748
        return (stdout, stderr, rc)
749

    
750

    
751
class _VM(object):
752
    """Windows Virtual Machine"""
753
    def __init__(self, disk, serial, params):
754
        """Create _VM instance
755

756
            disk: VM's hard disk
757
            serial: File to save the output of the serial port
758
        """
759

    
760
        self.disk = disk
761
        self.serial = serial
762
        self.params = params
763

    
764
        def random_mac():
765
            """creates a random mac address"""
766
            mac = [0x00, 0x16, 0x3e,
767
                   random.randint(0x00, 0x7f),
768
                   random.randint(0x00, 0xff),
769
                   random.randint(0x00, 0xff)]
770

    
771
            return ':'.join(['%02x' % x for x in mac])
772

    
773
        # Use ganeti's VNC port range for a random vnc port
774
        self.display = random.randint(11000, 14999) - 5900
775

    
776
        kvm, needed_args = get_kvm_binary()
777

    
778
        if kvm is None:
779
            FatalError("Can't find the kvm binary")
780

    
781
        args = [kvm]
782
        args.extend(needed_args)
783

    
784
        args.extend([
785
            '-smp', '1', '-m', '1024', '-drive',
786
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
787
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
788
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
789
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
790
            '-monitor', 'stdio'])
791

    
792
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
793
                                        stdout=subprocess.PIPE)
794

    
795
    def isalive(self):
796
        """Check if the VM is still alive"""
797
        return self.process.poll() is None
798

    
799
    def destroy(self):
800
        """Destroy the VM"""
801

    
802
        if not self.isalive():
803
            return
804

    
805
        def handler(signum, frame):
806
            self.process.terminate()
807
            time.sleep(1)
808
            if self.isalive():
809
                self.process.kill()
810
            self.process.wait()
811
            raise FatalError("VM destroy timed-out")
812

    
813
        signal.signal(signal.SIGALRM, handler)
814

    
815
        signal.alarm(self.params['shutdown_timeout'])
816
        self.process.communicate(input="system_powerdown\n")
817
        signal.alarm(0)
818

    
819
    def wait(self, timeout=0):
820
        """Wait for the VM to terminate"""
821

    
822
        def handler(signum, frame):
823
            self.destroy()
824
            raise FatalError("VM wait timed-out.")
825

    
826
        signal.signal(signal.SIGALRM, handler)
827

    
828
        signal.alarm(timeout)
829
        stdout, stderr = self.process.communicate()
830
        signal.alarm(0)
831

    
832
        return (stdout, stderr, self.process.poll())
833

    
834
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :