Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 2075a76a

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

    
59
    def needed_sysprep_params(self):
60
        """Returns a list of needed sysprep parameters. Each element in the
61
        list is a SysprepParam object.
62
        """
63
        password = self.SysprepParam(
64
            'password', 'Image Administrator Password', 20, lambda x: True)
65

    
66
        return [password]
67

    
68
    @sysprep('Disabling IPv6 privacy extensions')
69
    def disable_ipv6_privacy_extensions(self):
70
        """Disable IPv6 privacy extensions"""
71

    
72
        self._guest_exec('netsh interface ipv6 set global '
73
                         'randomizeidentifiers=disabled store=persistent')
74

    
75
    @sysprep('Disabling Teredo interface')
76
    def disable_teredo(self):
77
        """Disable Teredo interface"""
78

    
79
        self._guest_exec('netsh interface teredo set state disabled')
80

    
81
    @sysprep('Disabling ISATAP Adapters')
82
    def disable_isatap(self):
83
        """Disable ISATAP Adapters"""
84

    
85
        self._guest_exec('netsh interface isa set state disabled')
86

    
87
    @sysprep('Enabling ping responses')
88
    def enable_pings(self):
89
        """Enable ping responces"""
90

    
91
        self._guest_exec('netsh firewall set icmpsetting 8')
92

    
93
    @sysprep('Disabling hibernation support')
94
    def disable_hibernation(self):
95
        """Disable hibernation support and remove the hibernation file"""
96

    
97
        self._guest_exec(r'powercfg.exe /hibernate off')
98

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

    
103
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
104
        self._guest_exec(
105
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
106

    
107
    @sysprep('Clearing the event logs')
108
    def clear_logs(self):
109
        """Clear all the event logs"""
110

    
111
        self._guest_exec(
112
            r"cmd /q /c for /f %l in ('wevtutil el') do wevtutil cl %l")
113

    
114
    @sysprep('Executing sysprep on the image (may take more that 10 minutes)')
115
    def microsoft_sysprep(self):
116
        """Run the Microsoft System Preparation Tool. This will remove
117
        system-specific data and will make the image ready to be deployed.
118
        After this no other task may run.
119
        """
120

    
121
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
122
                         r'/quiet /generalize /oobe /shutdown')
123
        self.syspreped = True
124

    
125
    def do_sysprep(self):
126
        """Prepare system for image creation."""
127

    
128
        if getattr(self, 'syspreped', False):
129
            raise FatalError("Image is already syspreped!")
130

    
131
        txt = "System preparation parameter: `%s' is needed but missing!"
132
        for param in self.needed_sysprep_params():
133
            if param[0] not in self.sysprep_params:
134
                raise FatalError(txt % param[0])
135

    
136
        self.mount(readonly=False)
137
        try:
138
            disabled_uac = self._update_uac_remote_setting(1)
139
            token = self._enable_os_monitor()
140

    
141
            # disable the firewalls
142
            firewall_states = self._update_firewalls(0, 0, 0)
143

    
144
            # Delete the pagefile. It will be recreated when the system boots
145
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
146
            pagefile = "%s/pagefile.sys" % systemroot
147
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
148

    
149
        finally:
150
            self.umount()
151

    
152
        self.out.output("Shutting down helper VM ...", False)
153
        self.g.sync()
154
        # guestfs_shutdown which is the prefered way to shutdown the backend
155
        # process was introduced in version 1.19.16
156
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
157
            ret = self.g.shutdown()
158
        else:
159
            ret = self.g.kill_subprocess()
160

    
161
        self.out.success('done')
162

    
163
        vm = None
164
        monitor = None
165
        try:
166
            self.out.output("Starting windows VM ...", False)
167
            monitorfd, monitor = tempfile.mkstemp()
168
            os.close(monitorfd)
169
            vm, display = self._create_vm(monitor)
170
            self.out.success("started (console on vnc display: %d)." % display)
171

    
172
            self.out.output("Waiting for OS to boot ...", False)
