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

Martin Kolman mkolman at redhat.com
Tue Jul 28 14:42:32 UTC 2015


On Tue, 2015-07-28 at 16:11 +0200, Vratislav Podzimek wrote:
> 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.
I meant it for consistency with all the other properties, but it is
indeed unlikely we will need to call some code when this property is
read or set. So I'll just turn it to a public atribute.


> 
> > +
> > +    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.
Already done. :-)
> 
> > +
> > +    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().
Done.
> 
> > +        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()
> 


More information about the anaconda-patches mailing list