Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (28.6 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
40
from image_creator.util import FatalError, check_guestfs_version, get_command
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
BOOT_TIMEOUT = 300
54
SHUTDOWN_TIMEOUT = 120
55
CONNECTION_RETRIES = 5
56

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

    
107

    
108
class Windows(OSBase):
109
    """OS class for Windows"""
110
    def __init__(self, image, **kargs):
111
        super(Windows, self).__init__(image, **kargs)
112

    
113
        device = self.g.part_to_dev(self.root)
114

    
115
        self.last_part_num = self.g.part_list(device)[-1]['part_num']
116
        self.last_drive = None
117
        self.system_drive = None
118

    
119
        for drive, partition in self.g.inspect_get_drive_mappings(self.root):
120
            if partition == "%s%d" % (device, self.last_part_num):
121
                self.last_drive = drive
122
            if partition == self.root:
123
                self.system_drive = drive
124

    
125
        assert self.system_drive
126

    
127
        self.product_name = self.g.inspect_get_product_name(self.root)
128

    
129
    def needed_sysprep_params(self):
130
        """Returns a list of needed sysprep parameters. Each element in the
131
        list is a SysprepParam object.
132
        """
133
        password = self.SysprepParam(
134
            'password', 'Image Administrator Password', 20, lambda x: True)
135

    
136
        return [password]
137

    
138
    @sysprep('Disabling IPv6 privacy extensions')
139
    def disable_ipv6_privacy_extensions(self):
140
        """Disable IPv6 privacy extensions"""
141

    
142
        self._guest_exec('netsh interface ipv6 set global '
143
                         'randomizeidentifiers=disabled store=persistent')
144

    
145
    @sysprep('Disabling Teredo interface')
146
    def disable_teredo(self):
147
        """Disable Teredo interface"""
148

    
149
        self._guest_exec('netsh interface teredo set state disabled')
150

    
151
    @sysprep('Disabling ISATAP Adapters')
152
    def disable_isatap(self):
153
        """Disable ISATAP Adapters"""
154

    
155
        self._guest_exec('netsh interface isa set state disabled')
156

    
157
    @sysprep('Enabling ping responses')
158
    def enable_pings(self):
159
        """Enable ping responses"""
160

    
161
        self._guest_exec('netsh firewall set icmpsetting 8')
162

    
163
    @sysprep('Disabling hibernation support')
164
    def disable_hibernation(self):
165
        """Disable hibernation support and remove the hibernation file"""
166

    
167
        self._guest_exec(r'powercfg.exe /hibernate off')
168

    
169
    @sysprep('Setting the system clock to UTC')
170
    def utc(self):
171
        """Set the hardware clock to UTC"""
172

    
173
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
174
        self._guest_exec(
175
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
176

    
177
    @sysprep('Clearing the event logs')
178
    def clear_logs(self):
179
        """Clear all the event logs"""
180

    
181
        self._guest_exec(
182
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
183

    
184
    @sysprep('Executing Sysprep on the image (may take more that 10 minutes)')
185
    def microsoft_sysprep(self):
186
        """Run the Microsoft System Preparation Tool. This will remove
187
        system-specific data and will make the image ready to be deployed.
188
        After this no other task may run.
189
        """
190

    
191
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
192
                         r'/quiet /generalize /oobe /shutdown')
193
        self.syspreped = True
194

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

    
211
        self._guest_exec(
212
            "cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
213

    
214
    @sysprep('Shrinking the last filesystem')
215
    def shrink(self):
216
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
217

    
218
        # Query for the maximum number of reclaimable bytes
219
        cmd = (
220
            r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
221
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
222
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
223
            r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
224
            r'ECHO EXIT >> %SCRIPT% & ' +
225
            r'DISKPART /S %SCRIPT% & ' +
226
            r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
227
            r'DEL /Q %SCRIPT%"')
228

    
229
        stdout, stderr, rc = self._guest_exec(cmd)
230

    
231
        querymax = None
232
        for line in stdout.splitlines():
233
            # diskpart will return something like this:
234
            #
235
            #   The maximum number of reclaimable bytes is: xxxx MB
236
            #
237
            if line.find('reclaimable') >= 0:
238
                querymax = line.split(':')[1].split()[0].strip()
239
                assert querymax.isdigit(), \
240
                    "Number of reclaimable bytes not a number"
241

    
242
        if querymax is None:
243
            FatalError("Error in shrinking! "
244
                       "Couldn't find the max number of reclaimable bytes!")
245

    
246
        querymax = int(querymax)
247
        # From ntfsresize:
248
        # Practically the smallest shrunken size generally is at around
249
        # "used space" + (20-200 MB). Please also take into account that
250
        # Windows might need about 50-100 MB free space left to boot safely.
251
        # I'll give 100MB extra space just to be sure
252
        querymax -= 100
253

    
254
        if querymax < 0:
255
            self.out.warn("Not enought available space to shrink the image!")
256
            return
257

    
258
        cmd = (
259
            r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
260
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
261
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
262
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
263
            r'ECHO EXIT >> %SCRIPT% & ' +
264
            r'DISKPART /S %SCRIPT% & ' +
265
            r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
266
            r'DEL /Q %SCRIPT%"')
267

    
268
        stdout, stderr, rc = self._guest_exec(cmd)
269

    
270
        for line in stdout.splitlines():
271
            if line.find('shrunk') >= 0:
272
                self.out.output(line)
273

    
274
    def do_sysprep(self):
275
        """Prepare system for image creation."""
276

    
277
        if getattr(self, 'syspreped', False):
278
            raise FatalError("Image is already syspreped!")
279

    
280
        txt = "System preparation parameter: `%s' is needed but missing!"
281
        for param in self.needed_sysprep_params():
282
            if param[0] not in self.sysprep_params:
283
                raise FatalError(txt % param[0])
284

    
285
        self.mount(readonly=False)
286
        try:
287
            disabled_uac = self._update_uac_remote_setting(1)
288
            token = self._enable_os_monitor()
289

    
290
            # disable the firewalls
291
            firewall_states = self._update_firewalls(0, 0, 0)
292

    
293
            # Delete the pagefile. It will be recreated when the system boots
294
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
295
            pagefile = "%s/pagefile.sys" % systemroot
296
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
297

    
298
        finally:
299
            self.umount()
300

    
301
        self.out.output("Shutting down helper VM ...", False)
302
        self.g.sync()
303
        # guestfs_shutdown which is the prefered way to shutdown the backend
304
        # process was introduced in version 1.19.16
305
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
306
            ret = self.g.shutdown()
307
        else:
308
            ret = self.g.kill_subprocess()
309

    
310
        self.out.success('done')
311

    
312
        vm = None
313
        monitor = None
314
        try:
315
            self.out.output("Starting windows VM ...", False)
316
            monitorfd, monitor = tempfile.mkstemp()
317
            os.close(monitorfd)
318
            vm = _VM(self.image.device, monitor)
319
            self.out.success("started (console on vnc display: %d)." %
320
                             vm.display)
321

    
322
            self.out.output("Waiting for OS to boot ...", False)
323
            self._wait_vm_boot(vm, monitor, token)
324
            self.out.success('done')
325

    
326
            self.out.output("Checking connectivity to the VM ...", False)
327
            self._check_connectivity()
328
            self.out.success('done')
329

    
330
            self.out.output("Disabling automatic logon ...", False)
331
            self._disable_autologon()
332
            self.out.success('done')
333

    
334
            self.out.output('Preparing system for image creation:')
335

    
336
            tasks = self.list_syspreps()
337
            enabled = filter(lambda x: x.enabled, tasks)
338
            size = len(enabled)
339

    
340
            # Make sure shrink runs in the end, before ms sysprep
341
            enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
342
                             enabled)
343

    
344
            shrink_enabled = False
345
            if len(enabled) != size:
346
                enabled.append(self.shrink)
347
                shrink_enabled = True
348

    
349
            # Make sure the ms sysprep is the last task to run if it is enabled
350
            enabled = filter(
351
                lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
352
                enabled)
353

    
354
            ms_sysprep_enabled = False
355
            if len(enabled) != size:
356
                enabled.append(self.microsoft_sysprep)
357
                ms_sysprep_enabled = True
358

    
359
            cnt = 0
360
            for task in enabled:
361
                cnt += 1
362
                self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
363
                task()
364
                setattr(task.im_func, 'executed', True)
365

    
366
            self.out.output("Sending shut down command ...", False)
367
            if not ms_sysprep_enabled:
368
                self._shutdown()
369
            self.out.success("done")
370

    
371
            self.out.output("Waiting for windows to shut down ...", False)
372
            vm.wait(SHUTDOWN_TIMEOUT)
373
            self.out.success("done")
374
        finally:
375
            if monitor is not None:
376
                os.unlink(monitor)
377

    
378
            try:
379
                if vm is not None:
380
                    self.out.output("Destroying windows VM ...", False)
381
                    vm.destroy()
382
                    self.out.success("done")
383
            finally:
384
                self.out.output("Relaunching helper VM (may take a while) ...",
385
                                False)
386
                self.g.launch()
387
                self.out.success('done')
388

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

    
394
                    self._update_firewalls(*firewall_states)
395
                finally:
396
                    self.umount()
397

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

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

    
405
        for i in range(BOOT_TIMEOUT):
406
            time.sleep(1)
407
            with open(fname) as f:
408
                for line in f:
409
                    if line.startswith(msg):
410
                        return True
411
            if not vm.isalive():
412
                raise FatalError("Windows VM died unexpectedly!")
413

    
414
        raise FatalError("Windows VM booting timed out!")
415

    
416
    def _disable_autologon(self):
417
        """Disable automatic logon on the windows image"""
418

    
419
        winlogon = \
420
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
421

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

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

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

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

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

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

    
451
            h = hivex.Hivex(software, write=True)
452

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

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

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

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

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

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

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

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

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

    
512
            h.commit(None)
513

    
514
            self.g.upload(software, path)
515
        finally:
516
            os.unlink(software)
517

    
518
        return token
519

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

524
        1 will enable a firewall and 0 will disable it
525
        """
526

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

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

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

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

    
542
            h = hivex.Hivex(system, write=True)
543

    
544
            select = h.node_get_child(h.root(), 'Select')
545
            current_value = h.node_get_value(select, 'Current')
546

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

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

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

    
561
                old_value = h.node_get_value(node, 'EnableFirewall')
562

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

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

    
571
            h.commit(None)
572
            self.g.upload(system, path)
573

    
574
        finally:
575
            os.unlink(system)
576

    
577
        return old_values
578

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

584
        value = 1 will disable the UAC remote restrictions
585
        value = 0 will enable the UAC remote restrictions
586

587
        For more info see here: http://support.microsoft.com/kb/951016
588

589
        Returns:
590
            True if the key is changed
591
            False if the key is unchanged
592
        """
593

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

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

    
603
            h = hivex.Hivex(software, write=True)
604

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

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

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

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

    
625
            h.node_set_value(key, new_value)
626
            h.commit(None)
627

    
628
            self.g.upload(software, path)
629

    
630
        finally:
631
            os.unlink(software)
632

    
633
        return True
634

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

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

    
648
            h = hivex.Hivex(sam)
649

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

    
655
            users = [h.node_name(x) for x in h.node_children(key)]
656

    
657
        finally:
658
            os.unlink(sam)
659

    
660
        # Filter out the guest account
661
        return filter(lambda x: x != "Guest", users)
662

    
663
    def _check_connectivity(self):
664
        """Check if winexe works on the Windows VM"""
665

    
666
        passwd = self.sysprep_params['password']
667
        winexe = WinEXE('Administrator', passwd, 'localhost')
668
        winexe.uninstall().debug(9)
669

    
670
        for i in range(CONNECTION_RETRIES):
671
            (stdout, stderr, rc) = winexe.run('cmd /C')
672
            if rc == 0:
673
                return True
674
            log = tempfile.NamedTemporaryFile(delete=False)
675
            try:
676
                log.file.write(stdout)
677
            finally:
678
                log.close()
679
            self.out.output("failed! See: `%' for the full output" % log.name)
680
            if i < CONNECTION_RETRIES - 1:
681
                self.out.output("Retrying ...", False)
682
        raise FatalError("Connection to the VM failed after %d retries" %
683
                         CONNECTION_RETRIES)
684

    
685
    def _guest_exec(self, command, fatal=True):
686
        """Execute a command on a windows VM"""
687

    
688
        passwd = self.sysprep_params['password']
689

    
690
        winexe = WinEXE('Administrator', passwd, 'localhost')
691
        winexe.runas('Administrator', passwd).uninstall()
692

    
693
        try:
694
            (stdout, stderr, rc) = winexe.run(command)
695
        except WinexeTimeout:
696
            FatalError("Command: `%s' timeout out." % command)
697

    
698
        if rc != 0 and fatal:
699
            reason = stderr if len(stderr) else stdout
700
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
701
                            (command, rc, reason))
702
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
703
                             (command, rc, reason))
704

    
705
        return (stdout, stderr, rc)
706

    
707

    
708
class _VM(object):
709
    """Windows Virtual Machine"""
710
    def __init__(self, disk, serial):
711
        """Create _VM instance
712

713
            disk: VM's hard disk
714
            serial: File to save the output of the serial port
715
        """
716

    
717
        self.disk = disk
718
        self.serial = serial
719

    
720
        def random_mac():
721
            mac = [0x00, 0x16, 0x3e,
722
                   random.randint(0x00, 0x7f),
723
                   random.randint(0x00, 0xff),
724
                   random.randint(0x00, 0xff)]
725

    
726
            return ':'.join(map(lambda x: "%02x" % x, mac))
727

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

    
731
        args = [
732
            'kvm', '-smp', '1', '-m', '1024', '-drive',
733
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
734
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
735
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
736
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
737
            '-monitor', 'stdio']
738

    
739
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
740
                                        stdout=subprocess.PIPE)
741

    
742
    def isalive(self):
743
        """Check if the VM is still alive"""
744
        return self.process.poll() is None
745

    
746
    def destroy(self):
747
        """Destroy the VM"""
748

    
749
        if not self.isalive():
750
            return
751

    
752
        def handler(signum, frame):
753
            self.process.terminate()
754
            time.sleep(1)
755
            if self.isalive():
756
                self.process.kill()
757
            self.process.wait()
758
            self.out.output("timed-out")
759
            raise FatalError("VM destroy timed-out")
760

    
761
        signal.signal(signal.SIGALRM, handler)
762

    
763
        signal.alarm(SHUTDOWN_TIMEOUT)
764
        self.process.communicate(input="system_powerdown\n")
765
        signal.alarm(0)
766

    
767
    def wait(self, timeout=0):
768
        """Wait for the VM to terminate"""
769

    
770
        def handler(signum, frame):
771
            self.destroy()
772
            raise FatalError("VM wait timed-out.")
773

    
774
        signal.signal(signal.SIGALRM, handler)
775

    
776
        signal.alarm(timeout)
777
        stdout, stderr = self.process.communicate()
778
        signal.alarm(0)
779

    
780
        return (stdout, stderr, self.process.poll())
781

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