Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 0f0137fc

History | View | Annotate | Download (32 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
import re
53

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

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

    
113

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

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

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

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

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

    
154
        assert self.system_drive
155

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
252
        querymax = None
253
        for line in stdout.splitlines():
254
            # diskpart will return something like this:
255
            #
256
            #   The maximum number of reclaimable bytes is: xxxx MB
257
            #
258
            if line.find('reclaimable') >= 0:
259
                answer = line.split(':')[1].strip()
260
                m = re.search('(\d+) MB', answer)
261
                if m:
262
                    querymax = m.group(1)
263
                else:
264
                    FatalError(
265
                        "Unexpected output for `shrink querymax' command: %s" %
266
                        line)
267

    
268
        if querymax is None:
269
            FatalError("Error in shrinking! "
270
                       "Couldn't find the max number of reclaimable bytes!")
271

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

    
280
        if querymax < 0:
281
            self.out.warn("Not enough available space to shrink the image!")
282
            return
283

    
284
        self.out.output("\tReclaiming %dMB ..." % querymax)
285

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

    
296
        stdout, stderr, rc = self._guest_exec(cmd, False)
297

    
298
        if rc != 0:
299
            FatalError("Shrinking failed. Please make sure the media is "
300
                       "defraged with a command like this: "
301
                       "`Defrag.exe /U /X /W'")
302
        for line in stdout.splitlines():
303
            if line.find('shrunk') >= 0:
304
                self.out.output(line)
305

    
306
    def do_sysprep(self):
307
        """Prepare system for image creation."""
308

    
309
        if getattr(self, 'syspreped', False):
310
            raise FatalError("Image is already syspreped!")
311

    
312
        txt = "System preparation parameter: `%s' is needed but missing!"
313
        for name, param in self.needed_sysprep_params.items():
314
            if name not in self.sysprep_params:
315
                raise FatalError(txt % name)
316

    
317
        self.mount(readonly=False)
318
        try:
319
            disabled_uac = self._update_uac_remote_setting(1)
320
            token = self._enable_os_monitor()
321

    
322
            # disable the firewalls
323
            firewall_states = self._update_firewalls(0, 0, 0)
324

    
325
            # Delete the pagefile. It will be recreated when the system boots
326
            systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
327
            try:
328
                pagefile = "%s/pagefile.sys" % systemroot
329
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
330
            except RuntimeError:
331
                pass
332

    
333
        finally:
334
            self.umount()
335

    
336
        self.image.disable_guestfs()
337

    
338
        vm = None
339
        monitor = None
340
        try:
341
            self.out.output("Starting windows VM ...", False)
342
            monitorfd, monitor = tempfile.mkstemp()
343
            os.close(monitorfd)
344
            vm = _VM(self.image.device, monitor, self.sysprep_params)
345
            self.out.success("started (console on vnc display: %d)." %
346
                             vm.display)
347

    
348
            self.out.output("Waiting for OS to boot ...", False)
349
            self._wait_vm_boot(vm, monitor, token)
350
            self.out.success('done')
351

    
352
            self.out.output("Checking connectivity to the VM ...", False)
353
            self._check_connectivity()
354
            self.out.success('done')
355

    
356
            self.out.output("Disabling automatic logon ...", False)
357
            self._disable_autologon()
358
            self.out.success('done')
359

    
360
            self.out.output('Preparing system for image creation:')
361

    
362
            tasks = self.list_syspreps()
363
            enabled = [task for task in tasks if task.enabled]
364
            size = len(enabled)
365

    
366
            # Make sure shrink runs in the end, before ms sysprep
367
            enabled = [task for task in enabled if
368
                       self.sysprep_info(task).name != 'shrink']
369

    
370
            if len(enabled) != size:
371
                enabled.append(self.shrink)
372

    
373
            # Make sure the ms sysprep is the last task to run if it is enabled
374
            enabled = [task for task in enabled if
375
                       self.sysprep_info(task).name != 'microsoft-sysprep']
376

    
377
            ms_sysprep_enabled = False
378
            if len(enabled) != size:
379
                enabled.append(self.microsoft_sysprep)
380
                ms_sysprep_enabled = True
381

    
382
            cnt = 0
383
            for task in enabled:
384
                cnt += 1
385
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
386
                task()
387
                setattr(task.im_func, 'executed', True)
388

    
389
            self.out.output("Sending shut down command ...", False)
390
            if not ms_sysprep_enabled:
391
                self._shutdown()
392
            self.out.success("done")
393

    
394
            self.out.output("Waiting for windows to shut down ...", False)
395
            vm.wait(self.sysprep_params['shutdown_timeout'])
396
            self.out.success("done")
397
        finally:
398
            if monitor is not None:
399
                os.unlink(monitor)
400

    
401
            try:
402
                if vm is not None:
403
                    self.out.output("Destroying windows VM ...", False)
404
                    vm.destroy()
405
                    self.out.success("done")
406
            finally:
407
                self.image.enable_guestfs()
408

    
409
                self.mount(readonly=False)
410
                try:
411
                    if disabled_uac:
412
                        self._update_uac_remote_setting(0)
413

    
414
                    self._update_firewalls(*firewall_states)
415
                finally:
416
                    self.umount()
417

    
418
    def _shutdown(self):
419
        """Shuts down the windows VM"""
420
        self._guest_exec(r'shutdown /s /t 5')
421

    
422
    def _wait_vm_boot(self, vm, fname, msg):
423
        """Wait until a message appears on a file or the vm process dies"""
424

    
425
        for _ in range(self.sysprep_params['boot_timeout']):
426
            time.sleep(1)
427
            with open(fname) as f:
428
                for line in f:
429
                    if line.startswith(msg):
430
                        return True
431
            if not vm.isalive():
432
                raise FatalError("Windows VM died unexpectedly!")
433

    
434
        raise FatalError("Windows VM booting timed out!")
435

    
436
    def _disable_autologon(self):
437
        """Disable automatic logon on the windows image"""
438

    
439
        winlogon = \
440
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
441

    
442
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
443
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
444
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
445

    
446
    def _registry_file_path(self, regfile):
447
        """Retrieves the case sensitive path to a registry file"""
448

    
449
        systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
450
        path = "%s/system32/config/%s" % (systemroot, regfile)
451
        try:
452
            path = self.image.g.case_sensitive_path(path)
453
        except RuntimeError as error:
454
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
455
                             % (regfile, str(error)))
