Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ 010f6b30

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

    
50
kvm = get_command('kvm')
51

    
52
BOOT_TIMEOUT = 300
53

    
54

    
55
class Windows(OSBase):
56
    """OS class for Windows"""
57

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

    
65
        return [password]
66

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

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

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

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

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

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

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

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

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

    
96
        path = r'HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
97
        self._guest_exec(
98
            r'REG ADD %s /v RealTimeIsUniversal /t REG_DWORD /d 1 /f' % path)
99

    
100
    @sysprep('Executing sysprep on the image (may take more that 10 minutes)')
101
    def microsoft_sysprep(self):
102
        """Run the Microsoft System Preparation Tool. This will remove
103
        system-specific data and will make the image ready to be deployed.
104
        After this no other task may run.
105
        """
106

    
107
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
108
                         r'/quiet /generalize /oobe /shutdown')
109
        self.syspreped = True
110

    
111
    def do_sysprep(self):
112
        """Prepare system for image creation."""
113

    
114
        if getattr(self, 'syspreped', False):
115
            raise FatalError("Image is already syspreped!")
116

    
117
        txt = "System preparation parameter: `%s' is needed but missing!"
118
        for param in self.needed_sysprep_params():
119
            if param[0] not in self.sysprep_params:
120
                raise FatalError(txt % param[0])
121

    
122
        self.mount(readonly=False)
123
        try:
124
            disabled_uac = self._update_uac_remote_setting(1)
125
            token = self._enable_os_monitor()
126

    
127
            # disable the firewalls
128
            firewall_states = self._update_firewalls(0, 0, 0)
129

    
130
            # Delete the pagefile. It will be recreated when the system boots
131
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
132
            pagefile = "%s/pagefile.sys" % systemroot
133
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
134

    
135
        finally:
136
            self.umount()
137

    
138
        self.out.output("Shutting down helper VM ...", False)
139
        self.g.sync()
140
        # guestfs_shutdown which is the prefered way to shutdown the backend
141
        # process was introduced in version 1.19.16
142
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
143
            ret = self.g.shutdown()
144
        else:
145
            ret = self.g.kill_subprocess()
146

    
147
        self.out.success('done')
148

    
149
        vm = None
150
        monitor = None
151
        try:
152
            self.out.output("Starting windows VM ...", False)
153
            monitorfd, monitor = tempfile.mkstemp()
154
            os.close(monitorfd)
155
            vm, display = self._create_vm(monitor)
156
            self.out.success("started (console on vnc display: %d)." % display)
157

    
158
            self.out.output("Waiting for OS to boot ...", False)
159
            if not self._wait_on_file(monitor, token):
160
                raise FatalError("Windows booting timed out.")
161
            else:
162
                self.out.success('done')
163

    
164
            self.out.output("Disabling automatic logon ...", False)
165
            self._disable_autologon()
166
            self.out.success('done')
167

    
168
            self.out.output('Preparing system from image creation:')
169

    
170
            tasks = self.list_syspreps()
171
            enabled = filter(lambda x: x.enabled, tasks)
172

    
173
            size = len(enabled)
174

    
175
            # Make sure the ms sysprep is the last task to run if it is enabled
176
            enabled = filter(
177
                lambda x: x.im_func.func_name != 'microsoft_sysprep', enabled)
178

    
179
            ms_sysprep_enabled = False
180
            if len(enabled) != size:
181
                enabled.append(self.microsoft_sysprep)
182
                ms_sysprep_enabled = True
183

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

    
191
            self.out.output("Sending shut down command ...", False)
192
            if not ms_sysprep_enabled:
193
                self._shutdown()
194
            self.out.success("done")
195

    
196
            self.out.output("Waiting for windows to shut down ...", False)
197
            vm.wait()
198
            self.out.success("done")
199
        finally:
200
            if monitor is not None:
201
                os.unlink(monitor)
202

    
203
            if vm is not None:
204
                self._destroy_vm(vm)
205

    
206
            self.out.output("Relaunching helper VM (may take a while) ...",
207
                            False)
208
            self.g.launch()
209
            self.out.success('done')
210

    
211
            self.mount(readonly=False)
212
            try:
213
                if disabled_uac:
214
                    self._update_uac_remote_setting(0)
215

    
216
                self._update_firewalls(*firewall_states)
217
            finally:
218
                self.umount()
219

    
220
    def _create_vm(self, monitor):
221
        """Create a VM with the image attached as the disk
222

223
            monitor: a file to be used to monitor when the OS is up
224
        """
225

    
226
        def random_mac():
227
            mac = [0x00, 0x16, 0x3e,
228
                   random.randint(0x00, 0x7f),
229
                   random.randint(0x00, 0xff),
230
                   random.randint(0x00, 0xff)]
231

    
232
            return ':'.join(map(lambda x: "%02x" % x, mac))
233

    
234
        # Use ganeti's VNC port range for a random vnc port
235
        vnc_port = random.randint(11000, 14999)
236
        display = vnc_port - 5900
237

    
238
        vm = kvm('-smp', '1', '-m', '1024', '-drive',
239
                 'file=%s,format=raw,cache=none,if=virtio' % self.image.device,
240
                 '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
241
                 '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' %
242
                 random_mac(), '-vnc', ':%d' % display, '-serial',
243
                 'file:%s' % monitor, _bg=True)
244

    
245
        return vm, display
246

    
247
    def _destroy_vm(self, vm):
248
        """Destroy a VM previously created by _create_vm"""
249
        if vm.process.alive:
250
            vm.terminate()
251

    
252
    def _shutdown(self):
253
        """Shuts down the windows VM"""
254
        self._guest_exec(r'shutdown /s /t 5')
255

    
256
    def _wait_on_file(self, fname, msg):
257
        """Wait until a message appears on a file"""
258

    
259
        for i in range(BOOT_TIMEOUT):
260
            time.sleep(1)
261
            with open(fname) as f:
262
                for line in f:
263
                    if line.startswith(msg):
264
                        return True
265
        return False
266

    
267
    def _disable_autologon(self):
268
        """Disable automatic logon on the windows image"""
269

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

    
273
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
274
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
275
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
276

    
277
    def _registry_file_path(self, regfile):
278
        """Retrieves the case sensitive path to a registry file"""
279

    
280
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
281
        path = "%s/system32/config/%s" % (systemroot, regfile)
282
        try:
283
            path = self.g.case_sensitive_path(path)
284
        except RuntimeError as e:
285
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
286
                             % (regfile, str(e)))
287
        return path
288

    
289
    def _enable_os_monitor(self):
290
        """Add a script in the registry that will send a random string to the
291
        first serial port when the windows image finishes booting.
292
        """
293

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

    
296
        path = self._registry_file_path('SOFTWARE')
297
        softwarefd, software = tempfile.mkstemp()
298
        try:
299
            os.close(softwarefd)
300
            self.g.download(path, software)
301

    
302
            h = hivex.Hivex(software, write=True)
303

    
304
            # Enable automatic logon.
305
            # This is needed because we need to execute a script that we add in
306
            # the RunOnce registry entry and those programs only get executed
307
            # when a user logs on. There is a RunServicesOnce registry entry
308
            # whose keys get executed in the background when the logon dialog
309
            # box first appears, but they seem to only work with services and
310
            # not arbitrary command line expressions :-(
311
            #
312
            # Instructions on how to turn on automatic logon in Windows can be
313
            # found here: http://support.microsoft.com/kb/324737
314
            #
315
            # Warning: Registry change will not work if the “Logon Banner” is
316
            # defined on the server either by a Group Policy object (GPO) or by
317
            # a local policy.
318

    
319
            winlogon = h.root()
320
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
321
                          'Winlogon'):
322
                winlogon = h.node_get_child(winlogon, child)
323

    
324
            h.node_set_value(
325
                winlogon,
326
                {'key': 'DefaultUserName', 't': 1,
327
                 'value': "Administrator".encode('utf-16le')})
328
            h.node_set_value(
329
                winlogon,
330
                {'key': 'DefaultPassword', 't': 1,
331
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
332
            h.node_set_value(
333
                winlogon,
334
                {'key': 'AutoAdminLogon', 't': 1,
335
                 'value': "1".encode('utf-16le')})
336

    
337
            key = h.root()
338
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
339
                key = h.node_get_child(key, child)
340

    
341
            runonce = h.node_get_child(key, "RunOnce")
342
            if runonce is None:
343
                runonce = h.node_add_child(key, "RunOnce")
344

    
345
            value = (
346
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
347
                r'-ExecutionPolicy RemoteSigned '
348
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
349
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
350
                r'$port.Close()}"').encode('utf-16le')
351

    
352
            h.node_set_value(runonce,
353
                             {'key': "BootMonitor", 't': 1, 'value': value})
354

    
355
            h.commit(None)
356

    
357
            self.g.upload(software, path)
358
        finally:
359
            os.unlink(software)
360

    
361
        return token
362

    
363
    def _update_firewalls(self, domain, public, standard):
364
        """Enables or disables the firewall for the Domain, the Public and the
365
        Standard profile. Returns a triplete with the old values.
366

367
        1 will enable a firewall and 0 will disable it
368
        """
369

    
370
        if domain not in (0, 1):
371
            raise ValueError("Valid values for domain parameter are 0 and 1")
372

    
373
        if public not in (0, 1):
374
            raise ValueError("Valid values for public parameter are 0 and 1")
375

    
376
        if standard not in (0, 1):
377
            raise ValueError("Valid values for standard parameter are 0 and 1")
378

    
379
        path = self._registry_file_path("SYSTEM")
380
        systemfd, system = tempfile.mkstemp()
381
        try:
382
            os.close(systemfd)
383
            self.g.download(path, system)
384

    
385
            h = hivex.Hivex(system, write=True)
386

    
387
            select = h.node_get_child(h.root(), 'Select')
388
            current_value = h.node_get_value(select, 'Current')
389

    
390
            # expecting a little endian dword
391
            assert h.value_type(current_value)[1] == 4
392
            current = "%03d" % h.value_dword(current_value)
393

    
394
            firewall_policy = h.root()
395
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
396
                          'Parameters', 'FirewallPolicy'):
397
                firewall_policy = h.node_get_child(firewall_policy, child)
398

    
399
            old_values = []
400
            new_values = [domain, public, standard]
401
            for profile in ('Domain', 'Public', 'Standard'):
402
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
403

    
404
                old_value = h.node_get_value(node, 'EnableFirewall')
405

    
406
                # expecting a little endian dword
407
                assert h.value_type(old_value)[1] == 4
408
                old_values.append(h.value_dword(old_value))
409

    
410
                new_value = '\x00' if new_values.pop(0) == 0 else '\x01'
411
                h.node_set_value(node, {'key': 'EnableFirewall', 't': 4L,
412
                                        'value': '%s\x00\x00\x00' % new_value})
413

    
414
            h.commit(None)
415
            self.g.upload(system, path)
416

    
417
        finally:
418
            os.unlink(system)
419

    
420
        return old_values
421

    
422
    def _update_uac_remote_setting(self, value):
423
        """Updates the registry key value:
424
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
425
        \System]"LocalAccountTokenFilterPolicy"
426

427
        value = 1 will disable the UAC remote restrictions
428
        value = 0 will enable the UAC remote restrictions
429

430
        For more info see here: http://support.microsoft.com/kb/951016
431

432
        Returns:
433
            True if the key is changed
434
            False if the key is unchanged
435
        """
436

    
437
        if value not in (0, 1):
438
            raise ValueError("Valid values for value parameter are 0 and 1")
439

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

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

    
448
            key = h.root()
449
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
450
                          'System'):
451
                key = h.node_get_child(key, child)
452

    
453
            policy = None
454
            for val in h.node_values(key):
455
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
456
                    policy = val
457

    
458
            if policy is not None:
459
                dword = h.value_dword(policy)
460
                if dword == value:
461
                    return False
462
            elif value == 0:
463
                return False
464

    
465
            new_value = {
466
                'key': "LocalAccountTokenFilterPolicy", 't': 4L,
467
                'value': '%s\x00\x00\x00' % '\x00' if value == 0 else '\x01'}
468

    
469
            h.node_set_value(key, new_value)
470
            h.commit(None)
471

    
472
            self.g.upload(software, path)
473

    
474
        finally:
475
            os.unlink(software)
476

    
477
        return True
478

    
479
    def _do_collect_metadata(self):
480
        """Collect metadata about the OS"""
481
        super(Windows, self)._do_collect_metadata()
482
        self.meta["USERS"] = " ".join(self._get_users())
483

    
484
    def _get_users(self):
485
        """Returns a list of users found in the images"""
486
        path = self._registry_file_path('SAM')
487
        samfd, sam = tempfile.mkstemp()
488
        try:
489
            os.close(samfd)
490
            self.g.download(path, sam)
491

    
492
            h = hivex.Hivex(sam)
493

    
494
            key = h.root()
495
            # Navigate to /SAM/Domains/Account/Users/Names
496
            for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
497
                key = h.node_get_child(key, child)
498

    
499
            users = [h.node_name(x) for x in h.node_children(key)]
500

    
501
        finally:
502
            os.unlink(sam)
503

    
504
        # Filter out the guest account
505
        return filter(lambda x: x != "Guest", users)
506

    
507
    def _guest_exec(self, command, fatal=True):
508
        """Execute a command on a windows VM"""
509

    
510
        user = "Administrator%" + self.sysprep_params['password']
511
        addr = 'localhost'
512
        runas = '--runas=%s' % user
513
        winexe = subprocess.Popen(
514
            ['winexe', '-U', user, "//%s" % addr, runas, command],
515
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
516

    
517
        stdout, stderr = winexe.communicate()
518
        rc = winexe.poll()
519

    
520
        if rc != 0 and fatal:
521
            reason = stderr if len(stderr) else stdout
522
            raise FatalError("Command: `%s' failed. Reason: %s" %
523
                             (command, reason))
524

    
525
        return (stdout, stderr, rc)
526

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