Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / windows.py @ d4270a48

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

    
145
        # Query for the maximum number of reclaimable bytes
146
        cmd = (
147
            r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
148
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
149
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
150
            r'ECHO SHRINK QUERYMAX >> %SCRIPT% & ' +
151
            r'ECHO EXIT >> %SCRIPT% & ' +
152
            r'DISKPART /S %SCRIPT% & ' +
153
            r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
154
            r'DEL /Q %SCRIPT%"')
155

    
156
        stdout, stderr, rc = self._guest_exec(cmd)
157

    
158
        querymax = None
159
        for line in stdout.splitlines():
160
            # diskpart will return something like this:
161
            #
162
            #   The maximum number of reclaimable bytes is: xxxx MB
163
            #
164
            if line.find('reclaimable') >= 0:
165
                querymax = line.split(':')[1].split()[0].strip()
166
                assert querymax.isdigit(), \
167
                    "Number of reclaimable bytes not a number"
168

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

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

    
181
        if querymax < 0:
182
            self.out.warn("Not enought available space to shrink the image!")
183
            return
184

    
185
        cmd = (
186
            r'cmd /Q /C "SET SCRIPT=%TEMP%\QUERYMAX_%RANDOM%.TXT & ' +
187
            r'ECHO SELECT DISK 0 > %SCRIPT% & ' +
188
            'ECHO SELECT PARTITION %d >> %%SCRIPT%% & ' % self.last_part_num +
189
            'ECHO SHRINK DESIRED=%d >> %%SCRIPT%% & ' % querymax +
190
            r'ECHO EXIT >> %SCRIPT% & ' +
191
            r'DISKPART /S %SCRIPT% & ' +
192
            r'IF ERRORLEVEL 1 EXIT /B 1 & ' +
193
            r'DEL /Q %SCRIPT%"')
194

    
195
        stdout, stderr, rc = self._guest_exec(cmd)
196

    
197
        for line in stdout.splitlines():
198
            if line.find('shrunk') >= 0:
199
                self.out.output(line)
200

    
201
    def do_sysprep(self):
202
        """Prepare system for image creation."""
203

    
204
        if getattr(self, 'syspreped', False):
205
            raise FatalError("Image is already syspreped!")
206

    
207
        txt = "System preparation parameter: `%s' is needed but missing!"
208
        for param in self.needed_sysprep_params():
209
            if param[0] not in self.sysprep_params:
210
                raise FatalError(txt % param[0])
211

    
212
        self.mount(readonly=False)
213
        try:
214
            disabled_uac = self._update_uac_remote_setting(1)
215
            token = self._enable_os_monitor()
216

    
217
            # disable the firewalls
218
            firewall_states = self._update_firewalls(0, 0, 0)
219

    
220
            # Delete the pagefile. It will be recreated when the system boots
221
            systemroot = self.g.inspect_get_windows_systemroot(self.root)
222
            pagefile = "%s/pagefile.sys" % systemroot
223
            self.g.rm_rf(self.g.case_sensitive_path(pagefile))
224

    
225
        finally:
226
            self.umount()
227

    
228
        self.out.output("Shutting down helper VM ...", False)
229
        self.g.sync()
230
        # guestfs_shutdown which is the prefered way to shutdown the backend
231
        # process was introduced in version 1.19.16
232
        if check_guestfs_version(self.g, 1, 19, 16) >= 0:
233
            ret = self.g.shutdown()
234
        else:
235
            ret = self.g.kill_subprocess()
236

    
237
        self.out.success('done')
238

    
239
        vm = None
240
        monitor = None
241
        try:
242
            self.out.output("Starting windows VM ...", False)
243
            monitorfd, monitor = tempfile.mkstemp()
244
            os.close(monitorfd)
245
            vm, display = self._create_vm(monitor)
246
            self.out.success("started (console on vnc display: %d)." % display)
247

    
248
            self.out.output("Waiting for OS to boot ...", False)
249
            if not self._wait_on_file(monitor, token):
250
                raise FatalError("Windows booting timed out.")
251
            else:
252
                self.out.success('done')
253

    
254
            time.sleep(5)  # Just to be sure everything is up
255

    
256
            self.out.output("Disabling automatic logon ...", False)
257
            self._disable_autologon()
258
            self.out.success('done')
259

    
260
            self.out.output('Preparing system from image creation:')
261

    
262
            tasks = self.list_syspreps()
263
            enabled = filter(lambda x: x.enabled, tasks)
