a0302d1567bfca44e66c7ad2513056a9b7581762
[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):
47         super(Linux, self).__init__(image)
48         self._uuid = dict()
49         self._persistent = re.compile('/dev/[hsv]d[a-z][1-9]*')
50
51     @sysprep(enabled=False)
52     def remove_user_accounts(self, print_header=True):
53         """Remove all user accounts with id greater than 1000"""
54
55         if print_header:
56             self.out.output("Removing all user accounts with id greater than "
57                             "1000")
58
59         if 'USERS' not in self.meta:
60             return
61
62         # Remove users from /etc/passwd
63         passwd = []
64         removed_users = {}
65         metadata_users = self.meta['USERS'].split()
66         for line in self.g.cat('/etc/passwd').splitlines():
67             fields = line.split(':')
68             if int(fields[2]) > 1000:
69                 removed_users[fields[0]] = fields
70                 # remove it from the USERS metadata too
71                 if fields[0] in metadata_users:
72                     metadata_users.remove(fields[0])
73             else:
74                 passwd.append(':'.join(fields))
75
76         self.meta['USERS'] = " ".join(metadata_users)
77
78         # Delete the USERS metadata if empty
79         if not len(self.meta['USERS']):
80             del self.meta['USERS']
81
82         self.g.write('/etc/passwd', '\n'.join(passwd) + '\n')
83
84         # Remove the corresponding /etc/shadow entries
85         shadow = []
86         for line in self.g.cat('/etc/shadow').splitlines():
87             fields = line.split(':')
88             if fields[0] not in removed_users:
89                 shadow.append(':'.join(fields))
90
91         self.g.write('/etc/shadow', "\n".join(shadow) + '\n')
92
93         # Remove the corresponding /etc/group entries
94         group = []
95         for line in self.g.cat('/etc/group').splitlines():
96             fields = line.split(':')
97             # Remove groups tha have the same name as the removed users
98             if fields[0] not in removed_users:
99                 group.append(':'.join(fields))
100
101         self.g.write('/etc/group', '\n'.join(group) + '\n')
102
103         # Remove home directories
104         for home in [field[5] for field in removed_users.values()]:
105             if self.g.is_dir(home) and home.startswith('/home/'):
106                 self.g.rm_rf(home)
107
108     @sysprep()
109     def cleanup_passwords(self, print_header=True):
110         """Remove all passwords and lock all user accounts"""
111
112         if print_header:
113             self.out.output("Cleaning up passwords & locking all user "
114                             "accounts")
115
116         shadow = []
117
118         for line in self.g.cat('/etc/shadow').splitlines():
119             fields = line.split(':')
120             if fields[1] not in ('*', '!'):
121                 fields[1] = '!'
122
123             shadow.append(":".join(fields))
124
125         self.g.write('/etc/shadow', "\n".join(shadow) + '\n')
126
127     @sysprep()
128     def fix_acpid(self, print_header=True):
129         """Replace acpid powerdown action scripts to immediately shutdown the
130         system without checking if a GUI is running.
131         """
132
133         if print_header:
134             self.out.output('Fixing acpid powerdown action')
135
136         powerbtn_action = '#!/bin/sh\n\nPATH=/sbin:/bin:/usr/bin\n' \
137                           'shutdown -h now "Power button pressed"\n'
138
139         events_dir = '/etc/acpi/events'
140         if not self.g.is_dir(events_dir):
141             self.out.warn("No acpid event directory found")
142             return
143
144         event_exp = re.compile('event=(.+)', re.I)
145         action_exp = re.compile('action=(.+)', re.I)
146         for events_file in self.g.readdir(events_dir):
147             if events_file['ftyp'] != 'r':
148                 continue
149
150             fullpath = "%s/%s" % (events_dir, events_file['name'])
151             event = ""
152             action = ""
153             for line in self.g.cat(fullpath).splitlines():
154                 match = event_exp.match(line)
155                 if match:
156                     event = match.group(1)
157                     continue
158                 match = action_exp.match(line)
159                 if match:
160                     action = match.group(1)
161                     continue
162
163             if event.strip() in ("button[ /]power", "button/power.*"):
164                 if action:
165                     if not self.g.is_file(action):
166                         self.out.warn("Acpid action file: %s does not exist" %
167                                       action)
168                         return
169                     self.g.copy_file_to_file(action,
170                                              "%s.orig.snf-image-creator-%d" %
171                                              (action, time.time()))
172                     self.g.write(action, powerbtn_action)
173                     return
174                 else:
175                     self.out.warn("Acpid event file %s does not contain and "
176                                   "action")
177                     return
178             elif event.strip() == ".*":
179                 self.out.warn("Found action `.*'. Don't know how to handle "
180                               "this. Please edit `%s' image file manually to "
181                               "make the system immediatelly shutdown when an "
182                               "power button acpi event occures." %
183                               action.strip().split()[0])
184                 return
185
186         self.out.warn("No acpi power button event found!")
187
188     @sysprep()
189     def remove_persistent_net_rules(self, print_header=True):
190         """Remove udev rules that will keep network interface names persistent
191         after hardware changes and reboots. Those rules will be created again
192         the next time the image runs.
193         """
194
195         if print_header:
196             self.out.output('Removing persistent network interface names')
197
198         rule_file = '/etc/udev/rules.d/70-persistent-net.rules'
199         if self.g.is_file(rule_file):
200             self.g.rm(rule_file)
201
202     @sysprep()
203     def remove_swap_entry(self, print_header=True):
204         """Remove swap entry from /etc/fstab. If swap is the last partition
205         then the partition will be removed when shrinking is performed. If the
206         swap partition is not the last partition in the disk or if you are not
207         going to shrink the image you should probably disable this.
208         """
209
210         if print_header:
211             self.out.output('Removing swap entry from fstab')
212
213         new_fstab = ""
214         fstab = self.g.cat('/etc/fstab')
215         for line in fstab.splitlines():
216
217             entry = line.split('#')[0].strip().split()
218             if len(entry) == 6 and entry[2] == 'swap':
219                 continue
220
221             new_fstab += "%s\n" % line
222
223         self.g.write('/etc/fstab', new_fstab)
224
225     @sysprep()
226     def use_persistent_block_device_names(self, print_header=True):
227         """Scan fstab & grub configuration files and replace all non-persistent
228         device references with UUIDs.
229         """
230
231         if print_header:
232             self.out.output("Replacing fstab & grub non-persistent device "
233                             "references")
234
235         # convert all devices in fstab to persistent
236         persistent_root = self._persistent_fstab()
237
238         # convert all devices in grub1 to persistent
239         self._persistent_grub1(persistent_root)
240
241     def _persistent_grub1(self, new_root):
242         """Replaces non-persistent device name occurencies with persistent
243         ones in GRUB1 configuration files.
244         """
245         if self.g.is_file('/boot/grub/menu.lst'):
246             grub1 = '/boot/grub/menu.lst'
247         elif self.g.is_file('/etc/grub.conf'):
248             grub1 = '/etc/grub.conf'
249         else:
250             return
251
252         self.g.aug_init('/', 0)
253         try:
254             roots = self.g.aug_match('/files%s/title[*]/kernel/root' % grub1)
255             for root in roots:
256                 dev = self.g.aug_get(root)
257                 if not self._is_persistent(dev):
258                     # This is not always correct. Grub may contain root entries
259                     # for other systems, but we only support 1 OS per hard
260                     # disk, so this shouldn't harm.
261                     self.g.aug_set(root, new_root)
262         finally:
263             self.g.aug_save()
264             self.g.aug_close()
265
266     def _persistent_fstab(self):
267         """Replaces non-persistent device name occurencies in /etc/fstab with
268         persistent ones.
269         """
270         mpoints = self.g.mountpoints()
271         if len(mpoints) == 0:
272             pass  # TODO: error handling
273
274         device_dict = dict([[mpoint, dev] for dev, mpoint in mpoints])
275
276         root_dev = None
277         new_fstab = ""
278         fstab = self.g.cat('/etc/fstab')
279         for line in fstab.splitlines():
280
281             line, dev, mpoint = self._convert_fstab_line(line, device_dict)
282             new_fstab += "%s\n" % line
283
284             if mpoint == '/':
285                 root_dev = dev
286
287         self.g.write('/etc/fstab', new_fstab)
288         if root_dev is None:
289             pass  # TODO: error handling
290
291         return root_dev
292
293     def _convert_fstab_line(self, line, devices):
294         """Replace non-persistent device names in an fstab line to their UUID
295         equivalent
296         """
297         orig = line
298         line = line.split('#')[0].strip()
299         if len(line) == 0:
300             return orig, "", ""
301
302         entry = line.split()
303         if len(entry) != 6:
304             self.out.warn("Detected abnormal entry in fstab")
305             return orig, "", ""
306
307         dev = entry[0]
308         mpoint = entry[1]
309
310         if not self._is_persistent(dev):
311             if mpoint in devices:
312                 dev = "UUID=%s" % self._get_uuid(devices[mpoint])
313                 entry[0] = dev
314             else:
315                 # comment out the entry
316                 entry[0] = "#%s" % dev
317             return " ".join(entry), dev, mpoint
318
319         return orig, dev, mpoint
320
321     def _do_collect_metadata(self):
322         """Collect metadata about the OS"""
323         super(Linux, self)._do_collect_metadata()
324         self.meta["USERS"] = " ".join(self._get_passworded_users())
325
326         # Delete the USERS metadata if empty
327         if not len(self.meta['USERS']):
328             self.out.warn("No passworded users found!")
329             del self.meta['USERS']
330
331     def _get_passworded_users(self):
332         """Returns a list of non-locked user accounts"""
333         users = []
334         regexp = re.compile('(\S+):((?:!\S+)|(?:[^!*]\S+)|):(?:\S*:){6}')
335
336         for line in self.g.cat('/etc/shadow').splitlines():
337             match = regexp.match(line)
338             if not match:
339                 continue
340
341             user, passwd = match.groups()
342             if len(passwd) > 0 and passwd[0] == '!':
343                 self.out.warn("Ignoring locked %s account." % user)
344             else:
345                 users.append(user)
346
347         return users
348
349     def _is_persistent(self, dev):
350         """Checks if a device name is persistent."""
351         return not self._persistent.match(dev)
352
353     def _get_uuid(self, dev):
354         """Returns the UUID corresponding to a device"""
355         if dev in self._uuid:
356             return self._uuid[dev]
357
358         uuid = self.g.vfs_uuid(dev)
359         assert len(uuid)
360         self._uuid[dev] = uuid
361         return uuid
362
363 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :