5a3d29de9bd7c169475684954fd3330214e9a7f3
[snf-image-creator] / image_creator / os_type / linux.py
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 :