Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ efa7a61b

History | View | Annotate | Download (20.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

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

    
51
kvm = get_command('kvm')
52

    
53
BOOT_TIMEOUT = 300
54

    
55

    
56
class Windows(OSBase):
57
    """OS class for Windows"""
58
    def __init__(self, image, **kargs):
59
        super(Windows, self).__init__(image, **kargs)
60

    
61
        device = self.g.part_to_dev(self.root)
62

    
63
        self.last_part_num = self.g.part_list(device)[-1]['part_num']
64
        self.last_drive = None
65
        self.system_drive = None
66

    
67
        for drive, partition in self.g.inspect_get_drive_mappings(self.root):
68
            if partition == "%s%d" % (device, self.last_part_num):
69
                self.last_drive = drive
70
            if partition == self.root:
71
                self.system_drive = drive
72

    
73
        assert self.system_drive
74

    
75
    def needed_sysprep_params(self):
76
        """Returns a list of needed sysprep parameters. Each element in the
77
        list is a SysprepParam object.
78
        """
79
        password = self.SysprepParam(
80
            'password', 'Image Administrator Password', 20, lambda x: True)
81

    
82
        return [password]
83

    
84
    @sysprep('Disabling IPv6 privacy extensions')
85
    def disable_ipv6_privacy_extensions(self):
86
        """Disable IPv6 privacy extensions"""
87

    
88
        self._guest_exec('netsh interface ipv6 set global '
89
                         'randomizeidentifiers=disabled store=persistent')
90

    
91
    @sysprep('Disabling Teredo interface')
92
    def disable_teredo(self):
93
        """Disable Teredo interface"""
94

    
95
        self._guest_exec('netsh interface teredo set state disabled')
96

    
97
    @sysprep('Disabling ISATAP Adapters')
98
    def disable_isatap(self):
99
        """Disable ISATAP Adapters"""
100

    
101
        self._guest_exec('netsh interface isa set state disabled')
102

    
103
    @sysprep('Enabling ping responses')
104
    def enable_pings(self):
105
        """Enable ping responces"""
106

    
107
        self._guest_exec('netsh firewall set icmpsetting 8')
108

    
109
    @sysprep('Disabling hibernation support')
110
    def disable_hibernation(self):
111
        """Disable hibernation support and remove the hibernation file"""
112

    
113
        self._guest_exec(r'powercfg.exe /hibernate off')
114

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

    
119
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
120
        self._guest_exec(
121
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
122

    
123
    @sysprep('Clearing the event logs')
124
    def clear_logs(self):
125
        """Clear all the event logs"""
126

    
127
        self._guest_exec(
128
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
129

    
130
    @sysprep('Executing sysprep on the image (may take more that 10 minutes)')
131
    def microsoft_sysprep(self):
132
        """Run the Microsoft System Preparation Tool. This will remove
133
        system-specific data and will make the image ready to be deployed.
134
        After this no other task may run.
135
        """
136

    
137
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
138
                         r'/quiet /generalize /oobe /shutdown')
139
        self.syspreped = True
140

    
141
    @sysprep('Shrinking the last filesystem')
142
    def shrink(self):
143
        """Shrink the last filesystem. Make sure the filesystem is defragged"""
144
        pass
145

    
146
    def do_sysprep(self):
147
        """Prepare system for image creation."""
148

    
149
        if getattr(self, 'syspreped', False):
150
            raise FatalError("Image is already syspreped!")
151

    
152
        txt = "System preparation parameter: `%s' is needed but missing!"
153
        for param in self.needed_sysprep_params():
154
            if param[0] not in self.sysprep_params:
155
                raise FatalError(txt % param[0])
156

    
157
        self.mount(readonly=False)
158
        try:
159
            disabled_uac = self._update_uac_remote_setting(1)
160
            token = self._enable_os_monitor()
161

    
162
            # disable the firewalls
163
            firewall_states = self._update_firewalls(0, 0, 0)
164

    
165
            # Delete the pagefile. It will be recreated when the system boots
166
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
167
            pagefile = "%s/pagefile.sys" % systemroot
168
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
169

    
170
        finally:
171
            self.umount()
172

    
173
        self.out.output("Shutting down helper VM ...", False)
174
        self.g.sync()
175
        # guestfs_shutdown which is the prefered way to shutdown the backend
176
        # process was introduced in version 1.19.16
177
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
178
            ret = self.g.shutdown()
179
        else:
180
            ret = self.g.kill_subprocess()
181

    
182
        self.out.success('done')
183

    
184
        vm = None
185
        monitor = None
186
        try:
187
            self.out.output("Starting windows VM ...", False)
188
            monitorfd, monitor = tempfile.mkstemp()
189
            os.close(monitorfd)
190
            vm, display = self._create_vm(monitor)
191
            self.out.success("started (console on vnc display: %d)." % display)
192

    
193
            self.out.output("Waiting for OS to boot ...", False)
194
            if not self._wait_on_file(monitor, token):
195
                raise FatalError("Windows booting timed out.")
196
            else:
197
                self.out.success('done')
198

    
199
            time.sleep(5)  # Just to be sure everything is up
200

    
201
            self.out.output("Disabling automatic logon ...", False)
202
            self._disable_autologon()
203
            self.out.success('done')
204

    
205
            self.out.output('Preparing system from image creation:')
206

    
207
            tasks = self.list_syspreps()
208
            enabled = filter(lambda x: x.enabled, tasks)
209
            size = len(enabled)
210

    
211
            # Make sure shrink runs in the end, before ms sysprep
212
            enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
213
                             enabled)
214

    
215
            shrink_enabled = False
216
            if len(enabled) != size:
217
                enabled.append(self.shrink)
218
                shrink_enabled = True
219

    
220
            # Make sure the ms sysprep is the last task to run if it is enabled
221
            enabled = filter(
222
                lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
223
                enabled)
224

    
225
            ms_sysprep_enabled = False
226
            if len(enabled) != size:
227
                enabled.append(self.microsoft_sysprep)
228
                ms_sysprep_enabled = True
229

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

    
237
            self.out.output("Sending shut down command ...", False)
238
            if not ms_sysprep_enabled:
239
                self._shutdown()
240
            self.out.success("done")
241

    
242
            self.out.output("Waiting for windows to shut down ...", False)
243
            vm.wait()
244
            self.out.success("done")
245
        finally:
246
            if monitor is not None:
247
                os.unlink(monitor)
248

    
249
            if vm is not None:
250
                self._destroy_vm(vm)
251

    
252
            self.out.output("Relaunching helper VM (may take a while) ...",
253
                            False)
254
            self.g.launch()
255
            self.out.success('done')
256

    
257
            self.mount(readonly=False)
258
            try:
259
                if disabled_uac:
260
                    self._update_uac_remote_setting(0)
261

    
262
                self._update_firewalls(*firewall_states)
263
            finally:
264
                self.umount()
265

    
266
    def _create_vm(self, monitor):
267
        """Create a VM with the image attached as the disk
268

269
            monitor: a file to be used to monitor when the OS is up
270
        """
271

    
272
        def random_mac():
273
            mac = [0x00, 0x16, 0x3e,
274
                   random.randint(0x00, 0x7f),
275
                   random.randint(0x00, 0xff),
276
                   random.randint(0x00, 0xff)]
277

    
278
            return ':'.join(map(lambda x: "%02x" % x, mac))
279

    
280
        # Use ganeti's VNC port range for a random vnc port
281
        vnc_port = random.randint(11000, 14999)
282
        display = vnc_port - 5900
283

    
284
        vm = kvm(
285
            '-smp', '1', '-m', '1024', '-drive',
286
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.image.device,
287
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
288
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
289
            '-vnc', ':%d' % display, '-serial', 'file:%s' % monitor, _bg=True)
290

    
291
        return vm, display
292

    
293
    def _destroy_vm(self, vm):
294
        """Destroy a VM previously created by _create_vm"""
295
        if vm.process.alive:
296
            vm.terminate()
297

    
298
    def _shutdown(self):
299
        """Shuts down the windows VM"""
300
        self._guest_exec(r'shutdown /s /t 5')
301

    
302
    def _wait_on_file(self, fname, msg):
303
        """Wait until a message appears on a file"""
304

    
305
        for i in range(BOOT_TIMEOUT):
306
            time.sleep(1)
307
            with open(fname) as f:
308
                for line in f:
309
                    if line.startswith(msg):
310
                        return True
311
        return False
312

    
313
    def _disable_autologon(self):
314
        """Disable automatic logon on the windows image"""
315

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

    
319
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
320
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
321
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
322

    
323
    def _registry_file_path(self, regfile):
324
        """Retrieves the case sensitive path to a registry file"""
325

    
326
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
327
        path = "%s/system32/config/%s" % (systemroot, regfile)
328
        try:
329
            path = self.g.case_sensitive_path(path)
330
        except RuntimeError as e:
331
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
332
                             % (regfile, str(e)))