264
            size = len(enabled)
265

    
266
            # Make sure shrink runs in the end, before ms sysprep
267
            enabled = filter(lambda x: self.sysprep_info(x).name != 'shrink',
268
                             enabled)
269

    
270
            shrink_enabled = False
271
            if len(enabled) != size:
272
                enabled.append(self.shrink)
273
                shrink_enabled = True
274

    
275
            # Make sure the ms sysprep is the last task to run if it is enabled
276
            enabled = filter(
277
                lambda x: self.sysprep_info(x).name != 'microsoft-sysprep',
278
                enabled)
279

    
280
            ms_sysprep_enabled = False
281
            if len(enabled) != size:
282
                enabled.append(self.microsoft_sysprep)
283
                ms_sysprep_enabled = True
284

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

    
292
            self.out.output("Sending shut down command ...", False)
293
            if not ms_sysprep_enabled:
294
                self._shutdown()
295
            self.out.success("done")
296

    
297
            self.out.output("Waiting for windows to shut down ...", False)
298
            vm.wait()
299
            self.out.success("done")
300
        finally:
301
            if monitor is not None:
302
                os.unlink(monitor)
303

    
304
            if vm is not None:
305
                self._destroy_vm(vm)
306

    
307
            self.out.output("Relaunching helper VM (may take a while) ...",
308
                            False)
309
            self.g.launch()
310
            self.out.success('done')
311

    
312
            self.mount(readonly=False)
313
            try:
314
                if disabled_uac:
315
                    self._update_uac_remote_setting(0)
316

    
317
                self._update_firewalls(*firewall_states)
318
            finally:
319
                self.umount()
320

    
321
    def _create_vm(self, monitor):
322
        """Create a VM with the image attached as the disk
323

324
            monitor: a file to be used to monitor when the OS is up
325
        """
326

    
327
        def random_mac():
328
            mac = [0x00, 0x16, 0x3e,
329
                   random.randint(0x00, 0x7f),
330
                   random.randint(0x00, 0xff),
331
                   random.randint(0x00, 0xff)]
332

    
333
            return ':'.join(map(lambda x: "%02x" % x, mac))
334

    
335
        # Use ganeti's VNC port range for a random vnc port
336
        vnc_port = random.randint(11000, 14999)
337
        display = vnc_port - 5900
338

    
339
        vm = kvm(
340
            '-smp', '1', '-m', '1024', '-drive',
341
            'file=%s,format=raw,cache=unsafe,if=virtio' % self.image.device,
342
            '-netdev', 'type=user,hostfwd=tcp::445-:445,id=netdev0',
343
            '-device', 'virtio-net-pci,mac=%s,netdev=netdev0' % random_mac(),
344
            '-vnc', ':%d' % display, '-serial', 'file:%s' % monitor, _bg=True)
345

    
346
        return vm, display
347

    
348
    def _destroy_vm(self, vm):
349
        """Destroy a VM previously created by _create_vm"""
350
        if vm.process.alive:
351
            vm.terminate()
352

    
353
    def _shutdown(self):
354
        """Shuts down the windows VM"""
355
        self._guest_exec(r'shutdown /s /t 5')
356

    
357
    def _wait_on_file(self, fname, msg):
358
        """Wait until a message appears on a file"""
359

    
360
        for i in range(BOOT_TIMEOUT):
361
            time.sleep(1)
362
            with open(fname) as f:
363
                for line in f:
364
                    if line.startswith(msg):
365
                        return True
366
        return False
367

    
368
    def _disable_autologon(self):
369
        """Disable automatic logon on the windows image"""
370

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

    
374
        self._guest_exec('REG DELETE %s /v DefaultUserName /f' % winlogon)
375
        self._guest_exec('REG DELETE %s /v DefaultPassword /f' % winlogon)
376
        self._guest_exec('REG DELETE %s /v AutoAdminLogon /f' % winlogon)
377

    
378
    def _registry_file_path(self, regfile):
379
        """Retrieves the case sensitive path to a registry file"""
380

    
381
        systemroot = self.g.inspect_get_windows_systemroot(self.root)
382
        path = "%s/system32/config/%s" % (systemroot, regfile)
383
        try:
384
            path = self.g.case_sensitive_path(path)
385
        except RuntimeError as e:
386
            raise FatalError("Unable to retrieve registry file: %s. Reason: %s"
387
                             % (regfile, str(e)))
388
        return path
389

    
390
    def _enable_os_monitor(self):
