__init__.py 37.8 KB
Newer Older
Nikos Skalkotos's avatar
Nikos Skalkotos committed
1
2
# -*- coding: utf-8 -*-
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
3
# Copyright (C) 2011-2014 GRNET S.A.
4
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
5
6
7
8
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
9
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
10
11
12
13
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
14
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
15
16
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

18
"""This package hosts OS-specific code common for the various Microsoft
Nikos Skalkotos's avatar
Nikos Skalkotos committed
19
20
Windows OSs."""

21
from image_creator.os_type import OSBase, sysprep, add_sysprep_param
22
from image_creator.util import FatalError
23
from image_creator.os_type.windows.vm import VM, RANDOM_TOKEN as TOKEN
24
from image_creator.os_type.windows.registry import Registry
25
from image_creator.os_type.windows.winexe import WinEXE
26
from image_creator.os_type.windows.powershell import DRVINST_HEAD, SAFEBOOT, \
27
    DRVINST_TAIL, ADD_CERTIFICATE, ADD_DRIVER, INSTALL_DRIVER, REMOVE_DRIVER
Nikos Skalkotos's avatar
Nikos Skalkotos committed
28

29
import tempfile
30
import re
31
import os
32
import uuid
33
import time
34

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

91
92
93
94
95
96
97
98
99
# The PCI Device ID for VirtIO devices. 1af4 is the Vendor ID for Red Hat, Inc
VIRTIO_DEVICE_ID = re.compile(r'pci\\ven_1af4&dev_100[0-5]')
VIRTIO = (      # id    Name
    "netkvm",   # 1000	Virtio network device
    "viostor",  # 1001	Virtio block device
    "balloon",  # 1002	Virtio memory balloon
    "vioser",   # 1003	Virtio console
    "vioscsi",  # 1004	Virtio SCSI
    "viorng")   # 1005	Virtio RNG
100
101


102
103
def parse_inf(inf):
    """Parse the content of a Windows INF file and fetch all information found
104
105
106
107
108
    in the Version section, the target OS as well as the VirtIO drivers it
    defines.

    For more info check here:
        http://msdn.microsoft.com/en-us/library/windows/hardware/ff549520
109
110
    """

111
112
113
114
115
116
    driver = None
    target_os = set()

    sections = {}
    current = {}

117
118
    prev_line = ""
    for line in iter(inf):
119
120
        # Strip comments
        line = prev_line + line.split(';')[0].strip()
121
122
123
124
125
        prev_line = ""

        if not len(line):
            continue

126
        # Does the directive span more lines?
127
128
129
130
131
132
133
        if line[-1] == "\\":
            prev_line = line
            continue

        # Does the line denote a section?
        if line.startswith('[') and line.endswith(']'):
            section = line[1:-1].strip().lower()
134
135
136
137
138
            if section not in sections:
                current = {}
                sections[section] = current
            else:
                current = sections[section]
139
140
141
            continue

        # We only care about param = value lines
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        if line.find('=') > 0:
            param, value = line.split('=', 1)
            current[param.strip()] = value.strip()

    models = []
    if 'manufacturer' in sections:
        for value in sections['manufacturer'].values():
            value = value.split(',')
            if len(value) == 0:
                continue

            # %strkey%=models-section-name [,TargetOSVersion] ...
            models.append(value[0].strip().lower())
            for i in range(len(value) - 1):
                target_os.add(value[i+1].strip().lower())

    if len(models):
        # [models-section-name] | [models-section-name.TargetOSVersion]
160
161
        models_section_name = \
            re.compile('^(' + "|".join(models) + ')(\\..+)?$')
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
        for model in [s for s in sections if models_section_name.match(s)]:
            for value in sections[model].values():
                value = value.split(',')
                if len(value) == 1:
                    continue
                # The second value in a device-description entry is always the
                # hardware ID:
                #   install-section-name[,hw-id][,compatible-id...]
                hw_id = value[1].strip().lower()
                # If this matches a VirtIO device, then this is a VirtIO driver
                id_match = VIRTIO_DEVICE_ID.match(hw_id)
                if id_match:
                    driver = VIRTIO[int(id_match.group(0)[-1])]

    if 'version' in sections and 'strings' in sections:
        # Replace all strkey tokens with their actual value
