Remove backup file for shadow
[snf-image-creator] / image_creator / os_type / linux.py
index e128396..53e8a5a 100644 (file)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+#
 # Copyright 2012 GRNET S.A. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
 
+"""This module hosts OS-specific code for Linux"""
+
 from image_creator.os_type.unix import Unix, sysprep
-from image_creator.util import warn, output
 
 import re
 import time
 
 
 class Linux(Unix):
-    def __init__(self, rootdev, ghandler):
-        super(Linux, self).__init__(rootdev, ghandler)
+    """OS class for Linux"""
+    def __init__(self, image, **kargs):
+        super(Linux, self).__init__(image, **kargs)
         self._uuid = dict()
         self._persistent = re.compile('/dev/[hsv]d[a-z][1-9]*')
 
-    def is_persistent(self, dev):
-        return not self._persistent.match(dev)
+    @sysprep('Removing user accounts with id greater that 1000', enabled=False)
+    def remove_user_accounts(self):
+        """Remove all user accounts with id greater than 1000"""
 
-    def get_uuid(self, dev):
-        if dev in self._uuid:
-            return self._uuid[dev]
+        if 'USERS' not in self.meta:
+            return
+
+        # Remove users from /etc/passwd
+        passwd = []
+        removed_users = {}
+        metadata_users = self.meta['USERS'].split()
+        for line in self.image.g.cat('/etc/passwd').splitlines():
+            fields = line.split(':')
+            if int(fields[2]) > 1000:
+                removed_users[fields[0]] = fields
+                # remove it from the USERS metadata too
+                if fields[0] in metadata_users:
+                    metadata_users.remove(fields[0])
+            else:
+                passwd.append(':'.join(fields))
+
+        self.meta['USERS'] = " ".join(metadata_users)
+
+        # Delete the USERS metadata if empty
+        if not len(self.meta['USERS']):
+            del self.meta['USERS']
+
+        self.image.g.write('/etc/passwd', '\n'.join(passwd) + '\n')
 
-        for attr in self.g.blkid(dev):
-            if attr[0] == 'UUID':
-                self._uuid[dev] = attr[1]
-                return attr[1]
+        # Remove the corresponding /etc/shadow entries
+        shadow = []
+        for line in self.image.g.cat('/etc/shadow').splitlines():
+            fields = line.split(':')
+            if fields[0] not in removed_users:
+                shadow.append(':'.join(fields))
 
-    @sysprep()
-    def fix_acpid(self, print_header=True):
+        self.image.g.write('/etc/shadow', "\n".join(shadow) + '\n')
+
+        # Remove the corresponding /etc/group entries
+        group = []
+        for line in self.image.g.cat('/etc/group').splitlines():
+            fields = line.split(':')
+            # Remove groups tha have the same name as the removed users
+            if fields[0] not in removed_users:
+                group.append(':'.join(fields))
+
+        self.image.g.write('/etc/group', '\n'.join(group) + '\n')
+
+        # Remove home directories
+        for home in [field[5] for field in removed_users.values()]:
+            if self.image.g.is_dir(home) and home.startswith('/home/'):
+                self.image.g.rm_rf(home)
+
+    @sysprep('Cleaning up password & locking all user accounts')
+    def cleanup_passwords(self):
+        """Remove all passwords and lock all user accounts"""
+
+        shadow = []
+
+        for line in self.image.g.cat('/etc/shadow').splitlines():
+            fields = line.split(':')
+            if fields[1] not in ('*', '!'):
+                fields[1] = '!'
+
+            shadow.append(":".join(fields))
+
+        self.image.g.write('/etc/shadow', "\n".join(shadow) + '\n')
+
+        # Remove backup file for /etc/shadow
+        self.image.g.rm_rf('/etc/shadow-')
+
+    @sysprep('Fixing acpid powerdown action')
+    def fix_acpid(self):
         """Replace acpid powerdown action scripts to immediately shutdown the
         system without checking if a GUI is running.
         """
 
-        if print_header:
-            output('Fixing acpid powerdown action')
-
         powerbtn_action = '#!/bin/sh\n\nPATH=/sbin:/bin:/usr/bin\n' \
-                                'shutdown -h now \"Power button pressed\"\n'
+                          'shutdown -h now "Power button pressed"\n'
 
         events_dir = '/etc/acpi/events'
-        if not self.g.is_dir(events_dir):
-            warn("No acpid event directory found")
+        if not self.image.g.is_dir(events_dir):
+            self.out.warn("No acpid event directory found")
             return
 
         event_exp = re.compile('event=(.+)', re.I)
         action_exp = re.compile('action=(.+)', re.I)
-        for f in self.g.readdir(events_dir):
-            if f['ftyp'] != 'r':
+        for events_file in self.image.g.readdir(events_dir):
+            if events_file['ftyp'] != 'r':
                 continue
 