391
        """Add a script in the registry that will send a random string to the
392
        first serial port when the windows image finishes booting.
393
        """
394

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

    
397
        path = self._registry_file_path('SOFTWARE')
398
        softwarefd, software = tempfile.mkstemp()
399
        try:
400
            os.close(softwarefd)
401
            self.g.download(path, software)
402

    
403
            h = hivex.Hivex(software, write=True)
404

    
405
            # Enable automatic logon.
406
            # This is needed because we need to execute a script that we add in
407
            # the RunOnce registry entry and those programs only get executed
408
            # when a user logs on. There is a RunServicesOnce registry entry
409
            # whose keys get executed in the background when the logon dialog
410
            # box first appears, but they seem to only work with services and
411
            # not arbitrary command line expressions :-(
412
            #
413
            # Instructions on how to turn on automatic logon in Windows can be
414
            # found here: http://support.microsoft.com/kb/324737
415
            #
416
            # Warning: Registry change will not work if the “Logon Banner” is
417
            # defined on the server either by a Group Policy object (GPO) or by
418
            # a local policy.
419

    
420
            winlogon = h.root()
421
            for child in ('Microsoft', 'Windows NT', 'CurrentVersion',
422
                          'Winlogon'):
423
                winlogon = h.node_get_child(winlogon, child)
424

    
425
            h.node_set_value(
426
                winlogon,
427
                {'key': 'DefaultUserName', 't': 1,
428
                 'value': "Administrator".encode('utf-16le')})
429
            h.node_set_value(
430
                winlogon,
431
                {'key': 'DefaultPassword', 't': 1,
432
                 'value':  self.sysprep_params['password'].encode('utf-16le')})
433
            h.node_set_value(
434
                winlogon,
435
                {'key': 'AutoAdminLogon', 't': 1,
436
                 'value': "1".encode('utf-16le')})
437

    
438
            key = h.root()
439
            for child in ('Microsoft', 'Windows', 'CurrentVersion'):
440
                key = h.node_get_child(key, child)
441

    
442
            runonce = h.node_get_child(key, "RunOnce")
443
            if runonce is None:
444
                runonce = h.node_add_child(key, "RunOnce")
445

    
446
            value = (
447
                r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe '
448
                r'-ExecutionPolicy RemoteSigned '
449
                r'"&{$port=new-Object System.IO.Ports.SerialPort COM1,9600,'
450
                r'None,8,one;$port.open();$port.WriteLine(\"' + token + r'\");'
451
                r'$port.Close()}"').encode('utf-16le')
452

    
453
            h.node_set_value(runonce,
454
                             {'key': "BootMonitor", 't': 1, 'value': value})
455

    
456
            value = (
457
                r'REG ADD HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion'
458
                r'\policies\system /v LocalAccountTokenFilterPolicy'
459
                r' /t REG_DWORD /d 1 /f').encode('utf-16le')
460

    
461
            h.node_set_value(runonce,
462
                             {'key': "UpdateRegistry", 't': 1, 'value': value})
463

    
464
            h.commit(None)
465

    
466
            self.g.upload(software, path)
467
        finally:
468
            os.unlink(software)
469

    
470
        return token
471

    
472
    def _update_firewalls(self, domain, public, standard):
473
        """Enables or disables the firewall for the Domain, the Public and the
474
        Standard profile. Returns a triplete with the old values.
475

476
        1 will enable a firewall and 0 will disable it
477
        """
478

    
479
        if domain not in (0, 1):
480
            raise ValueError("Valid values for domain parameter are 0 and 1")
481

    
482
        if public not in (0, 1):
483
            raise ValueError("Valid values for public parameter are 0 and 1")
484

    
485
        if standard not in (0, 1):
486
            raise ValueError("Valid values for standard parameter are 0 and 1")
487

    
488
        path = self._registry_file_path("SYSTEM")
489
        systemfd, system = tempfile.mkstemp()
490
        try:
491
            os.close(systemfd)
492
            self.g.download(path, system)
493

    
494
            h = hivex.Hivex(system, write=True)
495

    
496
            select = h.node_get_child(h.root(), 'Select')
497
            current_value = h.node_get_value(select, 'Current')
498

    
499
            # expecting a little endian dword
500
            assert h.value_type(current_value)[1] == 4
501
            current = "%03d" % h.value_dword(current_value)
502

    
503
            firewall_policy = h.root()
504
            for child in ('ControlSet%s' % current, 'services', 'SharedAccess',
505
                          'Parameters', 'FirewallPolicy'):