178
179
180
        for key, val in sections['version'].items():
            if val.startswith('%') and val.endswith('%'):
                strkey = val[1:-1]
181
                if strkey in sections['strings']:
182
                    sections['version'][key] = sections['strings'][strkey]
183

184
185
    if len(target_os) == 0:
        target_os.add('ntx86')
186

187
    version = sections['version'] if 'version' in sections else {}
188

189
190
191
192
193
194
195
196
197
198
199
200
201
202
    return driver, target_os, version


def virtio_dir_check(dirname):
    """Check if the needed virtio driver files are present in the dirname
    directory
    """
    if not dirname:
        return ""  # value not set

    # Check files in a case insensitive manner
    files = set(os.listdir(dirname))

    for inf in [f for f in files if f.lower().endswith('.inf')]:
203
204
        with open(os.path.join(dirname, inf)) as content:
            driver, _, _ = parse_inf(content)
205
206
207
208
            if driver:
                return dirname

    raise ValueError("Invalid VirtIO directory. No VirtIO driver found")
209
210


211
DESCR = {
212
213
214
215
216
217
218
219
    "boot_timeout":
    "Time in seconds to wait for the Windows customization VM to boot.",
    "shutdown_timeout":
    "Time in seconds to wait for the Windows customization VM to shut down "
    "after the initial command is given.",
    "connection_retries":
    "Number of times to try to connect to the Windows customization VM after "
    "it has booted, before giving up.",
220
221
222
    "smp": "Number of CPUs to use for the Windows customization VM.",
    "mem": "Virtual RAM size in MiB for the Windows customization VM.",
    "admin": "Name of the Administration user.",
223
    "virtio": "Directory hosting the Windows virtio drivers.",
224
225
    "virtio_timeout":
    "Time in seconds to wait for the installation of the VirtIO drivers."}
226

Nikos Skalkotos's avatar
Nikos Skalkotos committed
227

Nikos Skalkotos's avatar
Nikos Skalkotos committed
228
class Windows(OSBase):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
229
    """OS class for Windows"""
230
231
232
    @add_sysprep_param('admin', "string", 'Administrator', DESCR['admin'])
    @add_sysprep_param('mem', "posint", 1024, DESCR['mem'])
    @add_sysprep_param('smp', "posint", 1, DESCR['smp'])
233
    @add_sysprep_param(
234
        'connection_retries', "posint", 5, DESCR['connection_retries'])
235
    @add_sysprep_param(
236
237
238
        'shutdown_timeout', "posint", 120, DESCR['shutdown_timeout'])
    @add_sysprep_param('boot_timeout', "posint", 300, DESCR['boot_timeout'])
    @add_sysprep_param('virtio', 'dir', "", DESCR['virtio'], virtio_dir_check)
239
240
    @add_sysprep_param(
        'virtio_timeout', 'posint', 300, DESCR['virtio_timeout'])
241
242
243
    def __init__(self, image, **kargs):
        super(Windows, self).__init__(image, **kargs)

244
        # The commit with the following message was added in
245
        # libguestfs 1.17.18 and was backported in version 1.16.11:
246
247
248
249
        #
        # When a Windows guest doesn't have a HKLM\SYSTEM\MountedDevices node,
        # inspection fails.  However inspection should not completely fail just
        # because we cannot get the drive letter mapping from a guest.
250
251
252
253
        #
        # Since Microsoft Sysprep removes the aforementioned key, image
        # creation for windows can only be supported if the installed guestfs
        # version is 1.17.18 or higher
254
255
256
        if self.image.check_guestfs_version(1, 17, 18) < 0 and \
                (self.image.check_guestfs_version(1, 17, 0) >= 0 or
                 self.image.check_guestfs_version(1, 16, 11) < 0):
257
            raise FatalError(
258
                'For windows support libguestfs 1.16.11 or above is required')
259

260
        device = self.image.g.part_to_dev(self.root)
261

262
        self.last_part_num = self.image.g.part_list(device)[-1]['part_num']
263

264
        self.product_name = self.image.g.inspect_get_product_name(self.root)
265
266
        self.systemroot = self.image.g.inspect_get_windows_systemroot(
            self.root)
267
268

        self.vm = VM(self.image.device, self.sysprep_params)
269
        self.registry = Registry(self.image)
270

271
        # If the image is already sysprepped we cannot further customize it
