Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 091c0335

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
        device = self.image.g.part_to_dev(self.root)
120

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

    
125
        for drive, partition in self.image.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.image.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.image.g.inspect_get_windows_systemroot(self.root)
295
            try:
296
                pagefile = "%s/pagefile.sys" % systemroot
297
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
298
            except RuntimeError:
299
                pass
300

    
301
        finally:
302
            self.umount()
303

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

    
313
        self.out.success('done')
314

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

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

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

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

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

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

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

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

    
350
            # Make sure the ms sysprep is the last task to run if it is enabled
351
            enabled = [task for task in enabled if
352
                       self.sysprep_info(task).name != 'microsoft-sysprep']
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(self.sysprep_params['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.image.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 _ in range(self.sysprep_params['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.image.g.inspect_get_windows_systemroot(self.root)
430
        path = "%s/system32/config/%s" % (systemroot, regfile)
431
        try:
432
            path = self.image.g.case_sensitive_path(path)
433
        except RuntimeError as error:
434
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
435
                             % (regfile, str(error)))
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.image.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.image.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.image.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.image.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.image.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.image.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
        samfd, sam = tempfile.mkstemp()
643
        try:
644
            os.close(samfd)
645
            self.image.g.download(self._registry_file_path('SAM'), sam)
646

    
647
            h = hivex.Hivex(sam)
648

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

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

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

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

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

    
684
                users.append(username)
685

    
686
        finally:
687
            os.unlink(sam)
688

    
689
        # Filter out the guest account
690
        return users
691

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

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

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

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

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

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

    
724
        passwd = self.sysprep_params['password']
725

    
726
        winexe = WinEXE('Administrator', passwd, 'localhost')
727
        winexe.runas('Administrator', passwd).uninstall()
728

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

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

    
741
        return (stdout, stderr, rc)
742

    
743

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

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

    
753
        self.disk = disk
754
        self.serial = serial
755
        self.params = params
756

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

    
764
            return ':'.join(['%02x' % x for x in mac])
765

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

    
769
        kvm = get_kvm_binary()
770

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

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

    
782
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
783
                                        stdout=subprocess.PIPE)
784

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

    
789
    def destroy(self):
790
        """Destroy the VM"""
791

    
792
        if not self.isalive():
793
            return
794

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

    
803
        signal.signal(signal.SIGALRM, handler)
804

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

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

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

    
816
        signal.signal(signal.SIGALRM, handler)
817

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

    
822
        return (stdout, stderr, self.process.poll())
823

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