From b3800e02363bdd760cf1b61245cb4c2804a9f9c9 Mon Sep 17 00:00:00 2001
From: Arun S A G <sagarun@gmail.com>
Date: Tue, 21 Apr 2015 12:23:09 -0700
Subject: [PATCH] Add support for tarpayload

---
 flags.py                 |  51 ++++----
 installclasses/fedora.py |   3 +
 installclasses/rhel.py   |   3 +
 tarpayload.py            | 297 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 331 insertions(+), 23 deletions(-)
 create mode 100644 tarpayload.py

diff --git a/flags.py b/flags.py
index 1c1dca8..c6b7a66 100644
--- a/flags.py
+++ b/flags.py
@@ -25,16 +25,16 @@ from constants import *
 class Flags:
 
     def __getattr__(self, attr):
-	if self.__dict__['flags'].has_key(attr):
-	    return self.__dict__['flags'][attr]
+        if self.__dict__['flags'].has_key(attr):
+            return self.__dict__['flags'][attr]
 
-	raise AttributeError, attr
+        raise AttributeError, attr
 
     def __setattr__(self, attr, val):
-	if self.__dict__['flags'].has_key(attr):
-	    self.__dict__['flags'][attr] = val
-	else:
-	    raise AttributeError, attr
+        if self.__dict__['flags'].has_key(attr):
+            self.__dict__['flags'][attr] = val
+        else:
+            raise AttributeError, attr
 
     def get(self, attr, val=None):
         if self.__dict__['flags'].has_key(attr):
@@ -73,24 +73,24 @@ class Flags:
                 and not self.__dict__['flags']['cmdline'].has_key("no" + flag) \
                 and self.__dict__['flags']['cmdline'][flag] != "0":
             self.__dict__['flags'][flag] = 1
-	
+
     def __init__(self):
-	self.__dict__['flags'] = {}
-	self.__dict__['flags']['test'] = 0
-	self.__dict__['flags']['livecdInstall'] = 0
+        self.__dict__['flags'] = {}
+        self.__dict__['flags']['test'] = 0
+        self.__dict__['flags']['livecdInstall'] = 0
         self.__dict__['flags']['dlabel'] = 0
-	self.__dict__['flags']['ibft'] = 1
-	self.__dict__['flags']['iscsi'] = 0
-	self.__dict__['flags']['serial'] = 0
-	self.__dict__['flags']['autostep'] = 0
-	self.__dict__['flags']['autoscreenshot'] = 0
-	self.__dict__['flags']['usevnc'] = 0
-	self.__dict__['flags']['vncquestion'] = True
+        self.__dict__['flags']['ibft'] = 1
+        self.__dict__['flags']['iscsi'] = 0
+        self.__dict__['flags']['serial'] = 0
+        self.__dict__['flags']['autostep'] = 0
+        self.__dict__['flags']['autoscreenshot'] = 0
+        self.__dict__['flags']['usevnc'] = 0
+        self.__dict__['flags']['vncquestion'] = True
         self.__dict__['flags']['mpath'] = 1
-	self.__dict__['flags']['dmraid'] = 1
-	self.__dict__['flags']['selinux'] = SELINUX_DEFAULT
+        self.__dict__['flags']['dmraid'] = 1
+        self.__dict__['flags']['selinux'] = SELINUX_DEFAULT
         self.__dict__['flags']['debug'] = 0
-	self.__dict__['flags']['targetarch'] = None
+        self.__dict__['flags']['targetarch'] = None
         self.__dict__['flags']['cmdline'] = self.createCmdlineDict()
         self.__dict__['flags']['useIPv4'] = True
         self.__dict__['flags']['useIPv6'] = True
@@ -104,6 +104,11 @@ class Flags:
         # device is
         self.__dict__['flags']['virtpconsole'] = None
 
+        if self.__dict__['flags']['cmdline'].has_key("tarpayload"):
+            self.__dict__['flags']['tarpayload'] = self.__dict__['flags']['cmdline']['tarpayload']
+        else:
+            self.__dict__['flags']['tarpayload'] = None
+
         for x in ['selinux']:
             if self.__dict__['flags']['cmdline'].has_key(x):
                 if self.__dict__['flags']['cmdline'][x]:
