Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ f63e359f

History | View | Annotate | Download (31.9 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.1 Professional": "GCRJD-8NW9H-F2CDX-CCM8D-9D6T9",
56
    "Windows 8.1 Professional N": "HMCNV-VVBFX-7HMBH-CTY9B-B4FXY",
57
    "Windows 8.1 Enterprise": "MHF9N-XY6XB-WVXMC-BTDCT-MKKG7",
58
    "Windows 8.1 Enterprise N": "TT4HM-HN7YT-62K67-RGRQJ-JFFXW",
59
    "Windows Server 2012 R2 Server Standard": "D2N9P-3P6X9-2R39C-7RTCD-MDVJX",
60
    "Windows Server 2012 R2 Datacenter": "W3GGN-FT8W3-Y4M27-J84CP-Q3VJ9",
61
    "Windows Server 2012 R2 Essentials": "KNC87-3J2TX-XB4WP-VCPJV-M4FWM",
62
    "Windows 8 Professional": "NG4HW-VH26C-733KW-K6F98-J8CK4",
63
    "Windows 8 Professional N": "XCVCF-2NXM9-723PB-MHCB7-2RYQQ",
64
    "Windows 8 Enterprise": "32JNW-9KQ84-P47T8-D8GGY-CWCK7",
65
    "Windows 8 Enterprise N": "JMNMF-RHW7P-DMY6X-RF3DR-X2BQT",
66
    "Windows Server 2012 Core": "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
67
    "Windows Server 2012 Core N": "8N2M2-HWPGY-7PGT9-HGDD8-GVGGY",
68
    "Windows Server 2012 Core Single Language":
69
    "2WN2H-YGCQR-KFX6K-CD6TF-84YXQ",
70
    "Windows Server 2012 Core Country Specific":
71
    "4K36P-JN4VD-GDC6V-KDT89-DYFKP",
72
    "Windows Server 2012 Server Standard": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
73
    "Windows Server 2012 Standard Core": "XC9B7-NBPP2-83J2H-RHMBY-92BT4",
74
    "Windows Server 2012 MultiPoint Standard": "HM7DN-YVMH3-46JC3-XYTG7-CYQJJ",
75
    "Windows Server 2012 MultiPoint Premium": "XNH6W-2V9GX-RGJ4K-Y8X6F-QGJ2G",
76
    "Windows Server 2012 Datacenter": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
77
    "Windows Server 2012 Datacenter Core": "48HP8-DN98B-MYWDG-T2DCC-8W83P",
78
    "Windows 7 Professional": "FJ82H-XT6CR-J8D7P-XQJJ2-GPDD4",
79
    "Windows 7 Professional N": "MRPKT-YTG23-K7D7T-X2JMM-QY7MG",
80
    "Windows 7 Professional E": "W82YF-2Q76Y-63HXB-FGJG9-GF7QX",
81
    "Windows 7 Enterprise": "33PXH-7Y6KF-2VJC9-XBBR8-HVTHH",
82
    "Windows 7 Enterprise N": "YDRBP-3D83W-TY26F-D46B2-XCKRJ",
83
    "Windows 7 Enterprise E": "C29WB-22CC8-VJ326-GHFJW-H9DH4",
84
    "Windows Server 2008 R2 Web": "6TPJF-RBVHG-WBW2R-86QPH-6RTM4",
85
    "Windows Server 2008 R2 HPC edition": "TT8MH-CG224-D3D7Q-498W2-9QCTX",
86
    "Windows Server 2008 R2 Standard": "YC6KT-GKW9T-YTKYR-T4X34-R7VHC",
87
    "Windows Server 2008 R2 Enterprise": "489J6-VHDMP-X63PK-3K798-CPX3Y",
88
    "Windows Server 2008 R2 Datacenter": "74YFP-3QFB3-KQT8W-PMXWJ-7M648",
89
    "Windows Server 2008 R2 for Itanium-based Systems":
90
    "GT63C-RJFQ3-4GMB6-BRFB9-CB83V",
91
    "Windows Vista Business": "YFKBB-PQJJV-G996G-VWGXY-2V3X8",
92
    "Windows Vista Business N": "HMBQG-8H2RH-C77VX-27R82-VMQBT",
93
    "Windows Vista Enterprise": "VKK3X-68KWM-X2YGT-QR4M6-4BWMV",
94
    "Windows Vista Enterprise N": "VTC42-BM838-43QHV-84HX6-XJXKV",
95
    "Windows Web Server 2008": "WYR28-R7TFJ-3X2YQ-YCY4H-M249D",
96
    "Windows Server 2008 Standard": "TM24T-X9RMF-VWXK6-X8JC9-BFGM2",
97
    "Windows Server 2008 Standard without Hyper-V":
98
    "W7VD6-7JFBR-RX26B-YKQ3Y-6FFFJ",
99
    "Windows Server 2008 Enterprise":
100
    "YQGMW-MPWTJ-34KDK-48M3W-X4Q6V",
101
    "Windows Server 2008 Enterprise without Hyper-V":
102
    "39BXF-X8Q23-P2WWT-38T2F-G3FPG",
103
    "Windows Server 2008 HPC": "RCTX3-KWVHP-BR6TB-RB6DM-6X7HP",
104
    "Windows Server 2008 Datacenter": "7M67G-PC374-GR742-YH8V4-TCBY3",
105
    "Windows Server 2008 Datacenter without Hyper-V":
106
    "22XQ2-VRXRG-P8D42-K34TD-G3QQC",
107
    "Windows Server 2008 for Itanium-Based Systems":
108
    "4DWFP-JF3DJ-B7DTH-78FJB-PDRHK"}