506
                firewall_policy = h.node_get_child(firewall_policy, child)
507

    
508
            old_values = []
509
            new_values = [domain, public, standard]
510
            for profile in ('Domain', 'Public', 'Standard'):
511
                node = h.node_get_child(firewall_policy, '%sProfile' % profile)
512

    
513
                old_value = h.node_get_value(node, 'EnableFirewall')
514

    
515
                # expecting a little endian dword
516
                assert h.value_type(old_value)[1] == 4
517
                old_values.append(h.value_dword(old_value))
518

    
519
                h.node_set_value(
520
                    node, {'key': 'EnableFirewall', 't': 4L,
521
                           'value': struct.pack("<I", new_values.pop(0))})
522

    
523
            h.commit(None)
524
            self.g.upload(system, path)
525

    
526
        finally:
527
            os.unlink(system)
528

    
529
        return old_values
530

    
531
    def _update_uac_remote_setting(self, value):
532
        """Updates the registry key value:
533
        [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
534
        \System]"LocalAccountTokenFilterPolicy"
535

536
        value = 1 will disable the UAC remote restrictions
537
        value = 0 will enable the UAC remote restrictions
538

539
        For more info see here: http://support.microsoft.com/kb/951016
540

541
        Returns:
542
            True if the key is changed
543
            False if the key is unchanged
544
        """
545

    
546
        if value not in (0, 1):
547
            raise ValueError("Valid values for value parameter are 0 and 1")
548

    
549
        path = self._registry_file_path('SOFTWARE')
550
        softwarefd, software = tempfile.mkstemp()
551
        try:
552
            os.close(softwarefd)
553
            self.g.download(path, software)
554

    
555
            h = hivex.Hivex(software, write=True)
556

    
557
            key = h.root()
558
            for child in ('Microsoft', 'Windows', 'CurrentVersion', 'Policies',
559
                          'System'):
560
                key = h.node_get_child(key, child)
561

    
562
            policy = None
563
            for val in h.node_values(key):
564
                if h.value_key(val) == "LocalAccountTokenFilterPolicy":
565
                    policy = val
566

    
567
            if policy is not None:
568
                dword = h.value_dword(policy)
569
                if dword == value:
570
                    return False
571
            elif value == 0:
572
                return False
573

    
574
            new_value = {'key': "LocalAccountTokenFilterPolicy", 't': 4L,
575
                         'value': struct.pack("<I", value)}
576

    
577
            h.node_set_value(key, new_value)
578
            h.commit(None)
579

    
580
            self.g.upload(software, path)
581

    
582
        finally:
583
            os.unlink(software)
584

    
585
        return True
586

    
587
    def _do_collect_metadata(self):
588
        """Collect metadata about the OS"""
589
        super(Windows, self)._do_collect_metadata()
590
        self.meta["USERS"] = " ".join(self._get_users())
591

    
592
    def _get_users(self):
593
        """Returns a list of users found in the images"""
594
        path = self._registry_file_path('SAM')
595
        samfd, sam = tempfile.mkstemp()
596
        try:
597
            os.close(samfd)
598
            self.g.download(path, sam)
599

    
600
            h = hivex.Hivex(sam)
601

    
602
            key = h.root()
603
            # Navigate to /SAM/Domains/Account/Users/Names
604
            for child in ('SAM', 'Domains', 'Account', 'Users', 'Names'):
605
                key = h.node_get_child(key, child)
606

    
607
            users = [h.node_name(x) for x in h.node_children(key)]
608

    
609
        finally:
610
            os.unlink(sam)
611

    
612
        # Filter out the guest account
613
        return filter(lambda x: x != "Guest", users)
614

    
615
    def _guest_exec(self, command, fatal=True):
616
        """Execute a command on a windows VM"""
617

    
618
        user = "Administrator%" + self.sysprep_params['password']
619
        addr = 'localhost'
620
        runas = '--runas=%s' % user
621
        winexe = subprocess.Popen(
622
            ['winexe', '-U', user, runas, "//%s" % addr, command],
623
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
624

    
625
        stdout, stderr = winexe.communicate()
626
        rc = winexe.poll()
627

    
628
        if rc != 0 and fatal:
629
            reason = stderr if len(stderr) else stdout
630
            raise FatalError("Command: `%s' failed. Reason: %s" %
631
                             (command, reason))
632

    
633
        return (stdout, stderr, rc)
634

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