Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ f953c647

History | View | Annotate | Download (32.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
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(
123
        'smp', int, 1, "Number of CPUs for the helper VM", _POSINT)
124
    @add_sysprep_param(
125
        'mem', int, 1024, "Virtual RAM size for the helper VM (MiB)", _POSINT)
126
    @add_sysprep_param('password', str, None, 'Image Administrator Password')
127
    def __init__(self, image, **kargs):
128
        super(Windows, self).__init__(image, **kargs)
129

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

    
146
        # Check if winexe is installed
147
        if not WinEXE.is_installed():
148
            raise FatalError(
149
                "For windows support `Winexe' needs to be installed")
150

    
151
        device = self.image.g.part_to_dev(self.root)
152

    
153
        self.last_part_num = self.image.g.part_list(device)[-1]['part_num']
154
        self.last_drive = None
155
        self.system_drive = None
156

    
157
        for drive, part in self.image.g.inspect_get_drive_mappings(self.root):
158
            if part == "%s%d" % (device, self.last_part_num):
159
                self.last_drive = drive
160
            if part == self.root:
161
                self.system_drive = drive
162

    
163
        assert self.system_drive
164

    
165
        self.product_name = self.image.g.inspect_get_product_name(self.root)
166
        self.syspreped = False
167

    
168
    @sysprep('Disabling IPv6 privacy extensions')
169
    def disable_ipv6_privacy_extensions(self):
170
        """Disable IPv6 privacy extensions"""
171

    
172
        self._guest_exec('netsh interface ipv6 set global '
173
                         'randomizeidentifiers=disabled store=persistent')
174

    
175
    @sysprep('Disabling Teredo interface')
176
    def disable_teredo(self):
177
        """Disable Teredo interface"""
178

    
179
        self._guest_exec('netsh interface teredo set state disabled')
180

    
181
    @sysprep('Disabling ISATAP Adapters')
182
    def disable_isatap(self):
183
        """Disable ISATAP Adapters"""
184

    
185
        self._guest_exec('netsh interface isa set state disabled')
186

    
187
    @sysprep('Enabling ping responses')
188
    def enable_pings(self):
189
        """Enable ping responses"""
190

    
191
        self._guest_exec('netsh firewall set icmpsetting 8')
192

    
193
    @sysprep('Disabling hibernation support')
194
    def disable_hibernation(self):
195
        """Disable hibernation support and remove the hibernation file"""
196

    
197
        self._guest_exec(r'powercfg.exe /hibernate off')
198

    
199
    @sysprep('Setting the system clock to UTC')
200
    def utc(self):
201
        """Set the hardware clock to UTC"""
202

    
203
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
204
        self._guest_exec(
205
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
206

    
207
    @sysprep('Clearing the event logs')
208
    def clear_logs(self):
209
        """Clear all the event logs"""
210

    
211
        self._guest_exec(
212
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
213

    
214
    @sysprep('Executing Sysprep on the image (may take more that 10 min)')
215
    def microsoft_sysprep(self):
216
        """Run the Microsoft System Preparation Tool. This will remove
217
        system-specific data and will make the image ready to be deployed.
218
        After this no other task may run.
219
        """
220

    
221
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
222
                         r'/quiet /generalize /oobe /shutdown')
223
        self.syspreped = True
224

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

    
241
        self._guest_exec(
242
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
243

    
244
    @sysprep('Shrinking the last filesystem')
245
    def shrink(self):
246
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
247

    
248
        # Query for the maximum number of reclaimable bytes
249
        cmd = (
250
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
251
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
252
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
253
            r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
254
            r'ECHO EXIT >> %SCRIPT% & ' +
255
            r'DISKPART /S %SCRIPT% & ' +
256
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
257
            r'DEL /Q %SCRIPT%"')
258

    
259
        stdout, stderr, rc = self._guest_exec(cmd)
260

    
261
        querymax = None
262
        for line in stdout.splitlines():
263
            # diskpart will return something like this:
264
            #
265
            #   The maximum number of reclaimable bytes is: xxxx MB
266
            #
267
            if line.find('reclaimable') >= 0:
268
                answer = line.split(':')[1].strip()
269
                m = re.search('(\d+) MB', answer)
270
                if m:
271
                    querymax = m.group(1)
272
                else:
273
                    FatalError(
274
                        "Unexpected output for `shrink querymax' command: %s" %
275
                        line)
276

    
277
        if querymax is None:
278
            FatalError("Error in shrinking! "
279
                       "Couldn't find the max number of reclaimable bytes!")
280

    
281
        querymax = int(querymax)
282
        # From ntfsresize:
283
        # Practically the smallest shrunken size generally is at around
284
        # "used space" + (20-200 MB). Please also take into account that
285
        # Windows might need about 50-100 MB free space left to boot safely.
286
        # I'll give 100MB extra space just to be sure
287
        querymax -= 100
288

    
289
        if querymax < 0:
290
            self.out.warn("Not enough available space to shrink the image!")
291
            return
292

    
293
        self.out.output("\tReclaiming %dMB ..." % querymax)
294

    
295
        cmd = (
296
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
297
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
298
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
299
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
300
            r'ECHO EXIT >> %SCRIPT% & ' +
301
            r'DISKPART /S %SCRIPT% & ' +
302
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
303
            r'DEL /Q %SCRIPT%"')
304

    
305
        stdout, stderr, rc = self._guest_exec(cmd, False)
306

    
307
        if rc != 0:
308
            FatalError("Shrinking failed. Please make sure the media is "
309
                       "defraged with a command like this: "
310
                       "`Defrag.exe /U /X /W'")
311
        for line in stdout.splitlines():
312
            if line.find('shrunk') >= 0:
313
                self.out.output(line)
314

    
315
    def do_sysprep(self):
316
        """Prepare system for image creation."""
317

    
318
        if getattr(self, 'syspreped', False):
319
            raise FatalError("Image is already syspreped!")
320

    
321
        txt = "System preparation parameter: `%s' is needed but missing!"
322
        for name, param in self.needed_sysprep_params.items():
323
            if name not in self.sysprep_params:
324
                raise FatalError(txt % name)
325

    
326
        self.mount(readonly=False)
327
        try:
328
            disabled_uac = self._update_uac_remote_setting(1)
329
            token = self._enable_os_monitor()
330

    
331
            # disable the firewalls
332
            firewall_states = self._update_firewalls(0, 0, 0)
333

    
334
            # Delete the pagefile. It will be recreated when the system boots
335
            systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
336
            try:
337
                pagefile = "%s/pagefile.sys" % systemroot
338
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
339
            except RuntimeError:
340
                pass
341

    
342
        finally:
343
            self.umount()
344

    
345
        self.image.disable_guestfs()
346

    
347
        vm = None
348
        monitor = None
349
        try:
350
            self.out.output("Starting windows VM ...", False)
351
            monitorfd, monitor = tempfile.mkstemp()
352
            os.close(monitorfd)
353
            vm = _VM(self.image.device, monitor, self.sysprep_params)
354
            self.out.success("started (console on VNC display: %d)" %
355
                             vm.display)
356

    
357
            self.out.output("Waiting for OS to boot ...", False)
358
            self._wait_vm_boot(vm, monitor, token)
359
            self.out.success('done')
360

    
361
            self.out.output("Checking connectivity to the VM ...", False)
362
            self._check_connectivity()
363
            self.out.success('done')
364

    
365
            self.out.output("Disabling automatic logon ...", False)
366
            self._disable_autologon()
367
            self.out.success('done')
368

    
369
            self.out.output('Preparing system for image creation:')
370

    
371
            tasks = self.list_syspreps()
372
            enabled = [task for task in tasks if task.enabled]
373
            size = len(enabled)
374

    
375
            # Make sure shrink runs in the end, before ms sysprep
376
            enabled = [task for task in enabled if
377
                       self.sysprep_info(task).name != 'shrink']
378

    
379
            if len(enabled) != size:
380
                enabled.append(self.shrink)
381

    
382
            # Make sure the ms sysprep is the last task to run if it is enabled
383
            enabled = [task for task in enabled if
384
                       self.sysprep_info(task).name != 'microsoft-sysprep']
385

    
386
            ms_sysprep_enabled = False
387
            if len(enabled) != size:
388
                enabled.append(self.microsoft_sysprep)
389
                ms_sysprep_enabled = True
390

    
391
            cnt = 0
392
            for task in enabled:
393
                cnt += 1
394
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
395
                task()
396
                setattr(task.im_func, 'executed', True)
397

    
398
            self.out.output("Sending shut down command ...", False)
399
            if not ms_sysprep_enabled:
400
                self._shutdown()
401
            self.out.success("done")
402

    
403
            self.out.output("Waiting for windows to shut down ...", False)
404
            vm.wait(self.sysprep_params['shutdown_timeout'])
405
            self.out.success("done")
406
        finally:
407
            if monitor is not None:
408
                os.unlink(monitor)
409

    
410
            try:
411
                if vm is not None:
412
                    self.out.output("Destroying windows VM ...", False)
413
                    vm.destroy()
414
                    self.out.success("done")
415
            finally:
416
                self.image.enable_guestfs()
417

    
418
                self.mount(readonly=False)
419
                try:
420
                    if disabled_uac:
421
                        self._update_uac_remote_setting(0)
422

    
423
                    self._update_firewalls(*firewall_states)
424
                finally:
425
                    self.umount()
426

    
427
    def _shutdown(self):
428
        """Shuts down the windows VM"""
429
        self._guest_exec(r'shutdown /s /t 5')
430

    
431
    def _wait_vm_boot(self, vm, fname, msg):
432
        """Wait until a message appears on a file or the vm process dies"""
433

    
434
        for _ in range(self.sysprep_params['boot_timeout']):
435
            time.sleep(1)
436
            with open(fname) as f:
437
                for line in f:
438
                    if line.startswith(msg):
439
                        return True
440
            if not vm.isalive():
441
                raise FatalError("Windows VM died unexpectedly!")
442

    
443
        raise FatalError("Windows VM booting timed out!")
444

    
445
    def _disable_autologon(self):
446
        """Disable automatic logon on the windows image"""
447

    
448
        winlogon = \
449
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
450

    
451
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
452
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
453
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
454

    
455
    def _registry_file_path(self, regfile):
456
        """Retrieves the case sensitive path to a registry file"""
457

    
458
        systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
459
        path = "%s/system32/config/%s" % (systemroot, regfile)
460
        try:
461
            path = self.image.g.case_sensitive_path(path)
462
        except RuntimeError as error:
463
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
464
                             % (regfile, str(error)))
465
        return path
466

    
467
    def _enable_os_monitor(self):
468
        """Add a script in the registry that will send a random string to the
469
        first serial port when the windows image finishes booting.
470
        """
471

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

    
474
        path = self._registry_file_path('SOFTWARE')
475
        softwarefd, software = tempfile.mkstemp()
476
        try:
477
            os.close(softwarefd)
478
            self.image.g.download(path, software)
479

    
480
            h = hivex.Hivex(software, write=True)
481

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

    
497
            winlogon = h.root()
498
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
499
                          'Winlogon'):
500
                winlogon = h.node_get_child(winlogon, child)
501

    
502
            h.node_set_value(
503
                winlogon,
504
                {'key': 'DefaultUserName', 't': 1,
505
                 'value': "Administrator".encode('utf-16le')})
506
            h.node_set_value(
507
                winlogon,
508
                {'key': 'DefaultPassword', 't': 1,
509
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
510
            h.node_set_value(
511
                winlogon,
512
                {'key': 'AutoAdminLogon', 't': 1,
513
                 'value': "1".encode('utf-16le')})
514

    
515
            key = h.root()
516
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
517
                key = h.node_get_child(key, child)
518

    
519
            runonce = h.node_get_child(key, "RunOnce")
520
            if runonce is None:
521
                runonce = h.node_add_child(key, "RunOnce")
522

    
523
            value = (
524
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
525
                r'-ExecutionPolicy RemoteSigned '
526
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
527
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
528
                r'$port.Close()}"').encode('utf-16le')
529

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

    
533
            value = (
534
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
535
                r'\policies\system /v LocalAccountTokenFilterPolicy'
536
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
537

    
538
            h.node_set_value(runonce,
539
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
540

    
541
            h.commit(None)
542

    
543
            self.image.g.upload(software, path)
544
        finally:
545
            os.unlink(software)
546

    
547
        return token
548

    
549
    def _update_firewalls(self, domain, public, standard):
550
        """Enables or disables the firewall for the Domain, the Public and the
551
        Standard profile. Returns a triplete with the old values.
552

553
        1 will enable a firewall and 0 will disable it
554
        """
555

    
556
        if domain not in (0, 1):
557
            raise ValueError("Valid values for domain parameter are 0 and 1")
558

    
559
        if public not in (0, 1):
560
            raise ValueError("Valid values for public parameter are 0 and 1")
561

    
562
        if standard not in (0, 1):
563
            raise ValueError("Valid values for standard parameter are 0 and 1")
564

    
565
        path = self._registry_file_path("SYSTEM")
566
        systemfd, system = tempfile.mkstemp()
567
        try:
568
            os.close(systemfd)
569
            self.image.g.download(path, system)
570

    
571
            h = hivex.Hivex(system, write=True)
572

    
573
            select = h.node_get_child(h.root(), 'Select')
574
            current_value = h.node_get_value(select, 'Current')
575

    
576
            # expecting a little endian dword
577
            assert h.value_type(current_value)[1] == 4
578
            current = "%03d" % h.value_dword(current_value)
579

    
580
            firewall_policy = h.root()
581
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
582
                          'Parameters', 'FirewallPolicy'):
583
                firewall_policy = h.node_get_child(firewall_policy, child)
584

    
585
            old_values = []
586
            new_values = [domain, public, standard]
587
            for profile in ('Domain', 'Public', 'Standard'):
588
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
589

    
590
                old_value = h.node_get_value(node, 'EnableFirewall')
591

    
592
                # expecting a little endian dword
593
                assert h.value_type(old_value)[1] == 4
594
                old_values.append(h.value_dword(old_value))
595

    
596
                h.node_set_value(
597
                    node, {'key': 'EnableFirewall', 't': 4L,
598
                           'value': struct.pack("<I", new_values.pop(0))})
599

    
600
            h.commit(None)
601
            self.image.g.upload(system, path)
602

    
603
        finally:
604
            os.unlink(system)
605

    
606
        return old_values
607

    
608
    def _update_uac_remote_setting(self, value):
609
        """Updates the registry key value:
610
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
611
        \System]"LocalAccountTokenFilterPolicy"
612

613
        value = 1 will disable the UAC remote restrictions
614
        value = 0 will enable the UAC remote restrictions
615

616
        For more info see here: http://support.microsoft.com/kb/951016
617

618
        Returns:
619
            True if the key is changed
620
            False if the key is unchanged
621
        """
622

    
623
        if value not in (0, 1):
624
            raise ValueError("Valid values for value parameter are 0 and 1")
625

    
626
        path = self._registry_file_path('SOFTWARE')
627
        softwarefd, software = tempfile.mkstemp()
628
        try:
629
            os.close(softwarefd)
630
            self.image.g.download(path, software)
631

    
632
            h = hivex.Hivex(software, write=True)
633

    
634
            key = h.root()
635
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
636
                          'System'):
637
                key = h.node_get_child(key, child)
638

    
639
            policy = None
640
            for val in h.node_values(key):
641
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
642
                    policy = val
643

    
644
            if policy is not None:
645
                dword = h.value_dword(policy)
646
                if dword == value:
647
                    return False
648
            elif value == 0:
649
                return False
650

    
651
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
652
                         'value': struct.pack("<I", value)}
653

    
654
            h.node_set_value(key, new_value)
655
            h.commit(None)
656

    
657
            self.image.g.upload(software, path)
658

    
659
        finally:
660
            os.unlink(software)
661

    
662
        return True
663

    
664
    def _do_collect_metadata(self):
665
        """Collect metadata about the OS"""
666
        super(Windows, self)._do_collect_metadata()
667
        self.meta["USERS"] = " ".join(self._get_users())
668

    
669
    def _get_users(self):
670
        """Returns a list of users found in the images"""
671
        samfd, sam = tempfile.mkstemp()
672
        try:
673
            os.close(samfd)
674
            self.image.g.download(self._registry_file_path('SAM'), sam)
675

    
676
            h = hivex.Hivex(sam)
677

    
678
            # Navigate to /SAM/Domains/Account/Users
679
            users_node = h.root()
680
            for child in ('SAM', 'Domains', 'Account', 'Users'):
681
                users_node = h.node_get_child(users_node, child)
682

    
683
            # Navigate to /SAM/Domains/Account/Users/Names
684
            names_node = h.node_get_child(users_node, 'Names')
685

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

    
699
            users = []
700
            for user_node in h.node_children(names_node):
701
                username = h.node_name(user_node)
702
                rid = h.value_type(h.node_get_value(user_node, ""))[0]
703
                # if RID is 500 (=0x1f4), the corresponding node name under
704
                # Users is '000001F4'
705
                key = ("%8.x" % rid).replace(' ', '0').upper()
706
                rid_node = h.node_get_child(users_node, key)
707
                f_value = h.value_value(h.node_get_value(rid_node, 'F'))[1]
708

    
709
                if disabled(f_value):
710
                    self.out.warn("Found disabled `%s' account!" % username)
711
                    continue
712

    
713
                users.append(username)
714

    
715
        finally:
716
            os.unlink(sam)
717

    
718
        # Filter out the guest account
719
        return users
720

    
721
    def _check_connectivity(self):
722
        """Check if winexe works on the Windows VM"""
723

    
724
        retries = self.sysprep_params['connection_retries']
725
        # If the connection_retries parameter is set to 0 disable the
726
        # connectivity check
727
        if retries == 0:
728
            return True
729

    
730
        passwd = self.sysprep_params['password']
731
        winexe = WinEXE('Administrator', passwd, 'localhost')
732
        winexe.uninstall().debug(9)
733

    
734
        for i in range(retries):
735
            (stdout, stderr, rc) = winexe.run('cmd /C')
736
            if rc == 0:
737
                return True
738
            log = tempfile.NamedTemporaryFile(delete=False)
739
            try:
740
                log.file.write(stdout)
741
            finally:
742
                log.close()
743
            self.out.output("failed! See: `%s' for the full output" % log.name)
744
            if i < retries - 1:
745
                self.out.output("retrying ...", False)
746

    
747
        raise FatalError("Connection to the Windows VM failed after %d retries"
748
                         % retries)
749

    
750
    def _guest_exec(self, command, fatal=True):
751
        """Execute a command on a windows VM"""
752

    
753
        passwd = self.sysprep_params['password']
754

    
755
        winexe = WinEXE('Administrator', passwd, 'localhost')
756
        winexe.runas('Administrator', passwd).uninstall()
757

    
758
        try:
759
            (stdout, stderr, rc) = winexe.run(command)
760
        except WinexeTimeout:
761
            FatalError("Command: `%s' timeout out." % command)
762

    
763
        if rc != 0 and fatal:
764
            reason = stderr if len(stderr) else stdout
765
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
766
                            (command, rc, reason))
767
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
768
                             (command, rc, reason))
