Commits

Rodrigo Bistolfi committed e4402fc Merge

Merged in m0e_lnx/vinstall (pull request #4)

  • Participants
  • Parent commits 746431b, 8d4d8d5

Comments (0)

Files changed (7)

       url='https://bitbucket.org/VLCore/vinstall/',
       packages=['vinstall', 'vinstall.core', 'vinstall.ui', 'vinstall.backend',
           'vinstall.controller'],
-      data_files=[("/usr/share/pixmaps", ["resources/logo.png"])],
       entry_points={
           'console_scripts': [
               'vinstall = vinstall.installer:main',]}

vinstall/backend/bootloader.py

+# coding: utf8
+
+
+"""Bootloader setup
+
+>>> lilo = Lilo("/dev/sda1") # pass main os root
+>>> lilo.backup_config()
+... for system in OperatingSystem.all():
+...     lilo.add_os(system)
+>>> lilo.write_config()
+>>> lilo.install()
+
+"""
+
+
+__author__ = "M0E-lnx"
+
+
+import subprocess, os, shutil
+from StringIO import StringIO
+from vinstall.backend import utils, media
+import os
+import tempfile
+import unittest
+
+
+LILO = object()
+GRUB = object()
+GRUB2 = object()
+
+
+class Lilo(object):
+    """LiLo bootloader
+    
+    """
+    def __init__(self, target, default_os, timeout=0, 
+            vga_mode="nornal", include_running_os=False):
+        
+        self.target = target
+        self.default_os = default_os
+        self.vga_mode = vga_mode
+        self.include_running_os = include_running_os
+        self.operating_systems = []
+        self._timeout = timeout
+        self.buffer = StringIO()
+        self.lilo_root = utils.get_mounted("/")
+        tamu = "/boot/tamu"
+        if not os.path.exists(tamu):
+            os.mkdir(tamu)
+    
+    def add_os(self, system):
+        if system.type == "linux":
+            self.add_unix_os(system)
+        elif "microsoft" in system.type:
+            self.add_ms_os(system)
+        else:
+            #XXX
+            pass
+
+    def add_unix_os(self, system):
+        """Add an operating system to the boot menu
+        
+        """
+        if system in self.operating_systems:
+            raise RuntimeError("OS already added")
+        if system.root != self.lilo_root:
+            
+            # mount os root partition if needed
+            umount = False
+            partition = media.Partition(system.root)
+            if not partition.is_mounted():
+                mountpoint = system.root.replace("/dev/", "/mnt/")
+                if not os.path.exists(mountpoint):
+                    os.mkdir(mountpoint)
+                partition.mount(mountpoint)
+                umount = True
+            
+            # copy kernel
+            kernel_from = os.path.join(partition.mountpoint,
+                    system.kernel_name)
+            kernel_dest = os.path.join("/boot/tamu", system.kernel)
+            shutil.copy2(kernel_from, kernel_dest)
+            
+            # copy initrd if needed
+            if system.initrd:
+                initrd_from = os.path.join(partition.mountpoint,
+                        system.initrd)
+                initrd_dest = os.path.join("/boot/tamu",
+                        system.initrd)
+                shutil.copy2(initrd_from, initrd_dest)
+            
+            # umount if needed
+            if umount:
+                partition.umount()
+
+            # write config lines
+            tab = " " * 4
+            self.buffer.write("# -- %s on %s --\n" % (system.label,
+                system.root))
+            self.buffer.write("image = %s\n" % system.kernel)
+            self.buffer.write("%s label = %s\n" % (tab, system.label))
+            if system.initrd:
+                self.buffer.write("%s initrd = %s\n" % (tab,
+                    system.initrd))
+            if system.appendline:
+                if "splash" not in system.appendline:
+                    self.buffer.write('%s append = "%s splash=silent"\n') % (tab,
+                            system.appendline)
+                else:
+                    self.buffer.write('s append = "%s"\n' % (tab,
+                            system.appendline))
+            self.buffer.write("%s read-only\n" % tab)
+            self.buffer.write("# -- \n\n")
+
+            self.operating_systems.append(system)
+
+    def add_ms_os(self, system):
+        """Add MS os to boot menu
+        
+        """
+
+    def set_default_os(self, system):
+        """Set the default OS
+        
+        """
+        self.default_os = system
+
+    def set_timeout(self, seconds):
+        """Wait for <timeout> seconds before booting the default OS
+        
+        """
+        self._timeout = int(seconds) * 10
+
+    def set_vga_mode(self, resolution, quality):
+        """Set the VGA mode
+        
+        """
+        values = {
+                "640x480" : {
+                    "low" : "769",
+                    "med" : "784",
+                    "high" : "785"},
+                "800x600" : {
+                    "low" : "771",
+                    "med" : "787",
+                    "high" : "788"},
+                "1024x768" : {
+                    "low" : "773",
+                    "med" : "790", 
+                    "high" : "791"},
+                "1280x1024": {
+                    "high":"794",
+                    "med":"793",
+                    "low":"775"}}
+
+        self.vga_mode = values[resolution][quality]
+
+    def backup_config(self):
+        """Backup existing lilo.conf
+        
+        """
+
+    def write_config(self):
+        """Write lilo.conf. 
+        
+        """
+        newline = "\n"
+        config_path = "/etc/lilo.conf"
+        header = (
+            "# LILO configuration file.",
+            "# ",
+            "# Generated by VASM.",
+            "# ",
+            "boot = %s" % self.target,
+            "compact",
+            "prompt",
+            "timeout = %s" % self.timeout,
+            "change-rules",
+            "reset",
+            "bitmap = /boot/bitmap/boot.bmp",
+            "vga = %s" % self.vga_mode,
+            "# OS list")
+        
+        with open(config_path, "w") as liloconf:
+            for i in header:
+                liloconf.write(i + newline)
+            for line in self.buffer:
+                liloconf.write(line)
+               
+    def install(self):
+        """Install the bootloader
+        
+        """
+        return subprocess.check_call(["/sbin/lilo"])
+
+
+class Grub2(object):
+    """Grub2 bootloader
+    
+    """
+    def __init__(self, target, timeout, vga_size, vga_quality):
+        self.config_path = "/etc/default/grub"
+        self.type = GRUB2
+        self.timeout = timeout
+        self.target = target
+        self.vga_mode = None
+    
+    def set_vga_mode(self, resolution="800x600"):
+        modes = {
+                "640x480": "640x480x16",
+                "800x600": "800x600x16",
+                "1024x768": "1024x768x16",
+                "1280x1024": "1280x1024x16"}
+        self.vga_mode = modes.get(resolution, "800x600")
+
+    def write_config(self, filepath="/etc/default/grub"):
+        """The grub2 config file is automatically generated, so we use
+        this method to save the values to /etc/default/grub which is 
+        parsed while the config file is generated 
+        
+        """
+
+        # set the correct vga_mode value
+        if not self.vga_mode:
+            self.set_vga_mode()
+
+        ndata = ["export GRUB_GFXMODE=%s"% self.vga_mode,
+                 "export GRUB_TERMINAL=gfxterm",
+                 "export GRUB_DISABLE_LINUX_UUID=true",
+                 "export GRUB_TIMEOUT=%s"% self.timeout,
+                 "if [ \"$GRUB_GFXMODE\" = \"1024x768x16\" ]; then",
+                 "    VGA=791 ",
+                 "elif [ \"$GRUB_GFXMODE\" = \"800x600x16\" ]; then",
+                 "    VGA=788 ",
+                 "elif [ \"$GRUB_GFXMODE\" = \"1280x1024x16\" ]; then",
+                 "    VGA=794 ",
+                 "else",
+                 "    VGA=normal",
+                 "fi",
+                 "# END OF /etc/default/grub",""]
+
+        # save the new file
+        with open(filepath, "w") as f:
+            f.writelines("\n".join(ndata))
+        
+        # after these are set, we just need to run grub-mkconfig
+        subprocess.check_call(["grub-mkconfig", "-o", "/boot/grub/grub.cfg"])
+
+    def install(self):
+        """Install grub to the target
+        
+        """
+        subprocess.check_call(["grub-install", "--no-floppy", self.target])
+
+
+class OperatingSystem(object):
+    """An OS to be added to a bootloader
+    
+    """
+    type = None         # str indicating OS type
+    kernel = None       # Kernel image
+    root = None         # path to OS root
+    appendline = None   # append line for lilo.conf 
+
+    @property
+    def kernel_name(self):
+        return os.path.split(self.kernel)[-1]
+
+    @classmethod
+    def all(cls, include_running_os=False):
+        """Return one OperatingSystem instance for each OS
+        installed
+        
+        """
+        os_prober = subprocess.check_output(["os-prober", "&>/dev/null"])
+        found_os = os_prober.split("\n")
+
+        if include_running_os and os.path.exists('/boot/vmlinuz'):
+            running_os = cls()
+            running_os.kernel = '/boot/vmlinuz'
+            running_os.label = 'VectorLinux'
+            running_os.type = "linux"
+            if os.path.exists('/boot/initrd'):
+                running_os.initrd = '/boot/initrd'
+            running_os.root = None #XXX
+            yield running_os
+
+        for i in found_os:
+            if ":" not in i:
+                continue
+            root, long_desc, short_desc, os_type = \
+                    [ s.strip() for s in i.split(":") ]
+            operating_system = cls()
+            if short_desc.startswith("Vector"):
+                short_desc = "VectorLinux - %s"% root.split("/")[-1]
+            operating_system.label = short_desc
+            if os_type.lower() == "linux":
+                operating_system.type = "linux"
+                bootinfo = get_linux_boot_info(root)
+                if bootinfo is None: 
+                    continue
+                operating_system.root = bootinfo["root"] or root
+                operating_system.kernel = bootinfo["kernel"]
+                operating_system.initrd = bootinfo["initrd"]
+                operating_system.appendline = bootinfo["append"]
+            elif "microsoft" in os.type.lower() or \
+                    "windows" in os_type.lower():
+                        operating_system.root = root
+                        operating_system.label = root.split("/")[-1]
+                        operating_system.type = "windows"
+            #XXX what about bsd, osx, etc
+            else:
+                continue
+            yield operating_system
+
+
+class MasterBootRecord(object):
+    """The MBR of a device
+    
+    """
+    def __init__(self, device):
+        self.device = device # path to the device as in /dev/sda
+
+    def read(self, bytes=2):
+        """Read n bytes
+        
+        """
+        with open(self.device) as f:
+            data = f.read(bytes)
+        return data
+
+    def bootloader(self):
+        """Return the bootloader installed in the MBR if any
+        
+        """
+        bytes = self.read()
+        if bytes == "\xEB\x63":
+            return GRUB2
+        elif bytes == "\xEB\x48":
+            return GRUB
+        elif bytes == "\xFA\xEB":
+            return LILO
+        else:
+            return None
+
+
+def get_linux_boot_info(root_partition):
+    """Use linux-boot-prober to find out how to boot this OS 
+
+    /dev/sdb1:/dev/sdb1::/boot/vmlinuz26:/boot/kernel26.img:root=/dev/sdb1
+    
+    """
+    proc = subprocess.check_output(['linux-boot-prober', root_partition])
+    hits = proc.split("\n")
+    if not hits:
+        return None
+    line = hits[0]
+    if ":" not in line:
+        return None
+    line = line.strip()
+    root, boot, _, kernel, initrd, append = \
+            [ i.strip() for i in line.split(":") ]
+    return dict(root=root, kernel=kernel, initrd=initrd, append=append)
+
+
+class FakeDevice(unittest.TestCase):
+    def setUp(self):
+        (fd, self.path) = tempfile.mkstemp(prefix="fake-device-")
+        f = os.fdopen(fd)
+        f.seek(140000)
+        os.write(fd, "0")
+
+    def tearDown(self):
+        os.unlink(self.path)
+
+
+if __name__ == "__main__":
+    unittest.main()

vinstall/backend/fstab.py

             "ext2": "defaults 0 0",
             "ext3": "defaults 0 0",
             "ext4": "defaults 0 0",
+            "jfs": "defaults 0 0",
             "iso9660": "noauto,ro,user 0 0",
             "msdos": "defaults 0 0",
             "swap": "sw 0 0",
             "linux-swap": "sw 0 0",
             "linux-swap(v1)": "sw 0 0"
         }
-        return opts["swap"] if "swap" in self.filesystem else \
-                opts.get(self.filesystem, "defaults 0 0")
+        if self.filesystem in ("swap","none","linux-swap","linux-swap(v1)"):
+            return opts["swap"]
+        else:
+            return opts.get(self.filesystem, "defauls 0 0")
 
     def find_uuid(self):
         """ Find the uuid for the specified device
         sep = " "*5
         if entry.uuid is None:
             entry.uuid = entry.find_uuid()
-        if entry.filesystem is None:
-            entry.filesyatem = entry.find_filesystem()
         # Use UUID when possible
         if entry.uuid is None:
             part = entry.device

vinstall/backend/media.py

         """Return all the partitions in the system
 
         """
-        disks = Disk.all()
+        disks = [i for i in Disk.all() if i.has_partition_table()]
         partitions = ( i for disk in disks for i in disk._disk.partitions() )
         for partition in partitions:
             if partition.type in ('NORMAL', 'LOGICAL'):
         return "%s - %s GB (%s)" % (self.model(),  self.size(), self.path())
 
     def model(self):
+        """Return the string describing the device model"""
+        return self._device.model
 
-        return self._disk.device.model
+    def free_space(self, units="MiB"):
+        """Return the ammount of free space available in units specified"""
+        return self._disk.usable_free_space.to(units)
 
-    def size(self, unit="GB"):
+    def _get_size_from_hw(self, unit="GiB"):
         """ Returns disk size, by default in GB, unit can be GB or MB
