#
# bootloader.py - generic boot loader handling backend for up2date and anaconda
#
# Jeremy Katz <[email protected]>
# Adrian Likins <[email protected]>
# Peter Jones <[email protected]>
#
# Copyright 2001-2005 Red Hat, Inc.
#
# This software may be freely redistributed under the terms of the GNU
# library public license.
#
# You should have received a copy of the GNU Library Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
"""Module for manipulation and creation of boot loader configurations"""

import os, sys
import stat
import time
import shutil
import butil
import lilo

import checkbootloader
import rhpl.executil
from bootloaderInfo import *


# return instance of the appropriate bootloader for our arch
def getBootloader():
   """Get the bootloader info object for your architecture"""
   if butil.getArch() == 'i386':
       return x86BootloaderInfo()
   elif butil.getArch() == 'ia64':
       return ia64BootloaderInfo()
   elif butil.getArch() == 's390' or butil.getArch() == "s390x":
       return s390BootloaderInfo()
   elif butil.getArch() == "alpha":
       return alphaBootloaderInfo()
   elif butil.getArch() == "x86_64":
       return x86BootloaderInfo()
   elif butil.getPPCMachine() == "iSeries":
       return iseriesBootloaderInfo()
   elif butil.getArch() == "ppc":
       return ppcBootloaderInfo()
   elif butil.getArch() == "sparc":
       return sparcBootloaderInfo()
   else:
       return bootloaderInfo()


# install the kernels in kernelList to your bootloader's config file
# (if needed)
# kernelList is a python list of (kernelVersion, extraInfo) tuples
# backupSuffix is the suffix to use backing up the config file if
#       we change it
# test is whether or not this is test mode
# filename is a filename to use instead of the default (testing only)
def installNewKernelImages(kernelList, backupSuffix = "rpmsave", test = 0,
                          filename = None):
   """Add the kernels in kernelList to the current boot loader config file"""

   if butil.getArch() == 'i386':
       return __installNewKernelImagesX86(kernelList, backupSuffix, test,
                                        filename)
   elif butil.getArch() == 'ia64':
       return __installNewKernelImagesIA64(kernelList, backupSuffix, test,
                                         filename)
   elif butil.getArch() == 'ppc':
       return __installNewKernelImagesPPC(kernelList, backupSuffix, test,
                                         filename)
   elif butil.getArch() == 'sparc':
       return __installNewKernelImagesSparc(kernelList, backupSuffix, test,
                                           filename)
   else:
       raise RuntimeError, "Don't know how to add new kernels for %s" % \
             (butil.getArch(),)


def __installNewKernelImagesX86(kernelList, backupSuffix, test, filename):
   theBootloader = checkbootloader.whichBootLoader()
   if theBootloader == "GRUB":
       __installNewKernelImagesX86Grub(kernelList, backupSuffix, test,
                                       filename)
   else:
       raise RuntimeError, "Cannot determine x86 bootloader in use."

def __installNewKernelImagesX86Grub(kernelList, backupSuffix, test, filename):
   # get the current default kernel in the grub config
   def getGrubDefault():
       pipe = os.popen("/sbin/grubby --default-kernel")
       ret = pipe.read()
       ret = string.strip(ret)

       return ret

   # set the default kernel in grub to the one at path
   def setGrubDefault(path, instRoot="/"):
       args = [instRoot + "/sbin/grubby", "--set-default", path]
       ret = rhpl.executil.execWithRedirect(args[0], args,
                                    stdout = None, stderr = None)

       return ret


   if not filename:
       filename = "/boot/grub/grub.conf"


   defaultImage = getGrubDefault()
   if test:
       print "defaultImage is %s" % (defaultImage,)

   # if we don't get any sort of answer back, do nothing
   if defaultImage:
       defaultType = getDefaultKernelType(defaultImage)

       # look for a kernel image of the same type
       for (newVersion, imageType) in kernelList:
           if defaultType == imageType:
               if test:
                   print "Would have set default to /boot/vmlinuz-%s" % (newVersion,)
               else:
                   setGrubDefault("/boot/vmlinuz-%s" % (newVersion,))