272
        with self.mount(readonly=True, silent=True):
273
274
275
            self.out.output("Checking media state ...", False)
            self.sysprepped = self.registry.get_setup_state() > 0
            self.virtio_state = self._virtio_state()
276
277
278
279
280
281
282
283
284
285
            arch = self.image.g.inspect_get_arch(self.root)
            if arch == 'x86_64':
                arch = 'amd64'
            elif arch == 'i386':
                arch = 'x86'
            major = self.image.g.inspect_get_major_version(self.root)
            minor = self.image.g.inspect_get_minor_version(self.root)
            # This is the OS version as defined in INF files to check if a
            # driver is valid for this OS.
            self.windows_version = "nt%s.%s.%s" % (arch, major, minor)
286
            self.out.success("done")
287

288
289
290
291
292
293
        # If the image is sysprepped no driver mappings will be present.
        self.systemdrive = None
        for drive, root in self.image.g.inspect_get_drive_mappings(self.root):
            if root == self.root:
                self.systemdrive = drive

294
295
    @sysprep('Disabling IPv6 privacy extensions')
    def disable_ipv6_privacy_extensions(self):
296
297
        """Disable IPv6 privacy extensions"""

298
299
        self.vm.rexec('netsh interface ipv6 set global '
                      'randomizeidentifiers=disabled store=persistent')
300

Nikos Skalkotos's avatar
Nikos Skalkotos committed
301
302
303
304
    @sysprep('Disabling Teredo interface')
    def disable_teredo(self):
        """Disable Teredo interface"""

305
        self.vm.rexec('netsh interface teredo set state disabled')
Nikos Skalkotos's avatar
Nikos Skalkotos committed
306
307
308
309
310

    @sysprep('Disabling ISATAP Adapters')
    def disable_isatap(self):
        """Disable ISATAP Adapters"""

311
        self.vm.rexec('netsh interface isa set state disabled')
Nikos Skalkotos's avatar
Nikos Skalkotos committed
312
313
314

    @sysprep('Enabling ping responses')
    def enable_pings(self):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
315
        """Enable ping responses"""
Nikos Skalkotos's avatar
Nikos Skalkotos committed
316

317
        self.vm.rexec('netsh firewall set icmpsetting 8')
Nikos Skalkotos's avatar
Nikos Skalkotos committed
318
319
320
321
322
323

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

        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
324
        self.vm.rexec(
Nikos Skalkotos's avatar
Nikos Skalkotos committed
325
326
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)

327
328
329
330
    @sysprep('Clearing the event logs')
    def clear_logs(self):
        """Clear all the event logs"""

331
        self.vm.rexec(
332
333
            "cmd /q /c for /f \"tokens=*\" %l in ('wevtutil el') do "
            "wevtutil cl \"%l\"")
334

Nikos Skalkotos's avatar
Nikos Skalkotos committed
335
    @sysprep('Executing Sysprep on the image (may take more that 10 min)')
336
    def microsoft_sysprep(self):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
337
338
339
        """Run the Microsoft System Preparation Tool. This will remove
        system-specific data and will make the image ready to be deployed.
        After this no other task may run.
340
341
        """

342
        self.vm.rexec(r'C:\Windows\system32\sysprep\sysprep '
343
                      r'/quiet /generalize /oobe /shutdown', uninstall=True)
344
        self.sysprepped = True
345

346
347
348
349
350
    @sysprep('Converting the image into a KMS client', enabled=False)
    def kms_client_setup(self):
        """Install the appropriate KMS client setup key to the image to convert
        it to a KMS client. Computers that are running volume licensing
        editions of Windows 8, Windows Server 2012, Windows 7, Windows Server
351
        2008 R2, Windows Vista, and Windows Server 2008 are by default KMS
352
353
354
355
356
357
358
359
360
361
        clients with no additional configuration needed.
        """
        try:
            setup_key = KMS_CLIENT_SETUP_KEYS[self.product_name]
        except KeyError:
            self.out.warn(
                "Don't know the KMS client setup key for product: `%s'" %
                self.product_name)
            return

362
        self.vm.rexec(
363
            r"cscript \Windows\system32\slmgr.vbs /ipk %s" % setup_key)
364

365
366
367
    @sysprep('Shrinking the last filesystem')
    def shrink(self):
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
368
369
370

        # Query for the maximum number of reclaimable bytes
        cmd = (
371
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
372
373
374
375
376
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
            r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
            r'ECHO EXIT >> %SCRIPT% & ' +
            r'DISKPART /S %SCRIPT% & ' +
377
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
378
379
            r'DEL /Q %SCRIPT%"')

380
        stdout, stderr, rc = self.vm.rexec(cmd)
381
382
383
384
385
386
387
388

        querymax = None
        for line in stdout.splitlines():
            # diskpart will return something like this:
            #
            #   The maximum number of reclaimable bytes is: xxxx MB
            #
            if line.find('reclaimable') >= 0:
389
                answer = line.split(':')[1].strip()
390
                m = re.search(r'(\d+) MB', answer)
391
392
393
394
395
396
                if m:
                    querymax = m.group(1)
                else:
                    FatalError(
                        "Unexpected output for `shrink querymax' command: %s" %
                        line)
397
398
399
400
401
402
403
404
405
406
407
408
409
410

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

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

        if querymax < 0:
411
            self.out.warn("Not enough available space to shrink the image!")
412
413
            return

414
415
        self.out.output("\tReclaiming %dMB ..." % querymax)

416
        cmd = (
417
            r'cmd /Q /V:ON /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
418
419
420
421
422
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
            r'ECHO EXIT >> %SCRIPT% & ' +
            r'DISKPART /S %SCRIPT% & ' +
423
            r'IF NOT !ERRORLEVEL! EQU 0 EXIT /B 1 & ' +
424
425
            r'DEL /Q %SCRIPT%"')

426
        stdout, stderr, rc = self.vm.rexec(cmd, fatal=False)
427

428
429
430
431
        if rc != 0:
            FatalError("Shrinking failed. Please make sure the media is "
                       "defraged with a command like this: "
                       "`Defrag.exe /U /X /W'")
432
433
        for line in stdout.splitlines():
            if line.find('shrunk') >= 0:
434
                self.out.output(" %s" % line)
435

436
437
438
    def do_sysprep(self):
        """Prepare system for image creation."""

439
440
        self.out.output('Preparing system for image creation:')

441
442
443
444
445
446
        # Check if winexe is installed
        if not WinEXE.is_installed():
            raise FatalError(
                "Winexe not found! In order to be able to customize a Windows "
                "image you need to have Winexe installed.")

447
        if self.sysprepped:
448
            raise FatalError(
449
                "Microsoft's System Preparation Tool has ran on the media. "
450
                "Further image customization is not possible.")
451

452
        if len(self.virtio_state['viostor']) == 0:
453
            raise FatalError(
454
455
                "The media has no VirtIO SCSI controller driver installed. "
                "Further image customization is not possible.")
456

457
        if len(self.virtio_state['netkvm']) == 0:
458
            raise FatalError(
459
460
                "The media has no VirtIO Ethernet Adapter driver installed. "
                "Further image customization is not possible.")
461

462
463
464
        admin = self.sysprep_params['admin'].value
        timeout = self.sysprep_params['boot_timeout'].value
        shutdown_timeout = self.sysprep_params['shutdown_timeout'].value
465

466
        self.out.output("Preparing media for boot ...", False)
467

468
        with self.mount(readonly=False, silent=True):
469
            activated = self.registry.reset_account(admin)
470
            v_val = self.registry.reset_passwd(admin)
471
            disabled_uac = self.registry.update_uac_remote_setting(1)
472
            self._add_boot_scripts()
473
474

            # disable the firewalls
475
            firewall_states = self.registry.update_firewalls(0, 0, 0)
476
477

            # Delete the pagefile. It will be recreated when the system boots
478
            try:
479
                pagefile = "%s/pagefile.sys" % self.systemroot
480
                self.image.g.rm_rf(self.image.g.case_sensitive_path(pagefile))
481
482
            except RuntimeError:
                pass
483

484
        self.out.success('done')
485

486
        self.image.disable_guestfs()
487
        booted = False
488
        try:
489
            self.out.output("Starting windows VM ...", False)
490
            self.vm.start()
491
492
493
            try:
                self.out.success("started (console on VNC display: %d)" %
                                 self.vm.display)
494

495
                self.out.output("Waiting for OS to boot ...", False)
496
                if not self.vm.wait_on_serial(timeout):