-
+        This method is used internally when the disk is not initialized.
         """
-        pow = 2 if unit == "MB" else 3
+        pow = 2 if unit == "MiB" else 3
         cylinders,  heads,  sectors = self._device.hw_geom
         sector_size = self._device.sector_size
         size = cylinders * heads * sectors * sector_size /1000 ** pow
         return size
 
-    def path(self):
+    def _get_size_from_disk(self, unit="GiB"):
+        """Returns disk size.  Used internally by size when size is available from
+        the disk object"""
+        return self._disk.size.to(unit)
+
+    def has_partition_table(self):
+        """Return True if the disk has a partition table.  False otherwise"""
+        return self._disk.type_name not in (None, "")
+
+    def size(self, unit="GiB"):
+        """ Returns disk size, by default in GB, unit can be GB or MB
 
-        return self._disk.device.path
+        """
+        if self.has_partition_table():
+            return self._get_size_from_disk(unit)
+        else:
+            return self._get_size_from_hw(unit)
+
+    def path(self):
+        """Return the node path for this device"""
+        return self._device.path
 
     def is_read_only(self):
 

vinstall/backend/partitioning.py

         """
         assert self.disk.type_name is None, "Disk already has a partition table"
         self.disk.set_label(table_type)
-        self.disk.commit()
 
     def delete_all_partitions(self):
         """ Delete all partitions from drive
 
         """
         self.disk.delete_all()
