Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 678b382b

History | View | Annotate | Download (29.9 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
from image_creator.winexe import WinEXE, WinexeTimeout
42

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

    
53
BOOT_TIMEOUT = 300
54
SHUTDOWN_TIMEOUT = 120
55
CONNECTION_RETRIES = 5
56

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

    
107

    
108
class Windows(OSBase):
109
    """OS class for Windows"""
110

    
111
    @add_sysprep_param('password', str, None, 'Image Administrator Password')
112
    def __init__(self, image, **kargs):
113
        super(Windows, self).__init__(image, **kargs)
114

    
115
        device = self.g.part_to_dev(self.root)
116

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

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

    
127
        assert self.system_drive
128

    
129
        self.product_name = self.g.inspect_get_product_name(self.root)
130
        self.syspreped = False
131

    
132
    @sysprep('Disabling IPv6 privacy extensions')
133
    def disable_ipv6_privacy_extensions(self):
134
        """Disable IPv6 privacy extensions"""
135

    
136
        self._guest_exec('netsh interface ipv6 set global '
137
                         'randomizeidentifiers=disabled store=persistent')
138

    
139
    @sysprep('Disabling Teredo interface')
140
    def disable_teredo(self):
141
        """Disable Teredo interface"""
142

    
143
        self._guest_exec('netsh interface teredo set state disabled')
144

    
145
    @sysprep('Disabling ISATAP Adapters')
146
    def disable_isatap(self):
147
        """Disable ISATAP Adapters"""
148

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

    
151
    @sysprep('Enabling ping responses')
152
    def enable_pings(self):
153
        """Enable ping responses"""
154

    
155
        self._guest_exec('netsh firewall set icmpsetting 8')
156

    
157
    @sysprep('Disabling hibernation support')
158
    def disable_hibernation(self):
159
        """Disable hibernation support and remove the hibernation file"""
160

    
161
        self._guest_exec(r'powercfg.exe /hibernate off')
162

    
163
    @sysprep('Setting the system clock to UTC')
164
    def utc(self):
165
        """Set the hardware clock to UTC"""
166

    
167
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
168
        self._guest_exec(
169
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
170

    
171
    @sysprep('Clearing the event logs')
172
    def clear_logs(self):
173
        """Clear all the event logs"""
174

    
175
        self._guest_exec(
176
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
177

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

    
185
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
186
                         r'/quiet /generalize /oobe /shutdown')
187
        self.syspreped = True
188

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

    
205
        self._guest_exec(
206
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
207

    
208
    @sysprep('Shrinking the last filesystem')
209
    def shrink(self):
210
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
211

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

    
223
        stdout, stderr, rc = self._guest_exec(cmd)
224

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

    
236
        if querymax is None:
237
            FatalError("Error in shrinking! "
238
                       "Couldn't find the max number of reclaimable bytes!")
239

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

    
248
        if querymax < 0:
249
            self.out.warn("Not enought available space to shrink the image!")
250
            return
251

    
252
        self.out.output("\tReclaiming %dMB ..." % querymax)
253

    
254
        cmd = (
255
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
256
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
257
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
258
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
259
            r'ECHO EXIT >> %SCRIPT% & ' +
260
            r'DISKPART /S %SCRIPT% & ' +
261
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
262
            r'DEL /Q %SCRIPT%"')
263

    
264
        stdout, stderr, rc = self._guest_exec(cmd)
265

    
266
        for line in stdout.splitlines():
267
            if line.find('shrunk') >= 0:
268
                self.out.output(line)
269

    
270
    def do_sysprep(self):
271
        """Prepare system for image creation."""
272

    
273
        if getattr(self, 'syspreped', False):
274
            raise FatalError("Image is already syspreped!")
275

    
276
        txt = "System preparation parameter: `%s' is needed but missing!"
277
        for name, param in self.needed_sysprep_params.items():
278
            if name not in self.sysprep_params:
279
                raise FatalError(txt % param)
280

    
281
        self.mount(readonly=False)
282
        try:
283
            disabled_uac = self._update_uac_remote_setting(1)
284
            token = self._enable_os_monitor()
285

    
286
            # disable the firewalls
287
            firewall_states = self._update_firewalls(0, 0, 0)
288

    
289
            # Delete the pagefile. It will be recreated when the system boots
290
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
291
            pagefile = "%s/pagefile.sys" % systemroot
292
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
293

    
294
        finally:
295
            self.umount()
296

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

    
306
        self.out.success('done')
307

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

    
318
            self.out.output("Waiting for OS to boot ...", False)
319
            self._wait_vm_boot(vm, monitor, token)
320
            self.out.success('done')
321

    
322
            self.out.output("Checking connectivity to the VM ...", False)
323
            self._check_connectivity()
324
            self.out.success('done')
325

    
326
            self.out.output("Disabling automatic logon ...", False)
327
            self._disable_autologon()
328
            self.out.success('done')
329

    
330
            self.out.output('Preparing system for image creation:')
331

    
332
            tasks = self.list_syspreps()
333
            enabled = [task for task in tasks if task.enabled]
334
            size = len(enabled)
335

    
336
            # Make sure shrink runs in the end, before ms sysprep
337
            enabled = [task for task in enabled if
338
                       self.sysprep_info(task).name != 'shrink']
339

    
340
            if len(enabled) != size:
341
                enabled.append(self.shrink)
342

    
343
            # Make sure the ms sysprep is the last task to run if it is enabled
344
            enabled = [task for task in enabled if
345
                       self.sysprep_info(task).name != 'microsoft-sysprep']
346

    
347
            ms_sysprep_enabled = False
348
            if len(enabled) != size:
349
                enabled.append(self.microsoft_sysprep)
350
                ms_sysprep_enabled = True
351

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

    
359
            self.out.output("Sending shut down command ...", False)
360
            if not ms_sysprep_enabled:
361
                self._shutdown()
362
            self.out.success("done")
363

    
364
            self.out.output("Waiting for windows to shut down ...", False)
365
            vm.wait(SHUTDOWN_TIMEOUT)
366
            self.out.success("done")
367
        finally:
368
            if monitor is not None:
369
                os.unlink(monitor)
370

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

    
382
                self.mount(readonly=False)
383
                try:
384
                    if disabled_uac:
385
                        self._update_uac_remote_setting(0)
386

    
387
                    self._update_firewalls(*firewall_states)
388
                finally:
389
                    self.umount()
390

    
391
    def _shutdown(self):
392
        """Shuts down the windows VM"""
393
        self._guest_exec(r'shutdown /s /t 5')
394

    
395
    def _wait_vm_boot(self, vm, fname, msg):
396
        """Wait until a message appears on a file or the vm process dies"""
397

    
398
        for _ in range(BOOT_TIMEOUT):
399
            time.sleep(1)
400
            with open(fname) as f:
401
                for line in f:
402
                    if line.startswith(msg):
403
                        return True
404
            if not vm.isalive():
405
                raise FatalError("Windows VM died unexpectedly!")
406

    
407
        raise FatalError("Windows VM booting timed out!")
408

    
409
    def _disable_autologon(self):
410
        """Disable automatic logon on the windows image"""
411

    
412
        winlogon = \
413
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
414

    
415
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
416
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
417
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
418

    
419
    def _registry_file_path(self, regfile):
420
        """Retrieves the case sensitive path to a registry file"""
421

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

    
431
    def _enable_os_monitor(self):
432
        """Add a script in the registry that will send a random string to the
433
        first serial port when the windows image finishes booting.
434
        """
435

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

    
438
        path = self._registry_file_path('SOFTWARE')
439
        softwarefd, software = tempfile.mkstemp()
440
        try:
441
            os.close(softwarefd)
442
            self.g.download(path, software)
443

    
444
            h = hivex.Hivex(software, write=True)
445

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

    
461
            winlogon = h.root()
462
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
463
                          'Winlogon'):
464
                winlogon = h.node_get_child(winlogon, child)
465

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

    
479
            key = h.root()
480
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
481
                key = h.node_get_child(key, child)
482

    
483
            runonce = h.node_get_child(key, "RunOnce")
484
            if runonce is None:
485
                runonce = h.node_add_child(key, "RunOnce")
486

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

    
494
            h.node_set_value(runonce,
495
                             {'key': "BootMonitor", 't': 1, 'value': value})
496

    
497
            value = (
498
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
499
                r'\policies\system /v LocalAccountTokenFilterPolicy'
500
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
501

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

    
505
            h.commit(None)
506

    
507
            self.g.upload(software, path)
508
        finally:
509
            os.unlink(software)
510

    
511
        return token
512

    
513
    def _update_firewalls(self, domain, public, standard):
514
        """Enables or disables the firewall for the Domain, the Public and the
515
        Standard profile. Returns a triplete with the old values.
516

517
        1 will enable a firewall and 0 will disable it
518
        """
519

    
520
        if domain not in (0, 1):
521
            raise ValueError("Valid values for domain parameter are 0 and 1")
522

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

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

    
529
        path = self._registry_file_path("SYSTEM")
530
        systemfd, system = tempfile.mkstemp()
531
        try:
532
            os.close(systemfd)
533
            self.g.download(path, system)
534

    
535
            h = hivex.Hivex(system, write=True)
536

    
537
            select = h.node_get_child(h.root(), 'Select')
538
            current_value = h.node_get_value(select, 'Current')
539

    
540
            # expecting a little endian dword
541
            assert h.value_type(current_value)[1] == 4
542
            current = "%03d" % h.value_dword(current_value)
543

    
544
            firewall_policy = h.root()
545
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
546
                          'Parameters', 'FirewallPolicy'):
547
                firewall_policy = h.node_get_child(firewall_policy, child)
548

    
549
            old_values = []
550
            new_values = [domain, public, standard]
551
            for profile in ('Domain', 'Public', 'Standard'):
552
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
553

    
554
                old_value = h.node_get_value(node, 'EnableFirewall')
555

    
556
                # expecting a little endian dword
557
                assert h.value_type(old_value)[1] == 4
558
                old_values.append(h.value_dword(old_value))
559

    
560
                h.node_set_value(
561
                    node, {'key': 'EnableFirewall', 't': 4L,
562
                           'value': struct.pack("<I", new_values.pop(0))})
563

    
564
            h.commit(None)
565
            self.g.upload(system, path)
566

    
567
        finally:
568
            os.unlink(system)
569

    
570
        return old_values
571

    
572
    def _update_uac_remote_setting(self, value):
573
        """Updates the registry key value:
574
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
575
        \System]"LocalAccountTokenFilterPolicy"
576

577
        value = 1 will disable the UAC remote restrictions
578
        value = 0 will enable the UAC remote restrictions
579

580
        For more info see here: http://support.microsoft.com/kb/951016
581

582
        Returns:
583
            True if the key is changed
584
            False if the key is unchanged
585
        """
586

    
587
        if value not in (0, 1):
588
            raise ValueError("Valid values for value parameter are 0 and 1")
589

    
590
        path = self._registry_file_path('SOFTWARE')
591
        softwarefd, software = tempfile.mkstemp()
592
        try:
593
            os.close(softwarefd)
594
            self.g.download(path, software)
595

    
596
            h = hivex.Hivex(software, write=True)
597

    
598
            key = h.root()
599
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
600
                          'System'):
601
                key = h.node_get_child(key, child)
602

    
603
            policy = None
604
            for val in h.node_values(key):
605
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
606
                    policy = val
607

    
608
            if policy is not None:
609
                dword = h.value_dword(policy)
610
                if dword == value:
611
                    return False
612
            elif value == 0:
613
                return False
614

    
615
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
616
                         'value': struct.pack("<I", value)}