769

    
770
        return (stdout, stderr, rc)
771

    
772

    
773
class _VM(object):
774
    """Windows Virtual Machine"""
775
    def __init__(self, disk, serial, params):
776
        """Create _VM instance
777

778
            disk: VM's hard disk
779
            serial: File to save the output of the serial port
780
        """
781

    
782
        self.disk = disk
783
        self.serial = serial
784
        self.params = params
785

    
786
        def random_mac():
787
            """creates a random mac address"""
788
            mac = [0x00, 0x16, 0x3e,
789
                   random.randint(0x00, 0x7f),
790
                   random.randint(0x00, 0xff),
791
                   random.randint(0x00, 0xff)]
792

    
793
            return ':'.join(['%02x' % x for x in mac])
794

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

    
798
        kvm, needed_args = get_kvm_binary()
799

    
800
        if kvm is None:
801
            FatalError("Can't find the kvm binary")
802

    
803
        args = [kvm]
804
        args.extend(needed_args)
805

    
806
        args.extend([
807
            '-smp', str(self.params['smp']), '-m', str(self.params['mem']),
808
            '-drive', 'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
809
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
810
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
811
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
812
            '-monitor', 'stdio'])
813

    
814
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
815
                                        stdout=subprocess.PIPE)
816

    
817
    def isalive(self):
818
        """Check if the VM is still alive"""
819
        return self.process.poll() is None
820

    
821
    def destroy(self):
822
        """Destroy the VM"""
823

    
824
        if not self.isalive():
825
            return
826

    
827
        def handler(signum, frame):
828
            self.process.terminate()
829
            time.sleep(1)
830
            if self.isalive():
831
                self.process.kill()
832
            self.process.wait()
833
            raise FatalError("VM destroy timed-out")
834

    
835
        signal.signal(signal.SIGALRM, handler)
836

    
837
        signal.alarm(self.params['shutdown_timeout'])
838
        self.process.communicate(input="system_powerdown\n")
839
        signal.alarm(0)
840

    
841
    def wait(self, timeout=0):
842
        """Wait for the VM to terminate"""
843

    
844
        def handler(signum, frame):
845
            self.destroy()
846
            raise FatalError("VM wait timed-out.")
847

    
848
        signal.signal(signal.SIGALRM, handler)
849

    
850
        signal.alarm(timeout)
851
        stdout, stderr = self.process.communicate()
852
        signal.alarm(0)
853

    
854
        return (stdout, stderr, self.process.poll())
855

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