173
            if not self._wait_on_file(monitor, token):
174
                raise FatalError("Windows booting timed out.")
175
            else:
176
                self.out.success('done')
177

    
178
            self.out.output("Disabling automatic logon ...", False)
179
            self._disable_autologon()
180
            self.out.success('done')
181

    
182
            self.out.output('Preparing system from image creation:')
183

    
184
            tasks = self.list_syspreps()
185
            enabled = filter(lambda x: x.enabled, tasks)
186
            size = len(enabled)
187

    
188
            # Make sure the ms sysprep is the last task to run if it is enabled
189
            enabled = filter(
190
                lambda x: x.im_func.func_name != 'microsoft_sysprep', enabled)
191

    
192
            ms_sysprep_enabled = False
193
            if len(enabled) != size:
194
                enabled.append(self.microsoft_sysprep)
195
                ms_sysprep_enabled = True
196

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

    
204
            self.out.output("Sending shut down command ...", False)
205
            if not ms_sysprep_enabled:
206
                self._shutdown()
207
            self.out.success("done")
208

    
209
            self.out.output("Waiting for windows to shut down ...", False)
210
            vm.wait()
211
            self.out.success("done")
212
        finally:
213
            if monitor is not None:
214
                os.unlink(monitor)
215

    
216
            if vm is not None:
217
                self._destroy_vm(vm)
218

    
219
            self.out.output("Relaunching helper VM (may take a while) ...",
220
                            False)
221
            self.g.launch()
222
            self.out.success('done')
223

    
224
            self.mount(readonly=False)
225
            try:
226
                if disabled_uac:
227
                    self._update_uac_remote_setting(0)
228

    
229
                self._update_firewalls(*firewall_states)
230
            finally:
231
                self.umount()
232

    
233
    def _create_vm(self, monitor):
234
        """Create a VM with the image attached as the disk
235

236
            monitor: a file to be used to monitor when the OS is up
237
        """
238

    
239
        def random_mac():
240
            mac = [0x00, 0x16, 0x3e,
241
                   random.randint(0x00, 0x7f),
242
                   random.randint(0x00, 0xff),
243
                   random.randint(0x00, 0xff)]
244

    
245
            return ':'.join(map(lambda x: "%02x" % x, mac))
246

    
247
        # Use ganeti's VNC port range for a random vnc port
248
        vnc_port = random.randint(11000, 14999)
249
        display = vnc_port - 5900
250

    
251
        vm = kvm('-smp', '1', '-m', '1024', '-drive',
252
                 'file=%s,format=raw,cache=none,if=virtio' % self.image.device,
253
                 '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
254
                 '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' %
255
                 random_mac(), '-vnc', ':%d' % display, '-serial',
256
                 'file:%s' % monitor, _bg=True)
257

    
258
        return vm, display
259

    
260
    def _destroy_vm(self, vm):
261
        """Destroy a VM previously created by _create_vm"""
262
        if vm.process.alive:
263
            vm.terminate()
264

    
265
    def _shutdown(self):
266
        """Shuts down the windows VM"""
267
        self._guest_exec(r'shutdown /s /t 5')
268

    
269
    def _wait_on_file(self, fname, msg):
270
        """Wait until a message appears on a file"""
271

    
272
        for i in range(BOOT_TIMEOUT):
273
            time.sleep(1)
274
            with open(fname) as f:
275
                for line in f:
276
                    if line.startswith(msg):
277
                        return True
278
        return False
279

    
280
    def _disable_autologon(self):
281
        """Disable automatic logon on the windows image"""
282

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

    
286
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
287
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
288
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
289

    
290
    def _registry_file_path(self, regfile):
291
        """Retrieves the case sensitive path to a registry file"""
292

    
293
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
294
        path = "%s/system32/config/%s" % (systemroot, regfile)
295
        try:
296
            path = self.g.case_sensitive_path(path)
297
        except RuntimeError as e:
298
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
299
                             % (regfile, str(e)))
300
        return path
301

    
302
    def _enable_os_monitor(self):
303
        """Add a script in the registry that will send a random string to the
304
        first serial port when the windows image finishes booting.
305
        """
306

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

    
309
        path = self._registry_file_path('SOFTWARE')
310
        softwarefd, software = tempfile.mkstemp()
311
        try:
312
            os.close(softwarefd)
313
            self.g.download(path, software)
314

    
315
            h = hivex.Hivex(software, write=True)
316

    
317
            # Enable automatic logon.
318
            # This is needed because we need to execute a script that we add in
319
            # the RunOnce registry entry and those programs only get executed
320
            # when a user logs on. There is a RunServicesOnce registry entry
321
            # whose keys get executed in the background when the logon dialog
322
            # box first appears, but they seem to only work with services and
323
            # not arbitrary command line expressions :-(
324
            #
325
            # Instructions on how to turn on automatic logon in Windows can be
326
            # found here: http://support.microsoft.com/kb/324737
327
            #
328
            # Warning: Registry change will not work if the “Logon Banner” is
329
            # defined on the server either by a Group Policy object (GPO) or by
330
            # a local policy.
331

    
332
            winlogon = h.root()
333
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
334
                          'Winlogon'):
335
                winlogon = h.node_get_child(winlogon, child)
336

    
337
            h.node_set_value(
338
                winlogon,
339
                {'key': 'DefaultUserName', 't': 1,
340
                 'value': "Administrator".encode('utf-16le')})
341
            h.node_set_value(
342
                winlogon,
343
                {'key': 'DefaultPassword', 't': 1,
344
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
345
            h.node_set_value(
346
                winlogon,
347
                {'key': 'AutoAdminLogon', 't': 1,
348
                 'value': "1".encode('utf-16le')})
349

    
350
            key = h.root()
351
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
352
                key = h.node_get_child(key, child)
353

    
354
            runonce = h.node_get_child(key, "RunOnce")
355
            if runonce is None:
356
                runonce = h.node_add_child(key, "RunOnce")
357

    
358
            value = (
359
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
360
                r'-ExecutionPolicy RemoteSigned '
361
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
362
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
363
                r'$port.Close()}"').encode('utf-16le')
364

    
365
            h.node_set_value(runonce,
366
                             {'key': "BootMonitor", 't': 1, 'value': value})
367

    
368
            value = (
369
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
370
                r'\policies\system /v LocalAccountTokenFilterPolicy'
371
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
372

    
373
            h.node_set_value(runonce,
374
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
375

    
376
            h.commit(None)
377

    
378
            self.g.upload(software, path)
379
        finally:
380
            os.unlink(software)
381

    
382
        return token
383

    
384
    def _update_firewalls(self, domain, public, standard):
385
        """Enables or disables the firewall for the Domain, the Public and the
386
        Standard profile. Returns a triplete with the old values.
387

388
        1 will enable a firewall and 0 will disable it
389
        """
390

    
391
        if domain not in (0, 1):
392
            raise ValueError("Valid values for domain parameter are 0 and 1")
393

    
394
        if public not in (0, 1):
395
            raise ValueError("Valid values for public parameter are 0 and 1")
396

    
397
        if standard not in (0, 1):
398
            raise ValueError("Valid values for standard parameter are 0 and 1")
399

    
400
        path = self._registry_file_path("SYSTEM")
401
        systemfd, system = tempfile.mkstemp()
402
        try:
403
            os.close(systemfd)
404
            self.g.download(path, system)
405

    
406
            h = hivex.Hivex(system, write=True)
407

    
408
            select = h.node_get_child(h.root(), 'Select')
409
            current_value = h.node_get_value(select, 'Current')
410

    
411
            # expecting a little endian dword
412
            assert h.value_type(current_value)[1] == 4
413
            current = "%03d" % h.value_dword(current_value)
414

    
415
            firewall_policy = h.root()
416
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
417
                          'Parameters', 'FirewallPolicy'):
418
                firewall_policy = h.node_get_child(firewall_policy, child)
419

    
420
            old_values = []
421
            new_values = [domain, public, standard]
422
            for profile in ('Domain', 'Public', 'Standard'):
423
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
424

    
425
                old_value = h.node_get_value(node, 'EnableFirewall')
426

    
427
                # expecting a little endian dword
428
                assert h.value_type(old_value)[1] == 4
429
                old_values.append(h.value_dword(old_value))
430

    
431
                h.node_set_value(
432
                    node, {'key': 'EnableFirewall', 't': 4L,
433
                           'value': struct.pack("<I", new_values.pop(0))})
434

    
435
            h.commit(None)
436
            self.g.upload(system, path)
437

    
438
        finally:
439
            os.unlink(system)
440

    
441
        return old_values
442

    
443
    def _update_uac_remote_setting(self, value):
444
        """Updates the registry key value:
445
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
446
        \System]"LocalAccountTokenFilterPolicy"
447

448
        value = 1 will disable the UAC remote restrictions
449
        value = 0 will enable the UAC remote restrictions
450

451
        For more info see here: http://support.microsoft.com/kb/951016
452

453
        Returns:
454
            True if the key is changed
455
            False if the key is unchanged
456
        """
457

    
458
        if value not in (0, 1):
459
            raise ValueError("Valid values for value parameter are 0 and 1")
460

    
461
        path = self._registry_file_path('SOFTWARE')
462
        softwarefd, software = tempfile.mkstemp()
463
        try:
464
            os.close(softwarefd)
465
            self.g.download(path, software)
466

    
467
            h = hivex.Hivex(software, write=True)
468

    
469
            key = h.root()
470
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
471
                          'System'):
472
                key = h.node_get_child(key, child)
473

    
474
            policy = None
475
            for val in h.node_values(key):
476
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
477
                    policy = val
478

    
479
            if policy is not None:
480
                dword = h.value_dword(policy)
481
                if dword == value:
482
                    return False
483
            elif value == 0:
484
                return False
485

    
486
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
487
                         'value': struct.pack("<I", value)}
488

    
489
            h.node_set_value(key, new_value)
490
            h.commit(None)
491

    
492
            self.g.upload(software, path)
493

    
494
        finally:
495
            os.unlink(software)
496

    
497
        return True
498

    
499
    def _do_collect_metadata(self):
500
        """Collect metadata about the OS"""
501
        super(Windows, self)._do_collect_metadata()
502
        self.meta["USERS"] = " ".join(self._get_users())
503

    
504
    def _get_users(self):
505
        """Returns a list of users found in the images"""
506
        path = self._registry_file_path('SAM')
507
        samfd, sam = tempfile.mkstemp()
508
        try:
509
            os.close(samfd)
510
            self.g.download(path, sam)
511

    
512
            h = hivex.Hivex(sam)
513

    
514
            key = h.root()
515
            # Navigate to /SAM/Domains/Account/Users/Names
516
            for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
517
                key = h.node_get_child(key, child)
518

    
519
            users = [h.node_name(x) for x in h.node_children(key)]
520

    
521
        finally:
522
            os.unlink(sam)
523

    
524
        # Filter out the guest account
525
        return filter(lambda x: x != "Guest", users)
526

    
527
    def _guest_exec(self, command, fatal=True):
528
        """Execute a command on a windows VM"""
529

    
530
        user = "Administrator%" + self.sysprep_params['password']
531
        addr = 'localhost'
532
        runas = '--runas=%s' % user
533
        winexe = subprocess.Popen(
534
            ['winexe', '-U', user, "//%s" % addr, runas, command],
535
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
536

    
537
        stdout, stderr = winexe.communicate()
538
        rc = winexe.poll()
539

    
540
        if rc != 0 and fatal:
541
            reason = stderr if len(stderr) else stdout
542
            raise FatalError("Command: `%s' failed. Reason: %s" %
543
                             (command, reason))
544

    
545
        return (stdout, stderr, rc)
546

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