497
498
                    raise FatalError("Windows VM booting timed out!")
                self.out.success('done')
499
                booted = True
500

501
502
503
504
505
                # Since the password is reset when logging in, sleep a little
                # bit before checking the connectivity, to avoid race
                # conditions
                time.sleep(2)

506
507
                self.out.output("Checking connectivity to the VM ...", False)
                self._check_connectivity()
508
                # self.out.success('done')
509

510
                # self.out.output("Disabling automatic logon ...", False)
511
512
                self._disable_autologon()
                self.out.success('done')
513

514
                self._exec_sysprep_tasks()
515

516
                self.out.output("Waiting for windows to shut down ...", False)
517
                self.vm.wait(shutdown_timeout)
518
519
520
521
522
                self.out.success("done")
            finally:
                # if the VM is not already dead here, a Fatal Error will have
                # already been raised. There is no reason to make the command
                # fatal.
523
                self.vm.stop(shutdown_timeout if booted else 1, fatal=False)
524
        finally:
525
526
            self.image.enable_guestfs()

527
            self.out.output("Reverting media boot preparations ...", False)
528
            with self.mount(readonly=False, silent=True, fatal=False):
529

530
531
532
533
534
535
                if not self.ismounted:
                    self.out.warn("The boot changes cannot be reverted. "
                                  "The snapshot may be in a corrupted state.")
                else:
                    if disabled_uac:
                        self.registry.update_uac_remote_setting(0)
536

537
538
539
                    if not activated:
                        self.registry.reset_account(admin, False)

540
541
542
543
544
545
                    if not self.sysprepped:
                        # Reset the old password
                        self.registry.reset_passwd(admin, v_val)

                    self.registry.update_firewalls(*firewall_states)
                    self.out.success("done")
546

547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
    def _exec_sysprep_tasks(self):
        """This function hosts the actual code for executing the enabled
        sysprep tasks. At the end of this method the VM is shut down if needed.
        """
        tasks = self.list_syspreps()
        enabled = [task for task in tasks if task.enabled]
        size = len(enabled)

        # Make sure shrink runs in the end, before ms sysprep
        enabled = [task for task in enabled
                   if self.sysprep_info(task).name != 'shrink']
        if len(enabled) != size:
            enabled.append(self.shrink)

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

        if len(enabled) != size:
            enabled.append(self.microsoft_sysprep)

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

        self.out.output("Sending shut down command ...", False)
576
        if not self.sysprepped:
577
578
            self._shutdown()
        self.out.success("done")
579

580
581
    def _shutdown(self):
        """Shuts down the windows VM"""
582
        self.vm.rexec(r'shutdown /s /t 5', uninstall=True)
583

584
585
586
587
588
589
    def _disable_autologon(self):
        """Disable automatic logon on the windows image"""

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

590
591
592
        self.vm.rexec('REG DELETE %s /v DefaultUserName /f' % winlogon)
        self.vm.rexec('REG DELETE %s /v DefaultPassword /f' % winlogon)
        self.vm.rexec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
593

594
595
    def _add_boot_scripts(self):
        """Add various scripts in the registry that will be executed during the
596
        next boot.
597
598
        """

599
600
        commands = {}

601
602
        # Disable hibernation. This is not needed for a VM
        commands['hibernate'] = r'powercfg.exe /hibernate off'
603
604
        # This script will send a random string to the first serial port. This
        # can be used to determine when the OS has booted.
605
        commands['BootMonitor'] = "cmd /q /a /c echo " + TOKEN + " > COM1"
606
607

        # This will update the password of the admin user to self.vm.password
608
609
        commands["UpdatePassword"] = "net user %s %s" % \
            (self.sysprep_params['admin'].value, self.vm.password)
610
611
612
613
614
615
616
617
618
619
620
621
622
623

        # This is previously done with hivex when we executed
        # self.registry.update_uac_remote_setting(1).
        # Although the command above works on all windows version and the
        # UAC remote restrictions are disabled, on Windows 2012 the registry
        # value seems corrupted after we run the command. Maybe this has to do
        # with a bug or a limitation in hivex. As a workaround we re-update the
        # value from within Windows.
        commands["UpdateRegistry"] = \
            (r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
             r'\policies\system /v LocalAccountTokenFilterPolicy'
             r' /t REG_DWORD /d 1 /f')

        self.registry.runonce(commands)