109

    
110
_POSINT = lambda x: type(x) == int and x >= 0
111

    
112

    
113
class Windows(OSBase):
114
    """OS class for Windows"""
115
    @add_sysprep_param(
116
        'shutdown_timeout', int, 120, "Shutdown Timeout (seconds)", _POSINT)
117
    @add_sysprep_param(
118
        'boot_timeout', int, 300, "Boot Timeout (seconds)", _POSINT)
119
    @add_sysprep_param(
120
        'connection_retries', int, 5, "Connection Retries", _POSINT)
121
    @add_sysprep_param('password', str, None, 'Image Administrator Password')
122
    def __init__(self, image, **kargs):
123
        super(Windows, self).__init__(image, **kargs)
124

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

    
141
        device = self.image.g.part_to_dev(self.root)
142

    
143
        self.last_part_num = self.image.g.part_list(device)[-1]['part_num']
144
        self.last_drive = None
145
        self.system_drive = None
146

    
147
        for drive, part in self.image.g.inspect_get_drive_mappings(self.root):
148
            if part == "%s%d" % (device, self.last_part_num):
149
                self.last_drive = drive
150
            if part == self.root:
151
                self.system_drive = drive
152

    
153
        assert self.system_drive
154

    
155
        self.product_name = self.image.g.inspect_get_product_name(self.root)
156
        self.syspreped = False
157

    
158
    @sysprep('Disabling IPv6 privacy extensions')
159
    def disable_ipv6_privacy_extensions(self):
160
        """Disable IPv6 privacy extensions"""
161

    
162
        self._guest_exec('netsh interface ipv6 set global '
163
                         'randomizeidentifiers=disabled store=persistent')
164

    
165
    @sysprep('Disabling Teredo interface')
166
    def disable_teredo(self):
167
        """Disable Teredo interface"""
168

    
169
        self._guest_exec('netsh interface teredo set state disabled')
170

    
171
    @sysprep('Disabling ISATAP Adapters')
172
    def disable_isatap(self):
173
        """Disable ISATAP Adapters"""
174

    
175
        self._guest_exec('netsh interface isa set state disabled')
176

    
177
    @sysprep('Enabling ping responses')
178
    def enable_pings(self):
179
        """Enable ping responses"""
180

    
181
        self._guest_exec('netsh firewall set icmpsetting 8')
182

    
183
    @sysprep('Disabling hibernation support')
184
    def disable_hibernation(self):
185
        """Disable hibernation support and remove the hibernation file"""
186

    
187
        self._guest_exec(r'powercfg.exe /hibernate off')
188

    
189
    @sysprep('Setting the system clock to UTC')
190
    def utc(self):
191
        """Set the hardware clock to UTC"""