-        self.disk.commit()
 
-    def add_partition(self,  size_mb=0):
-        """Add a partition to the disk
+    def add_partition(self,  size=0, units='MiB'):
+        """Add a partition to the disk.  
+        Args:  size = partition size
 
         """
         if self.disk.type_name is None:
             self.create_partition_table()
 
-        psize = reparted.Size(size_mb,  "MB")
+        psize = reparted.Size(size,  units)
         partition = reparted.Partition(self.disk,  size=psize)
         self.disk.add_partition(partition)
-        self.disk.commit()
+
+    def write_changes(self):
+        """Finalize changes to the disk.  This needs to be called
+        after creating partitions or deleting partitions to make sure
+        the changes are actually written to the disk."""
+        return self.disk.commit()
 
 

vinstall/backend/users.py

+#!/bin/env python
+# coding: utf8
+
+#    This file is part of vinstall.
+#
+#    vinstall is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License v3 as published by
+#    the Free Software Foundation.
+#
+#    vinstall is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with vinstall.  If not, see <http://www.gnu.org/licenses/>.
+
+__author__ = "Moises Henriquez"
+__author_email__ = "moc.liamg@xnl.E0M"[::-1]
+
+"""Api for managing and creating user accounts on VectorLinux"""
+import pwd
+import grp
+import crypt
+import os
+import subprocess as sp
+import unittest
+from utils import Chroot
+#from vinstall.backend.utils import Chroot
+
+
+class User(object):
+    DEFAULT_MEMBERSHIP=('disk','lp',
+                        'floppy','audio','video',
+                        'cdrom','games','slocate',
+                        'plugdev','netdev','scanner',
+                        'users', 'wheel')
+    DEFAULT_MEMBERSHIP_ROOT=('root','bin','daemon','sys','adm','disk','wheel')
+
+    def __init__(self):
+        self._data = None
+        self._root = "/"
+        self.login = None
+        self.password = None
+        self.fullname = None
+
+    def _setup_home(self):
+        """Setup the default home directory for this user"""
+        # This should not be needed, but just in case someone
+        # decides to call this directly.
+        assert self._system_data is not None, "User account not in system"
+        assert self.login is not None, "Does this account exist yet?"
+        SKELPATH = "/etc/skel"
+        TARGETDIR= self.home
+
+        topdir, dirnames, files = os.walk(SKELPATH).next()
+        
+        # Copy the skel files to the home dir
+        # FIXME:  Does useradd do this by default on VL?
+        for item in os.listdir(SKELPATH):
+            fcopy = ["/bin/cp", "-aru", os.path.join(SKELPATH, item), 
+                   os.path.join(TARGETDIR, item)]
+            fchown = ["/bin/chown", self.login, os.path.join(TARGETDIR, item)]
+            fchgrp = ["/bin/chgrp", self.login, os.path.join(TARGETDIR, item)]
+            for fun in (fcopy, fchown, fchgrp):
+                sp.check_call(fun)
+
+
+
+        # Set permissions to the items in home directory.
+        cmd = ["/bin/chmod", "0700", TARGETDIR]
+        sp.check_call(cmd)
+
+        cmd = ["/bin/chown", "-R", self.login, TARGETDIR ]
+        return sp.check_call(cmd)
+
+    def _encrypt_password(self, passwd):
+        """Return the encrypted password"""
+        salt = passwd[-1] + passwd[1]
+        return crypt.crypt(passwd, salt)
+
+    def create(self):
+        """Create the user account on the system"""
+        assert self._system_data is None, "User account already exists"
+        assert self.password is not None, "Password attribute has not been set"
+        assert self.login is not None, "Login attribute has not been set"
+        epass = self._encrypt_password(self.password)
+
+        # Add the group
+        cmd = ["/usr/sbin/groupadd", "-g", str(self.uid), self.login ]
+        sp.check_call(cmd)
+        if self.fullname:
+            cmd = ["/usr/sbin/useradd","-m","-c","%s"% self.fullname,
+                  "-s","/bin/bash", "-g", self.login,
+                  "-G", ",".join(self.DEFAULT_MEMBERSHIP), "-p", epass,
+                  self.login]
+        else:
+            cmd = ["/usr/sbin/useradd","-m", "-s", "/bin/bash", "-g", self.login,
+                  "-G", ",".join(self.DEFAULT_MEMBERSHIP), "-p", epass,
+                   self.login]
+
+        # Launch the command
+        # FIXME:  Is subprocess.call the right method to use?
+        # subprocess.Popen offers stdout, stderr and returnvalue.
+        sp.check_call(cmd)        
+
+        return self._setup_home()
+
+    def change_password(self, newpass):
+        """Change a users password"""
+        assert len(newpass) > 4, "Invalid password"
+        assert self._system_data is not None, "User does not exist in the system."
+        # Not needed for the installer.  Only for VASM
+        epass = self._encrypt_password(newpass)
+        assert epass != newpass, "Password cannot be encrypted correctly"
+        cmd = ["/usr/sbin/usermod", "-p", epass, self.login]
+        return sp.check_call(cmd)
+
+    def delete(self):
+        """delete this user account"""
+        # if self._data is None, this user account does not yet exist.
+
+        assert self._system_data is not None, "User not in system"
+        cmd = [ "/usr/sbin/userdel", "-r", self.login]
+        return sp.check_call(cmd)
+
+    def set_initial_group(self, grouname="users"):
+        """Set the initial group for this account.  This is normally 
+        'users' for human user accounts"""
+        assert self._system_data is not None, "User does not exist in the system."
+        cmd = [ "/usr/sbin/usermod", "-g", groupname, self.login]
+        return sp.check_call(cmd)
+
+    def set_supplementary_groups(self, grouplist=[]):
+        """Set the supplementary group memberships for this user"""
+        assert self._system_data is not None, "User does not exist in the system."
+        assert isinstance(grouplist, list), "grouplist argument must be a list"
+        cmd = [ "/usr/sbin/usermod", "-G", ",".join(grouplist), self.login ]
+        return sp.check_call(cmd)
+
+    def add_to_group(self, group):
+        """Add this user account to the specified group """
+        assert self._system_data is not None, "User not in system yet"
+        assert isinstance(group, str), "Group argument must be a string"
+        allgroups = [ g.gr_name for g in grp.getgrall() ]
+        assert group in allgroups, "Group %s does not exist in the system."% group
+        
+        cmd = [ "/usr/sbin/usermod", "-a", group, self.login ]
+        return sp.check_call(cmd)
+
+    @property
+    def _system_data(self):
+        """Return the system data related to this account"""
+        assert self.login is not None, "Login property must be set first"
+        ret = [ u for u in pwd.getpwall() if u.pw_name == self.login ]
+        if ret:
+            return ret[0]
+
+    @property
+    def _next_available_uid(self):
+        """Find the next available uid value"""
+        with Chroot(self._root):
+            ids = sorted([u.pw_uid for u in pwd.getpwall() if u.pw_uid >= 1000])
+            # FIXME: Is this fail-proof ?
+            if ids:
+                return max(ids) + 1
+            return 1000
+
+    @property
+    def _next_available_gid(self):
+        """Find the next available gid value"""
+        with Chroot(self._root):
+            gids = sorted([ g.gr_gid for g in grp.getgrall() if g.gr_gid >= 1000 ])
+            if gids: return max(gids) + 1
+            return 1000
+
+    @property
+    def uid(self):
+        """Return the uid value for this user account"""
+        if self._system_data:
+            return self._system_data.pw_uid
+        elif self.login == "root":
+            return 0
+        else:
+            # Find the next available uid
+            return self._next_available_uid
+
+    @property
+    def home(self):
+        if self._system_data:
+            return self._system_data.pw_dir
+        if self.login == "root":
+            return "/root"
+        return os.path.join("/home",self.login)
+    
+    @property
+    def gid(self):
+        """Return the gid value for this user or the next available value"""
+        if self._data:
+            return self._data.pw_gid
+        if self.login == "root":
+            return 0
+        return self._next_available_gid
+
+    @property
+    def groups(self):
+        if self._data or self._system_data:
+            # account exists, read the group list
+            return [ g.gr_name for g in grp.grpgrall() if self.login in g.gr_mem]
+        return []
+
+    @classmethod
+    def all(cls, root="/"):
+        with Chroot(root):
+            for entity in pwd.getpwall():
+                if entity.pw_uid >= 1000:
+                    account = cls()
+                    account._root = root
+                    account._data = entity
+                    account.login = entity.pw_name
+                    yield account
+
+
+class Tests(unittest.TestCase):
+    def setUp(self):
+        self.fakeuser = User()
+        self.root = "/"
+        self.fakeuser.login = "fakeuser"
+        self.fakeuser.password = "fakepass"
+        self.fakeuser._root = self.root
+
+    def test_invalid_password(self):
+        self.assertRaises(AssertionError,
+                          self.fakeuser.change_password,
+                          '')
+        for x in xrange(0,4):
+            self.assertRaises(AssertionError,
+                              self.fakeuser.change_password,
+                              "o"*x
+                              )
+            return
+
+    def test_pasword_encryption(self):
+        """Make sure password encryption works"""
+        return self.assertNotEqual(self.fakeuser.password,
+                                   self.fakeuser._encrypt_password(
+                self.fakeuser.password))
+
+    def test_delete_non_existing_account(self):
+        return self.assertRaises(AssertionError,
+                                 self.fakeuser.delete)
+
+    def test_add_user_to_nonexistant_group(self):
+        return self.assertRaises(AssertionError, self.fakeuser.add_to_group,
+                                 "fakenewgroup")
+
+    def test_add_nonexistant_user_to_group(self):
+        return self.assertRaises(AssertionError, self.fakeuser.add_to_group,
+                                 "wheel")
+
+    def test_set_supplementary_groups_argument(self):
+        return self.assertRaises(AssertionError,
+                                 self.fakeuser.set_supplementary_groups,
+                                 "foo, bar, nogroup")
+    
+    def test_setup_home_for_invalid_user(self):
+        return self.assertRaises(AssertionError,
+                                 self.fakeuser._setup_home)
+
+    def test_user_created(self):
+        self.fakeuser.create()
+        ruser = [ u for u in User.all() if u.login == self.fakeuser.login][0]
+        self.assertEqual(ruser.login, self.fakeuser.login)
+        # This should raise an exception because it already exists
+        self.assertRaises(AssertionError,
+                          self.fakeuser.create)
+        # Delete the account
+        self.fakeuser.delete()
+        
+    
+    def test_groups_retval(self):
+        return self.assertIsInstance(self.fakeuser.groups, list)
+
+    def test_uid_retval(self):
+        return self.assertIsInstance(self.fakeuser.uid, int)
+
+    def test_gid_retval(self):
+        return self.assertIsInstance(self.fakeuser.gid, int)
+
+    def test_home_retval(self):
+        return self.assertIsInstance(self.fakeuser.home, str)
+
+
+if __name__ == '__main__':
+    assert os.getuid() == 0, "Must be root to run these tests"
+    unittest.main()

vinstall/controller/automatic.py

 from vinstall.core import Render, Controller, model
 from vinstall.backend import media,  utils,  partitioning
 from vinstall.backend import fstab
+from vinstall.backend import users
+from vinstall.backend import service
+from vinstall.backend import bootloader
 import vinstall.controller.intro as intro
 import os, subprocess
 import glob
-
+import shutil
 
 class AutomaticMode(Controller):
     """Automatic install mode. We will look for devices suitable for
 
     """
     # Controller interface
+    ROOT_FS="ext3"
 
     def init(self):
         """Initialize some objects
 
         """
         self.disks = self.find_disks()
-        self.ROOT_FS = "ext3"
-        self.fstab_entries = []
 
     def render(self):
         """Show available disks for user selection
         for package in self.packages():
             yield self.installpkg, (package, disk)
         yield self.postinstall, tuple()
