Update version.py and ChangeLog for 0.6.1
[snf-image-creator] / image_creator / os_type / linux.py
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 :