[PATCH 2/2] Add kickstart liveimg install command

Brian C. Lane bcl at redhat.com
Fri May 3 00:54:11 UTC 2013


From: "Brian C. Lane" <bcl at redhat.com>

This implements a new kickstart install command:

liveimg --url=<url> [--proxy=<proxy>] [--checksum=<sha256>] [--noverifyssl]

This will skip source and software spoke, partition the drive and then
download the image from the url location to the disk. http, https, ftp
and file methods are supported. An optional proxy can be specified,
including auth. If --checksum is passed the image checksum is confirmed
before starting to copy the files to the target system and --noverifyssl
skips checking the SSL cert when using https.

The disk images need to be able to be mounted by the installer image.
eg. extX, squashfs. If the image contains a LiveOS directory with an
image file ending in .img inside it (eg. like a live iso squashfs.img)
it will mount the inner image as the source and copy the files from
there.

eg. squashfs.img -> LiveOS/ext3fs.img -> root filesystem

Note that the system being installed *must* have the various utilities
that Anaconda expect to be installed in a normal system. This list may
change at any time, it is up to the image creator to confirm that they
have everything Anaconda needs to complete the post-install setup.
---
 anaconda.spec.in                     |   2 +-
 pyanaconda/__init__.py               |   3 +
 pyanaconda/constants.py              |   1 +
 pyanaconda/packaging/livepayload.py  | 226 +++++++++++++++++++++++++++++++++--
 pyanaconda/ui/gui/spokes/software.py |   2 +-
 pyanaconda/ui/gui/spokes/source.py   |   2 +-
 6 files changed, 224 insertions(+), 12 deletions(-)

diff --git a/anaconda.spec.in b/anaconda.spec.in
index 84161d9..4a08d0e 100644
--- a/anaconda.spec.in
+++ b/anaconda.spec.in
@@ -21,7 +21,7 @@ Source0: %{name}-%{version}.tar.bz2
 %define gconfversion 2.28.1
 %define intltoolver 0.31.2-3
 %define libnlver 1.0
-%define pykickstartver 1.99.28
+%define pykickstartver 1.99.29
 %define yumver 3.4.3-32
 %define partedver 1.8.1
 %define pypartedver 2.5-2
diff --git a/pyanaconda/__init__.py b/pyanaconda/__init__.py
index 7804d16..02e7d1c 100644
--- a/pyanaconda/__init__.py
+++ b/pyanaconda/__init__.py
@@ -129,6 +129,9 @@ class Anaconda(object):
                 if flags.livecdInstall:
                     from pyanaconda.packaging.livepayload import LiveImagePayload
                     klass = LiveImagePayload
+                elif self.ksdata.method.method == "liveimg":
+                    from pyanaconda.packaging.livepayload import LiveImageKSPayload
+                    klass = LiveImageKSPayload
                 else:
                     from pyanaconda.packaging.yumpayload import YumPayload
                     klass = YumPayload
diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
index 7adb9b6..b4922d6 100644
--- a/pyanaconda/constants.py
+++ b/pyanaconda/constants.py
@@ -83,6 +83,7 @@ MOUNT_DIR = "/mnt/install"
 DRACUT_REPODIR = "/run/install/repo"
 DRACUT_ISODIR = "/run/install/source"
 ISO_DIR = MOUNT_DIR + "/isodir"
+IMAGE_DIR = MOUNT_DIR + "/image"
 INSTALL_TREE = MOUNT_DIR + "/source"
 BASE_REPO_NAME = "anaconda"
 
diff --git a/pyanaconda/packaging/livepayload.py b/pyanaconda/packaging/livepayload.py
index 1fdfff2..f0e352f 100644
--- a/pyanaconda/packaging/livepayload.py
+++ b/pyanaconda/packaging/livepayload.py
@@ -33,18 +33,24 @@ import os
 import stat
 from time import sleep
 from threading import Lock
+from urlgrabber.grabber import URLGrabber
+from urlgrabber.grabber import URLGrabError
+from pyanaconda.iutil import ProxyString, ProxyStringError
+import urllib
+import hashlib
+import glob
 
-from . import *
+from . import ImagePayload, PayloadSetupError, PayloadInstallError
 
-from pyanaconda.constants import *
-from pyanaconda.flags import flags
+from pyanaconda.constants import INSTALL_TREE, ROOT_PATH, THREAD_LIVE_PROGRESS
+from pyanaconda.constants import IMAGE_DIR
 
 from pyanaconda import iutil
 
 import logging
-log = logging.getLogger("anaconda")
+log = logging.getLogger("packaging")
 
-from pyanaconda.errors import *
+from pyanaconda.errors import errorHandler, ERROR_RAISE
 from pyanaconda.progress import progressQ
 from blivet.size import Size
 import blivet.util