192

    
193
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
194
        self._guest_exec(
195
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
196

    
197
    @sysprep('Clearing the event logs')
198
    def clear_logs(self):
199
        """Clear all the event logs"""
200

    
201
        self._guest_exec(
202
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
203

    
204
    @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
205
    def microsoft_sysprep(self):
206
        """Run the Microsoft System Preparation Tool. This will remove
207
        system-specific data and will make the image ready to be deployed.
208
        After this no other task may run.
209
        """
210

    
211
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
212
                         r'/quiet /generalize /oobe /shutdown')
213
        self.syspreped = True
214

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

    
231
        self._guest_exec(
232
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
233

    
234
    @sysprep('Shrinking the last filesystem')
235
    def shrink(self):
236
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
237

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

    
249
        stdout, stderr, rc = self._guest_exec(cmd)
250

    
251
        querymax = None
252
        for line in stdout.splitlines():
253
            # diskpart will return something like this:
254
            #
255
            #   The maximum number of reclaimable bytes is: xxxx MB
256
            #
257
            if line.find('reclaimable') >= 0:
258
                querymax = line.split(':')[1].split()[0].strip()
259
                assert querymax.isdigit(), \
260
                    "Number of reclaimable bytes not a number"
261

    
262
        if querymax is None:
263
            FatalError("Error in shrinking! "
264
                       "Couldn't find the max number of reclaimable bytes!")
265

    
266
        querymax = int(querymax)
267
        # From ntfsresize:
268
        # Practically the smallest shrunken size generally is at around
269
        # "used space" + (20-200 MB). Please also take into account that
270
        # Windows might need about 50-100 MB free space left to boot safely.
271
        # I'll give 100MB extra space just to be sure
272
        querymax -= 100
273

    
274
        if querymax < 0:
275
            self.out.warn("Not enough available space to shrink the image!")
276
            return
277

    
278
        self.out.output("\tReclaiming %dMB ..." % querymax)
279

    
280
        cmd = (
281
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
282
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
283
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
284
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
285
            r'ECHO EXIT >> %SCRIPT% & ' +
286
            r'DISKPART /S %SCRIPT% & ' +
287
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
288
            r'DEL /Q %SCRIPT%"')
289

    
290
        stdout, stderr, rc = self._guest_exec(cmd, False)
291

    
292
        if rc != 0:
293
            FatalError("Shrinking failed. Please make sure the media is "
294
                       "defraged with a command like this: "
295
                       "`Defrag.exe /U /X /W'")
296
        for line in stdout.splitlines():
297
            if line.find('shrunk') >= 0:
298
                self.out.output(line)
299

    
300
    def do_sysprep(self):
301
        """Prepare system for image creation."""
302

    
303
        if getattr(self, 'syspreped', False):
304
            raise FatalError("Image is already syspreped!")
305

    
306
        txt = "System preparation parameter: `%s' is needed but missing!"
307
        for name, param in self.needed_sysprep_params.items():
308
            if name not in self.sysprep_params:
309
                raise FatalError(txt % name)
310

    
311
        self.mount(readonly=False)
312
        try:
313
            disabled_uac = self._update_uac_remote_setting(1)
314
            token = self._enable_os_monitor()
315

    
316
            # disable the firewalls
317
            firewall_states = self._update_firewalls(0, 0, 0)
318

    
319
            # Delete the pagefile. It will be recreated when the system boots
320
            systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
321
            try:
322
                pagefile = "%s/pagefile.sys" % systemroot
323
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
324
            except RuntimeError:
325
                pass
326

    
327
        finally:
328
            self.umount()
329

    
330
        self.image.disable_guestfs()
331

    
332
        vm = None
333
        monitor = None
334
        try:
335
            self.out.output("Starting windows VM ...", False)
336
            monitorfd, monitor = tempfile.mkstemp()
337
            os.close(monitorfd)
338
            vm = _VM(self.image.device, monitor, self.sysprep_params)
339
            self.out.success("started (console on vnc display: %d)." %
340
                             vm.display)
341

    
342
            self.out.output("Waiting for OS to boot ...", False)
343
            self._wait_vm_boot(vm, monitor, token)
344
            self.out.success('done')
345

    
346
            self.out.output("Checking connectivity to the VM ...", False)
347
            self._check_connectivity()
348
            self.out.success('done')
349

    
350
            self.out.output("Disabling automatic logon ...", False)
351
            self._disable_autologon()
352
            self.out.success('done')
353

    
354
            self.out.output('Preparing system for image creation:')
355

    
356
            tasks = self.list_syspreps()
357
            enabled = [task for task in tasks if task.enabled]
358
            size = len(enabled)
359

    
360
            # Make sure shrink runs in the end, before ms sysprep
361
            enabled = [task for task in enabled if
362
                       self.sysprep_info(task).name != 'shrink']
363

    
364
            if len(enabled) != size:
365
                enabled.append(self.shrink)
366

    
367
            # Make sure the ms sysprep is the last task to run if it is enabled
368
            enabled = [task for task in enabled if
369
                       self.sysprep_info(task).name != 'microsoft-sysprep']
370

    
371
            ms_sysprep_enabled = False
372
            if len(enabled) != size:
373
                enabled.append(self.microsoft_sysprep)
374
                ms_sysprep_enabled = True
375

    
376
            cnt = 0
377
            for task in enabled:
378
                cnt += 1
379
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
380
                task()
381
                setattr(task.im_func, 'executed', True)
382

    
383
            self.out.output("Sending shut down command ...", False)
384
            if not ms_sysprep_enabled:
385
                self._shutdown()
386
            self.out.success("done")
387

    
388
            self.out.output("Waiting for windows to shut down ...", False)
389
            vm.wait(self.sysprep_params['shutdown_timeout'])
390
            self.out.success("done")
391
        finally:
392
            if monitor is not None:
393
                os.unlink(monitor)
394

    
395
            try:
396
                if vm is not None:
397
                    self.out.output("Destroying windows VM ...", False)
398
                    vm.destroy()
399
                    self.out.success("done")
400
            finally:
401
                self.image.enable_guestfs()
402

    
403
                self.mount(readonly=False)
404
                try:
405
                    if disabled_uac:
406
                        self._update_uac_remote_setting(0)
407

    
408
                    self._update_firewalls(*firewall_states)
409
                finally:
410
                    self.umount()
411

    
412
    def _shutdown(self):
413
        """Shuts down the windows VM"""
414
        self._guest_exec(r'shutdown /s /t 5')
415

    
416
    def _wait_vm_boot(self, vm, fname, msg):
417
        """Wait until a message appears on a file or the vm process dies"""
418

    
419
        for _ in range(self.sysprep_params['boot_timeout']):
420
            time.sleep(1)
421
            with open(fname) as f:
422
                for line in f:
423
                    if line.startswith(msg):
424
                        return True
425
            if not vm.isalive():
426
                raise FatalError("Windows VM died unexpectedly!")
427

    
428
        raise FatalError("Windows VM booting timed out!")
429

    
430
    def _disable_autologon(self):
431
        """Disable automatic logon on the windows image"""
432

    
433
        winlogon = \
434
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
435

    
436
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
437
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
438
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
439

    
440
    def _registry_file_path(self, regfile):
441
        """Retrieves the case sensitive path to a registry file"""
442

    
443
        systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
444
        path = "%s/system32/config/%s" % (systemroot, regfile)
445
        try:
446
            path = self.image.g.case_sensitive_path(path)
447
        except RuntimeError as error:
448
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
449
                             % (regfile, str(error)))
450
        return path
451

    
452
    def _enable_os_monitor(self):
453
        """Add a script in the registry that will send a random string to the
454
        first serial port when the windows image finishes booting.
455
        """
456

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

    
459
        path = self._registry_file_path('SOFTWARE')
460
        softwarefd, software = tempfile.mkstemp()
461
        try:
462
            os.close(softwarefd)
463
            self.image.g.download(path, software)
464

    
465
            h = hivex.Hivex(software, write=True)
466

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

    
482
            winlogon = h.root()
483
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
484
                          'Winlogon'):
485
                winlogon = h.node_get_child(winlogon, child)
486

    
487
            h.node_set_value(
488
                winlogon,
489
                {'key': 'DefaultUserName', 't': 1,
490
                 'value': "Administrator".encode('utf-16le')})
491
            h.node_set_value(
492
                winlogon,
493
                {'key': 'DefaultPassword', 't': 1,
494
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
495
            h.node_set_value(
496
                winlogon,
497
                {'key': 'AutoAdminLogon', 't': 1,
498
                 'value': "1".encode('utf-16le')})
499

    
500
            key = h.root()
501
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
502
                key = h.node_get_child(key, child)
503

    
504
            runonce = h.node_get_child(key, "RunOnce")
505
            if runonce is None:
506
                runonce = h.node_add_child(key, "RunOnce")
507

    
508
            value = (
509
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
510
                r'-ExecutionPolicy RemoteSigned '
511
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
512
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
513
                r'$port.Close()}"').encode('utf-16le')
514

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

    
518
            value = (
519
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
520
                r'\policies\system /v LocalAccountTokenFilterPolicy'
521
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
522

    
523
            h.node_set_value(runonce,
524
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
525

    
526
            h.commit(None)
527

    
528
            self.image.g.upload(software, path)
529
        finally:
530
            os.unlink(software)
531

    
532
        return token
533

    
534
    def _update_firewalls(self, domain, public, standard):
535
        """Enables or disables the firewall for the Domain, the Public and the
536
        Standard profile. Returns a triplete with the old values.
537

538
        1 will enable a firewall and 0 will disable it
539
        """
540

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

    
544
        if public not in (0, 1):
545
            raise ValueError("Valid values for public parameter are 0 and 1")
546

    
547
        if standard not in (0, 1):
548
            raise ValueError("Valid values for standard parameter are 0 and 1")
549

    
550
        path = self._registry_file_path("SYSTEM")
551
        systemfd, system = tempfile.mkstemp()
552
        try:
553
            os.close(systemfd)
554
            self.image.g.download(path, system)
555

    
556
            h = hivex.Hivex(system, write=True)
557

    
558
            select = h.node_get_child(h.root(), 'Select')
559
            current_value = h.node_get_value(select, 'Current')
560

    
561
            # expecting a little endian dword
562
            assert h.value_type(current_value)[1] == 4
563
            current = "%03d" % h.value_dword(current_value)
564

    
565
            firewall_policy = h.root()
566
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
567
                          'Parameters', 'FirewallPolicy'):
568
                firewall_policy = h.node_get_child(firewall_policy, child)
569

    
570
            old_values = []
571
            new_values = [domain, public, standard]
572
            for profile in ('Domain', 'Public', 'Standard'):
573
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
574

    
575
                old_value = h.node_get_value(node, 'EnableFirewall')
576

    
577
                # expecting a little endian dword
578
                assert h.value_type(old_value)[1] == 4
579
                old_values.append(h.value_dword(old_value))
580

    
581
                h.node_set_value(
582
                    node, {'key': 'EnableFirewall', 't': 4L,
583
                           'value': struct.pack("<I", new_values.pop(0))})
584

    
585
            h.commit(None)
586
            self.image.g.upload(system, path)
587

    
588
        finally:
589
            os.unlink(system)
590

    
591
        return old_values
592

    
593
    def _update_uac_remote_setting(self, value):
594
        """Updates the registry key value:
595
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
596
        \System]"LocalAccountTokenFilterPolicy"
597

598
        value = 1 will disable the UAC remote restrictions
599
        value = 0 will enable the UAC remote restrictions
600

601
        For more info see here: http://support.microsoft.com/kb/951016
602

603
        Returns:
604
            True if the key is changed
605
            False if the key is unchanged
606
        """
607

    
608
        if value not in (0, 1):
609
            raise ValueError("Valid values for value parameter are 0 and 1")
610

    
611
        path = self._registry_file_path('SOFTWARE')
612
        softwarefd, software = tempfile.mkstemp()
613
        try:
614
            os.close(softwarefd)
615
            self.image.g.download(path, software)
616

    
617
            h = hivex.Hivex(software, write=True)
618

    
619
            key = h.root()
620
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
621
                          'System'):
622
                key = h.node_get_child(key, child)
623

    
624
            policy = None
625
            for val in h.node_values(key):
626
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
627
                    policy = val
628

    
629
            if policy is not None:
630
                dword = h.value_dword(policy)
631
                if dword == value:
632
                    return False
633
            elif value == 0:
634
                return False
635

    
636
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
637
                         'value': struct.pack("<I", value)}
638

    
639
            h.node_set_value(key, new_value)
640
            h.commit(None)
641

    
642
            self.image.g.upload(software, path)
643

    
644
        finally:
645
            os.unlink(software)
646

    
647
        return True
648

    
649
    def _do_collect_metadata(self):
650
        """Collect metadata about the OS"""
651
        super(Windows, self)._do_collect_metadata()
652
        self.meta["USERS"] = " ".join(self._get_users())
653

    
654
    def _get_users(self):
655
        """Returns a list of users found in the images"""
656
        samfd, sam = tempfile.mkstemp()
657
        try:
658
            os.close(samfd)
659
            self.image.g.download(self._registry_file_path('SAM'), sam)
660

    
661
            h = hivex.Hivex(sam)
662

    
663
            # Navigate to /SAM/Domains/Account/Users
664
            users_node = h.root()
665
            for child in ('SAM', 'Domains', 'Account', 'Users'):
666
                users_node = h.node_get_child(users_node, child)
667

    
668
            # Navigate to /SAM/Domains/Account/Users/Names
669
            names_node = h.node_get_child(users_node, 'Names')
670

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

    
684
            users = []
685
            for user_node in h.node_children(names_node):
686
                username = h.node_name(user_node)
687
                rid = h.value_type(h.node_get_value(user_node, ""))[0]
688
                # if RID is 500 (=0x1f4), the corresponding node name under
689
                # Users is '000001F4'
690
                key = ("%8.x" % rid).replace(' ', '0').upper()
691
                rid_node = h.node_get_child(users_node, key)
692
                f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
693

    
694
                if disabled(f_value):
695
                    self.out.warn("Found disabled `%s' account!" % username)
696
                    continue
697

    
698
                users.append(username)
699

    
700
        finally:
701
            os.unlink(sam)
702

    
703
        # Filter out the guest account
704
        return users
705

    
706
    def _check_connectivity(self):
707
        """Check if winexe works on the Windows VM"""
708

    
709
        retries = self.sysprep_params['connection_retries']
710
        # If the connection_retries parameter is set to 0 disable the
711
        # connectivity check
712
        if retries == 0:
713
            return True
714

    
715
        passwd = self.sysprep_params['password']
716
        winexe = WinEXE('Administrator', passwd, 'localhost')
717
        winexe.uninstall().debug(9)
718

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

    
732
        raise FatalError("Connection to the Windows VM failed after %d retries"
733
                         % retries)
734

    
735
    def _guest_exec(self, command, fatal=True):
736
        """Execute a command on a windows VM"""
737

    
738
        passwd = self.sysprep_params['password']
739

    
740
        winexe = WinEXE('Administrator', passwd, 'localhost')
741
        winexe.runas('Administrator', passwd).uninstall()
742

    
743
        try:
744
            (stdout, stderr, rc) = winexe.run(command)
745
        except WinexeTimeout:
746
            FatalError("Command: `%s' timeout out." % command)
747

    
748
        if rc != 0 and fatal:
749
            reason = stderr if len(stderr) else stdout
750
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
751
                            (command, rc, reason))