333
        return path
334

    
335
    def _enable_os_monitor(self):
336
        """Add a script in the registry that will send a random string to the
337
        first serial port when the windows image finishes booting.
338
        """
339

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

    
342
        path = self._registry_file_path('SOFTWARE')
343
        softwarefd, software = tempfile.mkstemp()
344
        try:
345
            os.close(softwarefd)
346
            self.g.download(path, software)
347

    
348
            h = hivex.Hivex(software, write=True)
349

    
350
            # Enable automatic logon.
351
            # This is needed because we need to execute a script that we add in
352
            # the RunOnce registry entry and those programs only get executed
353
            # when a user logs on. There is a RunServicesOnce registry entry
354
            # whose keys get executed in the background when the logon dialog
355
            # box first appears, but they seem to only work with services and
356
            # not arbitrary command line expressions :-(
357
            #
358
            # Instructions on how to turn on automatic logon in Windows can be
359
            # found here: http://support.microsoft.com/kb/324737
360
            #
361
            # Warning: Registry change will not work if the “Logon Banner” is
362
            # defined on the server either by a Group Policy object (GPO) or by
363
            # a local policy.
364

    
365
            winlogon = h.root()
366
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
367
                          'Winlogon'):
368
                winlogon = h.node_get_child(winlogon, child)
