[anaconda][rhel7-branch][PATCH] Add support for autostep and --autoscreenshot (#1234896)

Vratislav Podzimek vpodzime at redhat.com
Tue Jul 28 14:11:09 UTC 2015


On Mon, 2015-07-27 at 18:55 +0200, Martin Kolman wrote:
> Add the missing support for the autostep [--autoscreenshot]
> kickstart command to Anaconda. If just autostep is specified, Anaconda will
> iterate over all active spokes just before leaving a hub.
> If the --autoscreenshot option specified, it will also take a screenshot of
> all hubs and of each spoke.
> 
> Also turn the take-screenshot code into a proper function that can
> be called when auto-screenshotting and remove the old copy-screenshot
> post-script (the last batch of screenshots is taken after the
> postscripts have run, so screenshot copying is now handled
> elsewhere).
> 
> Resolves: rhbz#1234896
> Signed-off-by: Martin Kolman <mkolman at redhat.com>
> ---
>  pyanaconda/constants.py              |   4 +
>  pyanaconda/iutil.py                  |  27 ++++++
>  pyanaconda/ui/gui/__init__.py        | 179 +++++++++++++++++++++++++++++++----
>  pyanaconda/ui/gui/hubs/__init__.py   |  46 ++++++++-
>  pyanaconda/ui/gui/spokes/__init__.py |   9 ++
>  pyanaconda/ui/gui/spokes/welcome.py  |   5 +-
>  6 files changed, 250 insertions(+), 20 deletions(-)
> 
> diff --git a/pyanaconda/constants.py b/pyanaconda/constants.py
> index 807bf0d..d9ee11f 100644
> --- a/pyanaconda/constants.py
> +++ b/pyanaconda/constants.py
> @@ -154,6 +154,10 @@ PW_ASCII_CHARS = string.digits + string.ascii_letters + string.punctuation + " "
>  # Recognizing a tarfile
>  TAR_SUFFIX = (".tar", ".tbz", ".tgz", ".txz", ".tar.bz2", "tar.gz", "tar.xz")
>  
> +# screenshots
> +SCREENSHOTS_DIRECTORY = "/tmp/anaconda-screenshots"
> +SCREENSHOTS_TARGET_DIRECTORY = "/root/anaconda-screenshots"
> +
>  # cmdline arguments that append instead of overwrite
>  CMDLINE_APPEND = ["modprobe.blacklist"]
>  
> diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py
> index c827431..9136406 100644
> --- a/pyanaconda/iutil.py
> +++ b/pyanaconda/iutil.py
> @@ -27,6 +27,7 @@ import os.path
>  import errno
>  import subprocess
>  import unicodedata
> +import shutil
>  import string
>  import tempfile
>  import types
> @@ -36,6 +37,7 @@ import signal
>  
>  from pyanaconda.flags import flags
>  from pyanaconda.constants import DRACUT_SHUTDOWN_EJECT, TRANSLATIONS_UPDATE_DIR, UNSUPPORTED_HW
> +from pyanaconda.constants import SCREENSHOTS_DIRECTORY, SCREENSHOTS_TARGET_DIRECTORY
>  from pyanaconda.regexes import URL_PARSE
>  
>  from pyanaconda.i18n import _
> @@ -1191,3 +1193,28 @@ def get_platform_groupid():
>  def parent_dir(directory):
>      """Return the parent's path"""
>      return "/".join(os.path.normpath(directory).split("/")[:-1])
> +
> +def sysroot_path(path):
> +    """Make the given relative or absolute path "sysrooted"
> +       :param str path: path to be sysrooted
> +       :returns: sysrooted path
> +       :rtype: str
> +    """
> +    return os.path.join(getSysroot(), path.lstrip(os.path.sep))
> +
> +def save_screenshots():
> +    """Save screenshots to the installed system"""
> +    if not os.path.exists(SCREENSHOTS_DIRECTORY):
> +        # there are no screenshots to copy
> +        return
> +    target_path = sysroot_path(SCREENSHOTS_TARGET_DIRECTORY)
> +    log.info("saving screenshots taken during the installation to: %s" % target_path)
> +    try:
> +        # create the screenshots directory
> +        mkdirChain(target_path)
> +        # copy all screenshots
> +        for filename in os.listdir(SCREENSHOTS_DIRECTORY):
> +            shutil.copy(os.path.join(SCREENSHOTS_DIRECTORY, filename), target_path)
> +
> +    except OSError:
> +        log.exception("saving screenshots to installed system failed")
> diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
> index 9251949..8ffc723 100644
> --- a/pyanaconda/ui/gui/__init__.py
> +++ b/pyanaconda/ui/gui/__init__.py
> @@ -27,10 +27,10 @@ from gi.repository import Gdk, Gtk, AnacondaWidgets, Keybinder, GdkPixbuf, GLib,
>  
>  from pyanaconda.i18n import _
>  from pyanaconda.constants import IPMI_ABORTED
> -from pyanaconda import product, iutil
> +from pyanaconda import product, iutil, constants
>  
>  from pyanaconda.ui import UserInterface, common
> -from pyanaconda.ui.gui.utils import gtk_action_wait, unbusyCursor
> +from pyanaconda.ui.gui.utils import gtk_action_wait, gtk_call_once, unbusyCursor
>  from pyanaconda import ihelp
>  import os.path
>  
> @@ -109,7 +109,7 @@ class GUIObject(common.UIObject):
>      helpFile = None
>      translationDomain = "anaconda"
>  
> -    screenshots_directory = "/tmp/anaconda-screenshots"
> +    handles_autostep = False
>  
>      def __init__(self, data):
>          """Create a new UIObject instance, including loading its uiFile and
> @@ -156,6 +156,12 @@ class GUIObject(common.UIObject):
>          Keybinder.init()
>          Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
>  
> +        self._automaticEntry = False
> +        self._autostepRunning = False
> +        self._autostepDone = False
> +        self._autostepDoneCallback = None
> +        self._lastAutostepSpoke = False
> +
>      def _findUIFile(self):
>          path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/")
>          dirs = path.split(":")
> @@ -171,28 +177,128 @@ class GUIObject(common.UIObject):
>          raise IOError("Could not load UI file '%s' for object '%s'" % (self.uiFile, self))
>  
>      def _handlePrntScreen(self, *args, **kwargs):
> -        global _screenshotIndex
>          global _last_screenshot_timestamp
>          # as a single press of the assigned key generates
>          # multiple callbacks, we need to skip additional
>          # callbacks for some time once a screenshot is taken
>          if (time.time() - _last_screenshot_timestamp) >= SCREENSHOT_DELAY:
> -            # Make sure the screenshot directory exists.
> -            if not os.access(self.screenshots_directory, os.W_OK):
> -                os.makedirs(self.screenshots_directory)
> -
> -            fn = os.path.join(self.screenshots_directory,
> -                              "screenshot-%04d.png" % _screenshotIndex)
> -            root_window = Gdk.get_default_root_window()
> -            pixbuf = Gdk.pixbuf_get_from_window(root_window, 0, 0,
> -                                                root_window.get_width(),
> -                                                root_window.get_height())
> -            pixbuf.savev(fn, 'png', [], [])
> -            log.info("screenshot nr. %d taken", _screenshotIndex)
> -            _screenshotIndex += 1
> +            self.take_screenshot()
>              # start counting from the time the screenshot operation is done
>              _last_screenshot_timestamp = time.time()
>  
> +    def take_screenshot(self, name=None):
> +        """Take a screenshot of the whole screen (works even with multiple displays)
> +
> +        :param name: optional name for the screenshot that will be appended to the filename,
> +                     after the standard prefix & screenshot number
> +        :type name: str or NoneType
> +        """
> +        global _screenshotIndex
> +        # Make sure the screenshot directory exists.
> +        iutil.mkdirChain(constants.SCREENSHOTS_DIRECTORY)
> +
> +        if name is None:
> +            screenshot_filename = "screenshot-%04d.png" % _screenshotIndex
> +        else:
> +            screenshot_filename = "screenshot-%04d-%s.png" % (_screenshotIndex, name)
> +
> +        fn = os.path.join(constants.SCREENSHOTS_DIRECTORY, screenshot_filename)
> +
> +        root_window = self.main_window.get_window()
> +        pixbuf = Gdk.pixbuf_get_from_window(root_window, 0, 0,
> +                                            root_window.get_width(),
> +                                            root_window.get_height())
> +        pixbuf.savev(fn, 'png', [], [])
> +        log.info("%s taken", screenshot_filename)
> +        _screenshotIndex += 1
> +
> +    @property
> +    def automaticEntry(self):
> +        """Report if the given GUIObject has been displayed under automatic control
> +
> +        This is needed for example for installations with an incomplete kickstart,
> +        as we need to differentiate the automatic screenshot pass from the user
> +        entering a spoke to manually configure things. We also need to skip applying
> +        changes if the spoke is entered automatically.
> +        """
> +        return self._automaticEntry
> +
> +    @automaticEntry.setter
> +    def automaticEntry(self, value):
> +        self._automaticEntry = value
> +
> +    @property
> +    def autostepRunning(self):
> +        """Report if the GUIObject is currently running autostep"""
> +        return self._autostepRunning
> +
> +    @autostepRunning.setter
> +    def autostepRunning(self, value):
> +        self._autostepRunning = value
> +
> +    @property
> +    def autostepDone(self):
> +        """Report if autostep for this GUIObject has been finished"""
> +        return self._autostepDone
> +
> +    @autostepDone.setter
> +    def autostepDone(self, value):
> +        self._autostepDone = value
> +
> +    @property
> +    def autostepDoneCallback(self):
> +        """A callback to be run once autostep has been finished"""
> +        return self._autostepDoneCallback
> +
> +    @autostepDoneCallback.setter
> +    def autostepDoneCallback(self, callback):
> +        self._autostepDoneCallback = callback
> +
> +    @property
> +    def lastAutostepSpoke(self):
> +        """Indicates if the screen is the last spoke to be processed on a hub"""
> +        return self._lastAutostepSpoke
> +
> +    @lastAutostepSpoke.setter
> +    def lastAutostepSpoke(self, value):
> +        self._lastAutostepSpoke = value
Is there any chance these properties will need to be overridden by
subclasses and add some more code to just getting/setting the internal
attribute? If not, these should be just public attributes read/set
directly.

> +
> +    def autostep(self):
> +        """Autostep through this graphical object and through
> +        any graphical objects managed by it (such as through spokes for a hub)
> +        """
> +        # report that autostep is running to prevent another from starting
> +        self.autostepRunning = True
> +        # take a screenshot of the current graphical object
> +        if self.data.autostep.autoscreenshot:
> +            # as autostep is triggered just before leaving a screen,
> +            # we can safely take a screenshot of the "parent" object at once
> +            # without using idle_add
> +            self.take_screenshot(self.__class__.__name__)
> +        self._doAutostep()
> +        # done
> +        self.autostepRunning = False
> +        self.autostepDone = True
> +        self._doPostAutostep()
> +
> +        # run the autostep-done callback (if any)
> +        if self.autostepDoneCallback:
> +            self.autostepDoneCallback(self)
> +
> +    def _doPostAutostep(self):
> +        """To be overridden by the given GUIObject sub-class with custom code
> +        that brings the GUI from the autostepping mode back to the normal mode.
> +        This usually means to "click" the continue button or its equivalent.
> +        """
> +        pass
> +
> +    def _doAutostep(self):
> +        """To be overridden by the given GUIObject sub-class with customized
> +        autostepping code - if needed
> +        (this is for example used to step through spokes in a hub)
> +        """
> +        pass
> +
>      @property
>      def window(self):
>          """Return the object out of the GtkBuilder representation
> @@ -410,6 +516,27 @@ class MainWindow(Gtk.Window):
>          """
>          self._setVisibleChild(spoke)
>  
> +        # autostep through the spoke if required
> +        if spoke.automaticEntry:
> +            # we need to use idle_add here to give GTK time to render the spoke
> +            gtk_call_once(self._autostep_spoke, spoke)
> +
> +    def _autostep_spoke(self, spoke):
> +        """Step through a spoke and make a screenshot if required.
> +        If this is the last spoke to be autostepped on a hub return to
> +        the hub so that we can proceed to the next one.
> +        """
> +        # it might be possible that autostep is specified, but autoscreenshot isn't
> +        if spoke.data.autostep.autoscreenshot:
> +            spoke.take_screenshot(spoke.__class__.__name__)
> +
> +        if spoke.autostepDoneCallback:
> +            spoke.autostepDoneCallback(spoke)
> +
> +        # if this is the last spoke then return to hub
> +        if spoke.lastAutostepSpoke:
> +            self.returnToHub()
> +
>      def returnToHub(self):
>          """Exit a spoke and return to a hub."""
>          self._setVisibleChild(self._current_action)
> @@ -753,11 +880,29 @@ class GraphicalUserInterface(UserInterface):
>      ### SIGNAL HANDLING METHODS
>      ###
>      def _on_continue_clicked(self, win, user_data=None):
> +        # Autostep needs to be triggered just before switching to the next screen
> +        # (or before quiting the installation if there are no more screens) to be consistent
> +        # in both fully automatic kickstart installation and for installation with an incomplete
> +        # kickstart. Therefore we basically "hook" the continue-clicked signal, start autostepping
> +        # and ignore any other continue-clicked signals until autostep is done.
> +        # Once autostep finishes, it emits the appropriate continue-clicked signal itself,
> +        # switching to the next screen (if any).
> +        if self.data.autostep.seen and self._currentAction.handles_autostep:
> +            if self._currentAction.autostepRunning:
> +                log.debug("ignoring the continue-clicked signal - autostep is running")
> +                return
> +            elif not self._currentAction.autostepDone:
> +                self._currentAction.autostep()
> +                return
> +
>          if not win.get_may_continue():
>              return
>  
>          # If we're on the last screen, clicking Continue quits.
>          if len(self._actions) == 1:
> +            # save the screenshots to the installed system before killing Anaconda
> +            # (the kickstart post scripts run to early, so we need to copy the screenshots now)
> +            iutil.save_screenshots()
>              Gtk.main_quit()
>              return
>  
> diff --git a/pyanaconda/ui/gui/hubs/__init__.py b/pyanaconda/ui/gui/hubs/__init__.py
> index ab68893..bb65329 100644
> --- a/pyanaconda/ui/gui/hubs/__init__.py
> +++ b/pyanaconda/ui/gui/hubs/__init__.py
> @@ -27,7 +27,7 @@ from pyanaconda.product import distributionText
>  
>  from pyanaconda.ui import common
>  from pyanaconda.ui.gui import GUIObject
> -from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup
> +from pyanaconda.ui.gui.utils import gtk_call_once, fire_gtk_action, escape_markup
>  
>  import logging
>  log = logging.getLogger("anaconda")
> @@ -54,6 +54,8 @@ class Hub(GUIObject, common.Hub):
>         additional standalone screens either before or after them.
>      """
>  
> +    handles_autostep = True
> +
>      def __init__(self, data, storage, payload, instclass):
>          """Create a new Hub instance.
>  
> @@ -91,6 +93,9 @@ class Hub(GUIObject, common.Hub):
>  
>          self._checker = None
>  
> +        self._spokesToStepIn = []
> +        self._spokeAutostepIndex = 0
> +
>      def _createBox(self):
>          from gi.repository import Gtk, AnacondaWidgets
>          from pyanaconda.ui.gui.utils import setViewportBackground
> @@ -353,6 +358,11 @@ class Hub(GUIObject, common.Hub):
>      def spoke_done(self, spoke):
>          spoke.visitedSinceApplied = True
>  
> +        # don't apply any actions if the spoke was visited automatically
> +        if spoke.automaticEntry:
> +            spoke.automaticEntry = False
> +            return
> +
>          # Don't take visitedSinceApplied into account here.  It will always be
>          # True from the line above.
>          if spoke.changed and (not spoke.skipTo or (spoke.skipTo and spoke.applyOnSkip)):
> @@ -384,3 +394,37 @@ class Hub(GUIObject, common.Hub):
>          else:
>              self.main_window.returnToHub()
>  
> +    def _doAutostep(self):
> +        """Autostep through all spokes managed by this hub"""
> +        log.info("autostepping through all spokes on hub %s", self.__class__.__name__)
> +        # created a list of all spokes in reverse alphabetic order, we will pop() from it when
            create^
> +        # processing all the spokes so the screenshots will actually be in alphabetic order
> +        self._spokesToStepIn = list(reversed(sorted(self._spokes.values(), key=lambda x: x.__class__.__name__)))
> +        # we can't just loop over all the spokes due to the asynchronous nature of GtkStack, so we start by
> +        # autostepping to the first spoke, this will trigger a callback that steps to the next spoke,
> +        # until we run out of unvisited spokes
> +        self._autostepSpoke()
As jkonecny already suggested, please add some blank lines in this
block.

> +
> +    def _autostepSpoke(self):
> +        if self._spokesToStepIn:
> +            spoke = self._spokesToStepIn.pop()
> +            self._spokeAutostepIndex += 1
> +            log.debug("stepping to spoke %s (%d/%d)", spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes))
> +            spoke.automaticEntry = True
> +            spoke.autostepDoneCallback = lambda x: self._autostepSpoke()
> +            # this is the last spoke, tell it to return to hub once the screenshot is taken
> +            if self._spokesToStepIn == []:
> +                spoke.lastAutostepSpoke = True
> +            #fire_gtk_action(self._on_spoke_clicked, None, None, spoke)
> +            fire_gtk_action(self._on_spoke_clicked, None, None, spoke)
The commented-out line could definitely be removed. And does this really
need to wait for self._on_spoke_clicked() to finish? If not, it should
use gtk_call_once() instead of fire_gtk_action().

> +        else:
> +            log.info("autostep for hub %s finished", self.__class__.__name__)
> +            gtk_call_once(self._doPostAutostep)
> +
> +    def _doPostAutostep(self):
> +        if self._spokesToStepIn:
> +            # there are still spokes that need to be stepped in
> +            return
> +        # we are done, re-emit the continue clicked signal we "consumed" previously
> +        # so that the Anaconda GUI can switch to the next screen (or quit)
> +        self.window.emit("continue-clicked")
> diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py
> index 1227d44..17866ee 100644
> --- a/pyanaconda/ui/gui/spokes/__init__.py
> +++ b/pyanaconda/ui/gui/spokes/__init__.py
> @@ -21,6 +21,7 @@
>  
>  from pyanaconda.ui import common
>  from pyanaconda.ui.gui import GUIObject
> +from pyanaconda.ui.gui.utils import gtk_call_once
>  from pyanaconda import ihelp
>  
>  __all__ = ["StandaloneSpoke", "NormalSpoke"]
> @@ -28,6 +29,9 @@ __all__ = ["StandaloneSpoke", "NormalSpoke"]
>  # Inherit abstract methods from common.StandaloneSpoke
>  # pylint: disable=abstract-method
>  class StandaloneSpoke(GUIObject, common.StandaloneSpoke):
> +
> +    handles_autostep = True
> +
>      def __init__(self, data, storage, payload, instclass):
>          GUIObject.__init__(self, data)
>          common.StandaloneSpoke.__init__(self, storage, payload, instclass)
> @@ -38,6 +42,11 @@ class StandaloneSpoke(GUIObject, common.StandaloneSpoke):
>      def _on_continue_clicked(self, win, user_data=None):
>          self.apply()
>  
> +    def _doPostAutostep(self):
> +        # we are done, re-emit the continue clicked signal we "consumed" previously
> +        # so that the Anaconda GUI can switch to the next screen
> +        gtk_call_once(self.window.emit, "continue-clicked")
> +
>  # Inherit abstract methods from common.NormalSpoke
>  # pylint: disable=abstract-method
>  class NormalSpoke(GUIObject, common.NormalSpoke):
> diff --git a/pyanaconda/ui/gui/spokes/welcome.py b/pyanaconda/ui/gui/spokes/welcome.py
> index 91b2b3a..028ad43 100644
> --- a/pyanaconda/ui/gui/spokes/welcome.py
> +++ b/pyanaconda/ui/gui/spokes/welcome.py
> @@ -314,8 +314,9 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke):
>      # Override the default in StandaloneSpoke so we can display the beta
>      # warning dialog first.
>      def _on_continue_clicked(self, window, user_data=None):
> -        # Don't display the betanag dialog if this is the final release.
> -        if not isFinal:
> +        # Don't display the betanag dialog if this is the final release or
> +        # when autostep has been requested as betanag breaks the autostep logic.
> +        if not isFinal and not self.data.autostep.seen:
>              dlg = self.builder.get_object("betaWarnDialog")
>              with self.main_window.enlightbox(dlg):
>                  rc = dlg.run()

-- 
Vratislav Podzimek

Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic



More information about the anaconda-patches mailing list