+        yield self.install_kernels, tuple()
+        # Bind mounts
+        yield self.set_bind_mounts, tuple()
+
+        yield self.fstab, (disk,)        
+        yield self.setup_services, tuple()
+        yield self.vector_version, tuple()
+        yield self.bootloader, (disk.path(),)
 
-        yield self.fstab, (disk,)
-        yield self.bootloader, (disk,)
+        # Clear bind mounts at the very end
+        yield self.clear_bind_mounts, tuple()
 
     def previous(self):
         """Go back to the first screen
         """Jump to the basic Setup
 
         """
+        if not self._check_continue():
+            return None
         return Setup
 
     # utility methods
         else:
             swap_size = int(available_ram / 2)
 
-        disk_size = disk.size(unit="MB")
-        root_size = disk_size - swap_size
+        disk_size = disk.size(unit="MiB")            
         partitioner = partitioning.DiskPartitioner(disk)
-        partitioner.delete_all_partitions()
-        partitioner.add_partition(size_mb = root_size)
-        partitioner.add_partition(size_mb = swap_size)
-        self.fstab_entries.append(("%s1" % disk.path(), "/", "defaults 0 1"))
-        self.fstab_entries.append(("%s2" % disk.path(), "none",
-            "swap     sw            0    0"))
+        if not disk.has_partition_table():
+            partitioner.create_partition_table()
+        else:
+            partitioner.delete_all_partitions()
+
+        root_size = disk.free_space("MiB") - swap_size
+        partitioner.add_partition(size=root_size, units='MiB')
+        # Read how much space is left on the device.
+        swap_size = disk.free_space("MiB")
+        partitioner.add_partition(size=swap_size, units='MiB')
+        partitioner.write_changes()
 
     def format_partitions(self, disk):
         """Format partitions in disk with the default filesystem
 
         """
         print "Looking for packages"
-        excluded =("vlsetup", "aaa_base")
+        excluded =("vlconfig2", "vlsetup", "aaa_base")
         for root, dirs, files in os.walk("/mnt/SOURCE/packages"):
             for file in files:
-                if file.endswith("txz"):
+                fname, fext = os.path.splitext(file)
+                if fext in (".txz",".tlz",".tgz"):
                     name = file.rsplit("-", 3)[0]
                     if name not in excluded:
                         yield os.path.join(root, file)
 
         """
         print "Installing fstab in", disk
