Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ b8d4b14a

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

    
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('Executing sysprep on the image (may take more that 10 minutes)')
76
    def microsoft_sysprep(self):
77
        """Run the Microsoft System Preparation Tool on the Image. This will
78
        remove system-specific data and will make the image ready to be
79
        deployed. After this no other task may run.
80
        """
81

    
82
        self._guest_exec(r'C:\Windows\system32\sysprep\sysprep '
83
                         r'/quiet /generalize /oobe /shutdown')
84
        self.syspreped = True
85

    
86
    def do_sysprep(self):
87
        """Prepare system for image creation."""
88

    
89
        if getattr(self, 'syspreped', False):
90
            raise FatalError("Image is already syspreped!")
91

    
92
        txt = "System preparation parameter: `%s' is needed but missing!"
93
        for param in self.needed_sysprep_params():
94
            if param[0] not in self.sysprep_params:
95
                raise FatalError(txt % param[0])
96

    
97
        self.mount(readonly=False)
98
        try:
99
            disabled_uac = self._update_uac_remote_setting(1)
100
            token = self._enable_os_monitor()
101
        finally:
102
            self.umount()
103

    
104
        self.out.output("Shutting down helper VM ...", False)
105
        self.g.sync()
106
        # guestfs_shutdown which is the prefered way to shutdown the backend
107
        # process was introduced in version 1.19.16
108
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
109
            ret = self.g.shutdown()
110
        else:
111
            ret = self.g.kill_subprocess()
112

    
113
        self.out.success('done')
114

    
115
        vm = None
116
        monitor = None
117
        try:
118
            self.out.output("Starting windows VM ...", False)
119
            monitorfd, monitor = tempfile.mkstemp()
120
            os.close(monitorfd)
121
            vm, display = self._create_vm(monitor)
122
            self.out.success("started (console on vnc display: %d)." % display)
123

    
124
            self.out.output("Waiting for OS to boot ...", False)
125
            if not self._wait_on_file(monitor, token):
126
                raise FatalError("Windows booting timed out.")
127
            else:
128
                self.out.success('done')
129

    
130
            self.out.output("Disabling automatic logon ...", False)
131
            self._disable_autologon()
132
            self.out.success('done')
133

    
134
            self.out.output('Preparing system from image creation:')
135

    
136
            tasks = self.list_syspreps()
137
            enabled = filter(lambda x: x.enabled, tasks)
138

    
139
            size = len(enabled)
140

    
141
            # Make sure the ms sysprep is the last task to run if it is enabled
142
            enabled = filter(
143
                lambda x: x.im_func.func_name != 'microsoft_sysprep', enabled)
144

    
145
            ms_sysprep_enabled = False
146
            if len(enabled) != size:
147
                enabled.append(self.ms_sysprep)
148
                ms_sysprep_enabled = True
149

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

    
157
            self.out.output("Shutting down windows VM ...", False)
158
            if not ms_sysprep_enabled:
159
                self._shutdown()
160
            self.out.success("done")
161

    
162
            vm.wait()
163
        finally:
164
            if monitor is not None:
165
                os.unlink(monitor)
166

    
167
            if vm is not None:
168
                self._destroy_vm(vm)
169

    
170
            self.out.output("Relaunching helper VM (may take a while) ...",
171
                            False)
172
            self.g.launch()
173
            self.out.success('done')
174

    
175
        if disabled_uac:
176
            self._update_uac_remote_setting(0)
177

    
178
    def _create_vm(self, monitor):
179
        """Create a VM with the image attached as the disk
180

181
            monitor: a file to be used to monitor when the OS is up
182
        """
183

    
184
        def random_mac():
185
            mac = [0x00, 0x16, 0x3e,
186
                   random.randint(0x00, 0x7f),
187
                   random.randint(0x00, 0xff),
188
                   random.randint(0x00, 0xff)]
189

    
190
            return ':'.join(map(lambda x: "%02x" % x, mac))
191

    
192
        # Use ganeti's VNC port range for a random vnc port
193
        vnc_port = random.randint(11000, 14999)
194
        display = vnc_port - 5900
195

    
196
        vm = kvm('-smp', '1', '-m', '1024', '-drive',
197
                 'file=%s,format=raw,cache=none,if=virtio' % self.image.device,
198
                 '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
199
                 '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' %
200
                 random_mac(), '-vnc', ':%d' % display, '-serial',
201
                 'file:%s' % monitor, _bg=True)
202

    
203
        return vm, display
204

    
205
    def _destroy_vm(self, vm):
206
        """Destroy a VM previously created by _create_vm"""
207
        if vm.process.alive:
208
            vm.terminate()
209

    
210
    def _shutdown(self):
211
        """Shuts down the windows VM"""
212
        self._guest_exec(r'shutdown /s /t 5')
213

    
214
    def _wait_on_file(self, fname, msg):
215
        """Wait until a message appears on a file"""
216

    
217
        for i in range(BOOT_TIMEOUT):
218
            time.sleep(1)
219
            with open(fname) as f:
220
                for line in f:
221
                    if line.startswith(msg):
222
                        return True
223
        return False
224

    
225
    def _disable_autologon(self):
226
        """Disable automatic logon on the windows image"""
227

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

    
231
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
232
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
233
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
234

    
235
    def _registry_file_path(self, regfile):
236
        """Retrieves the case sensitive path to a registry file"""
237

    
238
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
239
        path = "%s/system32/config/%s" % (systemroot, regfile)
240
        try:
241
            path = self.g.case_sensitive_path(path)
242
        except RuntimeError as e:
243
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
244
                             % (regfile, str(e)))
245
        return path
246

    
247
    def _enable_os_monitor(self):
248
        """Add a script in the registry that will send a random string to the
249
        first serial port when the windows image finishes booting.
250
        """
251

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

    
254
        path = self._registry_file_path('SOFTWARE')
255
        softwarefd, software = tempfile.mkstemp()
256
        try:
257
            os.close(softwarefd)
258
            self.g.download(path, software)
259

    
260
            h = hivex.Hivex(software, write=True)
261

    
262
            # Enable automatic logon.
263
            # This is needed because we need to execute a script that we add in
264
            # the RunOnce registry entry and those programs only get executed
265
            # when a user logs on. There is a RunServicesOnce registry entry
266
            # whose keys get executed in the background when the logon dialog
267
            # box first appears, but they seem to only work with services and
268
            # not arbitrary command line expressions :-(
269
            #
270
            # Instructions on how to turn on automatic logon in Windows can be
271
            # found here: http://support.microsoft.com/kb/324737
272
            #
273
            # Warning: Registry change will not work if the “Logon Banner” is
274
            # defined on the server either by a Group Policy object (GPO) or by
275
            # a local policy.
276

    
277
            winlogon = h.root()
278
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
279
                          'Winlogon'):
280
                winlogon = h.node_get_child(winlogon, child)
281

    
282
            h.node_set_value(
283
                winlogon,
284
                {'key': 'DefaultUserName', 't': 1,
285
                 'value': "Administrator".encode('utf-16le')})
286
            h.node_set_value(
287
                winlogon,
288
                {'key': 'DefaultPassword', 't': 1,
289
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
290
            h.node_set_value(
291
                winlogon,
292
                {'key': 'AutoAdminLogon', 't': 1,
293
                 'value': "1".encode('utf-16le')})
294

    
295
            key = h.root()
296
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
297
                key = h.node_get_child(key, child)
298

    
299
            runonce = h.node_get_child(key, "RunOnce")
300
            if runonce is None:
301
                runonce = h.node_add_child(key, "RunOnce")
302

    
303
            value = (
304
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
305
                r'-ExecutionPolicy RemoteSigned '
306
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
307
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
308
                r'$port.Close()}"').encode('utf-16le')
309

    
310
            h.node_set_value(runonce,
311
                             {'key': "BootMonitor", 't': 1, 'value': value})
312

    
313
            h.commit(None)
314

    
315
            self.g.upload(software, path)
316
        finally:
317
            os.unlink(software)
318

    
319
        return token
320

    
321
    def _update_uac_remote_setting(self, value):
322
        """Updates the registry key value:
323
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
324
        \System]"LocalAccountTokenFilterPolicy"
325

326
        value = 1 will disable the UAC remote restrictions
327
        value = 0 will enable the UAC remote restrictions
328

329
        For more info see here: http://support.microsoft.com/kb/951016
330

331
        Returns:
332
            True if the key is changed
333
            False if the key is unchanged
334
        """
335

    
336
        if value not in (0, 1):
337
            raise ValueError("Valid values for value parameter are 0 and 1")
338

    
339
        path = self._registry_file_path('SOFTWARE')
340
        softwarefd, software = tempfile.mkstemp()
341
        try:
342
            os.close(softwarefd)
343
            self.g.download(path, software)
344

    
345
            h = hivex.Hivex(software, write=True)
346

    
347
            key = h.root()
348
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
349
                          'System'):
350
                key = h.node_get_child(key, child)
351

    
352
            policy = None
353
            for val in h.node_values(key):
354
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
355
                    policy = val
356

    
357
            if policy is not None:
358
                dword = h.value_dword(policy)
359
                if dword == value:
360
                    return False
361
            elif value == 0:
362
                return False
363

    
364
            new_value = {
365
                'key': "LocalAccountTokenFilterPolicy", 't': 4L,
366
                'value': '%s\x00\x00\x00' % '\x00' if value == 0 else '\x01'}
367

    
368
            h.node_set_value(key, new_value)
369
            h.commit(None)
370

    
371
            self.g.upload(software, path)
372

    
373
        finally:
374
            os.unlink(software)
375

    
376
        return True
377

    
378
    def _do_collect_metadata(self):
379
        """Collect metadata about the OS"""
380
        super(Windows, self)._do_collect_metadata()
381
        self.meta["USERS"] = " ".join(self._get_users())
382

    
383
    def _get_users(self):
384
        """Returns a list of users found in the images"""
385
        path = self._registry_file_path('SAM')
386
        samfd, sam = tempfile.mkstemp()
387
        try:
388
            os.close(samfd)
389
            self.g.download(path, sam)
390

    
391
            h = hivex.Hivex(sam)
392

    
393
            key = h.root()
394
            # Navigate to /SAM/Domains/Account/Users/Names
395
            for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
396
                key = h.node_get_child(key, child)
397

    
398
            users = [h.node_name(x) for x in h.node_children(key)]
399

    
400
        finally:
401
            os.unlink(sam)
402

    
403
        # Filter out the guest account
404
        return filter(lambda x: x != "Guest", users)
405

    
406
    def _guest_exec(self, command, fatal=True):
407
        """Execute a command on a windows VM"""
408

    
409
        user = "Administrator%" + self.sysprep_params['password']
410
        addr = 'localhost'
411
        runas = '--runas=%s' % user
412
        winexe = subprocess.Popen(
413
            ['winexe', '-U', user, "//%s" % addr, runas, command],
414
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
415

    
416
        stdout, stderr = winexe.communicate()
417
        rc = winexe.poll()
418

    
419
        if rc != 0 and fatal:
420
            reason = stderr if len(stderr) else stdout
421
            raise FatalError("Command: `%s' failed. Reason: %s" %
422
                             (command, reason))
423

    
424
        return (stdout, stderr, rc)
425

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