Add various Windows syspreps
[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.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.g.write('/etc/passwd', '\n'.join(passwd) + '\n')
79
80         # Remove the corresponding /etc/shadow entries
81         shadow = []
82         for line in self.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.g.write('/etc/shadow', "\n".join(shadow) + '\n')
88
89         # Remove the corresponding /etc/group entries
90         group = []
91         for line in self.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.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.g.is_dir(home) and home.startswith('/home/'):
102                 self.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.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.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.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.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.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.g.is_file(action):
155                         self.out.warn("Acpid action file: %s does not exist" %
156                                       action)
157                         return
158                     self.g.copy_file_to_file(action,
159                                              "%s.orig.snf-image-creator-%d" %
160                                              (action, time.time()))
161                     self.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.g.is_file(rule_file):
186             self.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.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.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.g.is_file('/boot/grub/menu.lst'):
225             grub1 = '/boot/grub/menu.lst'
226         elif self.g.is_file('/etc/grub.conf'):
227             grub1 = '/etc/grub.conf'
228         else:
229             return
230
231         self.g.aug_init('/', 0)
232         try:
233             roots = self.g.aug_match('/files%s/title[*]/kernel/root' % grub1)
234             for root in roots:
235                 dev = self.g.aug_get(root)
236                 if not self._is_persistent(dev):
237                     # This is not always correct. Grub may contain root entries
238                     # for other systems, but we only support 1 OS per hard
239                     # disk, so this shouldn't harm.
240                     self.g.aug_set(root, new_root)
241         finally:
242             self.g.aug_save()
243             self.g.aug_close()
244
245     def _persistent_fstab(self):
246         """Replaces non-persistent device name occurencies in /etc/fstab with
247         persistent ones.
248         """
249         mpoints = self.g.mountpoints()
250         if len(mpoints) == 0:
251             pass  # TODO: error handling
252
253         device_dict = dict([[mpoint, dev] for dev, mpoint in mpoints])
254
255         root_dev = None
256         new_fstab = ""
257         fstab = self.g.cat('/etc/fstab')
258         for line in fstab.splitlines():
259
260             line, dev, mpoint = self._convert_fstab_line(line, device_dict)
261             new_fstab += "%s\n" % line
262
263             if mpoint == '/':
264                 root_dev = dev
265
266         self.g.write('/etc/fstab', new_fstab)
267         if root_dev is None:
268             pass  # TODO: error handling
269
270         return root_dev
271
272     def _convert_fstab_line(self, line, devices):
273         """Replace non-persistent device names in an fstab line to their UUID
274         equivalent
275         """
276         orig = line
277         line = line.split('#')[0].strip()
278         if len(line) == 0:
279             return orig, "", ""
280
281         entry = line.split()
282         if len(entry) != 6:
283             self.out.warn("Detected abnormal entry in fstab")
284             return orig, "", ""
285
286         dev = entry[0]
287         mpoint = entry[1]
288
289         if not self._is_persistent(dev):
290             if mpoint in devices:
291                 dev = "UUID=%s" % self._get_uuid(devices[mpoint])
292                 entry[0] = dev
293             else:
294                 # comment out the entry
295                 entry[0] = "#%s" % dev
296             return " ".join(entry), dev, mpoint
297
298         return orig, dev, mpoint
299
300     def _do_collect_metadata(self):
301         """Collect metadata about the OS"""
302         super(Linux, self)._do_collect_metadata()
303         self.meta["USERS"] = " ".join(self._get_passworded_users())
304
305         # Delete the USERS metadata if empty
306         if not len(self.meta['USERS']):
307             self.out.warn("No passworded users found!")
308             del self.meta['USERS']
309
310     def _get_passworded_users(self):
311         """Returns a list of non-locked user accounts"""
312         users = []
313         regexp = re.compile('(\S+):((?:!\S+)|(?:[^!*]\S+)|):(?:\S*:){6}')
314
315         for line in self.g.cat('/etc/shadow').splitlines():
316             match = regexp.match(line)
317             if not match:
318                 continue
319
320             user, passwd = match.groups()
321             if len(passwd) > 0 and passwd[0] == '!':
322                 self.out.warn("Ignoring locked %s account." % user)
323             else:
324                 users.append(user)
325
326         return users
327
328     def _is_persistent(self, dev):
329         """Checks if a device name is persistent."""
330         return not self._persistent.match(dev)
331
332     def _get_uuid(self, dev):
333         """Returns the UUID corresponding to a device"""
334         if dev in self._uuid:
335             return self._uuid[dev]
336
337         uuid = self.g.vfs_uuid(dev)
338         assert len(uuid)
339         self._uuid[dev] = uuid
340         return uuid
341
342 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :