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@redhat.com --- pyanaconda/constants.py | 4 + pyanaconda/iutil.py | 27 ++++++ pyanaconda/ui/gui/__init__.py | 172 +++++++++++++++++++++++++++++++---- pyanaconda/ui/gui/hubs/__init__.py | 55 +++++++++++ pyanaconda/ui/gui/spokes/__init__.py | 9 ++ pyanaconda/ui/gui/spokes/welcome.py | 5 +- 6 files changed, 253 insertions(+), 19 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..57baf90 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..2169aad 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,14 @@ class GUIObject(common.UIObject): Keybinder.init() Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
+ self._automaticEntry = False + self._autostepRunning = False + self._autostepDone = False + self._autostepDoneCallback = None + + # this indicates if the screen is the last spoke to be processed for a hub + 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 +179,119 @@ 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 + + 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 +509,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 +873,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..2563300 100644 --- a/pyanaconda/ui/gui/hubs/__init__.py +++ b/pyanaconda/ui/gui/hubs/__init__.py @@ -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,48 @@ 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__) + + # create a list of all spokes in reverse alphabetic order, we will pop() from it when + # 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() + + def _autostepSpoke(self): + """Process a single spoke, if no more spokes are available report autostep as finished for the hub.""" + # do we have some spokes to work on ? + if self._spokesToStepIn: + # take one of them + spoke = self._spokesToStepIn.pop() + + # increment the number of processed spokes + self._spokeAutostepIndex += 1 + + log.debug("stepping to spoke %s (%d/%d)", spoke.__class__.__name__, self._spokeAutostepIndex, len(self._spokes)) + + # notify the spoke about the upcoming automatic entry and set a callback that will be called + # once the spoke has been successfully processed + spoke.automaticEntry = True + spoke.autostepDoneCallback = lambda x: self._autostepSpoke() + + # if this is the last spoke, tell it to return to hub once processed + if self._spokesToStepIn == []: + spoke.lastAutostepSpoke = True + gtk_call_once(self._on_spoke_clicked, None, None, spoke) + 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()