752
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
753
                             (command, rc, reason))
754

    
755
        return (stdout, stderr, rc)
756

    
757

    
758
class _VM(object):
759
    """Windows Virtual Machine"""
760
    def __init__(self, disk, serial, params):
761
        """Create _VM instance
762

763
            disk: VM's hard disk
764
            serial: File to save the output of the serial port
765
        """
766

    
767
        self.disk = disk
768
        self.serial = serial
769
        self.params = params
770

    
771
        def random_mac():
772
            """creates a random mac address"""
773
            mac = [0x00, 0x16, 0x3e,
774
                   random.randint(0x00, 0x7f),
775
                   random.randint(0x00, 0xff),
776
                   random.randint(0x00, 0xff)]
777

    
778
            return ':'.join(['%02x' % x for x in mac])
779

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

    
783
        kvm, needed_args = get_kvm_binary()
784

    
785
        if kvm is None:
786
            FatalError("Can't find the kvm binary")
787

    
788
        args = [kvm]
789
        args.extend(needed_args)
790

    
791
        args.extend([
792
            '-smp', '1', '-m', '1024', '-drive',
793
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
794
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
795
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
796
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
797
            '-monitor', 'stdio'])
798

    
799
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
800
                                        stdout=subprocess.PIPE)
801

    
802
    def isalive(self):
803
        """Check if the VM is still alive"""
804
        return self.process.poll() is None
805

    
806
    def destroy(self):
807
        """Destroy the VM"""
808

    
809
        if not self.isalive():
810
            return
811

    
812
        def handler(signum, frame):
813
            self.process.terminate()
814
            time.sleep(1)
815
            if self.isalive():
816
                self.process.kill()
817
            self.process.wait()
818
            raise FatalError("VM destroy timed-out")
819

    
820
        signal.signal(signal.SIGALRM, handler)
821

    
822
        signal.alarm(self.params['shutdown_timeout'])
823
        self.process.communicate(input="system_powerdown\n")
824
        signal.alarm(0)
825

    
826
    def wait(self, timeout=0):
827
        """Wait for the VM to terminate"""
828

    
829
        def handler(signum, frame):
830
            self.destroy()
831
            raise FatalError("VM wait timed-out.")
832

    
833
        signal.signal(signal.SIGALRM, handler)
834

    
835
        signal.alarm(timeout)
836
        stdout, stderr = self.process.communicate()
837
        signal.alarm(0)
838

    
839
        return (stdout, stderr, self.process.poll())
840

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