@@ -55,6 +61,11 @@ _ = lambda x: gettext.ldgettext("anaconda", x)
 
 class LiveImagePayload(ImagePayload):
     """ A LivePayload copies the source image onto the target system. """
+    def __init__(self, *args, **kwargs):
+        super(LiveImagePayload, self).__init__(*args, **kwargs)
+        # Used to adjust size of ROOT_PATH when files are already present
+        self._adj_size = 0
+
     def setup(self, storage):
         super(LiveImagePayload, self).setup(storage)
 
@@ -78,16 +89,21 @@ class LiveImagePayload(ImagePayload):
         source = os.statvfs(INSTALL_TREE)
         source_size = source.f_frsize * (source.f_blocks - source.f_bfree)
         mountpoints = self.storage.mountpoints.copy()
+        last_pct = -1
         while self.pct < 100:
             dest_size = 0
             for mnt in mountpoints:
                 mnt_stat = os.statvfs(ROOT_PATH+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
 
-            with self.pct_lock:
-                self.pct = int(100 * dest_size / source_size)
-
-            progressQ.send_message(_("Installing software") + (" %d%%") % (min(100,self.pct),))
+            pct = int(100 * dest_size / source_size)
+            if pct != last_pct:
+                with self.pct_lock:
+                    self.pct = pct
+                last_pct = pct
+                progressQ.send_message(_("Installing software") + (" %d%%") % (min(100, self.pct),))
             sleep(0.777)
 
     def install(self):
@@ -136,3 +152,195 @@ class LiveImagePayload(ImagePayload):
     @property
     def spaceRequired(self):
         return Size(bytes=iutil.getDirSize("/")*1024)
+
+class URLGrabberProgress(object):
+    """ Provide methods for urlgrabber progress."""
+    def start(self, filename, url, basename, size, text):
+        """ Start of urlgrabber download
+
+            :param filename: path and file that download will be saved to
+            :type filename:  string
+            :param url:      url to download from
+            :type url:       string
+            :param basename: file that it will be saved to
+            :type basename:  string
+            :param size:     length of the file
+            :type size:      int
+            :param text:     unknown
+            :type text:      unknown
+        """
+        self.filename = filename
+        self.url = url
+        self.basename = basename
+        self.size = size
+        self.text = text
+        self._pct = -1
+
+    def update(self, bytes_read):
+        """ Download update
+
+            :param bytes_read: Bytes read so far
+            :type bytes_read:  int
+        """
+        if not bytes_read:
+            return
+        pct = min(100, int(100 * bytes_read / self.size))
+
+        if pct == self._pct:
+            return
+        self._pct = pct
+
+        progressQ.send_message(_("Downloading %(url)s (%(pct)d%%)") % \
+                {"url" : self.url, "pct" : pct})
+
+    def end(self, bytes_read):
+        """ Download complete
+
+            :param bytes_read: Bytes read so far
+            :type bytes_read:  int
+        """
+        progressQ.send_message(_("Downloading %(url)s (%(pct)d%%)") % \
+                {"url" : self.url, "pct" : 100})
+
+class LiveImageKSPayload(LiveImagePayload):
+    """ Install using a live filesystem image from the network """
+    def __init__(self, *args, **kwargs):
+        super(LiveImageKSPayload, self).__init__(*args, **kwargs)
+        self._min_size = 0
+        self.image_path = ROOT_PATH+"/disk.img"
+
+    def setup(self, storage):
+        """ Check the availability and size of the image.
+        """
+        super(LiveImagePayload, self).setup(storage)
+
+        self._proxies = {}
+        if self.data.method.proxy:
+            try:
+                proxy = ProxyString(self.data.method.proxy)
+                self._proxies = {"http": proxy.url,
+                                 "https": proxy.url}
+            except ProxyStringError as e:
+                log.info("Failed to parse proxy for liveimg --proxy=\"%s\": %s" \
+                         % (self.data.method.proxy, e))
+
+        error = None
+        try:
+            req = urllib.urlopen(self.data.method.url, proxies=self._proxies)
+        except IOError as e:
+            log.error("Error opening liveimg: %s" % e)
+            error = e
+        else:
+            # If it is a http request we need to check the code
+            method, x = self.data.method.url.split(":", 1)
+            if method.startswith("http") and req.getcode() != 200:
+                error = "http request returned %s" % req.getcode()
+
+        if error:
+            exn = PayloadInstallError(str(error))
+            if errorHandler.cb(exn) == ERROR_RAISE:
+                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"):
+            self._min_size = int(req.info().get("content-length")) * 4
+
+        log.debug("liveimg size is %s" % self._min_size)
+
+    def preInstall(self, *args, **kwargs):
+        """ Download image and loopback mount it.
+
+            This is called after partitioning is setup, we now have space
+            to grab the image. Download it to ROOT_PATH and provide feedback
+            during the download (using urlgrabber callback).
+        """
+        # Setup urlgrabber and call back to download image to ROOT_PATH
+        progress = URLGrabberProgress()
+        ugopts = {"ssl_verify_peer": not self.data.method.noverifyssl,
+                  "ssl_verify_host": not self.data.method.noverifyssl,
+                  "proxies" : self._proxies,
+                  "progress_obj" : progress,
+                  "copy_local" : True}
+
+        error = None
+        try:
+            ug = URLGrabber()
+            ug.urlgrab(self.data.method.url, 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.data.method.url
+                log.error(error)
+
+        if error:
+            exn = PayloadInstallError(str(error))
+            if errorHandler.cb(exn) == ERROR_RAISE:
+                raise exn
+
+        # Used to make install progress % look correct
+        self._adj_size = os.stat(self.image_path)[stat.ST_SIZE]
+
+        if self.data.method.checksum:
+            progressQ.send_message(_("Checking image checksum"))
+            sha256 = hashlib.sha256()
+            with open(self.image_path, "rb") as f:
+                while True:
+                    data = f.read(1024*1024)
+                    if not data:
+                        break
+                    sha256.update(data)
+            filesum = sha256.hexdigest()
+            log.debug("sha256 of %s is %s" % (self.data.method.url, filesum))
+
+            if self.data.method.checksum.lower() != filesum:
+                log.error("%s does not match checksum." % self.data.method.checksum)
+                exn = PayloadInstallError("Checksum of image does not match")
+                if errorHandler.cb(exn) == ERROR_RAISE:
+                    raise exn
+
+        # Mount the image and check to see if it is a LiveOS/*.img
+        # style squashfs image. If so, move it to IMAGE_DIR and mount the real
+        # root image on INSTALL_TREE
+        blivet.util.mount(self.image_path, INSTALL_TREE, fstype="auto", options="ro")
+        if os.path.exists(INSTALL_TREE+"/LiveOS"):
+            # Find the first .img in the directory and mount that on INSTALL_TREE
+            img_files = glob.glob(INSTALL_TREE+"/LiveOS/*.img")
+            if img_files:
+                img_file = os.path.basename(sorted(img_files)[0])
+
+                # move the mount to IMAGE_DIR
+                os.makedirs(IMAGE_DIR, 0755)
+                # work around inability to move shared filesystems
+                iutil.execWithRedirect("mount",
+                                       ["--make-rprivate", "/"])
+                iutil.execWithRedirect("mount",
+                                       ["--move", INSTALL_TREE, IMAGE_DIR])
+                blivet.util.mount(IMAGE_DIR+"/LiveOS/"+img_file, INSTALL_TREE,
+                                  fstype="auto", options="ro")
+
+    def postInstall(self):
+        """ Unmount image, remove image file from target
+        """
+        super(LiveImageKSPayload, self).postInstall()
+
+        if os.path.exists(IMAGE_DIR+"/LiveOS"):
+            blivet.util.umount(IMAGE_DIR)
+
+        if os.path.exists(self.image_path):
+            os.unlink(self.image_path)
+
+    @property
+    def spaceRequired(self):
+        """ We don't know the filesystem size until it is downloaded.
+
+            Default to 1G which should be enough for a minimal image download
+            and install.
+        """
+        if self._min_size:
+            return Size(bytes=self._min_size)
+        else:
+            return Size(bytes=1024*1024*1024)
diff --git a/pyanaconda/ui/gui/spokes/software.py b/pyanaconda/ui/gui/spokes/software.py
index b409aea..675b256 100644
--- a/pyanaconda/ui/gui/spokes/software.py
+++ b/pyanaconda/ui/gui/spokes/software.py
@@ -156,7 +156,7 @@ class SoftwareSelectionSpoke(NormalSpoke):
 
     @property
     def showable(self):
-        return not flags.livecdInstall
+        return not flags.livecdInstall and not self.data.method.method == "liveimg"
 
     @property
     def status(self):
diff --git a/pyanaconda/ui/gui/spokes/source.py b/pyanaconda/ui/gui/spokes/source.py
index e9ecc7c..401f0fc 100644
--- a/pyanaconda/ui/gui/spokes/source.py
+++ b/pyanaconda/ui/gui/spokes/source.py
@@ -692,7 +692,7 @@ class SourceSpoke(NormalSpoke):
 
     @property
     def showable(self):
-        return not flags.livecdInstall
+        return not flags.livecdInstall and not self.data.method.method == "liveimg"
 
     def _mirror_active(self):
         return self._protocolComboBox.get_active() == PROTOCOL_MIRROR
-- 
1.8.1.4



More information about the anaconda-patches mailing list