Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 06bfd21a

History | View | Annotate | Download (30.2 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
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
    def __init__(self, image, **kargs):
111
        super(Windows, self).__init__(image, **kargs)
112

    
113
        device = self.g.part_to_dev(self.root)
114

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

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

    
125
        assert self.system_drive
126

    
127
        self.product_name = self.g.inspect_get_product_name(self.root)
128

    
129
    def needed_sysprep_params(self):
130
        """Returns a list of needed sysprep parameters. Each element in the
131
        list is a SysprepParam object.
132
        """
133
        password = self.SysprepParam(
134
            'password', 'Image Administrator Password', 20, lambda x: True)
135

    
136
        return [password]
137

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

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

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

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

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

    
155
        self._guest_exec('netsh interface isa set state disabled')
156

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

    
161
        self._guest_exec('netsh firewall set icmpsetting 8')
162

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

    
167
        self._guest_exec(r'powercfg.exe /hibernate off')
168

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

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

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

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

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

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

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

    
211
        self._guest_exec(
212
            "cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
213

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

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

    
229
        stdout, stderr, rc = self._guest_exec(cmd)
230

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

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

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

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

    
258
        self.out.output("\tReclaiming %dMB ..." % querymax)
259

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

    
270
        stdout, stderr, rc = self._guest_exec(cmd)
271

    
272
        for line in stdout.splitlines():
273
            if line.find('shrunk') >= 0:
274
                self.out.output(line)
275

    
276
    def do_sysprep(self):
277
        """Prepare system for image creation."""
278

    
279
        if getattr(self, 'syspreped', False):
280
            raise FatalError("Image is already syspreped!")
281

    
282
        txt = "System preparation parameter: `%s' is needed but missing!"
283
        for param in self.needed_sysprep_params():
284
            if param[0] not in self.sysprep_params:
285
                raise FatalError(txt % param[0])
286

    
287
        self.mount(readonly=False)
288
        try:
289
            disabled_uac = self._update_uac_remote_setting(1)
290
            token = self._enable_os_monitor()
291

    
292
            # disable the firewalls
293
            firewall_states = self._update_firewalls(0, 0, 0)
294

    
295
            # Delete the pagefile. It will be recreated when the system boots
296
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
297
            pagefile = "%s/pagefile.sys" % systemroot
298
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
299

    
300
        finally:
301
            self.umount()
302

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

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

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

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

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

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

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

    
338
            tasks = self.list_syspreps()
339
            enabled = filter(lambda x: x.enabled, tasks)
340
            size = len(enabled)
341

    
342
            # Make sure shrink runs in the end, before ms sysprep
343
            enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
344
                             enabled)
345

    
346
            shrink_enabled = False
347
            if len(enabled) != size:
348
                enabled.append(self.shrink)
349
                shrink_enabled = True
350

    
351
            # Make sure the ms sysprep is the last task to run if it is enabled
352
            enabled = filter(
353
                lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
354
                enabled)
355

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

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

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

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

    
380
            try:
381
                if vm is not None:
382
                    self.out.output("Destroying windows VM ...", False)
383
                    vm.destroy()
384
                    self.out.success("done")
385
            finally:
386
                self.out.output("Relaunching helper VM (may take a while) ...",
387
                                False)
388
                self.g.launch()
389
                self.out.success('done')
390

    
391
                self.mount(readonly=False)
392
                try:
393
                    if disabled_uac:
394
                        self._update_uac_remote_setting(0)
395

    
396
                    self._update_firewalls(*firewall_states)
397
                finally:
398
                    self.umount()
399

    
400
    def _shutdown(self):
401
        """Shuts down the windows VM"""
402
        self._guest_exec(r'shutdown /s /t 5')
403

    
404
    def _wait_vm_boot(self, vm, fname, msg):
405
        """Wait until a message appears on a file or the vm process dies"""
406

    
407
        for i in range(BOOT_TIMEOUT):
408
            time.sleep(1)
409
            with open(fname) as f:
410
                for line in f:
411
                    if line.startswith(msg):
412
                        return True
413
            if not vm.isalive():
414
                raise FatalError("Windows VM died unexpectedly!")
415

    
416
        raise FatalError("Windows VM booting timed out!")
417

    
418
    def _disable_autologon(self):
419
        """Disable automatic logon on the windows image"""
420

    
421
        winlogon = \
422
            r'"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"'
423

    
424
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
425
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
426
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
427

    
428
    def _registry_file_path(self, regfile):
429
        """Retrieves the case sensitive path to a registry file"""
430

    
431
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
432
        path = "%s/system32/config/%s" % (systemroot, regfile)
433
        try:
434
            path = self.g.case_sensitive_path(path)
435
        except RuntimeError as e:
436
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
437
                             % (regfile, str(e)))
438
        return path
439

    
440
    def _enable_os_monitor(self):
441
        """Add a script in the registry that will send a random string to the
442
        first serial port when the windows image finishes booting.
443
        """
444

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

    
447
        path = self._registry_file_path('SOFTWARE')
448
        softwarefd, software = tempfile.mkstemp()
449
        try:
450
            os.close(softwarefd)
451
            self.g.download(path, software)
452

    
453
            h = hivex.Hivex(software, write=True)
454

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

    
470
            winlogon = h.root()
471
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
472
                          'Winlogon'):
473
                winlogon = h.node_get_child(winlogon, child)
474

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

    
488
            key = h.root()
489
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
490
                key = h.node_get_child(key, child)
491

    
492
            runonce = h.node_get_child(key, "RunOnce")
493
            if runonce is None:
494
                runonce = h.node_add_child(key, "RunOnce")
495

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

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

    
506
            value = (
507
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
508
                r'\policies\system /v LocalAccountTokenFilterPolicy'
509
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
510

    
511
            h.node_set_value(runonce,
512
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
513

    
514
            h.commit(None)
515

    
516
            self.g.upload(software, path)
517
        finally:
518
            os.unlink(software)
519

    
520
        return token
521

    
522
    def _update_firewalls(self, domain, public, standard):
523
        """Enables or disables the firewall for the Domain, the Public and the
524
        Standard profile. Returns a triplete with the old values.
525

526
        1 will enable a firewall and 0 will disable it
527
        """
528

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

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

    
535
        if standard not in (0, 1):
536
            raise ValueError("Valid values for standard parameter are 0 and 1")
537

    
538
        path = self._registry_file_path("SYSTEM")
539
        systemfd, system = tempfile.mkstemp()
540
        try:
541
            os.close(systemfd)
542
            self.g.download(path, system)
543

    
544
            h = hivex.Hivex(system, write=True)
545

    
546
            select = h.node_get_child(h.root(), 'Select')
547
            current_value = h.node_get_value(select, 'Current')
548

    
549
            # expecting a little endian dword
550
            assert h.value_type(current_value)[1] == 4
551
            current = "%03d" % h.value_dword(current_value)
552

    
553
            firewall_policy = h.root()
554
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
555
                          'Parameters', 'FirewallPolicy'):
556
                firewall_policy = h.node_get_child(firewall_policy, child)
557

    
558
            old_values = []
559
            new_values = [domain, public, standard]
560
            for profile in ('Domain', 'Public', 'Standard'):
561
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
562

    
563
                old_value = h.node_get_value(node, 'EnableFirewall')
564

    
565
                # expecting a little endian dword
566
                assert h.value_type(old_value)[1] == 4
567
                old_values.append(h.value_dword(old_value))
568

    
569
                h.node_set_value(
570
                    node, {'key': 'EnableFirewall', 't': 4L,
571
                           'value': struct.pack("<I", new_values.pop(0))})
572

    
573
            h.commit(None)
574
            self.g.upload(system, path)
575

    
576
        finally:
577
            os.unlink(system)
578

    
579
        return old_values
580

    
581
    def _update_uac_remote_setting(self, value):
582
        """Updates the registry key value:
583
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
584
        \System]"LocalAccountTokenFilterPolicy"
585

586
        value = 1 will disable the UAC remote restrictions
587
        value = 0 will enable the UAC remote restrictions
588

589
        For more info see here: http://support.microsoft.com/kb/951016
590

591
        Returns:
592
            True if the key is changed
593
            False if the key is unchanged
594
        """
595

    
596
        if value not in (0, 1):
597
            raise ValueError("Valid values for value parameter are 0 and 1")
598

    
599
        path = self._registry_file_path('SOFTWARE')
600
        softwarefd, software = tempfile.mkstemp()
601
        try:
602
            os.close(softwarefd)
603
            self.g.download(path, software)
604

    
605
            h = hivex.Hivex(software, write=True)
606

    
607
            key = h.root()
608
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
609
                          'System'):
610
                key = h.node_get_child(key, child)
611

    
612
            policy = None
613
            for val in h.node_values(key):
614
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
615
                    policy = val
616

    
617
            if policy is not None:
618
                dword = h.value_dword(policy)
619
                if dword == value:
620
                    return False
621
            elif value == 0:
622
                return False
623

    
624
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
625
                         'value': struct.pack("<I", value)}
626

    
627
            h.node_set_value(key, new_value)
628
            h.commit(None)
629

    
630
            self.g.upload(software, path)
631

    
632
        finally:
633
            os.unlink(software)
634

    
635
        return True
636

    
637
    def _do_collect_metadata(self):
638
        """Collect metadata about the OS"""
639
        super(Windows, self)._do_collect_metadata()
640
        self.meta["USERS"] = " ".join(self._get_users())
641

    
642
    def _get_users(self):
643
        """Returns a list of users found in the images"""
644
        path = self._registry_file_path('SAM')
645
        samfd, sam = tempfile.mkstemp()
646
        try:
647
            os.close(samfd)
648
            self.g.download(path, sam)
649

    
650
            h = hivex.Hivex(sam)
651

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

    
657
            # Navigate to /SAM/Domains/Account/Users/Names
658
            names_node = h.node_get_child(users_node, 'Names')
659

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

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

    
683
                if disabled(f_value):
684
                    self.out.warn("Found disabled `%s' account!" % username)
685
                    continue
686

    
687
                users.append(username)
688

    
689
        finally:
690
            os.unlink(sam)
691

    
692
        # Filter out the guest account
693
        return users
694

    
695
    def _check_connectivity(self):
696
        """Check if winexe works on the Windows VM"""
697

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

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

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

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

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

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

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

    
737
        return (stdout, stderr, rc)
738

    
739

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

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

    
749
        self.disk = disk
750
        self.serial = serial
751

    
752
        def random_mac():
753
            mac = [0x00, 0x16, 0x3e,
754
                   random.randint(0x00, 0x7f),
755
                   random.randint(0x00, 0xff),
756
                   random.randint(0x00, 0xff)]
757

    
758
            return ':'.join(map(lambda x: "%02x" % x, mac))
759

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

    
763
        args = [
764
            'kvm', '-smp', '1', '-m', '1024', '-drive',
765
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.disk,
766
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
767
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
768
            '-vnc', ':%d' % self.display, '-serial', 'file:%s' % self.serial,
769
            '-monitor', 'stdio']
770

    
771
        self.process = subprocess.Popen(args, stdin=subprocess.PIPE,
772
                                        stdout=subprocess.PIPE)
773

    
774
    def isalive(self):
775
        """Check if the VM is still alive"""
776
        return self.process.poll() is None
777

    
778
    def destroy(self):
779
        """Destroy the VM"""
780

    
781
        if not self.isalive():
782
            return
783

    
784
        def handler(signum, frame):
785
            self.process.terminate()
786
            time.sleep(1)
787
            if self.isalive():
788
                self.process.kill()
789
            self.process.wait()
790
            self.out.output("timed-out")
791
            raise FatalError("VM destroy timed-out")
792

    
793
        signal.signal(signal.SIGALRM, handler)
794

    
795
        signal.alarm(SHUTDOWN_TIMEOUT)
796
        self.process.communicate(input="system_powerdown\n")
797
        signal.alarm(0)
798

    
799
    def wait(self, timeout=0):
800
        """Wait for the VM to terminate"""
801

    
802
        def handler(signum, frame):
803
            self.destroy()
804
            raise FatalError("VM wait timed-out.")
805

    
806
        signal.signal(signal.SIGALRM, handler)
807

    
808
        signal.alarm(timeout)
809
        stdout, stderr = self.process.communicate()
810
        signal.alarm(0)
811

    
812
        return (stdout, stderr, self.process.poll())
813

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