617

    
618
            h.node_set_value(key, new_value)
619
            h.commit(None)
620

    
621
            self.g.upload(software, path)
622

    
623
        finally:
624
            os.unlink(software)
625

    
626
        return True
627

    
628
    def _do_collect_metadata(self):
629
        """Collect metadata about the OS"""
630
        super(Windows, self)._do_collect_metadata()
631
        self.meta["USERS"] = " ".join(self._get_users())
632

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

    
640
            h = hivex.Hivex(sam)
641

    
642
            # Navigate to /SAM/Domains/Account/Users
643
            users_node = h.root()
644
            for child in ('SAM', 'Domains', 'Account', 'Users'):
645
                users_node = h.node_get_child(users_node, child)
646

    
647
            # Navigate to /SAM/Domains/Account/Users/Names
648
            names_node = h.node_get_child(users_node, 'Names')
649

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

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

    
673
                if disabled(f_value):
674
                    self.out.warn("Found disabled `%s' account!" % username)
675
                    continue
676

    
677
                users.append(username)
678

    
679
        finally:
680
            os.unlink(sam)
681

    
682
        # Filter out the guest account
683
        return users
684

    
685
    def _check_connectivity(self):
686
        """Check if winexe works on the Windows VM"""
687

    
688
        passwd = self.sysprep_params['password']