624

625
        # Enable automatic logon.
626
627
628
629
630
631
        # This is needed in order for the scripts we added in the RunOnce
        # registry entry to get executed, since the RunOnce commands only get
        # executed when a user logs on. There is a RunServicesOnce registry
        # entry whose keys get executed in the background when the logon dialog
        # box first appears, but they seem to only work with services and not
        # arbitrary command line expressions :-(
632
633
634
635
636
637
638
639
        #
        # Instructions on how to turn on automatic logon in Windows can be
        # found here: http://support.microsoft.com/kb/324737
        #
        # Warning: Registry change will not work if the “Logon Banner” is
        # defined on the server either by a Group Policy object (GPO) or by a
        # local policy.

640
        self.registry.enable_autologon(self.sysprep_params['admin'].value)
641

642
643
644
    def _do_collect_metadata(self):
        """Collect metadata about the OS"""
        super(Windows, self)._do_collect_metadata()
645

646
647
648
        # We only care for active users
        _, users = self.registry.enum_users()
        self.meta["USERS"] = " ".join(users)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
649

650
651
652
    def _check_connectivity(self):
        """Check if winexe works on the Windows VM"""

653
        retries = self.sysprep_params['connection_retries'].value
654
655
656
657
658
659
        # If the connection_retries parameter is set to 0 disable the
        # connectivity check
        if retries == 0:
            return True

        for i in range(retries):
660
661
            (stdout, stderr, rc) = self.vm.rexec('cmd /C', fatal=False,
                                                 debug=True)
662
663
            if rc == 0:
                return True
664

665
666
            log = tempfile.NamedTemporaryFile(delete=False)
            try:
667
668
                log.file.write("STDOUT:\n%s\n" % stdout)
                log.file.write("STDERR:\n%s\n" % stderr)
669
670
            finally:
                log.close()
671
            self.out.output("failed! See: `%s' for the full output" % log.name)
672
673
674
675
676
            if i < retries - 1:
                self.out.output("retrying ...", False)

        raise FatalError("Connection to the Windows VM failed after %d retries"
                         % retries)
677

678
679
680
    def _virtio_state(self, directory=None):
        """Returns information about the VirtIO drivers found either in a
        directory or the media itself if the directory is None.
681
        """
682
683
        state = {}
        for driver in VIRTIO:
684
685
            state[driver] = {}

686
        def oem_files():
687
            """Parse oem*.inf files under the %SystemRoot%/Inf directory"""
688
689
            path = self.image.g.case_sensitive_path("%s/inf" % self.systemroot)
            oem = re.compile(r'^oem\d+\.inf', flags=re.IGNORECASE)
690
            for name in [f['name'] for f in self.image.g.readdir(path)]:
691
                if not oem.match(name):
692
                    continue
693
694
695
696
                yield name, \
                    self.image.g.cat("%s/%s" % (path, name)).splitlines()

        def local_files():
697
            """Parse *.inf files under a local directory"""
698
699
700
701
702
            assert os.path.isdir(directory)
            inf = re.compile(r'^.+\.inf', flags=re.IGNORECASE)
            for name in os.listdir(directory):
                fullpath = os.path.join(directory, name)
                if inf.match(name) and os.path.isfile(fullpath):
703
704
                    with open(fullpath, 'r') as content:
                        yield name, content
705
706

        for name, txt in oem_files() if directory is None else local_files():
707
            driver, target, content = parse_inf(txt)
708

709
710
711
            if driver:
                content['TargetOSVersions'] = target
                state[driver][name] = content
712

713
        return state
714

715
716
717
718
719
720
    def _fetch_virtio_drivers(self, dirname):
        """Examines a directory for VirtIO drivers and returns only the drivers
        that are suitable for this media.
        """
        collection = self._virtio_state(dirname)

721
722
723
724
        files = set([f.lower() for f in os.listdir(dirname)
                     if os.path.isfile(dirname + os.sep + f)])

        num = 0
725
726
        for drv_type, drvs in collection.items():
            for inf, content in drvs.items():
727
                valid = True