+        if not self._check_continue():
+            return
         fstab_obj = fstab.Fstab("/mnt/TARGET/etc/fstab")
-        for item in self.fstab_entries:
-            device, mntpoint, options = item
-            entry = fstab.FstabEntry(device, mntpoint, options)
+        root_entry = fstab.FstabEntry(device = "%s1" % disk.path(),
+                                      mountpoint = "/",
+                                      filesystem = self.ROOT_FS)
+        swap_entry = fstab.FstabEntry(device = "%s2" % disk.path(),
+                                      mountpoint = "none",
+                                      filesystem="swap")
+        for entry in (root_entry, swap_entry):
             fstab_obj.add_entry(entry)
 
 
+    def set_bind_mounts(self):
+        """Bind mount some paths into the target.
+        Later steps will need this when we chroot there """
+        if not self._check_continue():
+            return
+        for point in ("sys","proc","dev"):
+            subprocess.check_call(["mount","-o","bind","/%s"% point,"/mnt/TARGET/%s"%point])
+        return
+
+    def clear_bind_mounts(self):
+        """Clear the mountpoints that were mounted for the configuration"""
+        if not self._check_continue():
+            return
+        for point in ("sys","proc","dev"):
+            subprocess.check_call(["umount","-f","/mnt/TARGET/%s"%point])
+        return
+    
+    def setup_services(self):
+        """Setup default system services"""
+        #FIXME:  Define the default set of services for each runlevel
+        # At some other location so they can be used by advanced.py too
+        
+        if not self._check_continue():
+            return
+
+        svcbasic = [ "cron","portmap","lm_sensors","gpm","inetd" ]
+        svc2 = svcbasic
+        svc3 = svcbasic
+        svc3.extend(["samba","cups","sshd"])
+        svc4 = svcbasic
+        svc4.extend(["bluetooth","fuse","cups"])
+        svc5 = svc3
+        svc5.extend(["bluetooth","fuse"])
+
+        
+        for svc in service.Service.all("/mnt/TARGET"):
+            for model, runlevel in ((svc2, 2), (svc3, 3), (svc4, 4), (svc5, 5)):
+                if svc.name in model:
+                    svc.enable(runlevel)
+        return
+
+
     def bootloader(self, disk):
         """Install the default bootloader in disk
 
         """