456
        return path
457

    
458
    def _enable_os_monitor(self):
459
        """Add a script in the registry that will send a random string to the
460
        first serial port when the windows image finishes booting.
461
        """
462

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

    
465
        path = self._registry_file_path('SOFTWARE')
466
        softwarefd, software = tempfile.mkstemp()
467
        try:
468
            os.close(softwarefd)
469
            self.image.g.download(path, software)
470

    
471
            h = hivex.Hivex(software, write=True)
472

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

    
488
            winlogon = h.root()
489
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
490
                          'Winlogon'):
491
                winlogon = h.node_get_child(winlogon, child)
492

    
493
            h.node_set_value(
494
                winlogon,
495
                {'key': 'DefaultUserName', 't': 1,
496
                 'value': "Administrator".encode('utf-16le')})
497
            h.node_set_value(
498
                winlogon,
499
                {'key': 'DefaultPassword', 't': 1,
500
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
501
            h.node_set_value(
502
                winlogon,
503
                {'key': 'AutoAdminLogon', 't': 1,
504
                 'value': "1".encode('utf-16le')})
505

    
506
            key = h.root()
507
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
508
                key = h.node_get_child(key, child)
509

    
510
            runonce = h.node_get_child(key, "RunOnce")
511
            if runonce is None:
512
                runonce = h.node_add_child(key, "RunOnce")
513

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

    
521
            h.node_set_value(runonce,
522
                             {'key': "BootMonitor", 't': 1, 'value': value})
523

    
524
            value = (
525
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
526
                r'\policies\system /v LocalAccountTokenFilterPolicy'
527
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
528

    
529
            h.node_set_value(runonce,
530
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
531

    
532
            h.commit(None)
533

    
534
            self.image.g.upload(software, path)
535
        finally:
536
            os.unlink(software)
537

    
538
        return token
539

    
540
    def _update_firewalls(self, domain, public, standard):
541
        """Enables or disables the firewall for the Domain, the Public and the
542
        Standard profile. Returns a triplete with the old values.
543

544
        1 will enable a firewall and 0 will disable it
545
        """
546

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

    
550
        if public not in (0, 1):
551
            raise ValueError("Valid values for public parameter are 0 and 1")
552

    
553
        if standard not in (0, 1):
554
            raise ValueError("Valid values for standard parameter are 0 and 1")
555

    
556
        path = self._registry_file_path("SYSTEM")
557
        systemfd, system = tempfile.mkstemp()
558
        try:
559
            os.close(systemfd)
560
            self.image.g.download(path, system)
561

    
562
            h = hivex.Hivex(system, write=True)
563

    
564
            select = h.node_get_child(h.root(), 'Select')
565
            current_value = h.node_get_value(select, 'Current')
566

    
567
            # expecting a little endian dword
568
            assert h.value_type(current_value)[1] == 4
569
            current = "%03d" % h.value_dword(current_value)
570

    
571
            firewall_policy = h.root()
572
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
573
                          'Parameters', 'FirewallPolicy'):
574
                firewall_policy = h.node_get_child(firewall_policy, child)
575

    
576
            old_values = []
577
            new_values = [domain, public, standard]
578
            for profile in ('Domain', 'Public', 'Standard'):
579
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
580

    
581
                old_value = h.node_get_value(node, 'EnableFirewall')
582

    
583
                # expecting a little endian dword
584
                assert h.value_type(old_value)[1] == 4
585
                old_values.append(h.value_dword(old_value))
586

    
587
                h.node_set_value(
588
                    node, {'key': 'EnableFirewall', 't': 4L,
589
                           'value': struct.pack("<I", new_values.pop(0))})
590

    
591
            h.commit(None)
592
            self.image.g.upload(system, path)
593

    
594
        finally:
595
            os.unlink(system)
596

    
597
        return old_values
598

    
599
    def _update_uac_remote_setting(self, value):
600
        """Updates the registry key value:
601
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
602
        \System]"LocalAccountTokenFilterPolicy"
603

604
        value = 1 will disable the UAC remote restrictions
605
        value = 0 will enable the UAC remote restrictions
606

607
        For more info see here: http://support.microsoft.com/kb/951016
608

609
        Returns:
610
            True if the key is changed
611
            False if the key is unchanged
612
        """
613

    
614
        if value not in (0, 1):
615
            raise ValueError("Valid values for value parameter are 0 and 1")
616

    
617
        path = self._registry_file_path('SOFTWARE')
618
        softwarefd, software = tempfile.mkstemp()
619
        try:
620
            os.close(softwarefd)
621
            self.image.g.download(path, software)
622

    
623
            h = hivex.Hivex(software, write=True)
624

    
625
            key = h.root()
626
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
627
                          'System'):
628
                key = h.node_get_child(key, child)
629

    
630
            policy = None
631
            for val in h.node_values(key):
632
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
633
                    policy = val
634

    
635
            if policy is not None:
636
                dword = h.value_dword(policy)
637
                if dword == value:
638
                    return False
639
            elif value == 0:
640
                return False
641

    
642
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
643
                         'value': struct.pack("<I", value)}
644

    
645
            h.node_set_value(key, new_value)
646
            h.commit(None)
647

    
648
            self.image.g.upload(software, path)
649

    
650
        finally:
651
            os.unlink(software)
652

    
653
        return True
654

    
655
    def _do_collect_metadata(self):
656
        """Collect metadata about the OS"""
657
        super(Windows, self)._do_collect_metadata()
658
        self.meta["USERS"] = " ".join(self._get_users())
659

    
660
    def _get_users(self):
661
        """Returns a list of users found in the images"""
662
        samfd, sam = tempfile.mkstemp()
663
        try:
664
            os.close(samfd)
665
            self.image.g.download(self._registry_file_path('SAM'), sam)
666

    
667
            h = hivex.Hivex(sam)
668

    
669
            # Navigate to /SAM/Domains/Account/Users
670
            users_node = h.root()
671
            for child in ('SAM', 'Domains', 'Account', 'Users'):
672
                users_node = h.node_get_child(users_node, child)
673

    
674
            # Navigate to /SAM/Domains/Account/Users/Names
675
            names_node = h.node_get_child(users_node, 'Names')
676

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

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

    
700
                if disabled(f_value):
701
                    self.out.warn("Found disabled `%s' account!" % username)
702
                    continue
703

    
704
                users.append(username)
705

    
706
        finally:
707
            os.unlink(sam)
708

    
709
        # Filter out the guest account
710
        return users
711

    
712
    def _check_connectivity(self):
713
        """Check if winexe works on the Windows VM"""
714

    
715
        retries = self.sysprep_params['connection_retries']
716
        # If the connection_retries parameter is set to 0 disable the
717
        # connectivity check
718
        if retries == 0:
719
            return True
720

    
721
        passwd = self.sysprep_params['password']
722
        winexe = WinEXE('Administrator', passwd, 'localhost')
723
        winexe.uninstall().debug(9)
724

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

    
738
        raise FatalError("Connection to the Windows VM failed after %d retries"
739
                         % retries)
740

    
741
    def _guest_exec(self, command, fatal=True):
742
        """Execute a command on a windows VM"""
743

    
744
        passwd = self.sysprep_params['password']
745

    
746
        winexe = WinEXE('Administrator', passwd, 'localhost')
747
        winexe.runas('Administrator', passwd).uninstall()
748

    
749
        try:
750
            (stdout, stderr, rc) = winexe.run(command)
751
        except WinexeTimeout:
752
            FatalError("Command: `%s' timeout out." % command)
753

    
754
        if rc != 0 and fatal:
755
            reason = stderr if len(stderr) else stdout
756
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
757
                            (command, rc, reason))
758
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
759
                             (command, rc, reason))
760

    
761
        return (stdout, stderr, rc)
762

    
763

    
764
class _VM(object):
765
    """Windows Virtual Machine"""
766
    def __init__(self, disk, serial, params):
767
        """Create _VM instance
768

769
            disk: VM's hard disk
770
            serial: File to save the output of the serial port
771
        """
772

    
773
        self.disk = disk
774
        self.serial = serial
775
        self.params = params
776

    
777
        def random_mac():
778
            """creates a random mac address"""
779
            mac = [0x00, 0x16, 0x3e,
780
                   random.randint(0x00, 0x7f),
781
                   random.randint(0x00, 0xff),
782
                   random.randint(0x00, 0xff)]
783

    
784
            return ':'.join(['%02x' % x for x in mac])
785

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

    
789
        kvm, needed_args = get_kvm_binary()
790

    
791
        if kvm is None:
792
            FatalError("Can't find the kvm binary")
793

    
794
        args = [kvm]
795
        args.extend(needed_args)
796

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

    
805
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
806
                                        stdout=subprocess.PIPE)
807

    
808
    def isalive(self):
809
        """Check if the VM is still alive"""
810
        return self.process.poll() is None
811

    
812
    def destroy(self):
813
        """Destroy the VM"""
814

    
815
        if not self.isalive():
816
            return
817

    
818
        def handler(signum, frame):
819
            self.process.terminate()
820
            time.sleep(1)
821
            if self.isalive():
822
                self.process.kill()
823
            self.process.wait()
824
            raise FatalError("VM destroy timed-out")
825

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

    
828
        signal.alarm(self.params['shutdown_timeout'])
829
        self.process.communicate(input="system_powerdown\n")
830
        signal.alarm(0)
831

    
832
    def wait(self, timeout=0):
833
        """Wait for the VM to terminate"""
834

    
835
        def handler(signum, frame):
836
            self.destroy()
837
            raise FatalError("VM wait timed-out.")
838

    
839
        signal.signal(signal.SIGALRM, handler)
840

    
841
        signal.alarm(timeout)
842
        stdout, stderr = self.process.communicate()
843
        signal.alarm(0)
844

    
845
        return (stdout, stderr, self.process.poll())
846

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