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