@@ -117,11 +122,11 @@ class Flags:
             self.__dict__['flags']['debug'] = self.__dict__['flags']['cmdline']['debug']
 
         if self.__dict__['flags']['cmdline'].has_key("rpmarch"):
-            self.__dict__['flags']['targetarch'] = self.__dict__['flags']['cmdline']['rpmarch']             
+            self.__dict__['flags']['targetarch'] = self.__dict__['flags']['cmdline']['rpmarch']
 
         if not os.path.exists("/selinux/load"):
             self.__dict__['flags']['selinux'] = 0
 
-                
+
 global flags
 flags = Flags()
diff --git a/installclasses/fedora.py b/installclasses/fedora.py
index 678cba9..ade0667 100644
--- a/installclasses/fedora.py
+++ b/installclasses/fedora.py
@@ -86,6 +86,9 @@ class InstallClass(BaseInstallClass):
         if flags.livecdInstall:
             import livecd
             return livecd.LiveCDCopyBackend
+        elif flags.tarpayload:
+            import tarpayload
+            return tarpayload.TarBackend
         else:
             return yuminstall.YumBackend
 
diff --git a/installclasses/rhel.py b/installclasses/rhel.py
index 8e30d88..eb6ab7c 100644
--- a/installclasses/rhel.py
+++ b/installclasses/rhel.py
@@ -64,6 +64,9 @@ class InstallClass(BaseInstallClass):
         if flags.livecdInstall:
             import livecd
             return livecd.LiveCDCopyBackend
+        elif flags.tarpayload:
+            import tarpayload
+            return tarpayload.TarBackend
         else:
             return yuminstall.YumBackend
 