689
        winexe = WinEXE('Administrator', passwd, 'localhost')
690
        winexe.uninstall().debug(9)
691

    
692
        for i in range(CONNECTION_RETRIES):
693
            (stdout, stderr, rc) = winexe.run('cmd /C')
694
            if rc == 0:
695
                return True
696
            log = tempfile.NamedTemporaryFile(delete=False)
697
            try:
698
                log.file.write(stdout)
699
            finally:
700
                log.close()
701
            self.out.output("failed! See: `%s' for the full output" % log.name)
702
            if i < CONNECTION_RETRIES - 1:
703
                self.out.output("Retrying ...", False)
704
        raise FatalError("Connection to the VM failed after %d retries" %
705
                         CONNECTION_RETRIES)
706

    
707
    def _guest_exec(self, command, fatal=True):
708
        """Execute a command on a windows VM"""
709

    
710
        passwd = self.sysprep_params['password']
711

    
712
        winexe = WinEXE('Administrator', passwd, 'localhost')
713
        winexe.runas('Administrator', passwd).uninstall()
714

    
715
        try:
716
            (stdout, stderr, rc) = winexe.run(command)
717
        except WinexeTimeout:
718
            FatalError("Command: `%s' timeout out." % command)
719

    
720
        if rc != 0 and fatal:
721
            reason = stderr if len(stderr) else stdout
722
            self.out.output("Command: `%s' failed (rc=%d). Reason: %s" %
723
                            (command, rc, reason))
724
            raise FatalError("Command: `%s' failed (rc=%d). Reason: %s" %
725
                             (command, rc, reason))
726

    
727
        return (stdout, stderr, rc)
728

    
729

    
730
class _VM(object):
731
    """Windows Virtual Machine"""
732
    def __init__(self, disk, serial):
733
        """Create _VM instance
734

735
            disk: VM's hard disk
736
            serial: File to save the output of the serial port
737
        """
738

    
739
        self.disk = disk
740
        self.serial = serial
741

    
742
        def random_mac():
743
            """creates a random mac address"""
744
            mac = [0x00, 0x16, 0x3e,
745
                   random.randint(0x00, 0x7f),
746
                   random.randint(0x00, 0xff),
747
                   random.randint(0x00, 0xff)]
748

    
749
            return ':'.join(['%02x' % x for x in mac])
750

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

    
754
        args = [
755
            'kvm', '-smp', '1', '-m', '1024', '-drive',
756
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
757
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
758
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
759
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
760
            '-monitor', 'stdio']
761

    
762
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
763
                                        stdout=subprocess.PIPE)
764

    
765
    def isalive(self):
766
        """Check if the VM is still alive"""
767
        return self.process.poll() is None
768

    
769
    def destroy(self):
770
        """Destroy the VM"""
771

    
772
        if not self.isalive():
773
            return
774

    
775
        def handler(signum, frame):
776
            self.process.terminate()
777
            time.sleep(1)
778
            if self.isalive():
779
                self.process.kill()
780
            self.process.wait()
781
            raise FatalError("VM destroy timed-out")
782

    
783
        signal.signal(signal.SIGALRM, handler)
784

    
785
        signal.alarm(SHUTDOWN_TIMEOUT)
786
        self.process.communicate(input="system_powerdown\n")
787
        signal.alarm(0)
788

    
789
    def wait(self, timeout=0):
790
        """Wait for the VM to terminate"""
791

    
792
        def handler(signum, frame):
793
            self.destroy()
794
            raise FatalError("VM wait timed-out.")
795

    
796
        signal.signal(signal.SIGALRM, handler)
797

    
798
        signal.alarm(timeout)
799
        stdout, stderr = self.process.communicate()
800
        signal.alarm(0)
801

    
802
        return (stdout, stderr, self.process.poll())
803

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