Statistics
| Branch: | Tag: | Revision:

root / image_creator / os_type / linux.py @ f63e359f

History | View | Annotate | Download (12.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 for Linux"""
37

    
38
from image_creator.os_type.unix import Unix, sysprep
39

    
40
import re
41
import time
42

    
43

    
44
class Linux(Unix):
45
    """OS class for Linux"""
46
    def __init__(self, image, **kargs):
47
        super(Linux, self).__init__(image, **kargs)
48
        self._uuid = dict()
49
        self._persistent = re.compile('/dev/[hsv]d[a-z][1-9]*')
50

    
51
    @sysprep('Removing user accounts with id greater that 1000', enabled=False)
52
    def remove_user_accounts(self):
53
        """Remove all user accounts with id greater than 1000"""
54

    
55
        if 'USERS' not in self.meta:
56
            return
57

    
58
        # Remove users from /etc/passwd
59
        passwd = []
60
        removed_users = {}
61
        metadata_users = self.meta['USERS'].split()
62
        for line in self.image.g.cat('/etc/passwd').splitlines():
63
            fields = line.split(':')
64
            if int(fields[2]) > 1000:
65
                removed_users[fields[0]] = fields
66
                # remove it from the USERS metadata too
67
                if fields[0] in metadata_users:
68
                    metadata_users.remove(fields[0])
69
            else:
70
                passwd.append(':'.join(fields))
71

    
72
        self.meta['USERS'] = " ".join(metadata_users)
73

    
74
        # Delete the USERS metadata if empty
75
        if not len(self.meta['USERS']):
76
            del self.meta['USERS']
77

    
78
        self.image.g.write('/etc/passwd', '\n'.join(passwd) + '\n')
79

    
80
        # Remove the corresponding /etc/shadow entries
81
        shadow = []
82
        for line in self.image.g.cat('/etc/shadow').splitlines():
83
            fields = line.split(':')
84
            if fields[0] not in removed_users:
85
                shadow.append(':'.join(fields))
86

    
87
        self.image.g.write('/etc/shadow', "\n".join(shadow) + '\n')
88

    
89
        # Remove the corresponding /etc/group entries
90
        group = []
91
        for line in self.image.g.cat('/etc/group').splitlines():
92
            fields = line.split(':')
93
            # Remove groups tha have the same name as the removed users
94
            if fields[0] not in removed_users:
95
                group.append(':'.join(fields))
96

    
97
        self.image.g.write('/etc/group', '\n'.join(group) + '\n')
98

    
99
        # Remove home directories
100
        for home in [field[5] for field in removed_users.values()]:
101
            if self.image.g.is_dir(home) and home.startswith('/home/'):
102
                self.image.g.rm_rf(home)
103

    
104
    @sysprep('Cleaning up password & locking all user accounts')
105
    def cleanup_passwords(self):
106
        """Remove all passwords and lock all user accounts"""
107

    
108
        shadow = []
109

    
110
        for line in self.image.g.cat('/etc/shadow').splitlines():
111
            fields = line.split(':')
112
            if fields[1] not in ('*', '!'):
113
                fields[1] = '!'
114

    
115
            shadow.append(":".join(fields))
116

    
117
        self.image.g.write('/etc/shadow', "\n".join(shadow) + '\n')
118

    
119
        # Remove backup file for /etc/shadow
120
        self.image.g.rm_rf('/etc/shadow-')
121

    
122
    @sysprep('Fixing acpid powerdown action')
123
    def fix_acpid(self):
124
        """Replace acpid powerdown action scripts to immediately shutdown the
125
        system without checking if a GUI is running.
126
        """
127

    
128
        powerbtn_action = '#!/bin/sh\n\nPATH=/sbin:/bin:/usr/bin\n' \
129
                          'shutdown -h now "Power button pressed"\n'
130

    
131
        events_dir = '/etc/acpi/events'
132
        if not self.image.g.is_dir(events_dir):
133
            self.out.warn("No acpid event directory found")
134
            return
135

    
136
        event_exp = re.compile('event=(.+)', re.I)
137
        action_exp = re.compile('action=(.+)', re.I)
138
        for events_file in self.image.g.readdir(events_dir):
139
            if events_file['ftyp'] != 'r':
140
                continue
141

    
142
            fullpath = "%s/%s" % (events_dir, events_file['name'])
143
            event = ""
144
            action = ""
145
            for line in self.image.g.cat(fullpath).splitlines():
146
                match = event_exp.match(line)
147
                if match:
148
                    event = match.group(1)
149
                    continue
150
                match = action_exp.match(line)
151
                if match:
152
                    action = match.group(1)
153
                    continue
154

    
155
            if event.strip() in ("button[ /]power", "button/power.*"):
156
                if action:
157
                    if not self.image.g.is_file(action):
158
                        self.out.warn("Acpid action file: %s does not exist" %
159
                                      action)
160
                        return
161
                    self.image.g.copy_file_to_file(
162
                        action, "%s.orig.snf-image-creator-%d" %
163
                        (action, time.time()))
164
                    self.image.g.write(action, powerbtn_action)
165
                    return
166
                else:
167
                    self.out.warn("Acpid event file %s does not contain and "
168
                                  "action")
169
                    return
170
            elif event.strip() == ".*":
171
                self.out.warn("Found action `.*'. Don't know how to handle "
172
                              "this. Please edit `%s' image file manually to "
173
                              "make the system immediatelly shutdown when an "
174
                              "power button acpi event occures." %
175
                              action.strip().split()[0])
176
                return
177

    
178
        self.out.warn("No acpi power button event found!")
179

    
180
    @sysprep('Removing persistent network interface names')
181
    def remove_persistent_net_rules(self):
182
        """Remove udev rules that will keep network interface names persistent
183
        after hardware changes and reboots. Those rules will be created again
184
        the next time the image runs.
185
        """
186

    
187
        rule_file = '/etc/udev/rules.d/70-persistent-net.rules'
188
        if self.image.g.is_file(rule_file):
189
            self.image.g.rm(rule_file)
190

    
191
    @sysprep('Removing swap entry from fstab')
192
    def remove_swap_entry(self):
193
        """Remove swap entry from /etc/fstab. If swap is the last partition
194
        then the partition will be removed when shrinking is performed. If the
195
        swap partition is not the last partition in the disk or if you are not
196
        going to shrink the image you should probably disable this.
197
        """
198

    
199
        new_fstab = ""
200
        fstab = self.image.g.cat('/etc/fstab')
201
        for line in fstab.splitlines():
202

    
203
            entry = line.split('#')[0].strip().split()
204
            if len(entry) == 6 and entry[2] == 'swap':
205
                continue
206

    
207
            new_fstab += "%s\n" % line
208

    
209
        self.image.g.write('/etc/fstab', new_fstab)
210

    
211
    @sysprep('Replacing fstab & grub non-persistent device references')
212
    def use_persistent_block_device_names(self):
213
        """Scan fstab & grub configuration files and replace all non-persistent
214
        device references with UUIDs.
215
        """
216

    
217
        # convert all devices in fstab to persistent
218
        persistent_root = self._persistent_fstab()
219

    
220
        # convert all devices in grub1 to persistent
221
        self._persistent_grub1(persistent_root)
222

    
223
    def _persistent_grub1(self, new_root):
224
        """Replaces non-persistent device name occurencies with persistent
225
        ones in GRUB1 configuration files.
226
        """
227
        if self.image.g.is_file('/boot/grub/menu.lst'):
228
            grub1 = '/boot/grub/menu.lst'
229
        elif self.image.g.is_file('/etc/grub.conf'):
230
            grub1 = '/etc/grub.conf'
231
        else:
232
            return
233

    
234
        self.image.g.aug_init('/', 0)
235
        try:
236
            roots = self.image.g.aug_match(
237
                '/files%s/title[*]/kernel/root' % grub1)
238
            for root in roots:
239
                dev = self.image.g.aug_get(root)
240
                if not self._is_persistent(dev):
241
                    # This is not always correct. Grub may contain root entries
242
                    # for other systems, but we only support 1 OS per hard
243
                    # disk, so this shouldn't harm.
244
                    self.image.g.aug_set(root, new_root)
245
        finally:
246
            self.image.g.aug_save()
247
            self.image.g.aug_close()
248

    
249
    def _persistent_fstab(self):
250
        """Replaces non-persistent device name occurencies in /etc/fstab with
251
        persistent ones.
252
        """
253
        mpoints = self.image.g.mountpoints()
254
        if len(mpoints) == 0:
255
            pass  # TODO: error handling
256

    
257
        device_dict = dict([[mpoint, dev] for dev, mpoint in mpoints])
258

    
259
        root_dev = None
260
        new_fstab = ""
261
        fstab = self.image.g.cat('/etc/fstab')
262
        for line in fstab.splitlines():
263

    
264
            line, dev, mpoint = self._convert_fstab_line(line, device_dict)
265
            new_fstab += "%s\n" % line
266

    
267
            if mpoint == '/':
268
                root_dev = dev
269

    
270
        self.image.g.write('/etc/fstab', new_fstab)
271
        if root_dev is None:
272
            pass  # TODO: error handling
273

    
274
        return root_dev
275

    
276
    def _convert_fstab_line(self, line, devices):
277
        """Replace non-persistent device names in an fstab line to their UUID
278
        equivalent
279
        """
280
        orig = line
281
        line = line.split('#')[0].strip()
282
        if len(line) == 0:
283
            return orig, "", ""
284

    
285
        entry = line.split()
286
        if len(entry) != 6:
287
            self.out.warn("Detected abnormal entry in fstab")
288
            return orig, "", ""
289

    
290
        dev = entry[0]
291
        mpoint = entry[1]
292

    
293
        if not self._is_persistent(dev):
294
            if mpoint in devices:
295
                dev = "UUID=%s" % self._get_uuid(devices[mpoint])
296
                entry[0] = dev
297
            else:
298
                # comment out the entry
299
                entry[0] = "#%s" % dev
300
            return " ".join(entry), dev, mpoint
301

    
302
        return orig, dev, mpoint
303

    
304
    def _do_inspect(self):
305
        """Run various diagnostics to check if media is supported"""
306

    
307
        self.out.output(
308
            'Checking if the media contains logical volumes (LVM)...', False)
309

    
310
        has_lvm = True if len(self.image.g.lvs()) else False
311

    
312
        if has_lvm:
313
            self.out.output()
314
            self.image.set_unsupported('The media contains logical volumes')
315
        else:
316
            self.out.success('no')
317

    
318
    def _do_collect_metadata(self):
319
        """Collect metadata about the OS"""
320
        super(Linux, self)._do_collect_metadata()
321
        self.meta["USERS"] = " ".join(self._get_passworded_users())
322

    
323
        # Delete the USERS metadata if empty
324
        if not len(self.meta['USERS']):
325
            self.out.warn("No passworded users found!")
326
            del self.meta['USERS']
327

    
328
    def _get_passworded_users(self):
329
        """Returns a list of non-locked user accounts"""
330
        users = []
331
        regexp = re.compile(r'(\S+):((?:!\S+)|(?:[^!*]\S+)|):(?:\S*:){6}')
332

    
333
        for line in self.image.g.cat('/etc/shadow').splitlines():
334
            match = regexp.match(line)
335
            if not match:
336
                continue
337

    
338
            user, passwd = match.groups()
339
            if len(passwd) > 0 and passwd[0] == '!':
340
                self.out.warn("Ignoring locked %s account." % user)
341
            else:
342
                users.append(user)
343

    
344
        return users
345

    
346
    def _is_persistent(self, dev):
347
        """Checks if a device name is persistent."""
348
        return not self._persistent.match(dev)
349

    
350
    def _get_uuid(self, dev):
351
        """Returns the UUID corresponding to a device"""
352
        if dev in self._uuid:
353
            return self._uuid[dev]
354

    
355
        uuid = self.image.g.vfs_uuid(dev)
356
        assert len(uuid)
357
        self._uuid[dev] = uuid
358
        return uuid
359

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