369

    
370
            h.node_set_value(
371
                winlogon,
372
                {'key': 'DefaultUserName', 't': 1,
373
                 'value': "Administrator".encode('utf-16le')})
374
            h.node_set_value(
375
                winlogon,
376
                {'key': 'DefaultPassword', 't': 1,
377
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
378
            h.node_set_value(
379
                winlogon,
380
                {'key': 'AutoAdminLogon', 't': 1,
381
                 'value': "1".encode('utf-16le')})
382

    
383
            key = h.root()
384
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
385
                key = h.node_get_child(key, child)
386

    
387
            runonce = h.node_get_child(key, "RunOnce")
388
            if runonce is None:
389
                runonce = h.node_add_child(key, "RunOnce")
390

    
391
            value = (
392
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
393
                r'-ExecutionPolicy RemoteSigned '
394
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
395
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
396
                r'$port.Close()}"').encode('utf-16le')
397

    
398
            h.node_set_value(runonce,
399
                             {'key': "BootMonitor", 't': 1, 'value': value})
400

    
401
            value = (
402
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
403
                r'\policies\system /v LocalAccountTokenFilterPolicy'
404
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
405

    
406
            h.node_set_value(runonce,
407
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
408

    
409
            h.commit(None)
410

    
411
            self.g.upload(software, path)
412
        finally:
413
            os.unlink(software)
414

    
415
        return token
416

    
417
    def _update_firewalls(self, domain, public, standard):
418
        """Enables or disables the firewall for the Domain, the Public and the
419
        Standard profile. Returns a triplete with the old values.
420

421
        1 will enable a firewall and 0 will disable it
422
        """
423

    
424
        if domain not in (0, 1):
425
            raise ValueError("Valid values for domain parameter are 0 and 1")
426

    
427
        if public not in (0, 1):
428
            raise ValueError("Valid values for public parameter are 0 and 1")
429

    
430
        if standard not in (0, 1):
431
            raise ValueError("Valid values for standard parameter are 0 and 1")
432

    
433
        path = self._registry_file_path("SYSTEM")
434
        systemfd, system = tempfile.mkstemp()
435
        try:
436
            os.close(systemfd)
437
            self.g.download(path, system)
438

    
439
            h = hivex.Hivex(system, write=True)
440

    
441
            select = h.node_get_child(h.root(), 'Select')
442
            current_value = h.node_get_value(select, 'Current')
443

    
444
            # expecting a little endian dword
445
            assert h.value_type(current_value)[1] == 4
446
            current = "%03d" % h.value_dword(current_value)
447

    
448
            firewall_policy = h.root()
449
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
450
                          'Parameters', 'FirewallPolicy'):
451
                firewall_policy = h.node_get_child(firewall_policy, child)
452

    
453
            old_values = []
454
            new_values = [domain, public, standard]
455
            for profile in ('Domain', 'Public', 'Standard'):
456
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
457

    
458
                old_value = h.node_get_value(node, 'EnableFirewall')
459

    
460
                # expecting a little endian dword
461
                assert h.value_type(old_value)[1] == 4
462
                old_values.append(h.value_dword(old_value))
463

    
464
                h.node_set_value(
465
                    node, {'key': 'EnableFirewall', 't': 4L,
466
                           'value': struct.pack("<I", new_values.pop(0))})
467

    
468
            h.commit(None)
469
            self.g.upload(system, path)
470

    
471
        finally:
472
            os.unlink(system)
473

    
474
        return old_values
475

    
476
    def _update_uac_remote_setting(self, value):
477
        """Updates the registry key value:
478
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
479
        \System]"LocalAccountTokenFilterPolicy"
480

481
        value = 1 will disable the UAC remote restrictions
482
        value = 0 will enable the UAC remote restrictions
483

484
        For more info see here: http://support.microsoft.com/kb/951016
485

486
        Returns:
487
            True if the key is changed
488
            False if the key is unchanged
489
        """
490

    
491
        if value not in (0, 1):
492
            raise ValueError("Valid values for value parameter are 0 and 1")
493

    
494
        path = self._registry_file_path('SOFTWARE')
495
        softwarefd, software = tempfile.mkstemp()
496
        try:
497
            os.close(softwarefd)
498
            self.g.download(path, software)
499

    
500
            h = hivex.Hivex(software, write=True)
501

    
502
            key = h.root()
503
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
504
                          'System'):
505
                key = h.node_get_child(key, child)
506

    
507
            policy = None
508
            for val in h.node_values(key):
509
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
510
                    policy = val
511

    
512
            if policy is not None:
513
                dword = h.value_dword(policy)
514
                if dword == value:
515
                    return False
516
            elif value == 0:
517
                return False
518

    
519
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
520
                         'value': struct.pack("<I", value)}
521

    
522
            h.node_set_value(key, new_value)
523
            h.commit(None)
524

    
525
            self.g.upload(software, path)
526

    
527
        finally:
528
            os.unlink(software)
529

    
530
        return True
531

    
532
    def _do_collect_metadata(self):
533
        """Collect metadata about the OS"""
534
        super(Windows, self)._do_collect_metadata()
535
        self.meta["USERS"] = " ".join(self._get_users())
536

    
537
    def _get_users(self):
538
        """Returns a list of users found in the images"""
539
        path = self._registry_file_path('SAM')
540
        samfd, sam = tempfile.mkstemp()
541
        try:
542
            os.close(samfd)
543
            self.g.download(path, sam)
544

    
545
            h = hivex.Hivex(sam)
546

    
547
            key = h.root()
548
            # Navigate to /SAM/Domains/Account/Users/Names
549
            for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
550
                key = h.node_get_child(key, child)
551

    
552
            users = [h.node_name(x) for x in h.node_children(key)]
553

    
554
        finally:
555
            os.unlink(sam)
556

    
557
        # Filter out the guest account
558
        return filter(lambda x: x != "Guest", users)
559

    
560
    def _guest_exec(self, command, fatal=True):
561
        """Execute a command on a windows VM"""
562

    
563
        user = "Administrator%" + self.sysprep_params['password']
564
        addr = 'localhost'
565
        runas = '--runas=%s' % user
566
        winexe = subprocess.Popen(
567
            ['winexe', '-U', user, runas, "//%s" % addr, command],
568
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
569

    
570
        stdout, stderr = winexe.communicate()
571
        rc = winexe.poll()
572

    
573
        if rc != 0 and fatal:
574
            reason = stderr if len(stderr) else stdout
575
            raise FatalError("Command: `%s' failed. Reason: %s" %
576
                             (command, reason))
577

    
578
        return (stdout, stderr, rc)
579

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