diff --git a/tarpayload.py b/tarpayload.py
new file mode 100644
index 0000000..65eccb1
--- /dev/null
+++ b/tarpayload.py
@@ -0,0 +1,297 @@
+#
+# tarpayload.py: An anaconda backend to do an install from a tarball
+#
+# The basic idea is that with a tarball, we already have an install
+# and should be able to just copy those bits over to the disk.  So we dd
+# the image, move things to the "right" filesystem as needed/
+#
+# Copyright (C) 2015  Yahoo Inc.  All rights reserved.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author(s):  Arun S A G <sagaun@gmail.com>
+#
+
+import os, sys
+import stat
+import threading
+from time import sleep
+from urlgrabber.progress import text_progress_meter
+from urlgrabber.grabber import URLGrabber, URLGrabError
+import urllib
+
+from flags import flags
+from constants import *
+import storage
+
+
+import gettext
+_ = lambda x: gettext.ldgettext("anaconda", x)
+
+import backend
+import isys
+import iutil
+
+import packages
+import logging
+log = logging.getLogger("anaconda")
+
+
+THREAD_NAME = "TAR_PROGRESS"
+
+class PayloadSetupError(Exception):
+    pass
+
+class TarBackend(backend.AnacondaBackend):
+    def __init__(self, anaconda):
+        backend.AnacondaBackend.__init__(self, anaconda)
+        self.supportsUpgrades = False
+        self.supportsPackageSelection = False
+        self.skipFormatRoot = True
+        self._min_size = 0
+        self.tarballURL = flags.tarpayload
+        self.image_path = anaconda.rootPath + "/disk.img"
+        self.pct = 0
+        self.source_size = 1
+        self.pct_lock = None
+
+    def _getTarSize(self):
+        tar_size = 0
+        error = None
+        try:
+            req = urllib.urlopen(self.tarballURL)
+        except IOError as e:
+            log.error("Error opening liveimg: %s", e)
+            error = e
+
+        # If it is a http request we need to check the code
+        method = self.tarballURL.split(":", 1)[0]
+        if method.startswith("http") and req.getcode() != 200:
+            error = "http request returned %s" % req.getcode()
+
+        if error:
+            exn = PayloadSetupError(str(error))
+            raise exn
+
+        # At this point we know we can get the image and what its size is
+        # Make a guess as to minimum size needed:
+        # Enough space for image and image * 3
+        if req.info().get("content-length"):
+            tar_size = int(req.info().get("content-length"))
+            self._min_size = tar_size * 4
+            log.debug("liveimg size is %s", self._min_size)
+
+        return tar_size
+
+    def _getTarSizeMB(self):
+        return self._getTarSize() / 1048576
+
+    def _unmountNonFstabDirs(self, anaconda):
+        # unmount things that aren't listed in /etc/fstab.  *sigh*
+        dirs = []
+        if flags.selinux:
+            dirs.append("/selinux")
+        for dir in dirs:
+            try:
+                isys.umount("%s/%s" %(anaconda.rootPath,dir), removeDir = False)
+            except Exception, e:
+                log.error("unable to unmount %s: %s" %(dir, e))
+
+    def postAction(self, anaconda):
+        self._unmountNonFstabDirs(anaconda)
+        try:
+            anaconda.id.storage.umountFilesystems(swapoff = False)
+            os.rmdir(anaconda.rootPath)
+        except Exception, e:
+            log.error("Unable to unmount filesystems: %s" % e)
+
+    def doPreInstall(self, anaconda):
+        """ Download the tarball locally """
+        error = None
+        self._unmountNonFstabDirs(anaconda)
+        anaconda.id.storage.umountFilesystems(swapoff = False)
+        anaconda.id.storage.mountFilesystems()
+        progress = anaconda.id.instProgress
+        progress.set_label(_("Downloading image to hard drive."))
+        progress.processEvents()
+        ugopts = {"progress_obj" : text_progress_meter(),
+                  "copy_local" : True}
+        try:
+            ug = URLGrabber()
+            ug.urlgrab(self.tarballURL, self.image_path, **ugopts)
+        except URLGrabError as e:
+            log.error("Error downloading liveimg: %s", e)
+            error = e
+        else:
+            if not os.path.exists(self.image_path):
+                error = "Failed to download %s, file doesn't exist" % self.self.tarballURL
+                log.error(error)
+
+        if error:
+            exn = PayloadInstallError(str(error))
+            raise exn
+
+        # Used to make install progress % look correct
+        self._adj_size = os.stat(self.image_path)[stat.ST_SIZE]
+
+    def _progress(self, anaconda):
+        """Monitor the amount of disk space used on the target and source and
+           update the hub's progress bar.
+        """
+        mountpoints = anaconda.id.storage.mountpoints.copy()
+        last_pct = -1
+        while self.pct < 100:
+            dest_size = 0
+            for mnt in mountpoints:
+                mnt_stat = os.statvfs(anaconda.rootPath + mnt)
+                dest_size += mnt_stat.f_frsize * (mnt_stat.f_blocks - mnt_stat.f_bfree)
+            if dest_size >= self._adj_size:
+                dest_size -= self._adj_size
+
+            pct = int(100 * dest_size / self.source_size)
+            if pct != last_pct:
+                with self.pct_lock:
+                    self.pct = pct
+                last_pct = pct
+                print(("Installing software") + (" %d%%") % (min(100, self.pct),))
+            sleep(0.777)
+
+    def doInstall(self, anaconda):
+        log.info("Preparing to install packages")
+        progress = anaconda.id.instProgress
+        progress.set_label(_("Copying tarball to hard drive."))
+        progress.processEvents()
+        self.source_size = os.stat(self.image_path)[stat.ST_SIZE] * 2
+        self.pct_lock = threading.Lock()
+        th = threading.Thread(name=THREAD_NAME,
+                              target=self._progress,
+                              args=(anaconda,))
+        th.start()
+        cmd = "tar"
+        # preserve: ACL's, xattrs, and SELinux context
+        args = ["--selinux", "--xattrs", "--no-acls",
+                "--exclude", "/dev/", "--exclude", "/proc/",
+                "--exclude", "/sys/", "--exclude", "/run/", "--exclude", "/boot/*rescue*",
+                "-xaf", self.image_path, "-C", anaconda.rootPath]
+        try:
+            rc = iutil.execWithRedirect(cmd, args)
+        except (OSError, RuntimeError) as e:
+            msg = None
+            err = str(e)
+            log.error(err)
+        else:
+            err = None
+            msg = "%s exited with code %d" % (cmd, rc)
+            log.info(msg)
+
+        if err:
+            exn = PayloadInstallError(err or msg)
+            raise exn
+
+        isys.sync()
+        isys.sync()
+        th.join()
+        anaconda.id.instProgress = None
+
+    def doPostInstall(self, anaconda):
+        if os.path.exists(self.image_path):
+            os.unlink(self.image_path)
+
+        storage.writeEscrowPackets(anaconda)
+
+        packages.rpmSetupGraphicalSystem(anaconda)
+
+        # now write out the "real" fstab and mtab
+        anaconda.id.storage.write(anaconda.rootPath)
+        f = open(anaconda.rootPath + "/etc/mtab", "w+")
+        f.write(anaconda.id.storage.mtab)
+        f.close()
+
+        # copy over the modprobe.conf
+        if os.path.exists("/etc/modprobe.conf"):
+            shutil.copyfile("/etc/modprobe.conf",
+                            anaconda.rootPath + "/etc/modprobe.conf")
+        # set the same keyboard the user selected in the keyboard dialog:
+        anaconda.id.keyboard.write(anaconda.rootPath)
+
+        # rebuild the initrd(s)
+        vers = self.kernelVersionList(anaconda.rootPath)
+        log.info("Kernel versions %s" % vers)
+        for (v, arch, n) in vers:
+            packages.recreateInitrd(v, anaconda.rootPath)
+
+    def writeConfiguration(self):
+        pass
+
+    def kernelVersionList(self, rootPath = "/"):
+        vers = []
+        import glob
+        cmpfunc = cmp
+
+        files = glob.glob(rootPath + "/boot/vmlinuz-*")
+        files.extend(glob.glob(rootPath + "/boot/efi/EFI/redhat/vmlinuz-*"))
+        # strip off everything up to and including vmlinuz- to get versions
+        # Ignore rescue kernels
+        versions = [f.split("/")[-1][8:] for f in files if os.path.isfile(f) \
+                    and "-rescue-" not in f]
+        versions.sort(cmp=cmpfunc)
+        # Gross hack - figure out why rpmdb fails
+        for ver in versions:
+            vers.append((ver, "x86_64", "base"))
+
+        log.debug("kernel versions: %s", vers)
+        return vers
+
+    def getMinimumSizeMB(self, part):
+        if part == "/":
+            return self._getTarSizeMB()
+        return 0
+
+    def doBackendSetup(self, anaconda):
+        # ensure there's enough space on the rootfs
+        # FIXME: really, this should be in the general sanity checking, but
+        # trying to weave that in is a little tricky at present.
+        ossize = self._getTarSizeMB()
+        slash = anaconda.id.storage.rootDevice
+        if slash.size < ossize:
+            rc = anaconda.intf.messageWindow(_("Error"),
+                                        _("The root filesystem you created is "
+                                          "not large enough for this tarball "
+                                          "image (%.2f MB required).") % ossize,
+                                        type = "custom",
+                                        custom_icon = "error",
+                                        custom_buttons=[_("_Back"),
+                                                        _("_Exit installer")])
+            if rc == 0:
+                return DISPATCH_BACK
+            else:
+                sys.exit(1)
+
+    # package/group selection doesn't apply for this backend
+    def groupExists(self, group):
+        pass
+    def selectGroup(self, group, *args):
+        pass
+    def deselectGroup(self, group, *args):
+        pass
+    def selectPackage(self, pkg, *args):
+        pass
+    def deselectPackage(self, pkg, *args):
+        pass
+    def packageExists(self, pkg):
+        return True
+    def getDefaultGroups(self, anaconda):
+        return []
+    def writePackagesKS(self, f, anaconda):
+        pass
-- 
2.1.0

