Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / linux.py @ 71b0ab28

History | View | Annotate | Download (12 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from image_creator.os_type.unix import Unix, sysprep
35

    
36
import re
37
import time
38

    
39

    
40
class Linux(Unix):
41
    """OS class for Linux"""
42
    def __init__(self, rootdev, ghandler, output):
43
        super(Linux, self).__init__(rootdev, ghandler, output)
44
        self._uuid = dict()
45
        self._persistent = re.compile('/dev/[hsv]d[a-z][1-9]*')
46

    
47
    def _do_collect_metadata(self):
48
        """Collect metadata about the OS"""
49

    
50
        super(Linux, self)._do_collect_metadata()
51
        self.meta["USERS"] = " ".join(self._get_passworded_users())
52

    
53
        # Delete the USERS metadata if empty
54
        if not len(self.meta['USERS']):
55
            self.out.warn("No passworded users found!")
56
            del self.meta['USERS']
57

    
58
    def _get_passworded_users(self):
59
        users = []
60
        regexp = re.compile('(\S+):((?:!\S+)|(?:[^!*]\S+)|):(?:\S*:){6}')
61

    
62
        for line in self.g.cat('/etc/shadow').splitlines():
63
            match = regexp.match(line)
64
            if not match:
65
                continue
66

    
67
            user, passwd = match.groups()
68
            if len(passwd) > 0 and passwd[0] == '!':
69
                self.out.warn("Ignoring locked %s account." % user)
70
            else:
71
                users.append(user)
72

    
73
        return users
74

    
75
    def is_persistent(self, dev):
76
        return not self._persistent.match(dev)
77

    
78
    def get_uuid(self, dev):
79
        if dev in self._uuid:
80
            return self._uuid[dev]
81

    
82
        uuid = self.g.vfs_uuid(dev)
83
        assert len(uuid)
84
        self._uuid[dev] = uuid
85
        return uuid
86

    
87
    @sysprep(enabled=False)
88
    def remove_user_accounts(self, print_header=True):
89
        """Remove all user accounts with id greater than 1000"""
90

    
91
        if print_header:
92
            self.out.output("Removing all user accounts with id greater than "
93
                            "1000")
94

    
95
        if 'USERS' not in self.meta:
96
            return
97

    
98
        # Remove users from /etc/passwd
99
        passwd = []
100
        removed_users = {}
101
        metadata_users = self.meta['USERS'].split()
102
        for line in self.g.cat('/etc/passwd').splitlines():
103
            fields = line.split(':')
104
            if int(fields[2]) > 1000:
105
                removed_users[fields[0]] = fields
106
                # remove it from the USERS metadata too
107
                if fields[0] in metadata_users:
108
                    metadata_users.remove(fields[0])
109
            else:
110
                passwd.append(':'.join(fields))
111

    
112
        self.meta['USERS'] = " ".join(metadata_users)
113

    
114
        # Delete the USERS metadata if empty
115
        if not len(self.meta['USERS']):
116
            del self.meta['USERS']
117

    
118
        self.g.write('/etc/passwd', '\n'.join(passwd) + '\n')
119

    
120
        # Remove the corresponding /etc/shadow entries
121
        shadow = []
122
        for line in self.g.cat('/etc/shadow').splitlines():
123
            fields = line.split(':')
124
            if fields[0] not in removed_users:
125
                shadow.append(':'.join(fields))
126

    
127
        self.g.write('/etc/shadow', "\n".join(shadow) + '\n')
128

    
129
        # Remove the corresponding /etc/group entries
130
        group = []
131
        for line in self.g.cat('/etc/group').splitlines():
132
            fields = line.split(':')
133
            # Remove groups tha have the same name as the removed users
134
            if fields[0] not in removed_users:
135
                group.append(':'.join(fields))
136

    
137
        self.g.write('/etc/group', '\n'.join(group) + '\n')
138

    
139
        # Remove home directories
140
        for home in [field[5] for field in removed_users.values()]:
141
            if self.g.is_dir(home) and home.startswith('/home/'):
142
                self.g.rm_rf(home)
143

    
144
    @sysprep()
145
    def cleanup_passwords(self, print_header=True):
146
        """Remove all passwords and lock all user accounts"""
147

    
148
        if print_header:
149
            self.out.output("Cleaning up passwords & locking all user "
150
                            "accounts")
151

    
152
        shadow = []
153

    
154
        for line in self.g.cat('/etc/shadow').splitlines():
155
            fields = line.split(':')
156
            if fields[1] not in ('*', '!'):
157
                fields[1] = '!'
158

    
159
            shadow.append(":".join(fields))
160

    
161
        self.g.write('/etc/shadow', "\n".join(shadow) + '\n')
162

    
163
    @sysprep()
164
    def fix_acpid(self, print_header=True):
165
        """Replace acpid powerdown action scripts to immediately shutdown the
166
        system without checking if a GUI is running.
167
        """
168

    
169
        if print_header:
170
            self.out.output('Fixing acpid powerdown action')
171

    
172
        powerbtn_action = '#!/bin/sh\n\nPATH=/sbin:/bin:/usr/bin\n' \
173
                          'shutdown -h now "Power button pressed"\n'
174

    
175
        events_dir = '/etc/acpi/events'
176
        if not self.g.is_dir(events_dir):
177
            self.out.warn("No acpid event directory found")
178
            return
179

    
180
        event_exp = re.compile('event=(.+)', re.I)
181
        action_exp = re.compile('action=(.+)', re.I)
182
        for f in self.g.readdir(events_dir):
183
            if f['ftyp'] != 'r':
184
                continue
185

    
186
            fullpath = "%s/%s" % (events_dir, f['name'])
187
            event = ""
188
            action = ""
189
            for line in self.g.cat(fullpath).splitlines():
190
                m = event_exp.match(line)
191
                if m:
192
                    event = m.group(1)
193
                    continue
194
                m = action_exp.match(line)
195
                if m:
196
                    action = m.group(1)
197
                    continue
198

    
199
            if event.strip() in ("button[ /]power", "button/power.*"):
200
                if action:
201
                    if not self.g.is_file(action):
202
                        self.out.warn("Acpid action file: %s does not exist" %
203
                                      action)
204
                        return
205
                    self.g.copy_file_to_file(action,
206
                                             "%s.orig.snf-image-creator-%d" %
207
                                             (action, time.time()))
208
                    self.g.write(action, powerbtn_action)
209
                    return
210
                else:
211
                    self.out.warn("Acpid event file %s does not contain and "
212
                                  "action")
213
                    return
214
            elif event.strip() == ".*":
215
                self.out.warn("Found action `.*'. Don't know how to handle "
216
                              "this. Please edit `%s' image file manually to "
217
                              "make the system immediatelly shutdown when an "
218
                              "power button acpi event occures." %
219
                              action.strip().split()[0])
220
                return
221

    
222
        self.out.warn("No acpi power button event found!")
223

    
224
    @sysprep()
225
    def remove_persistent_net_rules(self, print_header=True):
226
        """Remove udev rules that will keep network interface names persistent
227
        after hardware changes and reboots. Those rules will be created again
228
        the next time the image runs.
229
        """
230

    
231
        if print_header:
232
            self.out.output('Removing persistent network interface names')
233

    
234
        rule_file = '/etc/udev/rules.d/70-persistent-net.rules'
235
        if self.g.is_file(rule_file):
236
            self.g.rm(rule_file)
237

    
238
    @sysprep()
239
    def remove_swap_entry(self, print_header=True):
240
        """Remove swap entry from /etc/fstab. If swap is the last partition
241
        then the partition will be removed when shrinking is performed. If the
242
        swap partition is not the last partition in the disk or if you are not
243
        going to shrink the image you should probably disable this.
244
        """
245

    
246
        if print_header:
247
            self.out.output('Removing swap entry from fstab')
248

    
249
        new_fstab = ""
250
        fstab = self.g.cat('/etc/fstab')
251
        for line in fstab.splitlines():
252

    
253
            entry = line.split('#')[0].strip().split()
254
            if len(entry) == 6 and entry[2] == 'swap':
255
                continue
256

    
257
            new_fstab += "%s\n" % line
258

    
259
        self.g.write('/etc/fstab', new_fstab)
260

    
261
    @sysprep()
262
    def use_persistent_block_device_names(self, print_header=True):
263
        """Scan fstab & grub configuration files and replace all non-persistent
264
        device references with UUIDs.
265
        """
266

    
267
        if print_header:
268
            self.out.output("Replacing fstab & grub non-persistent device "
269
                            "references")
270

    
271
        # convert all devices in fstab to persistent
272
        persistent_root = self._persistent_fstab()
273

    
274
        # convert all devices in grub1 to persistent
275
        self._persistent_grub1(persistent_root)
276

    
277
    def _persistent_grub1(self, new_root):
278
        if self.g.is_file('/boot/grub/menu.lst'):
279
            grub1 = '/boot/grub/menu.lst'
280
        elif self.g.is_file('/etc/grub.conf'):
281
            grub1 = '/etc/grub.conf'
282
        else:
283
            return
284

    
285
        self.g.aug_init('/', 0)
286
        try:
287
            roots = self.g.aug_match('/files%s/title[*]/kernel/root' % grub1)
288
            for root in roots:
289
                dev = self.g.aug_get(root)
290
                if not self.is_persistent(dev):
291
                    # This is not always correct. Grub may contain root entries
292
                    # for other systems, but we only support 1 OS per hard
293
                    # disk, so this shouldn't harm.
294
                    self.g.aug_set(root, new_root)
295
        finally:
296
            self.g.aug_save()
297
            self.g.aug_close()
298

    
299
    def _persistent_fstab(self):
300
        mpoints = self.g.mountpoints()
301
        if len(mpoints) == 0:
302
            pass  # TODO: error handling
303

    
304
        device_dict = dict([[mpoint, dev] for dev, mpoint in mpoints])
305

    
306
        root_dev = None
307
        new_fstab = ""
308
        fstab = self.g.cat('/etc/fstab')
309
        for line in fstab.splitlines():
310

    
311
            line, dev, mpoint = self._convert_fstab_line(line, device_dict)
312
            new_fstab += "%s\n" % line
313

    
314
            if mpoint == '/':
315
                root_dev = dev
316

    
317
        self.g.write('/etc/fstab', new_fstab)
318
        if root_dev is None:
319
            pass  # TODO: error handling
320

    
321
        return root_dev
322

    
323
    def _convert_fstab_line(self, line, devices):
324
        orig = line
325
        line = line.split('#')[0].strip()
326
        if len(line) == 0:
327
            return orig, "", ""
328

    
329
        entry = line.split()
330
        if len(entry) != 6:
331
            self.out.warn("Detected abnormal entry in fstab")
332
            return orig, "", ""
333

    
334
        dev = entry[0]
335
        mpoint = entry[1]
336

    
337
        if not self.is_persistent(dev):
338
            if mpoint in devices:
339
                dev = "UUID=%s" % self.get_uuid(devices[mpoint])
340
                entry[0] = dev
341
            else:
342
                # comment out the entry
343
                entry[0] = "#%s" % dev
344
            return " ".join(entry), dev, mpoint
345

    
346
        return orig, dev, mpoint
347

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