+        if not self._check_continue():
+            return
+
         print "Installing bootloader in", disk
+	print " installing to MBR in ", disk
+        vga_size = "800x600" # screen resolution
+        vga_quality = "16" # color depth
+        timeout = 5 # seconds
+        grub = bootloader.Grub2(target=disk,
+                               vga_size = vga_size,
+                               vga_quality = vga_quality,
+                               timeout = timeout)
+        with utils.Chroot("/mnt/TARGET"):
+            grub.write_config("/etc/default/grub")
+            grub.install()
+
+    def install_kernels(self):
+        """Copy the kernels from the initrd to the target"""
+        # kernel is in the ISO as 'sata' in isolinux/kernel/
+        # Represented in VINSTALL.INI as "sata=version"
+
+        src = self.config["install_media"]
+        kver = src.config.get("kernels", "sata").replace("\"","")
+        ksrc = os.path.join('/mnt', 'SOURCE', 'isolinux','kernel', 'sata')
+        ktgt = os.path.join('/mnt', 'TARGET', 'boot', 'vmlinuz-%s'% kver)
+        shutil.copyfile(ksrc, ktgt)
+        
+    def vector_version(self):
+        """write /etc/vector-version based on the information
+        in VINSTALL.INI"""
+
+        print "Writing vector-version"
+        installed = self.config["install_media"]
+        distro = installed.config.get("general", "distro").replace("\"","")
+        version = installed.config.get("general", "version").replace("\"","")
+        build_date = installed.config.get("general","build_date").replace("\"","")
+        vecver =  "%s built on %s"% (version, build_date)
+        f = open("/mnt/TARGET/etc/vector-version", 'w')
+        f.writelines([vecver, "\n"])
+        f.close()
 
     def _check_continue(self):
         return 'I_MEAN_IT' in os.environ
 
+        
 
 class Setup(Controller):
     """Minimal configuration - Ask for username and passwords
 
         """
         print "Creating user and setting passwords:", username, password, rootpassword
+        yield self.set_root_password, (rootpassword,)
+        yield self.create_user_account, (username, password)
+    
+    def set_root_password(self, rootpassword):
+        """Set the root password"""
+        admin = users.User()
+        admin.root = "/mnt/TARGET"
+        admin.login = "root"
+        with utils.Chroot("/mnt/TARGET"):
+            admin.change_password(rootpassword)
+        return
+    
+    def create_user_account(self, username, userpassword):
+        """Create a user account and set its password"""
+        account = users.User()
+        account.root = "/mnt/TARGET"
+        account.login = username
+        account.password = userpassword
+        with utils.Chroot("/mnt/TARGET"):
+            account.create()
+        
+        return