728
729
730
731
732
733
734
735
736
737
738
739
                found_match = False
                # Check if the driver is suitable for the input media
                for target in content['TargetOSVersions']:
                    if len(target) > len(self.windows_version):
                        match = target.startswith(self.windows_version)
                    else:
                        match = self.windows_version.startswith(target)
                    if match:
                        found_match = True

                if not found_match:  # Wrong Target
                    self.out.warn(
740
741
742
743
744
745
746
747
748
749
750
                        'Ignoring %s. Driver not targeted for this OS.' % inf)
                    valid = False
                elif 'CatalogFile' not in content:
                    self.out.warn(
                        'Ignoring %s. CatalogFile entry missing.' % inf)
                    valid = False
                elif content['CatalogFile'].lower() not in files:
                    self.out.warn('Ignoring %s. Catalog File not found.' % inf)
                    valid = False

                if not valid:
751
                    del collection[drv_type][inf]
752
                    continue
753

754
                num += 1
755
756
757
            if len(drvs) == 0:
                del collection[drv_type]

758
759
        self.out.output('Found %d valid driver%s' %
                        (num, "s" if num != 1 else ""))
760
761
        return collection

762
763
764
765
    def install_virtio_drivers(self, upgrade=True):
        """Install new VirtIO drivers in the input media. If upgrade is True,
        then the old drivers found in the media will be removed.
        """
766
767
768
769

        dirname = self.sysprep_params['virtio'].value
        if not dirname:
            raise FatalError('No directory hosting the VirtIO drivers defined')
770

771
772
        self.out.output('Installing VirtIO drivers:')

773
774
        valid_drvs = self._fetch_virtio_drivers(dirname)
        if not len(valid_drvs):
775
776
            self.out.warn('No suitable driver found to install!')
            return
777

778
779
780
781
782
        self._upload_virtio_drivers(dirname, valid_drvs, upgrade)
        self._boot_virtio_vm()

        self.out.output("VirtIO drivers were successfully installed")
        self.out.output()
783

784
785
786
787
788
789
    def _upload_virtio_drivers(self, dirname, drvs, delete_old=True):
        """Upload the VirtIO drivers and installation scripts to the media.
        The functions returns the temporary directory under %SystemRoot% that
        hosts the uploaded drivers.
        """
        with self.mount(readonly=False, silent=True):
790
791
            admin = self.sysprep_params['admin'].value
            v_val = self.registry.reset_passwd(admin)
792
            activated = self.registry.reset_account(admin)
793
            self.registry.enable_autologon(admin)
794
            self.registry.reset_first_logon_animation(False)
795
796
797
798
799
800
801
802
803
804
805

            tmp = uuid.uuid4().hex
            self.image.g.mkdir_p("%s/%s" % (self.systemroot, tmp))

            for fname in os.listdir(dirname):
                full_path = os.path.join(dirname, fname)
                if os.path.isfile(full_path):
                    self.image.g.upload(
                        full_path, "%s/%s/%s" % (self.systemroot, tmp, fname))

            self.registry.update_devices_dirs("%SystemRoot%\\" + tmp)
806

807
808
            drvs_install = DRVINST_HEAD

809
            for dtype in drvs:
810
                drvs_install += "".join([ADD_CERTIFICATE % d['CatalogFile']
811
                                         for d in drvs[dtype].values()])
812
                cmd = ADD_DRIVER if dtype != 'viostor' else INSTALL_DRIVER
813
                drvs_install += "".join([cmd % i for i in drvs[dtype]])
814

815
            if delete_old:
816
                # Add code to remove the old drivers
817
                for dtype in drvs:
818
819
                    for oem in self.virtio_state[dtype]:
                        drvs_install += REMOVE_DRIVER % oem
820
821
822
823
824
825
826

            if self.check_version(6, 1) <= 0:
                self._install_viostor_driver(dirname)
            else:
                # In newer windows, in order to reduce the boot process the
                # boot drivers are cached. To be able to boot with viostor, we
                # need to reboot in safe mode.
827
                drvs_install += SAFEBOOT
828

829
            drvs_install += DRVINST_TAIL
830

831
832
            target = "%s/%s/InstallDrivers.ps1" % (self.systemroot, tmp)
            self.image.g.write(target, drvs_install.replace('\n', '\r\n'))
833

834
835
836
837
            # The -windowstyle option was introduced in PowerShell V2. We need
            # to have at least Windows NT 6.1 (Windows 7 or Windows 2008R2) to
            # make this work.
            hidden_support = self.check_version(6, 1) >= 0
