Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 0611e6dd

History | View | Annotate | Download (31.1 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, check_guestfs_version, \
41
    get_kvm_binary
42
from image_creator.winexe import WinEXE, WinexeTimeout
43

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

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

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

    
106

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

    
119
        # This commit was added in libguestfs 1.17.18 and is critical because
120
        # Microsoft Sysprep removes this key:
121
        #
122
        # When a Windows guest doesn't have a HKLM\SYSTEM\MountedDevices node,
123
        # inspection fails.  However inspection should not completely fail just
124
        # because we cannot get the drive letter mapping from a guest.
125
        if check_guestfs_version(self.image.g, 1, 17, 18) < 0:
126
            raise FatalError(
127
                'For windows support libguestfs 1.17.18 or above is needed')
128

    
129
        device = self.image.g.part_to_dev(self.root)
130

    
131
        self.last_part_num = self.image.g.part_list(device)[-1]['part_num']
132
        self.last_drive = None
133
        self.system_drive = None
134

    
135
        for drive, part in self.image.g.inspect_get_drive_mappings(self.root):
136
            if part == "%s%d" % (device, self.last_part_num):
137
                self.last_drive = drive
138
            if part == self.root:
139
                self.system_drive = drive
140

    
141
        assert self.system_drive
142

    
143
        self.product_name = self.image.g.inspect_get_product_name(self.root)
144
        self.syspreped = False
145

    
146
    @sysprep('Disabling IPv6 privacy extensions')
147
    def disable_ipv6_privacy_extensions(self):
148
        """Disable IPv6 privacy extensions"""
149

    
150
        self._guest_exec('netsh interface ipv6 set global '
151
                         'randomizeidentifiers=disabled store=persistent')
152

    
153
    @sysprep('Disabling Teredo interface')
154
    def disable_teredo(self):
155
        """Disable Teredo interface"""
156

    
157
        self._guest_exec('netsh interface teredo set state disabled')
158

    
159
    @sysprep('Disabling ISATAP Adapters')
160
    def disable_isatap(self):
161
        """Disable ISATAP Adapters"""
162

    
163
        self._guest_exec('netsh interface isa set state disabled')
164

    
165
    @sysprep('Enabling ping responses')
166
    def enable_pings(self):
167
        """Enable ping responses"""
168

    
169
        self._guest_exec('netsh firewall set icmpsetting 8')
170

    
171
    @sysprep('Disabling hibernation support')
172
    def disable_hibernation(self):
173
        """Disable hibernation support and remove the hibernation file"""
174

    
175
        self._guest_exec(r'powercfg.exe /hibernate off')
176

    
177
    @sysprep('Setting the system clock to UTC')
178
    def utc(self):
179
        """Set the hardware clock to UTC"""
180

    
181
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
182
        self._guest_exec(
183
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
184

    
185
    @sysprep('Clearing the event logs')
186
    def clear_logs(self):
187
        """Clear all the event logs"""
188

    
189
        self._guest_exec(
190
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
191

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

    
199
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
200
                         r'/quiet /generalize /oobe /shutdown')
201
        self.syspreped = True
202

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

    
219
        self._guest_exec(
220
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
221

    
222
    @sysprep('Shrinking the last filesystem')
223
    def shrink(self):
224
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
225

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

    
237
        stdout, stderr, rc = self._guest_exec(cmd)
238

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

    
250
        if querymax is None:
251
            FatalError("Error in shrinking! "
252
                       "Couldn't find the max number of reclaimable bytes!")
253

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

    
262
        if querymax < 0:
263
            self.out.warn("Not enought available space to shrink the image!")
264
            return
265

    
266
        self.out.output("\tReclaiming %dMB ..." % querymax)
267

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

    
278
        stdout, stderr, rc = self._guest_exec(cmd, False)
279

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

    
288
    def do_sysprep(self):
289
        """Prepare system for image creation."""
290

    
291
        if getattr(self, 'syspreped', False):
292
            raise FatalError("Image is already syspreped!")
293

    
294
        txt = "System preparation parameter: `%s' is needed but missing!"
295
        for name, param in self.needed_sysprep_params.items():
296
            if name not in self.sysprep_params:
297
                raise FatalError(txt % name)
298

    
299
        self.mount(readonly=False)
300
        try:
301
            disabled_uac = self._update_uac_remote_setting(1)
302
            token = self._enable_os_monitor()
303

    
304
            # disable the firewalls
305
            firewall_states = self._update_firewalls(0, 0, 0)
306

    
307
            # Delete the pagefile. It will be recreated when the system boots
308
            systemroot = self.image.g.inspect_get_windows_systemroot(self.root)
309
            try:
310
                pagefile = "%s/pagefile.sys" % systemroot
311
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
312
            except RuntimeError:
313
                pass
314

    
315
        finally:
316
            self.umount()
317

    
318
        self.image.disable_guestfs()
319

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

    
330
            self.out.output("Waiting for OS to boot ...", False)
331
            self._wait_vm_boot(vm, monitor, token)
332
            self.out.success('done')
333

    
334
            self.out.output("Checking connectivity to the VM ...", False)
335
            self._check_connectivity()
336
            self.out.success('done')
337

    
338
            self.out.output("Disabling automatic logon ...", False)
339
            self._disable_autologon()
340
            self.out.success('done')
341

    
342
            self.out.output('Preparing system for image creation:')
343

    
344
            tasks = self.list_syspreps()
345
            enabled = [task for task in tasks if task.enabled]
346
            size = len(enabled)
347

    
348
            # Make sure shrink runs in the end, before ms sysprep
349
            enabled = [task for task in enabled if
350
                       self.sysprep_info(task).name != 'shrink']
351

    
352
            if len(enabled) != size:
353
                enabled.append(self.shrink)
354

    
355
            # Make sure the ms sysprep is the last task to run if it is enabled
356
            enabled = [task for task in enabled if
357
                       self.sysprep_info(task).name != 'microsoft-sysprep']
358

    
359
            ms_sysprep_enabled = False
360
            if len(enabled) != size:
361
                enabled.append(self.microsoft_sysprep)
362
                ms_sysprep_enabled = True
363

    
364
            cnt = 0
365
            for task in enabled:
366
                cnt += 1
367
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
368
                task()
369
                setattr(task.im_func, 'executed', True)
370

    
371
            self.out.output("Sending shut down command ...", False)
372
            if not ms_sysprep_enabled:
373
                self._shutdown()
374
            self.out.success("done")
375

    
376
            self.out.output("Waiting for windows to shut down ...", False)
377
            vm.wait(self.sysprep_params['shutdown_timeout'])
378
            self.out.success("done")
379
        finally:
380
            if monitor is not None:
381
                os.unlink(monitor)
382

    
383
            try:
384
                if vm is not None:
385
                    self.out.output("Destroying windows VM ...", False)
386
                    vm.destroy()
387
                    self.out.success("done")
388
            finally:
389
                self.image.enable_guestfs()
390

    
391
                self.mount(readonly=False)
392
                try:
393
                    if disabled_uac:
394
                        self._update_uac_remote_setting(0)
395

    
396
                    self._update_firewalls(*firewall_states)
397
                finally:
398
                    self.umount()
399

    
400
    def _shutdown(self):
401
        """Shuts down the windows VM"""
402
        self._guest_exec(r'shutdown /s /t 5')
403

    
404
    def _wait_vm_boot(self, vm, fname, msg):
405
        """Wait until a message appears on a file or the vm process dies"""
406

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

    
416
        raise FatalError("Windows VM booting timed out!")
417

    
418
    def _disable_autologon(self):
419
        """Disable automatic logon on the windows image"""
420

    
421
        winlogon = \
422
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
423

    
424
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
425
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
426
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
427

    
428
    def _registry_file_path(self, regfile):
429
        """Retrieves the case sensitive path to a registry file"""
430

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

    
440
    def _enable_os_monitor(self):
441
        """Add a script in the registry that will send a random string to the
442
        first serial port when the windows image finishes booting.
443
        """
444

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

    
447
        path = self._registry_file_path('SOFTWARE')
448
        softwarefd, software = tempfile.mkstemp()
449
        try:
450
            os.close(softwarefd)
451
            self.image.g.download(path, software)
452

    
453
            h = hivex.Hivex(software, write=True)
454

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

    
470
            winlogon = h.root()
471
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
472
                          'Winlogon'):
473
                winlogon = h.node_get_child(winlogon, child)
474

    
475
            h.node_set_value(
476
                winlogon,
477
                {'key': 'DefaultUserName', 't': 1,
478
                 'value': "Administrator".encode('utf-16le')})
479
            h.node_set_value(
480
                winlogon,
481
                {'key': 'DefaultPassword', 't': 1,
482
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
483
            h.node_set_value(
484
                winlogon,
485
                {'key': 'AutoAdminLogon', 't': 1,
486
                 'value': "1".encode('utf-16le')})
487

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

    
492
            runonce = h.node_get_child(key, "RunOnce")
493
            if runonce is None:
494
                runonce = h.node_add_child(key, "RunOnce")
495

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

    
503
            h.node_set_value(runonce,
504
                             {'key': "BootMonitor", 't': 1, 'value': value})
505

    
506
            value = (
507
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
508
                r'\policies\system /v LocalAccountTokenFilterPolicy'
509
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
510

    
511
            h.node_set_value(runonce,
512
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
513

    
514
            h.commit(None)
515

    
516
            self.image.g.upload(software, path)
517
        finally:
518
            os.unlink(software)
519

    
520
        return token
521

    
522
    def _update_firewalls(self, domain, public, standard):
523
        """Enables or disables the firewall for the Domain, the Public and the
524
        Standard profile. Returns a triplete with the old values.
525

526
        1 will enable a firewall and 0 will disable it
527
        """
528

    
529
        if domain not in (0, 1):
530
            raise ValueError("Valid values for domain parameter are 0 and 1")
531

    
532
        if public not in (0, 1):
533
            raise ValueError("Valid values for public parameter are 0 and 1")
534

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

    
538
        path = self._registry_file_path("SYSTEM")
539
        systemfd, system = tempfile.mkstemp()
540
        try:
541
            os.close(systemfd)
542
            self.image.g.download(path, system)
543

    
544
            h = hivex.Hivex(system, write=True)
545

    
546
            select = h.node_get_child(h.root(), 'Select')
547
            current_value = h.node_get_value(select, 'Current')
548

    
549
            # expecting a little endian dword
550
            assert h.value_type(current_value)[1] == 4
551
            current = "%03d" % h.value_dword(current_value)
552

    
553
            firewall_policy = h.root()
554
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
555
                          'Parameters', 'FirewallPolicy'):
556
                firewall_policy = h.node_get_child(firewall_policy, child)
557

    
558
            old_values = []
559
            new_values = [domain, public, standard]
560
            for profile in ('Domain', 'Public', 'Standard'):
561
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
562

    
563
                old_value = h.node_get_value(node, 'EnableFirewall')
564

    
565
                # expecting a little endian dword
566
                assert h.value_type(old_value)[1] == 4
567
                old_values.append(h.value_dword(old_value))
568

    
569
                h.node_set_value(
570
                    node, {'key': 'EnableFirewall', 't': 4L,
571
                           'value': struct.pack("<I", new_values.pop(0))})
572

    
573
            h.commit(None)
574
            self.image.g.upload(system, path)
575

    
576
        finally:
577
            os.unlink(system)
578

    
579
        return old_values
580

    
581
    def _update_uac_remote_setting(self, value):
582
        """Updates the registry key value:
583
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
584
        \System]"LocalAccountTokenFilterPolicy"
585

586
        value = 1 will disable the UAC remote restrictions
587
        value = 0 will enable the UAC remote restrictions
588

589
        For more info see here: http://support.microsoft.com/kb/951016
590

591
        Returns:
592
            True if the key is changed
593
            False if the key is unchanged
594
        """
595

    
596
        if value not in (0, 1):
597
            raise ValueError("Valid values for value parameter are 0 and 1")
598

    
599
        path = self._registry_file_path('SOFTWARE')
600
        softwarefd, software = tempfile.mkstemp()
601
        try:
602
            os.close(softwarefd)
603
            self.image.g.download(path, software)
604

    
605
            h = hivex.Hivex(software, write=True)
606

    
607
            key = h.root()
608
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
609
                          'System'):
610
                key = h.node_get_child(key, child)
611

    
612
            policy = None
613
            for val in h.node_values(key):
614
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
615
                    policy = val
616

    
617
            if policy is not None:
618
                dword = h.value_dword(policy)
619
                if dword == value:
620
                    return False
621
            elif value == 0:
622
                return False
623

    
624
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
625
                         'value': struct.pack("<I", value)}
626

    
627
            h.node_set_value(key, new_value)
628
            h.commit(None)
629

    
630
            self.image.g.upload(software, path)
631

    
632
        finally:
633
            os.unlink(software)
634

    
635
        return True
636

    
637
    def _do_collect_metadata(self):
638
        """Collect metadata about the OS"""
639
        super(Windows, self)._do_collect_metadata()
640
        self.meta["USERS"] = " ".join(self._get_users())
641

    
642
    def _get_users(self):
643
        """Returns a list of users found in the images"""
644
        samfd, sam = tempfile.mkstemp()
645
        try:
646
            os.close(samfd)
647
            self.image.g.download(self._registry_file_path('SAM'), sam)
648

    
649
            h = hivex.Hivex(sam)
650

    
651
            # Navigate to /SAM/Domains/Account/Users
652
            users_node = h.root()
653
            for child in ('SAM', 'Domains', 'Account', 'Users'):
654
                users_node = h.node_get_child(users_node, child)
655

    
656
            # Navigate to /SAM/Domains/Account/Users/Names
657
            names_node = h.node_get_child(users_node, 'Names')
658

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

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

    
682
                if disabled(f_value):
683
                    self.out.warn("Found disabled `%s' account!" % username)
684
                    continue
685

    
686
                users.append(username)
687

    
688
        finally:
689
            os.unlink(sam)
690

    
691
        # Filter out the guest account
692
        return users
693

    
694
    def _check_connectivity(self):
695
        """Check if winexe works on the Windows VM"""
696

    
697
        retries = self.sysprep_params['connection_retries']
698
        # If the connection_retries parameter is set to 0 disable the
699
        # connectivity check
700
        if retries == 0:
701
            return True
702

    
703
        passwd = self.sysprep_params['password']
704
        winexe = WinEXE('Administrator', passwd, 'localhost')
705
        winexe.uninstall().debug(9)
706

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

    
720
        raise FatalError("Connection to the Windows VM failed after %d retries"
721
                         % retries)
722

    
723
    def _guest_exec(self, command, fatal=True):
724
        """Execute a command on a windows VM"""
725

    
726
        passwd = self.sysprep_params['password']
727

    
728
        winexe = WinEXE('Administrator', passwd, 'localhost')
729
        winexe.runas('Administrator', passwd).uninstall()
730

    
731
        try:
732
            (stdout, stderr, rc) = winexe.run(command)
733
        except WinexeTimeout:
734
            FatalError("Command: `%s' timeout out." % command)
735

    
736
        if rc != 0 and fatal:
737
            reason = stderr if len(stderr) else stdout
738
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
739
                            (command, rc, reason))
740
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
741
                             (command, rc, reason))
742

    
743
        return (stdout, stderr, rc)
744

    
745

    
746
class _VM(object):
747
    """Windows Virtual Machine"""
748
    def __init__(self, disk, serial, params):
749
        """Create _VM instance
750

751
            disk: VM's hard disk
752
            serial: File to save the output of the serial port
753
        """
754

    
755
        self.disk = disk
756
        self.serial = serial
757
        self.params = params
758

    
759
        def random_mac():
760
            """creates a random mac address"""
761
            mac = [0x00, 0x16, 0x3e,
762
                   random.randint(0x00, 0x7f),
763
                   random.randint(0x00, 0xff),
764
                   random.randint(0x00, 0xff)]
765

    
766
            return ':'.join(['%02x' % x for x in mac])
767

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

    
771
        kvm, needed_args = get_kvm_binary()
772

    
773
        if kvm is None:
774
            FatalError("Can't find the kvm binary")
775

    
776
        args = [kvm]
777
        args.extend(needed_args)
778

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

    
787
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
788
                                        stdout=subprocess.PIPE)
789

    
790
    def isalive(self):
791
        """Check if the VM is still alive"""
792
        return self.process.poll() is None
793

    
794
    def destroy(self):
795
        """Destroy the VM"""
796

    
797
        if not self.isalive():
798
            return
799

    
800
        def handler(signum, frame):
801
            self.process.terminate()
802
            time.sleep(1)
803
            if self.isalive():
804
                self.process.kill()
805
            self.process.wait()
806
            raise FatalError("VM destroy timed-out")
807

    
808
        signal.signal(signal.SIGALRM, handler)
809

    
810
        signal.alarm(self.params['shutdown_timeout'])
811
        self.process.communicate(input="system_powerdown\n")
812
        signal.alarm(0)
813

    
814
    def wait(self, timeout=0):
815
        """Wait for the VM to terminate"""
816

    
817
        def handler(signum, frame):
818
            self.destroy()
819
            raise FatalError("VM wait timed-out.")
820

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

    
823
        signal.alarm(timeout)
824
        stdout, stderr = self.process.communicate()
825
        signal.alarm(0)
826

    
827
        return (stdout, stderr, self.process.poll())
828

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