Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 4246a133

History | View | Annotate | Download (30.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, 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
        device = self.g.part_to_dev(self.root)
120

    
121
        self.last_part_num = self.g.part_list(device)[-1]['part_num']
122
        self.last_drive = None
123
        self.system_drive = None
124

    
125
        for drive, partition in self.g.inspect_get_drive_mappings(self.root):
126
            if partition == "%s%d" % (device, self.last_part_num):
127
                self.last_drive = drive
128
            if partition == self.root:
129
                self.system_drive = drive
130

    
131
        assert self.system_drive
132

    
133
        self.product_name = self.g.inspect_get_product_name(self.root)
134
        self.syspreped = False
135

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

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

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

    
147
        self._guest_exec('netsh interface teredo set state disabled')
148

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

    
153
        self._guest_exec('netsh interface isa set state disabled')
154

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

    
159
        self._guest_exec('netsh firewall set icmpsetting 8')
160

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

    
165
        self._guest_exec(r'powercfg.exe /hibernate off')
166

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

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

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

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

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

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

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

    
209
        self._guest_exec(
210
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
211

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

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

    
227
        stdout, stderr, rc = self._guest_exec(cmd)
228

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

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

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

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

    
256
        self.out.output("\tReclaiming %dMB ..." % querymax)
257

    
258
        cmd = (
259
            r'cmd /Q /V:ON /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 NOT !ERRORLEVEL! EQU 0 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 name, param in self.needed_sysprep_params.items():
282
            if name not in self.sysprep_params:
283
                raise FatalError(txt % param)
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
            self.g.shutdown()
307
        else:
308
            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, self.sysprep_params)
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 = [task for task in tasks if task.enabled]
338
            size = len(enabled)
339

    
340
            # Make sure shrink runs in the end, before ms sysprep
341
            enabled = [task for task in enabled if
342
                       self.sysprep_info(task).name != 'shrink']
343

    
344
            if len(enabled) != size:
345
                enabled.append(self.shrink)
346

    
347
            # Make sure the ms sysprep is the last task to run if it is enabled
348
            enabled = [task for task in enabled if
349
                       self.sysprep_info(task).name != 'microsoft-sysprep']
350

    
351
            ms_sysprep_enabled = False
352
            if len(enabled) != size:
353
                enabled.append(self.microsoft_sysprep)
354
                ms_sysprep_enabled = True
355

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

    
363
            self.out.output("Sending shut down command ...", False)
364
            if not ms_sysprep_enabled:
365
                self._shutdown()
366
            self.out.success("done")
367

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
509
            h.commit(None)
510

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

    
515
        return token
516

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
571
        finally:
572
            os.unlink(system)
573

    
574
        return old_values
575

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

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

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

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

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

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

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

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

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

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

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

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

    
625
            self.g.upload(software, path)
626

    
627
        finally:
628
            os.unlink(software)
629

    
630
        return True
631

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

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

    
644
            h = hivex.Hivex(sam)
645

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

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

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

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

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

    
681
                users.append(username)
682

    
683
        finally:
684
            os.unlink(sam)
685

    
686
        # Filter out the guest account
687
        return users
688

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

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

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

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

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

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

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

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

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

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

    
738
        return (stdout, stderr, rc)
739

    
740

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

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

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

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

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

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

    
766
        kvm = get_kvm_binary()
767

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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