838
839
            cmd = (
                '%(drive)s:%(root)s\\System32\\WindowsPowerShell\\v1.0\\'
840
                'powershell.exe -ExecutionPolicy RemoteSigned %(hidden)s '
841
842
                '-File %(drive)s:%(root)s\\%(tmp)s\\InstallDrivers.ps1 '
                '%(drive)s:%(root)s\\%(tmp)s' %
843
                {'root': self.systemroot.replace('/', '\\'),
844
                 'drive': self.systemdrive,
845
                 'tmp': tmp,
846
                 'hidden': '-windowstyle hidden' if hidden_support else ""})
847

848
849
850
            # The value name of RunOnce keys can be prefixed with an asterisk
            # (*) to force the program to run even in Safe mode.
            self.registry.runonce({'*InstallDrivers': cmd})
851

852
853
854
855
856
        return tmp

    def _boot_virtio_vm(self):
        """Boot the media and install the VirtIO drivers"""

857
858
859
        timeout = self.sysprep_params['boot_timeout'].value
        shutdown_timeout = self.sysprep_params['shutdown_timeout'].value
        virtio_timeout = self.sysprep_params['virtio_timeout'].value
860
        self.out.output("Starting Windows VM ...", False)
861
        booted = False
862
        try:
863
864
865
866
867
868
869
            if self.check_version(6, 1) <= 0:
                self.vm.start()
            else:
                self.vm.interface = 'ide'
                self.vm.start(extra_disk=('/dev/null', 'virtio'))
                self.vm.interface = 'virtio'

870
            self.out.success("started (console on VNC display: %d)" %
871
                             self.vm.display)
872
            self.out.output("Waiting for Windows to boot ...", False)
873
874
875
            if not self.vm.wait_on_serial(timeout):
                raise FatalError("Windows VM booting timed out!")
            self.out.success('done')
876
            booted = True
877
            self.out.output("Installing new drivers ...", False)
878
879
880
            if not self.vm.wait_on_serial(virtio_timeout):
                raise FatalError("Windows VirtIO installation timed out!")
            self.out.success('done')
881
            self.out.output('Shutting down ...', False)
882
883
            self.vm.wait(shutdown_timeout)
            self.out.success('done')
884
        finally:
885
            self.vm.stop(shutdown_timeout if booted else 1, fatal=False)
886
887
888
889
890
891
892

        with self.mount(readonly=True, silent=True):
            self.virtio_state = self._virtio_state()
            viostor_service_found = self.registry.check_viostor_service()

        if not (len(self.virtio_state['viostor']) and viostor_service_found):
            raise FatalError("viostor was not successfully installed")
893

894
        if self.check_version(6, 1) > 0:
895
896
            # Hopefully restart in safe mode. Newer windows will not boot from
            # a viostor device unless we initially start them in safe mode
897
            try:
898
                self.out.output('Rebooting Windows VM in safe mode ...', False)
899
                self.vm.start()
900
901
                self.vm.wait(timeout + shutdown_timeout)
                self.out.success('done')
902
            finally:
903
                self.vm.stop(1, fatal=True)
904

905
906
907
908
909
910
911
912
    def _install_viostor_driver(self, dirname):
        """Quick and dirty installation of the VirtIO SCSI controller driver.
        It is done to make the image boot from the VirtIO disk.

        http://rwmj.wordpress.com/2010/04/30/
            tip-install-a-device-driver-in-a-windows-vm/
        """

913
        drivers_path = "%s/system32/drivers" % self.systemroot
914
915

        try:
916
            drivers_path = self.image.g.case_sensitive_path(drivers_path)
917
918
        except RuntimeError as err:
            raise FatalError("Unable to browse to directory: %s. Reason: %s" %
919
920
                             (drivers_path, str(err)))
        viostor = dirname + os.sep + 'viostor.sys'
921
        try:
922
            self.image.g.upload(viostor, drivers_path + '/viostor.sys')
923
924
        except RuntimeError as err:
            raise FatalError("Unable to upload file %s to %s. Reason: %s" %
925
                             (viostor, drivers_path, str(err)))
926
927
928

        self.registry.add_viostor()

Nikos Skalkotos's avatar
Nikos Skalkotos committed
929
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :