Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ aca3e650

History | View | Annotate | Download (30.8 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.17 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)
279

    
280
        for line in stdout.splitlines():
281
            if line.find('shrunk') >= 0:
282
                self.out.output(line)
283

    
284
    def do_sysprep(self):
285
        """Prepare system for image creation."""
286

    
287
        if getattr(self, 'syspreped', False):
288
            raise FatalError("Image is already syspreped!")
289

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

    
295
        self.mount(readonly=False)
296
        try:
297
            disabled_uac = self._update_uac_remote_setting(1)
298
            token = self._enable_os_monitor()
299

    
300
            # disable the firewalls
301
            firewall_states = self._update_firewalls(0, 0, 0)
302

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

    
311
        finally:
312
            self.umount()
313

    
314
        self.image.disable_guestfs()
315

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

    
326
            self.out.output("Waiting for OS to boot ...", False)
327
            self._wait_vm_boot(vm, monitor, token)
328
            self.out.success('done')
329

    
330
            self.out.output("Checking connectivity to the VM ...", False)
331
            self._check_connectivity()
332
            self.out.success('done')
333

    
334
            self.out.output("Disabling automatic logon ...", False)
335
            self._disable_autologon()
336
            self.out.success('done')
337

    
338
            self.out.output('Preparing system for image creation:')
339

    
340
            tasks = self.list_syspreps()
341
            enabled = [task for task in tasks if task.enabled]
342
            size = len(enabled)
343

    
344
            # Make sure shrink runs in the end, before ms sysprep
345
            enabled = [task for task in enabled if
346
                       self.sysprep_info(task).name != 'shrink']
347

    
348
            if len(enabled) != size:
349
                enabled.append(self.shrink)
350

    
351
            # Make sure the ms sysprep is the last task to run if it is enabled
352
            enabled = [task for task in enabled if
353
                       self.sysprep_info(task).name != 'microsoft-sysprep']
354

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

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

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

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

    
379
            try:
380
                if vm is not None:
381
                    self.out.output("Destroying windows VM ...", False)
382
                    vm.destroy()
383
                    self.out.success("done")
384
            finally:
385
                self.image.enable_guestfs()
386

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

    
392
                    self._update_firewalls(*firewall_states)
393
                finally:
394
                    self.umount()
395

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

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

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

    
412
        raise FatalError("Windows VM booting timed out!")
413

    
414
    def _disable_autologon(self):
415
        """Disable automatic logon on the windows image"""
416

    
417
        winlogon = \
418
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
419

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

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

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

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

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

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

    
449
            h = hivex.Hivex(software, write=True)
450

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

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

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

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

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

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

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

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

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

    
510
            h.commit(None)
511

    
512
            self.image.g.upload(software, path)
513
        finally:
514
            os.unlink(software)
515

    
516
        return token
517

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

522
        1 will enable a firewall and 0 will disable it
523
        """
524

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

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

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

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

    
540
            h = hivex.Hivex(system, write=True)
541

    
542
            select = h.node_get_child(h.root(), 'Select')
543
            current_value = h.node_get_value(select, 'Current')
544

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

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

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

    
559
                old_value = h.node_get_value(node, 'EnableFirewall')
560

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

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

    
569
            h.commit(None)
570
            self.image.g.upload(system, path)
571

    
572
        finally:
573
            os.unlink(system)
574

    
575
        return old_values
576

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

582
        value = 1 will disable the UAC remote restrictions
583
        value = 0 will enable the UAC remote restrictions
584

585
        For more info see here: http://support.microsoft.com/kb/951016
586

587
        Returns:
588
            True if the key is changed
589
            False if the key is unchanged
590
        """
591

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

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

    
601
            h = hivex.Hivex(software, write=True)
602

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

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

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

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

    
623
            h.node_set_value(key, new_value)
624
            h.commit(None)
625

    
626
            self.image.g.upload(software, path)
627

    
628
        finally:
629
            os.unlink(software)
630

    
631
        return True
632

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

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

    
645
            h = hivex.Hivex(sam)
646

    
647
            # Navigate to /SAM/Domains/Account/Users
648
            users_node = h.root()
649
            for child in ('SAM', 'Domains', 'Account', 'Users'):
650
                users_node = h.node_get_child(users_node, child)
651

    
652
            # Navigate to /SAM/Domains/Account/Users/Names
653
            names_node = h.node_get_child(users_node, 'Names')
654

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

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

    
678
                if disabled(f_value):
679
                    self.out.warn("Found disabled `%s' account!" % username)
680
                    continue
681

    
682
                users.append(username)
683

    
684
        finally:
685
            os.unlink(sam)
686

    
687
        # Filter out the guest account
688
        return users
689

    
690
    def _check_connectivity(self):
691
        """Check if winexe works on the Windows VM"""
692

    
693
        retries = self.sysprep_params['connection_retries']
694
        # If the connection_retries parameter is set to 0 disable the
695
        # connectivity check
696
        if retries == 0:
697
            return True
698

    
699
        passwd = self.sysprep_params['password']
700
        winexe = WinEXE('Administrator', passwd, 'localhost')
701
        winexe.uninstall().debug(9)
702

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

    
716
        raise FatalError("Connection to the Windows VM failed after %d retries"
717
                         % retries)
718

    
719
    def _guest_exec(self, command, fatal=True):
720
        """Execute a command on a windows VM"""
721

    
722
        passwd = self.sysprep_params['password']
723

    
724
        winexe = WinEXE('Administrator', passwd, 'localhost')
725
        winexe.runas('Administrator', passwd).uninstall()
726

    
727
        try:
728
            (stdout, stderr, rc) = winexe.run(command)
729
        except WinexeTimeout:
730
            FatalError("Command: `%s' timeout out." % command)
731

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

    
739
        return (stdout, stderr, rc)
740

    
741

    
742
class _VM(object):
743
    """Windows Virtual Machine"""
744
    def __init__(self, disk, serial, params):
745
        """Create _VM instance
746

747
            disk: VM's hard disk
748
            serial: File to save the output of the serial port
749
        """
750

    
751
        self.disk = disk
752
        self.serial = serial
753
        self.params = params
754

    
755
        def random_mac():
756
            """creates a random mac address"""
757
            mac = [0x00, 0x16, 0x3e,
758
                   random.randint(0x00, 0x7f),
759
                   random.randint(0x00, 0xff),
760
                   random.randint(0x00, 0xff)]
761

    
762
            return ':'.join(['%02x' % x for x in mac])
763

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

    
767
        kvm = get_kvm_binary()
768

    
769
        if kvm is None:
770
            FatalError("Can't find the kvm binary")
771

    
772
        args = [
773
            kvm, '-smp', '1', '-m', '1024', '-drive',
774
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
775
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
776
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
777
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
778
            '-monitor', 'stdio']
779

    
780
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
781
                                        stdout=subprocess.PIPE)
782

    
783
    def isalive(self):
784
        """Check if the VM is still alive"""
785
        return self.process.poll() is None
786

    
787
    def destroy(self):
788
        """Destroy the VM"""
789

    
790
        if not self.isalive():
791
            return
792

    
793
        def handler(signum, frame):
794
            self.process.terminate()
795
            time.sleep(1)
796
            if self.isalive():
797
                self.process.kill()
798
            self.process.wait()
799
            raise FatalError("VM destroy timed-out")
800

    
801
        signal.signal(signal.SIGALRM, handler)
802

    
803
        signal.alarm(self.params['shutdown_timeout'])
804
        self.process.communicate(input="system_powerdown\n")
805
        signal.alarm(0)
806

    
807
    def wait(self, timeout=0):
808
        """Wait for the VM to terminate"""
809

    
810
        def handler(signum, frame):
811
            self.destroy()
812
            raise FatalError("VM wait timed-out.")
813

    
814
        signal.signal(signal.SIGALRM, handler)
815

    
816
        signal.alarm(timeout)
817
        stdout, stderr = self.process.communicate()
818
        signal.alarm(0)
819

    
820
        return (stdout, stderr, self.process.poll())
821

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