-            fullpath = "%s/%s" % (events_dir, f['name'])
+            fullpath = "%s/%s" % (events_dir, events_file['name'])
             event = ""
             action = ""
-            for line in self.g.cat(fullpath).splitlines():
-                m = event_exp.match(line)
-                if m:
-                    event = m.group(1)
+            for line in self.image.g.cat(fullpath).splitlines():
+                match = event_exp.match(line)
+                if match:
+                    event = match.group(1)
                     continue
-                m = action_exp.match(line)
-                if m:
-                    action = m.group(1)
+                match = action_exp.match(line)
+                if match:
+                    action = match.group(1)
                     continue
 
-            if event.strip() == "button[ /]power":
+            if event.strip() in ("button[ /]power", "button/power.*"):
                 if action:
-                    if not self.g.is_file(action):
-                        warn("Acpid action file: %s does not exist" % action)
+                    if not self.image.g.is_file(action):
+                        self.out.warn("Acpid action file: %s does not exist" %
+                                      action)
                         return
-                    self.g.copy_file_to_file(action, \
-                      "%s.orig.snf-image-creator-%d" % (action, time.time()))
-                    self.g.write(action, powerbtn_action)
+                    self.image.g.copy_file_to_file(
+                        action, "%s.orig.snf-image-creator-%d" %
+                        (action, time.time()))
+                    self.image.g.write(action, powerbtn_action)
                     return
                 else:
-                    warn("Acpid event file %s does not contain and action")
+                    self.out.warn("Acpid event file %s does not contain and "
+                                  "action")
                     return
             elif event.strip() == ".*":
-                warn("Found action `.*'. Don't know how to handle this." \
-                    " Please edit \%s' image file manually to make the " \
-                    "system immediatelly shutdown when an power button acpi " \
-                    "event occures" % action)
+                self.out.warn("Found action `.*'. Don't know how to handle "
+                              "this. Please edit `%s' image file manually to "
+                              "make the system immediatelly shutdown when an "
+                              "power button acpi event occures." %
+                              action.strip().split()[0])
                 return
 
-    @sysprep()
-    def persistent_net_rules(self, print_header=True):
+        self.out.warn("No acpi power button event found!")
+
+    @sysprep('Removing persistent network interface names')
+    def remove_persistent_net_rules(self):
         """Remove udev rules that will keep network interface names persistent
         after hardware changes and reboots. Those rules will be created again
         the next time the image runs.
         """
 
-        if print_header:
-            output('Removing persistent network interface names')
-
         rule_file = '/etc/udev/rules.d/70-persistent-net.rules'
-        if self.g.is_file(rule_file):
-            self.g.rm(rule_file)
+        if self.image.g.is_file(rule_file):
+            self.image.g.rm(rule_file)
 
-    @sysprep()
-    def remove_swap_entry(self, print_header=True):
+    @sysprep('Removing swap entry from fstab')
+    def remove_swap_entry(self):
         """Remove swap entry from /etc/fstab. If swap is the last partition
         then the partition will be removed when shrinking is performed. If the
         swap partition is not the last partition in the disk or if you are not
         going to shrink the image you should probably disable this.
         """
 
-        if print_header:
-            output('Removing swap entry from fstab')
-
         new_fstab = ""
-        fstab = self.g.cat('/etc/fstab')
+        fstab = self.image.g.cat('/etc/fstab')
         for line in fstab.splitlines():
 
             entry = line.split('#')[0].strip().split()
@@ -146,17 +206,14 @@ class Linux(Unix):
 
             new_fstab += "%s\n" % line
 
-        self.g.write('/etc/fstab', new_fstab)
+        self.image.g.write('/etc/fstab', new_fstab)
 
-    @sysprep()
-    def persistent_devs(self, print_header=True):
+    @sysprep('Replacing fstab & grub non-persistent device references')
+    def use_persistent_block_device_names(self):
         """Scan fstab & grub configuration files and replace all non-persistent
-        device appearences with UUIDs.
+        device references with UUIDs.
         """
 
-        if print_header:
-            output('Replacing fstab & grub non-persistent device appearences')
-
         # convert all devices in fstab to persistent
         persistent_root = self._persistent_fstab()
 
@@ -164,29 +221,36 @@ class Linux(Unix):
         self._persistent_grub1(persistent_root)
 
     def _persistent_grub1(self, new_root):
-        if self.g.is_file('/boot/grub/menu.lst'):
+        """Replaces non-persistent device name occurencies with persistent
+        ones in GRUB1 configuration files.
+        """
+        if self.image.g.is_file('/boot/grub/menu.lst'):
             grub1 = '/boot/grub/menu.lst'
-        elif self.g.is_file('/etc/grub.conf'):
+        elif self.image.g.is_file('/etc/grub.conf'):
             grub1 = '/etc/grub.conf'
         else:
             return
 
-        self.g.aug_init('/', 0)
+        self.image.g.aug_init('/', 0)
         try:
-            roots = self.g.aug_match('/files%s/title[*]/kernel/root' % grub1)
+            roots = self.image.g.aug_match(
+                '/files%s/title[*]/kernel/root' % grub1)
             for root in roots:
-                dev = self.g.aug_get(root)
-                if not self.is_persistent(dev):
+                dev = self.image.g.aug_get(root)
+                if not self._is_persistent(dev):
                     # This is not always correct. Grub may contain root entries
                     # for other systems, but we only support 1 OS per hard
                     # disk, so this shouldn't harm.
-                    self.g.aug_set(root, new_root)
+                    self.image.g.aug_set(root, new_root)
         finally:
-            self.g.aug_save()
-            self.g.aug_close()
+            self.image.g.aug_save()
+            self.image.g.aug_close()
 
     def _persistent_fstab(self):
-        mpoints = self.g.mountpoints()
+        """Replaces non-persistent device name occurencies in /etc/fstab with
+        persistent ones.
+        """
+        mpoints = self.image.g.mountpoints()
         if len(mpoints) == 0:
             pass  # TODO: error handling
 
@@ -194,7 +258,7 @@ class Linux(Unix):
 
         root_dev = None
         new_fstab = ""
-        fstab = self.g.cat('/etc/fstab')
+        fstab = self.image.g.cat('/etc/fstab')
         for line in fstab.splitlines():
 
             line, dev, mpoint = self._convert_fstab_line(line, device_dict)
@@ -203,13 +267,16 @@ class Linux(Unix):
             if mpoint == '/':
                 root_dev = dev
 
-        self.g.write('/etc/fstab', new_fstab)
+        self.image.g.write('/etc/fstab', new_fstab)
         if root_dev is None:
             pass  # TODO: error handling
 
         return root_dev
 
     def _convert_fstab_line(self, line, devices):
+        """Replace non-persistent device names in an fstab line to their UUID
+        equivalent
+        """
         orig = line
         line = line.split('#')[0].strip()
         if len(line) == 0:
@@ -217,15 +284,15 @@ class Linux(Unix):
 
         entry = line.split()
         if len(entry) != 6:
-            warn("Detected abnormal entry in fstab")
+            self.out.warn("Detected abnormal entry in fstab")
             return orig, "", ""
 
         dev = entry[0]
         mpoint = entry[1]
 
-        if not self.is_persistent(dev):
+        if not self._is_persistent(dev):
             if mpoint in devices:
-                dev = "UUID=%s" % self.get_uuid(devices[mpoint])
+                dev = "UUID=%s" % self._get_uuid(devices[mpoint])
                 entry[0] = dev
             else:
                 # comment out the entry
@@ -234,4 +301,60 @@ class Linux(Unix):
 
         return orig, dev, mpoint
 
+    def _do_inspect(self):
+        """Run various diagnostics to check if media is supported"""
+
+        self.out.output(
+            'Checking if the media contains logical volumes (LVM)...', False)
+
+        has_lvm = True if len(self.image.g.lvs()) else False
+
+        if has_lvm:
+            self.out.output()
+            self.image.set_unsupported('The media contains logical volumes')
+        else:
+            self.out.success('no')
+
+    def _do_collect_metadata(self):
+        """Collect metadata about the OS"""
+        super(Linux, self)._do_collect_metadata()
+        self.meta["USERS"] = " ".join(self._get_passworded_users())
+
+        # Delete the USERS metadata if empty
+        if not len(self.meta['USERS']):
+            self.out.warn("No passworded users found!")
+            del self.meta['USERS']
+
+    def _get_passworded_users(self):
+        """Returns a list of non-locked user accounts"""
+        users = []
+        regexp = re.compile(r'(\S+):((?:!\S+)|(?:[^!*]\S+)|):(?:\S*:){6}')
+
+        for line in self.image.g.cat('/etc/shadow').splitlines():
+            match = regexp.match(line)
+            if not match:
+                continue
+
+            user, passwd = match.groups()
+            if len(passwd) > 0 and passwd[0] == '!':
+                self.out.warn("Ignoring locked %s account." % user)
+            else:
+                users.append(user)
+
+        return users
+
+    def _is_persistent(self, dev):
+        """Checks if a device name is persistent."""
+        return not self._persistent.match(dev)
+
+    def _get_uuid(self, dev):
+        """Returns the UUID corresponding to a device"""
+        if dev in self._uuid:
+            return self._uuid[dev]
+
+        uuid = self.image.g.vfs_uuid(dev)
+        assert len(uuid)
+        self._uuid[dev] = uuid
+        return uuid
+
 # vim: set sta sts=4 shiftwidth=4 sw=4 et ai :