[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