Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 5756c277

History | View | Annotate | Download (31.2 kB)

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

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

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

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

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

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

    
105

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

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

    
132
        device = self.image.g.part_to_dev(self.root)
133

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

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

    
144
        assert self.system_drive
145

    
146
        self.product_name = self.image.g.inspect_get_product_name(self.root)
147
        self.syspreped = False
148

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

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

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

    
160
        self._guest_exec('netsh interface teredo set state disabled')
161

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

    
166
        self._guest_exec('netsh interface isa set state disabled')
167

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

    
172
        self._guest_exec('netsh firewall set icmpsetting 8')
173

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

    
178
        self._guest_exec(r'powercfg.exe /hibernate off')
179

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

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

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

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

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

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

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

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

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

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

    
240
        stdout, stderr, rc = self._guest_exec(cmd)
241

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

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

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

    
265
        if querymax < 0:
266
            self.out.warn("Not enought available space to shrink the image!")
267
            return
268

    
269
        self.out.output("\tReclaiming %dMB ..." % querymax)
270

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

    
281
        stdout, stderr, rc = self._guest_exec(cmd, False)
282

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

    
291
    def do_sysprep(self):
292
        """Prepare system for image creation."""
293

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

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

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

    
307
            # disable the firewalls
308
            firewall_states = self._update_firewalls(0, 0, 0)
309

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

    
318
        finally:
319
            self.umount()
320

    
321
        self.image.disable_guestfs()
322

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

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

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

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

    
345
            self.out.output('Preparing system for image creation:')
346

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

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

    
355
            if len(enabled) != size:
356
                enabled.append(self.shrink)
357

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

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

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

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

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

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

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

    
399
                    self._update_firewalls(*firewall_states)
400
                finally:
401
                    self.umount()
402

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

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

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

    
419
        raise FatalError("Windows VM booting timed out!")
420

    
421
    def _disable_autologon(self):
422
        """Disable automatic logon on the windows image"""
423

    
424
        winlogon = \
425
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
426

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

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

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

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

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

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

    
456
            h = hivex.Hivex(software, write=True)
457

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

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

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

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

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

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

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

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

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

    
517
            h.commit(None)
518

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

    
523
        return token
524

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

529
        1 will enable a firewall and 0 will disable it
530
        """
531

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

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

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

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

    
547
            h = hivex.Hivex(system, write=True)
548

    
549
            select = h.node_get_child(h.root(), 'Select')
550
            current_value = h.node_get_value(select, 'Current')
551

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

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

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

    
566
                old_value = h.node_get_value(node, 'EnableFirewall')
567

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

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

    
576
            h.commit(None)
577
            self.image.g.upload(system, path)
578

    
579
        finally:
580
            os.unlink(system)
581

    
582
        return old_values
583

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

589
        value = 1 will disable the UAC remote restrictions
590
        value = 0 will enable the UAC remote restrictions
591

592
        For more info see here: http://support.microsoft.com/kb/951016
593

594
        Returns:
595
            True if the key is changed
596
            False if the key is unchanged
597
        """
598

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

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

    
608
            h = hivex.Hivex(software, write=True)
609

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

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

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

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

    
630
            h.node_set_value(key, new_value)
631
            h.commit(None)
632

    
633
            self.image.g.upload(software, path)
634

    
635
        finally:
636
            os.unlink(software)
637

    
638
        return True
639

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

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

    
652
            h = hivex.Hivex(sam)
653

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

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

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

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

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

    
689
                users.append(username)
690

    
691
        finally:
692
            os.unlink(sam)
693

    
694
        # Filter out the guest account
695
        return users
696

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

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

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

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

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

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

    
729
        passwd = self.sysprep_params['password']
730

    
731
        winexe = WinEXE('Administrator', passwd, 'localhost')
732
        winexe.runas('Administrator', passwd).uninstall()
733

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

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

    
746
        return (stdout, stderr, rc)
747

    
748

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

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

    
758
        self.disk = disk
759
        self.serial = serial
760
        self.params = params
761

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

    
769
            return ':'.join(['%02x' % x for x in mac])
770

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

    
774
        kvm, needed_args = get_kvm_binary()
775

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

    
779
        args = [kvm]
780
        args.extend(needed_args)
781

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

    
790
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
791
                                        stdout=subprocess.PIPE)
792

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

    
797
    def destroy(self):
798
        """Destroy the VM"""
799

    
800
        if not self.isalive():
801
            return
802

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

    
811
        signal.signal(signal.SIGALRM, handler)
812

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

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

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

    
824
        signal.signal(signal.SIGALRM, handler)
825

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

    
830
        return (stdout, stderr, self.process.poll())
831

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