Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ f59ad913

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

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

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

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

    
107

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

    
111
    @add_sysprep_param('password', 'Image Administrator Password', 20)
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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
293
        finally:
294
            self.umount()
295

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

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

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

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

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

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

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

    
331
            tasks = self.list_syspreps()
332
            enabled = filter(lambda x: x.enabled, tasks)
333
            size = len(enabled)
334

    
335
            # Make sure shrink runs in the end, before ms sysprep
336
            enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
337
                             enabled)
338

    
339
            shrink_enabled = False
340
            if len(enabled) != size:
341
                enabled.append(self.shrink)
342
                shrink_enabled = True
343

    
344
            # Make sure the ms sysprep is the last task to run if it is enabled
345
            enabled = filter(
346
                lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
347
                enabled)
348

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

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

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

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

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

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

    
389
                    self._update_firewalls(*firewall_states)
390
                finally:
391
                    self.umount()
392

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

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

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

    
409
        raise FatalError("Windows VM booting timed out!")
410

    
411
    def _disable_autologon(self):
412
        """Disable automatic logon on the windows image"""
413

    
414
        winlogon = \
415
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
416

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

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

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

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

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

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

    
446
            h = hivex.Hivex(software, write=True)
447

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

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

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

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

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

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

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

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

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

    
507
            h.commit(None)
508

    
509
            self.g.upload(software, path)
510
        finally:
511
            os.unlink(software)
512

    
513
        return token
514

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

519
        1 will enable a firewall and 0 will disable it
520
        """
521

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

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

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

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

    
537
            h = hivex.Hivex(system, write=True)
538

    
539
            select = h.node_get_child(h.root(), 'Select')
540
            current_value = h.node_get_value(select, 'Current')
541

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

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

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

    
556
                old_value = h.node_get_value(node, 'EnableFirewall')
557

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

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

    
566
            h.commit(None)
567
            self.g.upload(system, path)
568

    
569
        finally:
570
            os.unlink(system)
571

    
572
        return old_values
573

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

579
        value = 1 will disable the UAC remote restrictions
580
        value = 0 will enable the UAC remote restrictions
581

582
        For more info see here: http://support.microsoft.com/kb/951016
583

584
        Returns:
585
            True if the key is changed
586
            False if the key is unchanged
587
        """
588

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

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

    
598
            h = hivex.Hivex(software, write=True)
599

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

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

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

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

    
620
            h.node_set_value(key, new_value)
621
            h.commit(None)
622

    
623
            self.g.upload(software, path)
624

    
625
        finally:
626
            os.unlink(software)
627

    
628
        return True
629

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

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

    
643
            h = hivex.Hivex(sam)
644

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

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

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

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

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

    
680
                users.append(username)
681

    
682
        finally:
683
            os.unlink(sam)
684

    
685
        # Filter out the guest account
686
        return users
687

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

    
691
        passwd = self.sysprep_params['password']
692
        winexe = WinEXE('Administrator', passwd, 'localhost')
693
        winexe.uninstall().debug(9)
694

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

    
710
    def _guest_exec(self, command, fatal=True):
711
        """Execute a command on a windows VM"""
712

    
713
        passwd = self.sysprep_params['password']
714

    
715
        winexe = WinEXE('Administrator', passwd, 'localhost')
716
        winexe.runas('Administrator', passwd).uninstall()
717

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

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

    
730
        return (stdout, stderr, rc)
731

    
732

    
733
class _VM(object):
734
    """Windows Virtual Machine"""
735
    def __init__(self, disk, serial):
736
        """Create _VM instance
737

738
            disk: VM's hard disk
739
            serial: File to save the output of the serial port
740
        """
741

    
742
        self.disk = disk
743
        self.serial = serial
744

    
745
        def random_mac():
746
            mac = [0x00, 0x16, 0x3e,
747
                   random.randint(0x00, 0x7f),
748
                   random.randint(0x00, 0xff),
749
                   random.randint(0x00, 0xff)]
750

    
751
            return ':'.join(map(lambda x: "%02x" % x, mac))
752

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

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

    
764
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
765
                                        stdout=subprocess.PIPE)
766

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

    
771
    def destroy(self):
772
        """Destroy the VM"""
773

    
774
        if not self.isalive():
775
            return
776

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

    
786
        signal.signal(signal.SIGALRM, handler)
787

    
788
        signal.alarm(SHUTDOWN_TIMEOUT)
789
        self.process.communicate(input="system_powerdown\n")
790
        signal.alarm(0)
791

    
792
    def wait(self, timeout=0):
793
        """Wait for the VM to terminate"""
794

    
795
        def handler(signum, frame):
796
            self.destroy()
797
            raise FatalError("VM wait timed-out.")
798

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

    
801
        signal.alarm(timeout)
802
        stdout, stderr = self.process.communicate()
803
        signal.alarm(0)
804

    
805
        return (stdout, stderr, self.process.poll())
806

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