def __installNewKernelImagesIA64(kernelList, backupSuffix, test, filename):
   if not filename:
       filename = "/boot/efi/elilo.conf"

   config = updateLiloishConfigFile(kernelList, "/boot/efi/vmlinuz-%s",
                                    test, filename)

   backup = writeConfig(config, filename, backupSuffix, test)

def __installNewKernelImagesPPC(kernelList, backupSuffix, test, filename):
   if not filename:
       filename = "/etc/yaboot.conf"

   config = updateLiloishConfigFile(kernelList, "/boot/vmlinu-%s",
                                    test, filename)

   backup = writeConfig(config, filename, backupSuffix, test)

   ret = yabootInstall("/")
   if ret:
       restoreBackup(filename, backup)
       raise RuntimeError, "Real install of yaboot failed"

def __installNewKernelImagesSparc(kernelList, backupSuffix, test, filename):
   if not filename:
       filename = "/etc/silo.conf"

   config = updateLiloishConfigFile(kernelList, "/boot/vmlinuz-%s",
                                   test, filename)

   backup = writeConfig(config, filename, backupSuffix, test)

   ret = siloInstall("/")
   if ret:
       restoreBackup(filename, backup)
       raise RuntimeError, "Real install of silo failed"

# used for updating lilo-ish config files (eg elilo as well)
def updateLiloishConfigFile(kernelList, kernelPathFormat, test, configFile):
   config = lilo.LiloConfigFile()

   # XXX should be able to create if one doesn't exist
   if not os.access(configFile, os.R_OK):
       return None

   # read in the config file and make sure we don't have any unsupported
   # options
   config.read(configFile)
   if len(config.unsupported):
       raise RuntimeError, ("Unsupported options in config file: %s" %
                          (config.unsupported,))

   default = config.getDefaultLinux()
   if not default:
       raise RuntimeError, "Unable to find default linux entry"

   realDefault = config.getDefault()
   setdefault = None

   defaultType = getDefaultKernelType(default.path)
   rootDev = default.getEntry("root")

   for (newVersion, imageType) in kernelList:
       path = kernelPathFormat % (newVersion,)

       initrd = makeInitrd("-%s" % (newVersion,), "/")

       if not os.access(initrd, os.R_OK):
           initrd = None

       if imageType and imageType != defaultType:
           # linux-smp.linux-BOOT, etc
           label = "linux-"+imageType
       elif not imageType and defaultType:
           label = "linux-up"
       else:
           label = "linux"

       if label in config.listImages():
           backup = backupLabel(label, config.listImages())
           oldImage = config.getImage(label)[1]
           oldImage.addEntry("label", backup)
           if test:
               print "Renamed %s to %s" % (label, backup)

       # alikins did this once, I'm not doing it again - katzj
       if defaultType:
           if imageType:
               if defaultType == imageType:
                   setdefault = label
       else:
           if not imageType:
               setdefault = label

       addImage(path, initrd, label, config, default)

   # if the default linux image's label is the same as the real default's
   # label, then set what we've figured out as the default to be the
   # default
   if (default.getEntry('label') == realDefault.getEntry('label')) \
          and setdefault:
       config.addEntry("default", setdefault)
   else: # make sure the default entry is explicitly set
       config.addEntry("default", realDefault.getEntry('label'))

   if test:
       print config
   return config


# path is the path to the new kernel image
# initrd is the path to the initrd (or None if it doesn't exist)
# label is the label for the image
# config is the full LiloConfigFile object for the config file
# default is the LiloConfigFile object for the default Linux-y entry
def addImage(path, initrd, label, config, default):
   # these directives must be on a per-image basis and are non-sensical
   # otherwise
   dontCopy = ['initrd', 'alias', 'label']

   entry = lilo.LiloConfigFile(imageType = "image", path = path)

   if label:
       entry.addEntry("label", label)
   else:
       raise RuntimeError, "Unable to determine a label for %s.  Aborting" % (path,)

   if initrd:
       entry.addEntry("initrd", initrd)

   # go through all of the things listed for the default
   # config entry and if they're not in our blacklist
   # of stuff not to copy, go ahead and copy it
   entries = default.listEntries()
   for key in entries.keys():
       if key in dontCopy:
           pass
       else:
           entry.addEntry(key, default.getEntry(key))

   config.addImage(entry, 1)


# note that this function no longer actually creates an initrd.
# the kernel's %post does this now
def makeInitrd (kernelTag, instRoot):
   if butil.getArch() == 'ia64':
       initrd = "/boot/efi/EFI/redhat/initrd%s.img" % (kernelTag, )
   else:
       initrd = "/boot/initrd%s.img" % (kernelTag, )

   return initrd


# determine the type of the default kernel
# kernelPath is the path of the current default
def getDefaultKernelType(kernelPath):
   # XXX this isn't an ideal way to do this.  up2date was poking at the
   # rpmdb.  this is a simple but stupid way to figure out the simple
   # cases for now
   defaultType = None
   if kernelPath[-3:] == "smp":
       defaultType = "smp"
   elif kernelPath[-10:] == "enterprise":
       defaultType = "enterprise"
   elif kernelPath[-4:] == "BOOT":
       defaultType = "BOOT"
   elif kernelPath[-3:] == "ans":
       defaultType = "ans"
   elif kernelPath[-4:] == "briq":
       defaultType = "briq"
   elif kernelPath[-5:] == "teron":
       defaultType = "teron"
   elif kernelPath[-7:] == "pseries":
       defaultType = "pseries"
   elif kernelPath[-7:] == "iseries":
       defaultType = "iseries"
   else:
       defaultType = None

   return defaultType


# make a silly .bak entry
def backupLabel(label, imageList):
   backup = "%s.bak" % (label,)
   while backup in imageList:
       backup = backup + "_"

   # truncate long names.
   if len(backup) > 32:
       backup = backup[:28]+".bak"

   if backup in imageList:
       raise RuntimeError, "Attempts to create unique backup label for %s failed" % (label,)

   return backup


def writeConfig(config, filename, backupSuffix, test = 0):
   # write out the LILO config
   try:
       backup = backupFile(filename, backupSuffix)
       liloperms = stat.S_IMODE(os.stat(filename)[stat.ST_MODE])
       if test:
           filename = "/tmp/lilo.conf"

       config.write(filename, perms = liloperms)
   except:
       restoreBackup(filename, backup)
       raise RuntimeError, "Error installing updated config file.  Aborting"

   return backup


def backupFile(filename, suffix):
   backup = "%s.%s-%s" % (filename, suffix, repr(time.time()))

   # make sure the backup file doesn't exist... add _'s until it doesn't
   while os.access(backup, os.F_OK):
       backup = backup + "_"

   shutil.copy(filename, backup)

   return backup


def restoreBackup(filename, backup):
   shutil.copy(backup, filename)
   os.remove(backup)


def liloInstall(instRoot, test = 0, filename = None, testFlag = 0):
   args = [ instRoot + "/sbin/lilo", "-r", instRoot ]

   if test:
       args.extend(["-t"])

   if testFlag:
       print args
       ret = 0
   else:
       ret = rhpl.executil.execWithRedirect(args[0], args,
                                    stdout = None, stderr = None)

   return ret

def yabootInstall(instRoot, filename = None):
   args = [ instRoot + "/usr/sbin/ybin", "-r", instRoot ]

   ret = rhpl.executil.execWithRedirect(args[0], args,
                                        stdout = None, stderr = None)

   return ret

def siloInstall(instRoot, filename = None):
   args = [instRoot + "/sbin/silo", "-r", instRoot]

   ret = rhpl.executil.execWithRedirect(args[0], args,
                                        stdout = None, stderr = None)

   return ret