diff --git a/anaconda/.tx/config b/anaconda/.tx/config deleted file mode 100644 index db3663c..0000000 --- a/anaconda/.tx/config +++ /dev/null @@ -1,9 +0,0 @@ -[anaconda.f21-branch] -file_filter = po/.po -source_file = po/anaconda.pot -source_lang = en -type = PO - -[main] -host = https://www.transifex.com - diff --git a/anaconda/Makefile.am b/anaconda/Makefile.am index dcc6eed..4a1a037 100644 --- a/anaconda/Makefile.am +++ b/anaconda/Makefile.am @@ -44,18 +44,16 @@ dist_sbin_SCRIPTS = anaconda ARCHIVE_TAG = $(PACKAGE_NAME)-$(PACKAGE_VERSION)-$(PACKAGE_RELEASE) -TX_PULL_ARGS = -a -f -TX_PUSH_ARGS = -s - -INSTALLATION_GUIDE_REPO_URL = git://git.fedorahosted.org/git/docs/install-guide.git +ZANATA_PULL_ARGS = --transdir $(srcdir)/po/ +ZANATA_PUSH_ARGS = --srcdir $(srcdir)/po/ --push-type source --force tag: @git tag -s -a -m "Tag as $(ARCHIVE_TAG)" $(ARCHIVE_TAG) @echo "Tagged as $(ARCHIVE_TAG)" po-pull: - rpm -q transifex-client &>/dev/null || ( echo "need to run: yum install transifex-client"; exit 1 ) - tx pull $(TX_PULL_ARGS) + rpm -q zanata-python-client &>/dev/null || ( echo "need to run: yum install zanata-python-client"; exit 1 ) + zanata pull $(ZANATA_PULL_ARGS) po-empty: for lingua in $$(grep -v '^#' $(srcdir)/po/LINGUAS) ; do \ @@ -64,12 +62,12 @@ po-empty: exit 1 ; \ done -scratch: po-empty get-help +scratch: po-empty $(MAKE) ARCHIVE_TAG=HEAD dist git checkout -- $(srcdir)/po/$(PACKAGE_NAME).pot -scratch-bumpver: po-empty get-help - @opts="-n $(PACKAGE_NAME) -v $(PACKAGE_VERSION) -r $(PACKAGE_RELEASE) -b $(PACKAGE_BUGREPORT)" ; \ +scratch-bumpver: po-empty + @opts="-S -n $(PACKAGE_NAME) -v $(PACKAGE_VERSION) -r $(PACKAGE_RELEASE) -b $(PACKAGE_BUGREPORT)" ; \ if [ ! -z "$(IGNORE)" ]; then \ opts="$${opts} -i $(IGNORE)" ; \ fi ; \ @@ -79,18 +77,12 @@ scratch-bumpver: po-empty get-help if [ ! -z "$(BZDEBUG)" ]; then \ opts="$${opts} -d" ; \ fi ; \ - if [ ! -z "$(SKIP_ACKS)" ]; then \ - opts="$${opts} -s" ; \ - fi ; \ - ( cd $(srcdir) && scripts/makebumpver $${opts} ) || exit 1 ; \ - $(MAKE) -C po $(PACKAGE_NAME).pot-update ; + ( cd $(srcdir) && scripts/makebumpver --skip-zanata $${opts} ) || exit 1 ; \ + $(MAKE) -C po $(PACKAGE_NAME).pot-update ; -release: get-help +release: $(MAKE) dist && $(MAKE) tag && git checkout -- $(srcdir)/po/$(PACKAGE_NAME).pot -api: - doxygen docs/api.cfg - bumpver: po-pull @opts="-n $(PACKAGE_NAME) -v $(PACKAGE_VERSION) -r $(PACKAGE_RELEASE) -b $(PACKAGE_BUGREPORT)" ; \ if [ ! -z "$(IGNORE)" ]; then \ @@ -107,21 +99,21 @@ bumpver: po-pull fi ; \ ( cd $(srcdir) && scripts/makebumpver $${opts} ) || exit 1 ; \ $(MAKE) -C po $(PACKAGE_NAME).pot-update && \ - tx push $(TX_PUSH_ARGS) + zanata push $(ZANATA_PUSH_ARGS) # Install all packages specified as BuildRequires in the Anaconda specfile # -> installs packages needed to build Anaconda install-buildrequires: srcdir="$(srcdir)" && \ : $${srcdir:=.} && \ - yum install $$(grep BuildRequires: $${srcdir}/anaconda.spec.in | cut -d ' ' -f 2) + yum install $$(grep ^BuildRequires: $${srcdir}/anaconda.spec.in | cut -d ' ' -f 2) # Install all packages specified as Requires in the Anaconda specfile # -> installs packages needed to run Anaconda and the Anaconda unit tests install-requires: srcdir="$(srcdir)" && \ : $${srcdir:=.} && \ - yum install $$(grep Requires: $${srcdir}/anaconda.spec.in | cut -d ' ' -f 2) + yum install $$(grep ^Requires: $${srcdir}/anaconda.spec.in | cut -d ' ' -f 2 | grep -v ^anaconda) # Generate an updates.img based on the changed files since the release # was tagged. Updates are copied to ./updates-img and then the image is @@ -141,43 +133,6 @@ unittests-logpicker: PYTHONPATH=$(builddir)/pyanaconda/isys/.libs:tests/:$(srcdir):utils/ nosetests -v old_tests/logpicker_test # GUI TESTING -runspoke: - ANACONDA_DATA=$(srcdir)/data \ - ANACONDA_WIDGETS_OVERRIDES=$(srcdir)/widgets/python \ - ANACONDA_WIDGETS_DATA=$(srcdir)/widgets/data \ - ANACONDA_INSTALL_CLASSES=$(srcdir)/pyanaconda/installclasses \ - PYTHONPATH=$(srcdir):$(builddir)/pyanaconda/isys/.libs:$(srcdir)/widgets/python/:$(builddir)/widgets/src/.libs/ \ - LD_LIBRARY_PATH=$(builddir)/widgets/src/.libs \ - UIPATH=$(srcdir)/pyanaconda/ui/gui/ \ - GI_TYPELIB_PATH=$(builddir)/widgets/src/ \ - $(srcdir)/pyanaconda/ui/gui/tools/run-spoke.py ${SPOKE_MODULE} ${SPOKE_CLASS} - -runhub: - ANACONDA_DATA=$(srcdir)/data \ - ANACONDA_WIDGETS_OVERRIDES=$(srcdir)/widgets/python \ - ANACONDA_WIDGETS_DATA=$(srcdir)/widgets/data \ - ANACONDA_INSTALL_CLASSES=$(srcdir)/pyanaconda/installclasses \ - PYTHONPATH=$(srcdir):$(builddir)/pyanaconda/isys/.libs:$(srcdir)/widgets/python/:$(builddir)/widgets/src/.libs/ \ - LD_LIBRARY_PATH=$(builddir)/widgets/src/.libs \ - UIPATH=$(srcdir)/pyanaconda/ui/gui/ \ - GI_TYPELIB_PATH=$(builddir)/widgets/src/ \ - $(srcdir)/pyanaconda/ui/gui/tools/run-hub.py ${HUB_MODULE} ${HUB_CLASS} - -runtextspoke: - ANACONDA_DATA=$(srcdir)/data \ - ANACONDA_INSTALL_CLASSES=$(srcdir)/pyanaconda/installclasses \ - PYTHONPATH=$(srcdir):$(builddir)/pyanaconda/isys/.libs:$(srcdir)/widgets/python/:$(builddir)/widgets/src/.libs/ \ - LD_LIBRARY_PATH=$(builddir)/widgets/src/.libs \ - $(srcdir)/pyanaconda/ui/tui/tools/run-text-spoke.py ${SPOKE_MODULE} ${SPOKE_CLASS} - -runtexthub: - ANACONDA_DATA=$(srcdir)/data \ - ANACONDA_INSTALL_CLASSES=$(srcdir)/pyanaconda/installclasses \ - PYTHONPATH=$(srcdir):$(builddir)/pyanaconda/isys/.libs:$(srcdir)/widgets/python/:$(builddir)/widgets/src/.libs/ \ - LD_LIBRARY_PATH=$(builddir)/widgets/src/.libs \ - $(srcdir)/pyanaconda/ui/tui/tools/run-text-hub.py ${HUB_MODULE} ${HUB_CLASS} - - runglade: ANACONDA_DATA=$(srcdir)/data \ ANACONDA_WIDGETS_OVERRIDES=$(srcdir)/widgets/python \ @@ -190,24 +145,3 @@ runglade: GLADE_CATALOG_SEARCH_PATH=$(srcdir)/widgets/glade \ GLADE_MODULE_SEARCH_PATH=$(builddir)/widgets/src/.libs \ glade ${GLADE_FILE} - - -# Get content for the Anaconda built-in help system by cloning the -# installation guide git repository and running the help processing -# script (it is part of the repository). -# Once the help content has been generated copy it to our help folder, -# so that it can be included in the tarball. -# Skip the git clone if the repository already exists but run git pull -# to make sure it is up to date. We also clone the repository -# without history as it si rather big (>400 MB!), which is quite -# an overkill for <100 kB of help conent. :) -get-help: - if [ ! -d "install-guide" ]; then \ - if [ -f "installation_guide_repo_url" ]; then \ - git clone --depth=1 `cat installation_guide_repo_url` ; \ - else \ - git clone --depth=1 $(INSTALLATION_GUIDE_REPO_URL) ; \ - fi ; \ - fi ; \ - ( cd install-guide && git pull --rebase && python prepare_anaconda_help_content.py ) - cp -r install-guide/anaconda_help_content/* $(srcdir)/data/help diff --git a/anaconda/acinclude.m4 b/anaconda/acinclude.m4 new file mode 100644 index 0000000..61eb1a8 --- /dev/null +++ b/anaconda/acinclude.m4 @@ -0,0 +1,75 @@ +dnl autoconf macros for anaconda +dnl +dnl Copyright (C) 2014 Red Hat, Inc. +dnl +dnl This program is free software; you can redistribute it and/or modify +dnl it under the terms of the GNU Lesser General Public License as published +dnl by the Free Software Foundation; either version 2.1 of the License, or +dnl (at your option) any later version. +dnl +dnl This program is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +dnl GNU Lesser General Public License for more details. +dnl +dnl You should have received a copy of the GNU Lesser General Public License +dnl along with this program. If not, see . +dnl +dnl Author: David Shea + +dnl ANACONDA_SOFT_FAILURE(MESSAGE) +dnl +dnl Store a message that in some contexts could be considered indicative +dnl of a failure, but in other contexts could be indicative of who cares. +dnl +dnl For example, the anaconda widgets require a version of gtk3-devel of +dnl particular newness, and the widgets will fail to build if this library +dnl and headers are not available. On the other hand, gtk3 isn't required at +dnl all for most everything else, so it would be nice if a missing or old +dnl gtk3-devel didn't halt the configure script. +dnl +dnl Any message sent to this macro will be stored, and they can all be +dnl displayed at the end of configure using the ANACONDA_FAILURES macro. +AC_DEFUN([ANACONDA_SOFT_FAILURE], [dnl +AS_IF([test x"$anaconda_failure_messages" = x], + [anaconda_failure_messages="[$1]"], + [anaconda_failure_messages="$anaconda_failure_messages +[$1]" +])])dnl + +dnl ANACONDA_PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES) +dnl +dnl Check whether a module is available, using pkg-config. Instead of failing +dnl if a module is not found, store the failure in a message that can be +dnl printed using the ANACONDA_FAILURES macro. +dnl +dnl The syntax and behavior of VARIABLE-PREFIX and MODULES is the same as for +dnl PKG_CHECK_MODULES. +AC_DEFUN([ANACONDA_PKG_CHECK_MODULES], [dnl +PKG_CHECK_MODULES([$1], [$2], [], [ANACONDA_SOFT_FAILURE($[$1]_PKG_ERRORS)]) +])dnl + +dnl ANACONDA_PKG_CHECK_EXISTS(MODULES) +dnl +dnl Check whether a module exists, using pkg-config. Instead of failing +dnl if a module is not found, store the failure in a message that can be +dnl printed using the ANACONDA_FAILURES macro. +dnl +dnl The syntax and behavior of MOUDLES is the same as for +dnl PKG_CHECK_EXISTS. +AC_DEFUN([ANACONDA_PKG_CHECK_EXISTS], [dnl +PKG_CHECK_EXISTS([$1], [], [ANACONDA_SOFT_FAILURE([Check for $1 failed])]) +])dnl + +dnl ANACONDA_FAILURES +dnl +dnl Print the failure messages collected by ANACONDA_SOFT_FAILURE and +dnl ANACONDA_PKG_CHECK_MODULES +AC_DEFUN([ANACONDA_FAILURES], [dnl +AS_IF([test x"$anaconda_failure_messages" = x], [], [dnl +echo "" +echo "*** Anaconda encountered the following issues during configuration:" +echo "$anaconda_failure_messages" +echo "" +echo "*** Anaconda will not successfully build without these missing dependencies" +])])dnl diff --git a/anaconda/anaconda b/anaconda/anaconda index 4ec1a12..c17c797 100755 --- a/anaconda/anaconda +++ b/anaconda/anaconda @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # anaconda: The Red Hat Linux Installation program # @@ -44,85 +44,11 @@ if ("debug=1" in proc_cmdline) or ("debug" in proc_cmdline): cov.start() -import atexit, sys, os, time, subprocess, signal, errno +import atexit, sys, os, time, signal, stat, errno -# Install a global SIGCHLD handler to keep track of things that should be -# running for as long as anaconda does. The dictionary is of the form -# {pid: name, ...}. The handler will raise ExitError (defined below), so if -# not caught a SIGCHLD from a watched process will halt anaconda. -forever_pids = {} - -class ExitError(RuntimeError): - pass - -def sigchld_handler(num, frame): - # Check whether anything in the list of processes being watched has - # exited. We don't want to call waitpid(-1), since that would break - # anything else using wait/waitpid (like the subprocess module). - exited_pids = [] - exn_message = [] - - for child_pid in forever_pids: - try: - pid_result, status = iutil.eintr_retry_call(os.waitpid, child_pid, os.WNOHANG) - except OSError as e: - if e.errno == errno.ECHILD: - continue - - if pid_result: - proc_name = forever_pids[child_pid] - exited_pids.append(child_pid) - - if os.WIFEXITED(status): - status_str = "with status %s" % os.WEXITSTATUS(status) - elif os.WIFSIGNALED(status): - status_str = "on signal %s" % os.WTERMSIG(status) - else: - status_str = "with unknown status code %s" % status - - exn_message.append("%s exited %s" % (proc_name, status_str)) - - for child_pid in exited_pids: - del forever_pids[child_pid] - - if exn_message: - raise ExitError(", ".join(exn_message)) - -signal.signal(signal.SIGCHLD, sigchld_handler) - -# Fork a new process and add it to forever_pids. The return values are the -# the same as os.fork, but neither the parent nor the child will return -# until the process is watched. -def start_watched_pid(proc_name): - readpipe, writepipe = os.pipe() - childpid = os.fork() - if childpid == 0: - # No need for the write pipe in the child - iutil.eintr_retry_call(os.close, writepipe) - - # Wait for the parent to signal that it's ready to return - iutil.eintr_retry_call(os.read, readpipe, 1) - - # Ready to go - iutil.eintr_retry_call(os.close, readpipe) - return childpid - else: - # No need for the read pipe in the parent - iutil.eintr_retry_call(os.close, readpipe) - - # Add the pid to the list of watched pids - forever_pids[childpid] = proc_name - - # Signal to the child that we're ready to return - # D is for Done - iutil.eintr_retry_call(os.write, writepipe, 'D') - iutil.eintr_retry_call(os.close, writepipe) - return childpid - -def exitHandler(rebootData, storage, exitCode=None): +def exitHandler(rebootData, storage): # Clear the list of watched PIDs. - global forever_pids - forever_pids = {} + iutil.unwatchAllProcesses() # stop and save coverage here b/c later the file system may be unavailable if coverage is not None: @@ -133,9 +59,6 @@ def exitHandler(rebootData, storage, exitCode=None): if flags.usevnc: vnc.shutdownServer() - if exitCode: - anaconda.intf.shutdown() - if "nokill" in flags.cmdline: iutil.vtActivate(1) print("anaconda halting due to nokill flag.") @@ -158,10 +81,18 @@ def exitHandler(rebootData, storage, exitCode=None): uninhibit_screensaver(anaconda.dbus_session_connection, anaconda.dbus_inhibit_id) anaconda.dbus_inhibit_id = None + # Unsetup the payload, which most usefully unmounts live images + if anaconda.payload: + anaconda.payload.unsetup() + + # Clean up the PID file + if pidfile_created: + os.unlink(pidfile_path) + if not flags.imageInstall and not flags.livecdInstall \ and not flags.dirInstall: from pykickstart.constants import KS_SHUTDOWN, KS_WAIT - from pyanaconda.iutil import dracut_eject, get_mount_paths + from pyanaconda.iutil import dracut_eject, get_mount_paths, execWithRedirect if flags.eject or rebootData.eject: for cdrom in storage.devicetree.getDevicesByType("cdrom"): @@ -169,11 +100,11 @@ def exitHandler(rebootData, storage, exitCode=None): dracut_eject(cdrom.path) if rebootData.action == KS_SHUTDOWN: - subprocess.Popen(["systemctl", "--no-wall", "poweroff"]) + execWithRedirect("systemctl", ["--no-wall", "poweroff"]) elif rebootData.action == KS_WAIT: - subprocess.Popen(["systemctl", "--no-wall", "halt"]) + execWithRedirect("systemctl", ["--no-wall", "halt"]) else: # reboot action is KS_REBOOT or None - subprocess.Popen(["systemctl", "--no-wall", "reboot"]) + execWithRedirect("systemctl", ["--no-wall", "reboot"]) def startSpiceVDAgent(): status = iutil.execWithRedirect("spice-vdagent", []) @@ -184,125 +115,37 @@ def startSpiceVDAgent(): log.info("Started spice-vdagent.") def startX11(): - # Start X11 with its USR1 handler set to ignore, which will make it send - # us SIGUSR1 if it succeeds. If it fails, catch SIGCHLD and bomb out. - # Use a list so the value can be modified from the handler - x11_started = [False] - def sigusr1_handler(num, frame): - log.debug("X server has signalled a successful start.") - x11_started[0] = True + # Open /dev/tty5 for stdout and stderr redirects + xfd = open("/dev/tty5", "a") - # Fail after, let's say a minute, in case something weird happens - # and we don't receive SIGUSR1 - def sigalrm_handler(num, frame): - # Check that it didn't make it under the wire - if x11_started[0]: - return - log.error("Timeout trying to start the X server") - raise ExitError("Timeout trying to start the X server") + # Start Xorg and wait for it become ready + iutil.startX(["Xorg", "-br", "-logfile", "/tmp/X.log", + ":%s" % constants.X_DISPLAY_NUMBER, "vt6", "-s", "1440", "-ac", + "-nolisten", "tcp", "-dpi", "96", + "-noreset"], output_redirect=xfd) - try: - old_sigusr1_handler = signal.signal(signal.SIGUSR1, sigusr1_handler) - old_sigalrm_handler = signal.signal(signal.SIGALRM, sigalrm_handler) +# function to handle X startup special issues for anaconda +def doStartupX11Actions(): + """Start window manager""" + from copy import copy - childpid = start_watched_pid("Xorg") - - if not childpid: - # after this point the method should never return (or throw an exception - # outside) - try: - # dup /dev/tty5 to stdout and stderr - xfd = iutil.eintr_retry_call(os.open, "/dev/tty5", os.O_WRONLY | os.O_APPEND) - iutil.eintr_retry_call(os.dup2, xfd, sys.stdout.fileno()) - iutil.eintr_retry_call(os.dup2, xfd, sys.stderr.fileno()) - - # Close all other file descriptors - try: - maxfd = os.sysconf("SC_OPEN_MAX") - except ValueError: - maxfd = 1024 - - os.closerange(3, maxfd) - - # Replace the SIGUSR1 handler with SIG_IGN as described above - signal.signal(signal.SIGUSR1, signal.SIG_IGN) - - # Run it - os.execlp("Xorg", "Xorg", "-br", - "-logfile", "/tmp/X.log", - ":1", "vt6", "-s", "1440", "-ac", - "-nolisten", "tcp", "-dpi", "96", - "-noreset") - - # We should never get here - raise OSError(0, "Unable to exec") - except BaseException as e: - # catch all possible exceptions - # Reopen the logger in case it was closed by closerange. - # Use a different name because assigning to variables across a - # fork confuses the hell out of python. - child_log = logging.getLogger("anaconda") - child_log.error("Problems running Xorg: %s", e) - os._exit(1) - - # Parent process - # Start the timer - signal.alarm(60) - - # Wait for SIGUSR1 - while not x11_started[0]: - signal.pause() - - # Now that X is started, make $DISPLAY available - os.environ["DISPLAY"] = ":1" - finally: - # Put everything back where it was - signal.alarm(0) - signal.signal(signal.SIGUSR1, old_sigusr1_handler) - signal.signal(signal.SIGALRM, old_sigalrm_handler) - -def startMetacityWM(): # When metacity actually connects to the X server is unknowable, but # fortunately it doesn't matter. metacity does not need to be the first # connection to Xorg, and if anaconda starts up before metacity, metacity # will just take over and maximize the window and make everything right, # fingers crossed. + # Add XDG_DATA_DIRS to the environment to pull in our overridden schema + # files. + env_bak = copy(os.environ) + if 'XDG_DATA_DIRS' in os.environ: + xdg_data_dirs = '/usr/share/anaconda/window-manager:' + os.environ['XDG_DATA_DIRS'] + else: + xdg_data_dirs = '/usr/share/anaconda/window-manager:/usr/share' - childpid = start_watched_pid("metacity") - if not childpid: - # after this point the method should never return (or throw an exception - # outside) - try: - returncode = iutil.execWithRedirect('metacity', ["--display", ":1", "--sm-disable"]) - except BaseException as e: - # catch all possible exceptions - log.error("Problems running the window manager: %s", e) - os._exit(1) - - log.info("The window manager has terminated.") - os._exit(returncode) - - return childpid - -def startAuditDaemon(): - childpid = os.fork() - if not childpid: - cmd = '/sbin/auditd' - try: - os.execl(cmd, cmd) - except OSError as e: - log.error("Error running the audit daemon: %s", e) - os._exit(0) - # auditd will turn into a daemon so catch the immediate child pid now: - iutil.eintr_retry_call(os.waitpid, childpid, 0) - -# function to handle X startup special issues for anaconda -def doStartupX11Actions(): - """Start window manager""" - - # now start up the window manager - wm_pid = startMetacityWM() - log.info("Starting window manager, pid %s.", wm_pid) + childproc = iutil.startProgram(["metacity", "--display", ":1", "--sm-disable"], + env_add={'XDG_DATA_DIRS': xdg_data_dirs}) + os.environ = env_bak + iutil.watchProcess(childproc, "metacity") def set_x_resolution(runres): if runres and opts.display_mode == 'g' and not flags.usevnc: @@ -474,6 +317,7 @@ def parseArguments(argv=None, boot_cmdline=None): # Obvious ap.add_argument("--loglevel", metavar="LEVEL", help=help_parser.help_text("loglevel")) ap.add_argument("--syslog", metavar="HOST[:PORT]", help=help_parser.help_text("syslog")) + ap.add_argument("--remotelog", metavar="HOST:PORT", help=help_parser.help_text("remotelog")) from pykickstart.constants import SELINUX_DISABLED, SELINUX_ENFORCING from pyanaconda.constants import SELINUX_DEFAULT @@ -519,8 +363,8 @@ def parseArguments(argv=None, boot_cmdline=None): help=help_parser.help_text("extlinux")) ap.add_argument("--nombr", action="store_true", default=False, help=help_parser.help_text("nombr")) - ap.add_argument("--dnf", action="store_true", default=False, - help=help_parser.help_text("dnf")) + ap.add_argument("--nodnf", action="store_false", dest="dnf", default=True, + help=help_parser.help_text("nodnf")) ap.add_argument("--mpathfriendlynames", action="store_true", default=True, help=help_parser.help_text("mpathfriendlynames")) @@ -541,6 +385,12 @@ def setupPythonPath(): sys.path.extend(ADDON_PATHS) def setupEnvironment(): + from pyanaconda.users import createLuserConf + + # This method is run before any threads are started, so this is the one + # point where it's ok to modify the environment. + # pylint: disable=environment-modify + # Silly GNOME stuff if 'HOME' in os.environ and not "XAUTHORITY" in os.environ: os.environ['XAUTHORITY'] = os.environ['HOME'] + '/.Xauthority' @@ -559,6 +409,16 @@ def setupEnvironment(): if "LD_PRELOAD" in os.environ: del os.environ["LD_PRELOAD"] + # Go ahead and set $DISPLAY whether we're going to use X or not + if 'DISPLAY' in os.environ: + flags.preexisting_x11 = True + else: + os.environ["DISPLAY"] = ":%s" % constants.X_DISPLAY_NUMBER + + # Call createLuserConf now to setup $LIBUSER_CONF + # the config file can change later but the environment variable cannot + createLuserConf(iutil.getSysroot()) + def setupLoggingFromOpts(options): if (options.debug or options.updateSrc) and not options.loglevel: # debugging means debug logging if an explicit level hasn't been st @@ -567,15 +427,24 @@ def setupLoggingFromOpts(options): if options.loglevel and options.loglevel in anaconda_log.logLevelMap: log.info("Switching logging level to %s", options.loglevel) level = anaconda_log.logLevelMap[options.loglevel] - anaconda_log.logger.tty_loglevel = level + anaconda_log.logger.loglevel = level anaconda_log.setHandlersLevel(log, level) storage_log = logging.getLogger("storage") anaconda_log.setHandlersLevel(storage_log, level) packaging_log = logging.getLogger("packaging") anaconda_log.setHandlersLevel(packaging_log, level) - if options.syslog: - anaconda_log.logger.updateRemote(options.syslog) + if can_touch_runtime_system("syslog setup"): + if options.syslog: + anaconda_log.logger.updateRemote(options.syslog) + + if options.remotelog: + try: + host, port = options.remotelog.split(":", 1) + port = int(port) + anaconda_log.logger.setup_remotelog(host, port) + except ValueError: + log.error("Could not setup remotelog with %s", options.remotelog) def gtk_warning(title, reason): from gi.repository import Gtk @@ -643,6 +512,8 @@ def check_memory(anaconda, options, display_mode=None): reason % reason_args, buttons = (_("OK"),)) screen.finish() + + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) # override display mode if machine cannot nicely run X @@ -658,6 +529,7 @@ def check_memory(anaconda, options, display_mode=None): stdoutLog.warning(reason % reason_args) title = livecd_title gtk_warning(title, reason % reason_args) + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) else: reason += nolivecd_extra @@ -693,7 +565,7 @@ def setupDisplay(anaconda, options, addons=None): # Only consider vncconnect when vnc is a param if options.vncconnect: - cargs = string.split(options.vncconnect, ":") + cargs = options.vncconnect.split(":") vncS.vncconnecthost = cargs[0] if len(cargs) > 1 and len(cargs[1]) > 0: if len(cargs[1]) > 0: @@ -755,9 +627,6 @@ def setupDisplay(anaconda, options, addons=None): stdoutLog.warning("Not asking for VNC because we don't have Xvnc") flags.vncquestion = False - if 'DISPLAY' in os.environ: - flags.preexisting_x11 = True - # Should we try to start Xorg? want_x = anaconda.displayMode == 'g' and \ not (flags.preexisting_x11 or flags.usevnc) @@ -845,6 +714,7 @@ def prompt_for_ssh(): if not ip: stdoutLog.error("No IP addresses found, cannot continue installation.") + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) ipstr = ip @@ -915,8 +785,7 @@ if __name__ == "__main__": initThreading() from pyanaconda.threads import threadMgr - import gettext - _ = lambda x: gettext.ldgettext("anaconda", x) + from pyanaconda.i18n import _ from pyanaconda import constants from pyanaconda.addons import collect_addon_paths @@ -950,7 +819,7 @@ if __name__ == "__main__": # see if we're on s390x and if we've got an ssh connection uname = os.uname() if uname[4] == 's390x': - if 'TMUX' not in os.environ and not flags.ksprompt and not flags.imageInstall: + if 'TMUX' not in os.environ and 'ks' not in flags.cmdline and not flags.imageInstall: prompt_for_ssh() sys.exit(0) @@ -969,12 +838,13 @@ if __name__ == "__main__": from pyanaconda import isys - import string - from pyanaconda import iutil + iutil.ipmi_report(constants.IPMI_STARTED) + if opts.images and opts.dirinstall: stdoutLog.error("--images and --dirinstall cannot be used at the same time") + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) elif opts.dirinstall: if opts.dirinstall is True: @@ -1031,20 +901,97 @@ if __name__ == "__main__": from pyanaconda.anaconda import Anaconda anaconda = Anaconda() - iutil.setup_translations(gettext) + iutil.setup_translations() # reset python's default SIGINT handler signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGSEGV, isys.handleSegv) signal.signal(signal.SIGTERM, lambda num, frame: sys.exit(1)) + # synchronously-delivered signals such as SIGSEGV and SIGILL cannot be + # handled properly from python, so install signal handlers from the C + # function in isys. + isys.installSyncSignalHandlers() + setupEnvironment() + # make sure we have /var/log soon, some programs fail to start without it iutil.mkdirChain("/var/log") - pidfile = open("/var/run/anaconda.pid", "w") - pidfile.write("%s\n" % (os.getpid(),)) - del pidfile + # Share these with the exit handler, installed later + pidfile_path = '/var/run/anaconda.pid' + pidfile_created = False + + # Look for a stale pid file + try: + with open(pidfile_path, 'r') as pidfile: + pid = int(pidfile.read()) + except IOError as e: + # Ignore errors due to the file not existing. Other errors mean (most + # likely) that we're not running as root, there's a filesystem error, + # or someone filled our PID file with garbage, so just let those be + # raised. + if e.errno != errno.ENOENT: + raise + else: + # Is the PID still running? + if not os.path.isdir("/proc/%s" % pid): + log.info("Removing stale PID file: %s no longer running", pid) + os.unlink(pidfile_path) + # Is the PID anaconda? + else: + try: + with open("/proc/%s/stat" % pid, "r") as pidstat: + # The part we care about is in the start, "PID (name) ..." + procname = pidstat.read().split(' ', 2)[1] + if procname != "(anaconda)": + log.info("Removing stale PID file: PID %s is now %s", pid, procname) + os.unlink(pidfile_path) + # If it is anaconda, let the pidfile creation below fail + # and print an error + except IOError as e: + # Ignore failures due to the file not existing in case the + # process ended while we were trying to read about it. Assume + # in this case that the process was another anaconda instance, + # and the PID file was cleaned up. + # If the process ended between open and read, we'll get ESRCH + if e.errno not in (errno.ENOENT, errno.ESRCH): + raise + + # Attempt to create the pidfile + try: + # Set all of the read/write bits and let umask make it make sense + pidfile = iutil.eintr_retry_call(os.open, pidfile_path, os.O_WRONLY|os.O_CREAT|os.O_EXCL, + stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IWGRP|stat.S_IROTH|stat.S_IWOTH) + pidfile_created = True + iutil.eintr_retry_call(os.write, pidfile, "%s\n" % os.getpid()) + iutil.eintr_retry_call(os.close, pidfile) + except OSError as e: + # If the failure was anything other than EEXIST during the open call, + # just re-raise the exception + if e.errno != errno.EEXIST: + raise + + log.error("%s already exists, exiting", pidfile_path) + + # If we had a $DISPLAY at start and zenity is available, we may be + # running in a live environment and we can display an error dialog. + # Otherwise just print an error. + if flags.preexisting_x11 and os.access("/usr/bin/zenity", os.X_OK): + # The module-level _() calls are ok here because the language may + # be set from the live environment in this case, and anaconda's + # language setup hasn't happened yet. + # pylint: disable=found-_-in-module-class + iutil.execWithRedirect("zenity", + ["--error", "--title", _("Unable to create PID file"), "--text", + _("Anaconda is unable to create %s because the file" + + " already exists. Anaconda is already running, or a previous instance" + + " of anaconda has crashed.") % pidfile_path]) + else: + print("%s already exists, exiting" % pidfile_path) + + iutil.ipmi_report(constants.IPMI_FAILED) + sys.exit(1) + # add our own additional signal handlers signal.signal(signal.SIGHUP, startDebugger) @@ -1101,16 +1048,20 @@ if __name__ == "__main__": # text console with a traceback instead of being left looking at a blank # screen. python-meh will replace this excepthook with its own handler # once it gets going. - if not flags.imageInstall and not flags.livecdInstall \ - and not flags.dirInstall: + if can_touch_runtime_system("early exception handler"): def _earlyExceptionHandler(ty, value, traceback): + iutil.ipmi_report(constants.IPMI_FAILED) iutil.vtActivate(1) return sys.__excepthook__(ty, value, traceback) sys.excepthook = _earlyExceptionHandler if can_touch_runtime_system("start audit daemon"): - startAuditDaemon() + # auditd will turn into a daemon and exit. Ignore startup errors + try: + iutil.execWithRedirect("/sbin/auditd", []) + except OSError: + pass # setup links required for all install types for i in ("services", "protocols", "nsswitch.conf", "joe", "selinux", @@ -1124,7 +1075,7 @@ if __name__ == "__main__": log.info("anaconda called with cmdline = %s", sys.argv) log.info("Default encoding = %s ", sys.getdefaultencoding()) - os.system("udevadm control --env=ANACONDA=1") + iutil.execWithRedirect("udevadm", ["control", "--env=ANACONDA=1"]) # Collect all addon paths addon_paths = collect_addon_paths(constants.ADDON_PATHS) @@ -1134,6 +1085,11 @@ if __name__ == "__main__": # shipped with the installation media. ksdata = None if opts.ksfile and not opts.liveinst: + if not os.path.exists(opts.ksfile): + stdoutLog.error("Kickstart file %s is missing.", opts.ksfile) + iutil.ipmi_report(constants.IPMI_ABORTED) + sys.exit(1) + flags.automatedInstall = True flags.eject = False ksFiles = [opts.ksfile] @@ -1197,14 +1153,14 @@ if __name__ == "__main__": log.info("Failed to parse proxy \"%s\": %s", anaconda.proxy, e) else: # Set environmental variables to be used by pre/post scripts - os.environ["PROXY"] = proxy.noauth_url - os.environ["PROXY_USER"] = proxy.username or "" - os.environ["PROXY_PASSWORD"] = proxy.password or "" + iutil.setenv("PROXY", proxy.noauth_url) + iutil.setenv("PROXY_USER", proxy.username or "") + iutil.setenv("PROXY_PASSWORD", proxy.password or "") # Variables used by curl, libreport, etc. - os.environ["http_proxy"] = proxy.url - os.environ["ftp_proxy"] = proxy.url - os.environ["HTTPS_PROXY"] = proxy.url + iutil.setenv("http_proxy", proxy.url) + iutil.setenv("ftp_proxy", proxy.url) + iutil.setenv("HTTPS_PROXY", proxy.url) if flags.noverifyssl: ksdata.method.noverifyssl = flags.noverifyssl @@ -1263,34 +1219,57 @@ if __name__ == "__main__": from pyanaconda import localization # Set the language before loading an interface, when it may be too late. - # check if the LANG environmental variable is set - env_lang = os.environ.get("LANG") - if env_lang is not None: - # parse it using langtable - env_langs = localization.get_language_locales(env_lang) - # if parsed LANG is the same as our default language - ignore it; - # otherwise use it as valid language candidate - if env_langs and env_langs[0] != constants.DEFAULT_LANG: - env_lang = env_langs[0] # the first language is the best match - else: - env_lang = None + # GNU defines four (four!) ways to set the locale via the environment. + # At least three of those are just going to get in the way of anaconda's + # ability to set the language and locale after startup. If any of, in + # order, $LANGUAGE, $LC_ALL, or $LC_MESSAGES is in the environment, copy + # the information to $LANG, and then clear the rest. + for varname in ("LANGUAGE", "LC_ALL", "LC_MESSAGES"): + if varname in os.environ: + os.environ["LANG"] = os.environ[varname] # pylint: disable=environment-modify + break - requested_lang = opts.lang or ksdata.lang.lang or env_lang + for varname in ("LANGUAGE", "LC_ALL", "LC_MESSAGES"): + if varname in os.environ: + del os.environ[varname] # pylint: disable=environment-modify - if requested_lang: - locales = localization.get_language_locales(requested_lang) - if locales: - localization.setup_locale(locales[0], ksdata.lang) - ksdata.lang.seen = True - else: - log.error("Invalid locale '%s' given on command line or in kickstart", requested_lang) + # first, try to load firmware language if nothing is already set in + # the environment + if "LANG" not in os.environ: + localization.load_firmware_language(ksdata.lang) + + # If command line options or kickstart set LANG, override the environment + if opts.lang: + os.environ["LANG"] = opts.lang # pylint: disable=environment-modify + ksdata.lang.seen = True + elif ksdata.lang.lang: + os.environ["LANG"] = ksdata.lang.lang # pylint: disable=environment-modify + + # Make sure LANG is set to something + if "LANG" not in os.environ: + os.environ["LANG"] = constants.DEFAULT_LANG # pylint: disable=environment-modify + + # parse it using langtable and update the environment + # If langtable returns no locales, fall back to the default + env_langs = localization.get_language_locales(os.environ["LANG"]) + if env_langs: + # the first language is the best match + os.environ["LANG"] = env_langs[0] # pylint: disable=environment-modify else: - # no kickstart or bootoption - use default - localization.setup_locale(constants.DEFAULT_LANG, ksdata.lang) + log.error("Invalid locale '%s' given on command line or in kickstart", os.environ["LANG"]) + os.environ["LANG"] = constants.DEFAULT_LANG # pylint: disable=environment-modify + + localization.setup_locale(os.environ["LANG"], ksdata.lang) import blivet blivet.enable_installer_mode() + # Initialize the network now, in case the display needs it + from pyanaconda.network import networkInitialize, wait_for_connecting_NM_thread + + networkInitialize(ksdata) + threadMgr.add(AnacondaThread(name=constants.THREAD_WAIT_FOR_CONNECTING_NM, target=wait_for_connecting_NM_thread, args=(ksdata,))) + # now start the interface setupDisplay(anaconda, opts, addon_paths) @@ -1323,15 +1302,14 @@ if __name__ == "__main__": flags.imageInstall = True except ValueError as e: stdoutLog.error("error specifying image file: %s", e) + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) if image_count: anaconda.storage.setupDiskImages() - # only install interactive exception handler in interactive modes - if ksdata.displaymode.displayMode != DISPLAY_MODE_CMDLINE or flags.debug: - from pyanaconda import exception - anaconda.mehConfig = exception.initExceptionHandling(anaconda) + from pyanaconda import exception + anaconda.mehConfig = exception.initExceptionHandling(anaconda) # add our own additional signal handlers signal.signal(signal.SIGUSR1, lambda signum, frame: @@ -1339,9 +1317,8 @@ if __name__ == "__main__": signal.signal(signal.SIGUSR2, lambda signum, frame: anaconda.dumpState()) atexit.register(exitHandler, ksdata.reboot, anaconda.storage) - from blivet import storageInitialize + from blivet.osinstall import storageInitialize from pyanaconda.packaging import payloadMgr - from pyanaconda.network import networkInitialize, wait_for_connecting_NM_thread from pyanaconda.timezone import time_initialize if flags.rescue_mode: @@ -1349,14 +1326,12 @@ if __name__ == "__main__": else: cleanPStore() - networkInitialize(ksdata) if not flags.dirInstall: threadMgr.add(AnacondaThread(name=constants.THREAD_STORAGE, target=storageInitialize, args=(anaconda.storage, ksdata, anaconda.protected))) threadMgr.add(AnacondaThread(name=constants.THREAD_TIME_INIT, target=time_initialize, args=(ksdata.timezone, anaconda.storage, anaconda.bootloader))) - threadMgr.add(AnacondaThread(name=constants.THREAD_WAIT_FOR_CONNECTING_NM, target=wait_for_connecting_NM_thread, args=(ksdata,))) # Fallback to default for interactive or for a kickstart with no installation method. fallback = not (flags.automatedInstall and ksdata.method.method) @@ -1390,14 +1365,12 @@ if __name__ == "__main__": # setup ntp servers and start NTP daemon if not requested otherwise if can_touch_runtime_system("start chronyd"): if anaconda.ksdata.timezone.ntpservers: - ntp.save_servers_to_config(anaconda.ksdata.timezone.ntpservers) + pools, servers = ntp.internal_to_pools_and_servers(anaconda.ksdata.timezone.ntpservers) + ntp.save_servers_to_config(pools, servers) if not anaconda.ksdata.timezone.nontp: iutil.start_service("chronyd") - # try to load firmware language - localization.load_firmware_language(ksdata.lang) - # FIXME: This will need to be made cleaner once this file starts to take # shape with the new UI code. anaconda._intf.setup(ksdata) diff --git a/anaconda/anaconda.spec b/anaconda/anaconda.spec index 09a329f..89d4e3c 100644 --- a/anaconda/anaconda.spec +++ b/anaconda/anaconda.spec @@ -2,7 +2,7 @@ Summary: Graphical system installer Name: anaconda -Version: 21.48.21 +Version: 22.20.13 Release: 12%{?dist} Epoch: 1000 License: GPLv2+ @@ -10,7 +10,7 @@ Group: Applications/System URL: http://fedoraproject.org/wiki/Anaconda # To generate Source0 do: -# git clone http://git.fedorahosted.org/git/anaconda.git +# git clone https://github.com/rhinstaller/anaconda # git checkout -b archive-branch anaconda-%{version}-%{release} # ./autogen.sh # make dist @@ -28,17 +28,15 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) # Also update in AM_GNU_GETTEXT_VERSION in configure.ac %define gettextver 0.18.3 %define intltoolver 0.31.2-3 -%define pykickstartver 1.99.63 +%define pykickstartver 1.99.66 %define yumver 3.4.3-91 %define dnfver 0.4.18 %define partedver 1.8.1 %define pypartedver 2.5-2 -%define pythonpyblockver 0.45 %define nmver 0.9.9.0-10.git20130906 %define dbusver 1.2.3 %define yumutilsver 1.1.11-3 %define mehver 0.23-1 -%define sckeyboardver 1.3.1 %define firewalldver 0.3.5-1 %define pythonurlgrabberver 3.9.1-5 %define utillinuxver 2.15.1 @@ -51,6 +49,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) %define langtablever 0.0.18-1 %define libxklavierver 5.4 %define libtimezonemapver 0.4.1-2 +%define helpver 22.1-1 BuildRequires: audit-libs-devel BuildRequires: gettext >= %{gettextver} @@ -60,7 +59,6 @@ BuildRequires: gtk3-devel-docs BuildRequires: glib2-doc BuildRequires: gobject-introspection-devel BuildRequires: glade-devel -BuildRequires: pygobject3 BuildRequires: intltool >= %{intltoolver} BuildRequires: libgnomekbd-devel BuildRequires: libxklavier-devel >= %{libxklavierver} @@ -73,11 +71,6 @@ BuildRequires: python-devel BuildRequires: python-urlgrabber >= %{pythonurlgrabberver} BuildRequires: python-nose BuildRequires: systemd -BuildRequires: yum >= %{yumver} -BuildRequires: NetworkManager-devel >= %{nmver} -BuildRequires: NetworkManager-glib-devel >= %{nmver} -BuildRequires: dbus-devel >= %{dbusver} -BuildRequires: dbus-python BuildRequires: rpm-devel >= %{rpmver} BuildRequires: libarchive-devel >= %{libarchivever} %ifarch %livearches @@ -98,7 +91,7 @@ The anaconda package is a metapackage for the Anaconda installer. %package core Summary: Core of the Anaconda installer Requires: dnf >= %{dnfver} -Requires: python-blivet >= 1:0.61.9 +Requires: python-blivet >= 1:1.0.1 Requires: python-meh >= %{mehver} Requires: libreport-anaconda >= 2.0.21-1 Requires: libselinux-python @@ -107,6 +100,7 @@ Requires: parted >= %{partedver} Requires: pyparted >= %{pypartedver} Requires: yum >= %{yumver} Requires: python-urlgrabber >= %{pythonurlgrabberver} +Requires: python-requests Requires: qubes-artwork Requires: pykickstart >= %{pykickstartver} Requires: langtable-data >= %{langtablever} @@ -118,7 +112,6 @@ Requires: util-linux >= %{utillinuxver} Requires: dbus-python Requires: python-pwquality Requires: python-IPy -Requires: python-nss Requires: pytz Requires: realmd Requires: teamd @@ -132,6 +125,8 @@ Requires: isomd5sum >= %{isomd5sum} Requires: yum-utils >= %{yumutilsver} Requires: createrepo_c Requires: NetworkManager >= %{nmver} +Requires: NetworkManager-glib >= %{nmver} +Requires: NetworkManager-team Requires: dhclient Requires: libselinux-python Requires: kbd @@ -143,14 +138,17 @@ Requires: systemd Requires: fcoe-utils >= %{fcoeutilsver} %endif Requires: iscsi-initiator-utils >= %{iscsiver} -%ifarch %{ix86} x86_64 ia64 -Requires: dmidecode +%ifarch %{ix86} x86_64 %if ! 0%{?rhel} Requires: hfsplus-tools %endif %endif Requires: python-coverage +Requires: pygobject3-base + +# Used by rescue.py and the low RAM message in /sbin/anaconda +Requires: newt-python # required because of the rescue mode and VNC question Requires: anaconda-tui = %{epoch}:%{version}-%{release} @@ -184,7 +182,12 @@ Requires: keybinder3 %ifnarch s390 s390x Requires: NetworkManager-wifi %endif +Requires: anaconda-user-help >= %{helpver} Requires: yelp +Requires: pygobject3 + +# Needed to compile the gsettings files +BuildRequires: gsettings-desktop-schemas %description gui This package contains graphical user interface for the Anaconda installer. @@ -199,7 +202,6 @@ This package contains textual user interface for the Anaconda installer. %package widgets Summary: A set of custom GTK+ widgets for use with anaconda Group: System Environment/Libraries -Requires: pygobject3 Requires: python %description widgets @@ -247,6 +249,10 @@ find %{buildroot} -type f -name "*.la" | xargs %{__rm} %ifarch %livearches desktop-file-install ---dir=%{buildroot}%{_datadir}/applications %{buildroot}%{_datadir}/applications/liveinst.desktop + +# Add a sitecustomize.py to be loaded by liveinst +mkdir -p %{buildroot}%{_datadir}/anaconda/site-python +install -m 0644 pyanaconda/sitecustomize.py %{buildroot}%{_datadir}/anaconda/site-python/ %endif # NOTE: If you see "error: Installed (but unpackaged) file(s) found" that include liveinst files, # check the IS_LIVEINST_ARCH in configure.ac to make sure your architecture is properly defined @@ -268,19 +274,18 @@ update-desktop-database &> /dev/null || : %endif %files -%doc COPYING %files core -f %{name}.lang -%doc COPYING +%license COPYING %{_unitdir}/* %{_prefix}/lib/systemd/system-generators/* %{_bindir}/instperf +%{_bindir}/anaconda-disable-nm-ibft-plugin %{_sbindir}/anaconda %{_sbindir}/handle-sshpw %{_datadir}/anaconda -%{_datadir}/anaconda/help/* -%exclude %{_datadir}/anaconda/tzmapdata %{_prefix}/libexec/anaconda +%exclude %{_prefix}/libexec/anaconda/dd_* %{_libdir}/python*/site-packages/pyanaconda/* %exclude %{_libdir}/python*/site-packages/pyanaconda/rescue.py* %exclude %{_libdir}/python*/site-packages/pyanaconda/text.py* @@ -299,6 +304,8 @@ update-desktop-database &> /dev/null || : %files gui %{_libdir}/python*/site-packages/pyanaconda/ui/gui/* +%{_datadir}/anaconda/window-manager/glib-2.0/schemas/* +%{_datadir}/themes/Anaconda/* %files tui %{_libdir}/python*/site-packages/pyanaconda/rescue.py @@ -322,246 +329,875 @@ update-desktop-database &> /dev/null || : %{_prefix}/libexec/anaconda/dd_* %changelog -* Tue Dec 02 2014 Vratislav Podzimek - 21.48.21-1 -- Revert "Make Python's default encoding detection work on Live installations - (#1169019)" (awilliam) +* Tue May 19 2015 Samantha N. Bueno - 22.20.13-1 +- Fix a typo that caused us to discard corrected target sizes. (#1211746) + (dlehman) -* Mon Dec 01 2014 Samantha N. Bueno - 21.48.20-1 -- Revert "Search for a valid stage1 device on disks with stage1 mount points - (#1168118)" (sbueno+anaconda) +* Thu May 07 2015 Samantha N. Bueno - 22.20.12-1 +- Force an encoding of utf-8 on liveinst installs (#1217504) (dshea) +- Catch libblockdev's CryptoError when trying to unlock LUKS (#1217438) + (vpodzime) +- Distinguish between NTP pools and servers in GUI (vpodzime) +- Add support for chrony pool directive (mlichvar) +- Configure proxy settings for dnf payload (#1211122) (bcl) + +* Mon Apr 27 2015 Samantha N. Bueno - 22.20.11-1 +- Create and use snapshot of on-disk storage with no modifications (#1166598) + (vpodzime) +- Implement the class for storage snapshots (vpodzime) +- Prevent any changes in the StorageSpoke if just going back (vpodzime) +- Make StorageSpoke's on_back_clicked less complicated (vpodzime) + +* Thu Apr 23 2015 Samantha N. Bueno - 22.20.10-1 +- Ignore addon and anaconda sections in handle-sshpw (bcl) +- Ignore %%anaconda section in parse-kickstart (bcl) +- Remove the custom help button from the toolbar (bcl) +- Make anaconda more unicode-y (#1205183) (dshea) +- Consolidate the language environment variables. (#1209563) (dshea) +- Revert "Make Python's default encoding detection work on Live installations" + (#1209563) (dshea) +- iscsi: pass rd.* options of devices to be mouted in dracut (#1192398) + (rvykydal) +- Remove the old custom partitioning help dialog (mkolman) +- Reset the back_clicked flag if we stay on the Storage spoke (#1210003) + (vpodzime) +- Mark the back_clicked attribute of the Storage spoke as private (vpodzime) +- Set both .format's and .originalFormat's passphrase on unlock (#1208979) + (vpodzime) +- Make the Encrypt checkbox insensitive for encrypted non-BTRFS devices + (#1210254) (vpodzime) +- Don't unconditionally set ksdata.lang.seen to True (#1209927) (mkolman) + +* Tue Apr 07 2015 Samantha N. Bueno - 22.20.9-1 +- Revert "allow /boot on btrfs subvol or filesystem" (bcl) +- Connect scroll adjustments in the right class (#1206472) (dshea) + +* Thu Apr 02 2015 Samantha N. Bueno - 22.20.8-1 +- Allow overriding password policy check (sgallagh) +- Do not use min_luks_entropy with pre-existing devices (#1206101) (dshea) +- Remove the dnf cache directory when resetting the repo (dshea) +- Do not add separators to the addon list when not needed (dshea) +- Only use the instclass environment if it actually exists. (#1198953) (dshea) +- Prevent Storage spoke Done button method from multiple launch (jkonecny) +- Prevent spokes from being exited more times. (jkonecny) + +* Thu Mar 26 2015 Samantha N. Bueno - 22.20.7-1 +- Add documentation on %%anaconda kickstart command (bcl) +- Change --skip-tx to --skip-zanata in scratch-bumpver (bcl) +- Update translation documentation for Zanata (bcl) +- Switch translation support to fedora.zanata.org (bcl) +- Only depend on pygobject3-base in anaconda-core (#1204469) (mkolman) +- Use proxy when configured for the base repo (#1196953) (sjenning) +- Add boolean as return to ThreadManager.wait (jkonecny) +- Assume UTC if setting the system time without a timezone (#1200444) (dshea) +- Make sure LANG is always set to something (#1201896) (dshea) +- Implement %%anaconda kickstart section for pwpolicy (bcl) +- Add pwpolicy support to TUI interface (bcl) +- Add pwpolicy for the LUKS passphrase dialog. (bcl) +- Add pwpolicy for the user spoke. (bcl) +- Use pwpolicy for the root password spoke. (bcl) +- Add the text for weak passwords to constants (bcl) + +* Thu Mar 19 2015 Samantha N. Bueno - 22.20.6-1 +- Handle /boot on btrfs for live (#1200539) (bcl) +- Switch back to urllib for determining livepayload size (dshea) +- Revert "Replace python-urlgrabber with python-requests (#1141242)" (dshea) +- Tweak tmux configuration file (jkonecny) + +* Tue Mar 17 2015 Samantha N. Bueno - 22.20.5-1 +- Fix enlightbox call in ZFCPDialog. (#1151144) (sbueno+anaconda) +- Improve the addon repo name collision code (#1125322) (bcl) +- Handle New_Repository name collision source spoke (#1125322) (bcl) + +* Fri Mar 13 2015 Samantha N. Bueno - 22.20.4-1 +- Only insert strings into the environment (#1201411) (dshea) +- Fix the rescue kernel version list in writeBootLoader (#1201429) (dshea) +- Fix the handling of nfs:// URLs. (dshea) +- Add glob support for the -a/--add option in makeupdates (mkolman) +- Require newt-python in anaconda-core (dshea) +- Fix the help button mnemonic display on spokes (dshea) +- Display an error for exceptions during GUI setup (dshea) +- Remove unused invisible char properties (dshea) +- Add a check for invisible_char validity (dshea) +- Connect viewport adjustments to child focus adjustments (#1192155) (dshea) +- Try using the global LUKS passphrase if none is given for LV/part (#1196112) + (vpodzime) + +* Wed Mar 04 2015 Samantha N. Bueno - 22.20.3-1 +- Fix the import of mountExistingSystem (vpodzime) +- Fix import error in anaconda-cleanup. (sbueno+anaconda) +- Use the new static method to get possible PE sizes (vpodzime) + +* Tue Mar 03 2015 Samantha N. Bueno - 22.20.2-1 +- Fix a bad usage of execWithRedirect (#1197290) (dshea) +- Use the LUKS device for swap in fstab (#1196200) (vpodzime) +- Clear TUI source spoke errors that may have been leftover from a prior + attempt. (#1192259) (sbueno+anaconda) +- install.py: fix the 'is team device' check (awilliam) +- Revert "Update txconfig for f22-branch (again)...." (sbueno+anaconda) +- Revert "makebumpver needs to know about anaconda-1 transifex name" + (sbueno+anaconda) + +* Fri Feb 20 2015 Samantha N. Bueno - 22.20.1-1 +- Remove unused imports (dshea) +- Check for unused imports in __init__ files (dshea) +- Remove timestamp-based version support. (#1194494) (dshea) +- Add test lib methods to check regexes (dshea) +- Update txconfig for f22-branch (again).... (sbueno+anaconda) +- Update txconfig for f22-branch. (sbueno+anaconda) +- Cleanup BuildRequires (mkolman) +- Remove obsolete imports. (amulhern) +- Make print statement print output w/out surrounding parentheses. (amulhern) +- Remove an unused import (dshea) +- rpmostreepayload: Honor noverifyssl (walters) +- typo: packaging: Don't vary name of "verified" (walters) +- Disable the metacity mouse-button-modifier setting (dshea) +- Fix completion setting in TUI language spoke. (#1192230) (sbueno+anaconda) +- Remove the pylint false positives for the GLib module (dshea) +- Use ExtendAction for --ignore flag (amulhern) +- Use a simple ExtendAction for add_rpms option. (amulhern) +- Fix log message formating (mkolman) +- Don't clear nonexistent DNF package download location (#1193121) (mkolman) + +* Mon Feb 16 2015 Brian C. Lane - 22.20-1 +- Make range usage Python 3 compatible (#1014220) (mkolman) +- Make map() usage Python 3 compatible (#1014220) (mkolman) +- Make the iter*() dictionary methods Python 3 compatible (#1014220) (mkolman) +- Remove the autopart.py module from POTFILES.in (vpodzime) +- Adapt to autopart and installation-specific code move in blivet (#1192702) + (vpodzime) +- Revert "Move autopart functionality to anaconda" (vpodzime) + +* Fri Feb 13 2015 Brian C. Lane - 22.19-1 +- Make sure yum is included in the packageset for yumpayload (#1152753) (bcl) +- Tweak parallel args. (clumens) +- Remove the Encoding entry from the .desktop file (dshea) +- Add an option to startProgram to not reset the locale (dshea) +- Set $LIBUSER_CONF early (dshea) +- Do not set $TZ (dshea) +- Assume that a bunch of digits in a version number is a timestamp (dshea) +- Avoid setting $LANG and $LANGUAGE, except where we can't (dshea) +- Add a parameter to iutil.startProgram to extend the environment (dshea) +- Add a method to set environment variables for child processes (dshea) +- Set $DISPLAY before threads are started. (dshea) +- Add a pylint module to look for modifications to the environment (dshea) +- Remotely do kickstart tests as a kstest user instead of root. (clumens) +- Add some documentation. (clumens) +- Do all package/group checking in %%post to save a reboot. (clumens) +- Support kickstart test jobs out to multiple computers with parallel. + (clumens) +- Make it possible to ignore individual newly added dependencies (mkolman) +- Remove the old_tests directory (bcl) +- Use /usr/bin/python2 in scripts (bcl) +- Cleanup some pylint errors in analog (bcl) + +* Fri Feb 06 2015 Brian C. Lane - 22.18-1 +- dracut needs iscsi_firmware cmdline arg (#1185792) (bcl) +- Clear the default titlebar text (mkolman) +- Move the pygobject3 dependency to the core package (#1188850) (mkolman) +- Bump the livecd making timeout to 90 minutes. (clumens) +- If a VM isn't going to finish in 60 minutes, it likely isn't going to finish. + (clumens) +- Check that package globs install more than just the first package. (dshea) +- Remove some stray parenthesis (#1188618) (dshea) +- Replace urllib with python-requests for network access (#1014220) (mkolman) +- The repo has moved to github, so reflect that in the spec. (clumens) +- Fix pylint problems with the autopart commit. (clumens) +- network: adapt to NM fixing virtual device disconnection (#1084953) + (rvykydal) +- Replace xrange() with range() (vpodzime) +- Move autopart functionality to anaconda (vpodzime) + +* Fri Jan 30 2015 Brian C. Lane - 22.17-1 +- Fix pylint complaints about log lines (bcl) +- Add JENKINS_PROXY support to makebumpver (bcl) +- Copy the kickstart package tests for testing with yum (bcl) +- Pass multiple args to runone in run_kickstart_tests.sh (bcl) +- Ignore some accelerator collisions on the filter dialog. (clumens) +- Remove an unused variable. (clumens) +- network: fix a typo making creating virtual devices in %%pre fail (#1075195) + (rvykydal) +- network: support for bridge, require pykickstart with the support (#1075195) + (rvykydal) +- network: Catch exception from NM failing to create a bridge device (#1075195) + (rvykydal) +- network: add bridge support for kickstart %%pre phase (#1075195) (rvykydal) +- network: generate kickstart commands for bridge devices (#1075195) (rvykydal) +- network: add bridge support to kickstart (#1075195) (rvykydal) +- network: support for adding bridge devices (#1075195) (rvykydal) +- network: display bridge devices in status (#1075195) (rvykydal) +- Fix position of Refresh List button in filter spoke (#1065716) (rvykydal) +- Fix accelerator collision of Refresh button (#1065716) (rvykydal) +- gui: add Refresh button to network storage UI (#1065716) (rvykydal) +- iscsi: display portal (address:port) of node in node list (#1114820) + (rvykydal) +- iscsi: when logging into nodes consider ip:port of node (#1114820) (rvykydal) +- network: display only actual fqdn of ip we offer for vnc connection + (#1089429) (rvykydal) +- network: GUI: reactivate connection automatically after configuration + (#1033063) (rvykydal) +- Don't traceback if connection does not have read-only setting (#1158919) + (rvykydal) +- network: enable NM ibft plugin only for ip=ibft boot option (#804511) + (rvykydal) +- network: add support for vlan tag in iBFT (#804511) (rvykydal) +- network: pass team opts to dracut for netroot (#1075666) (rvykydal) +- Remove unused version macros from anaconda.spec.in (vpodzime) +- Don't process continue-clicked events for windows that aren't shown. + (clumens) +- Add back an empty %%files for the anaconda metapackage (dshea) +- Do not include dd_list and dd_extract in the anaconda-core package. (clumens) +- Replace long usage with int (#1014220) (mkolman) +- Do not use sys.exc_type (#1014220) (mkolman) +- Replace StandardError with Exception (#1014220) (mkolman) +- Make filter() usage Python 3 compatible (#1014220) (mkolman) +- network: add teamd package if team is used during installation (#1185670) + (rvykydal) +- network: add NetworkManager-team (#1182633) (rvykydal) +- Don't allow weak LUKS passwords either (bcl) +- Use %%license in anaconda.spec.in (bcl) +- Don't allow weak passwords (text mode). (sbueno+anaconda) +- Remove the press done twice to exit text (bcl) +- Don't allow weak user passwords (bcl) +- Don't allow weak root passwords (bcl) +- Increase minimum password length to 8 (bcl) +- Remove the unused re import from nm.py. (clumens) +- Remove IPy from nm.py for python 23 compatibility. (rvykydal) +- Show empty VGs in the custom spoke. (dlehman) +- Use the rpm database to find kernel package versions (#1074358) (dshea) +- Check whether a payload has an instclass (#1185588) (dshea) +- Remove the unused indexed_dict module (vpodzime) +- Use threadMgr to wait for exception handling to finish (vpodzime) +- Add a method for waiting for error handling to finish (vpodzime) +- Move HW errors processing to the code that runs in the main thread (vpodzime) +- Replace python-urlgrabber with python-requests (#1141242) (mkolman) + +* Fri Jan 23 2015 Brian C. Lane - 22.16-1 +- Add some tests for kickstart and package selection for dnf. (clumens) +- Double quote when printing error results from a kickstart test. (clumens) +- Restrict payload kernel versions to kernels in the payload (#1074358) (dshea) +- Actually add the new definition of an already-defined repo. (clumens) +- Move hdiso handling code to PackagePayload (#1180765) (dshea) +- Actually install the metacity theme data (dshea) +- Show the event box immediately when setting infobar messages. (dshea) +- Move environment group selection logic to PackagePayload (#1179362) (dshea) +- Add a parameter to environmentGroups for wheter to include optionlist. + (dshea) +- Remove unused methods for deselecting environments (dshea) + +* Fri Jan 16 2015 Brian C. Lane - 22.15-1 +- makebumpver needs to know about anaconda-1 transifex name (bcl) +- Switch to temporary transifex branch (bcl) +- Fix an issue in the previous pre-existing repo kickstart patch. (clumens) +- Require the livecd target to be larger now. (clumens) +- Hook up jenkins support into makebumpver. (clumens) +- Change default console font to eurlatgr (myllynen) +- Update help text for the nodnf option (mkolman) +- Run AnacondaExceptionHandler in cmdline mode (bcl) +- Install a metacity theme to remove the titlebar. (dshea) +- Move metacity gsettings overrides into anaconda (dshea) +- Maximize anaconda instead of running fullscreen (#1164457) (dshea) +- Use a formatter on remotelog lines (bcl) +- Include NetworkManager-glib in anaconda-core (bcl) +- Make colon optional while adding iSCSI Initiator Name (sujithpshankar) +- If using pre-existing, no size needs to be specified in ksdata (#1172172) + (amulhern) +- Add support for sending logs to a remote host with --remotelog (bcl) +- Implement askmethod in dnfpayload (dshea) +- Add an installclass property for the default package environment (#1175826) + (dshea) +- Fix the FIXME re: tui default software selection (dshea) +- Add missing translation contexts for TUI navigation keys (dshea) +- Translate 'c' in the tui software spoke (dshea) +- Expect addons to have categories for both GUI and TUI (vpodzime) +- Remove an unused import in pyanaconda/ui/__init__.py (vpodzime) + +* Fri Jan 09 2015 Brian C. Lane - 22.14-1 +- Add error checks to liveimg mount code (#1178703) (bcl) +- Switch kickstart tests to doing VNC instead of graphical. (clumens) +- Updates for new Size.convertTo() spec. (amulhern) +- Force a background in the main GtkBox in anaconda windows. (dshea) +- Animate the screen transitions. (dshea) +- Implement DNFPayload.environmentOptionIsDefault (#1179905) (dshea) +- Remove the directory dnf downloaded packages into. (clumens) +- Allow specifying pre-defined repos via kickstart with dnf backend (#1177988). + (clumens) +- Get rid of unnecessary python disable-msg in zfcp spoke. (sbueno+anaconda) +- Fix typo in commit 472be66b2af2af69e7eac15ec9c94ccc818e12b5. (dlehman) +- Fix some pylint errors in the zfcp panel. (sbueno+anaconda) +- Fix an accelerator collision found on the filter page. (sbueno+anaconda) +- Fix some issues pylint found. (sbueno+anaconda) +- Show disk paths on Other page in advstorage. (sbueno+anaconda) +- Don't treat the baserepo as special when gathering metadata (#1177502) + (dshea) +- Make dnf._base and dnf._base.comps always available. (dshea) +- Remove the checks for whether dnf and rpm were imported (dshea) +- Remove obsolete packaging code. (dshea) +- Do not bypass name setters in the custom spoke. (#1138370) (dlehman) +- Preserve kickstart url behavior for mirrorlist (#1109933) (bcl) +- Use a backslash to escape nfs spaces instead of x20 (#1109933) (bcl) +- Add missing translation context for Add ECKD DASD button in advstorage. + (sbueno+anaconda) +- Add translation contexts for z and zfcp panel in advstorage. + (sbueno+anaconda) +- Convert devices size to str for GUI for zFCP devices (amulhern) +- Fix string formatting of zFCP devices. (sbueno+anaconda) +- Fix the way zFCP devices are displayed in storage spoke. (#1024902) + (sbueno+anaconda) +- Show labels on Add zFCP dialog. (sbueno+anaconda) +- Fix failure to search by LUN in advanced storage spoke. (sbueno+anaconda) +- Get rid of the clear button in advanced storage spoke. (sbueno+anaconda) +- Fix up the z Panel in advanced storage. (sbueno+anaconda) +- Add support for adding zFCP devices in the GUI (sbueno+anaconda) +- Remove DirtyFSError related callbacks and entries. (amulhern) +- Remove allowDirty parameter from mountExistingSystem() call. (amulhern) +- Remove old workaround for missing EFI bits. (dmarlin) +- Wait for payload thread in TUI software spoke. (#1178214) (sbueno+anaconda) +- Start the network before the display (#1167103) (dshea) + +* Fri Dec 19 2014 Brian C. Lane - 22.13-1 +- Print the result to the log, not the variable name. (clumens) +- Ellipsize comboboxes (#1170275) (dshea) +- Allow the columns of the container combo box to flow (#1170275) (dshea) +- Allow specifying how much from kickstart_tests to keep. (clumens) +- Fix up two problems in run_kickstart_tests.sh. (clumens) +- Fix detecting errors in groups-and-envs-1.ks. (clumens) +- Add a network command to the various kickstart test ks files. (clumens) +- Reorganize run_kickstart_tests.sh a bit to use parallel. (clumens) +- Use the anaconda-user-help package for help content (mkolman) +- Fix which TUI field is being checked for which input (#1169533) (dshea) +- Fix pylint-reported issues in RPMOSTreePayload (vpodzime) +- rpmostreepayload: Rework remote add handling (walters) +- Remove Requires: python-nss (vpodzime) +- If there's no boot.iso, skip the kickstart tests. (clumens) +- Test that a kickstart file with both an environment and group installs both. + (clumens) +- Move the ostree test out of its own directory. (clumens) +- Add a general-purpose kickstart-driven testing setup. (clumens) +- Move the pykickstart version test into a different subdirectory. (clumens) +- Really hide and show passphrase warnings (#1162828) (dshea) +- Unsetup the payload on the way out of anaconda (#1164577) (dshea) +- Beware of 0 being the same bool value as None when setting time (vpodzime) +- Fix the last member of the struct_time struct (vpodzime) +- Use a flag to determine if the datetime spoke is shown (vpodzime) +- Put AM/PM label and buttons in a revealer and hide/unhide them (vpodzime) +- Fix issues with the date&time not being updated on timezone changes + (vpodzime) +- Fix the way we create the list of DASDs needing dasdfmt. (#1073982) + (sbueno+anaconda) +- Fix threading issues for dasdfmt in gui storage. (#1073982) (sbueno+anaconda) +- Add sshkey kickstart command (bcl) +- Skip setting up env and groups in software spoke for ks (#1173350) (bcl) +- Add missing dnf package selection support (#1169056) (bcl) +- Add variable substitution to DNF (#1164803) (bcl) +- Simplify and robustify handling of fstype combo box. (amulhern) +- Fix warnings about the default parameter that gdk deprecated (dshea) +- Remove the color override from MountpointSelector. (dshea) +- Move the layout indicator color to css and fix the colors (dshea) +- Don't crash in pre-commit if no files changed (dshea) +- Make the Selected Disks and Configure Mount Point dialogs wider (#1171834). + (clumens) +- Sync up the Selected Disks and Configure Mount Points dialogs (#1171838). + (clumens) +- Make sure /boot is not LVM LV if we're on s390x (#873135) (sbueno+anaconda) +- Only show the "SYSTEM" heading if there are data mount points under it. + (clumens) +- Remove an unused import in rpmostreepayload.py. (clumens) +- Use DNF by default (#1156483) (mkolman) +- Check system-release for whether to enable betanag or not (#1168829). + (clumens) +- rpmostreepayload: Avoid shutil.copytree in favor of cp -r to fix symlinks + (walters) +- Look for Requires: and BuildRequires: at the front of a line. (clumens) +- Don't attempt to install anaconda packages from the install-requires target. + (clumens) +- Remove _assureLogoImage (dshea) +- Add a stylesheet property to BaseInstallClass (dshea) +- Fix EOF error that occurs if user input required in x3270. (#1171135) + (jstodola) +- Print an error when the kickstart file is missing (bcl) +- Remove UserInterface.basepath and UserInterface.basemask definitions. + (amulhern) +- Remove pointless overrides identified by the pointless override checker. + (amulhern) +- Add a simple pointless-override checker to pylint checkers. (amulhern) + +* Thu Dec 04 2014 Brian C. Lane - 22.12-1 +- add code so that initramfs created for rescue kernel (#1170589) (gczarcinski) +- Start vncconfig for cutNpaste (hamzy) +- Handle unstaged changes in the pre-commit hook. (dshea) +- Use git status -z (dshea) - Make Python's default encoding detection work on Live installations (#1169019) (vpodzime) - Force translation files download instead of skipping them (#1169023) (vpodzime) - -* Fri Nov 28 2014 Vratislav Podzimek - 21.48.19-1 -- Search for a valid stage1 device on disks with stage1 mount points (#1168118) - (awilliam) - -* Thu Nov 27 2014 Vratislav Podzimek - 21.48.18-1 -- Tell curl it should follow redirects when fetching updates.img (#1168561) - (vpodzime) -- Snapshot free space after clearpart for swap suggestion (#1167965) (vpodzime) - -* Wed Nov 26 2014 Samantha N. Bueno - 21.48.17-1 +- Completely disable storage tests for the moment. (clumens) +- datetime_spoke: Fix warnings about removing nonexistent source (walters) +- Temporarily disable the BTRFSOnNonBTRFSComponent test. (clumens) +- Remove a slightly lighter grey background from the center of the hubs. + (clumens) +- Actually fix the message dropping commit. (clumens) - Make sure storage info bar is displayed (#1166730) (bcl) -- Fix SELINUX_DEFAULT import (#1167047) (bcl) -- Fix noselinux cmdline default (#1167047) (bcl) +- Clear Update Settings when Done clicked (#1167014) (bcl) +- Fix PWQError issues. (sbueno+anaconda) +- network: Add some doc strings (walters) +- It's spoke.title, not spoke.name (#1167036). (clumens) -* Thu Nov 20 2014 Samantha N. Bueno - 21.48.16-1 +* Fri Nov 21 2014 Brian C. Lane - 22.11-1 +- Skip tui askvnc reboot for dirinstall (#1164254) (bcl) +- If a message is for a spoke not on the current hub, throw it away. (clumens) +- Find storage test cases automatically. (clumens) +- Add new storage test cases that reuse results of earlier autopart runs. + (clumens) - Support high contrast mode in fedora-welcome (#1160499) (dshea) - -* Tue Nov 18 2014 Samantha N. Bueno - 21.48.15-1 +- How the GUI test suite disk is displayed has changed. (clumens) - do not delete liveimg --url=file:/// file (gczarcinski) +- Add support for doing a liveimg kickstart with local file (#1140358) (bcl) +- Create missing parent directories for user's home directory (#1163775) (bcl) +- Related bug can have different fixed-in and state (bcl) - Provide useful hints on TTY1 during the installation (mkolman) -- Fix typo from commit 9b3259874. (#1120964) (dlehman) -- Remove the old custom partitioning help dialog (mkolman) +- Decrease memory requirements on gui tests, and make that attr private. + (clumens) +- Don't use blivet in the gui tests. (clumens) +- Use MiB/GiB instead of MB/GB in GUI tests. (clumens) +- Make the No Space dialog look less terrible. (clumens) +- Add a test case where it's impossible to reclaim. (clumens) +- Use blivet's Size class instead of ints and such. (clumens) +- Get the gui tests running in parallel. (clumens) +- Add a basic test of the reclaim dialog. (clumens) +- Make images in raw format instead of qcow2. (clumens) +- Allow specifying which of the GUI tests you want to run. (clumens) - Check if we read something when emptying stdin queue (vpodzime) - Require min entropy for 'part --encrypted' devices (#1162695) (vpodzime) - Don't rely on terminal attributes being configurable (#1162702) (vpodzime) +- Check for a GLib source ID of None in unwatchAllProcesses. (dshea) - Disable payloads that failed to setup (#1162732) (dshea) -- Don't change langpacks config of installer environment (#1066017) (rvykydal) - -* Tue Nov 11 2014 Samantha N. Bueno - 21.48.14-1 -- Add support for doing a liveimg kickstart with local file (#1140358) (bcl) +- Only enable non-interactive yum plugins (#1111535) (dshea) - Add a placeholder for a product-specific logo (dshea) - Load a stylesheet from product.img (dshea) - Fix make distcheck (mkolman) - Include help content in the Anaconda tarball (mkolman) - Fix typo causing traceback when NTP is turned ON/OFF (vpodzime) -- Unpack the callback data given to us by blivet (vpodzime) -- Add timeout to callbacks waiting for enough entropy (#1073679) (vpodzime) +- Use /var/tmp for the temp directory when installing anaconda. (clumens) - Prevent tb on s390x when de-selecting a DASD and doing custom part. (sbueno+anaconda) - Revert "Revert productName repo name change (#1128474)" (bcl) +- Remove a comment that is a blatant lie. (clumens) +- Fix an environment variable setting in the test environment. (clumens) - Update the background image paths used in Fedora. (dshea) -- dracut/save-initramfs.sh: don't save /tmp (wwoods) - Add a pylint module to detect uses of interruptible system calls. (dshea) - Wrap interruptible system calls in a loop (#1160041) (dshea) -- Warn users about liveinst usage of --updates (#1153550) (bcl) +- Unpack the callback data given to us by blivet (vpodzime) +- Add timeout to callbacks waiting for enough entropy (#1073679) (vpodzime) + +* Tue Nov 04 2014 Brian C. Lane - 22.10-1 +- Remove gui, install, and ostree tests from TESTS. (clumens) +- Update the ostree test for the new ostree+grub patches. (clumens) +- Add a timeout when the ostree test checks for proper booting. (clumens) +- bootloader: Bridge efi_dir configuration earlier for rpmostreepayload + (walters) +- rpmostreepayload: Handle grub2+EFI layout (walters) +- rpmostreepayload: Copy all subdirectories of /usr/lib/ostree-boot (walters) +- Handle the case of rpmostreepayload + GRUB2 (walters) +- Test adding, removing, and reordering keyboard layouts. (clumens) +- Test displaying the help viewer on every screen. (clumens) +- Add functions to UITestCase to grab the contents of a view. (clumens) +- Extend the keyboard GUI test to test adding layout switching. (clumens) +- Add checks for selected language/locale on the welcome screen. (clumens) - Catch EOFError in raw_input (#1158841) (bcl) - Ensure we are specifying sensible target sizes for resize. (#1120964) (dlehman) - Set the autopart fstype for boot too (#1112697) (bcl) -- Unconditionally clear the process handle when nm-c-e exits (#1132645) (dshea) -- Make anaconda more scrollable (#1135024) (dshea) +- Ensure we are specifying sensible target sizes for resize. (#1120964) + (dlehman) +- Rework the placement of items on hubs. (dshea) - Lightly rearrange the nav_area (dshea) - Do not install interactive exception handler in cmdline mode (#1155979) (vpodzime) +- Remove dmidecode from Requires: (vpodzime) - Wait until all spokes are setup before updating continue button (bcl) - Allow adding prepboot to a blank disk in custom (#1155660) (bcl) +- Make anaconda more scrollable (#1135024) (dshea) - Remove unused imports (vpodzime) - -* Tue Oct 28 2014 Samantha N. Bueno - 21.48.13-1 -- Fix handling of md fwraid names in kickstart bootloader command. (#1156354) - (dlehman) -- Fix switching environments when no environment is selected (#1155756) (dshea) -- Use an empty string for no root password instead of None (#1155576) (dshea) - Just preserve the %%addon header args if an addon is missing (#1155026) (vpodzime) +- Add a test to verify the help dialog pops up. (clumens) +- Look up most widgets relative to the currently displayed screen. (clumens) +- Make a few more updates for labels that have changed in the GUI. (clumens) +- Warn users about liveinst usage of --updates (#1153550) (bcl) +- Fix handling of md fwraid names in kickstart bootloader command. (#1156354) + (dlehman) +- Use an empty string for no root password instead of None (#1155576) (dshea) +- Don't allow related bugs without acks (bcl) +- Fix switching environments when no environment is selected (#1018226) (dshea) +- Make size_from_input() and size_from_entry() methods handier. (amulhern) +- Changes around handling of size entries in custom spoke. (amulhern) +- network: handle dbus UnknownMethod exception on invalid objects (#1061796) + (rvykydal) -* Thu Oct 23 2014 Samantha N. Bueno - 21.48.12-1 -- Fix a spelling error (#1153672) (dshea) +* Wed Oct 22 2014 Brian C. Lane - 22.9-1 +- When I renamed the date & time spoke, I missed one string. (clumens) +- Fix two more problems with spoke selectors in GUI testing. (clumens) +- Fix the GRUB raid1 tests (dshea) +- Add syslinux to the packages in the gui_testing kickstart file, too. + (clumens) +- Update the gui_testing kickstart file for productization changes. (clumens) - Update checkSizes to work in terms of Size objects (#1129629). (clumens) - -* Mon Oct 20 2014 Samantha N. Bueno - 21.48.11-1 +- Install grub to all disks in a btrfs raid1 /boot (#989644) (dshea) +- Really fix issue with starting in cmdline mode on s390x. (sbueno+anaconda) +- The network spoke's title has changed. Reflect that in the test. (clumens) +- Grab memory.dat from running the GUI test. (clumens) - Don't panic prematurely on a missing size (#1154190) (amulhern) +- Fix more messages the new pylint found. (clumens) +- dracut/save-initramfs.sh: don't save /tmp (wwoods) +- Get rid of some unnecessary text from dasdfmt dialog. (sbueno+anaconda) +- Quit if no device type name selected. (amulhern) +- Fix stray comment. (amulhern) +- If there's no attached ANACTEST device, don't attempt to mount and run it. + (clumens) +- Fix a spelling error (#1153672) (dshea) - Log when using updates from /tmp/updates/ (bcl) - Fix # handling in SimpleConfigFile (#1045687) (bcl) - -* Wed Oct 15 2014 Samantha N. Bueno - 21.48.10-1 -- Change our docs that are close to ReST to proper ReST (vpodzime) +- Unconditionally clear the process handle when nm-c-e exits (#1132645) (dshea) +- Remove the code that reads /tmp/vncshell.pid. (dshea) +- Rewrite _bound_size() to bound_size() in storage_utils.py (amulhern) +- Changes for scheduling size change on an existing device (#1076055) + (amulhern) +- Remove too strict condition for changing size (#1076055) (amulhern) +- Omit calculation and use of active_dev_type. (amulhern) +- Add a method that extracts device type name from combo box (amulhern) +- Don't pass use_dev around to internal methods. (amulhern) +- Check identity, not equality, for RaidLevel objects. (amulhern) - Run restorecon on /etc/hostname (#1133368) (bcl) - Add authconfig and firewalld packages when used in ks (#1147687) (bcl) - Allow kickstart with no method (#972265) (bcl) - Fix a typo from 73d3a8e5. (sbueno+anaconda) - Respect both ways how to disable bootloader installation (vpodzime) -- Don't care about crash args in bootloader (#1116323) (vpodzime) -- Add nombr to anaconda to suppress updating MBR (#886502) (gczarcinski) -- Use translated versions of the AM/PM strings consistently (vpodzime) -- Import GUI-specific stuff only when running GUI in entropy handling - (vpodzime) -- Always store the information about display mode in ksdata (vpodzime) -- Make the date format locale-dependent in our GUI (#1044233) (vpodzime) -- A function for resolving date format and order (vpodzime) -- Reorganize the right side of the Custom spoke (#1094856) (vpodzime) -- Move _verifyLUKSDevicesHaveKey to Anaconda's codebase (vpodzime) -- Add support for thin pool profile specification in kickstart (vpodzime) -- Fix file name of the entropy dialog in POTFILES.in (vpodzime) -- Require minimum random data entropy when creating LUKS (#1073679) (vpodzime) -- Give blivet callbacks for reporting partitioning progress (vpodzime) -- Split localed's converted layouts and variants (#1073825) (vpodzime) -- Create free space snapshot before doing custom->autopart (vpodzime) -- Specify thin pool metadata/chunk size only if given by user (#1140635) - (vpodzime) -- Distribute the right docs files (vpodzime) +- Fix a bug unmounting /boot on efi+atomic installs. (clumens) +- Refactor handling of fsCombo considerations. (amulhern) +- Be more restrictive displaying btrfs device type. (amulhern) +- Get rid of unnecessary raid_level variable (amulhern) +- Use Size, not int, for size (#1076055) (amulhern) +- Remove an unused import (dshea) - Don't automatically select environments for kickstart installs (#1018226) (dshea) - Initialize the GUI lock in a way that doesn't break the API (dshea) - Don't check enabledPlugins if plugins are not yet enabled (#1142544) (dshea) -- Really fix an enlightbox call. (dshea) +- Add transifex branch check to makebumpver (bcl) +- Get rid of an unused variable in the localization test. (clumens) - Don't strip accents from the user-inputted keyboard string (dshea) - Convert strings to unicode in have_word_match (#1146581) (dshea) +- Use translated versions of the AM/PM strings consistently (vpodzime) +- Import GUI-specific stuff only when running GUI in entropy handling + (vpodzime) +- Always store the information about display mode in ksdata (vpodzime) +- Connect signals to handlers for day/month/year changes (vpodzime) - Switch to using the new help content path (#1072033) (mkolman) +- Remove unused variables in the datetime_spoke.py module (vpodzime) +- Add nombr to anaconda to suppress updating MBR (#886502) (gczarcinski) +- Make the date format locale-dependent in our GUI (#1044233) (vpodzime) +- A function for resolving date format and order (vpodzime) +- Make device/fs type comboboxes take less space (vpodzime) +- Skip running efibootmgr for noefi mode (#1047904) (bcl) - Fix a race between checking for Gtk.main_level and running Gtk.main (dshea) - Allow recursive lightbox calls (#1147337) (dshea) - Disable the ntp service with --nontp (#1135768) (dshea) -- Ignore partition start if there is a biosboot partition (#1044849) (bcl) -- Require a larger /boot (#1129629). (clumens) -- Remove duplicates when adding new devices (#887526) (bcl) -* Wed Oct 08 2014 Samantha N. Bueno - 21.48.9-1 -- Bump blivet version requires for all the DASD changes in 0.61.4. - (sbueno+anaconda) +* Wed Oct 08 2014 Brian C. Lane - 22.8-1 +- Add a test case for if all anaconda's Requires exist. (clumens) +- Only allow one anaconda instance (#1146735) (dshea) +- Ignore partition start if there is a biosboot partition (#1044849) (bcl) +- Remove duplicates when adding new devices (#887526) (bcl) +- Trim changelog entries from spec file (bcl) - We now need to specify an epoch for the python-blivet version requires. (clumens) +- Remove the last references to tzmapdata (dshea) +- Add VNC to the ostree test arguments. (clumens) - Fix autotools rules to properly include help placeholders (#1072033) (mkolman) -- Modify nm to return defaults when no dbus is available (bcl) -- Skip networkInitialize for image and dir installations (bcl) -- Ignore safe_dbus errors in keyboard setup (bcl) -- Skip syslog for dirinstall (bcl) +- Ignore an accelerator conflict between two Modify labels. (clumens) - s390x: show dialog if kernel cmdline in zipl.conf is too long. (sbueno+anaconda) +- Convert process watching to use GLib before we start a main loop (dshea) +- Convert python signal handlers to GLib signal handlers (dshea) +- Reorganize the right side of the Custom spoke (#1094856) (vpodzime) +- Graphically handle errors arising from ostree repo pull problems. (clumens) +- Fix file name of the entropy dialog in POTFILES.in (vpodzime) +- Add support for thin pool profile specification in kickstart (vpodzime) +- Require minimum random data entropy when creating LUKS (#1073679) (vpodzime) +- Give blivet callbacks for reporting partitioning progress (vpodzime) - Really exit when "Exit installer" in the error dialog is clicked (vpodzime) +- NM-wifi is missing on s390(x) (dan) + +* Tue Sep 30 2014 Brian C. Lane - 22.7-1 +- Fix Welcome spoke not showing up during kickstart installation (#1147943) + (mkolman) - Don't allow /boot on lvm on s390x. (sbueno+anaconda) +- Handle failures to instantiate storage devices when parsing kickstart. + (dlehman) - Add the new langsupport.py TUI spoke to POTFILES.in. (clumens) - Remove the now-unused imports of storageInitialize. (clumens) - Add support for language selection in text mode. (sbueno+anaconda) +- packaging: handle new NFS installation source with inst.stage2=nfs:... + (wwoods) +- Allow cdrom-swapping when doing "inst.ks=cdrom[:...]" (wwoods) +- anaconda-lib.sh: add tell_user() and dev_is_cdrom() (wwoods) - Don't force a user to return to the storage spoke after dasdfmt (sbueno+anaconda) - Don't run storageInitialize after dasdfmt (sbueno+anaconda) +- Shut up, parallel (dshea) +- Really fix unexpected exits in execReadlines (dshea) +- Add a context manager for executing code while UI signals are blocked. + (clumens) +- Avoid the possibility of size variables being unset (#1146585) (dshea) - s390x: Apply disk selection before dasdfmt to preserve data. (sbueno+anaconda) +- Fix a bad use of WIFSIGNALED (dshea) +- Handle 0's returned by Gdk (dshea) +- Adapt to corrected interpetation of logvol --percent. (dlehman) +- Always use iutil to start processes. (dshea) +- Move the X startup logic to iutil (dshea) +- Move process watching to iutil. (dshea) +- Close file descriptors while daemonizing auditd (dshea) +- Add an option to only capture stdout with execWithCapture (dshea) +- Simplify iutil.execReadlines. (dshea) +- Add close_fds to the Popen call. (dshea) +- Add an option to startProgram to reset signal handlers. (dshea) +- Add a method startProgram to handle process starting (dshea) +- Lock program_log_lock closer to where the log is written. (dshea) +- Record early crashes to ipmi (dshea) +- Clear the list of watched PIDs before exiting. (dshea) +- Remove the exitCode parameter from exitHandler. (dshea) +- Warn about uses of the string module. (dshea) +- Import _ from the i18n module instead of hand-crafting a copy of it (dshea) +- Import gettext in iutil instead of passing the module reference to iutil + (dshea) +- Fix a typo in a comment (dshea) +- When running on HiDPI monitors, scale anaconda by a factor of 2 (dshea) +- Sort the contents of the file system type combo box. (clumens) +- Remove the border on the layout testing box. (clumens) +- Explain what the IPMI constants mean. (clumens) +- Don't attempt terminal size detection on the s390 (#1145065) (mkolman) - Don't show the Add DASD button unless on s390x. (sbueno+anaconda) - Don't show the Add DASD button unless on s390x. (sbueno+anaconda) - Preserve network args on s390x. (sbueno+anaconda) -- Deprecate RUNKS cmdline option. (sbueno+anaconda) -- Re-order the tz's in text mode to mirror the graphical order. - (sbueno+anaconda) -- Fix an issue with bad NFS info specified in source spoke. (sbueno+anaconda) -- Warn if software selection size exceeds available space. (sbueno+anaconda) -- Fix q for quit issue in text mode (#997405) (sbueno+anaconda) -- Change the accelerator key for Add DASD label. (sbueno+anaconda) -- Add dialog box for adding DASDs. (sbueno+anaconda) -- Add a button for adding an ECKD DASD. (sbueno+anaconda) -- Change a confusing string in TUI NFS configuration screen. (#1057690) - (sbueno+anaconda) -- NM-wifi is missing on s390(x) (dan) -* Wed Oct 01 2014 Samantha N. Bueno - 21.48.8-1 +* Fri Sep 19 2014 Brian C. Lane - 22.6-1 +- Don't call storage.write for dirinstall (#1120206) (bcl) +- Fix pylint warning from a recent commit. (dlehman) +- Fix the link to the help-button-clicked signal (dshea) +- Assign mnemonics to two checkboxes on the user spoke that didn't have them. + (clumens) +- Remove "MB" from the size string on the HDISO combo box. (clumens) +- Use _Cancel and _Continue mnemonics on these two screens. (clumens) +- Rename to be the TIME & DATE spoke. (clumens) +- Ok -> OK on the proxy dialog. (clumens) +- Handle cancellation of new container creation. (dlehman) +- Reflect previous custom/autopart selection in the storage spoke. (dlehman) +- Clear out custom storage ksdata after first attempt to apply it. (dlehman) +- Pass size as Size when adjusting container after device removal. (#1141707) + (dlehman) +- Set flags.rescue_mode not anaconda.rescue (#1143056) (amulhern) +- Split localed's converted layouts and variants (#1073825) (vpodzime) +- Rename variable to not with a built-in (mkolman) +- Create free space snapshot before doing custom->autopart (vpodzime) +- Deprecate RUNKS cmdline option. (sbueno+anaconda) - Show help also when alt+F1 is pressed (mkolman) - Support display of the custom mnemonics on the help button (mkolman) - Activate the built-in help when F1 is pressed (mkolman) - Specify help file names for hubs and spokes (mkolman) - Add a help button to every Anaconda screen (mkolman) -- Don't attempt terminal size detection on the s390 (#1145065) (mkolman) -- Fix Welcome spoke not showing up during kickstart installation (#1147943) - (mkolman) -- Clear the list of watched PIDs before exiting. (#1146708) (dshea) -- Avoid the possibility of size variables being unset (#1146585) (dshea) -- Adapt to corrected interpetation of logvol --percent. (#1146156) (dlehman) -- Handle cancellation of new container creation. (dlehman) -- Reflect previous custom/autopart selection in the storage spoke. (#1144520) - (dlehman) -- Clear out custom storage ksdata after first attempt to apply it. (#1144560) - (dlehman) -- Pass size as Size when adjusting container after device removal. (#1141707) - (dlehman) -- Handle 0's returned by Gdk (dshea) -- When running on HiDPI monitors, scale anaconda by a factor of 2 (dshea) -- Highlight languages in langsupport that contain selected locales (dshea) -- Add a wrapper function for GtkTreeViewColumn.set_cell_data_func (dshea) -- Clear the kickstart password if cleared by the user (#1133185) (dshea) -- Remove inactive languages from LINGUAS. (dshea) -- Use suggested-action on more buttons (#1131254) (dshea) -- Filter empty comps groups from both specific and generic lists (dshea) -- Use one thread for payload setup. (dshea) - -* Wed Sep 17 2014 Samantha N. Bueno - 21.48.7-1 -- Set flags.rescue_mode not anaconda.rescue (#1101341) (amulhern) - -* Thu Sep 11 2014 Samantha N. Bueno - 21.48.6-1 -- Update tx config (sbueno+anaconda) +- Don't call BusyCursor before Gdk is setup (#1078868) (bcl) +- Fix SELINUX_DEFAULT import (#1137049) (bcl) +- Catch and rethrow BTRFSValueError as KickstartException (#1019685) (amulhern) +- Bump version so BTRFSValueError is found (#1019685) (amulhern) +- Don't change langpacks config of installer environment (#1066017) (rvykydal) +- network: fix typo 'Private ksy pasword' (#1120374) (rvykydal) +- Fix up a string style issue found in the last network commits. (clumens) +- network: WPA Enterprise: don't ask twice for password (#1120374) (rvykydal) +- network: add support for WPA Enterprise (#1120374) (rvykydal) +- network: add s390 network ifcfg options also for bond slaves (#1090558) + (rvykydal) +- network: copy resolv.conf to chroot before installing packages (#1048520) + (rvykydal) +- network: don't crash, just log for unrecognized bond options (#1039006) + (rvykydal) +- network: don't traceback on invalid team options (#1114282) (rvykydal) +- network: don't write HWADDR in ifcfgs generated by kickstart (#1130042) + (rvykydal) +- Re-order the tz's in text mode to mirror the graphical order. + (sbueno+anaconda) +- Apply a better check for whether to fail if authconfig is missing. (clumens) +- driver-updates: fix backspace/delete in dd menus (#1080380) (wwoods) +- Fix an issue with bad NFS info specified in source spoke. (sbueno+anaconda) +- Fix the SIGSEGV handler (dshea) +- Remove argument handling from methods without arguments (dshea) +- Warn if software selection size exceeds available space. (sbueno+anaconda) +- X doesn't start when making the livecd on the GUI test either. (clumens) +- Handle spaces in inst.repo, kickstart nfs, and url commands (#1109933) (bcl) +- Fix that urllib2 problem more thoroughly. (clumens) +- Fix a problem where urllib2 is not getting pulled into the initrd. (clumens) +- Specify thin pool metadata/chunk size only if given by user (#1140635) + (vpodzime) +- Fix q for quit issue in text mode (#997405) (sbueno+anaconda) +- Additional message if kickstart was used but did not finish (#1117908) + (amulhern) +- Move some statically detectable kickstart errors out of anaconda (#1117908) + (amulhern) - Use only the digits from productVersion (bcl) +- If a kickstart installation stops because it doesn't know something, log + that. (clumens) +- Don't care about crash args in bootloader (#1116323) (vpodzime) -* Tue Sep 09 2014 Samantha N. Bueno - 21.48.5-1 +* Wed Sep 10 2014 Brian C. Lane - 22.5-1 +- Fix noselinux cmdline default (#1137049) (bcl) - Revert productName repo name change (#1128474) (bcl) +- Remove the --disable-overwrite parameter for the Transifex client (mkolman) - Do not try to disable no firstboot services (#1139621) (vpodzime) +- Snapshot free space after clearpart for swap suggestion (#1132436) (vpodzime) +- Really fix an enlightbox call. (dshea) +- Correct issues merged from rhel-7 (dshea) +- A couple updates to installclasses. (clumens) +- Clear the kickstart password if cleared by the user (#1133185) (dshea) +- Change the accelerator key for Add DASD label. (sbueno+anaconda) +- Add dialog box for adding DASDs. (sbueno+anaconda) +- Add a button for adding an ECKD DASD. (sbueno+anaconda) - Let finding install classes be more flexible for Fedora (#1138820). (clumens) - -* Thu Sep 04 2014 Samantha N. Bueno - 21.48.4-1 +- fix inst.virtiolog (#1074499) (wwoods) +- Display container sizes to just two places, as well. (clumens) +- Fix two minor things on the source spoke. (clumens) +- border_width=5 -> border_width=6 in dasdfmt.glade. (clumens) - Use first part of Product for UEFI entry (#1128474) (bcl) +- We can't pass "text" in the ostree .ks file because lmc doesn't like that. + (clumens) +- Remove inactive languages from LINGUAS. (dshea) +- Do the ostree test in text mode for now. (clumens) +- Skip nvram update on ppc64 image/dir installations (#1136486) (bcl) - Use first part of Product as repo name (#1128474) (bcl) - makeupdates: Report git diff errors (bcl) +- For yum-based installs, move the progress bar while packages are installing. + (clumens) +- Remove the mnemonics from the custom part toolbar. (clumens) +- Remove references to ia64. (clumens) +- Change a confusing string in TUI NFS configuration screen. (#1057690) + (sbueno+anaconda) +- Fix two problems with the volume label and combo on custom partitioning. + (clumens) +- Disable the Modify SW link on livecd installs (#1133726). (clumens) +- Require dmidecode for ARM (#1134651, jdisnard). (clumens) +- Require a larger /boot (#1129629). (clumens) +- Use suggested-action on more buttons (#1131254) (dshea) +- CmdlineError should exit with a 1 (bcl) +- Let gtk determine the allocation for overlays. (dshea) -* Wed Aug 27 2014 Samantha N. Bueno - 21.48.3-1 -- Prevent crashes due to accessing X server from multiple threads (#1134507) - (vpodzime) +* Wed Aug 27 2014 Brian C. Lane - 22.4-1 +- jwb would like us to be clear that bugs could be the system firmware... + (pjones) +- Fix installing from a second iso (#1040722) (bcl) - Remove anaconda_make_pixbuf (dshea) +- Trick automake into taking our wildcards (dshea) +- Distribute the right docs files (vpodzime) - Require anaconda-widgets from anaconda-widgets-devel (dshea) - Run /sbin/ldconfig when installing or uninstalling anaconda-widgets (dshea) - Remove the shebang from anaconda.py (dshea) - Exclude the compiled text and rescue files from anaconda-core (dshea) - Update our copy of the GPL (dshea) +- Remove unused methods from packaging.Payload (dshea) - Rearrange the entry, example and tip on Advanced User dialog (vpodzime) +- Change our docs that are close to ReST to proper ReST (vpodzime) +- Remove old outdated docs nobody should read (vpodzime) +- Send run-hub and run-spoke into the great beyond (dshea) +- Use one thread for payload setup. (dshea) +- Remove logging to tty3 and tty5 (#1073336) (bcl) +- Make missing encryption key error message more helpful (#1074441) (amulhern) +- Fix problems with the hdiso method. (clumens) +- Update makebumpver to include flags on first request (bcl) + +* Fri Aug 15 2014 Brian C. Lane - 22.3-1 +- Add some tests for execReadlines (dshea) +- Remove iutil.fork_orphan (dshea) +- Move non-exec tests into a separate class. (dshea) - Write storage after liveimg install (#1080396) (bcl) +- Add an option to makebumpver to skip all checks. (clumens) +- Write sslverify=0 for url kickstart method (#1116858) (bcl) +- Add noverifyssl and proxy support to dracut ks handling (#1116858) (bcl) +- Log installation successes and failures via ipmitool (#782019). (clumens) +- Default the OK button on the iscsi dialog to insensitive. (clumens) +- Add repo --install support to DNF (#1119867) (bcl) +- Install selected ks repos to target (#1119867) (bcl) +- Add check for the format of grub2 encrypted password (#1070327) (bcl) - Add some sanity checking to live payload (vpodzime) - Use blivet's getFreeSpace for limitting automatic swap size (vpodzime) - Ask users for enough space right at the first time (#876916) (vpodzime) +- Use low level file i/o for rpm callback logging (#1035745) (bcl) +- In tui cmdline mode skip showError and log message (bcl) +- Modify nm to return defaults when no dbus is available (bcl) +- Skip networkInitialize for image and dir installations (bcl) +- Ignore safe_dbus errors in keyboard setup (bcl) +- Skip syslog for dirinstall (bcl) +- Clear out errors at the beginning of _save_right_side. (clumens) +- Filter empty comps groups from both specific and generic lists (dshea) +- Add a test for disadvised words. (dshea) +- Mountpoint encrypted checkbox reflects container state (#1000031) (bcl) +- Display a fatal error if unable to encrypt a password. (dshea) +- Change strings per stylistic advice from ECS (dshea) +- Untranslate the type column of the network device type combobox (dshea) +- Add more information to the custom part summary dialog (#975804). (clumens) - Don't require user creation when root is locked (#1030626) (bcl) +- Import LUKSDeviceWithoutKeyError from the right place (vpodzime) +- Move _verifyLUKSDevicesHaveKey to Anaconda's codebase (vpodzime) +- Fix issues reported by pyflakes (vpodzime) -* Fri Aug 01 2014 Samantha N. Bueno - 21.48.2-1 +* Thu Jul 31 2014 Brian C. Lane - 22.2-1 +- Return NULL on error in doSetSystemTime. (dshea) +- Remove the /usr/bin/liveinst symlink during uninstall (dshea) +- Highlight languages in langsupport that contain selected locales (dshea) +- Add a wrapper function for GtkTreeViewColumn.set_cell_data_func (dshea) +- Remove the STANDALONE #ifdef from auditd. (dshea) - Mark zRAM devices as protected and ignore them (vpodzime) - Make storage sanity check aware of base RAM requirements (#1123466) (vpodzime) - Move sanityCheck code to anaconda's codebase (vpodzime) +- Clean up stylesheet comments (dshea) +- Resurrect auditd (dshea) +- Fix the spacing on the non-verbose doc building messages (dshea) +- Switch to kinder, gentler autoconf errors (dshea) +- Clean up the handling of CFLAGS (dshea) +- Remove unused parts of the configure.ac files. (dshea) +- Add a couple of configure checks from autoscan (dshea) +- Include config.h in every C file. (dshea) +- Use the result from AC_FUNC_FORK at build time (dshea) +- Don't distribute the gnome desktop file with translations (dshea) +- Build documentation during build instead of dist (dshea) - Do not multiply/divide RAM sizes by 1024 back and forth (vpodzime) - Raise exception if reading lines from a killed process (vpodzime) - Use zRAM swap up to 2 GB of RAM (vpodzime) - RAM requirements depend on squashfs.img's origin (vpodzime) -* Wed Jul 23 2014 Samantha N. Bueno - 21.48.1-1 +* Fri Jul 25 2014 Brian C. Lane - 22.1-1 +- Add platform specific group selection (#884385) (bcl) +- Use parallel instead of xargs (vpodzime) +- Solidify the row separator in the welcome spoke. (dshea) +- Don't skip cpfmtxa formatted dasds if zerombr specified in ks. (#1073982) + (sbueno+anaconda) +- Fix TUI error message regarding username creation. (#1058637) + (sbueno+anaconda) +- Determine the lang selected arrow direction at render time (dshea) +- Lessen the visible resize when entering the welcome and lang spokes (dshea) +- Reset the want_x flag after the memory check (vpodzime) +- Fix crash caused by passing kwargs to log functions (vpodzime) +- Check graphical RAM requirements if running graphical installation (vpodzime) +- Document the inst.zram boot option (vpodzime) +- Adapt the memory requirements to zRAM swap usage (vpodzime) +- Remove an unused MEM-related constant and use the other one (vpodzime) +- Add a script for showing stats about zRAM (vpodzime) +- Set widgets to be focused when entering a spoke. (#1121285) (dshea) +- Allow a wider variety of mountpoints (#1109143) (dshea) +- Restrict the selected and insensitive style rules to anaconda widgets (dshea) +- Log more details about collect failure (bcl) +- Prevent crashes due to accessing X server from multiple threads (vpodzime) +- Add vnc to the arguments to qemu for the GUI testing. (clumens) +- Remove a commented out import (mkolman) * Wed Jul 16 2014 Brian C. Lane - 21.48-1 - Fix the custom accelerators in custom partitioning (#1118999) (dshea) @@ -1845,605 +2481,3 @@ update-desktop-database &> /dev/null || : - Move formating markup out of python where possible (dshea) - Use explicit children to set label attributes (dshea) - Turn on the image on the "Add a disk..." button. (dshea) - -* Wed Oct 16 2013 Brian C. Lane - 20.26-1 -- Install bootloader to loop device in disk image installations. (#1019502) - (dlehman) -- Don't try to configure a bootloader for s390 disk image installs. (#1019502) - (dlehman) -- Fix initramfs generation for disk image installations. (#1019502) (dlehman) -- Save mountpoints specified for existing btrfs volumes. (#892747) (dlehman) -- Add a command line option for disabling friendly multipath names. (#977815) - (dlehman) -- Remove en (dshea) -- "Fix" the zSeries device filter "label" (dshea) -- Replace placeholders with the strings from python (dshea) -- Add and fix keyboard accelerators (dshea) -- Check for labels with use_underline and no accelerator (dshea) -- Support checking the translation of plural strings (dshea) -- Specify a node id in check_accelerator exceptions (dshea) -- BTRFS cannot hold swap, no need to care about fstab swaps (vpodzime) -- Add ANACONDA_INSTALL_CLASSES to testenv.sh. (clumens) -- Put a version on the DNF requirement. (clumens) -- Revert "For now, ignore checking dnfpayload.py with pylint." (clumens) -- Fix the alignment of the Network Time switch (#1019301) (dshea) -- Tell blivet which swaps should appear in the fstab (#1011391) (vpodzime) -- Put only newly created or reformated swaps to the new root (vpodzime) -- Make code to get new devices reusable as property (vpodzime) -- Grab journal only from the last boot (vpodzime) -- DNFPayload: allow enable/disable calls for repos that do not exist. (ales) -- Add shell spoke to s390x installations (vpodzime) -- Put TUI spokes in common categories (vpodzime) -- MountpointSelector is a widget, set its property properly (#1013612) - (vpodzime) -- Include the journal log on installed system (bcl) -- DNFPayload: error handling and logging cleanups. (ales) -- DNFPayload: reset the transaction goal on new package selection check. (ales) -- DNFPayload: implement environmentGroups() (ales) -- Some partition scheme is always selected (#1017435) (vpodzime) - -* Fri Oct 11 2013 Brian C. Lane - 20.25-1 -- Don't use g_object_set on initialized objects. (dshea) -- Remove the "other" tab in the network spoke. (dshea) -- Fix duplicated id in custom.glade (dshea) -- Correctly generate rescue initrd (#1013087) (bcl) -- Refresh swap suggestion once we know which disks to use (vpodzime) -- Initialize the kickstart install method (#1017614) (dshea) -- Use correct format for raise in kickstart.py (bcl) -- Add install-requires target to the Anaconda makefile (mkolman) -- fix luksformat references (#1014493) (bcl) -- kickstart: check for correct format (#1014545) (bcl) -- Add checks for unexpanded macros. (dshea) -- UIScreen doesn't necessarily have the ready property (vpodzime) -- Print long widgets in a nice way (vpodzime) -- Consider errno 5 I/O errors hardware faults (vpodzime) -- Install kernel-lpae if supported (#1013015) (vpodzime) -- Bump firewalld version (mkolman) - -* Wed Oct 09 2013 Brian C. Lane - 20.24-1 -- Clear bootDisk and bootloader stage info on errors (#1013482) (bcl) -- Catch BootLoaderError when setting up bootloader (#1013474) (bcl) -- Fix an incorrect formatting string in makeupdates. (clumens) -- network: remove function we don't need anymore (rvykydal) -- Don't translate constant strings. (dshea) -- Take into account disk space when calculating swap suggestion (#1016673) - (vpodzime) -- DNFPayload: adapt to DNF change c3de85d6 of Base.install() error reporting. - (ales) -- DNFPayload: the new libcomps makes env.option_ids a list of GroupID objects. - (ales) -- Fix warning message when package version is not found in Koji (mkolman) - -* Tue Oct 08 2013 Brian C. Lane - 20.23-1 -- Use Unicode in the TUI buffer strings (#1015620) (dshea) -- DNFPayload: install DNF itself. (ales) -- DNFPayload: direct conf.persistdir to the sysimage. (ales) -- Add a tooltip to the container combobox (#975801) (bcl) -- Use different colors for different message types. (dshea) -- Exit on exception in the askVNC spoke (#962804) (dshea) -- Don't skip the strength check if overriding a kickstart password (dshea) -- Allow password spoke to be exited without password (#1004931) (dshea) -- Re-check the password strength when the username changes (dshea) -- Only call pwquality once per password. (dshea) -- Use GUICheck checks for the root password strength (dshea) -- Use constants for password check failure messages (dshea) -- Use a constant to indicate GUICheck success (dshea) -- Remove a redundant error property from UserSpoke (dshea) -- Fix the usages of PWQError. (#1014405) (dshea) -- Fix usage of GtkLevelBar in glade. (dshea) -- Clean up callbacks in the user spoke. (dshea) -- Removed an untrue portion of a doc comment (dshea) -- Support for removing services from firewall needs newer PyKickstart (mkolman) -- Add support for removing services from the firewall (#957809) (mkolman) - -* Fri Oct 04 2013 Brian C. Lane - 20.22-1 -- Only encrypt the TUI user password once (#1015220) (dshea) -- Don't try to collect removed modules (vpodzime) -- Moved the NFS nolock option into Payload._setupNFS (dshea) -- Grab journalctl logs if there is no /tmp/syslog (vpodzime) -- Pass layout and variant in specific format to Gkbd (#1011155) (vpodzime) -- Translate the "Quit" string at the end of liveinst. (dshea) - -* Fri Sep 27 2013 Brian C. Lane - 20.21-1 -- Remove another reference to log_picker. (clumens) -- Turn spinner back on for configuration (bcl) -- Use assertIsInstance in the kickstart version test. (clumens) -- If the full device path is given in repo=hd:, still select it in the UI - (#980479). (clumens) -- Display newly created partitions without a mountpoint, too (#886039). - (clumens) -- Don't require pressing escape twice to kill the media check window (#965625). - (clumens) -- Fix display of weak password warning (#1011850) (dshea) -- Fix the tui simpleline imports. (dshea) -- Don't confuse users by misleading tooltip (#1011112) (vpodzime) -- Assorted other pylint fixes for scripts and utils (dshea) -- Pass string format arguments as paramters to logging (dshea) -- Ignore the use of func_globals in a test case (dshea) -- Fix issues in the AnacondaWidgets python wrapper (dshea) -- Make exception handling more specific (dshea) -- Remove unused imports and variables (dshea) -- Remove unnecessary lambdas (dshea) -- Remove obsolete files. (dshea) -- Check whether the commit matches the tree (dshea) -- Run pylint on all python files (dshea) -- Don't use relative imports (dshea) -- Use g_signal_handler_disconnect instead of g_object_disconnect (#1010486) - (vpodzime) -- Fixup Eula class (bcl) -- Allow searching for keyboard layouts in English (#1009806) (vpodzime) -- network: don't create ksdata for devices enslaved in GUI (#1011826) - (rvykydal) -- Allow a proxy to be set before the method is saved (#1012096) (dshea) -- Export the pykickstart Eula command (vpodzime) - -* Wed Sep 25 2013 Brian C. Lane - 20.20-1 -- Encrypt normal user passwords when doing text install. (#977732) - (sbueno+anaconda) -- Escape the status before setting it as markup (vpodzime) -- network gui: do not crash on devices without settings (eg wireless) - (#1010519) (rvykydal) -- Make the keyboard layout preview dialog bigger (#1011140) (vpodzime) -- Return switching options with the same order as shown (#1011130) (vpodzime) -- Use a temporary directory for verifying ISO media (dshea) -- Skip devices not controllable by blivet (#1009809) (dshea) -- Add translation support to check_accelerators (dshea) -- Make sure autopart type is handled deterministicaly in text mode (#1010453) - (vpodzime) -- Don't rely on X server adding empty variant for its defaults (#1011658) - (vpodzime) -- Make Keyboard spoke's status consistent with other statuses (#1011166) - (vpodzime) -- LiveImageKSPayload skip the parent class setup method (#1010500) (bcl) -- Pass the actual format instead of Python built-in (#1009678) (vpodzime) -- Don't allow using updates with non-default network sources (#1008028) - (vpodzime) -- Use Sphinx documentation format in nm.py. (rvykydal) -- Changed the keyboard accelerator for iscsi "Retry Log In" (dshea) -- Only fail on a missing firewalld command if the firewall is enabled - (#1004976). (clumens) -- Cleanup some pylint failures in the network module (bcl) -- Add GtkNotebook support to the accelerators check. (dshea) - -* Fri Sep 20 2013 Brian C. Lane - 20.19-1 -- tui ErrorDialog needs to be modal (#983316) (bcl) -- Keyboard variant names may contain dashes (#1008730) (vpodzime) -- Forbid "root" as a user or group name. (#968451) (dshea) -- Set the password strength color based on strength (#965596) (dshea) -- Fix the password confirmation match check (#1009907) (dshea) -- Replace removed python modules with stubs in makeupdates (vpodzime) -- Unlock encrypted partitions before finding installations (#901917) (vpodzime) -- Network TUI: remove unused import, import nm. (rvykydal) -- Network TUI: show the same status as in gui. (rvykydal) -- Network TUI: don't traceback when applying config to device without link. - (rvykydal) -- Generate ifcfg VLAN_ID value for kickstart network --vlanid. (rvykydal) -- Network TUI: fix updating of ksdata in apply. (rvykydal) -- Network TUI: ignore slaves devices for configuration. (rvykydal) -- Clean up ifcfg file handling. (rvykydal) -- Check the validity of generated usernames in TUI (#965543) (dshea) -- Behave better when PYTHONPATH is already set (dshea) -- Decode keyboard layout descriptions as UTF-8 (#1009278) (dshea) -- Filter out devices with no media from custom (#960794) (bcl) - -* Wed Sep 18 2013 Brian C. Lane - 20.18-1 -- ProgressHub no longer exists in pyanaconda/ui/tui/hubs. (clumens) -- Search all disk types for install media (#1004726) (dshea) -- git commit check for ack flag on rhel branches (bcl) -- Fix Lightbox for compositing window managers (#1008446) (dshea) -- Add metalink support to yumpayload (bcl) -- Make progress screen in text mode standalone spoke instead of hub (vpodzime) -- Render the right arrow based on the widget direction (#1008397) (vpodzime) -- Mirror the GUI if an RTL language is chosen (#1008397) (vpodzime) -- Removed unused GUI elements (dshea) -- Clean up what is and isn't translatable and how. (dshea) -- Removed the exceptionsText constant (dshea) -- Add comments for translators to TUI input strings (#854226) (dshea) -- Use python-format on all intltool-extract strings (dshea) - -* Mon Sep 16 2013 Brian C. Lane - 20.17-1 -- Fix handling of blank size specs in the custom spoke. (#1004903) (dlehman) -- Block resize slider value changed handler when setting range. (#1007387) - (dlehman) -- Remove an unused import. (clumens) -- Create the XklWrapper singleton in background (vpodzime) -- Translate layout and switching options descriptions on the fly (vpodzime) -- Improve XklWrapper's API (vpodzime) -- Move upcase_first_letter function to iutil (vpodzime) -- Remove the Layout class and things we don't need in XklWrapper (vpodzime) -- Ignore the whole m4 directory (vpodzime) -- Do not schedule hubs with no spokes available (#1006357) (vpodzime) -- Retranslate language filtering placeholder texts (#1007090) (vpodzime) -- Use pigz to create updates.img (vpodzime) -- The Desktop class doesn't need to inherit from SimpleConfigFile. (clumens) -- Fix yet another pylint error caught after the fact. (clumens) -- Move all languages found by geoip to the top in Welcome spoke (mkolman) -- Don't set ksdata.lang.seen to True if using default value (mkolman) -- DNFPayload: reset the sack and repos on updateBaseRepo() (ales) -- refactor: YumPayload: selectKernelPackage()->_select_kernel_package() (ales) -- DNFPayload: mirrorlist can not be an empty string. (ales) -- DNFPayload: display the download step in progressQ. (ales) -- DNFPayload: logging the missed packages/groups. (ales) -- DNFPayload: select kernel packages. (ales) -- DNFPayload: log when the transaction process unexpectedly terminates. (ales) -- DNFpayload: disable all NSS operations in RPM. (ales) -- DNFPayload: keyerror in isRepoEnabled() (ales) -- DNFPayload: implement selectEnvironment() (ales) - -* Fri Sep 13 2013 Brian C. Lane - 20.16-1 -- add pre-commit hook to run pylint (bcl) -- Allow runpylint.sh to be passed files (bcl) -- handle case of no ifcfg and no hostname (#1002737) (bcl) -- Allow make targets to be run outside of $srcdir (dshea) -- Fix the wildcard usage in automake files. (dshea) -- Move the intltool Makefile rules into configure.ac (dshea) -- Fix a format parameter mapping (#1007472) (dshea) -- Check whether keyboard translations are stale (#972236) (dshea) -- Fix the handling of xklavier strings. (dshea) -- Center the Langsupport spoke's description (vpodzime) -- Set minimal width request for the locales box (vpodzime) -- Use constant for default keyboard layout (vpodzime) -- Try to use VConsole keymap name as X layout (#1007359) (vpodzime) -- Retranslate also layout indicator when retranslating BaseWindow (#1007087) - (vpodzime) -- Check ready state before baseRepo (#1007448) (bcl) -- Fix po/Rules-extract so it doesn't remove itself (dshea) -- Include LayoutIndicator and TimezoneMap to the Micsellaneous Widgets - (vpodzime) - -* Wed Sep 11 2013 Brian C. Lane - 20.15-1 -- Don't set up the resize slider for non-resizable devices. (#997690) (dlehman) -- Remove 'completed' property from Autopart spoke in text UI. (sbueno+anaconda) -- Clean up code for input handling in TUI spokes. (sbueno+anaconda) -- set_hostname should proceed only on DVD and live installations (vpodzime) -- Don't use temporary file and move when writing out an ifcfg file (vpodzime) -- Set hostname when leaving network spokes (vpodzime) -- Keep file-naming convention with the Lightbox widget (vpodzime) -- Let users configure autopart options in interactive text ks. (#1001061) - (sbueno+anaconda) -- Add parameters to format strings (dshea) -- Fix pre-processing of files for xgettext (#1005644) (dshea) -- Added a test to check for xgettext warnings (dshea) -- Make sure XklWrapper isn't dumped to the anaconda-tb file (vpodzime) -- Catch race of network device state vs reading its config properties (#980576) - (rvykydal) - -* Tue Sep 10 2013 Brian C. Lane - 20.14-1 -- Convert the lightbox into a GObject (#1000927) (dshea) -- Remove some more unused imports. (clumens) -- Move the Anaconda class to a proper module (vpodzime) -- Firstboot should be disabled by default after automated installations - (vpodzime) -- Fix typo introduced in refactorization (#1005511) (vpodzime) -- Remove unused imports in the network spoke. (clumens) -- Get rid of the now-unused new_firmware variable. (clumens) -- Remove magic from the passphrase dialog (#921948) (vpodzime) -- Don't pass extra arguments to LangLocaleHandler.__init__() (vpodzime) -- Fix check for device state when reading its IPXConfig (#1001776, # 1005198) - (rvykydal) - -* Mon Sep 09 2013 Brian C. Lane - 20.13-1 -- Fix handling of flexible specs in onpart for member devices. (#1004885) - (dlehman) -- Always regenerate initramfs (#994180) (bcl) -- Avoid the use of NamedTuple._make (dshea) -- Add superclass __init__()s and fix an indent (dshea) -- Pass logging string format variables as parameters (dshea) -- Remove unnecessary variables, imports, semicolons (dshea) -- Fix the user/group name regex (dshea) -- Fix problems with the test scripts (dshea) -- Handle kickstarts that don't specify timezone (#1001598) (mkolman) -- Don't set "date of last password change" /etc/shadow field (#985572) - (hdegoede) - -* Fri Sep 06 2013 Brian C. Lane - 20.12-1 -- Cleanup arch tests (dshea) -- Rearranged the automake tests. (dshea) -- Update po/ build files to the current gettext (dshea) -- Use libtool with gtkdoc-scanobj (dshea) -- Use autoconf to set the spec file Version. (dshea) -- Use the ustar format with make dist (dshea) -- Fix widgets autotools generation. (dshea) -- Require gtk-doc and GObject. (dshea) -- dracut no longer auto assembles everything (#960496) (bcl) -- Only ignore missing packages entries (#983316) (bcl) -- Fix a string that was modified before translation (#1004960) (dshea) -- Let users configure keyboard via anaconda in live installations (#1002533) - (vpodzime) -- Use copy instead of move for NTP configuration (#985566) (hdegoede) -- Share code between the Welcome and Langsupport spokes (vpodzime) -- Do not try to set None as hostname (#1002737) (vpodzime) -- Fix crash on LiveCD if network is configured before installing (#1002373) - (rvykydal) - -* Thu Sep 05 2013 Brian C. Lane - 20.11-1 -- Add more details to iso device selector (#971290) (bcl) -- Warn user if they enter a weak password in TUI. (#1001039) (sbueno+anaconda) -- Don't mark spoke as completed if no repo is set. (#1001538) (sbueno+anaconda) -- Don't enable chronyd if disabled in kickstart (#1002583) (mkolman) -- Run firstboot-only spokes on first boot by default (vpodzime) -- Let hubs specify which environments they support (vpodzime) -- Don't mount cdroms that contain no mountable media. (#1000889) (dlehman) -- Don't try to parse langcode if none given (vpodzime) -- Get rid of the non-deterministic expand_langs and its usage (vpodzime) -- Rework the Langsupport spoke to work with all locales (vpodzime) -- Rework the Welcome spoke to allow users choose from all locales (vpodzime) -- Improve import in GUI utils a bit (vpodzime) -- Remove the cryptic "language-default keyboard" checkbutton (vpodzime) -- Allow seting up locale without modifying ksdata (vpodzime) -- Remove an unused argument of get_available_translations (vpodzime) -- Setup language early to a value we can figure out (vpodzime) - -* Tue Sep 03 2013 Brian C. Lane - 20.10-1 -- Optionally hide the GUI option to install updates (dshea) -- Move the really_hide and really_show functions to utils (vpodzime) -- Search for all translations, not only one per langauge (#1001446) (vpodzime) -- Use the DEFAULT_LANG if GeoIP suggestion cannot be used (#1000715) (vpodzime) -- Network spoke: fix showing of ipv6 addresses (rvykydal) -- Use the sensitive-info log for sensitive location info (#986844) (mkolman) -- Add new logger for sensitive information (mkolman) -- Handle %%define changes for autofetch (mkolman) -- Update dumping of network info for new nmcli interface. (rvykydal) -- Text network spoke: more strict ipv6 address input checking (#909299) - (rvykydal) -- Network spoke: show global ipv6 addresses (rvykydal) -- Text network spoke: add to translated files (po/POTFILES.in) (#902299) - (rvykydal) -- Text network spoke: require netmask and gateway for static ipv4 (#902299) - (rvykydal) -- Text network spoke: Condense device configuration information (#902299) - (rvykydal) -- Text network spoke: fix ipv4 regex (#909299) (rvykydal) -- Resolved accelerator conflicts and marked excpetions. (dshea) -- Added tests for duplicated keyboard accelerators (dshea) -- Implement group creation with GID in GUI (#968085) (dshea) -- Remove unused imports. (dshea) -- Move dynamic labels out of custom.glade (#1000703) (dshea) - -* Mon Aug 26 2013 Brian C. Lane - 20.9-1 -- Text network spoke: basic configuration support (#909299) (rvykydal) -- Add support for network configuration in TUI. (#909299) (sbueno+anaconda) -- Remove partial matches from Koji search results (mkolman) -- Handle >=,<=,= for package version, fix -a/--add (mkolman) -- Return only network devices supported in installer from nm_devices (#999514) - (rvykydal) -- Obtain network device type specific dbus interface dynamically (#999514) - (rvykydal) -- Catch no-hwaddr exception only for the respective call (#999514) (rvykydal) -- Don't catch hwaddr not found exception for ethernet devices (#999514) - (rvykydal) -- Added a validation test for the GUI group list (dshea) -- Validate input fields on the user spoke. (#967245) (dshea) -- Added an input validation framework. (dshea) -- Pre-fetch widgets in advanced user dialog (dshea) -- Change validatePassword to be more flexible. (dshea) -- Moved regexes into regexes.py. (dshea) - -* Fri Aug 23 2013 Brian C. Lane - 20.8-1 -- Fix a SIGSEGV when returning from storage spoke (#983319) (dshea) -- makebumpver: Fix problem with single line body (bcl) -- For now, ignore checking dnfpayload.py with pylint. (clumens) -- Don't do str() on an exception we're passing into a string substitution. - (clumens) -- Check for hwaddress exceptions. (dshea) -- If LANG isn't set, set it to default value. (#997397) (sbueno+anaconda) -- Remove yet another unused import. (clumens) -- swap devices should be under the System portion (#962668). (clumens) -- Populate the repo store before changed can ever be called (#994940). - (clumens) -- Make the ISO choosing widget wider (#973376). (clumens) -- Don't recommend /usr as a separate mount point anymore (#981465). (clumens) -- Do not run another instance of the TUI for errors (#997661) (vpodzime) -- Do not try to exit from the installation thread (vpodzime) -- Tell which thread failed to be added by the ThreadMgr (vpodzime) - -* Wed Aug 21 2013 Brian C. Lane - 20.7-1 -- Modify the gtk_warning function in anaconda to use gtk3. (clumens) -- Fix some pylint warnings in the new DNF code. (clumens) -- Fix a couple more pykickstart handler version mismatches. (clumens) -- anaconda requires a later version of partitioning syntax now. (clumens) -- packaging: add dnf to the Anaconda's requires. (ales) -- Enable DNFPayload on specific triggers. (ales) -- DNFPayload: initial version. (ales) -- refactor: tear down the install device in PackagePayload.reset(). (ales) -- refactor: extract the device handling in YumPayload._configureBaseRepo up to - PackagePayload. (ales) -- refactor: move YumPayload._setUpMedia() up to PackagePayload._setupMedia(). - (ales) -- Tweaks in the Payload interface. (ales) -- remove: configureAddOnRepo from the Payload interface. (ales) -- Payload: forgotten comment in spaceRequired() (ales) -- Payload: define txID to None. (ales) -- The NFS text dialog should never attempt to use method.url (#998446). - (clumens) -- Remove the unittest target, since "make check" will do this for us. (clumens) -- Use the latest version of the RAID kickstart handler. (clumens) -- Update both the method and repo info. (dshea) -- remove the UBOOT class arm systems are now using EXTLINUX (dennis) -- ARM: switch to using extlinux by default (dennis) -- Update our pylint arguments. (clumens) -- Don't implicitly unpack exceptions. That won't be supporetd in the future. - (clumens) -- Modify how we call logging functions to take a list of parameters. (clumens) -- Use "raise Exception()" instead of "raise Exception, ..." (clumens) -- Hook up pylint and our nosetests to be run via "make check". (clumens) -- Drop unneeded required_space_text variable. (#997690) (dlehman) -- Specify also query script when getting locale's native name (vpodzime) -- Update runpylint.sh for pylint 1.0.0 (bcl) -- Clean up translation placeholders (#890157) (bcl) -- Don't override multilib setting unless the option was passed. (#987557) - (dlehman) -- Set the encoding of custom.py to utf-8 (dshea) -- Report if a package was not found in Koji during autofetch (mkolman) -- Convert makeupdates from getopt to argparse (mkolman) -- Fixed the interpretation of RAID levels (dshea) -- Consolidate get_object() calls. (dshea) -- Add ASCII-only upper and lower string functions. (dshea) -- Fix the User/Group already exists log messages. (dshea) -- Normalize keyboard layout and variant strings from langtable (vpodzime) -- A few tests for the keyboard layout and variant strings processing (vpodzime) -- More robust parsing of the layout and variant string specification (vpodzime) -- Move DEFAULT_VC_FONT to constants (vpodzime) -- Match langs with stripped accents when filtering languages (vpodzime) -- Fix the User subclass using an old version of the pykickstart superclass. - (clumens) -- Bring the kickstart version test back to life. (clumens) -- Don't read proxy for methods that have no proxy (dshea) - -* Wed Aug 14 2013 Brian C. Lane - 20.6-1 -- Import DBusGMainLoop directly (bcl) -- Catch AttributeError when looking for InstallClass (bcl) -- dracut/parse-kickstart should use the updated method-related classes - (#994978). (clumens) -- Ignore warnings about the global keyword and the DefaultInstall class. - (clumens) -- Fix all the pylint warnings in the anaconda file. (clumens) -- Deal with the last of the catching Exception warnings. (clumens) -- Always define a continueButton and quitButton property. (clumens) -- Fix pylint warnings in the installclasses. (clumens) -- Remove a directory that does not exist from the PYTHONPATH for pylint. - (clumens) -- Fix up some warnings about calling the superclass's __init__ method. - (clumens) -- Remove the reference to "anaconda" in reIPL. (clumens) -- Fix up almost all of the redefining warning messages. (clumens) -- Add a bunch of gobject-introspection related ignore lines. (clumens) -- StorageChecker ought to have a self.storage attribute. (clumens) -- Ignore another error pylint can't quite figure out. (clumens) -- pylint doesn't understand what's in AnacondaKSHandler. (clumens) -- Remove the "Add custom add-on" button. (clumens) -- Fix up places where overridden methods don't take the same number of args. - (clumens) -- Fix up all unused variable warnings. (clumens) -- Added files to MAINTAINERCLEANFILES (dshea) -- Reenabled the pylint test target (dshea) -- Cleanup the autogen scripts. (dshea) -- Remove m4 files from the widgets project. (dshea) -- Install gettext files at build time. (dshea) -- Use the python checks provided by automake. (dshea) -- Added a missing type check found by autoscan (dshea) -- Cleanup the widgets autoconf file (dshea) -- Add detail to logs when creating users and groups (dshea) -- Fix miscellaneous errors in installclass.py. (clumens) -- Fix a variety of errors in the packaging module. (clumens) -- Do not run pylint against executable files in pyanaconda/. (clumens) -- Clean up deprecated uses of the string module. (clumens) -- Call the right superclass's method. (clumens) -- Straighten out text UI methods so they have the same method signature. - (clumens) -- Remove the lines to ignore E0611. (clumens) -- Bootloader.read is completely unused; cut it. (clumens) -- Fix all the "X is defined outside of __init__" warnings. (clumens) -- Display the correct string for the space required by packages. (clumens) -- Remove lines that clearly just don't do anything. (clumens) -- If we're not going to use the return value, don't grab it. (clumens) -- kernelVersionList in tarpayload.py should act the same as all other versions. - (clumens) -- If we're not going to use the exception object, don't grab it. (clumens) -- Define stage2_device in the BootLoader class. (clumens) -- Don't call getPassAlgo before running createGroup. (clumens) -- Remove some unused values out of constants.py. (clumens) -- Finish taking care of pylint warnings in image.py. (clumens) -- Remove the unused network and write methods from the Anaconda object. - (clumens) -- Remove the disable-msg lines for a couple C messages. (clumens) -- Remove some easy unused argument warnings. (clumens) -- Remove everything from InstallInterfaceBase except what Rescue needs. - (clumens) -- Remove the duplicated imports. (clumens) -- Do not use a list or a dict as a default argument to a method. (clumens) -- Remove unneeded lambdas. (clumens) -- Fix up all the warnings where we used a reserved function name or keyword. - (clumens) -- Fix up about half of the "except:" and "except Exception:" lines. (clumens) -- Fix a couple undefined variable errors that were real bugs. (clumens) -- Ignore statements that have no effect. (clumens) -- Fix up all the wildcard imports except two in packaging. (clumens) -- Remove all unused import lines. (clumens) -- Remove unnecessary pass statements. (clumens) -- Fix bad indentation and tab-instead-of-space warnings from pylint. (clumens) -- Put the pylint test back into service. (clumens) -- Run make with multiple jobs in makeupdates (vpodzime) -- Use gtk_image_new_from_icon_name (bcl) -- Remove caching of unused device list. (dlehman) -- Check MBR gap size even when /boot is on a plain partition. (#986431) - (dlehman) - -* Thu Aug 08 2013 Brian C. Lane - 20.5-1 -- Don't wait for systemctl shutdown command to exit (#974383) (bcl) -- Fix the logging of the spice-vdagent status (dshea) -- Update PYTHONPATH so unit tests work right out of the source tree. (clumens) -- Don't check for a firstboot service file before processing the command. - (clumens) -- Strengthen the services command processing a bit. (clumens) -- Start spice-vdagent (#969405) (dshea) -- Skip password strength check for kickstart passwords (#986490) (dshea) -- Network spoke: fix refresh of device IP configuration (rvykydal) -- Add unsupported hardware dialog (#872728) (bcl) -- storage.py -> system.py in POTFILES.in (clumens) -- border_width=5 -> border_width=6 (clumens) -- No need to call threads_init anymore (bcl) -- Consolidate storage and networking under one category (#973013). (clumens) -- When doing a live install, set the ks method appropriately (#986069). - (clumens) -- Check that we're doing an HD install before examining the attr (#989428). - (clumens) - -* Thu Aug 01 2013 Brian C. Lane - 20.4-1 -- POTFILES.in: rename time.py to time_spoke.py (bcl) -- Only move INSTALL_TREE when it is mounted (#888196) (bcl) -- Use ksdata.method.seen (#986069) (bcl) -- Threaded Koji RPM lookups and downloads (mkolman) -- Fix the langcode parsing regexp (vpodzime) -- Move tests to old_tests and add some new, working tests (vpodzime) -- Replace hostname with hostnamectl (#989584) (rvykydal) -- Require fcoe-utils only on ix86 and x86_64 architectures (#989913) (vpodzime) -- Fix searching for local RPMs with no version required (vpodzime) -- Expand the '~' in the RPM_FOLDER_NAME (vpodzime) -- Set system date and time with our own function (vpodzime) -- Remove the useless, confusing and lying PoolsNote (vpodzime) -- Use tiny, fast and thread-safe ntplib module instead of ntpdate (vpodzime) -- For vnc require network in intramfs (#989156) (rvykydal) -- Fix makeupdates' package fetching when no version is specified (vpodzime) -- Make it clear on the summary dialog that changes take effect later. (clumens) -- Don't mark the summary dialog's tree view as insensitive. (clumens) -- Work with files in a more pythonic way in makeupdates (vpodzime) -- Honor hostname set in kickstart (#988483) (rvykydal) -- Do not automatically set UTC mode on kickstart installs. (clumens) -- Add automatic fetching of RPMs for new Defines & Requires (mkolman) -- Don't prompt for ssh on s390x if doing an image install. (#983056) (sbueno) -- Rename tz spoke to avoid potential conflict with std 'time' module. (sbueno) - -* Thu Jul 25 2013 Brian C. Lane - 20.3-1 -- Fix driver disk path for inst.dd= method (#987513) (bcl) -- Add support for NFS as install source in TUI. (#971298) (sbueno+anaconda) -- Allow logging into multiple iscsi nodes at once (#975831). (clumens) -- Fix crash while parsing ntp servers from DHCP6 (#969303) (dshea) -- Use ExceptionInfo namedtuple when dumping anaconda (#982299) (vpodzime) -- Wait for device connections for iface-bound iscsi in kickstart (#740105) - (rvykydal) -- Refer to blivet instead of storage in iscsi kickstart (#740105) (rvykydal) -- Mark disk 'selected' if only one present in TUI. (#975790) (sbueno+anaconda) -- Update devicetree only if we logged in to some target in add iscsi dialog. - (rvykydal) -- Don't show multipath members in specialized disks overview (#740105) - (rvykydal) -- Do not populate devicetree after each single login in iscsi dialog (#740105) - (rvykydal) -- Match also iface when logging into selected iface-bound iscsi target - (#740105) (rvykydal) -- Fix handling of non-ASCII names (#969309) (dshea) -- Use inline completion for the region/city selection (vpodzime) -- Fix copyright of the main anaconda script (vpodzime) - -* Mon Jul 15 2013 Brian C. Lane - 20.2-1 -- Use the new wait for connectivity function (mkolman) -- Improve waiting for network connectivity (mkolman) -- Use langtable to get default layout instead of our magic (#485137) (vpodzime) -- Adapt to the new localization module (vpodzime) -- Rewrite the localization module (vpodzime) -- Make the Welcome spoke wait for Geolocation lookup to finish (#975193) - (mkolman) - -* Tue Jul 09 2013 Brian C. Lane - 20.1-1 -- bump major version number diff --git a/anaconda/configure.ac b/anaconda/configure.ac index 860d7fa..41e1288 100644 --- a/anaconda/configure.ac +++ b/anaconda/configure.ac @@ -20,7 +20,7 @@ m4_define(python_required_version, 2.5) AC_PREREQ([2.63]) -AC_INIT([anaconda], [21.48.21], [anaconda-devel-list@redhat.com]) +AC_INIT([anaconda], [22.20.13], [anaconda-devel-list@redhat.com]) AM_INIT_AUTOMAKE([foreign no-dist-gzip dist-bzip2 tar-ustar]) AC_CONFIG_HEADERS([config.h]) @@ -29,13 +29,12 @@ AC_CONFIG_MACRO_DIR([m4]) AC_DEFINE_UNQUOTED([BUILD_DATE], ["`date +%m%d%Y`"], [Date of anaconda build]) AM_SILENT_RULES([yes]) # make --enable-silent-rules the default. +AC_USE_SYSTEM_EXTENSIONS +AC_SYS_LARGEFILE + # Checks for programs. -AC_PROG_AWK -AC_PROG_GREP AC_PROG_CC -AC_PROG_INSTALL AC_PROG_LN_S -AC_PROG_MAKE_SET AC_PROG_LIBTOOL AC_PROG_MKDIR_P @@ -45,7 +44,7 @@ AC_PROG_MKDIR_P AC_PATH_PROG([INTLTOOL_EXTRACT], [intltool-extract]) AC_PATH_PROG([INTLTOOL_MERGE], [intltool-merge]) AS_IF([test -z "$INTLTOOL_EXTRACT" -o -z "$INTLTOOL_MERGE"], - [AC_MSG_ERROR([*** intltool not found])]) + [ANACONDA_SOFT_FAILURE([intltool not found])]) # Add the bits for Makefile rules INTLTOOL_V_MERGE='$(INTLTOOL__v_MERGE_$(V))' @@ -69,10 +68,9 @@ AM_GNU_GETTEXT([external]) AM_GNU_GETTEXT_VERSION([0.18.3]) # Checks for header files. -AC_PATH_X AC_CHECK_HEADERS([fcntl.h stdlib.h string.h sys/time.h unistd.h], [], - [AC_MSG_FAILURE([*** Header file $ac_header not found.])], + [ANACONDA_SOFT_FAILURE([Header file $ac_header not found.])], []) # Checks for typedefs, structures, and compiler characteristics. @@ -86,7 +84,10 @@ AC_TYPE_INT64_T AC_FUNC_FORK AC_CHECK_FUNCS([getcwd memset mkdir strchr strdup], [], - [AC_MSG_FAILURE([*** Required function $ac_func not found.])]) + [ANACONDA_SOFT_FAILURE([Function $ac_func not found.])]) + +AC_CHECK_LIB([audit], [audit_open], [:], + [ANACONDA_SOFT_FAILURE([libaudit not found])]) AM_PATH_PYTHON(python_required_version) @@ -98,69 +99,21 @@ PKG_CHECK_MODULES([PYTHON], [python], [ AC_TRY_LINK_FUNC([Py_Initialize], [AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no]) - AC_MSG_FAILURE([*** Unable to use python library])]) + ANACONDA_SOFT_FAILURE([Unable to use python library])]) LIBS="$LIBS_save" ], - [AC_MSG_FAILURE([*** Unable to find python library])]) + [ANACONDA_SOFT_FAILURE([Unable to find python library])]) # Check for libraries we need that provide pkg-config scripts -PKG_PROG_PKG_CONFIG([0.23]) -PKG_CHECK_MODULES([RPM], [rpm >= 4.10.0]) -PKG_CHECK_MODULES([LIBARCHIVE], [libarchive >= 3.0.4]) - -# Set $RPM_OPT_FLAGS if we don't have it -if test -z $RPM_OPT_FLAGS ; then - CFLAGS="$CFLAGS -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions" -else - CFLAGS="$CFLAGS $RPM_OPT_FLAGS" -fi - -# NFS support can, in theory, be enabled or disabled -AC_ARG_ENABLE(nfs, - AC_HELP_STRING([--enable-nfs], - [enable NFS support (default is yes)]), - [nfs=$enableval], - [nfs=yes]) - -# IPv6 support can be enabled or disabled -AC_ARG_ENABLE(ipv6, - AC_HELP_STRING([--enable-ipv6], - [enable IPv6 support (default is yes)]), - [ipv6=$enableval], - [ipv6=yes]) -if test x$ipv6 = xyes ; then - AC_SUBST(IPV6_CFLAGS, [-DENABLE_IPV6]) -fi +ANACONDA_PKG_CHECK_MODULES([RPM], [rpm >= 4.10.0]) +ANACONDA_PKG_CHECK_MODULES([LIBARCHIVE], [libarchive >= 3.0.4]) # GCC likes to bomb out on some ridiculous warnings. Add your favorites # here. SHUT_UP_GCC="-Wno-unused-result" # Add remaining compiler flags we want to use -CFLAGS="$CFLAGS -Wall -Werror $SHUT_UP_GCC -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE" - -# Filter CFLAGS (remove duplicate flags) -cflags_filter() { - have= - first=1 - for flag in $* ; do - if test -z "`echo $have | grep -- $flag`" ; then - if test x$first == x1 ; then - first=2 - else - echo -n " " - fi - echo -n $flag - have="$have $flag" - fi - done -} -CFLAGS="`cflags_filter $CFLAGS`" - -# Unset $(LIBS) because different programs and libraries will have different -# lists of libraries to link with, we don't want everything linking against -# all libraries we checked for. -LIBS= +CFLAGS="$CFLAGS -Wall -Werror $SHUT_UP_GCC" # Get the release number from the spec file rel="`awk '/Release:/ { split($2, r, "%"); print r[[1]] }' $srcdir/anaconda.spec`" @@ -186,8 +139,9 @@ AC_CONFIG_FILES([Makefile data/liveinst/gnome/Makefile data/liveinst/pam.d/Makefile data/systemd/Makefile - data/help/Makefile - data/help/en-US/Makefile + data/window-manager/Makefile + data/window-manager/config/Makefile + data/window-manager/theme/Makefile po/Makefile.in scripts/Makefile pyanaconda/Makefile @@ -201,15 +155,16 @@ AC_CONFIG_FILES([Makefile pyanaconda/ui/gui/spokes/Makefile pyanaconda/ui/gui/spokes/advstorage/Makefile pyanaconda/ui/gui/spokes/lib/Makefile - pyanaconda/ui/gui/tools/Makefile pyanaconda/ui/gui/Makefile pyanaconda/ui/tui/hubs/Makefile pyanaconda/ui/tui/simpleline/Makefile pyanaconda/ui/tui/spokes/Makefile - pyanaconda/ui/tui/tools/Makefile pyanaconda/ui/tui/Makefile data/post-scripts/Makefile tests/Makefile utils/Makefile utils/dd/Makefile]) AC_OUTPUT + +# Gently advise the user about the build failures they are about to encounter +ANACONDA_FAILURES diff --git a/anaconda/configure.ac.orig b/anaconda/configure.ac.orig deleted file mode 100644 index 237228e..0000000 --- a/anaconda/configure.ac.orig +++ /dev/null @@ -1,272 +0,0 @@ -# configure.ac for anaconda -# -# Copyright (C) 2009 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -# Author: David Cantrell - -m4_define(python_required_version, 2.5) - -AC_PREREQ([2.63]) -AC_INIT([anaconda], [20.25.16], [anaconda-devel-list@redhat.com]) -AM_INIT_AUTOMAKE([foreign no-dist-gzip dist-bzip2 tar-ustar]) - -AC_CONFIG_HEADERS([config.h]) -AC_CONFIG_MACRO_DIR([m4]) - -AC_DEFINE_UNQUOTED([BUILD_DATE], ["`date +%m%d%Y`"], [Date of anaconda build]) -AM_SILENT_RULES([yes]) # make --enable-silent-rules the default. - -# Checks for programs. -AC_PROG_AWK -AC_PROG_GREP -AC_PROG_CC -AC_PROG_INSTALL -AC_PROG_LN_S -AC_PROG_MAKE_SET -AC_PROG_LIBTOOL - -# Check for the intltool programs -# These checks and subsitutions are adapted IT_PROG_INTLTOOL provided in -# intltool.m4, but without the parts where it breaks gettext. -AC_PATH_PROG([INTLTOOL_EXTRACT], [intltool-extract]) -AC_PATH_PROG([INTLTOOL_MERGE], [intltool-merge]) -AS_IF([test -z "$INTLTOOL_EXTRACT" -o -z "$INTLTOOL_MERGE"], - [AC_MSG_ERROR([*** intltool not found])]) - -# Add the bits for Makefile rules -INTLTOOL_V_MERGE='$(INTLTOOL__v_MERGE_$(V))' -INTLTOOL__v_MERGE_='$(INTLTOOL__v_MERGE_$(AM_DEFAULT_VERBOSITY))' -INTLTOOL__v_MERGE_0='@echo " ITMRG " $@;' -INTLTOOL_V_MERGE_OPTIONS='$(intltool__v_merge_options_$(V))' -intltool__v_merge_options_='$(intltool__v_merge_options_$(AM_DEFAULT_VERBOSITY))' -intltool__v_merge_options_0='-q' -AC_SUBST(INTLTOOL_V_MERGE) -AC_SUBST(INTLTOOL__v_MERGE_) -AC_SUBST(INTLTOOL__v_MERGE_0) -AC_SUBST(INTLTOOL_V_MERGE_OPTIONS) -AC_SUBST(intltool__v_merge_options_) -AC_SUBST(intltool__v_merge_options_0) - -INTLTOOL_DESKTOP_RULE='%.desktop: %.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*.po) ; $(INTLTOOL_V_MERGE)LC_ALL=C $(INTLTOOL_MERGE) $(INTLTOOL_V_MERGE_OPTIONS) -d -u -c $(top_builddir)/po/.intltool-merge-cache $(top_srcdir)/po $< $@' -AC_SUBST(INTLTOOL_DESKTOP_RULE) - -AM_GNU_GETTEXT([external]) -AM_GNU_GETTEXT_VERSION([0.18.1]) - -# Checks for libraries. -AC_CHECK_LIB([X11], [XGetWindowAttributes], - [AC_SUBST(X11_LIBS, [-lX11])], - [AC_MSG_FAILURE([*** libX11 not usable.])]) - -AC_CHECK_LIB([audit], [audit_open], - [AC_SUBST(AUDIT_LIBS, [-laudit])], - [AC_MSG_FAILURE([*** libaudit not usable.])]) - -AC_CHECK_LIB([z], [zlibVersion], - [AC_SUBST(ZLIB_LIBS, [-lz])], - [AC_MSG_FAILURE([*** libz not usable.])]) - -# Checks for header files. -AC_PATH_X -AC_FUNC_ALLOCA -AC_HEADER_RESOLV -AC_HEADER_MAJOR -AC_CHECK_HEADERS([argz.h arpa/inet.h fcntl.h inttypes.h libintl.h limits.h \ - malloc.h netdb.h netinet/in.h stddef.h stdint.h stdlib.h \ - string.h strings.h sys/ioctl.h sys/mount.h sys/param.h \ - sys/socket.h sys/time.h sys/vfs.h syslog.h termios.h \ - unistd.h utime.h wchar.h], - [], - [AC_MSG_FAILURE([*** Header file $ac_header not found.])], - []) - -# Checks for typedefs, structures, and compiler characteristics. -AC_TYPE_UID_T -AC_C_INLINE -AC_TYPE_INT32_T -AC_TYPE_MODE_T -AC_TYPE_OFF_T -AC_TYPE_PID_T -AC_TYPE_SIZE_T -AC_TYPE_SSIZE_T -AC_CHECK_MEMBERS([struct stat.st_rdev]) -AC_TYPE_UINT16_T -AC_TYPE_UINT32_T -AC_TYPE_UINT64_T -AC_TYPE_UINT8_T -AC_TYPE_INT64_T - -# Checks for library functions. -AC_FUNC_CHOWN -AC_FUNC_ERROR_AT_LINE -AC_FUNC_FORK -AC_FUNC_LSTAT_FOLLOWS_SLASHED_SYMLINK -AC_FUNC_MALLOC -AC_FUNC_MMAP -AC_FUNC_REALLOC -AC_CHECK_FUNCS([dup2 fdatasync ftruncate getcwd gethostbyname gettimeofday \ - lchown memmove memset mkdir mkfifo munmap realpath select \ - setenv sethostname socket strcasecmp strchr strcspn strdup \ - strerror strncasecmp strndup strrchr strstr strtol strtoul \ - strverscmp uname utime wcwidth], - [], - [AC_MSG_FAILURE([*** Required function $ac_func not found.])]) - -AM_PATH_PYTHON(python_required_version) - -# Check for the python extension paths -PKG_CHECK_MODULES([PYTHON], [python], [ - LIBS_save="$LIBS" - LIBS="$LIBS $PYTHON_LIBS" - AC_MSG_CHECKING([Python libraries]) - AC_TRY_LINK_FUNC([Py_Initialize], - [AC_MSG_RESULT([yes])], - [AC_MSG_RESULT([no]) - AC_MSG_FAILURE([*** Unable to use python library])]) - LIBS="$LIBS_save" - ], - [AC_MSG_FAILURE([*** Unable to find python library])]) - -# Check for libraries we need that provide pkg-config scripts -PKG_PROG_PKG_CONFIG([0.23]) -PKG_CHECK_MODULES([X11], [x11 >= 1.3]) -PKG_CHECK_MODULES([XCOMPOSITE], [xcomposite >= 0.4.1]) -PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.16.1]) -PKG_CHECK_MODULES([GTK_X11], [gtk+-x11-3.0 >= 3.0]) -PKG_CHECK_MODULES([GDK], [gdk-3.0]) -PKG_CHECK_MODULES([NETWORKMANAGER], [NetworkManager >= 0.7.1]) -PKG_CHECK_MODULES([LIBNL], [libnl-1 >= 1.0]) -PKG_CHECK_MODULES([LIBNM_GLIB], [libnm-glib >= 0.7.1 libnm-util >= 0.7.1]) -PKG_CHECK_MODULES([RPM], [rpm >= 4.10.0]) -PKG_CHECK_MODULES([LIBARCHIVE], [libarchive >= 3.0.4]) - -# Set $RPM_OPT_FLAGS if we don't have it -if test -z $RPM_OPT_FLAGS ; then - CFLAGS="$CFLAGS -g -pipe -Wp,-D_FORTIFY_SOURCE=2 -fexceptions" -else - CFLAGS="$CFLAGS $RPM_OPT_FLAGS" -fi - -# NFS support can, in theory, be enabled or disabled -AC_ARG_ENABLE(nfs, - AC_HELP_STRING([--enable-nfs], - [enable NFS support (default is yes)]), - [nfs=$enableval], - [nfs=yes]) - -# IPv6 support can be enabled or disabled -AC_ARG_ENABLE(ipv6, - AC_HELP_STRING([--enable-ipv6], - [enable IPv6 support (default is yes)]), - [ipv6=$enableval], - [ipv6=yes]) -if test x$ipv6 = xyes ; then - AC_SUBST(IPV6_CFLAGS, [-DENABLE_IPV6]) -fi - -# GCC likes to bomb out on some ridiculous warnings. Add your favorites -# here. -SHUT_UP_GCC="-Wno-unused-result" - -# Add remaining compiler flags we want to use -CFLAGS="$CFLAGS -Wall -Werror $SHUT_UP_GCC -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE" - -# Filter CFLAGS (remove duplicate flags) -cflags_filter() { - have= - first=1 - for flag in $* ; do - if test -z "`echo $have | grep -- $flag`" ; then - if test x$first == x1 ; then - first=2 - else - echo -n " " - fi - echo -n $flag - have="$have $flag" - fi - done -} -CFLAGS="`cflags_filter $CFLAGS`" - -# Unset $(LIBS) because different programs and libraries will have different -# lists of libraries to link with, we don't want everything linking against -# all libraries we checked for. -LIBS= - -# Get the release number from the spec file -rel="`awk '/Release:/ { split($2, r, "%"); print r[[1]] }' $srcdir/anaconda.spec`" -AC_SUBST(PACKAGE_RELEASE, [$rel]) - -# Perform arch related tests -AC_CANONICAL_BUILD -s_arch="`echo $build_cpu | sed -e s/i.86/i386/ -e s/powerpc.*/ppc/`" - -AM_CONDITIONAL(IS_LIVEINST_ARCH, - [test x$s_arch = xppc || test x$s_arch = xi386 || test x$s_arch = xx86_64]) - -AC_CONFIG_SUBDIRS([widgets]) - -AC_CONFIG_FILES([Makefile - data/Makefile - data/command-stubs/Makefile - docs/Makefile - dracut/Makefile - pyanaconda/installclasses/Makefile - data/liveinst/Makefile - data/liveinst/console.apps/Makefile - data/liveinst/gnome/Makefile - data/liveinst/pam.d/Makefile - data/pixmaps/Makefile - data/icons/Makefile - data/icons/hicolor/Makefile - data/icons/hicolor/16x16/Makefile - data/icons/hicolor/16x16/apps/Makefile - data/icons/hicolor/22x22/Makefile - data/icons/hicolor/22x22/apps/Makefile - data/icons/hicolor/24x24/Makefile - data/icons/hicolor/24x24/apps/Makefile - data/icons/hicolor/32x32/Makefile - data/icons/hicolor/32x32/apps/Makefile - data/icons/hicolor/48x48/Makefile - data/icons/hicolor/48x48/apps/Makefile - data/icons/hicolor/256x256/Makefile - data/icons/hicolor/256x256/apps/Makefile - data/systemd/Makefile - po/Makefile.in - scripts/Makefile - pyanaconda/Makefile - pyanaconda/isys/Makefile - pyanaconda/packaging/Makefile - pyanaconda/ui/Makefile - pyanaconda/ui/lib/Makefile - pyanaconda/ui/gui/categories/Makefile - pyanaconda/ui/gui/hubs/Makefile - pyanaconda/ui/gui/spokes/Makefile - pyanaconda/ui/gui/spokes/advstorage/Makefile - pyanaconda/ui/gui/spokes/lib/Makefile - pyanaconda/ui/gui/tools/Makefile - pyanaconda/ui/gui/Makefile - pyanaconda/ui/tui/hubs/Makefile - pyanaconda/ui/tui/simpleline/Makefile - pyanaconda/ui/tui/spokes/Makefile - pyanaconda/ui/tui/tools/Makefile - pyanaconda/ui/tui/Makefile - data/post-scripts/Makefile - tests/Makefile - utils/Makefile - utils/dd/Makefile]) -AC_OUTPUT diff --git a/anaconda/data/Makefile.am b/anaconda/data/Makefile.am index 463ca85..557016d 100644 --- a/anaconda/data/Makefile.am +++ b/anaconda/data/Makefile.am @@ -17,7 +17,7 @@ # # Author: Martin Sivak -SUBDIRS = command-stubs liveinst systemd post-scripts help +SUBDIRS = command-stubs liveinst systemd post-scripts window-manager CLEANFILES = *~ diff --git a/anaconda/data/anaconda-gtk.css b/anaconda/data/anaconda-gtk.css index 16a6ce3..e96a0ba 100644 --- a/anaconda/data/anaconda-gtk.css +++ b/anaconda/data/anaconda-gtk.css @@ -160,11 +160,9 @@ AnacondaSpokeWindow #nav-box { /* These rules were removed when the Adwaita theme moved from * gnome-themes-standard to gtk. The selectors had been wildcards, but after - * the move they were replaced with more specific selectors, because gtk is - * maintained by garbage people who don't care about how anyone else's - * applications look. We need to apply the old style to anconda's custom - * widgets in order for the selection highlight and insensitive shading to - * appear. + * the move they were replaced with more specific selectors. We need to apply + * the old style to anconda's custom widgets in order for the selection + * highlight and insensitive shading to appear. */ @define-color anaconda_selected_bg_color #4a90d9; @define-color anaconda_selected_fg_color #ffffff; @@ -200,3 +198,9 @@ AnacondaSpokeSelector:insensitive { GtkTreeView.solid-separator { -GtkTreeView-horizontal-separator: 0; } + +/* Set the layout indicator colors */ +AnacondaLayoutIndicator { + background-color: #fdfdfd; + color: black; +} diff --git a/anaconda/data/anaconda_options.txt b/anaconda/data/anaconda_options.txt index 12a10d9..c05f983 100644 --- a/anaconda/data/anaconda_options.txt +++ b/anaconda/data/anaconda_options.txt @@ -223,11 +223,16 @@ installed OS will be booted. /etc/grub.d/40_custom can be used with manually created menuentrys which can use configfile to point to the grub.cfg on the newly installed OS. -dnf -Use the experimental DNF package management backend instead of the YUM backend -that is used by default. For more information about the DNF project see: +nodnf +Don't use the DNF package management backend (default since F22) and +use the legacy YUM backend instead. +For more information about the DNF project see: http://dnf.baseurl.org mpathfriendlynames Tell multipathd to use user friendly names when naming devices during the installation. See the multipathd documentation for more info. + +remotelog +Send all the logs to a remote host:port using a TCP connection. The connection will +be retried if there is no listener (ie. won't block the installation). diff --git a/anaconda/data/command-stubs/list-harddrives-stub b/anaconda/data/command-stubs/list-harddrives-stub index ae5e6b9..28274e4 100755 --- a/anaconda/data/command-stubs/list-harddrives-stub +++ b/anaconda/data/command-stubs/list-harddrives-stub @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # scan system for harddrives and output device name/size # @@ -35,7 +35,7 @@ def main(argv): lst = list(lst) lst.sort() for dev, size in lst: - print(dev, size) + print("%s %s" % (dev, size)) if __name__ == "__main__": main(sys.argv) diff --git a/anaconda/data/help/Makefile.am b/anaconda/data/help/Makefile.am deleted file mode 100644 index ed2eda3..0000000 --- a/anaconda/data/help/Makefile.am +++ /dev/null @@ -1,21 +0,0 @@ -# data/Makefile.am for anaconda -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -# Author: Martin Kolman -SUBDIRS = en-US - -MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/data/help/en-US/Makefile.am b/anaconda/data/help/en-US/Makefile.am deleted file mode 100644 index 46590cb..0000000 --- a/anaconda/data/help/en-US/Makefile.am +++ /dev/null @@ -1,26 +0,0 @@ -# data/Makefile.am for anaconda -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# -# Author: Martin Kolman - -enhelpcontentdir = $(datadir)/anaconda/help/en-US -# The $(srcdir)/*.*ml expression matches both the *.html placeholders and *.xml help -# content files. It also works around the fact that autotools complain if the xml files -# are not present but $(srcdir)/*.xml is used. -dist_enhelpcontent_DATA = $(srcdir)/*.*ml - -MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/data/interactive-defaults.ks b/anaconda/data/interactive-defaults.ks index f692ccd..277a331 100644 --- a/anaconda/data/interactive-defaults.ks +++ b/anaconda/data/interactive-defaults.ks @@ -2,3 +2,10 @@ # This is not loaded if a kickstart file is provided on the command line. auth --enableshadow --passalgo=sha512 firstboot --enable + +%anaconda +# Default password policies +pwpolicy root --notstrict --minlen=6 --minquality=50 --nochanges --emptyok +pwpolicy user --notstrict --minlen=6 --minquality=50 --nochanges --emptyok +pwpolicy luks --notstrict --minlen=6 --minquality=50 --nochanges --emptyok +%end diff --git a/anaconda/data/liveinst/Makefile.am b/anaconda/data/liveinst/Makefile.am index 7a3562f..c4becd0 100644 --- a/anaconda/data/liveinst/Makefile.am +++ b/anaconda/data/liveinst/Makefile.am @@ -31,6 +31,9 @@ dist_xinit_SCRIPTS = zz-liveinst.sh install-exec-local: $(MKDIR_P) $(DESTDIR)$(bindir) $(LN_S) consolehelper $(DESTDIR)$(bindir)/liveinst + +uninstall-local: + rm -f $(DESTDIR)$(bindir)/liveinst endif EXTRA_DIST = README liveinst.desktop.in diff --git a/anaconda/data/liveinst/gnome/Makefile.am b/anaconda/data/liveinst/gnome/Makefile.am index ae05a5c..861f5b1 100644 --- a/anaconda/data/liveinst/gnome/Makefile.am +++ b/anaconda/data/liveinst/gnome/Makefile.am @@ -18,8 +18,9 @@ # Author: Kalev Lember welcomedir = $(datadir)/$(PACKAGE_NAME)/gnome -dist_welcome_DATA = fedora-welcome.desktop install-button.png +dist_welcome_DATA = install-button.png dist_welcome_SCRIPTS = fedora-welcome +welcome_DATA = fedora-welcome.desktop EXTRA_DIST = fedora-welcome.desktop.in diff --git a/anaconda/data/liveinst/liveinst b/anaconda/data/liveinst/liveinst index 9d47a19..e26619d 100755 --- a/anaconda/data/liveinst/liveinst +++ b/anaconda/data/liveinst/liveinst @@ -77,7 +77,7 @@ fi export ANACONDA_BUGURL=${ANACONDA_BUGURL:="https://bugzilla.redhat.com/bugzilla/"} -RELEASE=$(rpm -q --qf '%{Release}' fedora-release) +RELEASE=$(rpm -q --qf '%{Release}' --whatprovides system-release) if [ "${RELEASE:0:2}" = "0." ]; then export ANACONDA_ISFINAL="false" else @@ -85,6 +85,7 @@ else fi export PATH=/sbin:/usr/sbin:$PATH +export PYTHONPATH=/usr/share/anaconda/site-python if [ -x /usr/sbin/getenforce ]; then current=$(/usr/sbin/getenforce) @@ -154,7 +155,7 @@ if [ ! -z "$UPDATES" ]; then exit 1 fi - curl -L -o /tmp/updates.img $UPDATES + curl -o /tmp/updates.img $UPDATES # We officially support two updates.img formats: a filesystem image, and # a compressed cpio blob. diff --git a/anaconda/data/liveinst/liveinst.desktop.in b/anaconda/data/liveinst/liveinst.desktop.in index b0ef188..0a30444 100644 --- a/anaconda/data/liveinst/liveinst.desktop.in +++ b/anaconda/data/liveinst/liveinst.desktop.in @@ -7,6 +7,5 @@ Exec=/usr/bin/liveinst Terminal=false Type=Application Icon=anaconda -Encoding=UTF-8 StartupNotify=true NoDisplay=true diff --git a/anaconda/data/systemd/Makefile.am b/anaconda/data/systemd/Makefile.am index ababe99..2607986 100644 --- a/anaconda/data/systemd/Makefile.am +++ b/anaconda/data/systemd/Makefile.am @@ -28,6 +28,7 @@ dist_systemd_DATA = anaconda.service \ anaconda-shell@.service \ instperf.service \ anaconda-sshd.service \ + anaconda-nm-config.service \ zram.service dist_generator_SCRIPTS = anaconda-generator diff --git a/anaconda/data/systemd/anaconda-direct.service b/anaconda/data/systemd/anaconda-direct.service index 64deda6..4f215fa 100644 --- a/anaconda/data/systemd/anaconda-direct.service +++ b/anaconda/data/systemd/anaconda-direct.service @@ -11,7 +11,7 @@ Environment=HOME=/root MALLOC_CHECK_=2 MALLOC_PERTURB_=204 PATH=/usr/bin:/bin:/s Type=oneshot WorkingDirectory=/root ExecStart=/usr/sbin/anaconda -StandardInput=null +StandardInput=tty StandardOutput=journal+console StandardError=journal+console TimeoutSec=0 diff --git a/anaconda/data/systemd/anaconda-generator b/anaconda/data/systemd/anaconda-generator index f09cb3b..fc4c47c 100755 --- a/anaconda/data/systemd/anaconda-generator +++ b/anaconda/data/systemd/anaconda-generator @@ -31,3 +31,5 @@ for tty in hvc0 hvc1 xvc0 hvsi0 hvsi1 hvsi2; do service_on_tty anaconda-shell@.service $tty fi done + +ln -sf $systemd_dir/anaconda-nm-config.service $target_dir/anaconda-nm-config.service diff --git a/anaconda/data/systemd/anaconda-nm-config.service b/anaconda/data/systemd/anaconda-nm-config.service new file mode 100644 index 0000000..e7442cc --- /dev/null +++ b/anaconda/data/systemd/anaconda-nm-config.service @@ -0,0 +1,7 @@ +[Unit] +ConditionKernelCommandLine=!ip=ibft +Description=Anaconda NetworkManager configuration +Before=NetworkManager.service + +[Service] +ExecStart=/usr/bin/anaconda-disable-nm-ibft-plugin diff --git a/anaconda/data/tmux.conf b/anaconda/data/tmux.conf index 80620ba..89f788b 100644 --- a/anaconda/data/tmux.conf +++ b/anaconda/data/tmux.conf @@ -1,12 +1,16 @@ # tmux.conf for the anaconda environment + +bind -n M-tab next +bind -n F1 list-keys + set-option -s exit-unattached off set-option -g base-index 1 set-option -g set-remain-on-exit on +set-option -g history-limit 10000 new-session -s anaconda -n main "anaconda" -set-option status-right "" -set-option status-right-length 0 +set-option status-right '#[fg=blue]#(echo -n "Switch tab: Alt+Tab | Help: F1 ")' new-window -d -n shell "bash --login" new-window -d -n log "tail -F /tmp/anaconda.log" diff --git a/anaconda/pyanaconda/ui/gui/tools/Makefile.am b/anaconda/data/window-manager/Makefile.am similarity index 80% rename from anaconda/pyanaconda/ui/gui/tools/Makefile.am rename to anaconda/data/window-manager/Makefile.am index 09e5924..7b6d3b8 100644 --- a/anaconda/pyanaconda/ui/gui/tools/Makefile.am +++ b/anaconda/data/window-manager/Makefile.am @@ -1,4 +1,6 @@ -# Copyright (C) 2011 Red Hat, Inc. +# data/window-manager/Makefile.am for anaconda +# +# Copyright (C) 2015 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published @@ -13,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # -# Author: Chris Lumens +# Author: David Shea -EXTRA_DIST = README run-hub.py run-spoke.py - -MAINTAINERCLEANFILES = Makefile.in +SUBDIRS = config theme diff --git a/anaconda/data/window-manager/config/Makefile.am b/anaconda/data/window-manager/config/Makefile.am new file mode 100644 index 0000000..6602e4b --- /dev/null +++ b/anaconda/data/window-manager/config/Makefile.am @@ -0,0 +1,39 @@ +# data/window-manager/config/Makefile.am for anaconda +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# +# Author: David Shea + +schemadir = $(pkgdatadir)/window-manager/glib-2.0/schemas + +# These files need to be compiled by glib-compile-schemas. This is handled +# in the spec file scriptlets. +dist_schema_DATA = org.gnome.desktop.wm.keybindings.gschema.override \ + org.gnome.desktop.wm.preferences.gschema.override + +# GSettings insists on the override files being in the same directory as the +# schemas they modify, so pretend that this is the case with symlinks and +# create the compiled schema. +install-data-hook: + $(MKDIR_P) $(DESTDIR)$(schemadir) + $(LN_S) -f /usr/share/glib-2.0/schemas/org.gnome.desktop.wm.keybindings.gschema.xml $(DESTDIR)$(schemadir) + $(LN_S) -f /usr/share/glib-2.0/schemas/org.gnome.desktop.wm.preferences.gschema.xml $(DESTDIR)$(schemadir) + $(LN_S) -f /usr/share/glib-2.0/schemas/org.gnome.desktop.enums.xml $(DESTDIR)$(schemadir) + glib-compile-schemas $(DESTDIR)$(schemadir) + +uninstall-local: + rm -f $(DESTDIR)$(schemadir)/*.xml + rm -f $(DESTDIR)$(schemadir)/gschemas.compiled diff --git a/anaconda/data/window-manager/config/org.gnome.desktop.wm.keybindings.gschema.override b/anaconda/data/window-manager/config/org.gnome.desktop.wm.keybindings.gschema.override new file mode 100644 index 0000000..e84aaba --- /dev/null +++ b/anaconda/data/window-manager/config/org.gnome.desktop.wm.keybindings.gschema.override @@ -0,0 +1,35 @@ +[org.gnome.desktop.wm.keybindings] + switch-to-workspace-left=[] + switch-to-workspace-right=[] + switch-to-workspace-up=[] + switch-to-workspace-down=[] + switch-to-workspace-1=[] + switch-to-workspace-last=[] + switch-group=[] + switch-windows=[] + switch-panels=[] + cycle-group=[] + cycle-windows=[] + cycle-panels=[] + activate-window-menu=[] + toggle-maximized=[] + minimize=[] + maximize=[] + unmaximize=[] + begin-move=[] + begin-resize=[] + move-to-workspace-1=[] + move-to-workspace-left=[] + move-to-workspace-right=[] + move-to-workspace-up=[] + move-to-workspace-down=[] + move-to-workspace-last=[] + move-to-monitor-left=[] + move-to-monitor-right=[] + move-to-monitor-up=[] + move-to-monitor-down=[] + close=[] + panel-main-menu=[] + panel-run-dialog=[] + switch-applications=[] + switch-input-source=[] diff --git a/anaconda/data/window-manager/config/org.gnome.desktop.wm.preferences.gschema.override b/anaconda/data/window-manager/config/org.gnome.desktop.wm.preferences.gschema.override new file mode 100644 index 0000000..9d8d9a7 --- /dev/null +++ b/anaconda/data/window-manager/config/org.gnome.desktop.wm.preferences.gschema.override @@ -0,0 +1,6 @@ +[org.gnome.desktop.wm.preferences] + button-layout=':' + action-right-click-titlebar='none' + num-workspaces=1 + theme='Anaconda' + mouse-button-modifier='' diff --git a/anaconda/old_tests/regex/Makefile.am b/anaconda/data/window-manager/theme/Makefile.am similarity index 75% rename from anaconda/old_tests/regex/Makefile.am rename to anaconda/data/window-manager/theme/Makefile.am index af1bd58..b8ce478 100644 --- a/anaconda/old_tests/regex/Makefile.am +++ b/anaconda/data/window-manager/theme/Makefile.am @@ -1,6 +1,6 @@ -# tests/regex/Makefile.am for anaconda +# data/window-manager/config/Makefile.am for anaconda # -# Copyright (C) 2010 Red Hat, Inc. +# Copyright (C) 2015 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published @@ -15,8 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # -# Author: Brian C. Lane +# Author: David Shea -EXTRA_DIST = *.py +themedir = $(datadir)/themes/Anaconda/metacity-1 -MAINTAINERCLEANFILES = Makefile.in +dist_theme_DATA = metacity-theme-1.xml diff --git a/anaconda/data/window-manager/theme/metacity-theme-1.xml b/anaconda/data/window-manager/theme/metacity-theme-1.xml new file mode 100644 index 0000000..9757b5d --- /dev/null +++ b/anaconda/data/window-manager/theme/metacity-theme-1.xml @@ -0,0 +1,958 @@ + + + + + Anaconda + Daniel Borgmann <daniel.borgmann@gmail.com>, Andrea Cimitan <andrea.cimitan@gmail.com> + Â 2005-2007 Daniel Borgmann, Andrea Cimitan + Apr, 2007 + The Clearlooks "Gummy" Metacity Theme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <title color="shade/gtk:bg[SELECTED]/0.7" x="((3 `max` (width-title_width)) / 2)+1" y="(((height - title_height) / 2) `max` 0)"/> + <title color="shade/gtk:bg[SELECTED]/0.7" x="((3 `max` (width-title_width)) / 2)-1" y="(((height - title_height) / 2) `max` 0)"/> + <title color="shade/gtk:bg[SELECTED]/0.7" x="((3 `max` (width-title_width)) / 2)" y="(((height - title_height) / 2) `max` 0)-1"/> + <title color="#FFFFFF" x="(3 `max` (width-title_width)) / 2" y="(((height - title_height) / 2) `max` 0)"/> +</draw_ops> + +<draw_ops name="title_text_unfocused"> + <!--<title color="shade/gtk:bg[NORMAL]/1.07" x="5 `max` (width-title_width)/2+1" y="1 `max` ((height-title_height)/2)+1"/>--> + <title color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" x="4 `max` (width-title_width)/2" y="0 `max` ((height-title_height)/2)"/> +</draw_ops> + +<draw_ops name="title"> + <include name="title_text"/> +</draw_ops> + +<draw_ops name="title_unfocused"> + <include name="title_text_unfocused"/> +</draw_ops> + +<!-- ::: BUTTONS ::: --> +<draw_ops name="button_bg"> + <!-- inset --> + <gradient type="vertical" x="0" y="3" width="width" height="height-6"> + <color value="shade/gtk:bg[SELECTED]/0.96"/> + <color value="shade/gtk:bg[SELECTED]/1.05"/> + </gradient> + + <line color="shade/gtk:bg[SELECTED]/1.00" x1="2" y1="0" x2="width-3" y2="0"/> + <line color="shade/gtk:bg[SELECTED]/0.99" x1="1" y1="1" x2="width-2" y2="1"/> + <line color="shade/gtk:bg[SELECTED]/0.99" x1="0" y1="2" x2="width-1" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/0.98" x1="3" y1="0" x2="width-4" y2="0"/> + + <line color="shade/gtk:bg[SELECTED]/0.91" x1="2" y1="1" x2="width-3" y2="1"/> + <line color="shade/gtk:bg[SELECTED]/0.90" x1="1" y1="2" x2="width-2" y2="2"/> + + <line color="shade/gtk:bg[SELECTED]/1.03" x1="2" y1="height-1" x2="width-3" y2="height-1"/> + <line color="shade/gtk:bg[SELECTED]/1.00" x1="1" y1="height-2" x2="width-2" y2="height-2"/> + <line color="shade/gtk:bg[SELECTED]/1.01" x1="0" y1="height-3" x2="width-1" y2="height-3"/> + <line color="shade/gtk:bg[SELECTED]/1.06" x1="3" y1="height-1" x2="width-4" y2="height-1"/> + + <line color="shade/gtk:bg[SELECTED]/1.02" x1="2" y1="height-2" x2="width-3" y2="height-2"/> + <line color="shade/gtk:bg[SELECTED]/1.03" x1="1" y1="height-3" x2="width-2" y2="height-3"/> + + <!-- border outline --> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="3" y1="1" x2="width-4" y2="1"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="3" y1="height-2" x2="width-4" y2="height-2"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="1" y1="3" x2="1" y2="height-4"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="width-2" y1="3" x2="width-2" y2="height-4"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="2" y1="2" x2="width-3" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="2" y1="height-3" x2="width-3" y2="height-3"/> + + <!-- border smooth effect --> + <line color="shade/gtk:bg[SELECTED]/1.02" x1="3" y1="2" x2="width-4" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/1.00" x1="2" y1="3" x2="2" y2="height-4"/> + <line color="shade/gtk:bg[SELECTED]/0.90" x1="width-3" y1="3" x2="width-3" y2="height-4"/> + + <!-- inside highlight --> + <line color="shade/gtk:bg[SELECTED]/1.18" x1="4" y1="2" x2="width-5" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/1.1" x1="2" y1="4" x2="2" y2="height-5"/> + <!-- inside shadow --> + <line color="shade/gtk:bg[SELECTED]/1.0" x1="width-3" y1="4" x2="width-3" y2="height-5"/> + + <!-- fill gradient --> + <gradient type="vertical" x="3" y="3" width="width-6" height="(height)/2-1"> + <color value="shade/gtk:bg[SELECTED]/1.1"/> + <color value="shade/gtk:bg[SELECTED]/1.02"/> + </gradient> + <gradient type="vertical" x="3" y="(height)/2" width="width-6" height="(height)/2-2"> + <color value="shade/gtk:bg[SELECTED]/1.0"/> + <color value="shade/gtk:bg[SELECTED]/0.92"/> + </gradient> + + <!-- bottom border smooth effect --> + <line color="shade/gtk:bg[SELECTED]/0.84" x1="3" y1="height-3" x2="width-4" y2="height-3"/> + <line color="shade/gtk:bg[SELECTED]/0.92" x1="4" y1="height-3" x2="width-5" y2="height-3"/> +</draw_ops> + +<draw_ops name="button_bg_unfocused"> + <!-- inset --> + <gradient type="vertical" x="0" y="3" width="width" height="height-6"> + <color value="shade/gtk:bg[NORMAL]/0.92"/> + <color value="shade/gtk:bg[NORMAL]/0.96"/> + </gradient> + + <line color="shade/gtk:bg[NORMAL]/0.93" x1="2" y1="0" x2="width-3" y2="0"/> + <line color="shade/gtk:bg[NORMAL]/0.92" x1="1" y1="1" x2="width-2" y2="1"/> + <line color="shade/gtk:bg[NORMAL]/0.92" x1="0" y1="2" x2="width-1" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/0.91" x1="3" y1="0" x2="width-4" y2="0"/> + + <line color="shade/gtk:bg[NORMAL]/0.87" x1="2" y1="1" x2="width-3" y2="1"/> + <line color="shade/gtk:bg[NORMAL]/0.86" x1="1" y1="2" x2="width-2" y2="2"/> + + <line color="shade/gtk:bg[NORMAL]/0.945" x1="2" y1="height-1" x2="width-3" y2="height-1"/> + <line color="shade/gtk:bg[NORMAL]/0.93" x1="1" y1="height-2" x2="width-2" y2="height-2"/> + <line color="shade/gtk:bg[NORMAL]/0.935" x1="0" y1="height-3" x2="width-1" y2="height-3"/> + <line color="shade/gtk:bg[NORMAL]/0.96" x1="3" y1="height-1" x2="width-4" y2="height-1"/> + + <line color="shade/gtk:bg[NORMAL]/0.94" x1="2" y1="height-2" x2="width-3" y2="height-2"/> + <line color="shade/gtk:bg[NORMAL]/0.95" x1="1" y1="height-3" x2="width-2" y2="height-3"/> + + <!-- border outline --> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="3" y1="1" x2="width-4" y2="1"/> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="3" y1="height-2" x2="width-4" y2="height-2"/> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="1" y1="3" x2="1" y2="height-4"/> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="width-2" y1="3" x2="width-2" y2="height-4"/> + + <line color="shade/gtk:bg[NORMAL]/0.6" x1="2" y1="2" x2="width-3" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="2" y1="height-3" x2="width-3" y2="height-3"/> + + <!-- border smooth effect --> + <line color="shade/gtk:bg[NORMAL]/1.02" x1="3" y1="2" x2="width-4" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/1.00" x1="2" y1="3" x2="2" y2="height-4"/> + <line color="shade/gtk:bg[NORMAL]/0.95" x1="width-3" y1="3" x2="width-3" y2="height-4"/> + + <!-- inside highlight --> + <line color="shade/gtk:bg[NORMAL]/1.2" x1="4" y1="2" x2="width-5" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/1.1" x1="2" y1="4" x2="2" y2="height-5"/> + <!-- inside shadow --> + <line color="shade/gtk:bg[NORMAL]/1.05" x1="width-3" y1="4" x2="width-3" y2="height-5"/> + + <!-- fill gradient --> + <gradient type="vertical" x="3" y="3" width="width-6" height="(height)/2-1"> + <color value="shade/gtk:bg[NORMAL]/1.15"/> + <color value="shade/gtk:bg[NORMAL]/1.07"/> + </gradient> + <gradient type="vertical" x="3" y="(height)/2" width="width-6" height="(height)/2-2"> + <color value="shade/gtk:bg[NORMAL]/1.05"/> + <color value="shade/gtk:bg[NORMAL]/0.97"/> + </gradient> + + <!-- bottom border smooth effect --> + <line color="shade/gtk:bg[NORMAL]/0.89" x1="3" y1="height-3" x2="width-4" y2="height-3"/> + <line color="shade/gtk:bg[NORMAL]/0.97" x1="4" y1="height-3" x2="width-5" y2="height-3"/> +</draw_ops> + +<draw_ops name="button_bg_prelight"> + <include name="button_bg"/> + <tint color="shade/gtk:bg[SELECTED]/1.5" alpha="0.2" x="3" y="3" width="width-5" height="height-5"/> + <line color="shade/gtk:bg[SELECTED]/0.6" x1="width-3" y1="height-3" x2="width-3" y2="height-3"/> +</draw_ops> + +<draw_ops name="button_bg_pressed"> + <!-- outside highlight --> + <gradient type="vertical" x="width-2" y="2" width="1" height="height-4"> + <color value="shade/gtk:bg[SELECTED]/1.2"/> + <color value="shade/gtk:bg[SELECTED]/1.0"/> + </gradient> + <gradient type="vertical" x="width-1" y="3" width="1" height="height-6"> + <color value="shade/gtk:bg[SELECTED]/1.2"/> + <color value="shade/gtk:bg[SELECTED]/1.0"/> + </gradient> + <line color="shade/gtk:bg[SELECTED]/1.0" x1="2" y1="height-2" x2="width-3" y2="height-2"/> + <line color="shade/gtk:bg[SELECTED]/1.0" x1="3" y1="height-1" x2="width-4" y2="height-1"/> + + <!-- border outline --> + <line color="shade/gtk:bg[SELECTED]/0.55" x1="3" y1="1" x2="width-4" y2="1"/> + <line color="shade/gtk:bg[SELECTED]/0.55" x1="3" y1="height-2" x2="width-4" y2="height-2"/> + <line color="shade/gtk:bg[SELECTED]/0.55" x1="1" y1="3" x2="1" y2="height-4"/> + <line color="shade/gtk:bg[SELECTED]/0.55" x1="width-2" y1="3" x2="width-2" y2="height-4"/> + + <line color="shade/gtk:bg[SELECTED]/0.55" x1="2" y1="2" x2="width-3" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/0.55" x1="2" y1="height-3" x2="width-3" y2="height-3"/> + + <!-- inside shadow --> + <line color="shade/gtk:bg[SELECTED]/0.9" x1="3" y1="2" x2="width-4" y2="2"/> + <line color="shade/gtk:bg[SELECTED]/0.85" x1="2" y1="3" x2="2" y2="height-4"/> + + <!-- fill gradient --> + <gradient type="vertical" x="3" y="3" width="width-5" height="height-6"> + <color value="shade/gtk:bg[SELECTED]/0.95"/> + <color value="shade/gtk:bg[SELECTED]/0.9"/> + </gradient> + + <line color="shade/gtk:bg[SELECTED]/0.9" x1="3" y1="height-3" x2="width-4" y2="height-3"/> +</draw_ops> + +<draw_ops name="button_bg_unfocused_prelight"> + <include name="button_bg_unfocused"/> + <tint color="shade/gtk:bg[NORMAL]/1.5" alpha="0.3" x="3" y="3" width="width-5" height="height-5"/> + <line color="shade/gtk:bg[NORMAL]/0.6" x1="width-3" y1="height-3" x2="width-3" y2="height-3"/> +</draw_ops> + +<draw_ops name="button_bg_unfocused_pressed"> + <!-- outside highlight --> + <gradient type="vertical" x="width-2" y="2" width="1" height="height-4"> + <color value="shade/gtk:bg[NORMAL]/1.25"/> + <color value="shade/gtk:bg[NORMAL]/1.05"/> + </gradient> + <gradient type="vertical" x="width-1" y="3" width="1" height="height-6"> + <color value="shade/gtk:bg[NORMAL]/1.25"/> + <color value="shade/gtk:bg[NORMAL]/1.05"/> + </gradient> + <line color="shade/gtk:bg[NORMAL]/1.05" x1="2" y1="height-2" x2="width-3" y2="height-2"/> + <line color="shade/gtk:bg[NORMAL]/1.05" x1="3" y1="height-1" x2="width-4" y2="height-1"/> + + <!-- border outline --> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="3" y1="1" x2="width-4" y2="1"/> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="3" y1="height-2" x2="width-4" y2="height-2"/> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="1" y1="3" x2="1" y2="height-4"/> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="width-2" y1="3" x2="width-2" y2="height-4"/> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="2" y1="2" x2="width-3" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/0.55" x1="2" y1="height-3" x2="width-3" y2="height-3"/> + + <!-- inside shadow --> + <line color="shade/gtk:bg[NORMAL]/0.8" x1="3" y1="2" x2="width-4" y2="2"/> + <line color="shade/gtk:bg[NORMAL]/0.75" x1="2" y1="3" x2="2" y2="height-4"/> + + <!-- fill gradient --> + <gradient type="vertical" x="3" y="3" width="width-5" height="height-6"> + <color value="shade/gtk:bg[NORMAL]/0.9"/> + <color value="shade/gtk:bg[NORMAL]/0.85"/> + </gradient> + + <line color="shade/gtk:bg[NORMAL]/0.85" x1="3" y1="height-3" x2="width-4" y2="height-3"/> +</draw_ops> + +<!-- ::: ICONS ::: --> +<!-- + using a minimum icon size until there is a proper way to specify relative sizes + unfortunately it's logically impossible to always center the icons on non-square + buttons (utility windows) without distortion. + + icon_size = (Bmin`max`height-Bpad*2) + hpadding = (width - icon_size) / 2 = ((width-(Bmin`max`height-Bpad*2))/2) + vpadding = (height - icon_size) / 2 = ((height-(Bmin`max`height-Bpad*2))/2) +--> + +<!-- menu icon --> +<draw_ops name="menu_button_icon"> + <!--<icon x="0" y="0" width="width" height="height"/>--> + <icon x="(width-mini_icon_width)/2" y="(height-mini_icon_height)/2" width="mini_icon_width" height="mini_icon_height"/> +</draw_ops> + +<draw_ops name="menu_button_icon_unfocused"> + <!--<icon x="0" y="0" width="width" height="height" alpha="0.5"/>--> + <icon x="(width-mini_icon_width)/2" y="(height-mini_icon_height)/2" width="mini_icon_width" height="mini_icon_height"/> +</draw_ops> + +<draw_ops name="menu_button_normal"> + <include name="menu_button_icon"/> +</draw_ops> +<draw_ops name="menu_button_pressed"> + <include name="menu_button_icon"/> +</draw_ops> +<draw_ops name="menu_button_unfocused"> + <include name="menu_button_icon_unfocused"/> +</draw_ops> + +<!-- close icon --> +<draw_ops name="close_button_icon"> + <!-- outside border --> + + <!-- main cross --> + <line color="shade/gtk:bg[SELECTED]/0.7" width="4" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="((height-(Bmin`max`height-Bpad*2))/2)" + x2="width - ((width-(Bmin`max`height-Bpad*2))/2) - 1" y2="height - ((height-(Bmin`max`height-Bpad*2))/2) - 1"/> + <line color="shade/gtk:bg[SELECTED]/0.7" width="4" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="height - ((height-(Bmin`max`height-Bpad*2))/2) - 1" + x2="width - ((width-(Bmin`max`height-Bpad*2))/2) - 1" y2="((height-(Bmin`max`height-Bpad*2))/2)"/> + <!-- top-left --> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="((height-(Bmin`max`height-Bpad*2))/2)-1" + width="2" height="1"/> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="((width-(Bmin`max`height-Bpad*2))/2)-1" y="((height-(Bmin`max`height-Bpad*2))/2)/1" + width="1" height="2"/> + <!-- top-right --> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="width - ((width-(Bmin`max`height-Bpad*2))/2) -2" y="((height-(Bmin`max`height-Bpad*2))/2)-1" + width="2" height="1"/> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="width - ((width-(Bmin`max`height-Bpad*2))/2)" y="((height-(Bmin`max`height-Bpad*2))/2)" + width="1" height="2"/> + <!-- bottom-left --> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="height - ((height-(Bmin`max`height-Bpad*2))/2)" + width="2" height="1"/> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="((width-(Bmin`max`height-Bpad*2))/2)-1" y="height - ((height-(Bmin`max`height-Bpad*2))/2)-2" + width="1" height="2"/> + <!-- bottom-right --> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="width - ((width-(Bmin`max`height-Bpad*2))/2) -2" y="height - ((height-(Bmin`max`height-Bpad*2))/2)" + width="2" height="1"/> + <tint color="shade/gtk:bg[SELECTED]/0.7" alpha="1.0" + x="width - ((width-(Bmin`max`height-Bpad*2))/2)" y="height - ((height-(Bmin`max`height-Bpad*2))/2)-2" + width="1" height="2"/> + + <!-- icon --> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="2" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="((height-(Bmin`max`height-Bpad*2))/2)" + x2="width - ((width-(Bmin`max`height-Bpad*2))/2) - 1" y2="height - ((height-(Bmin`max`height-Bpad*2))/2) - 1"/> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="((height-(Bmin`max`height-Bpad*2))/2)" + x2="width- ((width-(Bmin`max`height-Bpad*2))/2)" y2="height - ((height-(Bmin`max`height-Bpad*2))/2)"/> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="2" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="height - ((height-(Bmin`max`height-Bpad*2))/2) - 1" + x2="width - ((width-(Bmin`max`height-Bpad*2))/2) - 1" y2="((height-(Bmin`max`height-Bpad*2))/2)"/> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="height - ((height-(Bmin`max`height-Bpad*2))/2) - 1" + x2="width - ((width-(Bmin`max`height-Bpad*2))/2)" y2="((height-(Bmin`max`height-Bpad*2))/2) - 1"/> +</draw_ops> + +<draw_ops name="close_button_icon_unfocused"> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="2" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="((height-(Bmin`max`height-Bpad*2))/2)" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)-1" y2="height - ((height-(Bmin`max`height-Bpad*2))/2)-1"/> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="((height-(Bmin`max`height-Bpad*2))/2)" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)" y2="height - ((height-(Bmin`max`height-Bpad*2))/2)"/> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="2" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="height - ((height-(Bmin`max`height-Bpad*2))/2)-1" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)-1" y2="((height-(Bmin`max`height-Bpad*2))/2)"/> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2)" y1="height - ((height-(Bmin`max`height-Bpad*2))/2)-1" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)" y2="((height-(Bmin`max`height-Bpad*2))/2) - 1"/> +</draw_ops> + +<draw_ops name="close_button_normal"> + <include name="button_bg"/> + <include name="close_button_icon"/> +</draw_ops> +<draw_ops name="close_button_prelight"> + <include name="button_bg_prelight"/> + <include name="close_button_icon"/> +</draw_ops> +<draw_ops name="close_button_pressed"> + <include name="button_bg_pressed"/> + <include name="close_button_icon"/> +</draw_ops> +<draw_ops name="close_button_unfocused"> + <include name="button_bg_unfocused"/> + <include name="close_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="close_button_unfocused_prelight"> + <include name="button_bg_unfocused_prelight"/> + <include name="close_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="close_button_unfocused_pressed"> + <include name="button_bg_unfocused_pressed"/> + <include name="close_button_icon_unfocused"/> +</draw_ops> + +<!-- maximize icon --> +<draw_ops name="maximize_button_icon"> + <!-- outside border --> + <rectangle color="shade/gtk:bg[SELECTED]/0.7" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)-1" y="((height-(Bmin`max`height-Bpad*2))/2)-1" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2+1" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2+1"/> + <rectangle color="shade/gtk:bg[SELECTED]/0.7" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)+1" y="((height-(Bmin`max`height-Bpad*2))/2)+2" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-3" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-4"/> + + <!-- icon --> + <rectangle color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="((height-(Bmin`max`height-Bpad*2))/2)" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-1" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-1"/> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2) + 1" y1="((height-(Bmin`max`height-Bpad*2))/2) + 1" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)" y2="((height-(Bmin`max`height-Bpad*2))/2) + 1"/> +</draw_ops> + +<draw_ops name="maximize_button_icon_unfocused"> + <rectangle color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="((height-(Bmin`max`height-Bpad*2))/2)" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-1" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-1"/> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2) + 1" y1="((height-(Bmin`max`height-Bpad*2))/2) + 1" + x2="width-((width-(Bmin`max`height-Bpad*2))/2)" y2="((height-(Bmin`max`height-Bpad*2))/2) + 1"/> +</draw_ops> + +<draw_ops name="maximize_button_normal"> + <include name="button_bg"/> + <include name="maximize_button_icon"/> +</draw_ops> +<draw_ops name="maximize_button_prelight"> + <include name="button_bg_prelight"/> + <include name="maximize_button_icon"/> +</draw_ops> +<draw_ops name="maximize_button_pressed"> + <include name="button_bg_pressed"/> + <include name="maximize_button_icon"/> +</draw_ops> +<draw_ops name="maximize_button_unfocused"> + <include name="button_bg_unfocused"/> + <include name="maximize_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="maximize_button_unfocused_prelight"> + <include name="button_bg_unfocused_prelight"/> + <include name="maximize_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="maximize_button_unfocused_pressed"> + <include name="button_bg_unfocused_pressed"/> + <include name="maximize_button_icon_unfocused"/> +</draw_ops> + +<!-- restore icon --> +<draw_ops name="restore_button_icon"> + <!-- outside border --> + <rectangle color="shade/gtk:bg[SELECTED]/0.7" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="((height-(Bmin`max`height-Bpad*2))/2)" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-1" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-1"/> + <rectangle color="shade/gtk:bg[SELECTED]/0.7" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)+2" y="((height-(Bmin`max`height-Bpad*2))/2)+3" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-5" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-6"/> + + <!-- icon --> + <rectangle color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2) + 1" y="((height-(Bmin`max`height-Bpad*2))/2) + 1" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-3" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-3"/> + <line color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2) + 2" y1="((height-(Bmin`max`height-Bpad*2))/2) + 2" + x2="width-((width-(Bmin`max`height-Bpad*2))/2) - 2" y2="((height-(Bmin`max`height-Bpad*2))/2) + 2"/> +</draw_ops> + +<draw_ops name="restore_button_icon_unfocused"> + <rectangle color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2) + 1" y="((height-(Bmin`max`height-Bpad*2))/2) + 1" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2-3" height="height-((height-(Bmin`max`height-Bpad*2))/2)*2-3"/> + <line color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" width="1" + x1="((width-(Bmin`max`height-Bpad*2))/2) + 2" y1="((height-(Bmin`max`height-Bpad*2))/2) + 2" + x2="width-((width-(Bmin`max`height-Bpad*2))/2) - 2" y2="((height-(Bmin`max`height-Bpad*2))/2) + 2"/> +</draw_ops> + +<draw_ops name="restore_button_normal"> + <include name="button_bg"/> + <include name="restore_button_icon"/> +</draw_ops> +<draw_ops name="restore_button_prelight"> + <include name="button_bg_prelight"/> + <include name="restore_button_icon"/> +</draw_ops> +<draw_ops name="restore_button_pressed"> + <include name="button_bg_pressed"/> + <include name="restore_button_icon"/> +</draw_ops> +<draw_ops name="restore_button_unfocused"> + <include name="button_bg_unfocused"/> + <include name="restore_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="restore_button_unfocused_prelight"> + <include name="button_bg_unfocused_prelight"/> + <include name="restore_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="restore_button_unfocused_pressed"> + <include name="button_bg_unfocused_pressed"/> + <include name="restore_button_icon_unfocused"/> +</draw_ops> + +<!-- minimize icon --> +<draw_ops name="minimize_button_icon"> + <!-- outside border --> + <rectangle color="shade/gtk:bg[SELECTED]/0.7" filled="false" + x="((width-(Bmin`max`height-Bpad*2))/2)-1" y="height - ((height-(Bmin`max`height-Bpad*2))/2) - 3" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2+1" height="3"/> + <!-- icon --> + <rectangle color="blend/gtk:bg[SELECTED]/#FFFFFF/0.75" filled="true" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="height - ((height-(Bmin`max`height-Bpad*2))/2) - 2" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2" height="2"/> +</draw_ops> + +<draw_ops name="minimize_button_icon_unfocused"> + <rectangle color="blend/gtk:fg[NORMAL]/gtk:bg[NORMAL]/0.45" filled="true" + x="((width-(Bmin`max`height-Bpad*2))/2)" y="height - ((height-(Bmin`max`height-Bpad*2))/2) - 2" + width="width-((width-(Bmin`max`height-Bpad*2))/2)*2" height="2"/> +</draw_ops> + +<draw_ops name="minimize_button_normal"> + <include name="button_bg"/> + <include name="minimize_button_icon"/> +</draw_ops> +<draw_ops name="minimize_button_prelight"> + <include name="button_bg_prelight"/> + <include name="minimize_button_icon"/> +</draw_ops> +<draw_ops name="minimize_button_pressed"> + <include name="button_bg_pressed"/> + <include name="minimize_button_icon"/> +</draw_ops> +<draw_ops name="minimize_button_unfocused"> + <include name="button_bg_unfocused"/> + <include name="minimize_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="minimize_button_unfocused_prelight"> + <include name="button_bg_unfocused_prelight"/> + <include name="minimize_button_icon_unfocused"/> +</draw_ops> +<draw_ops name="minimize_button_unfocused_pressed"> + <include name="button_bg_unfocused_pressed"/> + <include name="minimize_button_icon_unfocused"/> +</draw_ops> + +<draw_ops name="blank"> +<!-- nothing --> +</draw_ops> + +<!-- ::: FRAME STYLES ::: --> +<frame_style name="normal" geometry="normal"> + <piece position="entire_background" draw_ops="round_bevel_unfocused"/> + <piece position="title" draw_ops="title_unfocused"/> + <button function="close" state="normal" draw_ops="close_button_unfocused"/> + <button function="close" state="pressed" draw_ops="close_button_unfocused_pressed"/> + <button function="close" state="prelight" draw_ops="close_button_unfocused_prelight"/> + <button function="maximize" state="normal" draw_ops="maximize_button_unfocused"/> + <button function="maximize" state="pressed" draw_ops="maximize_button_unfocused_pressed"/> + <button function="maximize" state="prelight" draw_ops="maximize_button_unfocused_prelight"/> + <button function="minimize" state="normal" draw_ops="minimize_button_unfocused"/> + <button function="minimize" state="pressed" draw_ops="minimize_button_unfocused_pressed"/> + <button function="minimize" state="prelight" draw_ops="minimize_button_unfocused_prelight"/> + <button function="menu" state="normal" draw_ops="menu_button_normal"/> + <button function="menu" state="pressed" draw_ops="menu_button_pressed"/> +</frame_style> + +<frame_style name="normal_shaded" geometry="shaded" parent="normal"> + <piece position="entire_background" draw_ops="round_bevel_unfocused_shaded"/> +</frame_style> + +<frame_style name="focused" geometry="normal" parent="normal"> + <piece position="entire_background" draw_ops="round_bevel"/> + <piece position="title" draw_ops="title"/> + <button function="close" state="normal" draw_ops="close_button_normal"/> + <button function="close" state="pressed" draw_ops="close_button_pressed"/> + <button function="close" state="prelight" draw_ops="close_button_prelight"/> + <button function="maximize" state="normal" draw_ops="maximize_button_normal"/> + <button function="maximize" state="pressed" draw_ops="maximize_button_pressed"/> + <button function="maximize" state="prelight" draw_ops="maximize_button_prelight"/> + <button function="minimize" state="normal" draw_ops="minimize_button_normal"/> + <button function="minimize" state="pressed" draw_ops="minimize_button_pressed"/> + <button function="minimize" state="prelight" draw_ops="minimize_button_prelight"/> +</frame_style> + +<frame_style name="focused_shaded" geometry="shaded" parent="focused"> + <piece position="entire_background" draw_ops="round_bevel_shaded"/> +</frame_style> + +<frame_style name="normal_maximized" geometry="normal_maximized" parent="normal"> + <piece position="entire_background" draw_ops="bevel_maximized_unfocused"/> + <button function="maximize" state="normal" draw_ops="restore_button_unfocused"/> + <button function="maximize" state="pressed" draw_ops="restore_button_unfocused_pressed"/> + <button function="maximize" state="prelight" draw_ops="restore_button_unfocused_prelight"/> +</frame_style> + +<frame_style name="focused_maximized" geometry="normal_maximized" parent="focused"> + <piece position="entire_background" draw_ops="bevel_maximized"/> + <button function="maximize" state="normal" draw_ops="restore_button_normal"/> + <button function="maximize" state="pressed" draw_ops="restore_button_pressed"/> + <button function="maximize" state="prelight" draw_ops="restore_button_prelight"/> +</frame_style> + +<frame_style name="border" geometry="border" parent="normal"> + <piece position="entire_background" draw_ops="border"/> + <piece position="title" draw_ops="blank"/> +</frame_style> + +<frame_style name="utility_normal" geometry="utility" parent="normal"> + <piece position="entire_background" draw_ops="bevel_unfocused"/> +</frame_style> +<frame_style name="utility_focused" geometry="utility" parent="focused"> + <piece position="entire_background" draw_ops="bevel"/> +</frame_style> +<frame_style name="utility_focused_shaded" geometry="utility" parent="focused_shaded"> + <piece position="entire_background" draw_ops="bevel_shaded"/> +</frame_style> + +<frame_style_set name="normal"> + <frame focus="yes" state="normal" resize="both" style="focused"/> + <frame focus="no" state="normal" resize="both" style="normal"/> + <frame focus="yes" state="maximized" style="focused_maximized"/> + <frame focus="no" state="maximized" style="normal_maximized"/> + <frame focus="yes" state="shaded" style="focused_shaded"/> + <frame focus="no" state="shaded" style="normal_shaded"/> + <frame focus="yes" state="maximized_and_shaded" style="focused_maximized"/> + <frame focus="no" state="maximized_and_shaded" style="normal_maximized"/> +</frame_style_set> + +<frame_style_set name="utility" parent="normal"> + <frame focus="yes" state="normal" resize="both" style="utility_focused"/> + <frame focus="no" state="normal" resize="both" style="utility_normal"/> + <!-- this is a bunch of crack since utility windows shouldn't be maximized --> + <frame focus="yes" state="maximized" style="focused"/> + <frame focus="no" state="maximized" style="normal"/> + <frame focus="yes" state="shaded" style="utility_focused_shaded"/> + <frame focus="no" state="shaded" style="utility_normal"/> + <frame focus="yes" state="maximized_and_shaded" style="focused_shaded"/> + <frame focus="no" state="maximized_and_shaded" style="normal"/> +</frame_style_set> + +<frame_style_set name="border"> + <frame focus="yes" state="normal" resize="both" style="border"/> + <frame focus="no" state="normal" resize="both" style="border"/> + <frame focus="yes" state="maximized" style="border"/> + <frame focus="no" state="maximized" style="border"/> + <frame focus="yes" state="shaded" style="border"/> + <frame focus="no" state="shaded" style="border"/> + <frame focus="yes" state="maximized_and_shaded" style="border"/> + <frame focus="no" state="maximized_and_shaded" style="border"/> +</frame_style_set> + +<window type="normal" style_set="normal"/> +<window type="dialog" style_set="normal"/> +<window type="modal_dialog" style_set="normal"/> +<window type="menu" style_set="normal"/> +<window type="utility" style_set="utility"/> +<window type="border" style_set="border"/> + +<menu_icon function="close" state="normal" draw_ops="close_button_icon_unfocused"/> +<menu_icon function="maximize" state="normal" draw_ops="maximize_button_icon_unfocused"/> +<menu_icon function="unmaximize" state="normal" draw_ops="restore_button_icon_unfocused"/> +<menu_icon function="minimize" state="normal" draw_ops="minimize_button_icon_unfocused"/> + +</metacity_theme> diff --git a/anaconda/docs/Makefile.am b/anaconda/docs/Makefile.am index f8106b5..259338d 100644 --- a/anaconda/docs/Makefile.am +++ b/anaconda/docs/Makefile.am @@ -17,8 +17,8 @@ # # Author: David Cantrell <dcantrell@redhat.com> -EXTRA_DIST = *.rst *.txt +EXTRA_DIST = $(srcdir)/*.rst $(srcdir)/*.txt -CLEANFILES = api *.xml +CLEANFILES = api $(builddir)/*.xml MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/docs/api.cfg b/anaconda/docs/api.cfg deleted file mode 100644 index 47fa060..0000000 --- a/anaconda/docs/api.cfg +++ /dev/null @@ -1,1237 +0,0 @@ -# Doxyfile 1.4.6 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project -# -# All text after a hash (#) is considered a comment and will be ignored -# The format is: -# TAG = value [value, ...] -# For lists items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (" ") - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded -# by quotes) that should identify the project. - -PROJECT_NAME = "Anaconda API Reference" - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. -# This could be handy for archiving the generated documentation or -# if some version control system is used. - -PROJECT_NUMBER = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) -# base path where the generated documentation will be put. -# If a relative path is entered, it will be relative to the location -# where doxygen was started. If left blank the current directory will be used. - -OUTPUT_DIRECTORY = docs/api - -# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create -# 4096 sub-directories (in 2 levels) under the output directory of each output -# format and will distribute the generated files over these directories. -# Enabling this option can be useful when feeding doxygen a huge amount of -# source files, where putting all generated files in the same directory would -# otherwise cause performance problems for the file system. - -CREATE_SUBDIRS = YES - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# The default language is English, other supported languages are: -# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, -# Dutch, Finnish, French, German, Greek, Hungarian, Italian, Japanese, -# Japanese-en (Japanese with English messages), Korean, Korean-en, Norwegian, -# Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovene, Spanish, -# Swedish, and Ukrainian. - -OUTPUT_LANGUAGE = English - -# This tag can be used to specify the encoding used in the generated output. -# The encoding is not always determined by the language that is chosen, -# but also whether or not the output is meant for Windows or non-Windows users. -# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES -# forces the Windows encoding (this is the default for the Windows binary), -# whereas setting the tag to NO uses a Unix-style encoding (the default for -# all platforms other than Windows). - -USE_WINDOWS_ENCODING = NO - -# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will -# include brief member descriptions after the members that are listed in -# the file and class documentation (similar to JavaDoc). -# Set to NO to disable this. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend -# the brief description of a member or function before the detailed description. -# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator -# that is used to form the text in various listings. Each string -# in this list, if found as the leading text of the brief description, will be -# stripped from the text and the result after processing the whole list, is -# used as the annotated text. Otherwise, the brief description is used as-is. -# If left blank, the following values are used ("$name" is automatically -# replaced with the name of the entity): "The $name class" "The $name widget" -# "The $name file" "is" "provides" "specifies" "contains" -# "represents" "a" "an" "the" - -ABBREVIATE_BRIEF = - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# Doxygen will generate a detailed section even if there is only a brief -# description. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full -# path before files name in the file list and in the header files. If set -# to NO the shortest path that makes the file name unique will be used. - -FULL_PATH_NAMES = NO - -# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag -# can be used to strip a user-defined part of the path. Stripping is -# only done if one of the specified strings matches the left-hand part of -# the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the -# path to strip. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of -# the path mentioned in the documentation of a class, which tells -# the reader which header file to include in order to use a class. -# If left blank only the name of the header file containing the class -# definition is used. Otherwise one should specify the include paths that -# are normally passed to the compiler using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter -# (but less readable) file names. This can be useful is your file systems -# doesn't support long names like on DOS, Mac, or CD-ROM. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen -# will interpret the first line (until the first dot) of a JavaDoc-style -# comment as the brief description. If set to NO, the JavaDoc -# comments will behave just like the Qt-style comments (thus requiring an -# explicit @brief command for a brief description. - -JAVADOC_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen -# treat a multi-line C++ special comment block (i.e. a block of //! or /// -# comments) as a brief description. This used to be the default behaviour. -# The new default is to treat a multi-line C++ comment block as a detailed -# description. Set this tag to YES if you prefer the old behaviour instead. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the DETAILS_AT_TOP tag is set to YES then Doxygen -# will output the detailed description near the top, like JavaDoc. -# If set to NO, the detailed description appears after the member -# documentation. - -DETAILS_AT_TOP = YES - -# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented -# member inherits the documentation from any documented member that it -# re-implements. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce -# a new page for each member. If set to NO, the documentation of a member will -# be part of the file/class/namespace that contains it. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. -# Doxygen uses this value to replace tabs by spaces in code fragments. - -TAB_SIZE = 4 - -# This tag can be used to specify a number of aliases that acts -# as commands in the documentation. An alias has the form "name=value". -# For example adding "sideeffect=\par Side Effects:\n" will allow you to -# put the command \sideeffect (or @sideeffect) in the documentation, which -# will result in a user-defined paragraph with heading "Side Effects:". -# You can put \n's in the value part of an alias to insert newlines. - -ALIASES = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C -# sources only. Doxygen will then generate output that is more tailored for C. -# For instance, some of the names that are used will be different. The list -# of all members will be omitted, etc. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java -# sources only. Doxygen will then generate output that is more tailored for Java. -# For instance, namespaces will be presented as packages, qualified scopes -# will look different, etc. - -OPTIMIZE_OUTPUT_JAVA = NO - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want to -# include (a tag file for) the STL sources as input, then you should -# set this tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. -# func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. - -BUILTIN_STL_SUPPORT = NO - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES, then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. - -DISTRIBUTE_GROUP_DOC = NO - -# Set the SUBGROUPING tag to YES (the default) to allow class member groups of -# the same type (for instance a group of public functions) to be put as a -# subgroup of that type (e.g. under the Public Functions section). Set it to -# NO to prevent subgrouping. Alternatively, this can be done per class using -# the \nosubgrouping command. - -SUBGROUPING = YES - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in -# documentation are documented, even if no documentation was available. -# Private class members and static file members will be hidden unless -# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES all private members of a class -# will be included in the documentation. - -EXTRACT_PRIVATE = NO - -# If the EXTRACT_STATIC tag is set to YES all static members of a file -# will be included in the documentation. - -EXTRACT_STATIC = NO - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) -# defined locally in source files will be included in the documentation. -# If set to NO only classes defined in header files are included. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. When set to YES local -# methods, which are defined in the implementation section but not in -# the interface are included in the documentation. -# If set to NO (the default) only methods in the interface are included. - -EXTRACT_LOCAL_METHODS = NO - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all -# undocumented members of documented classes, files or namespaces. -# If set to NO (the default) these members will be included in the -# various overviews, but no documentation section is generated. -# This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. -# If set to NO (the default) these classes will be included in the various -# overviews. This option has no effect if EXTRACT_ALL is enabled. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all -# friend (class|struct|union) declarations. -# If set to NO (the default) these declarations will be included in the -# documentation. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any -# documentation blocks found inside the body of a function. -# If set to NO (the default) these blocks will be appended to the -# function's detailed documentation block. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation -# that is typed after a \internal command is included. If the tag is set -# to NO (the default) then the documentation will be excluded. -# Set it to YES to include the internal documentation. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate -# file names in lower-case letters. If set to YES upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. - -CASE_SENSE_NAMES = YES - -# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen -# will show members with their full class and namespace scopes in the -# documentation. If set to YES the scope will be hidden. - -HIDE_SCOPE_NAMES = NO - -# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen -# will put a list of the files that are included by a file in the documentation -# of that file. - -SHOW_INCLUDE_FILES = YES - -# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] -# is inserted in the documentation for inline members. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen -# will sort the (detailed) documentation of file and class members -# alphabetically by member name. If set to NO the members will appear in -# declaration order. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the -# brief documentation of file, namespace and class members alphabetically -# by member name. If set to NO (the default) the members will appear in -# declaration order. - -SORT_BRIEF_DOCS = YES - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be -# sorted by fully-qualified names, including namespaces. If set to -# NO (the default), the class list will be sorted only by class name, -# not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the -# alphabetical list. - -SORT_BY_SCOPE_NAME = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or -# disable (NO) the todo list. This list is created by putting \todo -# commands in the documentation. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or -# disable (NO) the test list. This list is created by putting \test -# commands in the documentation. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or -# disable (NO) the bug list. This list is created by putting \bug -# commands in the documentation. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or -# disable (NO) the deprecated list. This list is created by putting -# \deprecated commands in the documentation. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional -# documentation sections, marked by \if sectionname ... \endif. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines -# the initial value of a variable or define consists of for it to appear in -# the documentation. If the initializer consists of more lines than specified -# here it will be hidden. Use a value of 0 to hide initializers completely. -# The appearance of the initializer of individual variables and defines in the -# documentation can be controlled using \showinitializer or \hideinitializer -# command in the documentation regardless of this setting. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated -# at the bottom of the documentation of classes and structs. If set to YES the -# list will mention the files that were used to generate the documentation. - -SHOW_USED_FILES = YES - -# If the sources in your project are distributed over multiple directories -# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy -# in the documentation. The default is NO. - -SHOW_DIRECTORIES = NO - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from the -# version control system). Doxygen will invoke the program by executing (via -# popen()) the command <command> <input-file>, where <command> is the value of -# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file -# provided by doxygen. Whatever the program writes to standard output -# is used as the file version. See the manual for examples. - -FILE_VERSION_FILTER = - -#--------------------------------------------------------------------------- -# configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated -# by doxygen. Possible values are YES and NO. If left blank NO is used. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated by doxygen. Possible values are YES and NO. If left blank -# NO is used. - -WARNINGS = YES - -# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings -# for undocumented members. If EXTRACT_ALL is set to YES then this flag will -# automatically be disabled. - -WARN_IF_UNDOCUMENTED = YES - -# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some -# parameters in a documented function, or documenting parameters that -# don't exist or using markup commands wrongly. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be abled to get warnings for -# functions that are documented, but have no documentation for their parameters -# or return value. If set to NO (the default) doxygen will only warn about -# wrong or incomplete parameter documentation, but not about the absence of -# documentation. - -WARN_NO_PARAMDOC = NO - -# The WARN_FORMAT tag determines the format of the warning messages that -# doxygen can produce. The string should contain the $file, $line, and $text -# tags, which will be replaced by the file and line number from which the -# warning originated and the warning text. Optionally the format may contain -# $version, which will be replaced by the version of the file (if it could -# be obtained via FILE_VERSION_FILTER) - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning -# and error messages should be written. If left blank the output is written -# to stderr. - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag can be used to specify the files and/or directories that contain -# documented source files. You may enter file names like "myfile.cpp" or -# directories like "/usr/src/myproject". Separate the files or directories -# with spaces. - -INPUT = . - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank the following patterns are tested: -# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx -# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py - -FILE_PATTERNS = *.c *.h *.py - -# The RECURSIVE tag can be used to turn specify whether or not subdirectories -# should be searched for input files as well. Possible values are YES and NO. -# If left blank NO is used. - -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. - -EXCLUDE = CVS docs fonts pixmaps po - -# The EXCLUDE_SYMLINKS tag can be used select whether or not files or -# directories that are symbolic links (a Unix filesystem feature) are excluded -# from the input. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. Note that the wildcards are matched -# against the file with absolute path, so to exclude all test directories -# for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or -# directories that contain example code fragments that are included (see -# the \include command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp -# and *.h) to filter out the source-files in the directories. If left -# blank all files are included. - -EXAMPLE_PATTERNS = - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude -# commands irrespective of the value of the RECURSIVE tag. -# Possible values are YES and NO. If left blank NO is used. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or -# directories that contain image that are included in the documentation (see -# the \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command <filter> <input-file>, where <filter> -# is the value of the INPUT_FILTER tag, and <input-file> is the name of an -# input file. Doxygen will then use the output that the filter program writes -# to standard output. If FILTER_PATTERNS is specified, this tag will be -# ignored. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: -# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further -# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER -# is applied to all files. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will be used to filter the input files when producing source -# files to browse (i.e. when SOURCE_BROWSER is set to YES). - -FILTER_SOURCE_FILES = NO - -#--------------------------------------------------------------------------- -# configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will -# be generated. Documented entities will be cross-referenced with these sources. -# Note: To get rid of all source code in the generated output, make sure also -# VERBATIM_HEADERS is set to NO. - -SOURCE_BROWSER = YES - -# Setting the INLINE_SOURCES tag to YES will include the body -# of functions and classes directly in the documentation. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct -# doxygen to hide any special comment blocks from generated source code -# fragments. Normal C and C++ comments will always remain visible. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES (the default) -# then for each documented function all documented -# functions referencing it will be listed. - -REFERENCED_BY_RELATION = YES - -# If the REFERENCES_RELATION tag is set to YES (the default) -# then for each documented function all documented entities -# called/used by that function will be listed. - -REFERENCES_RELATION = YES - -# If the USE_HTAGS tag is set to YES then the references to source code -# will point to the HTML generated by the htags(1) tool instead of doxygen -# built-in source browser. The htags tool is part of GNU's global source -# tagging system (see http://www.gnu.org/software/global/global.html). You -# will need version 4.8.6 or higher. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen -# will generate a verbatim copy of the header file for each class for -# which an include is specified. Set to NO to disable this. - -VERBATIM_HEADERS = YES - -#--------------------------------------------------------------------------- -# configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index -# of all compounds will be generated. Enable this if the project -# contains a lot of classes, structs, unions or interfaces. - -ALPHABETICAL_INDEX = YES - -# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then -# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns -# in which this list will be split (can be a number in the range [1..20]) - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all -# classes will be put under the same header in the alphabetical index. -# The IGNORE_PREFIX tag can be used to specify one or more prefixes that -# should be ignored while generating the index headers. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES (the default) Doxygen will -# generate HTML output. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `html' will be used as the default path. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for -# each generated HTML page (for example: .htm,.php,.asp). If it is left blank -# doxygen will generate files with .html extension. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a personal HTML header for -# each generated HTML page. If it is left blank doxygen will generate a -# standard header. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a personal HTML footer for -# each generated HTML page. If it is left blank doxygen will generate a -# standard footer. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading -# style sheet that is used by each HTML page. It can be used to -# fine-tune the look of the HTML output. If the tag is left blank doxygen -# will generate a default style sheet. Note that doxygen will try to copy -# the style sheet file to the HTML output directory, so don't put your own -# stylesheet in the HTML output directory as well, or it will be erased! - -HTML_STYLESHEET = - -# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, -# files or namespaces will be aligned in HTML using tables. If set to -# NO a bullet list will be used. - -HTML_ALIGN_MEMBERS = YES - -# If the GENERATE_HTMLHELP tag is set to YES, additional index files -# will be generated that can be used as input for tools like the -# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) -# of the generated HTML documentation. - -GENERATE_HTMLHELP = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can -# be used to specify the file name of the resulting .chm file. You -# can add a path in front of the file if the result should not be -# written to the html output directory. - -CHM_FILE = - -# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can -# be used to specify the location (absolute path including file name) of -# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run -# the HTML help compiler on the generated index.hhp. - -HHC_LOCATION = - -# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag -# controls if a separate .chi index file is generated (YES) or that -# it should be included in the master .chm file (NO). - -GENERATE_CHI = NO - -# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag -# controls whether a binary table of contents is generated (YES) or a -# normal table of contents (NO) in the .chm file. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members -# to the contents of the HTML help documentation and to the tree view. - -TOC_EXPAND = NO - -# The DISABLE_INDEX tag can be used to turn on/off the condensed index at -# top of each HTML page. The value NO (the default) enables the index and -# the value YES disables it. - -DISABLE_INDEX = NO - -# This tag can be used to set the number of enum values (range [1..20]) -# that doxygen will group on one line in the generated HTML documentation. - -ENUM_VALUES_PER_LINE = 4 - -# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be -# generated containing a tree-like index structure (just like the one that -# is generated for HTML Help). For this to work a browser that supports -# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, -# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are -# probably better off using the HTML help feature. - -GENERATE_TREEVIEW = NO - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be -# used to set the initial width (in pixels) of the frame in which the tree -# is shown. - -TREEVIEW_WIDTH = 250 - -#--------------------------------------------------------------------------- -# configuration options related to the LaTeX output -#--------------------------------------------------------------------------- - -# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will -# generate Latex output. - -GENERATE_LATEX = NO - -# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `latex' will be used as the default path. - -LATEX_OUTPUT = latex - -# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be -# invoked. If left blank `latex' will be used as the default command name. - -LATEX_CMD_NAME = latex - -# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to -# generate index for LaTeX. If left blank `makeindex' will be used as the -# default command name. - -MAKEINDEX_CMD_NAME = makeindex - -# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact -# LaTeX documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_LATEX = NO - -# The PAPER_TYPE tag can be used to set the paper type that is used -# by the printer. Possible values are: a4, a4wide, letter, legal and -# executive. If left blank a4wide will be used. - -PAPER_TYPE = a4wide - -# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX -# packages that should be included in the LaTeX output. - -EXTRA_PACKAGES = - -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for -# the generated latex document. The header should contain everything until -# the first chapter. If it is left blank doxygen will generate a -# standard header. Notice: only use this tag if you know what you are doing! - -LATEX_HEADER = - -# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated -# is prepared for conversion to pdf (using ps2pdf). The pdf file will -# contain links (just like the HTML output) instead of page references -# This makes the output suitable for online browsing using a pdf viewer. - -PDF_HYPERLINKS = NO - -# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of -# plain latex in the generated Makefile. Set this option to YES to get a -# higher quality PDF documentation. - -USE_PDFLATEX = NO - -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. -# command to the generated LaTeX files. This will instruct LaTeX to keep -# running if errors occur, instead of asking the user for help. -# This option is also used when generating formulas in HTML. - -LATEX_BATCHMODE = NO - -# If LATEX_HIDE_INDICES is set to YES then doxygen will not -# include the index chapters (such as File Index, Compound Index, etc.) -# in the output. - -LATEX_HIDE_INDICES = NO - -#--------------------------------------------------------------------------- -# configuration options related to the RTF output -#--------------------------------------------------------------------------- - -# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output -# The RTF output is optimized for Word 97 and may not look very pretty with -# other RTF readers or editors. - -GENERATE_RTF = NO - -# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `rtf' will be used as the default path. - -RTF_OUTPUT = rtf - -# If the COMPACT_RTF tag is set to YES Doxygen generates more compact -# RTF documents. This may be useful for small projects and may help to -# save some trees in general. - -COMPACT_RTF = NO - -# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated -# will contain hyperlink fields. The RTF file will -# contain links (just like the HTML output) instead of page references. -# This makes the output suitable for online browsing using WORD or other -# programs which support those fields. -# Note: wordpad (write) and others do not support links. - -RTF_HYPERLINKS = NO - -# Load stylesheet definitions from file. Syntax is similar to doxygen's -# config file, i.e. a series of assignments. You only have to provide -# replacements, missing definitions are set to their default value. - -RTF_STYLESHEET_FILE = - -# Set optional variables used in the generation of an rtf document. -# Syntax is similar to doxygen's config file. - -RTF_EXTENSIONS_FILE = - -#--------------------------------------------------------------------------- -# configuration options related to the man page output -#--------------------------------------------------------------------------- - -# If the GENERATE_MAN tag is set to YES (the default) Doxygen will -# generate man pages - -GENERATE_MAN = NO - -# The MAN_OUTPUT tag is used to specify where the man pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `man' will be used as the default path. - -MAN_OUTPUT = man - -# The MAN_EXTENSION tag determines the extension that is added to -# the generated man pages (default is the subroutine's section .3) - -MAN_EXTENSION = .3 - -# If the MAN_LINKS tag is set to YES and Doxygen generates man output, -# then it will generate one additional man file for each entity -# documented in the real man page(s). These additional files -# only source the real man page, but without them the man command -# would be unable to find the correct page. The default is NO. - -MAN_LINKS = NO - -#--------------------------------------------------------------------------- -# configuration options related to the XML output -#--------------------------------------------------------------------------- - -# If the GENERATE_XML tag is set to YES Doxygen will -# generate an XML file that captures the structure of -# the code including all documentation. - -GENERATE_XML = NO - -# The XML_OUTPUT tag is used to specify where the XML pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be -# put in front of it. If left blank `xml' will be used as the default path. - -XML_OUTPUT = xml - -# The XML_SCHEMA tag can be used to specify an XML schema, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_SCHEMA = - -# The XML_DTD tag can be used to specify an XML DTD, -# which can be used by a validating XML parser to check the -# syntax of the XML files. - -XML_DTD = - -# If the XML_PROGRAMLISTING tag is set to YES Doxygen will -# dump the program listings (including syntax highlighting -# and cross-referencing information) to the XML output. Note that -# enabling this will significantly increase the size of the XML output. - -XML_PROGRAMLISTING = YES - -#--------------------------------------------------------------------------- -# configuration options for the AutoGen Definitions output -#--------------------------------------------------------------------------- - -# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will -# generate an AutoGen Definitions (see autogen.sf.net) file -# that captures the structure of the code including all -# documentation. Note that this feature is still experimental -# and incomplete at the moment. - -GENERATE_AUTOGEN_DEF = NO - -#--------------------------------------------------------------------------- -# configuration options related to the Perl module output -#--------------------------------------------------------------------------- - -# If the GENERATE_PERLMOD tag is set to YES Doxygen will -# generate a Perl module file that captures the structure of -# the code including all documentation. Note that this -# feature is still experimental and incomplete at the -# moment. - -GENERATE_PERLMOD = NO - -# If the PERLMOD_LATEX tag is set to YES Doxygen will generate -# the necessary Makefile rules, Perl scripts and LaTeX code to be able -# to generate PDF and DVI output from the Perl module output. - -PERLMOD_LATEX = NO - -# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be -# nicely formatted so it can be parsed by a human reader. This is useful -# if you want to understand what is going on. On the other hand, if this -# tag is set to NO the size of the Perl module output will be much smaller -# and Perl will parse it just the same. - -PERLMOD_PRETTY = YES - -# The names of the make variables in the generated doxyrules.make file -# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. -# This is useful so different doxyrules.make files included by the same -# Makefile don't overwrite each other's variables. - -PERLMOD_MAKEVAR_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the preprocessor -#--------------------------------------------------------------------------- - -# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will -# evaluate all C-preprocessor directives found in the sources and include -# files. - -ENABLE_PREPROCESSING = YES - -# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro -# names in the source code. If set to NO (the default) only conditional -# compilation will be performed. Macro expansion can be done in a controlled -# way by setting EXPAND_ONLY_PREDEF to YES. - -MACRO_EXPANSION = NO - -# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES -# then the macro expansion is limited to the macros specified with the -# PREDEFINED and EXPAND_AS_DEFINED tags. - -EXPAND_ONLY_PREDEF = NO - -# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files -# in the INCLUDE_PATH (see below) will be search if a #include is found. - -SEARCH_INCLUDES = YES - -# The INCLUDE_PATH tag can be used to specify one or more directories that -# contain include files that are not input files but should be processed by -# the preprocessor. - -INCLUDE_PATH = - -# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard -# patterns (like *.h and *.hpp) to filter out the header-files in the -# directories. If left blank, the patterns specified with FILE_PATTERNS will -# be used. - -INCLUDE_FILE_PATTERNS = - -# The PREDEFINED tag can be used to specify one or more macro names that -# are defined before the preprocessor is started (similar to the -D option of -# gcc). The argument of the tag is a list of macros of the form: name -# or name=definition (no spaces). If the definition and the = are -# omitted =1 is assumed. To prevent a macro definition from being -# undefined via #undef or recursively expanded use the := operator -# instead of the = operator. - -PREDEFINED = - -# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then -# this tag can be used to specify a list of macro names that should be expanded. -# The macro definition that is found in the sources will be used. -# Use the PREDEFINED tag if you want to use a different macro definition. - -EXPAND_AS_DEFINED = - -# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then -# doxygen's preprocessor will remove all function-like macros that are alone -# on a line, have an all uppercase name, and do not end with a semicolon. Such -# function macros are typically used for boiler-plate code, and will confuse -# the parser if not removed. - -SKIP_FUNCTION_MACROS = YES - -#--------------------------------------------------------------------------- -# Configuration::additions related to external references -#--------------------------------------------------------------------------- - -# The TAGFILES option can be used to specify one or more tagfiles. -# Optionally an initial location of the external documentation -# can be added for each tagfile. The format of a tag file without -# this location is as follows: -# TAGFILES = file1 file2 ... -# Adding location for the tag files is done as follows: -# TAGFILES = file1=loc1 "file2 = loc2" ... -# where "loc1" and "loc2" can be relative or absolute paths or -# URLs. If a location is present for each tag, the installdox tool -# does not have to be run to correct the links. -# Note that each tag file must have a unique name -# (where the name does NOT include the path) -# If a tag file is not located in the directory in which doxygen -# is run, you must also specify the path to the tagfile here. - -TAGFILES = - -# When a file name is specified after GENERATE_TAGFILE, doxygen will create -# a tag file that is based on the input files it reads. - -GENERATE_TAGFILE = - -# If the ALLEXTERNALS tag is set to YES all external classes will be listed -# in the class index. If set to NO only the inherited external classes -# will be listed. - -ALLEXTERNALS = NO - -# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will -# be listed. - -EXTERNAL_GROUPS = YES - -# The PERL_PATH should be the absolute path and name of the perl script -# interpreter (i.e. the result of `which perl'). - -PERL_PATH = /usr/bin/perl - -#--------------------------------------------------------------------------- -# Configuration options related to the dot tool -#--------------------------------------------------------------------------- - -# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will -# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base -# or super classes. Setting the tag to NO turns the diagrams off. Note that -# this option is superseded by the HAVE_DOT option below. This is only a -# fallback. It is recommended to install and use dot, since it yields more -# powerful graphs. - -CLASS_DIAGRAMS = YES - -# If set to YES, the inheritance and collaboration graphs will hide -# inheritance and usage relations if the target is undocumented -# or is not a class. - -HIDE_UNDOC_RELATIONS = YES - -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is -# available from the path. This tool is part of Graphviz, a graph visualization -# toolkit from AT&T and Lucent Bell Labs. The other options in this section -# have no effect if this option is set to NO (the default) - -HAVE_DOT = NO - -# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect inheritance relations. Setting this tag to YES will force the -# the CLASS_DIAGRAMS tag to NO. - -CLASS_GRAPH = YES - -# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for each documented class showing the direct and -# indirect implementation dependencies (inheritance, containment, and -# class references variables) of the class with other documented classes. - -COLLABORATION_GRAPH = YES - -# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen -# will generate a graph for groups, showing the direct groups dependencies - -GROUP_GRAPHS = YES - -# If the UML_LOOK tag is set to YES doxygen will generate inheritance and -# collaboration diagrams in a style similar to the OMG's Unified Modeling -# Language. - -UML_LOOK = NO - -# If set to YES, the inheritance and collaboration graphs will show the -# relations between templates and their instances. - -TEMPLATE_RELATIONS = NO - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT -# tags are set to YES then doxygen will generate a graph for each documented -# file showing the direct and indirect include dependencies of the file with -# other documented files. - -INCLUDE_GRAPH = YES - -# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and -# HAVE_DOT tags are set to YES then doxygen will generate a graph for each -# documented header file showing the documented files that directly or -# indirectly include this file. - -INCLUDED_BY_GRAPH = YES - -# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will -# generate a call dependency graph for every global function or class method. -# Note that enabling this option will significantly increase the time of a run. -# So in most cases it will be better to enable call graphs for selected -# functions only using the \callgraph command. - -CALL_GRAPH = NO - -# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen -# will graphical hierarchy of all classes instead of a textual one. - -GRAPHICAL_HIERARCHY = YES - -# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES -# then doxygen will show the dependencies a directory has on other directories -# in a graphical way. The dependency relations are determined by the #include -# relations between the files in the directories. - -DIRECTORY_GRAPH = YES - -# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -# generated by dot. Possible values are png, jpg, or gif -# If left blank png will be used. - -DOT_IMAGE_FORMAT = png - -# The tag DOT_PATH can be used to specify the path where the dot tool can be -# found. If left blank, it is assumed the dot tool can be found in the path. - -DOT_PATH = - -# The DOTFILE_DIRS tag can be used to specify one or more directories that -# contain dot files that are included in the documentation (see the -# \dotfile command). - -DOTFILE_DIRS = - -# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width -# (in pixels) of the graphs generated by dot. If a graph becomes larger than -# this value, doxygen will try to truncate the graph, so that it fits within -# the specified constraint. Beware that most browsers cannot cope with very -# large images. - -MAX_DOT_GRAPH_WIDTH = 1024 - -# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height -# (in pixels) of the graphs generated by dot. If a graph becomes larger than -# this value, doxygen will try to truncate the graph, so that it fits within -# the specified constraint. Beware that most browsers cannot cope with very -# large images. - -MAX_DOT_GRAPH_HEIGHT = 1024 - -# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the -# graphs generated by dot. A depth value of 3 means that only nodes reachable -# from the root by following a path via at most 3 edges will be shown. Nodes -# that lay further from the root node will be omitted. Note that setting this -# option to 1 or 2 may greatly reduce the computation time needed for large -# code bases. Also note that a graph may be further truncated if the graph's -# image dimensions are not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH -# and MAX_DOT_GRAPH_HEIGHT). If 0 is used for the depth value (the default), -# the graph is not depth-constrained. - -MAX_DOT_GRAPH_DEPTH = 0 - -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, which results in a white background. -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). - -DOT_TRANSPARENT = NO - -# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) -# support this, this feature is disabled by default. - -DOT_MULTI_TARGETS = NO - -# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will -# generate a legend page explaining the meaning of the various boxes and -# arrows in the dot generated graphs. - -GENERATE_LEGEND = YES - -# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will -# remove the intermediate dot files that are used to generate -# the various graphs. - -DOT_CLEANUP = YES - -#--------------------------------------------------------------------------- -# Configuration::additions related to the search engine -#--------------------------------------------------------------------------- - -# The SEARCHENGINE tag specifies whether or not a search engine should be -# used. If set to NO the values of all tags below this one will be ignored. - -SEARCHENGINE = NO diff --git a/anaconda/docs/boot-options.txt b/anaconda/docs/boot-options.txt index 983d9f2..07ead6e 100644 --- a/anaconda/docs/boot-options.txt +++ b/anaconda/docs/boot-options.txt @@ -312,9 +312,14 @@ Requires the remote syslog process to accept incoming connections. === inst.virtiolog === Forward logs through the named virtio port (a character device at -`/dev/virtio-ports/<name>`). A port named `org.fedoraproject.anaconda.log.0` +`/dev/virtio-ports/<name>`). + +If not provided, a port named `org.fedoraproject.anaconda.log.0` will be used by default, if found. +See https://fedoraproject.org/wiki/Anaconda/Logging for more info on setting +up logging via virtio. + === inst.zram === Forces/disables (on/off) usage of zRAM swap for the installation process. diff --git a/anaconda/docs/kickstart.rst b/anaconda/docs/kickstart.rst new file mode 100644 index 0000000..59529c0 --- /dev/null +++ b/anaconda/docs/kickstart.rst @@ -0,0 +1,63 @@ +Anaconda Kickstart Documentation +================================ + +Anaconda uses `kickstart <https://github.com/rhinstaller/pykickstart>`_ to automate +installation and as a data store for the user interface. It also extends the kickstart +commands `documented here <https://fedoraproject.org/wiki/Anaconda/Kickstart>`_ +by adding a new kickstart section named %anaconda where commands to control the behavior +of Anaconda will be defined. + +.. contents:: %anaconda section commands + + +pwpolicy +======== + +pwpolicy <name> [--minlen=LENGTH] [--minquality=QUALITY] [--strict|notstrict] [--emptyok|notempty] [--changesok|nochanges] + Set the policy to use for the named password entry. + + name + Name of the password entry, currently supported values are: root, user and luks + + --minlen (**8**) + Minimum password length. This is passed on to libpwquality. + + --minquality (**50**) + Minimum libpwquality to consider good. When using --strict it will not allow + passwords with a quality lower than this. + + --strict (**DEFAULT**) + Strict password enforcement. Passwords not meeting the --minquality level will + not be allowed. + + --notstrict + Passwords not meeting the --minquality level will be allowed after Done is clicked + twice. + + --emptyok (**DEFAULT**) + Allow empty password. + + --notempty + Don't allow an empty password + + --changesok + Allow UI to be used to change the password/user when it has already been set in + the kickstart. + + --nochanges (**DEFAULT**) + Do not allow UI to be used to change the password/user if it has been set in + the kickstart. + +The defaults for these are set in the /usr/share/anaconda/interactive-defaults.ks file +provided by Anaconda. If a product, such as Fedora Workstation, wishes to override them +then a product.img needs to be created with a new version of the file included. + +When using a kickstart the defaults can be overridded by placing a %anaconda section into +the kickstart, like this:: + + %anaconda + pwpolicy root --minlen=10 --minquality=60 --strict --notempty --nochanges + %end + +.. note:: The commit message for pwpolicy included some incorrect examples. + diff --git a/anaconda/docs/lvm_sanity_checks.txt b/anaconda/docs/lvm_sanity_checks.txt deleted file mode 100644 index 967d17b..0000000 --- a/anaconda/docs/lvm_sanity_checks.txt +++ /dev/null @@ -1,13 +0,0 @@ -Updated June 11, 2002 ---------------------- - -Current list of things we check for (good for regression testing): - -- That selected PE is smaller than any PV in VG. -- That size requests for LV are a multiple of the PE. -- That maximum LV for a given PE is bigger than any currently defined LV in VG. -- That if you change the PE, all the reclamped LV will fit in VG. -- That VG_name+LV_name is unique for all VG. -- That mount points are used only once. -- Give warning if more than 10% of a PV is lost because of the PE selected. -- That the bootable partition is not a LV. diff --git a/anaconda/docs/making-screenshots b/anaconda/docs/making-screenshots deleted file mode 100644 index 1afdede..0000000 --- a/anaconda/docs/making-screenshots +++ /dev/null @@ -1,15 +0,0 @@ -How to make screenshots: - - -This will only currently work for graphical installs. - -During a graphical installation, you can now press SHIFT-Print Screen -and a screenshot of the current installation screen will be taken. - -These are stored in the following directory: - - /root/anaconda-screenshots/ - -The screenshots can be accessed once the newly-installed system is rebooted. - - diff --git a/anaconda/docs/mediacheck.txt b/anaconda/docs/mediacheck.txt deleted file mode 100644 index d320977..0000000 --- a/anaconda/docs/mediacheck.txt +++ /dev/null @@ -1,25 +0,0 @@ -Mediacheck documentation -October, 2008 - - -Mediacheck is a tool we use to test the integrity of ISO images. The -ISO9660 specification allows for a 512 byte region which is for -application use. We use it to store a checksum of the ISO image. Of -couse putting the checksum into this region will change the checksum -of the final ISO image, so when we verify the checksum later we have -to fill in this 512 region with the original contents before the -implantation. It is set to all ASCII 32 decimal spaces. - -If you have a set of ISO images you can implant the checksum data with -this command: - - implantmd5iso <isoname> - -NOTE: You cannot re-implant an ISO once its been implanted. - -To check a ISO image you can use this command line: - - checkisomd5 <isoname> - -The tools are in the isomd5sum package. - diff --git a/anaconda/docs/rescue-mode b/anaconda/docs/rescue-mode deleted file mode 100644 index 37614d5..0000000 --- a/anaconda/docs/rescue-mode +++ /dev/null @@ -1,16 +0,0 @@ -Rescue mode ------------ - -1/4/1999 Michael Fulbright - -Rescue mode is implemented via a bootable CDROM currently. The user -specifies 'linux rescue' at the syslinux prompt when the CDROM boots. -Then the kernel and an initial ramdisk is loaded. The installer is -run and the user is dropped into bash. - -The upd-instroot script in the anaconda/ source toplevel directory -is responsible for creating the instimage which is used as the -root for the rescue environment. It is located in /mnt/cdrom/Redhat/instimage -when the CDROM is mounted under /mnt/cdrom. - - diff --git a/anaconda/docs/threads.txt b/anaconda/docs/threads.txt deleted file mode 100644 index c9961b6..0000000 --- a/anaconda/docs/threads.txt +++ /dev/null @@ -1,100 +0,0 @@ -Threads in anaconda? No! -David Cantrell <dcantrell@redhat.com> - - -INTRODUCTION - - Threads make a lot of people run screaming. That's entirely - understandable because thread concurrency can be a pain. In this short - document, I want to explain why threads are in anaconda and how to work - with them in the code. - -MISCONCEPTIONS - - Just to make sure everyone is on the same page, threads are similar to - processes. The big advantage we get is easier shared data structures. - Threads can communicate over more methods than just signals. But, - multithreaded does not mean that we are taking every operation out to - a separate thread. - -ANACONDA THREADS - - The idea for anaconda threads is to simplify life for things that can - or need to run parallel to other operations. So we will reserve the - use of threads for tasks that fit in to this category well (the logging - system, for instance) and keep the bulk of the installer in the main - thread. - -THREADS AND PYTHON - - Python has a nice model for threading. Threads in Python extend the - threading.Thread class. So an easy way to identify something that will - run or can be run as a thread is seeing a class definition like this: - - class SomeClass(threading.Thread): - - You still have your __init__ method for the constructor, but threads - also have a run() method and a join() method (there are others, but - I will just discuss these). - - The run() method is called when you start the thread. This is where - you want to do the work. Normally this happens in the class - constructor, but in threads we need that separated out to a different - method. - - The join() method is to block execution of other threads. Whatever you - put in the join() method will run and other threads will be blocked - while it runs. Now, this method is only run when you call it explicitly - from another thread, so think of it as similar to waitpid(). - - Python has the thread and threading modules. Use threading as it's - built on top of thread and provides a threading system similar to the - POSIX thread model. - - More information: - http://docs.python.org/lib/module-threading.html - -THREAD NAMES - - Threads have names in Python. They are automatically assigned or you - can specify the name. For anaconda it probably makes more sense to - name our threads since we won't be launching more than one for the - same task. - - The convention I'm using is: NameThr - For example: RelNotesThr - - The name is arbitrary, but we should probably have some sort of - consistency. - -PYGTK AND THREADS - - GTK+ presents the biggest challenge for threads, but it's not - impossible. When you call gtk.main(), you need to call it like this: - - gtk.threads_enter() - gtk.main() - gtk.threads_leave() - - Calls to PyGTK methods or fiddling with GTK objects...all that has to - be wrapped in threads_enter/threads_leave calls. The suggested syntax - is: - - gtk.threads_enter() - try: - # do stuff - finally: - gtk.threads_leave() - - Suggested reading: - - http://www.async.com.br/faq/pygtk/index.py?req=show&file=faq20.006.htp - http://developer.gnome.org/doc/API/2.0/gdk/gdk-Threads.html - - -I will try to expand this document as I move through more threading code. -Email me if you have any questions. - --- -David Cantrell -<dcantrell@redhat.com> diff --git a/anaconda/docs/transifex.txt b/anaconda/docs/transifex.txt deleted file mode 100644 index afbc9ce..0000000 --- a/anaconda/docs/transifex.txt +++ /dev/null @@ -1,129 +0,0 @@ -Transifex and anaconda Development -09-Mar-2011 -by: David Cantrell <dcantrell@redhat.com> ------------------------------------------------------------------------------ - -Setting up the new transifex-client on your system for anaconda builds. - -1) Install the transifex-client package: - yum install transifex-client - -or- - yum --enablerepo=updates-testing install transifex-client - -or- - yum --enablerepo=epel-testing install transifex-client - -2) Create a Transifex.net account at https://fedora.transifex.net/ - NOTE: This system is not linked to FAS, it's hosted by another company, - so it requires another account at this time. I'm sure this will change - in the future, but this is how it is for now. - -3) Configure 'tx' on your system: - tx init - Accept default host, fill in your username and password generated in #2. - -Now tx is set up on your system. The translation files will only be pulled -when a 'make release' is done. The 'make dist' step will just create a tar -file of the what we have in our repo. The 'make bumpver' step will also -push a new anaconda.pot file to Transifex, so any string changes are pushed -to them on a regular basis. - -NOTE: tx pull is slow. This is why I only added it to the 'make bumpver' -step. - -There are some other procedures related to tx that will have to be done -when we create new branches or when there are translation errors. - - -MAKING A RELEASE ----------------- - -git clean -d -x -f -./autogen.sh && ./configure --disable-static \ ---enable-introspection --enable-gtk-doc -make bumpver # tx pull by dependent po-pull target -git commit -a -m "New version." # DO NOT run 'git clean -d -x -f' after -make && make release # signed tag happens after dist now - -The process here is mostly the same. I do not recommend that you run -git clean between 'make bumpver' and 'make release'. The reason is you -will have to run 'tx pull' again and that's slow, plus translations may -have changed between the two steps. - -The 'make tag' step now runs after 'make dist' in case dist generation -fails. That way you don't end up with a partially created dist AND a -bad tag you have to delete. - -The 'make scratch' target will also run po-pull to get translations. If -we need translation files in other targets, we can add po-pull as a -dependent target. - - -DEALING WITH ERRORS IN *.po FILES ---------------------------------- - -Translators sometimes introduce errors in the .po files. What we generally -do is try to fix it if it's an obvious typo, or just revert the change and -go back to the old po file. Reverting is harder now since we are not -storing po files in our repo, but in severe cases we can go and fetch the -last build and pull the affected po file from there and use it to revert the -changes. - -Here's an example of a po file error that will halt a 'make release': - - rm -f af.gmo && /usr/bin/msgfmt -c --statistics -o af.gmo af.po - af.po:7: field `Language-Team' still has initial default value - af.po:1614: number of format specifications in 'msgid' and 'msgstr[1]' does not match - /usr/bin/msgfmt: found 1 fatal error - -In this case, I am going to the last known good af.po. To update Transifex, -I do: - - cp /path/to/last/known/good/af.po po/af.po - touch po/af.po - tx push -t -l af - -The touch is necessary because transifex.net uses timestamps to determine -if it should update its translation data with the po file you are asking -it to use. - - -CREATING A NEW ANACONDA BRANCH ------------------------------- - -When we make a new branch, we need to branch the translation files. - -First you need to populate the project with the initial po files. I suggest -using the ones from the master branch, e.g.: - - git checkout master - git clean -xdf - tx pull -a - # leave the *.po files in the po/ subdirectory - git checkout BRANCH_NAME - -Next you need to update the transifex config with the new branch: - - tx set --execute --auto-local -r anaconda.BRANCH_NAME -s en -t PO \ - -f po/anaconda.pot "po/<lang>.po" - -The last argument is correct as-is, it's not a place where you substitute -something for <lang>. The BRANCH_NAME will be something other than 'master'. -For example, when we branch for F-20: - - tx set --execute --auto-local -r anaconda.f20-branch -s en -t PO \ - -f po/anaconda.pot "po/<lang>.po" - -Check the .tx/config file on the branch to ensure it references the correct -anaconda.BRANCH_NAME in Transifex and remove the [anaconda.master] block so -that it doesn't try to push to master and the new branch. - -Now you can run: - - tx push -s -t - -This will push the po files and anaconda.pot from master to the BRANCH_NAME -resource for anaconda in Transifex. This is just an initial seed that the -translation team can work with. And since we branch from master, the code -should be more or less in sync with the po files at branch time. - -Don't forget to commit the new .tx/config file to the branch. diff --git a/anaconda/docs/translations.txt b/anaconda/docs/translations.txt new file mode 100644 index 0000000..50a529d --- /dev/null +++ b/anaconda/docs/translations.txt @@ -0,0 +1,116 @@ +Translations and anaconda Development +24-Feb-2015 +by: David Cantrell <dcantrell@redhat.com> + Brian C. Lane <bcl@redhat.com> +----------------------------------------------------------------------------- + +Anaconda, as of version 23.1, is using https://fedora.zanata.com for +translations. You will need an account in order to pull translations until bug +https://bugzilla.redhat.com/show_bug.cgi?id=1172618 (anonymous pull requests) +has been resolved. + +The zanata-python-client is used by the build scripts, not the full zanata +package. + + +CLIENT SETUP +------------ +Setting up the zanata-python-client on your system for anaconda builds. + +1) Install the zanata-python-client package: + yum install zanata-python-client + +2) Create a Zanata account at https://fedora.zanata.org + NOTE: This system is linked to FAS, but the 'remember my login' option + only appears to work for about an hour, not 7 days as it claims. + +3) Navigate to Dashboard->Settings->Client and click 'Generate New API Key' + +4) Copy the contents of the Configuration text box to ~/.config/zanata.ini + and make sure permissions are 0600. + +Now zanata is set up on your system. The translation files will only be pulled +when a 'make release' is done. The 'make dist' step will just create a tar +file of the what we have in our repo. The 'make bumpver' step will also push a +new anaconda.pot file to Zanata, so any string changes are pushed to them on a +regular basis. + +NOTE: zanata pull is slow. This is why I only added it to the 'make bumpver' +step. + +There are some other procedures related to zanata that will have to be done +when we create new branches or when there are translation errors. + + +MAKING A RELEASE +---------------- + +git clean -d -x -f +./autogen.sh && ./configure --disable-static \ +--enable-introspection --enable-gtk-doc +make bumpver # zanata pull by dependent po-pull target +git commit -a -m "New version." # DO NOT run 'git clean -d -x -f' after +make && make release # signed tag happens after dist now + +The process here is mostly the same. I do not recommend that you run +git clean between 'make bumpver' and 'make release'. The reason is you +will have to run 'zanata pull' again and that's slow, plus translations may +have changed between the two steps. + +The 'make tag' step now runs after 'make dist' in case dist generation +fails. That way you don't end up with a partially created dist AND a +bad tag you have to delete. + +The 'make scratch' and 'make po-empty' targets will now create empty +translation files so that local test builds can be created without needing a +zanata account. + + +DEALING WITH ERRORS IN *.po FILES +--------------------------------- + +Translators sometimes introduce errors in the .po files. What we generally +do is try to fix it if it's an obvious typo, or just revert the change and +go back to the old po file. Reverting is harder now since we are not +storing po files in our repo, but in severe cases we can go and fetch the +last build and pull the affected po file from there and use it to revert the +changes. + +Here's an example of a po file error that will halt a 'make release': + + rm -f af.gmo && /usr/bin/msgfmt -c --statistics -o af.gmo af.po + af.po:7: field `Language-Team' still has initial default value + af.po:1614: number of format specifications in 'msgid' and 'msgstr[1]' does not match + /usr/bin/msgfmt: found 1 fatal error + +In this case, I am going to the last known good af.po. To update Zanata, +I do: + + cp /path/to/last/known/good/af.po po/af.po + zanata push --push-type target --lang af + + +CREATING A NEW ANACONDA BRANCH +------------------------------ + +When we make a new branch, we need to branch the translation files. + +On https://fedora.zanata.com go to the project and version to use as a base. +Select 'New Version+' from the hidden drop down menu on the right. Give it a +name that matches the git branch. eg. f23-branch and select the version to copy +from (usually master). Wait for it to finish processing documents. + +Create a new git branch: + + git checkout master + git clean -xdf + git checkout BRANCH_NAME + +At https://fedora.zanata.com select the new version and then select the +'Download Config file' from the hidden dropdown menu next to 'Settings'. This +will download a new zanata.xml file. Replace the zanata.xml file in the root +directory of anaconda project with this new one. + + git add -u + git commit -m "New Zanata config file" + git push --set-upstream origin BRANCH_NAME diff --git a/anaconda/dracut/anaconda-diskroot b/anaconda/dracut/anaconda-diskroot index 432c942..22da180 100755 --- a/anaconda/dracut/anaconda-diskroot +++ b/anaconda/dracut/anaconda-diskroot @@ -37,6 +37,17 @@ path="$2" # optional, could be empty modprobe -q loop +# If we're waiting for a cdrom kickstart, the user might need to swap discs. +# So if this is a CDROM drive, make a note of it, but don't mount it (yet). +# Once we get the kickstart either the udev trigger or disk-reinsertion will +# retrigger this script, and we'll mount the disk as normal. +if str_starts "$kickstart" "cdrom" && [ ! -e /tmp/ks.cfg.done ]; then + if dev_is_cdrom "$dev"; then + > /tmp/anaconda-on-cdrom + exit 0 + fi +fi + info "anaconda using disk root at $dev" mount $dev $repodir || warn "Couldn't mount $dev" anaconda_live_root_dir $repodir $path diff --git a/anaconda/dracut/anaconda-lib.sh b/anaconda/dracut/anaconda-lib.sh index 9075c5c..8128ca0 100755 --- a/anaconda/dracut/anaconda-lib.sh +++ b/anaconda/dracut/anaconda-lib.sh @@ -165,6 +165,26 @@ when_any_cdrom_appears() { >> $rulesfile } +plymouth_running() { + type plymouth >/dev/null 2>&1 && plymouth --ping 2>/dev/null +} + +# print something to the display (and put it in the log so we know what's up) +tell_user() { + if plymouth_running; then + # NOTE: if we're doing graphical splash but we don't have all the + # font-rendering libraries, no message will appear. + plymouth display-message --text="$*" + echo "$*" # this goes to journal only + else + echo "$*" >&2 # this goes to journal+console + fi +} + +dev_is_cdrom() { + udevadm info --query=property --name=$1 | grep -q 'ID_CDROM=1' +} + # dracut doesn't bring up the network unless: # a) $netroot is set (i.e. you have a network root device), or # b) /tmp/net.ifaces exists. diff --git a/anaconda/dracut/anaconda-netroot.sh b/anaconda/dracut/anaconda-netroot.sh index 6217953..0a9face 100755 --- a/anaconda/dracut/anaconda-netroot.sh +++ b/anaconda/dracut/anaconda-netroot.sh @@ -27,9 +27,14 @@ case $repo in . /lib/nfs-lib.sh info "anaconda mounting NFS repo at $repo" str_starts "$repo" "nfsiso:" && repo=nfs:${repo#nfsiso:} + + # Replace hex space with a real one. All uses of repo need to be quoted + # after this point. + repo=${repo//\\x20/ } + # HACK: work around some Mysterious NFS4 Badness (#811242 and friends) # by defaulting to nfsvers=3 when no version is requested - nfs_to_var $repo $netif + nfs_to_var "$repo" $netif if [ "$nfs" != "nfs4" ] && ! strstr "$options" "vers="; then repo="nfs:$options,nfsvers=3:$server:$path" fi diff --git a/anaconda/dracut/driver-updates b/anaconda/dracut/driver-updates index 373884f..24a18ba 100644 --- a/anaconda/dracut/driver-updates +++ b/anaconda/dracut/driver-updates @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2013 by Red Hat, Inc. All rights reserved. # @@ -43,6 +43,7 @@ import os import subprocess import time import glob +import readline # pylint:disable=unused-import log = logging.getLogger("DD") @@ -312,7 +313,7 @@ def fake_drivers(num): """ Generate a number of fake drivers for testing """ drivers = [] - for i in xrange(0, num): + for i in range(0, num): d = Driver() d.source = "driver-%d" % i d.flags = "modules" @@ -475,7 +476,7 @@ def selection_menu(items, title, info_func, multi_choice=True, refresh=False): num_items = page_length else: num_items = len(items) % page_length - for i in xrange(0, num_items): + for i in range(0, num_items): item_idx = ((page-1) * page_length) + i if multi_choice: if items[item_idx].selected: diff --git a/anaconda/dracut/fetch-kickstart-disk b/anaconda/dracut/fetch-kickstart-disk index d9ead33..37a1050 100755 --- a/anaconda/dracut/fetch-kickstart-disk +++ b/anaconda/dracut/fetch-kickstart-disk @@ -14,16 +14,27 @@ info "anaconda: fetching kickstart from $dev:$path" mnt="$(find_mount $dev)" if [ -n "$mnt" ]; then - cp $mnt$path /tmp/ks.cfg + cp $mnt$path /tmp/ks.cfg 2>&1 else tmpmnt="$(mkuniqdir /run/install tmpmnt)" if mount -o ro $dev $tmpmnt; then - cp $tmpmnt/$path /tmp/ks.cfg + cp $tmpmnt/$path /tmp/ks.cfg 2>&1 umount $tmpmnt rmdir $tmpmnt fi fi + +# if we're waiting for a cdrom kickstart, tell the user so they can swap discs +if str_starts "$kickstart" "cdrom"; then + if [ ! -f /tmp/ks.cfg ]; then + tell_user "Please insert CDROM containing '$path'..." + exit 0 + elif [ -f /tmp/anaconda-on-cdrom ]; then + tell_user "Kickstart loaded. Please re-insert installation media." + fi +fi + if [ -f /tmp/ks.cfg ]; then parse_kickstart /tmp/ks.cfg run_kickstart diff --git a/anaconda/dracut/parse-kickstart b/anaconda/dracut/parse-kickstart index 7c60405..9163df3 100755 --- a/anaconda/dracut/parse-kickstart +++ b/anaconda/dracut/parse-kickstart @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 #vim: set fileencoding=utf8 # parse-kickstart - read a kickstart file and emit equivalent dracut boot args # @@ -88,12 +88,26 @@ class NFS(commands.nfs.FC6_NFS): else: method="nfs:%s:%s" % (self.server, self.dir) + # Spaces on the cmdline need to be '\ ' + method = method.replace(" ", "\\ ") return "inst.repo=%s" % method class URL(commands.url.F18_Url): def dracut_args(self, args, lineno, obj): - # FIXME: self.proxy, self.noverifyssl - return "inst.repo=%s" % self.url + # Spaces in the url need to be %20 + if self.url: + method = self.url.replace(" ", "%20") + else: + method = None + + args = ["inst.repo=%s" % method] + + if self.noverifyssl: + args.append("rd.noverifyssl") + if self.proxy: + args.append("proxy=%s" % self.proxy) + + return "\n".join(args) class Updates(commands.updates.F7_Updates): def dracut_args(self, args, lineno, obj): @@ -127,7 +141,7 @@ class DriverDisk(commands.driverdisk.F14_DriverDisk): # are processed later. return "\n".join(dd_net) -class Network(commands.network.F21_Network): +class Network(commands.network.F22_Network): def dracut_args(self, args, lineno, net): ''' NOTE: The first 'network' line get special treatment: @@ -321,6 +335,16 @@ def ksnet_to_dracut(args, lineno, net, bootdev=False): return " ".join(line) +def add_s390_settings(dev, ifcfg): + s390cfg = s390_settings(dev) + if s390cfg['SUBCHANNELS']: + ifcfg.pop('HWADDR', None) + ifcfg['SUBCHANNELS'] = s390cfg['SUBCHANNELS'] + if s390cfg['NETTYPE']: + ifcfg['NETTYPE'] = s390cfg['NETTYPE'] + if s390cfg['OPTIONS']: + ifcfg['OPTIONS'] = s390cfg['OPTIONS'] + def ksnet_to_ifcfg(net, filename=None): '''Write an ifcfg file for the given kickstart network config''' dev = net.device @@ -329,8 +353,8 @@ def ksnet_to_ifcfg(net, filename=None): if not dev: return if (not os.path.isdir("/sys/class/net/%s" % dev) - and not net.bondslaves and not net.teamslaves): - log.info("can't find device %s" % dev) + and not net.bondslaves and not net.teamslaves and not net.bridgeslaves): + log.info("can't find device %s", dev) return ifcfg = dict() if filename is None: @@ -338,20 +362,16 @@ def ksnet_to_ifcfg(net, filename=None): if not os.path.isdir("/tmp/ifcfg"): os.makedirs("/tmp/ifcfg") ifcfg['DEVICE'] = dev - ifcfg['HWADDR'] = readsysfile("/sys/class/net/%s/address" % dev) + hwaddr = readsysfile("/sys/class/net/%s/address" % dev) + if "ifname={0}:{1}".format(dev, hwaddr).upper() in open("/proc/cmdline").read().upper(): + # rename by initscript's 60-net-rules on target system after switchroot + ifcfg['HWADDR'] = hwaddr ifcfg['UUID'] = str(uuid.uuid4()) # we set real ONBOOT value in anaconda, here # we use it to activate devcies by NM on start ifcfg['ONBOOT'] = "yes" if net.activate else "no" - s390cfg = s390_settings(dev) - if s390cfg['SUBCHANNELS']: - ifcfg.pop('HWADDR') - ifcfg['SUBCHANNELS'] = s390cfg['SUBCHANNELS'] - if s390cfg['NETTYPE']: - ifcfg['NETTYPE'] = s390cfg['NETTYPE'] - if s390cfg['OPTIONS']: - ifcfg['OPTIONS'] = s390cfg['OPTIONS'] + add_s390_settings(dev, ifcfg) # dhcp etc. ifcfg['BOOTPROTO'] = net.bootProto @@ -417,11 +437,12 @@ def ksnet_to_ifcfg(net, filename=None): 'NAME' : "%s slave %s" % (dev, i), 'UUID' : str(uuid.uuid4()), 'ONBOOT' : "yes", - 'MASTER' : dev, + 'MASTER' : ifcfg['UUID'], 'HWADDR' : readsysfile("/sys/class/net/%s/address" % slave), } + add_s390_settings(slave, slave_ifcfg) slave_filename = "/tmp/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) - log.info("writing ifcfg %s for slave %s of bond %s" % (slave_filename, slave, dev)) + log.info("writing ifcfg %s for slave %s of bond %s", slave_filename, slave, dev) write_ifcfg(slave_filename, slave_ifcfg) if net.teamslaves: @@ -445,7 +466,44 @@ def ksnet_to_ifcfg(net, filename=None): slave_ifcfg['TEAM_PORT_CONFIG'] = "'" + cfg + "'" slave_filename = "/tmp/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) - log.info("writing ifcfg %s for slave %s of team %s" % (slave_filename, slave, dev)) + log.info("writing ifcfg %s for slave %s of team %s", slave_filename, slave, dev) + write_ifcfg(slave_filename, slave_ifcfg) + + if net.bridgeslaves: + + ifcfg.pop('HWADDR', None) + ifcfg['TYPE'] = "Bridge" + ifcfg['NAME'] = "Bridge connection %s" % dev + + options = {} + for opt in net.bridgeopts.split(","): + key, _sep, value = opt.partition("=") + if not value: + log.error("Invalid bridge option %s", opt) + continue + key = key.replace('-', '_') + options[key] = value + stp = options.pop("stp", None) + if stp: + ifcfg['STP'] = stp + delay = options.pop("forward_delay", None) + if delay: + ifcfg['DELAY'] = delay + if options: + keyvalues = ["%s=%s" % (key, options[key]) for key in options] + ifcfg['BRIDGING_OPTS'] = '"' + " ".join(keyvalues) + '"' + + for i, slave in enumerate(net.bridgeslaves.split(","), 1): + slave_ifcfg = { + 'TYPE' : "Ethernet", + 'NAME' : "%s slave %s" % (dev, i), + 'UUID' : str(uuid.uuid4()), + 'ONBOOT' : "yes", + 'BRIDGE' : dev, + 'HWADDR' : readsysfile("/sys/class/net/%s/address" % slave), + } + slave_filename = "/tmp/ifcfg/ifcfg-%s" % "_".join(slave_ifcfg['NAME'].split(" ")) + log.info("writing ifcfg %s for slave %s of bridge %s", slave_filename, slave, dev) write_ifcfg(slave_filename, slave_ifcfg) if net.vlanid: @@ -459,7 +517,7 @@ def ksnet_to_ifcfg(net, filename=None): ifcfg['PHYSDEV'] = dev filename = "/tmp/ifcfg/ifcfg-%s" % interface_name - log.info("writing ifcfg %s for %s" % (filename, dev)) + log.info("writing ifcfg %s for %s", filename, dev) if write_ifcfg(filename, ifcfg): return filename @@ -470,7 +528,7 @@ def write_ifcfg(filename, ifcfg): for k,v in ifcfg.items(): f.write("%s=%s\n" % (k,v)) except IOError as e: - log.error("can't write %s: %s" % (filename, e)) + log.error("can't write %s: %s", filename, e) return False return True @@ -479,6 +537,7 @@ def process_kickstart(ksfile): handler.ksdevice = os.environ.get('ksdevice') parser = KickstartParser(handler, missingIncludeIsFatal=False, errorsAreFatal=False) parser.registerSection(NullSection(handler, sectionOpen="%addon")) + parser.registerSection(NullSection(handler, sectionOpen="%anaconda")) log.info("processing kickstart file %s", ksfile) processed_file = preprocessKickstart(ksfile) try: diff --git a/anaconda/dracut/python-deps b/anaconda/dracut/python-deps index e626ff4..05a48e3 100755 --- a/anaconda/dracut/python-deps +++ b/anaconda/dracut/python-deps @@ -1,6 +1,7 @@ -#!/usr/bin/python +#!/usr/bin/python2 # python-deps - find the dependencies of a given python script. +import copy import os, sys from modulefinder import ModuleFinder # pylint: disable=wildcard-import @@ -9,6 +10,8 @@ from distutils.sysconfig import * sitedir = get_python_lib() libdir = get_config_var('LIBDEST') +alsoNeeded = { libdir+"/urllib.py": libdir+"/urllib2.py" } + # A couple helper functions... def moduledir(pyfile): '''Given a python file, return the module dir it belongs to, or None.''' @@ -30,10 +33,15 @@ def pyfiles(moddir): # OK. Use modulefinder to find all the modules etc. this script uses! mods = [] deps = [] -for script in sys.argv[1:]: + +scripts = copy.copy(sys.argv[1:]) + +while scripts: + script = scripts.pop() + finder = ModuleFinder() finder.run_script(script) # parse the script - for name, mod in finder.modules.iteritems(): + for mod in finder.modules.itervalues(): if not mod.__file__: # this module is builtin, so we can skip it continue @@ -45,6 +53,9 @@ for script in sys.argv[1:]: deps += list(pyfiles(moddir)) # ...get the whole module mods.append(moddir) + if mod.__file__ in alsoNeeded and alsoNeeded[mod.__file__] not in deps: + scripts.append(alsoNeeded[mod.__file__]) + # Include some bits that the python install itself needs print(get_makefile_filename()) print(get_config_h_filename()) diff --git a/anaconda/old_tests/Makefile.am b/anaconda/old_tests/Makefile.am deleted file mode 100644 index c1b3ffe..0000000 --- a/anaconda/old_tests/Makefile.am +++ /dev/null @@ -1,22 +0,0 @@ -# tests/Makefile.am for anaconda -# -# Copyright (C) 2009 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author: David Cantrell <dcantrell@redhat.com> - -SUBDIRS = mock kickstart_test regex pyanaconda_test logpicker_test - -MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/old_tests/mock/Makefile.am b/anaconda/old_tests/mock/Makefile.am deleted file mode 100644 index 87ebd00..0000000 --- a/anaconda/old_tests/mock/Makefile.am +++ /dev/null @@ -1,21 +0,0 @@ -# tests/Makefile.am for anaconda -# -# Copyright (C) 2009 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author: David Cantrell <dcantrell@redhat.com> - -EXTRA_DIST = *.py -MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/old_tests/mock/__init__.py b/anaconda/old_tests/mock/__init__.py deleted file mode 100644 index 3fd749a..0000000 --- a/anaconda/old_tests/mock/__init__.py +++ /dev/null @@ -1,94 +0,0 @@ -# Mocking library for module and code injection, replay tests and other -# unit testing purposes -# -# Copyright (C) 2010 -# Red Hat, Inc. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author(s): Martin Sivak <msivak@redhat.com> - - -from disk import * -from mock import * -import unittest - -def slow(f): - """Decorates a test method as being slow, usefull for python-nose filtering""" - f.slow = True - return f - -def acceptance(f): - """Decorates test as belonging to acceptance testing and not useable in common devellopment unit testing. To be used with python-nose filtering.""" - f.acceptance = True - return f - -class TestCase(unittest.TestCase): - def __init__(self, *args, **kwargs): - unittest.TestCase.__init__(self, *args, **kwargs) - self.injectedModules = {} - - def setupModules(self, a): - """Mock specified list of modules and store the list so it can be - properly unloaded during tearDown""" - - import sys - self.preexistingModules = set(sys.modules.keys()) - - for m in a: - sys.modules[m] = Mock() - self.injectedModules[m] = sys.modules[m] - - def modifiedModule(self, mname, mod = None): - """Mark module (and all it's parents) as tainted""" - - oldname="" - for m in mname.split("."): - self.injectedModules[oldname+m] = mod - oldname += m + "." - self.injectedModules[mname] = mod - - def tearDownModules(self): - """Unload previously Mocked modules""" - - import sys - - for m in sys.modules.keys(): - if m in self.preexistingModules and not m in self.injectedModules: - continue - - del sys.modules[m] - - def take_over_io(self, disk, target_module): - """ Trick target_module into using disk object as the filesystem. - - This is achieved by overriding the module's 'open' binding as well - as many global bindings in os.path. - """ - target_module.open = disk.open - self.modifiedModule(target_module.__name__) - - import glob - self.modifiedModule("glob") - glob.glob = disk.glob_glob - - import os - self.modifiedModule("os.path") - # this is what os.path implementaion points at, reimport: - import posixpath - self.modifiedModule("posixpath") - os.listdir = disk.os_listdir - os.path.exists = disk.os_path_exists - os.path.isdir = disk.os_path_isdir - os.access = disk.os_access \ No newline at end of file diff --git a/anaconda/old_tests/mock/disk.py b/anaconda/old_tests/mock/disk.py deleted file mode 100644 index 926fe8f..0000000 --- a/anaconda/old_tests/mock/disk.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2010 -# Red Hat, Inc. All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author(s): Martin Sivak <msivak@redhat.com> - -from StringIO import StringIO -import fnmatch -import os - -class DiskIO(object): - """Simple object to simplify mocking of file operations in Mock - based testing""" - - class TestFile(StringIO): - def __init__(self, store, path, content = ""): - StringIO.__init__(self, content) - self._store = store - self._path = path - self._ro = False - - def flush(self): - self._store[self._path] = self.getvalue() - - def close(self): - self.flush() - return StringIO.close(self) - - def __del__(self): - try: - self.close() - except (AttributeError, ValueError): - pass - - def __enter__(self): - return self - - def __exit__(self, *_): - self.close() - - class Dir(object): - pass - - class Link(object): - pass - - def __init__(self): - self.reset() - - def __getitem__(self, key): - return self.fs[key] - - def __setitem__(self, key, value): - self.fs[key] = value - - def reset(self): - self.fs = { - "/proc": self.Dir, - "/proc/cmdline": "linux", - } - self._pwd = "/" - - #Emulate file objects - def open(self, filename, mode = "r"): - path = os.path.join(self._pwd, filename) - content = self.fs.get(path, None) - if content == self.Dir: - raise IOError("[Errno 21] Is a directory: '%s'" % (path)) - elif mode.startswith("w"): - self.fs[path] = "" - f = self.TestFile(self.fs, path, self.fs[path]) - elif mode.endswith("a"): - if not path in self.fs: - self.fs[path] = "" - f = self.TestFile(self.fs, path, self.fs[path]) - f.seek(0, os.SEEK_END) - elif content == None: - raise IOError("[Errno 2] No such file or directory: '%s'" % (path,)) - elif mode.endswith("+"): - f = self.TestFile(self.fs, path, content) - if mode.startswith('r'): - f.seek(0, os.SEEK_SET) - else: - f.seek(0, os.SEEK_END) - else: - f = self.TestFile(self.fs, path, content) - - return f - - #Emulate os calls - def glob_glob(self, pattern): - return fnmatch.filter(self.fs.keys(), pattern) - - def os_listdir(self, path): - return [entry[len(path):].lstrip('/') for entry in self.fs.keys()\ - if entry.startswith(path) and entry != path] - - def os_path_exists(self, path): - path = os.path.join(self._pwd, path) - return self.fs.has_key(path) - - def os_path_isdir(self, path): - if not path.endswith("/"): - path += "/" - path += "*" - return len(fnmatch.filter(self.fs.keys(), path)) > 0 - - def os_remove(self, path): - path = os.path.join(self._pwd, path) - try: - del self.fs[path] - except KeyError: - raise OSError("[Errno 2] No such file or directory: '%s'" % (path,)) - - def os_access(self, path, mode): - return self.os_path_exists(path) diff --git a/anaconda/old_tests/mock/mock.py b/anaconda/old_tests/mock/mock.py deleted file mode 100644 index 9053e81..0000000 --- a/anaconda/old_tests/mock/mock.py +++ /dev/null @@ -1,271 +0,0 @@ -# mock.py -# Test tools for mocking and patching. -# Copyright (C) 2007-2009 Michael Foord -# E-mail: fuzzyman AT voidspace DOT org DOT uk - -# mock 0.6.0 -# http://www.voidspace.org.uk/python/mock/ - -# Released subject to the BSD License -# Please see http://www.voidspace.org.uk/python/license.shtml - -# Scripts maintained at http://www.voidspace.org.uk/python/index.shtml -# Comments, suggestions and bug reports welcome. - - -__all__ = ( - 'Mock', - 'patch', - 'patch_object', - 'sentinel', - 'DEFAULT' -) - -__version__ = '0.6.0' - -class SentinelObject(object): - def __init__(self, name): - self.name = name - - def __repr__(self): - return '<SentinelObject "%s">' % self.name - - -class Sentinel(object): - def __init__(self): - self._sentinels = {} - - def __getattr__(self, name): - return self._sentinels.setdefault(name, SentinelObject(name)) - - -sentinel = Sentinel() - -DEFAULT = sentinel.DEFAULT - -class OldStyleClass: - pass -ClassType = type(OldStyleClass) - -def _is_magic(name): - return '__%s__' % name[2:-2] == name - -def _copy(value): - if type(value) in (dict, list, tuple, set): - return type(value)(value) - return value - - -class Mock(object): - - def __init__(self, spec=None, side_effect=None, return_value=DEFAULT, - name=None, parent=None, wraps=None): - self._parent = parent - self._name = name - if spec is not None and not isinstance(spec, list): - spec = [member for member in dir(spec) if not _is_magic(member)] - - self._methods = spec - self._children = {} - self._return_value = return_value - self.side_effect = side_effect - self._wraps = wraps - - self.reset_mock() - - - def reset_mock(self): - self.called = False - self.call_args = None - self.call_count = 0 - self.call_args_list = [] - self.method_calls = [] - for child in self._children.itervalues(): - child.reset_mock() - if isinstance(self._return_value, Mock): - self._return_value.reset_mock() - - - def __get_return_value(self): - if self._return_value is DEFAULT: - self._return_value = Mock() - return self._return_value - - def __set_return_value(self, value): - self._return_value = value - - return_value = property(__get_return_value, __set_return_value) - - - def __call__(self, *args, **kwargs): - self.called = True - self.call_count += 1 - self.call_args = (args, kwargs) - self.call_args_list.append((args, kwargs)) - - parent = self._parent - name = self._name - while parent is not None: - parent.method_calls.append((name, args, kwargs)) - if parent._parent is None: - break - name = parent._name + '.' + name - parent = parent._parent - - ret_val = DEFAULT - if self.side_effect is not None: - if (isinstance(self.side_effect, Exception) or - isinstance(self.side_effect, (type, ClassType)) and - issubclass(self.side_effect, Exception)): - raise self.side_effect - - ret_val = self.side_effect(*args, **kwargs) - if ret_val is DEFAULT: - ret_val = self.return_value - - if self._wraps is not None and self._return_value is DEFAULT: - return self._wraps(*args, **kwargs) - if ret_val is DEFAULT: - ret_val = self.return_value - return ret_val - - - def __getattr__(self, name): - if self._methods is not None: - if name not in self._methods: - raise AttributeError("Mock object has no attribute '%s'" % name) - elif _is_magic(name): - raise AttributeError(name) - - if name not in self._children: - wraps = None - if self._wraps is not None: - wraps = getattr(self._wraps, name) - self._children[name] = Mock(parent=self, name=name, wraps=wraps) - - return self._children[name] - - - def assert_called_with(self, *args, **kwargs): - assert self.call_args == (args, kwargs), 'Expected: %s\nCalled with: %s' % ((args, kwargs), self.call_args) - - -def _dot_lookup(thing, comp, import_path): - try: - return getattr(thing, comp) - except AttributeError: - __import__(import_path) - return getattr(thing, comp) - - -def _importer(target): - components = target.split('.') - import_path = components.pop(0) - thing = __import__(import_path) - - for comp in components: - import_path += ".%s" % comp - thing = _dot_lookup(thing, comp, import_path) - return thing - - -class _patch(object): - def __init__(self, target, attribute, new, spec, create): - self.target = target - self.attribute = attribute - self.new = new - self.spec = spec - self.create = create - self.has_local = False - - - def __call__(self, func): - if hasattr(func, 'patchings'): - func.patchings.append(self) - return func - - def patched(*args, **keywargs): - # don't use a with here (backwards compatability with 2.5) - extra_args = [] - for patching in patched.patchings: - arg = patching.__enter__() - if patching.new is DEFAULT: - extra_args.append(arg) - args += tuple(extra_args) - try: - return func(*args, **keywargs) - finally: - for patching in getattr(patched, 'patchings', []): - patching.__exit__() - - patched.patchings = [self] - patched.__name__ = func.__name__ - patched.compat_co_firstlineno = getattr(func, "compat_co_firstlineno", - func.func_code.co_firstlineno) - return patched - - - def get_original(self): - target = self.target - name = self.attribute - create = self.create - - original = DEFAULT - if _has_local_attr(target, name): - try: - original = target.__dict__[name] - except AttributeError: - # for instances of classes with slots, they have no __dict__ - original = getattr(target, name) - elif not create and not hasattr(target, name): - raise AttributeError("%s does not have the attribute %r" % (target, name)) - return original - - - def __enter__(self): - new, spec, = self.new, self.spec - original = self.get_original() - if new is DEFAULT: - # XXXX what if original is DEFAULT - shouldn't use it as a spec - inherit = False - if spec == True: - # set spec to the object we are replacing - spec = original - if isinstance(spec, (type, ClassType)): - inherit = True - new = Mock(spec=spec) - if inherit: - new.return_value = Mock(spec=spec) - self.temp_original = original - setattr(self.target, self.attribute, new) - return new - - - def __exit__(self, *_): - if self.temp_original is not DEFAULT: - setattr(self.target, self.attribute, self.temp_original) - else: - delattr(self.target, self.attribute) - del self.temp_original - - -def patch_object(target, attribute, new=DEFAULT, spec=None, create=False): - return _patch(target, attribute, new, spec, create) - - -def patch(target, new=DEFAULT, spec=None, create=False): - try: - target, attribute = target.rsplit('.', 1) - except (TypeError, ValueError): - raise TypeError("Need a valid target to patch. You supplied: %r" % (target,)) - target = _importer(target) - return _patch(target, attribute, new, spec, create) - - - -def _has_local_attr(obj, name): - try: - return name in vars(obj) - except TypeError: - # objects without a __dict__ - return hasattr(obj, name) diff --git a/anaconda/old_tests/pyanaconda_test/Makefile.am b/anaconda/old_tests/pyanaconda_test/Makefile.am deleted file mode 100644 index 889d867..0000000 --- a/anaconda/old_tests/pyanaconda_test/Makefile.am +++ /dev/null @@ -1,45 +0,0 @@ -# tests/pyanaconda_test/Makefile.am for anaconda -# -# Copyright (C) 2010 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author: Tomas Mlcoch <tmlcoch@redhat.com> - -EXTRA_DIST = *.py - -MAINTAINERCLEANFILES = Makefile.in - -ANACDIR = $(top_builddir)/pyanaconda -TESTS_ENVIRONMENT = PYTHONPATH=$(top_builddir)/tests:$(ANACDIR)/isys/.libs:$(ANACDIR):$(top_builddir) - -TESTS = backend_test.py \ - bootloader_test.py \ - cmdline_test.py \ - desktop_test.py \ - flags_test.py \ - image_test.py \ - language_test.py \ - network_test.py \ - packages_test.py \ - packaging_test.py \ - partintfhelpers_test.py \ - product_test.py \ - rescue_test.py \ - security_test.py \ - simpleconfig_test.py \ - timezone_test.py \ - upgrade_test.py \ - users_test.py \ - vnc_test.py diff --git a/anaconda/old_tests/pyanaconda_test/backend_test.py b/anaconda/old_tests/pyanaconda_test/backend_test.py deleted file mode 100644 index b09ee69..0000000 --- a/anaconda/old_tests/pyanaconda_test/backend_test.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/python - -import mock - -class BackendTest(mock.TestCase): - - def setUp(self): - import pykickstart.commands - - self.setupModules(["_isys", "block", 'parted', 'storage', - 'pyanaconda.storage.formats', 'logging', - 'logging.config', - 'ConfigParser', 'pyanaconda.anaconda_log', - 'pyanaconda.storage.storage_log', - 'pyanaconda.yuminstall']) - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - import logging - self.logger = mock.Mock() - logging.getLogger.return_value = self.logger - - self.DD_EXTRACTED = '/tmp/DD' - - import pyanaconda.backend - pyanaconda.backend.os = mock.Mock() - pyanaconda.backend.DD_EXTRACTED = self.DD_EXTRACTED - pyanaconda.backend.glob = mock.Mock() - pyanaconda.backend.shutil = mock.Mock() - - self.logger.reset_mock() - - def tearDown(self): - self.tearDownModules() - - def anaconda_backend_copy_firmware_test(self): - import pyanaconda.backend - FILE = 'foo' - pyanaconda.backend.glob.glob.return_value = [FILE] - - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.copyFirmware() - self.assertEqual(pyanaconda.backend.shutil.copyfile.call_args[0][0], FILE) - - def anaconda_backend_do_post_install_test(self): - import pyanaconda.backend - from pyanaconda.constants import ROOT_PATH - FILE = 'foo' - A = 'a' - B = 'b' - C = 'c' - pyanaconda.backend.AnacondaBackend.copyFirmware = mock.Mock() - pyanaconda.backend.AnacondaBackend.kernelVersionList = mock.Mock( - return_value=[(A, B, C)]) - pyanaconda.backend.packages = mock.Mock() - pyanaconda.backend.glob.glob.return_value = [FILE] - - pyanaconda.backend.os.path.exists.return_value=True - pyanaconda.backend.os.path.basename.return_value="" - - pyanaconda.backend.storage = mock.Mock() - pyanaconda.backend.sys = mock.Mock() - - anaconda = mock.Mock() - anaconda.extraModules = True - - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.doPostInstall(anaconda) - - self.assertEqual(pyanaconda.backend.packages.method_calls[0], - ('recreateInitrd', (A, ROOT_PATH), {})) - self.assertEqual(pyanaconda.backend.shutil.method_calls[0], - ('copytree', (FILE, ROOT_PATH + '/root/'), {})) - self.assertEqual(pyanaconda.backend.shutil.method_calls[1], - ('copytree', (self.DD_EXTRACTED, ROOT_PATH + '/root/DD'), {})) - self.assertTrue(pyanaconda.backend.storage.writeEscrowPackets.called) - - def anaconda_backend_do_install_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - self.assertRaises(NotImplementedError, ab.doInstall, anaconda) - - def anaconda_backend_kernel_version_list_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ret = ab.kernelVersionList() - self.assertEqual([], ret) - - def anaconda_backend_get_minimum_size_mb_test(self): - import pyanaconda.backend - PART = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ret = ab.getMinimumSizeMB(PART) - self.assertEqual(0, ret) - - def anaconda_backend_do_backend_setup_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.doBackendSetup(anaconda) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_group_exists_test(self): - import pyanaconda.backend - GROUP = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.groupExists(GROUP) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_select_group_test(self): - import pyanaconda.backend - GROUP = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.selectGroup(GROUP) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_deselect_group_test(self): - import pyanaconda.backend - GROUP = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.deselectGroup(GROUP) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_package_exists_test(self): - import pyanaconda.backend - PKG = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.packageExists(PKG) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_select_package_test(self): - import pyanaconda.backend - PKG = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.selectPackage(PKG) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_deselect_package_test(self): - import pyanaconda.backend - PKG = mock.Mock() - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.deselectPackage(PKG) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_get_default_groups_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.getDefaultGroups(anaconda) - self.assertTrue(self.logger.warning.called) - - def anaconda_backend_write_configuration_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - ab = pyanaconda.backend.AnacondaBackend(anaconda) - ab.writeConfiguration() - self.assertTrue(self.logger.warning.called) - - def do_backend_setup_1_test(self): - import pyanaconda.backend - RET = -1 - pyanaconda.backend.DISPATCH_BACK = RET - anaconda = mock.Mock() - anaconda.backend.doBackendSetup.return_value = RET - ret = pyanaconda.backend.doBackendSetup(anaconda) - self.assertEqual(RET, ret) - - def do_post_selection_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - pyanaconda.backend.doPostSelection(anaconda) - self.assertTrue(anaconda.backend.doPostSelection.called) - - def do_pre_install_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - pyanaconda.backend.doPreInstall(anaconda) - self.assertTrue(anaconda.backend.doPreInstall.called) - - def do_post_install_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - pyanaconda.backend.doPostInstall(anaconda) - self.assertTrue(anaconda.backend.doPostInstall.called) - - def do_install_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - pyanaconda.backend.doInstall(anaconda) - self.assertTrue(anaconda.backend.doInstall.called) - - def do_base_package_select_1_test(self): - import pyanaconda.backend - pyanaconda.backend.kickstart = mock.Mock() - anaconda = mock.Mock() - anaconda.ksdata = True - - pyanaconda.backend.doBasePackageSelect(anaconda) - self.assertTrue(anaconda.backend.resetPackageSelections.called) - - def do_base_package_select_2_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - anaconda.ksdata = False - - pyanaconda.backend.doBasePackageSelect(anaconda) - self.assertTrue(anaconda.backend.resetPackageSelections.called) - self.assertTrue(anaconda.instClass.setPackageSelection.called) - self.assertTrue(anaconda.instClass.setGroupSelection.called) - - def write_configuration_test(self): - import pyanaconda.backend - anaconda = mock.Mock() - pyanaconda.backend.writeConfiguration(anaconda) - self.assertTrue(anaconda.write.called) - self.assertTrue(anaconda.backend.writeConfiguration) diff --git a/anaconda/old_tests/pyanaconda_test/bootloader_test.py b/anaconda/old_tests/pyanaconda_test/bootloader_test.py deleted file mode 100644 index 6aa93bb..0000000 --- a/anaconda/old_tests/pyanaconda_test/bootloader_test.py +++ /dev/null @@ -1,59 +0,0 @@ -import mock - -class ArgumentsTest(mock.TestCase): - def setUp(self): - self.setupModules( - ['_isys', 'logging', 'pyanaconda.anaconda_log', 'block', - 'pyanaconda.storage', - 'pyanaconda.storage.devicelibs', - 'pyanaconda.storage.errors']) - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - def tearDown(self): - self.tearDownModules() - - def test_basic(self): - from pyanaconda.bootloader import Arguments - a = Arguments() - a.update(set(["a", "b", "c"])) - b = Arguments() - b.add("b") - diff = a - b - self.assertEqual(diff, set(["a", "c"])) - self.assertIsInstance(diff, Arguments) - assert(str(diff) in ["a c", "c a"]) - - def test_merge_ip(self): - from pyanaconda.bootloader import Arguments - # test that _merge_ip() doesnt break the simple case: - a = Arguments(["one", "two", "ip=eth0:dhcp"]) - a._merge_ip() - self.assertEqual(a, Arguments(["one", "two", "ip=eth0:dhcp"])) - - # test that it does what it's supposed to: - a = Arguments(["one", "two", "ip=eth0:dhcp", "ip=eth0:auto6", - "ip=wlan0:dhcp", - "ip=10.34.102.102::10.34.39.255:24:aklab:eth2:none"]) - a._merge_ip() - self.assertEqual(a, set([ - "one", "two", - "ip=wlan0:dhcp", - "ip=10.34.102.102::10.34.39.255:24:aklab:eth2:none", - "ip=eth0:auto6,dhcp"])) - - def test_output_with_merge(self): - from pyanaconda.bootloader import Arguments - a = Arguments(["ip=eth0:dhcp"]) - self.assertEqual(str(a), "ip=eth0:dhcp") - a = Arguments(["ip=eth0:dhcp", "ip=eth0:auto6"]) - assert(str(a) in ["ip=eth0:auto6,dhcp", "ip=eth0:dhcp,auto6"]) - - def test_sorting(self): - from pyanaconda.bootloader import Arguments - a = Arguments(["ip=eth0:dhcp", "rhgb", "quiet", - "root=/dev/mapper/destroyers-rubies", "rd.md=0", - "rd.luks=0"]) - # 'rhgb quiet' should be the final entries: - assert(str(a).endswith("rhgb quiet")) diff --git a/anaconda/old_tests/pyanaconda_test/desktop_test.py b/anaconda/old_tests/pyanaconda_test/desktop_test.py deleted file mode 100644 index 2078c9a..0000000 --- a/anaconda/old_tests/pyanaconda_test/desktop_test.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/python - -import mock -import sys - -class DesktopTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - self.fs = mock.DiskIO() - - self.fs.open('/mnt/sysimage/etc/inittab', 'w').write('id:5:initdefault:') - self.fs.open('/mnt/sysimage/etc/sysconfig/desktop', 'w').write('') - - import pyanaconda.desktop - pyanaconda.desktop.log = mock.Mock() - pyanaconda.desktop.open = self.fs.open - - def tearDown(self): - self.tearDownModules() - - def set_default_run_level_1_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - self.assertRaises(RuntimeError, dskt.setDefaultRunLevel, 1) - self.assertRaises(RuntimeError, dskt.setDefaultRunLevel, 2) - self.assertRaises(RuntimeError, dskt.setDefaultRunLevel, 4) - - def set_default_run_level_2_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultRunLevel(3) - self.assertEqual(dskt.runlevel, 3) - dskt.setDefaultRunLevel(5) - self.assertEqual(dskt.runlevel, 5) - - def get_default_run_level_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - self.assertEqual(dskt.getDefaultRunLevel(), dskt.runlevel) - - def set_get_default_run_level_1_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultRunLevel(3) - self.assertEqual(dskt.getDefaultRunLevel(), 3) - - def set_get_default_run_level_2_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultRunLevel(5) - self.assertEqual(dskt.getDefaultRunLevel(), 5) - - def set_default_desktop_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultDesktop('desktop') - self.assertEqual(dskt.info['DESKTOP'], 'desktop') - - def get_default_desktop_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.info['DESKTOP'] = 'foobar' - ret = dskt.getDefaultDesktop() - self.assertEqual(ret, 'foobar') - - def set_get_default_desktop_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultDesktop('foo') - ret = dskt.getDefaultDesktop() - self.assertEqual(ret, 'foo') - - def write_1_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.write() - self.assertEqual(self.fs['/mnt/sysimage/etc/inittab'], - 'id:3:initdefault:') - - def write_2_test(self): - import pyanaconda.desktop - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultRunLevel(5) - dskt.write() - self.assertEqual(self.fs['/mnt/sysimage/etc/inittab'], - 'id:5:initdefault:') - - def write_3_test(self): - import pyanaconda.desktop - pyanaconda.desktop.os = mock.Mock() - pyanaconda.desktop.os.path.isdir.return_value = True - dskt = pyanaconda.desktop.Desktop() - dskt.setDefaultDesktop('foo') - dskt.write() - self.assertEqual(self.fs['/mnt/sysimage/etc/inittab'], - 'id:3:initdefault:') - self.assertEqual(self.fs['/mnt/sysimage/etc/sysconfig/desktop'], - 'DESKTOP="foo"\n') - diff --git a/anaconda/old_tests/pyanaconda_test/flags_test.py b/anaconda/old_tests/pyanaconda_test/flags_test.py deleted file mode 100644 index e4046d5..0000000 --- a/anaconda/old_tests/pyanaconda_test/flags_test.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/python - -# Test Bug 500198 - -import mock -import sys - - -class FlagsTest(mock.TestCase): - """Simulate /proc/cmdline parameters parsing (#500198)""" - - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - - self.fs = mock.DiskIO() - - import pyanaconda.flags - - self.mock2 = mock.Mock() - pyanaconda.flags.open = mock.Mock(return_value=self.mock2) - - def tearDown(self): - self.tearDownModules() - - def createcmdlinedict_1_test(self): - """/proc/cmdline without BOOT_IMAGE param""" - import pyanaconda.flags - - self.cmd = 'vmlinuz initrd=initrd.img stage2=hd:LABEL="Fedora" xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - cmddict = pyanaconda.flags.flags.createCmdlineDict() - - self.assertEqual(set(cmddict.keys()), - set(['vmlinuz', 'initrd', 'stage2', 'xdriver', 'nomodeset'])) - - def createcmdlinedict_2_test(self): - """/proc/cmdline param: quotes at end""" - import pyanaconda.flags - - self.cmd = 'vmlinuz BOOT_IMAGE=/boot/img initrd=initrd.img stage2=hd:LABEL="Fedora"' - self.mock2.read = mock.Mock(return_value=self.cmd) - try: - cmddict = pyanaconda.flags.flags.createCmdlineDict() - except (ValueError): - self.assertTrue(False, "ValueError exception was raised.") - - self.assertEqual(set(cmddict.keys()), - set(['vmlinuz', 'BOOT_IMAGE', 'initrd', 'stage2'])) - - def createcmdlinedict_3_test(self): - """/proc/cmdline param BOOT_IMAGE with quotes (no quotes at end)""" - import pyanaconda.flags - - self.cmd = 'vmlinuz BOOT_IMAGE="img img" initrd=initrd.img' - self.mock2.read = mock.Mock(return_value=self.cmd) - cmddict = pyanaconda.flags.flags.createCmdlineDict() - - self.assertEqual(set(cmddict.keys()), - set(['vmlinuz', 'BOOT_IMAGE', 'initrd'])) - - def createcmdlinedict_4_test(self): - """/proc/cmdline param BOOT_IMAGE with quotes (no quotes at end) v2""" - import pyanaconda.flags - - self.cmd = 'vmlinuz BOOT_IMAGE="/boot/img" stage2=hd:LABEL="Fedora" initrd=initrd.img' - self.mock2.read = mock.Mock(return_value=self.cmd) - cmddict = pyanaconda.flags.flags.createCmdlineDict() - - self.assertEqual(set(cmddict.keys()), - set(['vmlinuz', 'BOOT_IMAGE', 'initrd', 'stage2'])) - - def createcmdlinedict_5_test(self): - """/proc/cmdline param: BOOT_IMAGE with quotes (+ quotes at end)""" - import pyanaconda.flags - - self.cmd = 'vmlinuz BOOT_IMAGE="/boot/img img" initrd=initrd.img stage2=hd:LABEL="Fedora"' - self.mock2.read = mock.Mock(return_value=self.cmd) - try: - cmddict = pyanaconda.flags.flags.createCmdlineDict() - except (ValueError): - self.assertTrue(False, "ValueError exception was raised.") - - self.assertEqual(set(cmddict.keys()), - set(['vmlinuz', 'BOOT_IMAGE', 'initrd', 'stage2'])) - - def setattr_getattr_1_test(self): - import pyanaconda.flags - RET = 1 - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - pyanaconda.flags.flags.sshd = RET - self.assertEqual(RET, pyanaconda.flags.flags.sshd) - - def setattr_getattr_2_test(self): - import pyanaconda.flags - RET = 0 - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - pyanaconda.flags.flags.sshd = RET - self.assertEqual(RET, pyanaconda.flags.flags.sshd) - - def setattr_getattr_3_test(self): - import pyanaconda.flags - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - - def f(): return pyanaconda.flags.flags.fooattr - self.assertRaises(AttributeError, f) - - def setattr_getattr_4_test(self): - import pyanaconda.flags - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - - def f(): pyanaconda.flags.flags.fooattr = 1 - self.assertRaises(AttributeError, f) - - def get_1_test(self): - import pyanaconda.flags - RET = 'text' - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - ret = pyanaconda.flags.flags.get('foobar', RET) - self.assertEqual(RET, ret) - - def get_2_test(self): - import pyanaconda.flags - RET = 'text' - self.cmd = 'vmlinuz initrd=initrd.img xdriver=vesa nomodeset' - self.mock2.read = mock.Mock(return_value=self.cmd) - ret = pyanaconda.flags.flags.get('sshd', RET) - self.assertNotEqual(RET, ret) diff --git a/anaconda/old_tests/pyanaconda_test/image_test.py b/anaconda/old_tests/pyanaconda_test/image_test.py deleted file mode 100644 index f3faeac..0000000 --- a/anaconda/old_tests/pyanaconda_test/image_test.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python - -import mock -import sys - -IMAGENAME = 'Fedora-13-i386-DVD.iso' - -class ImageTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - self.fs = mock.DiskIO() - - DISCINFO = "1273712438.740122\n" - DISCINFO += "Fedora 13\n" - DISCINFO += "i386\n" - DISCINFO += "ALL\n" - - DISCINFO2 = "1273712438.740122\n" - DISCINFO2 += "Fedora 13\n" - DISCINFO2 += "i386\n" - DISCINFO2 += "1,2\n" - - self.fs.open('/mnt/install/cdimage/.discinfo', 'w').write(DISCINFO) - self.fs.open('/tmp/.discinfo', 'w').write(DISCINFO2) - - import pyanaconda.image - pyanaconda.image.gettext = mock.Mock() - pyanaconda.image.log = mock.Mock() - pyanaconda.image.open = self.fs.open - pyanaconda.image.isys = mock.Mock() - pyanaconda.image._arch = 'i386' - pyanaconda.image.stat = mock.Mock() - pyanaconda.image.stat.ST_SIZE = 0 - - pyanaconda.image.os = mock.Mock() - pyanaconda.image.os.R_OK = 0 - pyanaconda.image.os.stat.return_value = [2048] - pyanaconda.image.os.listdir.return_value=[IMAGENAME] - - def tearDown(self): - self.tearDownModules() - - def get_media_id_1_test(self): - import pyanaconda.image - ret = pyanaconda.image.getMediaId('/mnt/install/cdimage') - self.assertEqual(ret, '1273712438.740122') - - def get_media_id_2_test(self): - import pyanaconda.image - pyanaconda.image.os.access = mock.Mock(return_value=False) - ret = pyanaconda.image.getMediaId('/') - self.assertEqual(ret, None) - - def mount_directory_1_test(self): - import pyanaconda.image - pyanaconda.image.os.path.ismount = mock.Mock(return_value=False) - pyanaconda.image.mountDirectory('hd:/dev/sda1:/', mock.Mock()) - self.assertEqual(pyanaconda.image.isys.method_calls, - [('mount', ('/dev/sda1', '/mnt/install/isodir'), {'fstype': 'auto', 'options':''})]) - - def mount_directory_2_test(self): - import pyanaconda.image - pyanaconda.image.os.path.ismount = mock.Mock(return_value=False) - pyanaconda.image.mountDirectory('hd:sda1:/', mock.Mock()) - self.assertEqual(pyanaconda.image.isys.method_calls, - [('mount', ('/dev/sda1', '/mnt/install/isodir'), {'fstype': 'auto', 'options':''})]) - - def mount_directory_3_test(self): - import pyanaconda.image - pyanaconda.image.os.path.ismount = mock.Mock(return_value=True) - pyanaconda.image.mountDirectory('hd:sda1:/', mock.Mock()) - self.assertEqual(pyanaconda.image.isys.method_calls, []) - - def mount_image_1_test(self): - import pyanaconda.image - self.assertRaises(SystemError, pyanaconda.image.mountImage, '', '/mnt/install/cdimage', mock.Mock()) - - def mount_image_2_test(self): - import pyanaconda.image - pyanaconda.image.os.path.ismount = mock.Mock(return_value=False) - ret = pyanaconda.image.mountImage('', '/mnt/install/cdimage', mock.Mock()) - - self.assertEqual(pyanaconda.image.isys.method_calls, - [('isIsoImage', ('/Fedora-13-i386-DVD.iso',), {}), - ('mount', ('/Fedora-13-i386-DVD.iso', '/mnt/install/cdimage'), - {'readOnly': True, 'fstype': 'iso9660'}), - ('umount', ('/mnt/install/cdimage',), {'removeDir': False}), - ('mount', ('/Fedora-13-i386-DVD.iso', '/mnt/install/cdimage'), - {'readOnly': True, 'fstype': 'iso9660'}), - ]) - - def scan_for_media_1_test(self): - import pyanaconda.image - storage = mock.Mock() - storage.devicetree.devices = [] - ret = pyanaconda.image.scanForMedia(mock.Mock(), storage) - self.assertEqual(ret, None) - - def scan_for_media_2_test(self): - import pyanaconda.image - device = mock.Mock() - device.type = 'cdrom' - device.name = 'deviceName' - storage = mock.Mock() - storage.devicetree.devices = [device] - ret = pyanaconda.image.scanForMedia('/tmp', storage) - self.assertEqual(ret, 'deviceName') - self.assertEqual(device.method_calls, - [('format.mount', (), {'mountpoint': '/tmp'})]) - - def umount_image_1_test(self): - import pyanaconda.image - pyanaconda.image.umountImage('/tmp') - self.assertEqual(pyanaconda.image.isys.method_calls, - [('umount', ('/tmp',), {'removeDir': False})]) - - def unmount_cd_1_test(self): - import pyanaconda.image - window = mock.Mock() - pyanaconda.image.unmountCD(None, window) - self.assertEqual(window.method_calls, []) - - def unmount_cd_2_test(self): - import pyanaconda.image - window = mock.Mock() - device = mock.Mock() - pyanaconda.image.unmountCD(device, window) - self.assertEqual(window.method_calls, []) - self.assertEqual(device.method_calls, - [('format.unmount', (), {})]) - - def verify_media_1_test(self): - import pyanaconda.image - ret = pyanaconda.image.verifyMedia('/tmp') - self.assertTrue(ret) - - def verify_media_2_test(self): - import pyanaconda.image - ret = pyanaconda.image.verifyMedia('/tmp', timestamp=1337) - self.assertFalse(ret) - - def verify_media_3_test(self): - import pyanaconda.image - pyanaconda.image._arch = 'x86_64' - ret = pyanaconda.image.verifyMedia('/tmp', 1) - self.assertFalse(ret) diff --git a/anaconda/old_tests/pyanaconda_test/indexed_dict_test.py b/anaconda/old_tests/pyanaconda_test/indexed_dict_test.py deleted file mode 100644 index 14185b6..0000000 --- a/anaconda/old_tests/pyanaconda_test/indexed_dict_test.py +++ /dev/null @@ -1,49 +0,0 @@ -import mock - -class IndexedDictTest(mock.TestCase): - def setUp(self): - self.setupModules(['_isys']) - - def tearDown(self): - self.tearDownModules() - - def instantiation_test(self): - from pyanaconda.indexed_dict import IndexedDict - d = IndexedDict() - self.assertIsInstance(d, IndexedDict) - - def append_test(self): - from pyanaconda.indexed_dict import IndexedDict - d = IndexedDict() - stored_data = [1, 2, 3] - d["some_step"] = stored_data - self.assertIs(d["some_step"], stored_data) - - def cant_append_test(self): - from pyanaconda.indexed_dict import IndexedDict - def assign_int(indexed_dict): - indexed_dict[3] = [1, 2, 3] - d = IndexedDict() - self.assertRaises(TypeError, d.__setitem__, 3, [1, 2, 3]) - - def referencing_test(self): - from pyanaconda.indexed_dict import IndexedDict - d = IndexedDict() - d["first"] = 10 - d["second"] = 20 - d["third"] = 30 - self.assertEqual(d[0], 10) - self.assertEqual(d[1], 20) - self.assertEqual(d[2], 30) - self.assertRaises(IndexError, d.__getitem__, 3) - - def index_test(self): - from pyanaconda.indexed_dict import IndexedDict - d = IndexedDict() - d["first"] = 10 - d["second"] = 20 - d["third"] = 30 - - self.assertEqual(d.index("first"), 0) - self.assertEqual(d.index("second"), 1) - self.assertEqual(d.index("third"), 2) diff --git a/anaconda/old_tests/pyanaconda_test/iutil_test.py b/anaconda/old_tests/pyanaconda_test/iutil_test.py deleted file mode 100644 index 2f143ec..0000000 --- a/anaconda/old_tests/pyanaconda_test/iutil_test.py +++ /dev/null @@ -1,37 +0,0 @@ -import mock -import sys - -class IutilTest(mock.TestCase): - def setUp(self): - self.setupModules( - ['_isys', 'logging', 'pyanaconda.anaconda_log', 'block']) - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - def tearDown(self): - self.tearDownModules() - - def copy_to_sysimage_test(self): - from pyanaconda import iutil - fs = mock.DiskIO() - self.take_over_io(fs, iutil) - self.assertEqual(iutil.copy_to_sysimage("/etc/securetty"), False) - - fs["/etc/securetty"] = "tty1" - iutil.os.makedirs = mock.Mock() - iutil.shutil.copy = mock.Mock() - self.assertEqual(iutil.copy_to_sysimage("/etc/securetty"), True) - iutil.os.makedirs.assert_called_with("/mnt/sysimage/etc") - iutil.shutil.copy.assert_called_with("/etc/securetty", - "/mnt/sysimage/etc/securetty") - - def testExecCaptureNonZeroFatal (self): - import iutil - try: - argv = ["-c", "import sys; sys.exit(3);"] - iutil.execWithCapture(sys.executable, argv, root=None, fatal=True) - except RuntimeError, ex: - self.assertIn("return code: 3", str(ex)) - else: - self.fail("RuntimeError not raised") diff --git a/anaconda/old_tests/pyanaconda_test/network_test.py b/anaconda/old_tests/pyanaconda_test/network_test.py deleted file mode 100644 index 756c11d..0000000 --- a/anaconda/old_tests/pyanaconda_test/network_test.py +++ /dev/null @@ -1,389 +0,0 @@ -#!/usr/bin/python - -import mock -import os - -class NetworkTest(mock.TestCase): - - def setUp(self): - self.setupModules(['_isys', 'block', 'logging', 'ConfigParser']) - self.fs = mock.DiskIO() - - self.OK = 22 - self.SYSCONFIGDIR = "/tmp/etc/sysconfig" - self.NETSCRIPTSDIR = "%s/network-scripts" % (self.SYSCONFIGDIR) - self.NETWORKCONFFILE = '%s/network' % self.SYSCONFIGDIR - self.IFCFGLOG = '/tmp/ifcfg.log' - self.DEFAULT_HOSTNAME = 'localhost.localdomain' - - self.CONT = "DEVICE=eth0\nHWADDR=00:11:22:50:55:50\nTYPE=Ethernet\nBOOTPROTO=dhcp\n" - self.DEVICE = 'eth0' - self.DEV_FILE = self.NETSCRIPTSDIR + '/ifcfg-eth0' - self.DEV_KEY_FILE = self.NETSCRIPTSDIR + '/keys-eth0' - self.fs.open(self.DEV_FILE, 'w').write(self.CONT) - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - import pyanaconda.network - pyanaconda.network.socket = mock.Mock() - pyanaconda.network.socket.gethostname.return_value = self.DEFAULT_HOSTNAME - pyanaconda.network.open = self.fs.open - pyanaconda.simpleconfig.open = self.fs.open - pyanaconda.network.sysconfigDir = self.SYSCONFIGDIR - pyanaconda.network.netscriptsDir = self.NETSCRIPTSDIR - pyanaconda.network.networkConfFile = self.NETWORKCONFFILE - pyanaconda.network.ifcfgLogFile = self.IFCFGLOG - self.fs.open(self.IFCFGLOG, 'w') - - # Network mock - pyanaconda.network.Network.update = mock.Mock() - self.setNMControlledDevices_backup = pyanaconda.network.Network.setNMControlledDevices - pyanaconda.network.Network.setNMControlledDevices = mock.Mock() - pyanaconda.network.Network.netdevices = {} - - def tearDown(self): - self.tearDownModules() - - def sanity_check_hostname_1_test(self): - import pyanaconda.network - (valid, err) = pyanaconda.network.sanityCheckHostname('desktop') - self.assertTrue(valid) - - def sanity_check_hostname_2_test(self): - import pyanaconda.network - (valid, err) = pyanaconda.network.sanityCheckHostname('') - self.assertFalse(valid) - - def sanity_check_hostname_3_test(self): - import pyanaconda.network - (valid, err) = pyanaconda.network.sanityCheckHostname('c'*256) - self.assertFalse(valid) - - def sanity_check_hostname_4_test(self): - import pyanaconda.network - (valid, err) = pyanaconda.network.sanityCheckHostname('_asf') - self.assertFalse(valid) - - def sanity_check_hostname_5_test(self): - import pyanaconda.network - (valid, err) = pyanaconda.network.sanityCheckHostname('a?f') - self.assertFalse(valid) - - def get_default_hostname_1_test(self): - import pyanaconda.network - - HOSTNAME = 'host1' - pyanaconda.network.getActiveNetDevs = mock.Mock(return_value=['dev']) - pyanaconda.network.isys = mock.Mock() - pyanaconda.network.isys.getIPAddresses.return_value = ['10.0.0.1'] - pyanaconda.network.socket = mock.Mock() - pyanaconda.network.socket.gethostbyaddr.return_value = [HOSTNAME, '', ''] - - ret = pyanaconda.network.getDefaultHostname(mock.Mock()) - self.assertEqual(ret, HOSTNAME) - - def get_default_hostname_2_test(self): - import pyanaconda.network - - HOSTNAME = 'host2' - pyanaconda.network.getActiveNetDevs = mock.Mock(return_value=[]) - pyanaconda.network.isys = mock.Mock() - pyanaconda.network.socket = mock.Mock() - anaconda = mock.Mock() - anaconda.network.hostname = HOSTNAME - - ret = pyanaconda.network.getDefaultHostname(anaconda) - self.assertEqual(ret, HOSTNAME) - - def get_default_hostname_3_test(self): - import pyanaconda.network - - HOSTNAME = 'host3' - pyanaconda.network.getActiveNetDevs = mock.Mock(return_value=[]) - pyanaconda.network.isys = mock.Mock() - pyanaconda.network.socket = mock.Mock() - pyanaconda.network.socket.gethostname.return_value = HOSTNAME - anaconda = mock.Mock() - anaconda.network.hostname = '' - - ret = pyanaconda.network.getDefaultHostname(anaconda) - self.assertEqual(ret, HOSTNAME) - - def get_default_hostname_4_test(self): - import pyanaconda.network - - pyanaconda.network.getActiveNetDevs = mock.Mock(return_value=[]) - pyanaconda.network.isys = mock.Mock() - pyanaconda.network.socket = mock.Mock() - pyanaconda.network.socket.gethostname.return_value = '' - anaconda = mock.Mock() - anaconda.network.hostname = '' - - ret = pyanaconda.network.getDefaultHostname(anaconda) - self.assertEqual(ret, self.DEFAULT_HOSTNAME) - - def sanity_check_ip_string_1_test(self): - import pyanaconda.network - - IPADDR = '10.0.0.5' - pyanaconda.network.sanityCheckIPString(IPADDR) - - def sanity_check_ip_string_2_test(self): - import pyanaconda.network - - IPADDR = "ff06:0:0:0:0:0:0:c3" - pyanaconda.network.sanityCheckIPString(IPADDR) - - def sanity_check_ip_string_3_test(self): - import pyanaconda.network - - IPADDR = "ff06:.:.:0:0:0:0:c3" - self.assertRaises(pyanaconda.network.IPError, - pyanaconda.network.sanityCheckIPString, IPADDR) - - def sanity_check_ip_string_4_test(self): - import pyanaconda.network - import socket - pyanaconda.network.socket.error = socket.error - pyanaconda.network.socket.inet_pton = mock.Mock(side_effect=socket.error) - - IPADDR = "1.8.64.512" - self.assertRaises(pyanaconda.network.IPError, - pyanaconda.network.sanityCheckIPString, IPADDR) - - def sanity_check_ip_string_5_test(self): - import pyanaconda.network - import socket - pyanaconda.network.socket.error = socket.error - pyanaconda.network.socket.inet_pton = mock.Mock(side_effect=socket.error) - - IPADDR = "top.secret.address" - self.assertRaises(pyanaconda.network.IPError, - pyanaconda.network.sanityCheckIPString, IPADDR) - - def has_active_net_dev_1_test(self): - import pyanaconda.network - - pyanaconda.network.dbus = mock.Mock() - pyanaconda.network.dbus.Interface().Get.return_value = \ - pyanaconda.network.isys.NM_STATE_CONNECTED_GLOBAL - - ret = pyanaconda.network.hasActiveNetDev() - self.assertTrue(ret) - self.assertTrue(pyanaconda.network.dbus.Interface().Get.called) - - def has_active_net_dev_2_test(self): - import pyanaconda.network - - pyanaconda.network.dbus = mock.Mock(side_effect=Exception) - - ret = pyanaconda.network.hasActiveNetDev() - self.assertFalse(ret) - - def has_active_net_dev_3_test(self): - import pyanaconda.network - - pyanaconda.network.dbus = mock.Mock() - pyanaconda.network.dbus.Interface().Get.return_value = self.OK - pyanaconda.network.isys.NM_STATE_CONNECTED = (self.OK - 5) - - ret = pyanaconda.network.hasActiveNetDev() - self.assertFalse(ret) - - def networkdevice_read_test(self): - import pyanaconda.network - - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - ret = nd.read() - self.assertEqual(ret, 4) - self.assertEqual(nd.info, - {'DEVICE': 'eth0', 'HWADDR': '00:11:22:50:55:50', - 'BOOTPROTO': 'dhcp', 'TYPE': 'Ethernet'}) - - def networkdevice_clear_test(self): - import pyanaconda.network - - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.info = {'DEVICE': 'eth0', 'HWADDR': '00:11:22:50:55:50', 'TYPE': 'Ethernet'} - nd.clear() - self.assertEqual(nd.info, {}) - - def networkdevice_str_test(self): - import pyanaconda.network - pyanaconda.network.arch = mock.Mock() - pyanaconda.network.arch.isS390.return_value = False - - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.info = {'HWADDR': '00:11:22:50:55:50', 'DEVICE': 'eth0', 'TYPE': 'Ethernet'} - self.assertIn('DEVICE="eth0"', str(nd)) - self.assertIn('TYPE="Ethernet"', str(nd)) - - def networkdevice_load_ifcfg_file_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.loadIfcfgFile() - self.assertFalse(nd._dirty) - self.assertEqual(nd.info, - {'DEVICE': 'eth0', 'HWADDR': '00:11:22:50:55:50', - 'TYPE': 'Ethernet', 'BOOTPROTO': 'dhcp'}) - - def networkdevice_write_ifcfg_file_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.info = {'HWADDR': '66:55:44:33:22:11', 'DEVICE': 'eth1', 'TYPE': 'Ethernet'} - nd._dirty = True - nd.writeIfcfgFile() - self.assertIn('DEVICE="eth1"\n', self.fs[self.DEV_FILE]) - self.assertIn('HWADDR="66:55:44:33:22:11"', self.fs[self.DEV_FILE]) - self.assertIn('TYPE="Ethernet"', self.fs[self.DEV_FILE]) - - def networkdevice_set_1_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.set(('key', 'value')) - self.assertEqual(nd.info, {'KEY': 'value'}) - self.assertTrue(nd._dirty) - - def networkdevice_set_2_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.set(('key', 'value')) - nd.set(('key', 'other_value')) - self.assertEqual(nd.info, {'KEY': 'other_value'}) - self.assertTrue(nd._dirty) - - def networkdevice_set_3_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.set(('key', 'value')) - nd._dirty = False - nd.set(('key', 'other_value')) - self.assertEqual(nd.info, {'KEY': 'other_value'}) - self.assertTrue(nd._dirty) - - def networkdevice_set_gateway_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.setGateway('10.0.0.1') - self.assertEqual(nd.info, {'GATEWAY': '10.0.0.1'}) - self.assertTrue(nd._dirty) - - def networkdevice_set_gateway_ipv6_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.setGateway('fe80::5675:d0ff:feac:4d3f') - self.assertEqual(nd.info, {'IPV6_DEFAULTGW': 'fe80::5675:d0ff:feac:4d3f'}) - self.assertTrue(nd._dirty) - - def networkdevice_set_dns_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.setDNS('10.0.0.1, 10.0.0.2') - self.assertEqual(nd.info, {'DNS1': '10.0.0.1'}) - self.assertEqual(nd.info, {'DNS2': '10.0.0.2'}) - self.assertTrue(nd._dirty) - - def networkdevice_keyfile_path_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - ret = nd.keyfilePath - self.assertEqual(ret, self.DEV_KEY_FILE) - - def networkdevice_write_wepkey_file_1_test(self): - import pyanaconda.network - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.wepkey = False - ret = nd.writeWepkeyFile() - self.assertFalse(ret) - - def networkdevice_write_wepkey_file_2_test(self): - import pyanaconda.network - TMP_FILE = '/tmp/wep.key' - TMP_DIR = '/tmp/wepkeyfiles' - pyanaconda.network.tempfile = mock.Mock() - pyanaconda.network.tempfile.mkstemp.return_value = (88, TMP_FILE) - pyanaconda.network.os = mock.Mock() - pyanaconda.network.os.path = os.path - pyanaconda.network.shutil = mock.Mock() - - nd = pyanaconda.network.NetworkDevice(self.NETSCRIPTSDIR, self.DEVICE) - nd.iface = self.DEVICE - nd.wepkey = '12345' - - nd.writeWepkeyFile(dir=TMP_DIR) - self.assertEqual(pyanaconda.network.os.write.call_args[0], (88, 'KEY1=12345\n')) - self.assertEqual(pyanaconda.network.shutil.move.call_args[0], - (TMP_FILE, '%s/keys-%s' % (TMP_DIR, self.DEVICE))) - - def network_nm_controlled_devices_1_test(self): - import pyanaconda.network - nw = pyanaconda.network.Network() - nw.netdevices = {'dev': mock.Mock()} - pyanaconda.network.Network.setNMControlledDevices = self.setNMControlledDevices_backup - nw.setNMControlledDevices() - self.assertEqual(nw.netdevices['dev'].method_calls, - [('set', (('NM_CONTROLLED', 'yes'),), {})]) - - def network_nm_controlled_devices_2_test(self): - import pyanaconda.network - nw = pyanaconda.network.Network() - nw.netdevices = {'dev': mock.Mock()} - pyanaconda.network.Network.setNMControlledDevices = self.setNMControlledDevices_backup - nw.setNMControlledDevices(['']) - self.assertEqual(nw.netdevices['dev'].method_calls, - [('set', (('NM_CONTROLLED', 'no'),), {})]) - - def network_write_ks_test(self): - import pyanaconda.network - TMPFILE = '/tmp/networkKS' - f = self.fs.open(TMPFILE, 'w') - - nw = pyanaconda.network.Network() - nw.netdevices[self.DEVICE] = pyanaconda.network.NetworkDevice( - self.NETSCRIPTSDIR, self.DEVICE) - nw.netdevices[self.DEVICE].loadIfcfgFile() - nw.writeKS(f) - f.close() - - self.assertEqual(self.fs[TMPFILE], - 'network --device eth0 --bootproto dhcp --noipv6\n') - - def network_wait_for_connection_1_test(self): - import pyanaconda.network - pyanaconda.network.dbus = mock.Mock() - pyanaconda.network.dbus.Interface().Get.return_value = \ - pyanaconda.network.isys.NM_STATE_CONNECTED_GLOBAL - - ret = pyanaconda.network.waitForConnection() - self.assertTrue(ret) - - def network_wait_for_connection_2_test(self): - import pyanaconda.network - pyanaconda.network.dbus = mock.Mock() - pyanaconda.network.dbus.Interface().Get.return_value = self.OK-5 - pyanaconda.network.isys = mock.Mock() - pyanaconda.network.isys.NM_STATE_CONNECTED = self.OK - pyanaconda.network.time.sleep = mock.Mock() - - ret = pyanaconda.network.waitForConnection() - self.assertFalse(ret) - - def network_bring_up_test(self): - import pyanaconda.network - pyanaconda.network.Network.write = mock.Mock() - pyanaconda.network.waitForConnection = mock.Mock() - - nw = pyanaconda.network.Network() - nw.bringUp() - self.assertTrue(pyanaconda.network.Network.write.called) - self.assertTrue(pyanaconda.network.waitForConnection.called) - - def iface_for_host_ip_test(self): - import pyanaconda.network - pyanaconda.network.arch = mock.Mock() - pyanaconda.network.arch.execWithCapture.return_value = \ - "10.0.0.2 dev eth0 src 10.0.0.1" - - ret = pyanaconda.network.ifaceForHostIP('10.0.0.2') - self.assertEqual(ret, 'eth0') diff --git a/anaconda/old_tests/pyanaconda_test/packages_test.py b/anaconda/old_tests/pyanaconda_test/packages_test.py deleted file mode 100644 index 18de7ab..0000000 --- a/anaconda/old_tests/pyanaconda_test/packages_test.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/python - -import mock - -class PackagesTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "block", "logging", "parted", "storage", - "pyanaconda.storage.formats", "ConfigParser", - "pyanaconda.storage.storage_log"]) - self.fs = mock.DiskIO() - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - import pyanaconda.packages - - def tearDown(self): - self.tearDownModules() - - def do_post_action_test(self): - import pyanaconda.packages - anaconda = mock.Mock() - pyanaconda.packages.doPostAction(anaconda) - self.assertTrue(anaconda.instClass.postAction.called) diff --git a/anaconda/old_tests/pyanaconda_test/packaging_test.py b/anaconda/old_tests/pyanaconda_test/packaging_test.py deleted file mode 100644 index 99bc871..0000000 --- a/anaconda/old_tests/pyanaconda_test/packaging_test.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/python - -import mock - -class PackagingTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "logging"]) - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - from pykickstart.version import makeVersion - from pyanaconda.flags import flags - - # set some things specially since we're just testing - flags.testing = True - - # set up ksdata - self.ksdata = makeVersion() - - from pyanaconda.packaging import Payload - self.payload = Payload(self.ksdata) - - def tearDown(self): - self.tearDownModules() - #os.system("rm -rf %s" % self.root) - - def payload_abstract_test(self): - self.assertRaises(NotImplementedError, self.payload.setup, None) - self.assertRaises(NotImplementedError, self.payload.description, None) - - def payload_repo_test(self): - # ksdata repo list initially empty - self.assertEqual(self.payload.data.repo.dataList(), []) - - # create and add a new ksdata repo - repo_name = "test1" - repo = self.ksdata.RepoData(name=repo_name, baseurl="http://localhost/") - self.payload.addRepo(repo) - - # verify the repo was added - self.assertEqual(self.payload.data.repo.dataList(), [repo]) - self.assertEqual(self.payload.getAddOnRepo(repo_name), repo) - - # remove the repo - self.payload.removeRepo(repo_name) - - # verify the repo was removed - self.assertEqual(self.payload.getAddOnRepo(repo_name), None) - - def payload_group_test(self): - import pykickstart.constants - from pykickstart.parser import Group - - # verify that ksdata group lists are initially empty - self.assertEqual(self.payload.data.packages.groupList, []) - self.assertEqual(self.payload.data.packages.excludedGroupList, []) - - self.payload.deselectGroup("core") - self.assertEqual(self.payload.groupSelected("core"), False) - - # select a group and verify the selection is reflected afterward - self.payload.selectGroup("core", optional=True) - self.assertTrue(self.payload.groupSelected("core")) - - # verify the group is not in the excluded group list - self.assertTrue(Group("core") not in self.payload.data.packages.excludedGroupList) - - # verify the include (optional/all) is recorded - groups = self.payload.data.packages.groupList - group = groups[[g.name for g in groups].index("core")] - self.assertEqual(group.include, pykickstart.constants.GROUP_ALL) - - # select more groups - self.payload.selectGroup("base") - self.payload.selectGroup("development", default=False) - - # verify include types for newly selected groups - group = groups[[g.name for g in groups].index("development")] - self.assertEqual(group.include, pykickstart.constants.GROUP_REQUIRED) - - group = groups[[g.name for g in groups].index("base")] - self.assertEqual(group.include, pykickstart.constants.GROUP_DEFAULT) - - # deselect a group and verify the set of groups is correct afterward - self.payload.deselectGroup("base") - self.assertFalse(self.payload.groupSelected("base")) - self.assertTrue(self.payload.groupSelected("core")) - self.assertTrue(self.payload.groupSelected("development")) - - def payload_package_test(self): - # verify that ksdata package lists are initially empty - self.assertEqual(self.payload.data.packages.packageList, []) - self.assertEqual(self.payload.data.packages.excludedList, []) - - name = "vim-common" - - # deselect a package - self.payload.deselectPackage(name) - self.assertEqual(self.payload.packageSelected(name), False) - - # select the same package and verify it - self.payload.selectPackage(name) - self.assertEqual(self.payload.packageSelected(name), True) - self.assertTrue(name in self.payload.data.packages.packageList) - self.assertFalse(name in self.payload.data.packages.excludedList) - - # select some other packages - self.payload.selectPackage("bash") - self.payload.selectPackage("gnote") - - # deselect one of them and then verify the selection state of them all - self.payload.deselectPackage("bash") - self.assertFalse(self.payload.packageSelected("bash")) - self.assertTrue(self.payload.packageSelected("gnote")) - self.assertTrue(self.payload.packageSelected(name)) - - def payload_get_release_version_test(self): - # Given no URL, _getReleaseVersion should be able to get a releasever - # from pyanaconda.constants.productVersion. This trickery is due to the - # fact that pyanaconda/packaging/__init__.py will have already imported - # productVersion from pyanaconda.constants. - import pyanaconda.packaging - pyanaconda.packaging.productVersion = "17-Beta" - self.assertEqual(self.payload._getReleaseVersion(None), "17") diff --git a/anaconda/old_tests/pyanaconda_test/partintfhelpers_test.py b/anaconda/old_tests/pyanaconda_test/partintfhelpers_test.py deleted file mode 100644 index efc0680..0000000 --- a/anaconda/old_tests/pyanaconda_test/partintfhelpers_test.py +++ /dev/null @@ -1,480 +0,0 @@ -#!/usr/bin/python - -import mock - -class PartIntfHelpersTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "block", 'parted', 'storage', - 'pyanaconda.storage.formats', 'logging', - 'ConfigParser', 'pyanaconda.storage.storage_log']) - - self.fs = mock.DiskIO() - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - import pyanaconda.partIntfHelpers - - def tearDown(self): - self.tearDownModules() - - # sanityCheckVolumeGroupName tests - - def sanitycheckvolumegroupname_right_hostname_1_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "hostname" - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertEqual(ret, None) - - def sanitycheckvolumegroupname_right_hostname_2_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "h" - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertEqual(ret, None) - - def sanitycheckvolumegroupname_right_hostname_3_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "a" * 127 - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertEqual(ret, None) - - def sanitycheckvolumegroupname_right_hostname_4_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "h-o_s-t.name" - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertEqual(ret, None) - - def sanitycheckvolumegroupname_empty_hostname_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "" - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_long_hostname_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = "asdfasdfas" * 13 - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_1_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = 'lvm' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_2_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = 'root' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_3_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = '.' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_4_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = '..' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_5_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = 'foo bar' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - def sanitycheckvolumegroupname_bad_hostname_6_test(self): - import pyanaconda.partIntfHelpers - HOSTNAME = 'foob@r' - ret = pyanaconda.partIntfHelpers.sanityCheckVolumeGroupName(HOSTNAME) - self.assertNotEqual(ret, None) - - # sanityCheckLogicalVolumeName test - - def sanitychecklogicalvolumename_right_name_1_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "name" - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertEqual(ret, None) - - def sanitychecklogicalvolumename_right_name_2_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "name_00.9" - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertEqual(ret, None) - - def sanitychecklogicalvolumename_right_name_3_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "a" - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertEqual(ret, None) - - def sanitychecklogicalvolumename_empty_name_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "" - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_long_name_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "b" * 129 - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_bad_name_1_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "group" - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_bad_name_2_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = "." - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_bad_name_3_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = ".." - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_bad_name_4_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = 'foo bar' - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - def sanitychecklogicalvolumename_bad_name_5_test(self): - import pyanaconda.partIntfHelpers - LOGVOLNAME = 'foob@r' - ret = pyanaconda.partIntfHelpers.sanityCheckLogicalVolumeName(LOGVOLNAME) - self.assertNotEqual(ret, None) - - # sanityCheckMountPoint test - - def sanitycheckmountpoint_right_name_1_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/foob@r' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertEqual(ret, None) - - def sanitycheckmountpoint_right_name_2_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/var' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertEqual(ret, None) - - def sanitycheckmountpoint_right_name_3_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertEqual(ret, None) - - def sanitycheckmountpoint_bad_name_1_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '//' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def sanitycheckmountpoint_bad_name_2_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/foo bar' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def sanitycheckmountpoint_bad_name_3_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/./' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def sanitycheckmountpoint_bad_name_4_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/../' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def sanitycheckmountpoint_bad_name_5_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/..' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def sanitycheckmountpoint_bad_name_6_test(self): - import pyanaconda.partIntfHelpers - MNTPT = '/.' - ret = pyanaconda.partIntfHelpers.sanityCheckMountPoint(MNTPT) - self.assertNotEqual(ret, None) - - def dodeletedevice_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - STORAGE = mock.Mock() - DEVICE = None - ret = pyanaconda.partIntfHelpers.doDeleteDevice(INTF, STORAGE, DEVICE) - self.assertFalse(ret) - - def dodeletedevice_2_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - STORAGE = mock.Mock() - STORAGE.deviceImmutable.return_value = True - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doDeleteDevice(INTF, STORAGE, DEVICE) - self.assertFalse(ret) - - def dodeletedevice_3_test(self): - import pyanaconda.partIntfHelpers - pyanaconda.partIntfHelpers.confirmDelete = mock.Mock(return_value=False) - INTF = mock.Mock() - STORAGE = mock.Mock() - STORAGE.deviceImmutable.return_value = False - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doDeleteDevice(INTF, STORAGE, DEVICE) - self.assertFalse(ret) - - def dodeletedevice_4_test(self): - import pyanaconda.partIntfHelpers - pyanaconda.partIntfHelpers.confirmDelete = mock.Mock(return_value=False) - INTF = mock.Mock() - STORAGE = mock.Mock() - STORAGE.deviceImmutable.return_value = False - STORAGE.deviceDeps.return_value = [] - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doDeleteDevice(INTF, STORAGE, DEVICE, - confirm=0) - self.assertTrue(ret) - self.assertTrue(STORAGE.destroyDevice.called) - - def dodeletedevice_5_test(self): - import pyanaconda.partIntfHelpers - pyanaconda.partIntfHelpers.confirmDelete = mock.Mock(return_value=True) - INTF = mock.Mock() - STORAGE = mock.Mock() - STORAGE.deviceImmutable.return_value = False - STORAGE.deviceDeps.return_value = [] - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doDeleteDevice(INTF, STORAGE, DEVICE) - self.assertTrue(ret) - self.assertTrue(STORAGE.destroyDevice.called) - - def doclearpartitioneddevice_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - INTF.messageWindow.return_value = 0 - STORAGE = mock.Mock() - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doClearPartitionedDevice(INTF, STORAGE, - DEVICE) - self.assertFalse(ret) - - def doclearpartitioneddevice_2_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - INTF.messageWindow.return_value = 1 - STORAGE = mock.Mock() - STORAGE.partitions = [] - DEVICE = mock.Mock() - ret = pyanaconda.partIntfHelpers.doClearPartitionedDevice(INTF, STORAGE, - DEVICE) - self.assertFalse(ret) - - def doclearpartitioneddevice_3_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - INTF.messageWindow.return_value = 1 - DEVICE = mock.Mock() - p = mock.Mock() - p.disk = DEVICE - p.partedPartition.number = 0 - STORAGE = mock.Mock() - STORAGE.partitions = [p] - STORAGE.deviceImmutable.return_value = False - STORAGE.deviceDeps.return_value = [] - - ret = pyanaconda.partIntfHelpers.doClearPartitionedDevice(INTF, STORAGE, - DEVICE) - self.assertTrue(ret) - - def checkforswapnomatch_test(self): - import pyanaconda.partIntfHelpers - pyanaconda.partIntfHelpers.parted.PARTITION_SWAP = 5 - device = mock.Mock() - device.exists.return_value = True - device.getFlag.return_value = True - device.format.type == "swap" - ANACONDA = mock.Mock() - ANACONDA.storage.partitions = [device] - ANACONDA.intf.messageWindow.return_value = 1 - pyanaconda.partIntfHelpers.checkForSwapNoMatch(ANACONDA) - self.assertTrue(ANACONDA.storage.formatDevice.called) - - def musthaveselecteddrive_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - pyanaconda.partIntfHelpers.mustHaveSelectedDrive(INTF) - self.assertTrue(INTF.messageWindow.called) - - def querynoformatpreexisting_test(self): - import pyanaconda.partIntfHelpers - RET = 22 - INTF = mock.Mock() - ret = INTF.messageWindow.return_value = RET - self.assertEqual(RET, ret) - - def partitionsanityerrors_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - ERRORS = [] - ret = pyanaconda.partIntfHelpers.partitionSanityErrors(INTF, ERRORS) - self.assertEqual(1, ret) - - def partitionsanityerrors_2_test(self): - import pyanaconda.partIntfHelpers - RET = 5 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - ERRORS = ['err string', 'foo string'] - ret = pyanaconda.partIntfHelpers.partitionSanityErrors(INTF, ERRORS) - self.assertEqual(RET, ret) - self.assertTrue(ERRORS[0] in INTF.messageWindow.call_args[0][1]) - self.assertTrue(ERRORS[1] in INTF.messageWindow.call_args[0][1]) - - def partitionsanitywarnings_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - WARNINGS = [] - ret = pyanaconda.partIntfHelpers.partitionSanityWarnings(INTF, WARNINGS) - self.assertEqual(1, ret) - - def partitionsanitywarnings_2_test(self): - import pyanaconda.partIntfHelpers - RET = 5 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - WARNINGS = ['warning string', 'foo string'] - ret = pyanaconda.partIntfHelpers.partitionSanityWarnings(INTF, WARNINGS) - self.assertEqual(RET, ret) - self.assertTrue(WARNINGS[0] in INTF.messageWindow.call_args[0][1]) - self.assertTrue(WARNINGS[1] in INTF.messageWindow.call_args[0][1]) - - def partitionpreexistformatwarnings_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - WARNINGS = [] - ret = pyanaconda.partIntfHelpers.partitionPreExistFormatWarnings(INTF, WARNINGS) - self.assertEqual(1, ret) - - def partitionpreexistformatwarnings_2_test(self): - import pyanaconda.partIntfHelpers - RET = 10 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - WARNINGS = [('foo', 'foobar', '/foodir')] - ret = pyanaconda.partIntfHelpers.partitionPreExistFormatWarnings(INTF, WARNINGS) - self.assertEqual(RET, ret) - self.assertTrue(WARNINGS[0][0] in INTF.messageWindow.call_args[0][1]) - - def getpreexistformatwarnings_1_test(self): - import pyanaconda.partIntfHelpers - STORAGE = mock.Mock() - STORAGE.devicetree.devices = [] - ret = pyanaconda.partIntfHelpers.getPreExistFormatWarnings(STORAGE) - self.assertEqual([], ret) - - def getpreexistformatwarnings_2_test(self): - import pyanaconda.partIntfHelpers - STORAGE = mock.Mock() - device = mock.Mock() - device.exists = True - device.name = 'foodev' - device.path = '/foodevdir' - device.format.name = 'fffoodev' - device.format.mountpoint = '/mnt/foo' - device.format.exists = False - device.format.hidden = False - STORAGE.devicetree.devices = [device] - ret = pyanaconda.partIntfHelpers.getPreExistFormatWarnings(STORAGE) - self.assertEqual([('/foodevdir', 'fffoodev', '/mnt/foo')], ret) - - def confirmdelete_1_test(self): - import pyanaconda.partIntfHelpers - INTF = mock.Mock() - DEVICE = False - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(None, ret) - - def confirmdelete_2_test(self): - import pyanaconda.partIntfHelpers - RET = 51 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - DEVICE = mock.Mock() - DEVICE.type = "lvmvg" - DEVICE.name = "devname" - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(RET, ret) - self.assertTrue(DEVICE.name in INTF.messageWindow.call_args[0][1]) - - def confirmdelete_3_test(self): - import pyanaconda.partIntfHelpers - RET = 52 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - DEVICE = mock.Mock() - DEVICE.type = "lvmlv" - DEVICE.name = "devname" - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(RET, ret) - self.assertTrue(DEVICE.name in INTF.messageWindow.call_args[0][1]) - - def confirmdelete_4_test(self): - import pyanaconda.partIntfHelpers - RET = 53 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - DEVICE = mock.Mock() - DEVICE.type = "mdarray" - DEVICE.name = "devname" - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(RET, ret) - - def confirmdelete_5_test(self): - import pyanaconda.partIntfHelpers - RET = 54 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - DEVICE = mock.Mock() - DEVICE.type = "partition" - DEVICE.name = "devname" - DEVICE.path = "/dev/devname" - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(RET, ret) - self.assertTrue(DEVICE.path in INTF.messageWindow.call_args[0][1]) - - def confirmdelete_6_test(self): - import pyanaconda.partIntfHelpers - RET = 55 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - DEVICE = mock.Mock() - DEVICE.type = "other" - DEVICE.name = "devname" - ret = pyanaconda.partIntfHelpers.confirmDelete(INTF, DEVICE) - self.assertEqual(RET, ret) - self.assertTrue(DEVICE.type in INTF.messageWindow.call_args[0][1]) - self.assertTrue(DEVICE.name in INTF.messageWindow.call_args[0][1]) - - def confirmresetpartitionstate_test(self): - import pyanaconda.partIntfHelpers - RET = 61 - INTF = mock.Mock() - INTF.messageWindow.return_value = RET - ret = pyanaconda.partIntfHelpers.confirmResetPartitionState(INTF) - self.assertEqual(RET, ret) - diff --git a/anaconda/old_tests/pyanaconda_test/product_test.py b/anaconda/old_tests/pyanaconda_test/product_test.py deleted file mode 100644 index 9b0d864..0000000 --- a/anaconda/old_tests/pyanaconda_test/product_test.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/python - -import mock -import sys -import __builtin__ - -import ConfigParser - -class ProductTest(mock.TestCase): - - def setUp(self): - self.setupModules(['_isys', 'block', 'os']) - self.fs = mock.DiskIO() - - # os module global mock - self.modifiedModule("os") - os = sys.modules['os'] - os.access = mock.Mock(return_value=False) - os.uname.return_value = ('', '', '', '', 'i386') - os.environ = {} - - # fake /tmp/product/.buildstamp file - self.BUGURL = 'http://bug.url' - self.FINAL = 'false' - self.ARCH = 'i386' - self.NAME = '__anaconda' - self.UUID = '123456.%s' % self.ARCH - self.VERSION = '14' - self.FILENAME = '/tmp/product/.buildstamp' - self.FILE = \ - "[Main]\n"\ - "BugURL: %s\n"\ - "IsFinal: %s\n"\ - "Arch: %s\n"\ - "Product: %s\n"\ - "UUID: %s\n"\ - "Version: %s\n" % \ - (self.BUGURL, self.FINAL, self.ARCH, self.NAME, self.UUID, self.VERSION) - - self.fs.open(self.FILENAME, 'w').write(self.FILE) - - # mock builtin open function - self.open = __builtin__.open - __builtin__.open = self.fs.open - - if 'pyanaconda.product' in sys.modules: - del(sys.modules["pyanaconda.product"]) - - def tearDown(self): - __builtin__.open = self.open - self.tearDownModules() - - def bug_url_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertEqual(pyanaconda.product.bugUrl, self.BUGURL) - - def is_final_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertFalse(pyanaconda.product.isFinal) - - def product_arch_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertEqual(pyanaconda.product.productArch, self.ARCH) - - def product_name_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertEqual(pyanaconda.product.productName, self.NAME) - - def product_stamp_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertEqual(pyanaconda.product.productStamp, self.UUID) - - def product_version_test(self): - sys.modules['os'].access = mock.Mock(return_value=True) - import pyanaconda.product - self.assertEqual(pyanaconda.product.productVersion, self.VERSION) - diff --git a/anaconda/old_tests/pyanaconda_test/rescue_test.py b/anaconda/old_tests/pyanaconda_test/rescue_test.py deleted file mode 100644 index b93f75e..0000000 --- a/anaconda/old_tests/pyanaconda_test/rescue_test.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/python - -import mock - -class RescueTest(mock.TestCase): - - def setUp(self): - self.setupModules( - ['_isys', 'block', 'parted', 'storage', 'pyanaconda.storage.formats', - 'logging', 'add_drive_text', 'ConfigParser', - 'pyanaconda.storage.storage_log', 'pyanaconda.anaconda_log', 'snack' - ]) - - self.fs = mock.DiskIO() - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - import snack - snack.SnackScreen = mock.Mock() - - import pyanaconda.rescue - pyanaconda.rescue.open = self.fs.open - - def tearDown(self): - self.tearDownModules() - - # - # RescueInterface class tests - # - - def rescueinterface_waitwindow_test(self): - import pyanaconda.rescue - RET = 'foo1' - pyanaconda.rescue.WaitWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - ri = pyanaconda.rescue.RescueInterface() - ret = ri.waitWindow(TITLE, TEXT) - self.assertEqual(ret, RET) - - def rescueinterface_progresswindow_test(self): - import pyanaconda.rescue - RET = 'foo2' - pyanaconda.rescue.ProgressWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - TOTAL = 100 - ri = pyanaconda.rescue.RescueInterface() - ret = ri.progressWindow(TITLE, TEXT, TOTAL) - self.assertEqual(ret, RET) - - def rescueinterface_detailedmessagewindow_test(self): - import pyanaconda.rescue - RET = 'foo3' - pyanaconda.rescue.RescueInterface.messageWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - ri = pyanaconda.rescue.RescueInterface() - ret = ri.detailedMessageWindow(TITLE, TEXT) - self.assertEqual(ret, RET) - - def rescueinterface_messagewindow_1_test(self): - import pyanaconda.rescue - pyanaconda.rescue.ButtonChoiceWindow = mock.Mock() - TITLE = 'title' - TEXT = 'text' - TYPE = 'ok' - ri = pyanaconda.rescue.RescueInterface() - ri.detailedMessageWindow(TITLE, TEXT, TYPE) - self.assertTrue(pyanaconda.rescue.ButtonChoiceWindow.called) - - def rescueinterface_messagewindow_2_test(self): - import pyanaconda.rescue - RET='yes' - pyanaconda.rescue.ButtonChoiceWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - TYPE = 'yesno' - ri = pyanaconda.rescue.RescueInterface() - ret = ri.messageWindow(TITLE, TEXT, TYPE) - self.assertEqual(ret, 1) - - def rescueinterface_messagewindow_3_test(self): - import pyanaconda.rescue - RET = 'barfoo' - pyanaconda.rescue.ButtonChoiceWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - TYPE = 'custom' - CUSTOM_BUTT = ['foo_bar', 'bar_foo'] - ri = pyanaconda.rescue.RescueInterface() - ret = ri.messageWindow(TITLE, TEXT, TYPE, custom_buttons=CUSTOM_BUTT) - self.assertEqual(ret, 1) - - def rescueinterface_messagewindow_4_test(self): - import pyanaconda.rescue - RET = 'foo4' - pyanaconda.rescue.OkCancelWindow = mock.Mock(return_value=RET) - TITLE = 'title' - TEXT = 'text' - TYPE = 'otherfoo' - ri = pyanaconda.rescue.RescueInterface() - ret = ri.messageWindow(TITLE, TEXT, TYPE) - self.assertEqual(ret, RET) - - def rescueinterface_enablenetwork_1_test(self): - import pyanaconda.rescue - anaconda = mock.Mock() - anaconda.network.netdevices = {} - - ri = pyanaconda.rescue.RescueInterface() - ret = ri.enableNetwork(anaconda) - self.assertFalse(ret) - - def rescueinterface_passphraseentrywindow_test(self): - import pyanaconda.rescue - RET = ('secret', False) - pyanaconda.rescue.PassphraseEntryWindow = mock.Mock() - pyanaconda.rescue.PassphraseEntryWindow().run.return_value = RET - DEVICE = 'dev' - - ri = pyanaconda.rescue.RescueInterface() - ret = ri.passphraseEntryWindow(DEVICE) - self.assertEqual(ret, RET) - self.assertTrue(pyanaconda.rescue.PassphraseEntryWindow().pop.called) - - def rescueinterface_resetinitializediskquestion_test(self): - import pyanaconda.rescue - ri = pyanaconda.rescue.RescueInterface() - ri._initLabelAnswers = {'foo': 'bar'} - ri.resetInitializeDiskQuestion() - - def rescueinterface_resetreinitinconsistentlvmquestion_test(self): - import pyanaconda.rescue - ri = pyanaconda.rescue.RescueInterface() - ri._inconsistentLVMAnswers = {'foo': 'bar'} - ri.resetReinitInconsistentLVMQuestion() - self.assertEqual(ri._inconsistentLVMAnswers, {}) - - def rescueinterface_questioninitializedisk_test(self): - import pyanaconda.rescue - ri = pyanaconda.rescue.RescueInterface() - ret = ri.questionInitializeDisk('/', '', 0) - self.assertFalse(ret) - - def rescueinterface_questionreinitinconsistentlvm_test(self): - import pyanaconda.rescue - ri = pyanaconda.rescue.RescueInterface() - ret = ri.questionReinitInconsistentLVM() - self.assertFalse(ret) - - def rescueinterface_questioninitializedasd_test(self): - import pyanaconda.rescue - ri = pyanaconda.rescue.RescueInterface() - ret = ri.questionInitializeDASD('', '') - self.assertEqual(ret, 1) - - # - # module function tests - # - - def makefstab_test(self): - import pyanaconda.rescue - INSTPATH = '/tmp' - FSTAB = 'rootfs / rootfs rw 0 0' - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.access.return_value = True - self.fs.open('/proc/mounts', 'w').write(FSTAB) - self.fs.open('%s/etc/fstab' % INSTPATH, 'w') - - ret = pyanaconda.rescue.makeFStab(INSTPATH) - self.assertEqual(self.fs['%s/etc/fstab' % INSTPATH], FSTAB) - - def makeresolvconf_1_test(self): - import pyanaconda.rescue - INSTPATH = '/tmp' - RESOLV = "nameserver 10.0.0.1" - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.access.return_value = False - pyanaconda.rescue.shutil = mock.Mock() - - pyanaconda.rescue.makeResolvConf(INSTPATH) - self.assertFalse(pyanaconda.rescue.shutil.copyfile.called) - - def makeresolvconf_2_test(self): - import pyanaconda.rescue - INSTPATH = '/tmp' - RESOLV = "nameserver 10.0.0.1" - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.access.return_value = True - pyanaconda.rescue.shutil = mock.Mock() - self.fs.open('%s/etc/resolv.conf' % INSTPATH, 'w').write(RESOLV) - - pyanaconda.rescue.makeResolvConf(INSTPATH) - self.assertFalse(pyanaconda.rescue.shutil.copyfile.called) - - def makeresolvconf_3_test(self): - import pyanaconda.rescue - INSTPATH = '/tmp' - RESOLV = "nameserver 10.0.0.1" - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.access.return_value = True - pyanaconda.rescue.shutil = mock.Mock() - self.fs.open('%s/etc/resolv.conf' % INSTPATH, 'w').write('') - self.fs.open('/etc/resolv.conf', 'w').write('') - - pyanaconda.rescue.makeResolvConf(INSTPATH) - self.assertFalse(pyanaconda.rescue.shutil.copyfile.called) - self.assertEqual(self.fs['%s/etc/resolv.conf' % INSTPATH], '') - - def makeresolvconf_4_test(self): - import pyanaconda.rescue - INSTPATH = '/tmp' - RESOLV = "nameserver 10.0.0.1" - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.access.return_value = True - pyanaconda.rescue.shutil = mock.Mock() - self.fs.open('%s/etc/resolv.conf' % INSTPATH, 'w').write('') - self.fs.open('/etc/resolv.conf', 'w').write(RESOLV) - - pyanaconda.rescue.makeResolvConf(INSTPATH) - self.assertTrue(pyanaconda.rescue.shutil.copyfile.called) - self.assertEqual(self.fs['%s/etc/resolv.conf' % INSTPATH], - 'nameserver 10.0.0.1') - - def startnetworking_test(self): - import pyanaconda.rescue - NETWORK = mock.Mock() - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.startNetworking(NETWORK, '') - self.assertEqual(pyanaconda.rescue.os.system.call_args, - (('/usr/sbin/ifconfig lo 127.0.0.1',), {})) - self.assertTrue(NETWORK.bringUp.called) - - def runshell_1_test(self): - import pyanaconda.rescue - import sys - TMPFILE = '/tmp/abc' - MSG = "foo bar" - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.path.exists.return_value = True - pyanaconda.rescue.subprocess = mock.Mock() - proc = mock.Mock() - proc.returncode = 0 - pyanaconda.rescue.subprocess.Popen.return_value = proc - - stdout = sys.stdout - sys.stdout = self.fs.open(TMPFILE, 'w') - pyanaconda.rescue.runShell(msg=MSG) - sys.stdout.close() - sys.stdout = stdout - - self.assertTrue(MSG in self.fs[TMPFILE]) - self.assertEqual(pyanaconda.rescue.subprocess.Popen.call_args, - ((['/usr/bin/firstaidkit-qs'],), {})) - - def runshell_2_test(self): - import pyanaconda.rescue - import sys - TMPFILE = '/tmp/abc' - MSG = "foo bar" - - def fake_f(filename, _=""): - return filename == "/bin/bash" - - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.path.exists = fake_f - pyanaconda.rescue.iutil = mock.Mock() - proc = mock.Mock() - proc.returncode = 0 - pyanaconda.rescue.subprocess.Popen.return_value = proc - - stdout = sys.stdout - sys.stdout = self.fs.open(TMPFILE, 'w') - pyanaconda.rescue.runShell(msg=MSG) - sys.stdout.close() - sys.stdout = stdout - - self.assertTrue(MSG in self.fs[TMPFILE]) - self.assertTrue(pyanaconda.rescue.iutil.execConsole.called) - - def runshell_3_test(self): - import pyanaconda.rescue - import sys - TMPFILE = '/tmp/abc' - SCREEN = mock.Mock() - pyanaconda.rescue.os = mock.Mock() - pyanaconda.rescue.os.path.exists.return_value = True - pyanaconda.rescue.subprocess = mock.Mock() - proc = mock.Mock() - proc.returncode = 0 - pyanaconda.rescue.subprocess.Popen.return_value = proc - - pyanaconda.rescue.runShell(screen=SCREEN) - - self.assertTrue(SCREEN.suspend.called) - self.assertTrue(SCREEN.finish.called) diff --git a/anaconda/old_tests/pyanaconda_test/security_test.py b/anaconda/old_tests/pyanaconda_test/security_test.py deleted file mode 100644 index 33b94c1..0000000 --- a/anaconda/old_tests/pyanaconda_test/security_test.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/python - -import mock -import sys - -class SecurityTest(mock.TestCase): - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - - self.fs = mock.DiskIO() - - import pyanaconda.security - pyanaconda.security.log = mock.Mock() - pyanaconda.security.open = self.fs.open - pyanaconda.security.iutil = mock.Mock() - - import pyanaconda.flags - pyanaconda.flags.flags.selinux = 1 - - def tearDown(self): - self.tearDownModules() - - def set_get_selinux_test(self): - import pyanaconda.security - - states = pyanaconda.security.selinux_states - scrt = pyanaconda.security.Security() - - for state in states: - scrt.setSELinux(state) - self.assertEqual(scrt.getSELinux(), state) - - def set_get_selinux_bad_sate_test(self): - import pyanaconda.security - - states = pyanaconda.security.selinux_states - scrt = pyanaconda.security.Security() - scrt.setSELinux('bad_state') - self.assertTrue(scrt.getSELinux() in states) - - def write_test(self): - """Simulate writing security (simulate executing lokkit and authconfig)""" - import pyanaconda.security - - scrt = pyanaconda.security.Security() - pyanaconda.security.ROOT_PATH = "/tmp/security" - scrt.write() - - self.assertEqual(pyanaconda.security.iutil.method_calls, - [('execWithRedirect', - ('/usr/sbin/lokkit', ['--selinux=enforcing']), - {'root': '/tmp/security', 'stderr': '/dev/null', 'stdout': '/dev/null'} - ), - ('resetRpmDb', (), {}), - ('execWithRedirect', - ('/usr/sbin/authconfig', - ['--update', '--nostart', '--enableshadow', '--passalgo=sha512'] - ), - {'root': '/tmp/security', 'stderr': '/dev/tty5', 'stdout': '/dev/tty5'} - ) - ] - ) diff --git a/anaconda/old_tests/pyanaconda_test/simpleconfig_test.py b/anaconda/old_tests/pyanaconda_test/simpleconfig_test.py deleted file mode 100644 index 265ed88..0000000 --- a/anaconda/old_tests/pyanaconda_test/simpleconfig_test.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/python - -import mock -import os -import sys - -class SimpleconfigTest(mock.TestCase): - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - import pyanaconda.simpleconfig - - # Stuff for IfcfgFile class tests - self.DIR = '/tmp/' - self.IFACE = 'eth0' - self.PATH = "%sifcfg-%s" % (self.DIR, self.IFACE) - self.CONTENT = '# Broadcom Corporation NetXtreme BCM5761 Gigabit Ethernet\n' - self.CONTENT += 'DEVICE=eth0\n' - self.CONTENT += 'HWADDR=00:10:18:61:35:98\n' - self.CONTENT += 'ONBOOT=no\n' - open(self.PATH, 'w').write(self.CONTENT) - - def tearDown(self): - self.tearDownModules() - - def uppercase_ascii_string_letters_test(self): - """Converting to uppercase (letters)""" - import pyanaconda.simpleconfig - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('abcd') - self.assertEqual(ret, 'ABCD') - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('aBCD') - self.assertEqual(ret, 'ABCD') - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('ABCD') - self.assertEqual(ret, 'ABCD') - - def uppercase_ascii_string_numbers_test(self): - """Converting to uppercase (numbers)""" - import pyanaconda.simpleconfig - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('123') - self.assertEqual(ret, '123') - - def uppercase_ascii_string_others_test(self): - """Converting to uppercase (special chars)""" - import pyanaconda.simpleconfig - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('--') - self.assertEqual(ret, '--') - ret = pyanaconda.simpleconfig.uppercase_ASCII_string(' ') - self.assertEqual(ret, ' ') - ret = pyanaconda.simpleconfig.uppercase_ASCII_string('') - self.assertEqual(ret, '') - - def unquote_test(self): - from pyanaconda.simpleconfig import unquote - self.assertEqual(unquote("plain string"), "plain string") - self.assertEqual(unquote('"double quote"'), "double quote") - self.assertEqual(unquote("'single quote'"), "single quote") - - def quote_test(self): - from pyanaconda.simpleconfig import quote - self.assertEqual(quote("nospaces"), "nospaces") - self.assertEqual(quote("plain string"), '"plain string"') - self.assertEqual(quote("alwaysquote", always=True), '"alwaysquote"') - - def set_and_get_test(self): - """Setting and getting values""" - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.SimpleConfigFile() - scf.set(('key1', 'value1')) - self.assertEqual(scf.get('key1'), 'value1') - scf.set(('KEY2', 'value2')) - self.assertEqual(scf.get('key2'), 'value2') - scf.set(('KEY3', 'value3')) - self.assertEqual(scf.get('KEY3'), 'value3') - scf.set(('key4', 'value4')) - self.assertEqual(scf.get('KEY4'), 'value4') - - def unset_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.SimpleConfigFile() - scf.set(('key1', 'value1')) - scf.unset(('key1')) - self.assertEqual(scf.get('key1'), '') - - def write_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.SimpleConfigFile() - scf.set(('key1', 'value1')) - scf.write('/tmp/file') - self.assertEqual(open('/tmp/file').read(), 'KEY1=value1\n') - - def read_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.SimpleConfigFile() - open('/tmp/file', 'w').write('KEY1="value1"\n') - scf.read('/tmp/file') - self.assertEqual(scf.get('key1'), 'value1') - - def read_write_test(self): - from pyanaconda.simpleconfig import SimpleConfigFile - scf = SimpleConfigFile() - scf.read(self.PATH) - scf.write("/tmp/file") - self.assertEqual(open("/tmp/file").read(), self.CONTENT) - - def write_new_keys_test(self): - from pyanaconda.simpleconfig import SimpleConfigFile - scf = SimpleConfigFile() - scf.read(self.PATH) - scf.set(("key1", "value1")) - scf.write("/tmp/file") - self.assertEqual(open("/tmp/file").read(), - self.CONTENT+"KEY1=value1\n") - - def remove_key_test(self): - from pyanaconda.simpleconfig import SimpleConfigFile - scf = SimpleConfigFile() - scf.read(self.PATH) - scf.unset("BOOT") - scf.write("/tmp/file") - scf.reset() - scf.read("/tmp/file") - self.assertEqual(scf.get("BOOT"), "") - - def ifcfgfile_path_property_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.IfcfgFile(self.DIR, self.IFACE) - self.assertEqual(scf.path, self.PATH) - - def ifcfgfile_read_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.IfcfgFile(self.DIR, self.IFACE) - scf.read() - self.assertEqual(scf.get('device'), 'eth0') - self.assertEqual(scf.get('hwaddr'), '00:10:18:61:35:98') - self.assertEqual(scf.get('onboot'), 'no') - - def ifcfgfile_read_and_clear_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.IfcfgFile(self.DIR, self.IFACE) - scf.read() - scf.clear() - self.assertEqual(scf.get('device'), '') - self.assertEqual(scf.get('hwaddr'), '') - self.assertEqual(scf.get('onboot'), '') - - def ifcfgfile_write_test(self): - import pyanaconda.simpleconfig - scf = pyanaconda.simpleconfig.IfcfgFile(self.DIR, self.IFACE) - scf.set(('device', 'eth0')) - scf.set(('hwaddr', '00:11:22:33:44:55')) - scf.set(('onboot', 'no')) - scf.write() - self.assertEqual(open(self.PATH).read(), - 'DEVICE="eth0"\nHWADDR="00:11:22:33:44:55"\nONBOOT="no"\n') - diff --git a/anaconda/old_tests/pyanaconda_test/timezone_test.py b/anaconda/old_tests/pyanaconda_test/timezone_test.py deleted file mode 100644 index a53f292..0000000 --- a/anaconda/old_tests/pyanaconda_test/timezone_test.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python - -import mock -import sys - -ZONE = 'Europe/Prague' -UTC = 2 - -class TimeZoneTest(mock.TestCase): - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - - self.fs = mock.DiskIO() - - import pyanaconda.timezone - pyanaconda.timezone.log = mock.Mock() - pyanaconda.timezone.open = self.fs.open - pyanaconda.timezone.os.access = mock.Mock(return_value = True) - pyanaconda.timezone.shutil.copyfile = mock.Mock() - pyanaconda.timezone.os = mock.Mock() - pyanaconda.timezone.os.access.return_value = True - pyanaconda.timezone.shutil = mock.Mock() - #pyanaconda.timezone.shutil.copyfile = mock.Mock() - - def tearDown(self): - self.tearDownModules() - - def get_timezone_info_test(self): - import pyanaconda.timezone - tz = pyanaconda.timezone.Timezone() - info = tz.getTimezoneInfo() - self.assertEqual( (tz.tz, tz.utc), info ) - - def set_timezone_info_test(self): - import pyanaconda.timezone - tz = pyanaconda.timezone.Timezone() - tz.setTimezoneInfo(ZONE, UTC) - self.assertEqual((ZONE, UTC), (tz.tz, tz.utc)) - - def write_test(self): - import pyanaconda.timezone - - tz = pyanaconda.timezone.Timezone() - tz.tz = ZONE - tz.utc = True - - PATH = '' - ADJTIME = '0.013782 1279118821 0.000000\n1279118821\nUTC\n' - f = self.fs.open('/mnt/sysimage/etc/adjtime', 'w') - f.write(ADJTIME) - f.close() - - tz.write() - self.assertEqual(self.fs['/mnt/sysimage/etc/adjtime'], ADJTIME) - - diff --git a/anaconda/old_tests/pyanaconda_test/users_test.py b/anaconda/old_tests/pyanaconda_test/users_test.py deleted file mode 100644 index c457ef7..0000000 --- a/anaconda/old_tests/pyanaconda_test/users_test.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/python - -import mock -import sys - -GIDNUMBER = 'pw_gid' -HOMEDIRECTORY = 'pw_dir' - -class UsersTest(mock.TestCase): - def setUp(self): - self.setupModules(["_isys", "block", "ConfigParser"]) - - self.fs = mock.DiskIO() - self.anaconda = mock.Mock() - self.anaconda.security.auth.find.return_value = -1 - - import pyanaconda.users - pyanaconda.users.log = mock.Mock() - pyanaconda.users.iutil = mock.Mock() - pyanaconda.users.iutil.mkdirChain = mock.Mock() - - pyanaconda.users.os = mock.Mock() - pyanaconda.users.os.fork.return_value=False - pyanaconda.users.os.waitpid.return_value=(1, 1) - pyanaconda.users.os.WEXITSTATUS.return_value=0 - - pyanaconda.users.libuser.admin = mock.Mock() - pyanaconda.users.libuser.GIDNUMBER = GIDNUMBER - pyanaconda.users.libuser.HOMEDIRECTORY = HOMEDIRECTORY - pyanaconda.users.libuser.admin().lookupGroupByName.return_value = False - pyanaconda.users.libuser.admin().lookupUserByName.return_value = False - pyanaconda.users.libuser.admin().initGroup().get.return_value = [''] - pyanaconda.users.libuser.admin().initGroup().reset_mock() - pyanaconda.users.libuser.admin().reset_mock() - - def tearDown(self): - self.tearDownModules() - - def create_group_test(self): - import pyanaconda.users - - GROUP = 'Group' - GID = 100 - group_dict = { "name" : GROUP, - "gid" : GID, - "root" : "" - } - - usr = pyanaconda.users.Users(self.anaconda) - self.assertTrue(usr.createGroup(GROUP, **group_dict)) - - methods = pyanaconda.users.libuser.admin().method_calls[:] - try: - if methods[2][0] == 'addGroup': - methods.pop() - except: - pass - - self.assertEqual(methods, - [('lookupGroupByName', (GROUP,), {}), ('initGroup', (GROUP,), {}),]) - - self.assertEqual( - pyanaconda.users.libuser.admin().initGroup().method_calls, - [('set', (GIDNUMBER, GID), {})]) - - def create_user_test(self): - import pyanaconda.users - - USER = 'TestUser' - PASS = 'abcde' - user_dict = { "name" : USER, - "password" : PASS, - "groups" : [], - "homedir" : "", - "isCrypted" : False, - "shell" : "", - "uid" : None, - "root" : "" - } - - usr = pyanaconda.users.Users(self.anaconda) - self.assertTrue(usr.createUser(USER, **user_dict)) - - self.assertTrue(pyanaconda.users.iutil.mkdirChain.called) - methods = [x[0] for x in pyanaconda.users.libuser.admin().method_calls] - - self.assertEqual(methods, - ['lookupUserByName', 'initUser', 'initGroup', 'addUser','addGroup', - 'setpassUser', 'lookupGroupByName']) - - self.assertEqual(pyanaconda.users.libuser.admin().initUser.call_args_list, - [((USER,), {})]) - - self.assertEqual(pyanaconda.users.libuser.admin().initGroup.call_args_list, - [((USER,), {})]) - - self.assertEqual(pyanaconda.users.libuser.admin().initUser().method_calls, - [('set', (GIDNUMBER, ['']), {}), - ('set', (HOMEDIRECTORY, '/home/%s' % USER), {})] - ) - - self.assertEqual(pyanaconda.users.libuser.admin().initGroup().method_calls, - [('get', (GIDNUMBER,), {})]) - - def check_user_exists_test(self): - import pyanaconda.users - - USER = 'TestUser' - - usr = pyanaconda.users.Users(self.anaconda) - self.assertTrue(usr.checkUserExists(USER, root='')) - self.assertEqual(pyanaconda.users.libuser.admin().method_calls, - [('lookupUserByName', (USER,), {})]) - - def get_pass_algo_md5_test(self): - import pyanaconda.users - usr = pyanaconda.users.Users(self.anaconda) - self.assertEqual(usr.getPassAlgo(), None) - - def set_user_password_test(self): - import pyanaconda.users - - USER = 'TestUser' - PASS = 'abcde' - CRYPTED = False - LOCK = False - - usr = pyanaconda.users.Users(self.anaconda) - usr.setUserPassword(USER, PASS, CRYPTED, LOCK) - - methods = [x[0] for x in pyanaconda.users.libuser.admin().method_calls] - self.assertEqual(methods, - ['lookupUserByName', 'setpassUser', 'modifyUser']) - - def set_root_password_test(self): - import pyanaconda.users - - usr = pyanaconda.users.Users(self.anaconda) - usr.setRootPassword() - methods = [x[0] for x in pyanaconda.users.libuser.admin().method_calls] - self.assertEqual(methods, - ['lookupUserByName', 'setpassUser', 'modifyUser']) diff --git a/anaconda/old_tests/pyanaconda_test/vnc_test.py b/anaconda/old_tests/pyanaconda_test/vnc_test.py deleted file mode 100644 index 80cc527..0000000 --- a/anaconda/old_tests/pyanaconda_test/vnc_test.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/python - -import mock -import os - -class VncTest(mock.TestCase): - - def setUp(self): - self.setupModules(["_isys", "block", "logging", "ConfigParser"]) - self.fs = mock.DiskIO() - - self.anaconda = mock.Mock() - self.anaconda.ksdata.vnc.password = '' - - import pyanaconda - pyanaconda.anaconda_log = mock.Mock() - - self.OK = 22 - - import pyanaconda.vnc - pyanaconda.vnc.log = mock.Mock() - pyanaconda.vnc.os = mock.Mock() - pyanaconda.vnc.subprocess = mock.Mock() - pyanaconda.vnc.subprocess.Popen().communicate.return_value = (1, 2) - pyanaconda.vnc.subprocess.Popen().returncode = self.OK - pyanaconda.vnc.open = self.fs.open - - self.ROOT = '/' - self.DISPLAY = '2' - self.DESKTOP = 'Desktop' - self.PASS = '' - self.LOG_FILE = '/tmp/vnc.log' - self.PW_FILE = '/tmp/vncpassword' - self.VNCCONNECTHOST = 'host' - - def tearDown(self): - self.tearDownModules() - - def set_vnc_password_1_test(self): - import pyanaconda.vnc - server = pyanaconda.vnc.VncServer() - server.anaconda = self.anaconda - pyanaconda.vnc.iutil = mock.Mock() - pyanaconda.vnc.os.pipe.return_value = (1, 2) - - server.setVNCPassword() - self.assertEqual( - pyanaconda.vnc.iutil.execWithRedirect.call_args_list, - [(('vncpasswd', ['-f']), {'stdin': 1, 'stdout': '/tmp/vncpassword'})]) - - def initialize_test(self): - import pyanaconda.vnc - - IP = '192.168.0.21' - HOSTNAME = 'desktop' - - dev = mock.Mock() - dev.get.return_value = 'eth0' - pyanaconda.vnc.network = mock.Mock() - pyanaconda.vnc.network.Network().netdevices = [dev] - pyanaconda.vnc.network.getActiveNetDevs.return_value = [0] - pyanaconda.vnc.network.getDefaultHostname.return_value = HOSTNAME - pyanaconda.vnc.isys = mock.Mock() - pyanaconda.vnc.isys.getIPAddresses = mock.Mock(return_value=[IP]) - - server = pyanaconda.vnc.VncServer(display=self.DISPLAY) - server.initialize() - expected = "%s:%s (%s)" % (HOSTNAME, self.DISPLAY, IP) - self.assertEqual(server.connxinfo, expected) - - def openlogfile_test(self): - import pyanaconda.vnc - FILE = 'file' - pyanaconda.vnc.os.O_RDWR = os.O_RDWR - pyanaconda.vnc.os.O_CREAT = os.O_CREAT - pyanaconda.vnc.os.open.return_value = FILE - - server = pyanaconda.vnc.VncServer(log_file=self.LOG_FILE) - ret = server.openlogfile() - self.assertEqual(ret, FILE) - self.assertEqual(pyanaconda.vnc.os.open.call_args, - ((self.LOG_FILE, os.O_RDWR | os.O_CREAT), {}) - ) - - def connect_to_view_test(self): - import pyanaconda.vnc - pyanaconda.vnc.subprocess.Popen().communicate.return_value = (self.OK, '') - - server = pyanaconda.vnc.VncServer(vncconnecthost=self.VNCCONNECTHOST) - ret = server.connectToView() - self.assertTrue(ret) - - params = pyanaconda.vnc.subprocess.Popen.call_args[0][0] - self.assertTrue(self.VNCCONNECTHOST in params) - self.assertTrue(params[params.index(self.VNCCONNECTHOST)-1] == "-connect") - - def start_server_test(self): - import pyanaconda.vnc - pyanaconda.vnc.VncServer.initialize = mock.Mock() - pyanaconda.vnc.VncServer.setVNCPassword = mock.Mock() - pyanaconda.vnc.VncServer.VNCListen = mock.Mock() - pyanaconda.vnc.subprocess.Popen().poll.return_value = None - pyanaconda.vnc.os.environ = {} - pyanaconda.vnc.time.sleep = mock.Mock() - - server = pyanaconda.vnc.VncServer(root=self.ROOT, display=self.DISPLAY, - desktop=self.DESKTOP, password=self.PASS, vncconnecthost="") - server.openlogfile = mock.Mock() - server.startServer() - - params = pyanaconda.vnc.subprocess.Popen.call_args[0][0] - self.assertTrue('desktop=%s'%self.DESKTOP in params) - self.assertTrue(':%s'%self.DISPLAY in params) - self.assertTrue(pyanaconda.vnc.VncServer.VNCListen.called) - self.assertTrue("DISPLAY" in pyanaconda.vnc.os.environ) - self.assertEqual(pyanaconda.vnc.os.environ['DISPLAY'], ":%s" % self.DISPLAY) diff --git a/anaconda/old_tests/regex/__init__.py b/anaconda/old_tests/regex/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/anaconda/po/POTFILES.in b/anaconda/po/POTFILES.in index 7b5d1ac..e3ab399 100644 --- a/anaconda/po/POTFILES.in +++ b/anaconda/po/POTFILES.in @@ -23,10 +23,6 @@ pyanaconda/text.py pyanaconda/users.py pyanaconda/vnc.py -# Install class definitions -pyanaconda/installclasses/fedora.py -pyanaconda/installclasses/rhel.py - # Packaging module source files pyanaconda/packaging/__init__.py pyanaconda/packaging/livepayload.py @@ -131,6 +127,7 @@ pyanaconda/ui/gui/spokes/custom.glade pyanaconda/ui/gui/spokes/advstorage/fcoe.glade pyanaconda/ui/gui/spokes/advstorage/iscsi.glade pyanaconda/ui/gui/spokes/advstorage/dasd.glade +pyanaconda/ui/gui/spokes/advstorage/zfcp.glade pyanaconda/ui/gui/spokes/lib/cart.glade pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade pyanaconda/ui/gui/spokes/lib/dasdfmt.glade diff --git a/anaconda/po/anaconda.pot b/anaconda/po/anaconda.pot index a340661..775ca05 100644 --- a/anaconda/po/anaconda.pot +++ b/anaconda/po/anaconda.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: anaconda 21.48.20\n" +"Project-Id-Version: anaconda 22.20.12\n" "Report-Msgid-Bugs-To: anaconda-devel-list@redhat.com\n" -"POT-Creation-Date: 2014-12-02 09:06+0100\n" +"POT-Creation-Date: 2015-05-19 12:36-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,181 +18,182 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: pyanaconda/bootloader.py:247 -msgid "/boot filesystem" +#: pyanaconda/bootloader.py:248 +msgid "/boot file system" msgstr "" -#: pyanaconda/bootloader.py:404 +#: pyanaconda/bootloader.py:405 #, python-format msgid "" "RAID sets that contain '%(desc)s' must have one of the following raid " "levels: %(raid_level)s." msgstr "" -#: pyanaconda/bootloader.py:412 +#: pyanaconda/bootloader.py:413 #, python-format msgid "" "RAID sets that contain '%(desc)s' must have one of the following metadata " "versions: %(metadata_versions)s." msgstr "" -#: pyanaconda/bootloader.py:420 +#: pyanaconda/bootloader.py:421 #, python-format msgid "" "RAID sets that contain '%(desc)s' must have one of the following device " "types: %(types)s." msgstr "" -#: pyanaconda/bootloader.py:436 +#: pyanaconda/bootloader.py:437 #, python-format msgid "%(name)s must have one of the following disklabel types: %(types)s." msgstr "" -#: pyanaconda/bootloader.py:448 +#: pyanaconda/bootloader.py:449 #, python-format msgid "%(desc)s cannot be of type %(type)s." msgstr "" -#: pyanaconda/bootloader.py:454 +#: pyanaconda/bootloader.py:455 #, python-format msgid "%(desc)s must be mounted on one of %(mountpoints)s." msgstr "" -#: pyanaconda/bootloader.py:466 +#: pyanaconda/bootloader.py:467 #, python-format msgid "%(desc)s must be between %(min)d and %(max)d MB in size" msgstr "" -#: pyanaconda/bootloader.py:472 +#: pyanaconda/bootloader.py:473 #, python-format msgid "%(desc)s must not be smaller than %(min)dMB." msgstr "" -#: pyanaconda/bootloader.py:481 +#: pyanaconda/bootloader.py:482 #, python-format msgid "%(desc)s must not be larger than %(max)dMB." msgstr "" -#: pyanaconda/bootloader.py:499 +#: pyanaconda/bootloader.py:500 #, python-format msgid "%(desc)s must be within the first %(max_end)s of the disk." msgstr "" -#: pyanaconda/bootloader.py:509 +#: pyanaconda/bootloader.py:510 #, python-format msgid "%s must be on a primary partition." msgstr "" -#: pyanaconda/bootloader.py:645 pyanaconda/bootloader.py:745 +#: pyanaconda/bootloader.py:646 pyanaconda/bootloader.py:746 #, python-format msgid "%s cannot be on an encrypted block device." msgstr "" -#: pyanaconda/bootloader.py:700 +#: pyanaconda/bootloader.py:701 #, python-format msgid "%s cannot be on an iSCSI disk on s390(x)" msgstr "" -#: pyanaconda/bootloader.py:704 +#: pyanaconda/bootloader.py:705 #, python-format msgid "%(desc)s cannot be of type %(type)s" msgstr "" -#: pyanaconda/bootloader.py:1348 +#: pyanaconda/bootloader.py:1375 #, python-format msgid "" -"bootloader stage2 device %(stage2dev)s is on a multi-disk array, but " -"bootloader stage1 device %(stage1dev)s is not. A drive failure in " -"%(stage2dev)s could render the system unbootable." +"boot loader stage2 device %(stage2dev)s is on a multi-disk array, but boot " +"loader stage1 device %(stage1dev)s is not. A drive failure in %(stage2dev)s " +"could render the system unbootable." msgstr "" -#: pyanaconda/bootloader.py:1354 +#: pyanaconda/bootloader.py:1381 #, python-format msgid "" -"bootloader stage2 device %(stage2dev)s is on a multi-disk array, but " -"bootloader stage1 device %(stage1dev)s is not part of this array. The stage1 " -"bootloader will only be installed to a single drive." +"boot loader stage2 device %(stage2dev)s is on a multi-disk array, but boot " +"loader stage1 device %(stage1dev)s is not part of this array. The stage1 " +"boot loader will only be installed to a single drive." msgstr "" -#: pyanaconda/bootloader.py:1645 +#: pyanaconda/bootloader.py:1669 #, python-format msgid "" "%(deviceName)s may not have enough space for grub2 to embed core.img when " "using the %(fsType)s file system on %(deviceType)s" msgstr "" -#: pyanaconda/constants.py:72 +#: pyanaconda/constants.py:73 msgid "Start VNC" msgstr "" -#: pyanaconda/constants.py:73 +#: pyanaconda/constants.py:74 msgid "Use text mode" msgstr "" -#: pyanaconda/constants.py:137 +#: pyanaconda/constants.py:138 msgid "The password is empty." msgstr "" -#: pyanaconda/constants.py:138 +#: pyanaconda/constants.py:139 msgid "The passwords do not match." msgstr "" -#: pyanaconda/constants.py:139 -msgid "The passwords you entered were different. Please try again." -msgstr "" - #: pyanaconda/constants.py:140 -msgid "" -"The password you have provided is weak. You will have to press Done twice to " -"confirm it." +msgid "The passwords you entered were different. Please try again." msgstr "" #: pyanaconda/constants.py:141 #, python-format -msgid "" -"The password you have provided is weak: %s. You will have to press Done " -"twice to confirm it." +msgid "The password you have provided is weak. %s" msgstr "" #: pyanaconda/constants.py:142 -msgid "You have provided a weak password. Press Done again to use anyway." +#, python-format +msgid "The password you have provided is weak: %s. %s" msgstr "" #: pyanaconda/constants.py:143 +msgid "You have provided a weak password. Press Done again to use anyway." +msgstr "" + +#: pyanaconda/constants.py:144 #, python-format msgid "You have provided a weak password: %s. Press Done again to use anyway." msgstr "" -#: pyanaconda/constants.py:144 +#: pyanaconda/constants.py:145 msgid "" "The password you have provided contains non-ASCII characters. You may not be " "able to switch between keyboard layouts to login. Press Done to continue." msgstr "" #: pyanaconda/constants.py:146 +msgid "You will have to press Done twice to confirm it." +msgstr "" + +#: pyanaconda/constants.py:148 msgid "Empty" msgstr "" -#: pyanaconda/constants.py:146 pyanaconda/ui/gui/spokes/lib/passphrase.py:118 +#: pyanaconda/constants.py:148 pyanaconda/ui/gui/spokes/lib/passphrase.py:125 #: pyanaconda/ui/gui/spokes/lib/passphrase.glade:177 msgid "Weak" msgstr "" -#: pyanaconda/constants.py:146 pyanaconda/ui/gui/spokes/lib/passphrase.py:121 +#: pyanaconda/constants.py:148 pyanaconda/ui/gui/spokes/lib/passphrase.py:128 msgid "Fair" msgstr "" -#: pyanaconda/constants.py:146 pyanaconda/ui/gui/spokes/lib/passphrase.py:124 +#: pyanaconda/constants.py:148 pyanaconda/ui/gui/spokes/lib/passphrase.py:131 msgid "Good" msgstr "" -#: pyanaconda/constants.py:146 pyanaconda/ui/gui/spokes/lib/passphrase.py:127 +#: pyanaconda/constants.py:148 pyanaconda/ui/gui/spokes/lib/passphrase.py:134 msgid "Strong" msgstr "" #: pyanaconda/constants_text.py:40 pyanaconda/rescue.py:333 -#: pyanaconda/rescue.py:365 pyanaconda/rescue.py:378 pyanaconda/rescue.py:446 -#: pyanaconda/rescue.py:462 pyanaconda/rescue.py:473 anaconda:644 +#: pyanaconda/rescue.py:365 pyanaconda/rescue.py:433 pyanaconda/rescue.py:449 +#: pyanaconda/rescue.py:460 anaconda:513 msgid "OK" msgstr "" @@ -208,7 +209,7 @@ msgstr "" msgid "No" msgstr "" -#: pyanaconda/errors.py:104 +#: pyanaconda/errors.py:109 #, python-format msgid "" "The following errors occurred with your partitioning:\n" @@ -218,33 +219,25 @@ msgid "" "The installation will now terminate." msgstr "" -#: pyanaconda/errors.py:110 +#: pyanaconda/errors.py:115 #, python-format msgid "An error occurred while resizing the device %s." msgstr "" -#: pyanaconda/errors.py:119 +#: pyanaconda/errors.py:124 msgid "" "An error has occurred - no valid devices were found on which to create new " "file systems. Please check your hardware for the cause of this problem." msgstr "" -#: pyanaconda/errors.py:126 -#, python-format -msgid "" -"The following file systems for your Linux system were not unmounted " -"cleanly. Would you like to mount them anyway?\n" -"%s" -msgstr "" - -#: pyanaconda/errors.py:137 +#: pyanaconda/errors.py:133 msgid "" "There is an entry in your /etc/fstab file that contains an invalid or " -"incorrect filesystem type:\n" +"incorrect file system type:\n" "\n" msgstr "" -#: pyanaconda/errors.py:143 +#: pyanaconda/errors.py:139 #, python-format msgid "" "The ISO image %s has a size which is not a multiple of 2048 bytes. This may " @@ -255,7 +248,7 @@ msgid "" "using this image?" msgstr "" -#: pyanaconda/errors.py:157 +#: pyanaconda/errors.py:153 msgid "" "The installer has tried to mount the installation image, but cannot find it " "on the hard drive.\n" @@ -263,26 +256,26 @@ msgid "" "Should I try again to locate the image?" msgstr "" -#: pyanaconda/errors.py:167 +#: pyanaconda/errors.py:163 #, python-format msgid "An error occurred mounting the source device %s. Retry?" msgstr "" -#: pyanaconda/errors.py:175 +#: pyanaconda/errors.py:171 #, python-format msgid "" "An error occurred unmounting the disc. Please make sure you're not " "accessing %s from the shell on tty2 and then click OK to retry." msgstr "" -#: pyanaconda/errors.py:183 +#: pyanaconda/errors.py:179 #, python-format msgid "" "The group '%s' is required for this installation. This group does not exist. " "This is a fatal error and installation will be aborted." msgstr "" -#: pyanaconda/errors.py:189 +#: pyanaconda/errors.py:185 #, python-format msgid "" "You have specified that the group '%s' should be installed. This group does " @@ -290,7 +283,7 @@ msgid "" "installation?" msgstr "" -#: pyanaconda/errors.py:194 +#: pyanaconda/errors.py:190 #, python-format msgid "" "You have specified that the group '%s' should be excluded from " @@ -298,14 +291,14 @@ msgid "" "group and continue with installation?" msgstr "" -#: pyanaconda/errors.py:206 +#: pyanaconda/errors.py:202 #, python-format msgid "" "The package '%s' is required for this installation. This package does not " "exist. This is a fatal error and installation will be aborted." msgstr "" -#: pyanaconda/errors.py:213 +#: pyanaconda/errors.py:209 #, python-format msgid "" "You have specified that the package '%s' should be installed. This package " @@ -313,7 +306,7 @@ msgid "" "installation?" msgstr "" -#: pyanaconda/errors.py:224 +#: pyanaconda/errors.py:220 #, python-format msgid "" "There was an error running the kickstart script at line %(lineno)s. This is " @@ -323,27 +316,32 @@ msgid "" "%(details)s" msgstr "" -#: pyanaconda/errors.py:232 +#: pyanaconda/errors.py:228 msgid "" "The following error occurred while installing. This is a fatal error and " "installation will be aborted." msgstr "" -#: pyanaconda/errors.py:240 +#: pyanaconda/errors.py:236 msgid "" "The following software marked for installation has errors. This is likely " "caused by an error with\n" "your installation source." msgstr "" -#: pyanaconda/errors.py:248 +#: pyanaconda/errors.py:244 msgid "" -"The following error occurred while installing the bootloader. The system " +"The following error occurred while installing the boot loader. The system " "will not be bootable. Would you like to ignore this and continue with " "installation?" msgstr "" -#: pyanaconda/errors.py:261 +#: pyanaconda/errors.py:256 +#, python-format +msgid "Unable to encrypt password: unsupported algorithm %s" +msgstr "" + +#: pyanaconda/errors.py:263 #, python-format msgid "" "Installation was stopped due to an error installing the boot loader. The " @@ -354,7 +352,7 @@ msgid "" "The installer will now terminate." msgstr "" -#: pyanaconda/exception.py:96 +#: pyanaconda/exception.py:79 #, python-format msgid "" "The installation was stopped due to what seems to be a problem with your " @@ -365,16 +363,16 @@ msgid "" " The installer will now terminate." msgstr "" -#: pyanaconda/exception.py:100 +#: pyanaconda/exception.py:83 msgid "Hardware error occured" msgstr "" -#: pyanaconda/exception.py:134 +#: pyanaconda/exception.py:136 #, python-format msgid "" "\n" "The installation was stopped due to incomplete spokes detected while running " -"in non-interactive cmdline mode. Since there can not be any questions in " +"in non-interactive cmdline mode. Since there cannot be any questions in " "cmdline mode, edit your kickstart file and retry installation.\n" "The exact error message is: \n" "\n" @@ -383,76 +381,88 @@ msgid "" "The installer will now terminate." msgstr "" -#: pyanaconda/install.py:77 +#: pyanaconda/exception.py:144 +#, python-format +msgid "" +"\n" +"Running in cmdline mode, no interactive debugging allowed.\n" +"The exact error message is: \n" +"\n" +"%s.\n" +"\n" +"The installer will now terminate." +msgstr "" + +#: pyanaconda/install.py:80 msgid "Configuring installed system" msgstr "" -#: pyanaconda/install.py:90 +#: pyanaconda/install.py:93 msgid "Writing network configuration" msgstr "" -#: pyanaconda/install.py:94 +#: pyanaconda/install.py:97 msgid "Creating users" msgstr "" -#: pyanaconda/install.py:101 +#: pyanaconda/install.py:105 msgid "Configuring addons" msgstr "" -#: pyanaconda/install.py:104 +#: pyanaconda/install.py:108 msgid "Generating initramfs" msgstr "" -#: pyanaconda/install.py:108 +#: pyanaconda/install.py:119 #, python-format msgid "Joining realm: %s" msgstr "" -#: pyanaconda/install.py:111 +#: pyanaconda/install.py:122 msgid "Running post-installation scripts" msgstr "" -#: pyanaconda/install.py:165 +#: pyanaconda/install.py:170 #, python-format msgid "Waiting for %s threads to finish" msgstr "" -#: pyanaconda/install.py:171 +#: pyanaconda/install.py:176 msgid "Setting up the installation environment" msgstr "" -#: pyanaconda/install.py:203 +#: pyanaconda/install.py:207 msgid "Discovering realm to join" msgstr "" -#: pyanaconda/install.py:254 -msgid "Installing bootloader" +#: pyanaconda/install.py:265 +msgid "Installing boot loader" msgstr "" -#: pyanaconda/install.py:257 pyanaconda/packaging/livepayload.py:170 -#: pyanaconda/packaging/yumpayload.py:1390 -#: pyanaconda/packaging/dnfpayload.py:539 +#: pyanaconda/install.py:268 pyanaconda/packaging/livepayload.py:176 +#: pyanaconda/packaging/yumpayload.py:1371 +#: pyanaconda/packaging/dnfpayload.py:667 msgid "Performing post-installation setup tasks" msgstr "" -#: pyanaconda/iutil.py:541 +#: pyanaconda/iutil.py:811 msgid "No host url" msgstr "" -#: pyanaconda/iutil.py:562 +#: pyanaconda/iutil.py:832 msgid "malformed URL, cannot parse it." msgstr "" -#: pyanaconda/iutil.py:578 +#: pyanaconda/iutil.py:848 msgid "URL has no host component" msgstr "" -#: pyanaconda/kickstart.py:157 +#: pyanaconda/kickstart.py:164 #, python-format msgid "Escrow certificate %s requires the network." msgstr "" -#: pyanaconda/kickstart.py:165 +#: pyanaconda/kickstart.py:172 #, python-format msgid "" "The following error was encountered while downloading the escrow " @@ -461,280 +471,277 @@ msgid "" "%s" msgstr "" -#: pyanaconda/kickstart.py:274 +#: pyanaconda/kickstart.py:281 #, python-format msgid "%s is missing. Cannot setup authentication." msgstr "" -#: pyanaconda/kickstart.py:299 +#: pyanaconda/kickstart.py:306 #, python-format msgid "autopart fstype of %s is invalid." msgstr "" -#: pyanaconda/kickstart.py:316 +#: pyanaconda/kickstart.py:323 #, python-format msgid "Settings default fstype to %s failed." msgstr "" -#: pyanaconda/kickstart.py:347 +#: pyanaconda/kickstart.py:354 msgid "GRUB2 does not support installation to a partition." msgstr "" -#: pyanaconda/kickstart.py:402 +#: pyanaconda/kickstart.py:414 #, python-format msgid "More than one match found for given boot drive \"%s\"." msgstr "" -#: pyanaconda/kickstart.py:405 +#: pyanaconda/kickstart.py:417 #, python-format msgid "Requested boot drive \"%s\" doesn't exist or cannot be used." msgstr "" -#: pyanaconda/kickstart.py:447 +#: pyanaconda/kickstart.py:459 #, python-format msgid "" -"BTRFS partition \"%(device)s\" has a format of \"%(format)s\", but should " +"Btrfs partition \"%(device)s\" has a format of \"%(format)s\", but should " "have a format of \"btrfs\"." msgstr "" -#: pyanaconda/kickstart.py:452 +#: pyanaconda/kickstart.py:464 #, python-format -msgid "Tried to use undefined partition \"%s\" in BTRFS volume specification." +msgid "Tried to use undefined partition \"%s\" in Btrfs volume specification." msgstr "" -#: pyanaconda/kickstart.py:465 +#: pyanaconda/kickstart.py:477 msgid "" -"BTRFS volume defined without any member devices. Either specify member " +"Btrfs volume defined without any member devices. Either specify member " "devices or use --useexisting." msgstr "" -#: pyanaconda/kickstart.py:474 pyanaconda/kickstart.py:852 -#: pyanaconda/kickstart.py:1393 +#: pyanaconda/kickstart.py:486 pyanaconda/kickstart.py:859 +#: pyanaconda/kickstart.py:1438 #, python-format msgid "The mount point \"%s\" is not valid. It must start with a /." msgstr "" -#: pyanaconda/kickstart.py:489 +#: pyanaconda/kickstart.py:501 #, python-format -msgid "BTRFS volume \"%s\" specified with --useexisting does not exist." +msgid "Btrfs volume \"%s\" specified with --useexisting does not exist." msgstr "" -#: pyanaconda/kickstart.py:585 +#: pyanaconda/kickstart.py:587 #, python-format msgid "" "Disklabel \"%s\" given in clearpart command is not supported on this " "platform." msgstr "" -#: pyanaconda/kickstart.py:597 +#: pyanaconda/kickstart.py:599 #, python-format msgid "Disk \"%s\" given in clearpart command does not exist." msgstr "" -#: pyanaconda/kickstart.py:610 +#: pyanaconda/kickstart.py:612 #, python-format msgid "Device \"%s\" given in clearpart device list does not exist." msgstr "" -#: pyanaconda/kickstart.py:637 +#: pyanaconda/kickstart.py:639 #, python-format msgid "NIC \"%s\" given in fcoe command does not exist." msgstr "" -#: pyanaconda/kickstart.py:688 +#: pyanaconda/kickstart.py:690 #, python-format msgid "%s is missing. Cannot setup firewall." msgstr "" -#: pyanaconda/kickstart.py:736 pyanaconda/kickstart.py:747 +#: pyanaconda/kickstart.py:738 pyanaconda/kickstart.py:749 #, python-format msgid "Disk \"%s\" given in ignoredisk command does not exist." msgstr "" -#: pyanaconda/kickstart.py:760 +#: pyanaconda/kickstart.py:762 #, python-format msgid "" "Network interface \"%(nic)s\" required by iSCSI \"%(iscsiTarget)s\" target " "is not up." msgstr "" -#: pyanaconda/kickstart.py:770 +#: pyanaconda/kickstart.py:772 msgid "" "iscsi --iface must be specified (binding used) either for all targets or for " "none" msgstr "" -#: pyanaconda/kickstart.py:858 +#: pyanaconda/kickstart.py:865 #, python-format msgid "" "No volume group exists with the name \"%s\". Specify volume groups before " "logical volumes." msgstr "" -#: pyanaconda/kickstart.py:865 +#: pyanaconda/kickstart.py:872 #, python-format msgid "" "No thin pool exists with the name \"%s\". Specify thin pools before thin " "volumes." msgstr "" -#: pyanaconda/kickstart.py:873 +#: pyanaconda/kickstart.py:880 msgid "logvol --noformat must also use the --name= option." msgstr "" -#: pyanaconda/kickstart.py:878 pyanaconda/kickstart.py:946 +#: pyanaconda/kickstart.py:885 pyanaconda/kickstart.py:946 #, python-format msgid "Logical volume \"%s\" given in logvol command does not exist." msgstr "" -#: pyanaconda/kickstart.py:889 pyanaconda/kickstart.py:898 -#: pyanaconda/kickstart.py:956 pyanaconda/kickstart.py:1196 -#: pyanaconda/kickstart.py:1205 pyanaconda/kickstart.py:1286 +#: pyanaconda/kickstart.py:896 pyanaconda/kickstart.py:905 +#: pyanaconda/kickstart.py:956 pyanaconda/kickstart.py:1215 +#: pyanaconda/kickstart.py:1224 pyanaconda/kickstart.py:1306 #, python-format msgid "Target size \"%(size)s\" for device \"%(device)s\" is invalid." msgstr "" -#: pyanaconda/kickstart.py:912 +#: pyanaconda/kickstart.py:919 #, python-format msgid "" "Logical volume name \"%(logvol)s\" is already in use in volume group " "\"%(volgroup)s\"." msgstr "" -#: pyanaconda/kickstart.py:919 -#, python-format -msgid "" -"No size given for logical volume \"%s\". Use one of --useexisting, --size, " -"or --percent." -msgstr "" - -#: pyanaconda/kickstart.py:922 +#: pyanaconda/kickstart.py:924 #, python-format msgid "" "Logical volume size \"%(logvolSize)s\" must be larger than the volume group " "extent size of \"%(extentSize)s\"." msgstr "" -#: pyanaconda/kickstart.py:926 -msgid "Percentage must be between 0 and 100." -msgstr "" - -#: pyanaconda/kickstart.py:936 pyanaconda/kickstart.py:1223 -#: pyanaconda/kickstart.py:1443 +#: pyanaconda/kickstart.py:935 pyanaconda/kickstart.py:1242 +#: pyanaconda/kickstart.py:1488 #, python-format -msgid "The \"%s\" filesystem type is not supported." +msgid "The \"%s\" file system type is not supported." msgstr "" -#: pyanaconda/kickstart.py:1064 pyanaconda/kickstart.py:1068 +#: pyanaconda/kickstart.py:1029 +msgid "No passphrase given for encrypted LV" +msgstr "" + +#: pyanaconda/kickstart.py:1083 pyanaconda/kickstart.py:1087 #, python-format msgid "The %s kickstart command is not currently supported." msgstr "" -#: pyanaconda/kickstart.py:1093 +#: pyanaconda/kickstart.py:1112 #, python-format msgid "No disk found for specified BIOS disk \"%s\"." msgstr "" -#: pyanaconda/kickstart.py:1128 +#: pyanaconda/kickstart.py:1147 #, python-format msgid "RAID partition \"%s\" is defined multiple times." msgstr "" -#: pyanaconda/kickstart.py:1139 pyanaconda/kickstart.py:1368 +#: pyanaconda/kickstart.py:1158 pyanaconda/kickstart.py:1413 #, python-format msgid "PV partition \"%s\" is defined multiple times." msgstr "" -#: pyanaconda/kickstart.py:1150 pyanaconda/kickstart.py:1378 +#: pyanaconda/kickstart.py:1169 pyanaconda/kickstart.py:1423 #, python-format -msgid "BTRFS partition \"%s\" is defined multiple times." +msgid "Btrfs partition \"%s\" is defined multiple times." msgstr "" -#: pyanaconda/kickstart.py:1173 +#: pyanaconda/kickstart.py:1192 #, python-format msgid "The size \"%s\" is invalid." msgstr "" -#: pyanaconda/kickstart.py:1180 +#: pyanaconda/kickstart.py:1199 msgid "part --noformat must also use the --onpart option." msgstr "" -#: pyanaconda/kickstart.py:1185 pyanaconda/kickstart.py:1277 +#: pyanaconda/kickstart.py:1204 pyanaconda/kickstart.py:1297 #, python-format msgid "Partition \"%s\" given in part command does not exist." msgstr "" -#: pyanaconda/kickstart.py:1238 pyanaconda/kickstart.py:1252 +#: pyanaconda/kickstart.py:1257 pyanaconda/kickstart.py:1271 #, python-format msgid "Disk \"%s\" given in part command does not exist." msgstr "" -#: pyanaconda/kickstart.py:1241 +#: pyanaconda/kickstart.py:1260 #, python-format msgid "Cannot install to unpartitionable device \"%s\"." msgstr "" -#: pyanaconda/kickstart.py:1248 +#: pyanaconda/kickstart.py:1267 #, python-format msgid "Disk \"%s\" in part command is not partitioned." msgstr "" -#: pyanaconda/kickstart.py:1261 +#: pyanaconda/kickstart.py:1280 #, python-format msgid "The maximum size \"%s\" is invalid." msgstr "" -#: pyanaconda/kickstart.py:1400 +#: pyanaconda/kickstart.py:1350 +msgid "No passphrase given for encrypted part" +msgstr "" + +#: pyanaconda/kickstart.py:1445 msgid "raid --noformat must also use the --device option." msgstr "" -#: pyanaconda/kickstart.py:1405 +#: pyanaconda/kickstart.py:1450 #, python-format msgid "RAID device \"%s\" given in raid command does not exist." msgstr "" -#: pyanaconda/kickstart.py:1426 +#: pyanaconda/kickstart.py:1471 #, python-format msgid "" "RAID device \"%(device)s\" has a format of \"%(format)s\", but should have a " "format of \"mdmember\"." msgstr "" -#: pyanaconda/kickstart.py:1431 +#: pyanaconda/kickstart.py:1476 #, python-format msgid "Tried to use undefined partition \"%s\" in RAID specification." msgstr "" -#: pyanaconda/kickstart.py:1459 +#: pyanaconda/kickstart.py:1504 #, python-format msgid "RAID volume \"%s\" specified with --useexisting does not exist." msgstr "" -#: pyanaconda/kickstart.py:1466 +#: pyanaconda/kickstart.py:1511 #, python-format msgid "The RAID volume name \"%s\" is already in use." msgstr "" -#: pyanaconda/kickstart.py:1676 +#: pyanaconda/kickstart.py:1726 #, python-format msgid "" "Physical volume \"%(device)s\" has a format of \"%(format)s\", but should " "have a format of \"lvmpv\"." msgstr "" -#: pyanaconda/kickstart.py:1681 +#: pyanaconda/kickstart.py:1731 #, python-format msgid "Tried to use undefined partition \"%s\" in Volume Group specification" msgstr "" -#: pyanaconda/kickstart.py:1687 +#: pyanaconda/kickstart.py:1737 #, python-format msgid "" "Volume group \"%s\" defined without any physical volumes. Either specify " "physical volumes or use --useexisting." msgstr "" -#: pyanaconda/kickstart.py:1697 +#: pyanaconda/kickstart.py:1747 #, python-format msgid "" "Volume group given physical extent size of \"%(extentSize)s\", but must be " @@ -742,96 +749,101 @@ msgid "" "%(validExtentSizes)s." msgstr "" -#: pyanaconda/kickstart.py:1704 +#: pyanaconda/kickstart.py:1754 msgid "" "volgroup --noformat and volgroup --useexisting must also use the --name= " "option." msgstr "" -#: pyanaconda/kickstart.py:1709 +#: pyanaconda/kickstart.py:1759 #, python-format msgid "Volume group \"%s\" given in volgroup command does not exist." msgstr "" -#: pyanaconda/kickstart.py:1712 +#: pyanaconda/kickstart.py:1762 #, python-format msgid "The volume group name \"%s\" is already in use." msgstr "" -#: pyanaconda/kickstart.py:1765 +#: pyanaconda/kickstart.py:1818 msgid "" "The upgrade kickstart command is no longer supported. Upgrade functionality " "is provided through fedup." msgstr "" -#: pyanaconda/kickstart.py:1980 +#: pyanaconda/kickstart.py:2092 msgid "Running pre-installation scripts" msgstr "" -#: pyanaconda/network.py:103 -msgid "Hostname cannot be None or an empty string." +#: pyanaconda/network.py:98 +msgid "Host name cannot be None or an empty string." msgstr "" -#: pyanaconda/network.py:106 -msgid "Hostname must be 255 or fewer characters in length." +#: pyanaconda/network.py:101 +msgid "Host name must be 255 or fewer characters in length." msgstr "" -#: pyanaconda/network.py:109 +#: pyanaconda/network.py:104 msgid "" -"Hostnames can only contain the characters 'a-z', 'A-Z', '0-9', '-', or '.', " +"Host names can only contain the characters 'a-z', 'A-Z', '0-9', '-', or '.', " "parts between periods must contain something and cannot start or end with " "'-'." msgstr "" -#: pyanaconda/network.py:1265 pyanaconda/ui/gui/spokes/custom.py:518 -#: pyanaconda/ui/gui/spokes/network.py:297 +#: pyanaconda/network.py:1325 pyanaconda/ui/gui/spokes/custom.py:545 +#: pyanaconda/ui/gui/spokes/network.py:302 #: pyanaconda/ui/gui/spokes/lib/accordion.py:61 msgid "Unknown" msgstr "" -#: pyanaconda/network.py:1269 +#: pyanaconda/network.py:1329 msgid "Connecting..." msgstr "" -#: pyanaconda/network.py:1271 +#: pyanaconda/network.py:1331 msgid "Disconnecting..." msgstr "" -#: pyanaconda/network.py:1291 +#: pyanaconda/network.py:1351 #, python-format msgid "Wired (%(interface_name)s) connected" msgstr "" -#: pyanaconda/network.py:1294 +#: pyanaconda/network.py:1354 #, python-format msgid "Wireless connected to %(access_point)s" msgstr "" -#: pyanaconda/network.py:1297 +#: pyanaconda/network.py:1357 #, python-format msgid "Bond %(interface_name)s (%(list_of_slaves)s) connected" msgstr "" -#: pyanaconda/network.py:1301 +#: pyanaconda/network.py:1361 #, python-format msgid "Team%(interface_name)s (%(list_of_slaves)s) connected" msgstr "" -#: pyanaconda/network.py:1307 +#: pyanaconda/network.py:1365 #, python-format -msgid "Vlan %(interface_name)s (%(parent_device)s, ID %(vlanid)s) connected" +msgid "Bridge%(interface_name)s (%(list_of_slaves)s) connected" msgstr "" -#: pyanaconda/network.py:1322 +#: pyanaconda/network.py:1371 +#, python-format +msgid "VLAN %(interface_name)s (%(parent_device)s, ID %(vlanid)s) connected" +msgstr "" + +#: pyanaconda/network.py:1388 #, python-format msgid "Connected: %(list_of_interface_names)s" msgstr "" -#: pyanaconda/network.py:1325 +#: pyanaconda/network.py:1391 msgid "Not connected" msgstr "" -#: pyanaconda/network.py:1328 pyanaconda/ui/gui/spokes/network.glade:2099 +#: pyanaconda/network.py:1394 pyanaconda/ui/gui/spokes/network.glade:2097 msgid "No network devices available" msgstr "" @@ -845,12 +857,12 @@ msgstr "" msgid "New %(name)s %(version)s Installation" msgstr "" -#: pyanaconda/rescue.py:183 pyanaconda/rescue.py:350 pyanaconda/rescue.py:439 +#: pyanaconda/rescue.py:184 pyanaconda/rescue.py:350 pyanaconda/rescue.py:426 #, python-format msgid "Run %s to unmount the system when you are finished." msgstr "" -#: pyanaconda/rescue.py:186 +#: pyanaconda/rescue.py:187 msgid "When finished please exit from the shell and your system will reboot." msgstr "" @@ -858,8 +870,8 @@ msgstr "" msgid "Unable to find /bin/sh to execute! Not starting shell" msgstr "" -#: pyanaconda/rescue.py:288 pyanaconda/rescue.py:361 pyanaconda/rescue.py:371 -#: pyanaconda/rescue.py:442 pyanaconda/rescue.py:473 +#: pyanaconda/rescue.py:288 pyanaconda/rescue.py:358 pyanaconda/rescue.py:429 +#: pyanaconda/rescue.py:460 msgid "Rescue" msgstr "" @@ -901,18 +913,11 @@ msgstr "" msgid "Exit" msgstr "" -#: pyanaconda/rescue.py:347 pyanaconda/rescue.py:436 +#: pyanaconda/rescue.py:347 pyanaconda/rescue.py:423 msgid "The system will reboot automatically when you exit from the shell." msgstr "" -#: pyanaconda/rescue.py:362 -#, python-format -msgid "" -"Your system had dirty file systems which you chose not to mount. Press " -"return to get a shell from which you can fsck and mount your partitions. %s" -msgstr "" - -#: pyanaconda/rescue.py:372 +#: pyanaconda/rescue.py:359 #, python-format msgid "" "Your system has been mounted under %(rootPath)s.\n" @@ -925,7 +930,7 @@ msgid "" "%(msg)s" msgstr "" -#: pyanaconda/rescue.py:443 +#: pyanaconda/rescue.py:430 #, python-format msgid "" "An error occurred trying to mount some or all of your system. Some of it may " @@ -934,24 +939,24 @@ msgid "" "Press <return> to get a shell." msgstr "" -#: pyanaconda/rescue.py:451 +#: pyanaconda/rescue.py:438 msgid "You don't have any Linux partitions. Rebooting.\n" msgstr "" -#: pyanaconda/rescue.py:455 +#: pyanaconda/rescue.py:442 msgid " The system will reboot automatically when you exit from the shell." msgstr "" -#: pyanaconda/rescue.py:459 +#: pyanaconda/rescue.py:446 msgid "Rescue Mode" msgstr "" -#: pyanaconda/rescue.py:460 +#: pyanaconda/rescue.py:447 #, python-format msgid "You don't have any Linux partitions. Press return to get a shell.%s" msgstr "" -#: pyanaconda/rescue.py:472 +#: pyanaconda/rescue.py:459 #, python-format msgid "Your system is mounted under the %s directory." msgstr "" @@ -976,12 +981,12 @@ msgid "Standard Partition" msgstr "" #: pyanaconda/storage_utils.py:54 pyanaconda/storage_utils.py:81 -msgid "BTRFS" +#: pyanaconda/ui/gui/spokes/custom.glade:27 +msgid "Btrfs" msgstr "" #: pyanaconda/storage_utils.py:55 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:222 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:715 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:713 #: pyanaconda/ui/gui/spokes/lib/resize.glade:152 msgid "Disk" msgstr "" @@ -1019,66 +1024,66 @@ msgstr "" #: pyanaconda/storage_utils.py:76 msgid "" "The PReP boot partition is required as part of the\n" -"bootloader configuration on some PPC platforms." +"boot loader configuration on some PPC platforms." msgstr "" -#: pyanaconda/storage_utils.py:180 +#: pyanaconda/storage_utils.py:184 #, python-format msgid "" "Your root partition is less than 250 megabytes which is usually too small to " "install %s." msgstr "" -#: pyanaconda/storage_utils.py:185 +#: pyanaconda/storage_utils.py:189 #, python-format msgid "" "You have not defined a root partition (/), which is required for " "installation of %s to continue." msgstr "" -#: pyanaconda/storage_utils.py:199 +#: pyanaconda/storage_utils.py:203 msgid "" "This platform requires /boot on a dedicated partition or logical volume. If " "you do not want a /boot volume, you must place / on a dedicated non-LVM " "partition." msgstr "" -#: pyanaconda/storage_utils.py:209 +#: pyanaconda/storage_utils.py:213 #, python-format msgid "" "Your %(mount)s partition is less than %(size)s which is lower than " "recommended for a normal %(productName)s install." msgstr "" -#: pyanaconda/storage_utils.py:219 +#: pyanaconda/storage_utils.py:223 #, python-format msgid "" "Your %(mount)s partition is too small for %(format)s formatting (allowable " "size is %(minSize)s to %(maxSize)s)" msgstr "" -#: pyanaconda/storage_utils.py:225 +#: pyanaconda/storage_utils.py:229 #, python-format msgid "" "Your %(mount)s partition is too large for %(format)s formatting (allowable " "size is %(minSize)s to %(maxSize)s)" msgstr "" -#: pyanaconda/storage_utils.py:234 -msgid "No valid bootloader target device found. See below for details." +#: pyanaconda/storage_utils.py:238 +msgid "No valid boot loader target device found. See below for details." msgstr "" -#: pyanaconda/storage_utils.py:246 +#: pyanaconda/storage_utils.py:250 msgid "You have not created a bootable partition." msgstr "" -#: pyanaconda/storage_utils.py:268 +#: pyanaconda/storage_utils.py:272 msgid "" "Your BIOS-based system needs a special partition to boot from a GPT disk " "label. To continue, please create a 1MiB 'biosboot' type partition." msgstr "" -#: pyanaconda/storage_utils.py:279 +#: pyanaconda/storage_utils.py:283 #, python-format msgid "" "You have not specified a swap partition. %(requiredMem)s of memory is " @@ -1086,13 +1091,13 @@ msgid "" "have %(installedMem)s." msgstr "" -#: pyanaconda/storage_utils.py:286 +#: pyanaconda/storage_utils.py:290 msgid "" "You have not specified a swap partition. Although not strictly required in " "all cases, it will significantly improve performance for most installations." msgstr "" -#: pyanaconda/storage_utils.py:293 +#: pyanaconda/storage_utils.py:297 msgid "" "At least one of your swap devices does not have a UUID, which is common in " "swap space created using older versions of mkswap. These devices will be " @@ -1100,20 +1105,22 @@ msgid "" "paths can change under a variety of circumstances. " msgstr "" -#: pyanaconda/storage_utils.py:304 +#: pyanaconda/storage_utils.py:308 #, python-format msgid "" "This mount point is invalid. The %s directory must be on the / file system." msgstr "" -#: pyanaconda/storage_utils.py:309 +#: pyanaconda/storage_utils.py:313 #, python-format msgid "The mount point %s must be on a linux file system." msgstr "" -#: pyanaconda/storage_utils.py:337 +#: pyanaconda/storage_utils.py:341 #, python-format -msgid "LUKS device %s has no encryption key" +msgid "" +"Encryption requested for LUKS device %s but no encryption key specified for " +"this device." msgstr "" #: pyanaconda/text.py:99 @@ -1127,42 +1134,46 @@ msgstr "" msgid "Passphrase" msgstr "" -#: pyanaconda/vnc.py:75 +#: pyanaconda/vnc.py:74 #, python-format msgid "%(productName)s %(productVersion)s installation" msgstr "" -#: pyanaconda/vnc.py:132 +#: pyanaconda/vnc.py:135 #, python-format msgid "%(productName)s %(productVersion)s installation on host %(name)s" msgstr "" -#: pyanaconda/vnc.py:151 +#: pyanaconda/vnc.py:154 #, python-format msgid "Attempting to connect to vnc client on host %s..." msgstr "" -#: pyanaconda/vnc.py:165 +#: pyanaconda/vnc.py:168 msgid "Connected!" msgstr "" -#: pyanaconda/vnc.py:168 +#: pyanaconda/vnc.py:171 msgid "Will try to connect again in 15 seconds..." msgstr "" -#: pyanaconda/vnc.py:174 +#: pyanaconda/vnc.py:178 #, python-format msgid "Giving up attempting to connect after %d try!\n" msgid_plural "Giving up attempting to connect after %d tries!\n" msgstr[0] "" msgstr[1] "" -#: pyanaconda/vnc.py:185 +#: pyanaconda/vnc.py:186 +msgid "Attempting to start vncconfig" +msgstr "" + +#: pyanaconda/vnc.py:199 #, python-format msgid "Please manually connect your vnc client to %s to begin the install." msgstr "" -#: pyanaconda/vnc.py:187 +#: pyanaconda/vnc.py:201 #, python-format msgid "" "Please manually connect your vnc client to <IP ADDRESS>:%s to begin the " @@ -1170,15 +1181,15 @@ msgid "" "ADDRESS>." msgstr "" -#: pyanaconda/vnc.py:192 +#: pyanaconda/vnc.py:206 msgid "Starting VNC..." msgstr "" -#: pyanaconda/vnc.py:234 +#: pyanaconda/vnc.py:243 msgid "The VNC server is now running." msgstr "" -#: pyanaconda/vnc.py:238 +#: pyanaconda/vnc.py:247 msgid "" "\n" "\n" @@ -1189,7 +1200,7 @@ msgid "" "\n" msgstr "" -#: pyanaconda/vnc.py:243 +#: pyanaconda/vnc.py:252 msgid "" "\n" "\n" @@ -1199,7 +1210,7 @@ msgid "" "\n" msgstr "" -#: pyanaconda/vnc.py:247 +#: pyanaconda/vnc.py:256 msgid "" "\n" "\n" @@ -1207,7 +1218,7 @@ msgid "" "\n" msgstr "" -#: pyanaconda/vnc.py:249 +#: pyanaconda/vnc.py:258 msgid "" "\n" "\n" @@ -1215,63 +1226,55 @@ msgid "" "\n" msgstr "" -#: pyanaconda/vnc.py:269 +#: pyanaconda/vnc.py:280 msgid "" "VNC password must be six to eight characters long.\n" "Please enter a new one, or leave blank for no password." msgstr "" -#: pyanaconda/installclasses/fedora.py:29 -msgid "_Fedora" -msgstr "" - -#: pyanaconda/installclasses/rhel.py:29 -msgid "Red Hat Enterprise Linux" -msgstr "" - -#: pyanaconda/packaging/__init__.py:1130 +#: pyanaconda/packaging/__init__.py:1113 msgid "Failed to set up installation source" msgstr "" -#: pyanaconda/packaging/__init__.py:1131 +#: pyanaconda/packaging/__init__.py:1114 msgid "Error downloading package metadata" msgstr "" -#: pyanaconda/packaging/__init__.py:1132 +#: pyanaconda/packaging/__init__.py:1115 msgid "No installation source available" msgstr "" -#: pyanaconda/packaging/livepayload.py:98 -#: pyanaconda/packaging/livepayload.py:119 +#: pyanaconda/packaging/livepayload.py:104 +#: pyanaconda/packaging/livepayload.py:125 msgid "Installing software" msgstr "" -#: pyanaconda/packaging/livepayload.py:220 -#: pyanaconda/packaging/livepayload.py:229 +#: pyanaconda/packaging/livepayload.py:237 +#: pyanaconda/packaging/livepayload.py:246 #, python-format msgid "Downloading %(url)s (%(pct)d%%)" msgstr "" -#: pyanaconda/packaging/livepayload.py:358 +#: pyanaconda/packaging/livepayload.py:375 msgid "Checking image checksum" msgstr "" -#: pyanaconda/packaging/yumpayload.py:1207 +#: pyanaconda/packaging/yumpayload.py:1186 #, python-format msgid "CmdlineError: Missing package: %s" msgstr "" -#: pyanaconda/packaging/yumpayload.py:1352 -#: pyanaconda/packaging/dnfpayload.py:493 +#: pyanaconda/packaging/yumpayload.py:1331 +#: pyanaconda/packaging/dnfpayload.py:621 msgid "Starting package installation process" msgstr "" -#: pyanaconda/packaging/yumpayload.py:1388 -#: pyanaconda/packaging/dnfpayload.py:522 +#: pyanaconda/packaging/yumpayload.py:1369 +#: pyanaconda/packaging/dnfpayload.py:650 msgid "Preparing transaction from installation source" msgstr "" -#: pyanaconda/packaging/yumpayload.py:1389 +#: pyanaconda/packaging/yumpayload.py:1370 msgid "Installing" msgstr "" @@ -1282,40 +1285,40 @@ msgid "" "(%(percent)d%%) done." msgstr "" -#: pyanaconda/packaging/dnfpayload.py:510 +#: pyanaconda/packaging/dnfpayload.py:638 msgid "Downloading packages" msgstr "" -#: pyanaconda/packaging/dnfpayload.py:532 +#: pyanaconda/packaging/dnfpayload.py:660 #, python-format msgid "Installing %s" msgstr "" -#: pyanaconda/packaging/rpmostreepayload.py:115 +#: pyanaconda/packaging/rpmostreepayload.py:163 #, python-format msgid "Starting pull of %(branchName)s from %(source)s" msgstr "" -#: pyanaconda/packaging/rpmostreepayload.py:122 +#: pyanaconda/packaging/rpmostreepayload.py:179 #, python-format msgid "Preparing deployment of %s" msgstr "" -#: pyanaconda/packaging/rpmostreepayload.py:134 +#: pyanaconda/packaging/rpmostreepayload.py:199 #, python-format msgid "Deployment starting: %s" msgstr "" -#: pyanaconda/packaging/rpmostreepayload.py:137 +#: pyanaconda/packaging/rpmostreepayload.py:202 #, python-format msgid "Deployment complete: %s" msgstr "" -#: pyanaconda/ui/helpers.py:94 pyanaconda/ui/tui/spokes/storage.py:387 +#: pyanaconda/ui/helpers.py:94 pyanaconda/ui/tui/spokes/storage.py:389 msgid "Checking storage configuration..." msgstr "" -#: pyanaconda/ui/categories/__init__.py:49 +#: pyanaconda/ui/categories/__init__.py:48 msgid "DEFAULT TITLE" msgstr "" @@ -1343,7 +1346,7 @@ msgstr "" #: pyanaconda/ui/lib/space.py:37 #, python-format msgid "" -"Not enough space in filesystems for the current software selection. An " +"Not enough space in file systems for the current software selection. An " "additional %s is needed." msgstr "" @@ -1399,11 +1402,11 @@ msgstr "" msgid "Starting automated install" msgstr "" -#: pyanaconda/ui/tui/hubs/summary.py:82 +#: pyanaconda/ui/tui/hubs/summary.py:85 msgid "The following mandatory spokes are not completed:" msgstr "" -#: pyanaconda/ui/tui/hubs/summary.py:90 +#: pyanaconda/ui/tui/hubs/summary.py:93 msgid "" " Please make your choice from above ['q' to quit | 'b' to begin " "installation |\n" @@ -1411,19 +1414,22 @@ msgid "" msgstr "" #. TRANSLATORS: 'b' to begin installation -#. TRANSLATORS: 'b' to go back -#: pyanaconda/ui/tui/hubs/summary.py:104 -#: pyanaconda/ui/tui/spokes/langsupport.py:117 +#: pyanaconda/ui/tui/hubs/summary.py:107 +msgctxt "TUI|Spoke Navigation" msgid "b" msgstr "" -#: pyanaconda/ui/tui/hubs/summary.py:107 +#: pyanaconda/ui/tui/hubs/summary.py:110 #: pyanaconda/ui/tui/hubs/__init__.py:114 msgid "Please complete all spokes before continuing" msgstr "" #. TRANSLATORS: 'c' to continue -#: pyanaconda/ui/tui/hubs/summary.py:118 +#: pyanaconda/ui/tui/hubs/summary.py:121 +#: pyanaconda/ui/tui/hubs/__init__.py:111 +#: pyanaconda/ui/tui/simpleline/base.py:456 +#: pyanaconda/ui/tui/spokes/software.py:189 +msgctxt "TUI|Spoke Navigation" msgid "c" msgstr "" @@ -1431,13 +1437,6 @@ msgstr "" msgid "Default HUB title" msgstr "" -#. TRANSLATORS: 'c' to continue -#: pyanaconda/ui/tui/hubs/__init__.py:111 -#: pyanaconda/ui/tui/simpleline/base.py:456 -msgctxt "TUI|Spoke Navigation" -msgid "c" -msgstr "" - #: pyanaconda/ui/tui/tuiobject.py:30 pyanaconda/ui/gui/main.glade:72 msgid "Error" msgstr "" @@ -1478,78 +1477,85 @@ msgctxt "TUI|Spoke Navigation" msgid "q" msgstr "" -#: pyanaconda/ui/tui/simpleline/base.py:569 +#: pyanaconda/ui/tui/simpleline/base.py:571 msgid "Press ENTER to continue" msgstr "" -#: pyanaconda/ui/tui/simpleline/base.py:619 +#: pyanaconda/ui/tui/simpleline/base.py:621 msgid "" " Please make your choice from above ['q' to quit | 'c' to continue |\n" " 'r' to refresh]: " msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:39 +#: pyanaconda/ui/tui/spokes/askvnc.py:42 msgid "VNC" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:55 +#: pyanaconda/ui/tui/spokes/askvnc.py:58 msgid "" "X was unable to start on your machine. Would you like to start VNC to " "connect to this computer from another computer and perform a graphical " "installation or continue with a text mode installation?" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:103 +#: pyanaconda/ui/tui/spokes/askvnc.py:106 msgid "q" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:115 +#: pyanaconda/ui/tui/spokes/askvnc.py:122 msgid "VNC Password" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:123 +#: pyanaconda/ui/tui/spokes/askvnc.py:130 msgid "" "Please provide VNC password (must be six to eight characters long).\n" "You will have to type it twice. Leave blank for no password" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:142 +#: pyanaconda/ui/tui/spokes/askvnc.py:149 +#: pyanaconda/ui/gui/spokes/network.py:1319 msgid "Password: " msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:143 +#: pyanaconda/ui/tui/spokes/askvnc.py:150 msgid "Password (confirm): " msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:146 +#: pyanaconda/ui/tui/spokes/askvnc.py:153 msgid "Passwords do not match!" msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:149 +#: pyanaconda/ui/tui/spokes/askvnc.py:156 msgid "The password must be at least six characters long." msgstr "" -#: pyanaconda/ui/tui/spokes/askvnc.py:153 +#: pyanaconda/ui/tui/spokes/askvnc.py:160 msgid "The password cannot be more than eight characters long." msgstr "" -#: pyanaconda/ui/tui/spokes/langsupport.py:40 +#: pyanaconda/ui/tui/spokes/langsupport.py:38 msgid "Language settings" msgstr "" -#: pyanaconda/ui/tui/spokes/langsupport.py:70 +#: pyanaconda/ui/tui/spokes/langsupport.py:65 msgid "Language is not set." msgstr "" -#: pyanaconda/ui/tui/spokes/langsupport.py:80 +#: pyanaconda/ui/tui/spokes/langsupport.py:75 msgid "Available locales" msgstr "" -#: pyanaconda/ui/tui/spokes/langsupport.py:83 +#: pyanaconda/ui/tui/spokes/langsupport.py:78 msgid "Available languages" msgstr "" -#: pyanaconda/ui/tui/spokes/langsupport.py:125 +#. TRANSLATORS: 'b' to go back +#: pyanaconda/ui/tui/spokes/langsupport.py:112 +msgctxt "TUI|Spoke Navigation|Language Support" +msgid "b" +msgstr "" + +#: pyanaconda/ui/tui/spokes/langsupport.py:120 msgid "" "Please select language support to install.\n" "[b to language list, c to continue, q to quit]: " @@ -1586,11 +1592,11 @@ msgstr "" #: pyanaconda/ui/tui/spokes/network.py:140 #, python-format -msgid "Hostname: %s\n" +msgid "Host Name: %s\n" msgstr "" #: pyanaconda/ui/tui/spokes/network.py:152 -msgid "Set hostname" +msgid "Set host name" msgstr "" #: pyanaconda/ui/tui/spokes/network.py:154 @@ -1599,9 +1605,9 @@ msgid "Configure device %s" msgstr "" #: pyanaconda/ui/tui/spokes/network.py:173 -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:225 -#: pyanaconda/ui/gui/spokes/network.glade:2236 -msgid "Hostname" +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:241 +#: pyanaconda/ui/gui/spokes/network.glade:2233 +msgid "Host Name" msgstr "" #: pyanaconda/ui/tui/spokes/network.py:191 @@ -1614,10 +1620,10 @@ msgid "Can't apply configuration, device activation failed." msgstr "" #: pyanaconda/ui/tui/spokes/network.py:234 -#: pyanaconda/ui/gui/spokes/network.py:1378 -#: pyanaconda/ui/gui/spokes/network.py:1448 +#: pyanaconda/ui/gui/spokes/network.py:1466 +#: pyanaconda/ui/gui/spokes/network.py:1535 #, python-format -msgid "Hostname is not valid: %s" +msgid "Host name is not valid: %s" msgstr "" #: pyanaconda/ui/tui/spokes/network.py:258 @@ -1687,7 +1693,7 @@ msgstr "" #: pyanaconda/ui/tui/spokes/password.py:70 #: pyanaconda/ui/gui/spokes/password.glade:86 -#: pyanaconda/ui/gui/spokes/user.glade:172 +#: pyanaconda/ui/gui/spokes/user.glade:170 msgid "Password" msgstr "" @@ -1707,63 +1713,68 @@ msgstr "" msgid "Installation Destination" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:121 -#: pyanaconda/ui/gui/spokes/storage.py:360 +#: pyanaconda/ui/tui/spokes/storage.py:123 +#: pyanaconda/ui/gui/spokes/storage.py:380 msgid "No disks selected" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:131 -#: pyanaconda/ui/gui/spokes/storage.py:372 -msgid "Error checking storage configuration" +#: pyanaconda/ui/tui/spokes/storage.py:126 +#: pyanaconda/ui/gui/spokes/storage.py:383 +msgid "Kickstart insufficient" msgstr "" #: pyanaconda/ui/tui/spokes/storage.py:133 -#: pyanaconda/ui/gui/spokes/storage.py:374 +#: pyanaconda/ui/gui/spokes/storage.py:392 +msgid "Error checking storage configuration" +msgstr "" + +#: pyanaconda/ui/tui/spokes/storage.py:135 +#: pyanaconda/ui/gui/spokes/storage.py:394 msgid "Warning checking storage configuration" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:136 -#: pyanaconda/ui/gui/spokes/storage.py:376 +#: pyanaconda/ui/tui/spokes/storage.py:138 +#: pyanaconda/ui/gui/spokes/storage.py:396 msgid "Automatic partitioning selected" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:138 -#: pyanaconda/ui/gui/spokes/storage.py:378 +#: pyanaconda/ui/tui/spokes/storage.py:140 +#: pyanaconda/ui/gui/spokes/storage.py:398 msgid "Custom partitioning selected" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:174 -#: pyanaconda/ui/gui/spokes/storage.py:602 +#: pyanaconda/ui/tui/spokes/storage.py:176 +#: pyanaconda/ui/gui/spokes/storage.py:649 msgid "" "No disks detected. Please shut down the computer, connect at least one " "disk, and restart to complete installation." msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:176 -#: pyanaconda/ui/gui/spokes/storage.py:604 +#: pyanaconda/ui/tui/spokes/storage.py:178 +#: pyanaconda/ui/gui/spokes/storage.py:651 msgid "No disks selected; please select at least one disk to install to." msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:191 -#: pyanaconda/ui/tui/spokes/source.py:364 -#: pyanaconda/ui/gui/spokes/source.py:652 -#: pyanaconda/ui/gui/spokes/storage.py:553 +#: pyanaconda/ui/tui/spokes/storage.py:193 +#: pyanaconda/ui/tui/spokes/source.py:366 +#: pyanaconda/ui/gui/spokes/source.py:657 +#: pyanaconda/ui/gui/spokes/storage.py:600 msgid "Probing storage..." msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:212 +#: pyanaconda/ui/tui/spokes/storage.py:214 msgid "Select all" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:305 +#: pyanaconda/ui/tui/spokes/storage.py:307 msgid "" "The following unformatted DASDs have been detected on your system. You can " "choose to format them now with dasdfmt or cancel to leave them unformatted. " -"Unformatted DASDs can not be used during installation.\n" +"Unformatted DASDs cannot be used during installation.\n" "\n" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:307 +#: pyanaconda/ui/tui/spokes/storage.py:309 msgid "" "Warning: All storage changes made using the installer will be lost when you " "choose to format.\n" @@ -1771,37 +1782,37 @@ msgid "" "Proceed to run dasdfmt?\n" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:322 +#: pyanaconda/ui/tui/spokes/storage.py:324 #: pyanaconda/ui/gui/spokes/lib/dasdfmt.py:92 #, python-format msgid "Formatting /dev/%s. This may take a moment." msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:366 +#: pyanaconda/ui/tui/spokes/storage.py:368 msgid "Generating updated storage configuration" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:371 -#: pyanaconda/ui/tui/spokes/storage.py:383 +#: pyanaconda/ui/tui/spokes/storage.py:373 +#: pyanaconda/ui/tui/spokes/storage.py:385 #, python-format msgid "storage configuration failed: %s" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:430 +#: pyanaconda/ui/tui/spokes/storage.py:432 msgid "Autopartitioning Options" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:456 +#: pyanaconda/ui/tui/spokes/storage.py:458 msgid "" "Installation requires partitioning of your hard drive. Select what space to " "use for the install target." msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:495 +#: pyanaconda/ui/tui/spokes/storage.py:497 msgid "Partition Scheme Options" msgstr "" -#: pyanaconda/ui/tui/spokes/storage.py:519 +#: pyanaconda/ui/tui/spokes/storage.py:521 msgid "Select a partition scheme configuration." msgstr "" @@ -1809,41 +1820,41 @@ msgstr "" msgid "Timezone settings" msgstr "" -#: pyanaconda/ui/tui/spokes/time_spoke.py:62 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:521 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:527 +#: pyanaconda/ui/tui/spokes/time_spoke.py:61 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:533 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:539 #, python-format msgid "%s timezone" msgstr "" -#: pyanaconda/ui/tui/spokes/time_spoke.py:64 +#: pyanaconda/ui/tui/spokes/time_spoke.py:63 msgid "Timezone is not set." msgstr "" -#: pyanaconda/ui/tui/spokes/time_spoke.py:72 +#: pyanaconda/ui/tui/spokes/time_spoke.py:71 #, python-format msgid "Available timezones in region %s" msgstr "" -#: pyanaconda/ui/tui/spokes/time_spoke.py:75 +#: pyanaconda/ui/tui/spokes/time_spoke.py:74 msgid "Available regions" msgstr "" -#: pyanaconda/ui/tui/spokes/time_spoke.py:134 +#: pyanaconda/ui/tui/spokes/time_spoke.py:133 msgid "" "Please select the timezone.\n" "Use numbers or type names directly [b to region list, q to quit]: " msgstr "" -#: pyanaconda/ui/tui/spokes/shell_spoke.py:34 +#: pyanaconda/ui/tui/spokes/shell_spoke.py:33 msgid "Shell" msgstr "" -#: pyanaconda/ui/tui/spokes/shell_spoke.py:49 +#: pyanaconda/ui/tui/spokes/shell_spoke.py:48 msgid "Start shell" msgstr "" -#: pyanaconda/ui/tui/spokes/shell_spoke.py:58 +#: pyanaconda/ui/tui/spokes/shell_spoke.py:57 msgid "Exit the shell to continue" msgstr "" @@ -1854,7 +1865,7 @@ msgstr "" #: pyanaconda/ui/tui/spokes/source.py:57 #: pyanaconda/ui/tui/spokes/source.py:106 #: pyanaconda/ui/gui/spokes/source.py:547 -#: pyanaconda/ui/gui/spokes/source.glade:759 +#: pyanaconda/ui/gui/spokes/source.glade:757 msgid "Closest mirror" msgstr "" @@ -1876,106 +1887,102 @@ msgid "Error setting up software source" msgstr "" #: pyanaconda/ui/tui/spokes/source.py:108 -#: pyanaconda/ui/tui/spokes/software.py:87 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:529 -#: pyanaconda/ui/gui/spokes/software.py:216 +#: pyanaconda/ui/tui/spokes/software.py:121 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:541 +#: pyanaconda/ui/gui/spokes/software.py:224 #: pyanaconda/ui/gui/spokes/source.py:549 msgid "Nothing selected" msgstr "" #: pyanaconda/ui/tui/spokes/source.py:119 -#: pyanaconda/ui/tui/spokes/software.py:67 +#: pyanaconda/ui/tui/spokes/software.py:109 msgid "Processing..." msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:125 -#, python-format -msgid "Repo URL set to: %s" -msgstr "" - -#: pyanaconda/ui/tui/spokes/source.py:144 +#: pyanaconda/ui/tui/spokes/source.py:135 msgid "CD/DVD" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:144 +#: pyanaconda/ui/tui/spokes/source.py:135 msgid "local ISO file" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:144 +#: pyanaconda/ui/tui/spokes/source.py:135 msgid "Network" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:148 +#: pyanaconda/ui/tui/spokes/source.py:139 +#: pyanaconda/ui/gui/spokes/source.py:850 +msgid "" +"The installation source is in use by the installer and cannot be changed." +msgstr "" + +#: pyanaconda/ui/tui/spokes/source.py:146 msgid "Choose an installation source type." msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:241 -#: pyanaconda/ui/tui/spokes/source.py:279 +#: pyanaconda/ui/tui/spokes/source.py:243 +#: pyanaconda/ui/tui/spokes/source.py:281 msgid "Specify Repo Options" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:245 +#: pyanaconda/ui/tui/spokes/source.py:247 msgid "Repo URL" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:283 +#: pyanaconda/ui/tui/spokes/source.py:285 msgid "<server>:/<path>" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:284 +#: pyanaconda/ui/tui/spokes/source.py:286 msgid "NFS mount options" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:318 -msgid "Failed to set up installation source. Check the source address." -msgstr "" - -#: pyanaconda/ui/tui/spokes/source.py:326 +#: pyanaconda/ui/tui/spokes/source.py:328 msgid "Select device containing the ISO file" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:385 +#: pyanaconda/ui/tui/spokes/source.py:387 msgid "No mountable devices found" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:413 +#: pyanaconda/ui/tui/spokes/source.py:415 msgid "Select an ISO to use as install source" msgstr "" -#: pyanaconda/ui/tui/spokes/source.py:442 +#: pyanaconda/ui/tui/spokes/source.py:444 msgid "No *.iso files found in device root folder" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:39 +#: pyanaconda/ui/tui/spokes/software.py:40 msgid "Software selection" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:65 -#: pyanaconda/ui/gui/spokes/software.py:200 +#: pyanaconda/ui/tui/spokes/software.py:107 +#: pyanaconda/ui/gui/spokes/software.py:208 msgid "Error checking software selection" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:69 -#: pyanaconda/ui/gui/spokes/software.py:203 +#: pyanaconda/ui/tui/spokes/software.py:111 +#: pyanaconda/ui/gui/spokes/software.py:211 msgid "Installation source not set up" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:71 -#: pyanaconda/ui/gui/spokes/software.py:206 +#: pyanaconda/ui/tui/spokes/software.py:113 +#: pyanaconda/ui/gui/spokes/software.py:214 msgid "Source changed - please verify" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:86 -#: pyanaconda/ui/gui/spokes/software.py:214 +#: pyanaconda/ui/tui/spokes/software.py:120 +#: pyanaconda/ui/gui/spokes/software.py:222 msgid "Custom software selected" msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:111 +#: pyanaconda/ui/tui/spokes/software.py:147 msgid "Installation source needs to be set up first." msgstr "" -#: pyanaconda/ui/tui/spokes/software.py:121 -#: pyanaconda/ui/tui/spokes/software.py:131 +#: pyanaconda/ui/tui/spokes/software.py:157 msgid "Base environment" msgstr "" @@ -1983,20 +1990,20 @@ msgstr "" msgid "User creation" msgstr "" -#: pyanaconda/ui/tui/spokes/user.py:114 pyanaconda/ui/gui/spokes/user.py:347 +#: pyanaconda/ui/tui/spokes/user.py:115 pyanaconda/ui/gui/spokes/user.py:362 msgid "No user will be created" msgstr "" -#: pyanaconda/ui/tui/spokes/user.py:116 +#: pyanaconda/ui/tui/spokes/user.py:117 msgid "You must set a password" msgstr "" -#: pyanaconda/ui/tui/spokes/user.py:118 pyanaconda/ui/gui/spokes/user.py:349 +#: pyanaconda/ui/tui/spokes/user.py:119 pyanaconda/ui/gui/spokes/user.py:364 #, python-format msgid "Administrator %s will be created" msgstr "" -#: pyanaconda/ui/tui/spokes/user.py:120 pyanaconda/ui/gui/spokes/user.py:351 +#: pyanaconda/ui/tui/spokes/user.py:121 pyanaconda/ui/gui/spokes/user.py:366 #, python-format msgid "User %s will be created" msgstr "" @@ -2006,123 +2013,116 @@ msgid "Warnings" msgstr "" #: pyanaconda/ui/tui/spokes/warnings.py:44 -#: pyanaconda/ui/gui/spokes/welcome.glade:518 +#: pyanaconda/ui/gui/spokes/welcome.glade:517 msgid "" "This hardware (or a combination thereof) is not supported by Red Hat. For " "more information on supported hardware, please refer to http://www.redhat." "com/hardware." msgstr "" -#: pyanaconda/ui/tui/spokes/progress.py:36 +#: pyanaconda/ui/tui/spokes/progress.py:37 msgid "Progress" msgstr "" -#: pyanaconda/ui/tui/spokes/progress.py:136 +#: pyanaconda/ui/tui/spokes/progress.py:139 msgid "\tInstallation complete. Press return to quit" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:48 +#: pyanaconda/ui/tui/spokes/__init__.py:49 msgid "Default spoke title" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:60 +#: pyanaconda/ui/tui/spokes/__init__.py:61 msgid "testing status..." msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:102 +#: pyanaconda/ui/tui/spokes/__init__.py:103 msgid "New value" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:122 +#: pyanaconda/ui/tui/spokes/__init__.py:128 #, python-format msgid "%s: " msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:123 +#: pyanaconda/ui/tui/spokes/__init__.py:129 #, python-format msgid "%s (confirm): " msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:126 +#: pyanaconda/ui/tui/spokes/__init__.py:132 msgid "" "You must enter your root password and confirm it by typing it a second time " "to continue." msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:146 -#, python-format +#: pyanaconda/ui/tui/spokes/__init__.py:154 msgid "" -"You have provided a weak password: %s\n" +"\n" "Would you like to use it anyway?" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:149 -msgid "" -"You have provided a weak password.\n" -"Would you like to use it anyway?" -msgstr "" - -#: pyanaconda/ui/tui/spokes/__init__.py:157 +#: pyanaconda/ui/tui/spokes/__init__.py:171 msgid "" "You have provided a password containing non-ASCII characters.\n" "You may not be able to switch between keyboard layouts to login.\n" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:163 +#: pyanaconda/ui/tui/spokes/__init__.py:177 #, python-format msgid "Enter new value for '%s' and press enter\n" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:171 +#: pyanaconda/ui/tui/spokes/__init__.py:185 #, python-format msgid "" -"You have provided an invalid username: %s\n" -"Tip: Keep your username shorter than 32 characters and do not use spaces.\n" +"You have provided an invalid user name: %s\n" +"Tip: Keep your user name shorter than 32 characters and do not use spaces.\n" msgstr "" -#: pyanaconda/ui/tui/spokes/__init__.py:282 +#: pyanaconda/ui/tui/spokes/__init__.py:296 msgid "Password set." msgstr "" -#: pyanaconda/ui/gui/__init__.py:475 widgets/src/BaseWindow.c:113 +#: pyanaconda/ui/gui/__init__.py:334 widgets/src/BaseWindow.c:115 msgid "Help!" msgstr "" -#: pyanaconda/ui/gui/__init__.py:728 widgets/src/StandaloneWindow.c:49 +#: pyanaconda/ui/gui/__init__.py:752 widgets/src/StandaloneWindow.c:51 msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/__init__.py:743 +#: pyanaconda/ui/gui/__init__.py:767 msgid "_No" msgstr "" -#: pyanaconda/ui/gui/__init__.py:743 +#: pyanaconda/ui/gui/__init__.py:767 msgid "_Yes" msgstr "" -#: pyanaconda/ui/gui/hubs/__init__.py:231 +#: pyanaconda/ui/gui/hubs/__init__.py:224 msgid "" "Please complete items marked with this icon before continuing to the next " "step." msgstr "" -#: pyanaconda/ui/gui/hubs/progress.py:110 +#: pyanaconda/ui/gui/hubs/progress.py:111 msgid "Complete!" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.py:208 +#: pyanaconda/ui/gui/hubs/progress.py:211 #, python-format msgid "" "%s is now successfully installed on your system and ready for you to use! " "When you are ready, reboot your system to start using it!" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.py:211 +#: pyanaconda/ui/gui/hubs/progress.py:214 msgctxt "GUI|Progress" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.py:219 +#: pyanaconda/ui/gui/hubs/progress.py:222 #, python-format msgid "" "%s is now successfully installed, but some configuration still needs to be " @@ -2130,38 +2130,38 @@ msgid "" "Finish it and then click the Finish configuration button please." msgstr "" -#: pyanaconda/ui/gui/hubs/progress.py:224 +#: pyanaconda/ui/gui/hubs/progress.py:227 #, python-format msgid "" "%s is now successfully installed and ready for you to use!\n" "Go ahead and reboot to start using it!" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:106 +#: pyanaconda/ui/gui/spokes/custom.py:107 #, python-format msgid "Create a new %(container_type)s ..." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:107 +#: pyanaconda/ui/gui/spokes/custom.py:108 #, python-format msgid "Create or select %(container_type)s" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:109 +#: pyanaconda/ui/gui/spokes/custom.py:110 msgid "Device reconfiguration failed. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:111 +#: pyanaconda/ui/gui/spokes/custom.py:112 msgid "" "Storage configuration reset due to unrecoverable error. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:135 +#: pyanaconda/ui/gui/spokes/custom.py:143 #: pyanaconda/ui/gui/spokes/custom.glade:93 msgid "MANUAL PARTITIONING" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:342 +#: pyanaconda/ui/gui/spokes/custom.py:369 #, python-format msgctxt "GUI|Custom Partitioning" msgid "%d _storage device selected" @@ -2169,57 +2169,63 @@ msgid_plural "%d _storage devices selected" msgstr[0] "" msgstr[1] "" -#: pyanaconda/ui/gui/spokes/custom.py:468 +#: pyanaconda/ui/gui/spokes/custom.py:495 #, python-format msgid "" "When you create mount points for your %(name)s %(version)s installation, " "you'll be able to view their details here." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:628 +#: pyanaconda/ui/gui/spokes/custom.py:655 #, python-format msgid "" "/boot/efi must be on a device of type %(oneFsType)s or %(anotherFsType)s" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:632 +#: pyanaconda/ui/gui/spokes/custom.py:659 #, python-format msgid "%(fs)s must be on a device of type %(type)s" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:635 -#: pyanaconda/ui/gui/spokes/custom.py:637 +#: pyanaconda/ui/gui/spokes/custom.py:662 +#: pyanaconda/ui/gui/spokes/custom.py:664 #, python-format msgid "%s cannot be encrypted" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:639 -msgid "You must create a new filesystem on the root device." +#: pyanaconda/ui/gui/spokes/custom.py:666 +msgid "You must create a new file system on the root device." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:644 +#: pyanaconda/ui/gui/spokes/custom.py:671 #, python-format msgid "Device does not support RAID level selection %s." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:746 +#: pyanaconda/ui/gui/spokes/custom.py:784 msgid "Device resize request failed. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:817 +#: pyanaconda/ui/gui/spokes/custom.py:857 msgid "Device reformat request failed. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1187 +#: pyanaconda/ui/gui/spokes/custom.py:1228 +#: pyanaconda/ui/gui/spokes/custom.py:2114 +#, python-format +msgid "Invalid device name: %s" +msgstr "" + +#: pyanaconda/ui/gui/spokes/custom.py:1234 #, python-format msgid "Specified name %s already in use." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1345 +#: pyanaconda/ui/gui/spokes/custom.py:1438 msgid "No disks assigned" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1351 +#: pyanaconda/ui/gui/spokes/custom.py:1444 #, python-format msgctxt "GUI|Custom Partitioning|Devices" msgid " and %d other" @@ -2227,141 +2233,145 @@ msgid_plural " and %d others" msgstr[0] "" msgstr[1] "" -#: pyanaconda/ui/gui/spokes/custom.py:1442 +#: pyanaconda/ui/gui/spokes/custom.py:1502 +msgid "The container is encrypted." +msgstr "" + +#: pyanaconda/ui/gui/spokes/custom.py:1538 msgid "" -"The space available to this mountpoint can be changed by modifying the " +"The space available to this mount point can be changed by modifying the " "volume below." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1444 +#: pyanaconda/ui/gui/spokes/custom.py:1540 msgid "This file system may not be resized." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1498 +#: pyanaconda/ui/gui/spokes/custom.py:1594 msgid "" "Error checking storage configuration. Click for details or press Done again " "to continue." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1500 +#: pyanaconda/ui/gui/spokes/custom.py:1596 msgid "" "Warning checking storage configuration. Click for details or press Done " "again to continue." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1606 +#: pyanaconda/ui/gui/spokes/custom.py:1702 #, python-format msgid "Added new %(type)s to existing container %(name)s." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1618 +#: pyanaconda/ui/gui/spokes/custom.py:1713 msgid "Failed to add new device. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1624 +#: pyanaconda/ui/gui/spokes/custom.py:1718 msgid "Invalid partition size set. Use a valid integer." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1732 +#: pyanaconda/ui/gui/spokes/custom.py:1825 msgid "Device removal request failed. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1965 +#: pyanaconda/ui/gui/spokes/custom.py:2055 #, python-format msgid "Volume Group name %s is already in use. Not saving changes." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:1996 +#: pyanaconda/ui/gui/spokes/custom.py:2085 #, python-format msgid "(%s free)" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2133 +#: pyanaconda/ui/gui/spokes/custom.py:2232 #, python-format msgid "" "This Software RAID array is missing %(missingMembers)d of %(totalMembers)d " "member partitions. You can remove it or select a different device." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2139 +#: pyanaconda/ui/gui/spokes/custom.py:2238 #, python-format msgid "" "This LVM Volume Group is missing %(missingPVs)d of %(totalPVs)d physical " "volumes. You can remove it or select a different device." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2222 +#: pyanaconda/ui/gui/spokes/custom.py:2321 msgid "No disks selected." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2228 +#: pyanaconda/ui/gui/spokes/custom.py:2326 msgid "Not enough free space on selected disks." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2234 -#: pyanaconda/ui/gui/spokes/custom.py:2260 +#: pyanaconda/ui/gui/spokes/custom.py:2331 +#: pyanaconda/ui/gui/spokes/custom.py:2356 msgid "Automatic partitioning failed. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2463 +#: pyanaconda/ui/gui/spokes/custom.py:2580 msgid "" "Continuing with this action will reset all your partitioning selections to " "their current on-disk state." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2471 +#: pyanaconda/ui/gui/spokes/custom.py:2588 msgid "_Reset selections" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2471 +#: pyanaconda/ui/gui/spokes/custom.py:2588 msgid "_Preserve current selections" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.py:2546 +#: pyanaconda/ui/gui/spokes/custom.py:2663 msgid "Failed to unlock encrypted block device. Click for details" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:392 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:401 msgctxt "GUI|Spoke" -msgid "DATE & _TIME" +msgid "_TIME & DATE" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:512 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:524 msgid "Restoring hardware time..." msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:523 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:535 msgid "Invalid timezone" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:695 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:709 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:891 -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:725 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:711 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:725 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:911 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:751 msgid "PM" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:697 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:706 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:890 -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:893 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:713 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:722 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:910 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:913 msgid "AM" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:1040 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:1061 msgid "You need to set up networking first if you want to use NTP" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.py:1045 +#: pyanaconda/ui/gui/spokes/datetime_spoke.py:1065 msgid "You have no working NTP server configured" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.py:392 +#: pyanaconda/ui/gui/spokes/filter.py:451 msgctxt "GUI|Spoke" msgid "_INSTALLATION DESTINATION" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.py:494 +#: pyanaconda/ui/gui/spokes/filter.py:573 #, python-format msgctxt "GUI|Installation Destination|Filter" msgid "%d _storage device selected" @@ -2396,177 +2406,195 @@ msgctxt "GUI|Spoke" msgid "_LANGUAGE SUPPORT" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:85 +#: pyanaconda/ui/gui/spokes/network.py:87 msgid "Status unknown (missing)" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:88 +#: pyanaconda/ui/gui/spokes/network.py:90 msgid "Status unknown" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:90 +#: pyanaconda/ui/gui/spokes/network.py:92 msgid "Unmanaged" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:93 -#: pyanaconda/ui/gui/spokes/network.py:97 +#: pyanaconda/ui/gui/spokes/network.py:95 +#: pyanaconda/ui/gui/spokes/network.py:99 msgid "Unavailable" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:95 +#: pyanaconda/ui/gui/spokes/network.py:97 msgid "Firmware missing" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:101 +#: pyanaconda/ui/gui/spokes/network.py:103 msgid "Cable unplugged" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:103 +#: pyanaconda/ui/gui/spokes/network.py:105 msgid "Disconnected" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:108 +#: pyanaconda/ui/gui/spokes/network.py:110 msgid "Connecting" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:110 +#: pyanaconda/ui/gui/spokes/network.py:112 msgid "Authentication required" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:112 +#: pyanaconda/ui/gui/spokes/network.py:114 msgid "Connected" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:114 +#: pyanaconda/ui/gui/spokes/network.py:116 msgid "Disconnecting" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:116 +#: pyanaconda/ui/gui/spokes/network.py:118 msgid "Connection failed" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:298 +#: pyanaconda/ui/gui/spokes/network.py:303 msgid "Ethernet" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:299 +#: pyanaconda/ui/gui/spokes/network.py:304 msgid "Wireless" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:300 +#: pyanaconda/ui/gui/spokes/network.py:305 #: pyanaconda/ui/gui/spokes/network.glade:15 msgid "Bond" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:301 -#: pyanaconda/ui/gui/spokes/network.glade:23 -msgid "Vlan" +#: pyanaconda/ui/gui/spokes/network.py:306 +#: pyanaconda/ui/gui/spokes/network.glade:27 +msgid "VLAN" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:302 -#: pyanaconda/ui/gui/spokes/network.glade:19 +#: pyanaconda/ui/gui/spokes/network.py:307 +#: pyanaconda/ui/gui/spokes/network.glade:23 msgid "Team" msgstr "" +#: pyanaconda/ui/gui/spokes/network.py:308 +#: pyanaconda/ui/gui/spokes/network.glade:19 +msgid "Bridge" +msgstr "" + #. TRANSLATORS: ethernet cable is unplugged -#: pyanaconda/ui/gui/spokes/network.py:692 +#: pyanaconda/ui/gui/spokes/network.py:729 msgid "unplugged" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:782 -#: pyanaconda/ui/gui/spokes/network.glade:536 -#: pyanaconda/ui/gui/spokes/network.glade:837 +#: pyanaconda/ui/gui/spokes/network.py:819 +#: pyanaconda/ui/gui/spokes/network.glade:540 +#: pyanaconda/ui/gui/spokes/network.glade:841 msgid "IPv4 Address" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:783 -#: pyanaconda/ui/gui/spokes/network.glade:512 -#: pyanaconda/ui/gui/spokes/network.glade:849 -#: pyanaconda/ui/gui/spokes/network.glade:1417 +#: pyanaconda/ui/gui/spokes/network.py:820 +#: pyanaconda/ui/gui/spokes/network.glade:516 +#: pyanaconda/ui/gui/spokes/network.glade:853 +#: pyanaconda/ui/gui/spokes/network.glade:1420 msgid "IPv6 Address" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:785 -#: pyanaconda/ui/gui/spokes/network.py:787 -#: pyanaconda/ui/gui/spokes/network.glade:1405 +#: pyanaconda/ui/gui/spokes/network.py:822 +#: pyanaconda/ui/gui/spokes/network.py:824 +#: pyanaconda/ui/gui/spokes/network.glade:1408 msgid "IP Address" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:876 +#: pyanaconda/ui/gui/spokes/network.py:914 #, python-format msgid "%d Mb/s" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1080 +#: pyanaconda/ui/gui/spokes/network.py:1134 msgid "WEP" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1083 +#: pyanaconda/ui/gui/spokes/network.py:1137 msgid "WPA" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1086 +#: pyanaconda/ui/gui/spokes/network.py:1140 msgid "WPA2" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1090 +#: pyanaconda/ui/gui/spokes/network.py:1144 msgid "Enterprise" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1095 widgets/src/SpokeSelector.c:57 -#: pyanaconda/ui/gui/spokes/filter.glade:158 -#: pyanaconda/ui/gui/spokes/filter.glade:560 -#: pyanaconda/ui/gui/spokes/filter.glade:904 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:532 +#: pyanaconda/ui/gui/spokes/network.py:1149 widgets/src/SpokeSelector.c:59 +#: pyanaconda/ui/gui/spokes/filter.glade:160 +#: pyanaconda/ui/gui/spokes/filter.glade:566 +#: pyanaconda/ui/gui/spokes/filter.glade:897 +#: pyanaconda/ui/gui/spokes/filter.glade:1514 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:531 msgid "None" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1219 -#: pyanaconda/ui/gui/spokes/network.glade:2438 +#: pyanaconda/ui/gui/spokes/network.py:1281 +#: pyanaconda/ui/gui/spokes/network.glade:2435 msgid "Authentication required by wireless network" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1220 +#: pyanaconda/ui/gui/spokes/network.py:1282 #, python-format msgid "" "Passwords or encryption keys are required to access\n" "the wireless network '%(network_id)s'." msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1233 +#: pyanaconda/ui/gui/spokes/network.py:1296 msgctxt "GUI|Network|Secrets Dialog" msgid "_Password:" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1241 +#: pyanaconda/ui/gui/spokes/network.py:1304 msgctxt "GUI|Network|Secrets Dialog" msgid "_Key:" msgstr "" -#: pyanaconda/ui/gui/spokes/network.py:1298 -msgctxt "GUI|Spoke" -msgid "_NETWORK & HOSTNAME" +#: pyanaconda/ui/gui/spokes/network.py:1314 +msgid "User name: " msgstr "" -#: pyanaconda/ui/gui/spokes/password.py:51 +#: pyanaconda/ui/gui/spokes/network.py:1325 +msgid "Identity: " +msgstr "" + +#: pyanaconda/ui/gui/spokes/network.py:1330 +msgid "Private key password: " +msgstr "" + +#: pyanaconda/ui/gui/spokes/network.py:1386 +msgctxt "GUI|Spoke" +msgid "_NETWORK & HOST NAME" +msgstr "" + +#: pyanaconda/ui/gui/spokes/password.py:52 msgctxt "GUI|Spoke" msgid "_ROOT PASSWORD" msgstr "" -#: pyanaconda/ui/gui/spokes/password.py:98 #: pyanaconda/ui/gui/spokes/password.py:99 +#: pyanaconda/ui/gui/spokes/password.py:100 msgid "The password is set." msgstr "" -#: pyanaconda/ui/gui/spokes/password.py:121 +#: pyanaconda/ui/gui/spokes/password.py:127 msgid "Root password is set" msgstr "" -#: pyanaconda/ui/gui/spokes/password.py:123 +#: pyanaconda/ui/gui/spokes/password.py:129 msgid "Root account is disabled" msgstr "" -#: pyanaconda/ui/gui/spokes/password.py:125 +#: pyanaconda/ui/gui/spokes/password.py:131 msgid "Root password is not set" msgstr "" @@ -2575,46 +2603,46 @@ msgctxt "GUI|Spoke" msgid "_SOFTWARE SELECTION" msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:92 +#: pyanaconda/ui/gui/spokes/software.py:98 #: pyanaconda/ui/gui/spokes/source.py:54 msgid "Downloading package metadata..." msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:95 +#: pyanaconda/ui/gui/spokes/software.py:101 msgid "Downloading group metadata..." msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:135 +#: pyanaconda/ui/gui/spokes/software.py:143 #: pyanaconda/ui/gui/spokes/source.py:529 msgid "Checking software dependencies..." msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:140 +#: pyanaconda/ui/gui/spokes/software.py:148 msgid "Error checking software dependencies" msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:356 +#: pyanaconda/ui/gui/spokes/software.py:374 msgid "Error checking software dependencies. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:460 +#: pyanaconda/ui/gui/spokes/software.py:478 msgid "" "The software marked for installation has the following errors. This is " "likely caused by an error with your installation source. You can quit the " "installer, change your software source, or change your software selections." msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:465 +#: pyanaconda/ui/gui/spokes/software.py:483 msgctxt "GUI|Software Selection|Error Dialog" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:466 +#: pyanaconda/ui/gui/spokes/software.py:484 msgctxt "GUI|Software Selection|Error Dialog" msgid "_Modify Software Source" msgstr "" -#: pyanaconda/ui/gui/spokes/software.py:467 +#: pyanaconda/ui/gui/spokes/software.py:485 msgctxt "GUI|Software Selection|Error Dialog" msgid "Modify _Selections" msgstr "" @@ -2654,7 +2682,7 @@ msgid "_INSTALLATION SOURCE" msgstr "" #: pyanaconda/ui/gui/spokes/source.py:469 -#: pyanaconda/ui/gui/spokes/source.py:668 +#: pyanaconda/ui/gui/spokes/source.py:673 msgid "Failed to set up installation source; check the repo url" msgstr "" @@ -2666,66 +2694,74 @@ msgstr "" msgid "Error setting up ISO file" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:670 +#: pyanaconda/ui/gui/spokes/source.py:675 msgid "" "Failed to set up installation source; check the repo url and proxy settings" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:699 +#: pyanaconda/ui/gui/spokes/source.py:704 #, python-format msgid "Device: %s" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:700 +#: pyanaconda/ui/gui/spokes/source.py:705 #, python-format msgid "Label: %s" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:888 +#: pyanaconda/ui/gui/spokes/source.py:907 msgid "URL is empty" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:892 +#: pyanaconda/ui/gui/spokes/source.py:911 msgid "Invalid URL" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:909 +#: pyanaconda/ui/gui/spokes/source.py:928 msgid "Protocol in URL does not match selected protocol" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:912 +#: pyanaconda/ui/gui/spokes/source.py:931 msgid "NFS server is empty" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:919 +#: pyanaconda/ui/gui/spokes/source.py:938 msgid "Invalid host name" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:922 +#: pyanaconda/ui/gui/spokes/source.py:941 msgid "Remote directory is required" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:943 +#: pyanaconda/ui/gui/spokes/source.py:962 msgid "Duplicate repository names." msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:950 +#: pyanaconda/ui/gui/spokes/source.py:969 msgid "Empty repository name" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:953 +#: pyanaconda/ui/gui/spokes/source.py:972 msgid "Invalid repository name" msgstr "" -#: pyanaconda/ui/gui/spokes/source.py:959 +#: pyanaconda/ui/gui/spokes/source.py:978 msgid "Repository name conflicts with internal repository name." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:113 +#: pyanaconda/ui/gui/spokes/storage.py:117 msgid "Please wait... software metadata still loading." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:114 +#: pyanaconda/ui/gui/spokes/storage.py:120 +#, python-format +msgid "" +"Your current <b>%(product)s</b> software selection requires <b>%(total)s</b> " +"of available space, including <b>%(software)s</b> for software and <b>" +"%(swap)s</b> for swap space." +msgstr "" + +#: pyanaconda/ui/gui/spokes/storage.py:129 #, python-format msgid "" "Your current <a href=\"\" title=\"%(tooltip)s\"><b>%(product)s</b> software " @@ -2733,12 +2769,12 @@ msgid "" "%(software)s</b> for software and <b>%(swap)s</b> for swap space." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:164 +#: pyanaconda/ui/gui/spokes/storage.py:179 #, python-format msgid "%s The disks you've selected have the following amounts of free space:" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:172 +#: pyanaconda/ui/gui/spokes/storage.py:189 #, python-format msgid "" "<b>You don't have enough space available to install %s</b>. You can shrink " @@ -2746,14 +2782,14 @@ msgid "" "adjust your partitions on your own in the custom partitioning interface." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:196 +#: pyanaconda/ui/gui/spokes/storage.py:213 #, python-format msgid "" -"%(sw_text)s You don't have enough space available to install <b>%(product)s</" -"b>, even if you used all of the free space available on the selected disks." +" You don't have enough space available to install <b>%(product)s</b>, even " +"if you used all of the free space available on the selected disks." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:207 +#: pyanaconda/ui/gui/spokes/storage.py:225 #, python-format msgid "" "<b>You don't have enough space available to install %(productName)s</b>, " @@ -2762,110 +2798,118 @@ msgid "" "install a smaller version of <b>%(productName)s</b>, or quit the installer." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:228 +#: pyanaconda/ui/gui/spokes/storage.py:246 msgctxt "GUI|Spoke" msgid "INSTALLATION _DESTINATION" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:311 +#: pyanaconda/ui/gui/spokes/storage.py:331 msgid "Saving storage configuration..." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:317 -#: pyanaconda/ui/gui/spokes/storage.py:329 +#: pyanaconda/ui/gui/spokes/storage.py:337 +#: pyanaconda/ui/gui/spokes/storage.py:349 msgid "Failed to save storage configuration..." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:365 -#: pyanaconda/ui/gui/spokes/storage.py:635 +#: pyanaconda/ui/gui/spokes/storage.py:385 +#: pyanaconda/ui/gui/spokes/storage.py:682 msgid "Formatting DASDs" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:499 +#: pyanaconda/ui/gui/spokes/storage.py:531 msgid "Error checking storage configuration. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:501 +#: pyanaconda/ui/gui/spokes/storage.py:533 msgid "Warning checking storage configuration. Click for details." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:537 +#: pyanaconda/ui/gui/spokes/storage.py:574 +#, python-format +msgid "" +"FCP device %(hba_id)s\n" +"WWPN %(wwpn)s\n" +"LUN %(lun)s" +msgstr "" + +#: pyanaconda/ui/gui/spokes/storage.py:584 #, python-format msgid "%s free" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:584 +#: pyanaconda/ui/gui/spokes/storage.py:631 #, python-format msgid "%(count)d disk selected; %(capacity)s capacity; %(free)s free" msgid_plural "%(count)d disks selected; %(capacity)s capacity; %(free)s free" msgstr[0] "" msgstr[1] "" -#: pyanaconda/ui/gui/spokes/storage.py:672 +#: pyanaconda/ui/gui/spokes/storage.py:715 msgid "" -"You have chosen to skip bootloader installation. Your system may not be " +"You have chosen to skip boot loader installation. Your system may not be " "bootable." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:886 +#: pyanaconda/ui/gui/spokes/storage.py:962 msgid "You'll be able to make space available during custom partitioning." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:902 +#: pyanaconda/ui/gui/spokes/storage.py:978 msgid "" "The following errors were encountered when checking your storage " "configuration. You can modify your storage layout or quit the installer." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:907 +#: pyanaconda/ui/gui/spokes/storage.py:983 msgctxt "GUI|Storage|Error Dialog" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:908 +#: pyanaconda/ui/gui/spokes/storage.py:984 msgctxt "GUI|Storage|Error Dialog" msgid "_Modify Storage Layout" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:921 +#: pyanaconda/ui/gui/spokes/storage.py:998 msgid "" "The following warnings were encountered when checking your storage " "configuration. These are not fatal, but you may wish to make changes to " "your storage layout." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.py:925 +#: pyanaconda/ui/gui/spokes/storage.py:1002 msgid "_OK" msgstr "" -#: pyanaconda/ui/gui/spokes/user.py:71 +#: pyanaconda/ui/gui/spokes/user.py:72 #, python-format msgid "Invalid group name: %s" msgstr "" -#: pyanaconda/ui/gui/spokes/user.py:212 +#: pyanaconda/ui/gui/spokes/user.py:215 msgctxt "GUI|Spoke" msgid "_USER CREATION" msgstr "" -#: pyanaconda/ui/gui/spokes/user.py:285 pyanaconda/ui/gui/spokes/user.py:286 +#: pyanaconda/ui/gui/spokes/user.py:293 pyanaconda/ui/gui/spokes/user.py:294 msgid "The password was set by kickstart." msgstr "" -#: pyanaconda/ui/gui/spokes/user.py:314 -msgid "Invalid username" +#: pyanaconda/ui/gui/spokes/user.py:329 +msgid "Invalid user name" msgstr "" -#: pyanaconda/ui/gui/spokes/user.py:316 +#: pyanaconda/ui/gui/spokes/user.py:331 msgid "Full name cannot contain colon characters" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.py:254 +#: pyanaconda/ui/gui/spokes/welcome.py:258 #, python-format msgid "WELCOME TO %(name)s %(version)s." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.py:270 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.py:269 #, python-format msgid "" "The following nodes were discovered using the iSCSI initiator <b>" @@ -2877,28 +2921,28 @@ msgstr "" msgid "DATA" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/accordion.py:278 +#: pyanaconda/ui/gui/spokes/lib/accordion.py:282 #, python-format msgid "" "You haven't created any mount points for your %(product)s %(version)s " "installation yet. You can:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/accordion.py:287 +#: pyanaconda/ui/gui/spokes/lib/accordion.py:291 msgid "_Click here to create them automatically." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/accordion.py:312 +#: pyanaconda/ui/gui/spokes/lib/accordion.py:316 msgid "Create new mount points by clicking the '+' button." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/accordion.py:320 +#: pyanaconda/ui/gui/spokes/lib/accordion.py:324 msgid "" "Or, assign new mount points to existing partitions after selecting them " "below." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/accordion.py:325 +#: pyanaconda/ui/gui/spokes/lib/accordion.py:329 msgid "_New mount points will use the following partitioning scheme:" msgstr "" @@ -2906,16 +2950,16 @@ msgstr "" #, python-format msgid "" "<b>%(count)d disk; %(size)s capacity; %(free)s free space</b> (unpartitioned " -"and in filesystems)" +"and in file systems)" msgid_plural "" "<b>%(count)d disks; %(size)s capacity; %(free)s free space</b> " -"(unpartitioned and in filesystems)" +"(unpartitioned and in file systems)" msgstr[0] "" msgstr[1] "" #: pyanaconda/ui/gui/spokes/lib/cart.py:190 msgctxt "GUI|Selected Disks Dialog" -msgid "_Do not install bootloader" +msgid "_Do not install boot loader" msgstr "" #: pyanaconda/ui/gui/spokes/lib/cart.py:193 @@ -2924,82 +2968,91 @@ msgctxt "GUI|Selected Disks Dialog" msgid "_Set as Boot Device" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:63 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:64 #, python-format msgid "" "The RAID level you have selected (%(level)s) requires more disks (%(min)d) " "than you currently have selected (%(count)d)." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:67 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:68 #, python-format msgid "CONFIGURE %(container_type)s" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:68 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:69 #, python-format msgid "" "Please create a name for this %(container_type)s and select at least one " "disk below." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:71 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:72 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:74 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:75 msgid "Volume Group" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:73 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:74 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:75 +msgid "_Volume Group:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:76 msgid "Volume" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:113 -msgid "Can not relabel already existing filesystem." +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:76 +msgid "_Volume:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:118 -msgid "Can not set label on filesystem." +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:136 +msgid "Cannot relabel already existing file system." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:120 -msgid "Unacceptable label format for filesystem." +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:141 +msgid "Cannot set label on file system." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:130 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:143 +msgid "Unacceptable label format for file system." +msgstr "" + +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:153 msgid "That mount point is already in use. Try something else?" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:132 -msgid "Please enter a valid mountpoint." +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:155 +msgid "Please enter a valid mount point." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:134 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:146 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:157 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:169 msgid "That mount point is invalid. Try something else?" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:287 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:310 msgid "container" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:359 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:389 #, python-format msgctxt "GUI|Custom Partitioning|Confirm Delete Dialog" -msgid "Delete _all other filesystems in the %s root as well." +msgid "Delete _all other file systems in the %s root as well." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:369 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:399 #, python-format msgid "Are you sure you want to delete all of the data on %s?" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:371 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:401 #, python-format msgid "" "Are you sure you want to delete all of the data on %s, including snapshots " "and/or subvolumes?" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:648 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py:685 msgid "Invalid container name" msgstr "" @@ -3013,16 +3066,16 @@ msgctxt "GUI|Detailed Error Dialog" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/passphrase.py:34 +#: pyanaconda/ui/gui/spokes/lib/passphrase.py:35 #, python-format msgid "You have provided a weak passphrase: %s" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/passphrase.py:35 +#: pyanaconda/ui/gui/spokes/lib/passphrase.py:36 msgid "Passphrases do not match." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/passphrase.py:157 +#: pyanaconda/ui/gui/spokes/lib/passphrase.py:165 msgid "Passphrase contains non-ASCII characters" msgstr "" @@ -3057,23 +3110,23 @@ msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.py:213 msgid "" -"You can remove existing filesystems you no longer need to free up space for " -"this installation. Removing a filesystem will permanently delete all of the " -"data it contains." +"You can remove existing file systems you no longer need to free up space for " +"this installation. Removing a file system will permanently delete all of " +"the data it contains." msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.py:219 msgid "" -"There is also free space available in pre-existing filesystems. While it's " +"There is also free space available in pre-existing file systems. While it's " "risky and we recommend you back up your data first, you can recover that " "free disk space and make it available for this installation below." msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.py:229 #, python-format -msgid "<b>%(count)s disk; %(size)s reclaimable space</b> (in filesystems)" +msgid "<b>%(count)s disk; %(size)s reclaimable space</b> (in file systems)" msgid_plural "" -"<b>%(count)s disks; %(size)s reclaimable space</b> (in filesystems)" +"<b>%(count)s disks; %(size)s reclaimable space</b> (in file systems)" msgstr[0] "" msgstr[1] "" @@ -3094,48 +3147,58 @@ msgctxt "GUI|Reclaim Dialog" msgid "Preserve _all" msgstr "" -#: widgets/src/BaseWindow.c:109 -msgid "DISTRIBUTION INSTALLATION" +#: pyanaconda/ui/gui/spokes/lib/summary.py:54 +#, python-format +msgid "%(description)s (%(deviceName)s)" msgstr "" -#: widgets/src/BaseWindow.c:110 -msgid "SPOKE NAME" +#: pyanaconda/ui/gui/spokes/lib/summary.py:58 +#, python-format +msgid "%(deviceName)s on %(container)s" msgstr "" #: widgets/src/BaseWindow.c:111 +msgid "DISTRIBUTION INSTALLATION" +msgstr "" + +#: widgets/src/BaseWindow.c:112 +msgid "SPOKE NAME" +msgstr "" + +#: widgets/src/BaseWindow.c:113 msgid "PRE-RELEASE / TESTING" msgstr "" -#: widgets/src/DiskOverview.c:54 +#: widgets/src/DiskOverview.c:56 msgid "New Device" msgstr "" -#: widgets/src/DiskOverview.c:56 widgets/src/DiskOverview.c:57 +#: widgets/src/DiskOverview.c:58 widgets/src/DiskOverview.c:59 msgid "0 MB" msgstr "" -#: widgets/src/MountpointSelector.c:52 +#: widgets/src/MountpointSelector.c:54 msgid "0 GB" msgstr "" -#: widgets/src/SpokeSelector.c:58 +#: widgets/src/SpokeSelector.c:60 msgid "New Selector" msgstr "" -#: widgets/src/SpokeWindow.c:46 +#: widgets/src/SpokeWindow.c:48 msgid "_Done" msgstr "" -#: widgets/src/StandaloneWindow.c:50 +#: widgets/src/StandaloneWindow.c:52 msgid "_Continue" msgstr "" -#: widgets/src/LayoutIndicator.c:32 +#: widgets/src/LayoutIndicator.c:34 #, c-format msgid "Current layout: '%s'. Click to switch to the next layout." msgstr "" -#: widgets/src/LayoutIndicator.c:33 +#: widgets/src/LayoutIndicator.c:35 #, c-format msgid "Current layout: '%s'. Add more layouts to enable switching." msgstr "" @@ -3182,631 +3245,630 @@ msgctxt "GUI|Advanced User" msgid "_Add user to the following groups:" msgstr "" -#: pyanaconda/ui/gui/spokes/advanced_user.glade:291 +#: pyanaconda/ui/gui/spokes/advanced_user.glade:290 msgid "Example:" msgstr "" -#: pyanaconda/ui/gui/spokes/advanced_user.glade:308 +#: pyanaconda/ui/gui/spokes/advanced_user.glade:307 msgid "wheel, my-team (1245), project-x (29935)" msgstr "" -#: pyanaconda/ui/gui/spokes/advanced_user.glade:342 +#: pyanaconda/ui/gui/spokes/advanced_user.glade:341 msgid "Tip:" msgstr "" -#: pyanaconda/ui/gui/spokes/advanced_user.glade:361 +#: pyanaconda/ui/gui/spokes/advanced_user.glade:360 msgid "" "You may input a comma-separated list of group names and group IDs here. " "Groups that do not already exist will be created; specify their GID in " "parentheses. " msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:108 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:110 msgctxt "GUI|Date and Time|NTP" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:122 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:124 msgctxt "GUI|Date and Time|NTP" msgid "_OK" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:147 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:149 msgid "Add and mark for usage NTP servers" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:170 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:171 msgid "New NTP Server" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:189 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:190 msgid "Add NTP Server" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:240 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:210 +msgid "This URL refers to a pool of NTP servers" +msgstr "" + +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:256 +msgid "Pool" +msgstr "" + +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:269 msgid "Working" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:248 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:277 msgid "Use" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:276 -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:490 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:305 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:519 msgid "Configure NTP" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:297 -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:879 -msgid "DATE & TIME" +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:326 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:934 +msgid "TIME & DATE" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:347 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:376 msgctxt "GUI|Date and Time" msgid "_Region:" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:377 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:406 msgid "Region" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:394 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:423 msgctxt "GUI|Date and Time" msgid "_City:" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:424 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:453 msgid "City" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:452 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:481 msgctxt "GUI|Date and Time" msgid "_Network Time" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:470 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:499 msgid "Use Network Time" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:557 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:586 msgid "Hours" msgstr "" #. TRANSLATORS: This is the separator between hours and minutes, like in HH:MM -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:570 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:599 msgid ":" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:590 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:619 msgid "Minutes" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:608 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:637 msgid "Hour Up" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:626 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:655 msgid "Hour Down" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:644 -msgid "AM/PM Up" -msgstr "" - -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:662 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:673 msgid "Minutes Up" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:680 -msgid "AM/PM Down" -msgstr "" - -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:698 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:691 msgid "Minutes Down" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:767 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:735 +msgid "AM/PM Up" +msgstr "" + +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:771 +msgid "AM/PM Down" +msgstr "" + +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:822 msgctxt "GUI|Date and Time" msgid "24-_hour" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:785 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:840 msgctxt "GUI|Date and Time" msgid "_AM/PM" msgstr "" -#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:844 +#: pyanaconda/ui/gui/spokes/datetime_spoke.glade:899 msgid "Set Date & Time" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:58 -#: pyanaconda/ui/gui/spokes/storage.glade:446 -#: pyanaconda/ui/gui/spokes/storage.glade:1026 +#: pyanaconda/ui/gui/spokes/filter.glade:60 +#: pyanaconda/ui/gui/spokes/storage.glade:444 +#: pyanaconda/ui/gui/spokes/storage.glade:1024 msgid "INSTALLATION DESTINATION" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:131 +#: pyanaconda/ui/gui/spokes/filter.glade:133 msgid "Search Results:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:144 +#: pyanaconda/ui/gui/spokes/filter.glade:146 msgctxt "GUI|Installation Destination|Filter|Search" msgid "Search _By:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:159 +#: pyanaconda/ui/gui/spokes/filter.glade:161 msgid "Port / Target / LUN #" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:160 +#: pyanaconda/ui/gui/spokes/filter.glade:162 msgid "Target WWID" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:197 +#: pyanaconda/ui/gui/spokes/filter.glade:199 msgctxt "GUI|Installation Destination|Filter|Search|Port Target LUN" msgid "_Port:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:222 +#: pyanaconda/ui/gui/spokes/filter.glade:224 msgctxt "GUI|Installation Destination|Filter|Search|Port Target LUN" msgid "_Target:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:248 +#: pyanaconda/ui/gui/spokes/filter.glade:252 msgctxt "GUI|Installation Destination|Filter|Search|Port Target LUN" msgid "_LUN:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:288 +#: pyanaconda/ui/gui/spokes/filter.glade:293 msgctxt "GUI|Installation Destination|Filter|Search|WWID" msgid "_WWID:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:360 -#: pyanaconda/ui/gui/spokes/filter.glade:563 -#: pyanaconda/ui/gui/spokes/filter.glade:741 +#: pyanaconda/ui/gui/spokes/filter.glade:366 +#: pyanaconda/ui/gui/spokes/filter.glade:1267 +#: pyanaconda/ui/gui/spokes/keyboard.glade:520 +#: pyanaconda/ui/gui/spokes/source.glade:987 +#: pyanaconda/ui/gui/spokes/lib/cart.glade:133 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:235 +#: pyanaconda/ui/gui/spokes/lib/resize.glade:163 +msgid "Name" +msgstr "" + +#: pyanaconda/ui/gui/spokes/filter.glade:380 +#: pyanaconda/ui/gui/spokes/filter.glade:569 +#: pyanaconda/ui/gui/spokes/filter.glade:748 msgid "WWID" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:374 -#: pyanaconda/ui/gui/spokes/filter.glade:755 -#: pyanaconda/ui/gui/spokes/filter.glade:1101 -#: pyanaconda/ui/gui/spokes/filter.glade:1351 +#: pyanaconda/ui/gui/spokes/filter.glade:394 +#: pyanaconda/ui/gui/spokes/filter.glade:762 +#: pyanaconda/ui/gui/spokes/filter.glade:954 +#: pyanaconda/ui/gui/spokes/filter.glade:1295 #: pyanaconda/ui/gui/spokes/lib/cart.glade:145 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:235 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:728 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:248 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:726 msgid "Capacity" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:388 -#: pyanaconda/ui/gui/spokes/filter.glade:562 -#: pyanaconda/ui/gui/spokes/filter.glade:784 -#: pyanaconda/ui/gui/spokes/filter.glade:906 -#: pyanaconda/ui/gui/spokes/filter.glade:1130 +#: pyanaconda/ui/gui/spokes/filter.glade:408 +#: pyanaconda/ui/gui/spokes/filter.glade:568 +#: pyanaconda/ui/gui/spokes/filter.glade:791 +#: pyanaconda/ui/gui/spokes/filter.glade:899 +#: pyanaconda/ui/gui/spokes/filter.glade:983 msgid "Interconnect" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:402 +#: pyanaconda/ui/gui/spokes/filter.glade:422 msgid "Model" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:416 -#: pyanaconda/ui/gui/spokes/filter.glade:1379 +#: pyanaconda/ui/gui/spokes/filter.glade:436 +#: pyanaconda/ui/gui/spokes/filter.glade:1323 +#: pyanaconda/ui/gui/spokes/filter.glade:1517 msgid "LUN" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:430 +#: pyanaconda/ui/gui/spokes/filter.glade:450 +#: pyanaconda/ui/gui/spokes/filter.glade:1337 msgid "Port" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:444 +#: pyanaconda/ui/gui/spokes/filter.glade:464 msgid "Target" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:458 -#: pyanaconda/ui/gui/spokes/filter.glade:561 -#: pyanaconda/ui/gui/spokes/filter.glade:769 -#: pyanaconda/ui/gui/spokes/filter.glade:905 -#: pyanaconda/ui/gui/spokes/filter.glade:1115 +#: pyanaconda/ui/gui/spokes/filter.glade:478 +#: pyanaconda/ui/gui/spokes/filter.glade:567 +#: pyanaconda/ui/gui/spokes/filter.glade:776 +#: pyanaconda/ui/gui/spokes/filter.glade:898 +#: pyanaconda/ui/gui/spokes/filter.glade:968 msgid "Vendor" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:479 -msgctxt "GUI|Installation Destination|Filter|Search" -msgid "_Clear" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:493 +#: pyanaconda/ui/gui/spokes/filter.glade:499 msgctxt "GUI|Installation Destination|Filter|Search" msgid "_Find" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:515 +#: pyanaconda/ui/gui/spokes/filter.glade:521 msgctxt "GUI|Installation Destination|Filter" msgid "Searc_h" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:546 +#: pyanaconda/ui/gui/spokes/filter.glade:552 msgctxt "GUI|Installation Destination|Filter|Multipath" msgid "Filter _By:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:599 +#: pyanaconda/ui/gui/spokes/filter.glade:605 msgctxt "GUI|Installation Destination|Filter|Multipath|Vendor" msgid "Show Only _Devices From:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:637 +#: pyanaconda/ui/gui/spokes/filter.glade:643 msgctxt "GUI|Installation Destination|Filter|Multipath|Interconnect" msgid "Show Only _Devices With:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:675 +#: pyanaconda/ui/gui/spokes/filter.glade:681 msgctxt "GUI|Installation Destination|Filter|Multipath|WWID" msgid "Show Only _Devices Containing:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:798 +#: pyanaconda/ui/gui/spokes/filter.glade:805 msgid "Paths" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:819 -msgctxt "GUI|Installation Destination|Filter|Multipath" -msgid "_Clear" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:833 +#: pyanaconda/ui/gui/spokes/filter.glade:826 msgctxt "GUI|Installation Destination|Filter|Multipath" msgid "_Find" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:858 +#: pyanaconda/ui/gui/spokes/filter.glade:851 msgctxt "GUI|Installation Destination|Filter" msgid "_Multipath Devices" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:890 +#: pyanaconda/ui/gui/spokes/filter.glade:883 msgctxt "GUI|Installation Destination|Filter|Other" msgid "Filter _By:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:907 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:261 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:754 +#: pyanaconda/ui/gui/spokes/filter.glade:900 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:752 msgid "ID" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:943 +#: pyanaconda/ui/gui/spokes/filter.glade:940 +msgid "Identifier" +msgstr "" + +#: pyanaconda/ui/gui/spokes/filter.glade:1046 msgctxt "GUI|Installation Destination|Filter|Other|Vendor" msgid "Show Only _Devices From:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:981 +#: pyanaconda/ui/gui/spokes/filter.glade:1084 msgctxt "GUI|Installation Destination|Filter|Other|Interconnect" msgid "Show Only _Devices With:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1019 +#: pyanaconda/ui/gui/spokes/filter.glade:1122 msgctxt "GUI|Installation Destination|Filter|Other|ID" msgid "Show Only _Devices Containing:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1087 -msgid "Identifier" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:1151 -msgctxt "GUI|Installation Destination|Filter|Other" -msgid "_Clear" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:1165 -msgctxt "GUI|Installation Destination|Filter|Other" -msgid "_Find" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:1190 +#: pyanaconda/ui/gui/spokes/filter.glade:1173 msgctxt "GUI|Installation Destination|Filter" msgid "_Other SAN Devices" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1222 +#: pyanaconda/ui/gui/spokes/filter.glade:1205 msgctxt "GUI|Installation Destination|Filter|zSeries" -msgid "_Filter By:" +msgid "Filter B_y:" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1236 +#: pyanaconda/ui/gui/spokes/filter.glade:1215 msgctxt "GUI|Installation Destination|Filter|zSeries" -msgid "_Label" +msgid "_Find" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1247 -msgctxt "GUI|Installation Destination|Filter|zSeries" -msgid "_Go" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:1260 -msgctxt "GUI|Installation Destination|Filter|zSeries" -msgid "_Clear" -msgstr "" - -#: pyanaconda/ui/gui/spokes/filter.glade:1323 +#: pyanaconda/ui/gui/spokes/filter.glade:1253 +#: pyanaconda/ui/gui/spokes/filter.glade:1515 msgid "CCW" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1337 -#: pyanaconda/ui/gui/spokes/lib/summary.glade:152 +#: pyanaconda/ui/gui/spokes/filter.glade:1281 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:155 msgid "Type" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1365 +#: pyanaconda/ui/gui/spokes/filter.glade:1309 +#: pyanaconda/ui/gui/spokes/filter.glade:1516 msgid "WWPN" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1411 +#: pyanaconda/ui/gui/spokes/filter.glade:1382 +msgctxt "GUI|Installation Destination|Filter|zSeries|CCW" +msgid "_CCW:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/filter.glade:1429 +msgctxt "GUI|Installation Destination|Filter|zSeries|WWPN" +msgid "_WWPN:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/filter.glade:1471 +msgctxt "GUI|Installation Destination|Filter|zSeries|LUN" +msgid "_LUN:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/filter.glade:1541 msgctxt "GUI|Installation Destination|Filter" msgid "_zSeries Devices" msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1434 +#: pyanaconda/ui/gui/spokes/filter.glade:1564 msgctxt "GUI|Installation Destination|Filter" -msgid "_Add ZFCP LUN..." +msgid "_Add zFCP LUN..." msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1449 +#: pyanaconda/ui/gui/spokes/filter.glade:1579 +msgctxt "GUI|Installation Destination|Filter" msgid "Add EC_KD DASD..." msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1464 +#: pyanaconda/ui/gui/spokes/filter.glade:1594 msgctxt "GUI|Installation Destination|Filter" msgid "Add _iSCSI Target..." msgstr "" -#: pyanaconda/ui/gui/spokes/filter.glade:1479 +#: pyanaconda/ui/gui/spokes/filter.glade:1609 msgctxt "GUI|Installation Destination|Filter" msgid "Add FCo_E SAN..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:16 -msgid "bond" +#: pyanaconda/ui/gui/spokes/filter.glade:1624 +msgid "Refresh _List" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:20 -msgid "team" -msgstr "" - -#: pyanaconda/ui/gui/spokes/network.glade:24 -msgid "vlan" -msgstr "" - -#: pyanaconda/ui/gui/spokes/network.glade:31 +#: pyanaconda/ui/gui/spokes/network.glade:35 msgid "Add device" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:45 +#: pyanaconda/ui/gui/spokes/network.glade:49 msgctxt "GUI|Network|Add Device Dialog" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:59 +#: pyanaconda/ui/gui/spokes/network.glade:63 msgctxt "GUI|Network|Add Device Dialog" msgid "_Add" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:88 +#: pyanaconda/ui/gui/spokes/network.glade:92 msgctxt "GUI|Network|Add Device Dialog" msgid "_Select the type of device you wish to add" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:161 -#: pyanaconda/ui/gui/spokes/network.glade:2273 -msgid "NETWORK & HOSTNAME" +#: pyanaconda/ui/gui/spokes/network.glade:165 +#: pyanaconda/ui/gui/spokes/network.glade:2270 +msgid "NETWORK & HOST NAME" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:200 +#: pyanaconda/ui/gui/spokes/network.glade:204 msgid "" "Please use the live desktop environment's tools for customizing your network " -"configuration. You can set the hostname here." +"configuration. You can set the host name here." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:414 +#: pyanaconda/ui/gui/spokes/network.glade:418 msgid "Slaves" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:438 -#: pyanaconda/ui/gui/spokes/network.glade:874 -#: pyanaconda/ui/gui/spokes/network.glade:1442 +#: pyanaconda/ui/gui/spokes/network.glade:442 +#: pyanaconda/ui/gui/spokes/network.glade:878 +#: pyanaconda/ui/gui/spokes/network.glade:1445 msgid "DNS" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:464 -#: pyanaconda/ui/gui/spokes/network.glade:861 -#: pyanaconda/ui/gui/spokes/network.glade:1429 +#: pyanaconda/ui/gui/spokes/network.glade:468 +#: pyanaconda/ui/gui/spokes/network.glade:865 +#: pyanaconda/ui/gui/spokes/network.glade:1432 msgid "Default Route" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:488 +#: pyanaconda/ui/gui/spokes/network.glade:492 msgid "Subnet Mask" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:560 -#: pyanaconda/ui/gui/spokes/network.glade:825 -#: pyanaconda/ui/gui/spokes/network.glade:1273 +#: pyanaconda/ui/gui/spokes/network.glade:564 +#: pyanaconda/ui/gui/spokes/network.glade:829 +#: pyanaconda/ui/gui/spokes/network.glade:1276 msgid "Speed" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:584 -#: pyanaconda/ui/gui/spokes/network.glade:813 +#: pyanaconda/ui/gui/spokes/network.glade:588 +#: pyanaconda/ui/gui/spokes/network.glade:817 msgid "Hardware Address" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:608 +#: pyanaconda/ui/gui/spokes/network.glade:612 msgid "Parent" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:620 -msgid "Vlan ID" -msgstr "" - -#: pyanaconda/ui/gui/spokes/network.glade:695 -msgctxt "GUI|Network|Wired" -msgid "C_onfigure..." +#: pyanaconda/ui/gui/spokes/network.glade:624 +msgid "VLAN ID" msgstr "" #: pyanaconda/ui/gui/spokes/network.glade:699 +msgctxt "GUI|Network|Wired" +msgid "_Configure..." +msgstr "" + +#: pyanaconda/ui/gui/spokes/network.glade:703 msgid "To apply the configuration immediately turn the device off and on." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:986 +#: pyanaconda/ui/gui/spokes/network.glade:990 msgid "Security" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:998 +#: pyanaconda/ui/gui/spokes/network.glade:1002 msgctxt "GUI|Network|Wireless" msgid "_Network Name" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1012 +#: pyanaconda/ui/gui/spokes/network.glade:1016 msgid "Network Name" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1024 +#: pyanaconda/ui/gui/spokes/network.glade:1028 msgid "Security Key" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1108 +#: pyanaconda/ui/gui/spokes/network.glade:1111 msgctxt "GUI|Network|Wireless" msgid "_Use as Hotspot..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1125 +#: pyanaconda/ui/gui/spokes/network.glade:1128 msgctxt "GUI|Network|Wireless" msgid "_Stop Hotspot..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1142 +#: pyanaconda/ui/gui/spokes/network.glade:1145 msgctxt "GUI|Network|Wireless" msgid "_Configure..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1261 +#: pyanaconda/ui/gui/spokes/network.glade:1264 msgid "IMEI" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1285 +#: pyanaconda/ui/gui/spokes/network.glade:1288 msgid "Provider" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1467 +#: pyanaconda/ui/gui/spokes/network.glade:1470 msgctxt "GUI|Network|Mobile Broadband" msgid "_Configure..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1578 +#: pyanaconda/ui/gui/spokes/network.glade:1581 msgid "VPN Type" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1590 +#: pyanaconda/ui/gui/spokes/network.glade:1593 msgid "Gateway" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1602 +#: pyanaconda/ui/gui/spokes/network.glade:1605 msgid "Group Name" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1614 +#: pyanaconda/ui/gui/spokes/network.glade:1617 msgid "Group Password" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1626 -#: pyanaconda/ui/gui/spokes/user.glade:118 -msgid "Username" +#: pyanaconda/ui/gui/spokes/network.glade:1629 +#: pyanaconda/ui/gui/spokes/user.glade:116 +msgid "User Name" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1733 +#: pyanaconda/ui/gui/spokes/network.glade:1736 msgctxt "GUI|Network|VPN" msgid "_Configure..." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1856 +#: pyanaconda/ui/gui/spokes/network.glade:1859 msgctxt "GUI|Network|Proxy" msgid "_Method" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1881 +#: pyanaconda/ui/gui/spokes/network.glade:1884 msgctxt "GUI|Network|Proxy" msgid "_Configuration URL" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1907 +#: pyanaconda/ui/gui/spokes/network.glade:1909 msgctxt "GUI|Network|Proxy" msgid "HTTP _Proxy" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1921 +#: pyanaconda/ui/gui/spokes/network.glade:1923 msgctxt "GUI|Network|Proxy" msgid "H_TTPS Proxy" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1935 +#: pyanaconda/ui/gui/spokes/network.glade:1937 msgctxt "GUI|Network|Proxy" msgid "_FTP Proxy" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:1949 +#: pyanaconda/ui/gui/spokes/network.glade:1951 msgctxt "GUI|Network|Proxy" msgid "_Socks Host" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2142 +#: pyanaconda/ui/gui/spokes/network.glade:2140 msgid "Network Config Box" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2158 +#: pyanaconda/ui/gui/spokes/network.glade:2156 msgctxt "GUI|Network" msgid "Unloc_k" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2179 +#: pyanaconda/ui/gui/spokes/network.glade:2177 msgctxt "GUI|Network" msgid "_Airplane Mode" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2198 +#: pyanaconda/ui/gui/spokes/network.glade:2196 msgid "More Network Config Box" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2218 +#: pyanaconda/ui/gui/spokes/network.glade:2216 msgctxt "GUI|Network" -msgid "_Hostname:" +msgid "_Host Name:" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2279 +#: pyanaconda/ui/gui/spokes/network.glade:2276 msgid "NETWORK CONFIGURATION" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2324 +#: pyanaconda/ui/gui/spokes/network.glade:2321 msgid "" "We'll need network access to fetch information about your location and to " "make software\n" "updates available for you." msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2354 +#: pyanaconda/ui/gui/spokes/network.glade:2351 msgid "Authentication" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2368 +#: pyanaconda/ui/gui/spokes/network.glade:2365 msgctxt "GUI|Network|Authentication Dialog" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2383 +#: pyanaconda/ui/gui/spokes/network.glade:2380 msgctxt "GUI|Network|Authentication Dialog" msgid "C_onnect" msgstr "" -#: pyanaconda/ui/gui/spokes/network.glade:2457 +#: pyanaconda/ui/gui/spokes/network.glade:2454 msgid "" "Passwords or encryption keys are required to access\n" "the wireless network" @@ -3826,7 +3888,7 @@ msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:27 msgctxt "GUI|Storage|Need Space Dialog" -msgid "Cancel & _add more disks" +msgid "_Cancel & add more disks" msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:43 @@ -3835,39 +3897,39 @@ msgid "_Reclaim space" msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:78 -#: pyanaconda/ui/gui/spokes/storage.glade:295 +#: pyanaconda/ui/gui/spokes/storage.glade:293 msgid "INSTALLATION OPTIONS" msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:95 -#: pyanaconda/ui/gui/spokes/storage.glade:311 +#: pyanaconda/ui/gui/spokes/storage.glade:309 msgid "" "Here we'll describe how much space is needed for the current software " "selection." msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:121 -#: pyanaconda/ui/gui/spokes/storage.glade:337 +#: pyanaconda/ui/gui/spokes/storage.glade:335 msgid "disk free" msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:137 -#: pyanaconda/ui/gui/spokes/storage.glade:352 +#: pyanaconda/ui/gui/spokes/storage.glade:350 msgid "Free space available for use." msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:153 -#: pyanaconda/ui/gui/spokes/storage.glade:368 +#: pyanaconda/ui/gui/spokes/storage.glade:366 msgid "fs free" msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:169 -#: pyanaconda/ui/gui/spokes/storage.glade:384 +#: pyanaconda/ui/gui/spokes/storage.glade:382 msgid "Free space unavailable but reclaimable from existing partitions." msgstr "" #: pyanaconda/ui/gui/spokes/storage.glade:192 -#: pyanaconda/ui/gui/spokes/storage.glade:407 +#: pyanaconda/ui/gui/spokes/storage.glade:405 msgid "Here we'll describe what your options are." msgstr "" @@ -3875,99 +3937,99 @@ msgstr "" msgid "Need Space" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:244 +#: pyanaconda/ui/gui/spokes/storage.glade:242 msgctxt "GUI|Storage|No Space Dialog" msgid "_Quit installer" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:260 +#: pyanaconda/ui/gui/spokes/storage.glade:258 msgctxt "GUI|Storage|No Space Dialog" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:434 +#: pyanaconda/ui/gui/spokes/storage.glade:432 msgid "No Space" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:509 +#: pyanaconda/ui/gui/spokes/storage.glade:507 msgid "Device Selection" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:528 +#: pyanaconda/ui/gui/spokes/storage.glade:526 msgid "" "Select the device(s) you'd like to install to. They will be left untouched " "until you click on the main menu's \"Begin Installation\" button." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:546 +#: pyanaconda/ui/gui/spokes/storage.glade:544 msgid "Local Standard Disks" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:610 -#: pyanaconda/ui/gui/spokes/storage.glade:750 +#: pyanaconda/ui/gui/spokes/storage.glade:608 +#: pyanaconda/ui/gui/spokes/storage.glade:748 msgid "Disks left unselected here will not be touched." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:629 +#: pyanaconda/ui/gui/spokes/storage.glade:627 msgid "Specialized & Network Disks" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:699 +#: pyanaconda/ui/gui/spokes/storage.glade:697 msgctxt "GUI|Storage" msgid "_Add a disk..." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:716 +#: pyanaconda/ui/gui/spokes/storage.glade:714 msgid "Add Specialized Disk" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:769 +#: pyanaconda/ui/gui/spokes/storage.glade:767 msgid "Other Storage Options" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:795 +#: pyanaconda/ui/gui/spokes/storage.glade:793 msgid "Partitioning" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:812 +#: pyanaconda/ui/gui/spokes/storage.glade:810 msgid "Encryption" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:825 +#: pyanaconda/ui/gui/spokes/storage.glade:823 msgid "A_utomatically configure partitioning." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:841 +#: pyanaconda/ui/gui/spokes/storage.glade:839 msgid "_I will configure partitioning." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:873 +#: pyanaconda/ui/gui/spokes/storage.glade:871 msgid "_Encrypt my data." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:888 +#: pyanaconda/ui/gui/spokes/storage.glade:886 msgid "You'll set a passphrase next." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:909 +#: pyanaconda/ui/gui/spokes/storage.glade:907 msgid "I would like to _make additional space available." msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:925 +#: pyanaconda/ui/gui/spokes/storage.glade:923 msgid "Storage Options" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:957 +#: pyanaconda/ui/gui/spokes/storage.glade:955 msgid "summary" msgstr "" -#: pyanaconda/ui/gui/spokes/storage.glade:990 +#: pyanaconda/ui/gui/spokes/storage.glade:988 msgctxt "GUI|Storage" -msgid "_Full disk summary and bootloader..." +msgid "_Full disk summary and boot loader..." msgstr "" #: pyanaconda/ui/gui/spokes/keyboard.glade:15 -#: pyanaconda/ui/gui/spokes/keyboard.glade:389 +#: pyanaconda/ui/gui/spokes/keyboard.glade:388 msgid "KEYBOARD LAYOUT" msgstr "" @@ -4021,76 +4083,69 @@ msgctxt "GUI|Keyboard Layout" msgid "_Test the layout configuration below:" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:332 +#: pyanaconda/ui/gui/spokes/keyboard.glade:331 msgid "Alt + Shift to switch layouts." msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:347 +#: pyanaconda/ui/gui/spokes/keyboard.glade:346 msgctxt "GUI|Keyboard Layout" msgid "_Options" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:422 +#: pyanaconda/ui/gui/spokes/keyboard.glade:421 msgctxt "GUI|Keyboard Layout|Add Layout" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:436 +#: pyanaconda/ui/gui/spokes/keyboard.glade:435 msgctxt "GUI|Keyboard Layout|Add Layout" msgid "_Add" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:468 +#: pyanaconda/ui/gui/spokes/keyboard.glade:467 msgid "ADD A KEYBOARD LAYOUT" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:485 +#: pyanaconda/ui/gui/spokes/keyboard.glade:484 msgid "You may add a keyboard layout by selecting it below:" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:521 -#: pyanaconda/ui/gui/spokes/source.glade:991 -#: pyanaconda/ui/gui/spokes/lib/cart.glade:133 -#: pyanaconda/ui/gui/spokes/lib/resize.glade:163 -msgid "Name" -msgstr "" - -#: pyanaconda/ui/gui/spokes/keyboard.glade:534 +#: pyanaconda/ui/gui/spokes/keyboard.glade:533 msgid "Available Layouts" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:576 +#: pyanaconda/ui/gui/spokes/keyboard.glade:574 msgid "Add Layout" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:608 +#: pyanaconda/ui/gui/spokes/keyboard.glade:606 msgctxt "GUI|Keyboard Layout|Switching Options" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:622 +#: pyanaconda/ui/gui/spokes/keyboard.glade:620 msgctxt "GUI|Keyboard Layout|Switching Options" msgid "_OK" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:654 +#: pyanaconda/ui/gui/spokes/keyboard.glade:652 msgid "LAYOUT SWITCHING OPTIONS" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:671 +#: pyanaconda/ui/gui/spokes/keyboard.glade:669 msgid "" "Which combination(s) would you prefer for switching between keyboard layouts?" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:707 +#: pyanaconda/ui/gui/spokes/keyboard.glade:705 msgid "use" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:720 +#: pyanaconda/ui/gui/spokes/keyboard.glade:718 msgid "description" msgstr "" -#: pyanaconda/ui/gui/spokes/keyboard.glade:754 +#: pyanaconda/ui/gui/spokes/keyboard.glade:752 msgid "Layout Options" msgstr "" @@ -4103,9 +4158,9 @@ msgid "Select additional language support to be installed:" msgstr "" #: pyanaconda/ui/gui/spokes/langsupport.glade:138 -#: pyanaconda/ui/gui/spokes/langsupport.glade:242 +#: pyanaconda/ui/gui/spokes/langsupport.glade:241 #: pyanaconda/ui/gui/spokes/welcome.glade:273 -#: pyanaconda/ui/gui/spokes/welcome.glade:373 +#: pyanaconda/ui/gui/spokes/welcome.glade:372 msgid "nativeName" msgstr "" @@ -4119,12 +4174,12 @@ msgstr "" msgid "selected" msgstr "" -#: pyanaconda/ui/gui/spokes/langsupport.glade:194 -#: pyanaconda/ui/gui/spokes/welcome.glade:337 +#: pyanaconda/ui/gui/spokes/langsupport.glade:193 +#: pyanaconda/ui/gui/spokes/welcome.glade:336 msgid "Type here to search." msgstr "" -#: pyanaconda/ui/gui/spokes/langsupport.glade:228 +#: pyanaconda/ui/gui/spokes/langsupport.glade:227 msgid "checked" msgstr "" @@ -4144,12 +4199,12 @@ msgid "_Confirm:" msgstr "" #: pyanaconda/ui/gui/spokes/password.glade:104 -#: pyanaconda/ui/gui/spokes/user.glade:189 +#: pyanaconda/ui/gui/spokes/user.glade:187 msgid "Confirm Password" msgstr "" #: pyanaconda/ui/gui/spokes/password.glade:150 -#: pyanaconda/ui/gui/spokes/user.glade:252 +#: pyanaconda/ui/gui/spokes/user.glade:251 msgid "empty password" msgstr "" @@ -4189,7 +4244,7 @@ msgstr "" #: pyanaconda/ui/gui/spokes/source.glade:239 msgctxt "GUI|Software Source|Proxy Dialog" -msgid "_Ok" +msgid "_OK" msgstr "" #: pyanaconda/ui/gui/spokes/source.glade:267 @@ -4199,195 +4254,195 @@ msgstr "" #: pyanaconda/ui/gui/spokes/source.glade:300 msgctxt "GUI|Software Source|Proxy Dialog" -msgid "_Proxy URL" +msgid "_Proxy URL:" msgstr "" #: pyanaconda/ui/gui/spokes/source.glade:317 msgid "<span size=\"small\"><b>Example:</b> squid.mysite.org:3128</span>" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:349 +#: pyanaconda/ui/gui/spokes/source.glade:348 msgctxt "GUI|Software Source|Proxy Dialog" msgid "_Use Authentication" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:376 +#: pyanaconda/ui/gui/spokes/source.glade:375 msgctxt "GUI|Software Source|Proxy Dialog" -msgid "User_name" +msgid "User _name:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:393 +#: pyanaconda/ui/gui/spokes/source.glade:392 msgctxt "GUI|Software Source|Proxy Dialog" -msgid "Pass_word" +msgid "Pass_word:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:473 +#: pyanaconda/ui/gui/spokes/source.glade:471 msgid "INSTALLATION SOURCE" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:532 +#: pyanaconda/ui/gui/spokes/source.glade:530 msgid "Which installation source would you like to use?" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:546 +#: pyanaconda/ui/gui/spokes/source.glade:544 msgctxt "GUI|Software Source" msgid "_Auto-detected installation media:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:573 +#: pyanaconda/ui/gui/spokes/source.glade:571 msgid "Device:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:585 +#: pyanaconda/ui/gui/spokes/source.glade:583 msgid "Label:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:594 -#: pyanaconda/ui/gui/spokes/source.glade:694 +#: pyanaconda/ui/gui/spokes/source.glade:592 +#: pyanaconda/ui/gui/spokes/source.glade:692 msgctxt "GUI|Software Source" msgid "_Verify" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:618 +#: pyanaconda/ui/gui/spokes/source.glade:616 msgctxt "GUI|Software Source" msgid "_ISO file:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:646 +#: pyanaconda/ui/gui/spokes/source.glade:644 msgctxt "GUI|Software Source" msgid "D_evice:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:677 +#: pyanaconda/ui/gui/spokes/source.glade:675 msgctxt "GUI|Software Source" msgid "_Choose an ISO" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:718 +#: pyanaconda/ui/gui/spokes/source.glade:716 msgctxt "GUI|Software Source" msgid "_On the network:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:782 +#: pyanaconda/ui/gui/spokes/source.glade:779 msgctxt "GUI|Software Source" msgid "_Proxy setup..." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:797 +#: pyanaconda/ui/gui/spokes/source.glade:794 msgctxt "GUI|Software Source" msgid "This URL refers to a _mirror list." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:835 +#: pyanaconda/ui/gui/spokes/source.glade:832 msgctxt "GUI|Software Source" msgid "N_FS mount options:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:850 -#: pyanaconda/ui/gui/spokes/source.glade:851 +#: pyanaconda/ui/gui/spokes/source.glade:847 +#: pyanaconda/ui/gui/spokes/source.glade:848 msgid "This field is optional." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:885 +#: pyanaconda/ui/gui/spokes/source.glade:881 msgid "Updates" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:912 +#: pyanaconda/ui/gui/spokes/source.glade:908 msgctxt "GUI|Software Source" msgid "" "Don't install the latest available software _updates. Install the default " "versions provided by the installation source above." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:937 +#: pyanaconda/ui/gui/spokes/source.glade:933 msgid "Additional repositories" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:978 +#: pyanaconda/ui/gui/spokes/source.glade:974 msgid "Enabled" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1023 -#: pyanaconda/ui/gui/spokes/source.glade:1024 +#: pyanaconda/ui/gui/spokes/source.glade:1019 +#: pyanaconda/ui/gui/spokes/source.glade:1020 msgid "Add a new repository." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1025 +#: pyanaconda/ui/gui/spokes/source.glade:1021 msgctxt "GUI|Software Source" msgid "A_dd" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1042 -#: pyanaconda/ui/gui/spokes/source.glade:1043 +#: pyanaconda/ui/gui/spokes/source.glade:1038 +#: pyanaconda/ui/gui/spokes/source.glade:1039 msgid "Remove the selected repository." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1044 +#: pyanaconda/ui/gui/spokes/source.glade:1040 msgctxt "GUI|Software Source" msgid "_Remove" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1061 -#: pyanaconda/ui/gui/spokes/source.glade:1062 +#: pyanaconda/ui/gui/spokes/source.glade:1057 +#: pyanaconda/ui/gui/spokes/source.glade:1058 msgid "Revert to the previous list of repositories." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1063 +#: pyanaconda/ui/gui/spokes/source.glade:1059 msgctxt "GUI|Software Source" msgid "Rese_t" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1092 -#: pyanaconda/ui/gui/spokes/source.glade:1093 +#: pyanaconda/ui/gui/spokes/source.glade:1089 +#: pyanaconda/ui/gui/spokes/source.glade:1090 msgid "URL for the repository, without protocol." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1106 -#: pyanaconda/ui/gui/spokes/source.glade:1107 +#: pyanaconda/ui/gui/spokes/source.glade:1102 +#: pyanaconda/ui/gui/spokes/source.glade:1103 msgid "URL of proxy in the form of protocol://host:[port]" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1120 -#: pyanaconda/ui/gui/spokes/source.glade:1121 -msgid "Optional proxy username." +#: pyanaconda/ui/gui/spokes/source.glade:1115 +#: pyanaconda/ui/gui/spokes/source.glade:1116 +msgid "Optional proxy user name." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1134 +#: pyanaconda/ui/gui/spokes/source.glade:1128 msgid "Optional proxy password." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1146 +#: pyanaconda/ui/gui/spokes/source.glade:1140 msgctxt "GUI|Software Source" msgid "This URL refers to a mirror _list." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1166 +#: pyanaconda/ui/gui/spokes/source.glade:1160 msgctxt "GUI|Software Source" msgid "_Name:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1182 -#: pyanaconda/ui/gui/spokes/source.glade:1183 +#: pyanaconda/ui/gui/spokes/source.glade:1176 +#: pyanaconda/ui/gui/spokes/source.glade:1177 msgid "Protocol for the repository URL." msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1204 +#: pyanaconda/ui/gui/spokes/source.glade:1198 msgctxt "GUI|Software Source" msgid "Pro_xy URL:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1221 +#: pyanaconda/ui/gui/spokes/source.glade:1215 msgctxt "GUI|Software Source" -msgid "U_sername:" +msgid "U_ser name:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1238 +#: pyanaconda/ui/gui/spokes/source.glade:1232 msgctxt "GUI|Software Source" msgid "Pass_word:" msgstr "" -#: pyanaconda/ui/gui/spokes/source.glade:1254 -#: pyanaconda/ui/gui/spokes/source.glade:1255 +#: pyanaconda/ui/gui/spokes/source.glade:1248 +#: pyanaconda/ui/gui/spokes/source.glade:1249 msgid "Name of the repository." msgstr "" @@ -4425,29 +4480,29 @@ msgstr "" msgid "Languages" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:389 +#: pyanaconda/ui/gui/spokes/welcome.glade:388 msgid "Locales" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:423 +#: pyanaconda/ui/gui/spokes/welcome.glade:422 msgid "WELCOME" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:444 +#: pyanaconda/ui/gui/spokes/welcome.glade:443 msgctxt "GUI|Welcome|Unsupported Hardware Dialog" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:458 +#: pyanaconda/ui/gui/spokes/welcome.glade:457 msgctxt "GUI|Welcome|Unsupported Hardware Dialog" msgid "_Continue" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:489 +#: pyanaconda/ui/gui/spokes/welcome.glade:488 msgid "Unsupported Hardware Detected" msgstr "" -#: pyanaconda/ui/gui/spokes/welcome.glade:547 +#: pyanaconda/ui/gui/spokes/welcome.glade:546 msgid "Unsupported Hardware" msgstr "" @@ -4463,37 +4518,37 @@ msgstr "" #: pyanaconda/ui/gui/spokes/user.glade:80 msgctxt "GUI|User" -msgid "_Username" +msgid "_User name" msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:101 +#: pyanaconda/ui/gui/spokes/user.glade:100 msgid "Full Name" msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:133 +#: pyanaconda/ui/gui/spokes/user.glade:131 msgctxt "GUI|User" msgid "_Password" msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:151 +#: pyanaconda/ui/gui/spokes/user.glade:149 msgctxt "GUI|User" msgid "_Confirm password" msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:203 +#: pyanaconda/ui/gui/spokes/user.glade:201 msgid "" -"<b>Tip:</b> Keep your username shorter than 32 characters and do not use " +"<b>Tip:</b> Keep your user name shorter than 32 characters and do not use " "spaces." msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:213 +#: pyanaconda/ui/gui/spokes/user.glade:211 msgctxt "GUI|User" -msgid "Require a password to use this account" +msgid "_Require a password to use this account" msgstr "" -#: pyanaconda/ui/gui/spokes/user.glade:271 +#: pyanaconda/ui/gui/spokes/user.glade:270 msgctxt "GUI|User" -msgid "Make this user administrator" +msgid "_Make this user administrator" msgstr "" #: pyanaconda/ui/gui/spokes/user.glade:289 @@ -4501,182 +4556,178 @@ msgctxt "GUI|User" msgid "_Advanced..." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:27 -msgid "Btrfs" -msgstr "" - #: pyanaconda/ui/gui/spokes/custom.glade:62 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:540 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:539 msgid "RAID0 <span foreground=\"grey\">(Performance)</span>" msgstr "" #: pyanaconda/ui/gui/spokes/custom.glade:66 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:544 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:543 msgid "RAID1 <span foreground=\"grey\">(Redundancy)</span>" msgstr "" #: pyanaconda/ui/gui/spokes/custom.glade:70 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:548 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:547 msgid "RAID4 <span foreground=\"grey\">(Error Checking)</span>" msgstr "" #: pyanaconda/ui/gui/spokes/custom.glade:74 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:552 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:551 msgid "RAID5 <span foreground=\"grey\">(Distributed Error Checking)</span>" msgstr "" #: pyanaconda/ui/gui/spokes/custom.glade:78 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:556 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:555 msgid "RAID6 <span foreground=\"grey\">(Redundant Error Checking)</span>" msgstr "" #: pyanaconda/ui/gui/spokes/custom.glade:82 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:560 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:559 msgid "RAID10 <span foreground=\"grey\">(Performance, Redundancy)</span>" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:184 +#: pyanaconda/ui/gui/spokes/custom.glade:181 msgid "Add a new mount point." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:185 +#: pyanaconda/ui/gui/spokes/custom.glade:182 msgctxt "GUI|Custom Partitioning" msgid "Add" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:200 +#: pyanaconda/ui/gui/spokes/custom.glade:197 msgid "Remove the selected mount point(s)." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:201 +#: pyanaconda/ui/gui/spokes/custom.glade:198 msgctxt "GUI|Custom Partitioning" msgid "Remove" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:216 +#: pyanaconda/ui/gui/spokes/custom.glade:213 msgid "Reload storage configuration from disk." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:217 +#: pyanaconda/ui/gui/spokes/custom.glade:214 msgctxt "GUI|Custom Partitioning" msgid "Refresh" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:282 -#: pyanaconda/ui/gui/spokes/custom.glade:1014 -#: pyanaconda/ui/gui/spokes/custom.glade:1158 -#: pyanaconda/ui/gui/spokes/custom.glade:1245 +#: pyanaconda/ui/gui/spokes/custom.glade:279 +#: pyanaconda/ui/gui/spokes/custom.glade:1013 +#: pyanaconda/ui/gui/spokes/custom.glade:1157 +#: pyanaconda/ui/gui/spokes/custom.glade:1244 msgid "Selected Device" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:300 -#: pyanaconda/ui/gui/spokes/custom.glade:1032 -#: pyanaconda/ui/gui/spokes/custom.glade:1176 -#: pyanaconda/ui/gui/spokes/custom.glade:1263 +#: pyanaconda/ui/gui/spokes/custom.glade:297 +#: pyanaconda/ui/gui/spokes/custom.glade:1031 +#: pyanaconda/ui/gui/spokes/custom.glade:1175 +#: pyanaconda/ui/gui/spokes/custom.glade:1262 msgid "Device description" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:361 +#: pyanaconda/ui/gui/spokes/custom.glade:358 msgid "Mount _Point:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:409 +#: pyanaconda/ui/gui/spokes/custom.glade:405 msgid "_Desired Capacity:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:461 +#: pyanaconda/ui/gui/spokes/custom.glade:456 msgid "_Label:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:509 +#: pyanaconda/ui/gui/spokes/custom.glade:503 msgid "Device(s):" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:539 +#: pyanaconda/ui/gui/spokes/custom.glade:533 msgctxt "GUI|Custom Partitioning|Configure|Devices" msgid "_Modify..." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:571 +#: pyanaconda/ui/gui/spokes/custom.glade:565 msgid "_Name:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:627 +#: pyanaconda/ui/gui/spokes/custom.glade:620 msgctxt "GUI|Custom Partitioning|Configure" msgid "Device _Type:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:669 +#: pyanaconda/ui/gui/spokes/custom.glade:664 msgctxt "GUI|Custom Partitioning|Configure" msgid "_Encrypt" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:710 +#: pyanaconda/ui/gui/spokes/custom.glade:705 msgctxt "GUI|Custom Partitioning|Configure" msgid "File S_ystem:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:745 +#: pyanaconda/ui/gui/spokes/custom.glade:740 msgctxt "GUI|Custom Partitioning|Configure" msgid "Ref_ormat" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:799 +#: pyanaconda/ui/gui/spokes/custom.glade:794 msgctxt "GUI|Custom Partitioning|Configure" msgid "_Volume Group:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:851 +#: pyanaconda/ui/gui/spokes/custom.glade:848 msgctxt "GUI|Custom Partitioning|Configure" msgid "_Modify..." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:883 +#: pyanaconda/ui/gui/spokes/custom.glade:880 msgctxt "GUI|Custom Partitioning|Configure" msgid "RA_ID Level:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:946 +#: pyanaconda/ui/gui/spokes/custom.glade:945 msgid "_Update Settings" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:969 +#: pyanaconda/ui/gui/spokes/custom.glade:968 msgid "" "Note: The settings you make on this screen will not be applied until you " "click on the main menu's 'Begin Installation' button." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1066 +#: pyanaconda/ui/gui/spokes/custom.glade:1065 msgid "" "This device is encrypted and cannot be read without a valid passphrase. You " "may unlock it below." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1087 +#: pyanaconda/ui/gui/spokes/custom.glade:1086 msgctxt "GUI|Custom Partitioning|Encrypted" msgid "_Passphrase:" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1115 +#: pyanaconda/ui/gui/spokes/custom.glade:1114 msgctxt "GUI|Custom Partitioning|Encrypted" msgid "_Unlock" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1210 +#: pyanaconda/ui/gui/spokes/custom.glade:1209 msgid "" "This device cannot be edited directly. You can remove it or select a " "different device." msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1358 +#: pyanaconda/ui/gui/spokes/custom.glade:1357 msgid "AVAILABLE SPACE" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1411 +#: pyanaconda/ui/gui/spokes/custom.glade:1410 msgid "TOTAL SPACE" msgstr "" -#: pyanaconda/ui/gui/spokes/custom.glade:1451 +#: pyanaconda/ui/gui/spokes/custom.glade:1450 msgctxt "GUI|Custom Partitioning" msgid "_Reset All" msgstr "" @@ -4703,7 +4754,7 @@ msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/fcoe.glade:108 msgctxt "GUI|Advanced Storage|FCoE" -msgid "Use auto _vlan" +msgid "Use auto _VLAN" msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/fcoe.glade:144 @@ -4721,27 +4772,27 @@ msgid "" "check your configuration and try again" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:38 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:40 msgctxt "GUI|Advanced Storage|iSCSI|Configure" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:52 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:54 msgctxt "GUI|Advanced Storage|iSCSI|Configure" msgid "_OK" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:84 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:86 msgid "ADD iSCSI STORAGE TARGET" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:113 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:115 msgid "" "To use iSCSI disks, you must provide the address of your iSCSI target and " "the iSCSI initiator name you've configured for your host." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:141 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:142 msgctxt "GUI|Advanced Storage|iSCSI|Configure" msgid "_Target IP Address:" msgstr "" @@ -4752,17 +4803,17 @@ msgid "iSCSI _Initiator Name:" msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:185 -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:743 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:751 msgid "No credentials (discovery authentication disabled)" msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:186 -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:744 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:752 msgid "CHAP pair" msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:187 -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:745 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:753 msgid "CHAP pair and a reverse pair" msgstr "" @@ -4779,7 +4830,7 @@ msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:262 msgctxt "GUI|Advanced Storage|iSCSI|Configure|CHAP" -msgid "CHAP _Username:" +msgid "CHAP _User name:" msgstr "" #: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:276 @@ -4787,118 +4838,122 @@ msgctxt "GUI|Advanced Storage|iSCSI|Configure|CHAP" msgid "CHAP _Password:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:385 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:382 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP" -msgid "CHAP _Username:" +msgid "CHAP _User name:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:399 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:396 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP" msgid "CHAP _Password:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:413 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:410 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP" -msgid "Reverse CHAP User_name:" +msgid "Reverse CHAP User _name:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:427 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:424 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP" msgid "Reverse CHAP Pass_word:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:460 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:457 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Start" msgid "_Start Discovery" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:497 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:494 msgid "Discovering iSCSI targets. This may take a moment..." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:537 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:534 msgid "Discovery login rejected." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:552 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:549 msgid "" "The following error occurred discovering iSCSI targets. Please double check " "your authorization information and try again." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:574 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:571 msgctxt "GUI|Advanced Storage|iSCSI|Configure|Retry" msgid "_Retry Discovery" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:614 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:611 msgctxt "GUI|Advanced Storage|iSCSI|Configure" msgid "_Bind targets to network interfaces" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:689 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:686 msgid "Node Name" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:700 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:697 msgid "Interface" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:729 -msgctxt "GUI|Advanced Storage|iSCSI|Login" -msgid "_Node login authentication type:" +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:708 +msgid "Portal" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:746 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:737 +msgctxt "GUI|Advanced Storage|iSCSI|Login" +msgid "_Node Login Authentication Type:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:754 msgid "Use the credentials from discovery" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:781 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:789 msgctxt "GUI|Advanced Storage|iSCSI|Login|CHAP" -msgid "CHAP _Username:" +msgid "CHAP _User name:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:795 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:803 msgctxt "GUI|Advanced Storage|iSCSI|Login|CHAP" msgid "CHAP _Password:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:904 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:909 msgctxt "GUI|Advanced Storage|iSCSI|Login|Reverse CHAP" -msgid "CHAP _Username:" +msgid "CHAP _User name:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:918 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:923 msgctxt "GUI|Advanced Storage|iSCSI|Login|Reverse CHAP" msgid "CHAP _Password:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:932 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:937 msgctxt "GUI|Advanced Storage|iSCSI|Login|Reverse CHAP" -msgid "_Reverse CHAP Username:" +msgid "_Reverse CHAP User name:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:946 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:951 msgctxt "GUI|Advanced Storage|iSCSI|Login|Reverse CHAP" msgid "Reverse CHAP Pass_word:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:994 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:999 msgctxt "GUI|Advanced Storage|iSCSI|Login|Login" msgid "_Log In" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1050 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1055 msgid "Node login failed." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1065 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1070 msgid "" "The following error occurred logging into the selected iSCSI node. Please " "double check your authorization information and try again" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1087 +#: pyanaconda/ui/gui/spokes/advstorage/iscsi.glade:1092 msgctxt "GUI|Advanced Storage|iSCSI|Login|Retry" msgid "Retry _Log In" msgstr "" @@ -4921,35 +4976,79 @@ msgstr "" msgid "To use DASD disks, you must provide the device number." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:126 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:125 msgctxt "GUI|Advanced Storage|DASD|Configure" msgid "_Device number:" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:146 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:145 msgctxt "GUI|Advanced Storage|DASD|Configure" msgid "_Start Discovery" msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:183 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:182 msgid "Discovering DASD devices. This may take a moment..." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:225 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:224 +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:267 msgid "Device discovery failed." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:242 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:241 msgid "" "The following error occurred discovering DASD devices. Please double check " "your configuration information and try again." msgstr "" -#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:268 +#: pyanaconda/ui/gui/spokes/advstorage/dasd.glade:267 msgctxt "GUI|Advanced Storage|DASD|Configure" msgid "_Retry Discovery" msgstr "" +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:82 +msgid "ADD zFCP STORAGE TARGET" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:111 +msgid "" +"To use zFCP disks, you must provide the device number, WWPN, and LUN " +"configured for the device." +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:139 +msgctxt "GUI|Advanced Storage|zFCP|Device Number" +msgid "_Device number:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:168 +msgid "WWPN:" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:188 +msgctxt "GUI|Advanced Storage|zFCP|Start Discovery" +msgid "_Start Discovery" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:225 +msgid "Discovering zFCP devices. This may take a moment..." +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:284 +msgid "" +"The following error occurred discovering zFCP devices. Please double check " +"your configuration information and try again." +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:310 +msgctxt "GUI|Advanced Storage|zFCP|Retry Discovery" +msgid "_Retry Discovery" +msgstr "" + +#: pyanaconda/ui/gui/spokes/advstorage/zfcp.glade:356 +msgid "LUN:" +msgstr "" + #: pyanaconda/ui/gui/spokes/lib/cart.glade:26 #: pyanaconda/ui/gui/spokes/lib/cart.glade:78 msgid "SELECTED DISKS" @@ -4965,12 +5064,13 @@ msgid "Boot" msgstr "" #: pyanaconda/ui/gui/spokes/lib/cart.glade:120 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:222 msgid "Description" msgstr "" #: pyanaconda/ui/gui/spokes/lib/cart.glade:158 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:248 -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:741 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:261 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:739 msgid "Free" msgstr "" @@ -5051,60 +5151,60 @@ msgstr "" msgid "eg: \"20 GB\", \"500mb\" (minus the quotation marks)" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:536 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:535 msgid "Single <span foreground=\"grey\">(No Redundancy, No Striping)</span>" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:584 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:583 msgctxt "GUI|Custom Partitioning|Container Dialog" msgid "_Cancel" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:598 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:597 msgctxt "GUI|Custom Partitioning|Container Dialog" msgid "_Save" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:630 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:629 msgid "CONFIGURE CONTAINER" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:645 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:644 msgid "" "Please create a name for this container and select at least one disk below." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:663 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:662 msgctxt "GUI|Custom Partitioning|Container Dialog" msgid "_Name:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:783 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:781 msgid "RAID Level:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:811 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:809 msgid "Encrypt" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:852 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:850 msgid "Please enter a valid name." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:882 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:880 msgctxt "GUI|Custom Partitioning|Container Dialog" msgid "Si_ze policy:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:897 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:895 msgid "Automatic" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:898 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:896 msgid "As large as possible" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:899 +#: pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade:897 msgid "Fixed" msgstr "" @@ -5126,7 +5226,7 @@ msgstr "" msgid "" "The following unformatted DASDs have been detected on your system. You can " "choose to format them now with dasdfmt or cancel to leave them unformatted. " -"Unformatted DASDs can not be used during installation." +"Unformatted DASDs cannot be used during installation." msgstr "" #: pyanaconda/ui/gui/spokes/lib/dasdfmt.glade:86 @@ -5154,13 +5254,6 @@ msgstr "" msgid "Disk formatting complete." msgstr "" -#: pyanaconda/ui/gui/spokes/lib/dasdfmt.glade:262 -#: pyanaconda/ui/gui/spokes/lib/refresh.glade:269 -msgid "" -"Pressing 'OK' below will take you to the disk selection screen where you " -"will need to re-select your disks." -msgstr "" - #: pyanaconda/ui/gui/spokes/lib/detailederror.glade:25 msgid "An unknown error occurred during installation. Details are below." msgstr "" @@ -5249,6 +5342,12 @@ msgstr "" msgid "Disk rescan complete." msgstr "" +#: pyanaconda/ui/gui/spokes/lib/refresh.glade:269 +msgid "" +"Pressing 'OK' below will take you to the disk selection screen where you " +"will need to re-select your disks." +msgstr "" + #: pyanaconda/ui/gui/spokes/lib/resize.glade:55 msgctxt "GUI|Reclaim Dialog" msgid "_Cancel" @@ -5268,7 +5367,7 @@ msgid "Description goes here." msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.glade:174 -msgid "Filesystem" +msgid "File System" msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.glade:185 @@ -5276,7 +5375,7 @@ msgid "Reclaimable Space" msgstr "" #: pyanaconda/ui/gui/spokes/lib/resize.glade:196 -#: pyanaconda/ui/gui/spokes/lib/summary.glade:141 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:144 msgid "Action" msgstr "" @@ -5295,37 +5394,41 @@ msgctxt "GUI|Reclaim Dialog" msgid "_Shrink" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:23 -#: pyanaconda/ui/gui/spokes/lib/summary.glade:87 +#: pyanaconda/ui/gui/spokes/lib/resize.glade:367 +msgid "Reclaim" +msgstr "" + +#: pyanaconda/ui/gui/spokes/lib/summary.glade:25 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:89 msgid "SUMMARY OF CHANGES" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:40 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:42 msgctxt "GUI|Summary Dialog" msgid "_Cancel & Return to Custom Partitioning" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:55 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:57 msgctxt "GUI|Summary Dialog" msgid "_Accept Changes" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:103 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:105 msgid "" "Your customizations will result in the following changes taking effect after " "you return to the main menu and begin installation:" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:130 +#: pyanaconda/ui/gui/spokes/lib/summary.glade:133 msgid "Order" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:163 -msgid "Device Name" +#: pyanaconda/ui/gui/spokes/lib/summary.glade:166 +msgid "Device" msgstr "" -#: pyanaconda/ui/gui/spokes/lib/summary.glade:174 -msgid "Mountpoint" +#: pyanaconda/ui/gui/spokes/lib/summary.glade:177 +msgid "Mount point" msgstr "" #: pyanaconda/ui/gui/spokes/lib/entropy_dialog.glade:35 @@ -5364,39 +5467,39 @@ msgid "Quit" msgstr "" #: pyanaconda/ui/gui/hubs/summary.glade:8 -#: pyanaconda/ui/gui/hubs/summary.glade:143 +#: pyanaconda/ui/gui/hubs/summary.glade:147 msgid "INSTALLATION SUMMARY" msgstr "" -#: pyanaconda/ui/gui/hubs/summary.glade:80 +#: pyanaconda/ui/gui/hubs/summary.glade:84 msgctxt "GUI|Summary" msgid "_Quit" msgstr "" -#: pyanaconda/ui/gui/hubs/summary.glade:96 +#: pyanaconda/ui/gui/hubs/summary.glade:100 msgctxt "GUI|Summary" msgid "_Begin Installation" msgstr "" -#: pyanaconda/ui/gui/hubs/summary.glade:127 +#: pyanaconda/ui/gui/hubs/summary.glade:131 msgid "We won't touch your disks until you click 'Begin Installation'." msgstr "" #: pyanaconda/ui/gui/hubs/progress.glade:8 -#: pyanaconda/ui/gui/hubs/progress.glade:257 +#: pyanaconda/ui/gui/hubs/progress.glade:261 msgid "CONFIGURATION" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.glade:87 +#: pyanaconda/ui/gui/hubs/progress.glade:91 msgid "Preparing to install" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.glade:145 +#: pyanaconda/ui/gui/hubs/progress.glade:149 msgctxt "GUI|Progress" msgid "_Finish configuration" msgstr "" -#: pyanaconda/ui/gui/hubs/progress.glade:198 +#: pyanaconda/ui/gui/hubs/progress.glade:202 msgctxt "GUI|Progress" msgid "_Reboot" msgstr "" @@ -5413,7 +5516,7 @@ msgstr "" msgid "Install the live CD to your hard disk" msgstr "" -#: /home/vpodzime/anaconda/data/liveinst/console.apps/liveinst:5 +#: /home/sbueno/scm/anaconda/data/liveinst/console.apps/liveinst:5 msgid "Starting Install to Hard Drive" msgstr "" @@ -5422,14 +5525,14 @@ msgstr "" msgid "Welcome to Fedora" msgstr "" -#: anaconda:594 +#: anaconda:463 #, python-format msgid "" "%(product_name)s requires %(needed_ram)s MB of memory to install, but you " "only have %(total_ram)s MB on this machine.\n" msgstr "" -#: anaconda:596 +#: anaconda:465 #, python-format msgid "" "The %(product_name)s graphical installer requires %(needed_ram)s MB of " @@ -5437,17 +5540,17 @@ msgid "" "." msgstr "" -#: anaconda:599 +#: anaconda:468 msgid "" "\n" "Press <return> to reboot your system.\n" msgstr "" -#: anaconda:601 +#: anaconda:470 msgid "Not enough RAM" msgstr "" -#: anaconda:602 +#: anaconda:471 msgid "" " Try the text mode installer by running:\n" "\n" @@ -5456,38 +5559,49 @@ msgid "" " from a root terminal." msgstr "" -#: anaconda:605 +#: anaconda:474 msgid " Starting text mode." msgstr "" -#: anaconda:642 +#: anaconda:511 msgid "Fatal Error" msgstr "" -#: anaconda:767 +#: anaconda:636 msgid "DISPLAY variable not set. Starting text mode." msgstr "" -#: anaconda:775 +#: anaconda:644 msgid "Graphical installation is not available. Starting text mode." msgstr "" -#: anaconda:783 +#: anaconda:652 msgid "" "Text mode provides a limited set of installation options. It does not offer " "custom partitioning for full control over the disk layout. Would you like to " "use VNC mode instead?" msgstr "" -#: anaconda:872 +#: anaconda:742 #, python-format msgid "Please ssh install@%s to begin the install." msgstr "" -#: anaconda:874 +#: anaconda:744 msgid "Please ssh install@<host> to continue installation." msgstr "" +#: anaconda:985 +msgid "Unable to create PID file" +msgstr "" + +#: anaconda:986 +#, python-format +msgid "" +"Anaconda is unable to create %s because the file already exists. Anaconda is " +"already running, or a previous instance of anaconda has crashed." +msgstr "" + #: data/liveinst/gnome/fedora-welcome:94 msgid "Try Fedora" msgstr "" diff --git a/anaconda/pyanaconda/addons.py b/anaconda/pyanaconda/addons.py index d360946..24a2367 100644 --- a/anaconda/pyanaconda/addons.py +++ b/anaconda/pyanaconda/addons.py @@ -56,9 +56,9 @@ def collect_addon_paths(toplevel_addon_paths, ui_subdir="gui"): if os.path.isdir(addon_spoke_path): module_paths["spokes"].append(("%s.%s.spokes.%%s" % (addon_id, ui_subdir), addon_spoke_path)) - addon_category_path = os.path.join(path, addon_id, ui_subdir, "categories") - if os.path.isdir(addon_spoke_path): - module_paths["categories"].append(("%s.%s.categories.%%s" % (addon_id, ui_subdir), addon_category_path)) + addon_category_path = os.path.join(path, addon_id, "categories") + if os.path.isdir(addon_category_path): + module_paths["categories"].append(("%s.categories.%%s" % addon_id, addon_category_path)) return module_paths @@ -75,17 +75,17 @@ class AddonRegistry(object): def __str__(self): return functools.reduce(lambda acc, (id, addon): acc + str(addon), - self.__dict__.iteritems(), "") + self.__dict__.items(), "") def execute(self, storage, ksdata, instClass, users): """This method calls execute on all the registered addons.""" - for v in self.__dict__.itervalues(): + for v in self.__dict__.values(): if hasattr(v, "execute"): v.execute(storage, ksdata, instClass, users) def setup(self, storage, ksdata, instClass): """This method calls setup on all the registered addons.""" - for v in self.__dict__.itervalues(): + for v in self.__dict__.values(): if hasattr(v, "setup"): v.setup(storage, ksdata, instClass) diff --git a/anaconda/pyanaconda/anaconda.py b/anaconda/pyanaconda/anaconda.py index 57116bb..cb4138d 100644 --- a/anaconda/pyanaconda/anaconda.py +++ b/anaconda/pyanaconda/anaconda.py @@ -188,7 +188,7 @@ class Anaconda(object): # gather up info on the running threads threads = "\nThreads\n-------\n" - for thread_id, frame in sys._current_frames().iteritems(): + for thread_id, frame in sys._current_frames().items(): threads += "\nThread %s\n" % (thread_id,) threads += "".join(format_stack(frame)) @@ -211,8 +211,11 @@ class Anaconda(object): if self.displayMode == 'g': from pyanaconda.ui.gui import GraphicalUserInterface + # Run the GUI in non-fullscreen mode, so live installs can still + # use the window manager self._intf = GraphicalUserInterface(self.storage, self.payload, - self.instClass, gui_lock=self.gui_initialized) + self.instClass, gui_lock=self.gui_initialized, + fullscreen=False) # needs to be refreshed now we know if gui or tui will take place addon_paths = addons.collect_addon_paths(constants.ADDON_PATHS, diff --git a/anaconda/pyanaconda/anaconda_argparse.py b/anaconda/pyanaconda/anaconda_argparse.py index c7ac77f..916397f 100644 --- a/anaconda/pyanaconda/anaconda_argparse.py +++ b/anaconda/pyanaconda/anaconda_argparse.py @@ -161,7 +161,7 @@ class AnacondaArgumentParser(ArgumentParser): according to the option definitions set by add_argument. boot_cmdline can be given as a string (to be parsed by BootArgs), or a - dict (or any object with .iteritems()) of {bootarg:value} pairs. + dict (or any object with .items()) of {bootarg:value} pairs. If boot_cmdline is None, the boot_cmdline data will be whatever BootArgs reads by default (/proc/cmdline, /run/initramfs/etc/cmdline, /etc/cmdline). @@ -186,7 +186,7 @@ class AnacondaArgumentParser(ArgumentParser): # go over all options corresponding to current boot cmdline # and do any modifications necessary # NOTE: program cmdline overrides boot cmdline - for arg, val in bootargs.iteritems(): + for arg, val in bootargs.items(): option = self._get_bootarg_option(arg) if option is None: # this boot option is unknown to Anaconda, skip it @@ -349,7 +349,7 @@ class HelpTextParser(object): with open(self._path) as lines: for parsed_option, parsed_text in self.read(lines): self._help_text[parsed_option] = parsed_text - except StandardError: + except Exception: # pylint: disable=broad-except log.error("error reading help text file %s", self._path) return self._help_text.get(option, "") diff --git a/anaconda/pyanaconda/anaconda_log.py b/anaconda/pyanaconda/anaconda_log.py index 5c7b78b..8cb48ec 100644 --- a/anaconda/pyanaconda/anaconda_log.py +++ b/anaconda/pyanaconda/anaconda_log.py @@ -23,7 +23,7 @@ # import logging -from logging.handlers import SysLogHandler, SYSLOG_UDP_PORT +from logging.handlers import SysLogHandler, SocketHandler, SYSLOG_UDP_PORT import os import sys import types @@ -32,16 +32,13 @@ import warnings from pyanaconda.flags import flags from pyanaconda.constants import LOGLVL_LOCK -DEFAULT_TTY_LEVEL = logging.INFO +DEFAULT_LEVEL = logging.INFO ENTRY_FORMAT = "%(asctime)s,%(msecs)03d %(levelname)s %(name)s: %(message)s" -TTY_FORMAT = "%(levelname)s %(name)s: %(message)s" STDOUT_FORMAT = "%(asctime)s %(message)s" DATE_FORMAT = "%H:%M:%S" MAIN_LOG_FILE = "/tmp/anaconda.log" -MAIN_LOG_TTY = "/dev/tty3" PROGRAM_LOG_FILE = "/tmp/program.log" -PROGRAM_LOG_TTY = "/dev/tty5" STORAGE_LOG_FILE = "/tmp/storage.log" PACKAGING_LOG_FILE = "/tmp/packaging.log" SENSITIVE_INFO_LOG_FILE = "/tmp/sensitive-info.log" @@ -89,12 +86,16 @@ class AnacondaSyslogHandler(SysLogHandler): """Map the priority level to a syslog level """ return self.levelMap.get(level, SysLogHandler.mapPriority(self, level)) +class AnacondaSocketHandler(SocketHandler): + def makePickle(self, record): + return self.formatter.format(record) + "\n" + class AnacondaLog: SYSLOG_CFGFILE = "/etc/rsyslog.conf" VIRTIO_PORT = "/dev/virtio-ports/org.fedoraproject.anaconda.log.0" def __init__ (self): - self.tty_loglevel = DEFAULT_TTY_LEVEL + self.loglevel = DEFAULT_LEVEL self.remote_syslog = None # Rename the loglevels so they are the same as in syslog. logging.addLevelName(logging.WARNING, "WARN") @@ -117,22 +118,12 @@ class AnacondaLog: for logr in [self.anaconda_logger, storage_logger]: logr.setLevel(logging.DEBUG) self.forwardToSyslog(logr) - # Logging of basic stuff and storage to tty3. - # XXX Use os.uname here since it's too early to be importing the - # storage module. - if not os.uname()[4].startswith('s390') and os.access(MAIN_LOG_TTY, os.W_OK): - self.addFileHandler(MAIN_LOG_TTY, logr, - fmtStr=TTY_FORMAT, - autoLevel=True) # External program output log program_logger = logging.getLogger("program") program_logger.setLevel(logging.DEBUG) self.addFileHandler(PROGRAM_LOG_FILE, program_logger, minLevel=logging.DEBUG) - self.addFileHandler(PROGRAM_LOG_TTY, program_logger, - fmtStr=TTY_FORMAT, - autoLevel=True) self.forwardToSyslog(program_logger) # Create the packaging logger. @@ -175,7 +166,7 @@ class AnacondaLog: fmtStr=STDOUT_FORMAT, minLevel=logging.INFO) # Add a simple handler - file or stream, depending on what we're given. - def addFileHandler (self, dest, addToLogger, minLevel=DEFAULT_TTY_LEVEL, + def addFileHandler (self, dest, addToLogger, minLevel=DEFAULT_LEVEL, fmtStr=ENTRY_FORMAT, autoLevel=False): try: @@ -216,8 +207,16 @@ class AnacondaLog: self.anaconda_logger.warning("%s", warnings.formatwarning( message, category, filename, lineno, line)) + def setup_remotelog(self, host, port): + remotelog = AnacondaSocketHandler(host, port) + remotelog.setFormatter(logging.Formatter(ENTRY_FORMAT, DATE_FORMAT)) + remotelog.setLevel(logging.DEBUG) + logging.getLogger().addHandler(remotelog) + def restartSyslog(self): - os.system("systemctl restart rsyslog.service") + # Import here instead of at the module level to avoid an import loop + from pyanaconda.iutil import execWithRedirect + execWithRedirect("systemctl", ["restart", "rsyslog.service"]) def updateRemote(self, remote_syslog): """Updates the location of remote rsyslogd to forward to. @@ -237,12 +236,13 @@ class AnacondaLog: """ TEMPLATE = "*.* %s;anaconda_syslog\n" - if not os.path.exists(self.VIRTIO_PORT) \ - or not os.access(self.VIRTIO_PORT, os.W_OK): + vport = flags.cmdline.get('virtiolog', self.VIRTIO_PORT) + + if not os.access(vport, os.W_OK): return with open(self.SYSLOG_CFGFILE, 'a') as cfgfile: - cfgfile.write(TEMPLATE % (self.VIRTIO_PORT,)) + cfgfile.write(TEMPLATE % (vport,)) self.restartSyslog() diff --git a/anaconda/pyanaconda/bootloader.py b/anaconda/pyanaconda/bootloader.py index c5360c2..85730eb 100644 --- a/anaconda/pyanaconda/bootloader.py +++ b/anaconda/pyanaconda/bootloader.py @@ -28,12 +28,13 @@ import shutil import struct import blivet from parted import PARTITION_BIOS_GRUB +from glob import glob from pyanaconda import iutil from blivet.devicelibs import raid from pyanaconda.isys import sync from pyanaconda.product import productName -from pyanaconda.flags import flags +from pyanaconda.flags import flags, can_touch_runtime_system from blivet.errors import StorageError from blivet.fcoe import fcoe import pyanaconda.network @@ -245,7 +246,7 @@ class BootLoader(object): stage2_mountpoints = ["/boot", "/"] stage2_bootable = False stage2_must_be_primary = True - stage2_description = N_("/boot filesystem") + stage2_description = N_("/boot file system") stage2_max_end = Size("2 TiB") @property @@ -779,7 +780,7 @@ class BootLoader(object): @update_only.setter def update_only(self, value): if value and not self.can_update: - raise ValueError("this bootloader does not support updates") + raise ValueError("this boot loader does not support updates") elif self.can_update: self._update_only = value @@ -818,6 +819,14 @@ class BootLoader(object): if usr_device: dracut_devices.extend([usr_device]) + netdevs = storage.devicetree.getDevicesByInstance(NetworkStorageDevice) + rootdev = storage.rootDevice + if any(rootdev.dependsOn(netdev) for netdev in netdevs): + dracut_devices = set(dracut_devices) + for dev in storage.mountpoints.values(): + if any(dev.dependsOn(netdev) for netdev in netdevs): + dracut_devices.add(dev) + done = [] for device in dracut_devices: for dep in storage.devices: @@ -867,6 +876,11 @@ class BootLoader(object): continue self.boot_args.add("ifname=%s:%s" % (nic, hwaddr.lower())) + # Add iscsi_firmware to trigger dracut running iscsistart + # See rhbz#1099603 and rhbz#1185792 + if len(glob("/sys/firmware/iscsi_boot*")) > 0: + self.boot_args.add("iscsi_firmware") + # # preservation of most of our boot args # @@ -927,7 +941,7 @@ class BootLoader(object): def write_config(self): """ Write the bootloader configuration. """ if not self.config_file: - raise BootLoaderError("no config file defined for this bootloader") + raise BootLoaderError("no config file defined for this boot loader") config_path = os.path.normpath(iutil.getSysroot() + self.config_file) if os.access(config_path, os.R_OK): @@ -1096,12 +1110,13 @@ class GRUB(BootLoader): if not self.password: raise BootLoaderError("cannot encrypt empty password") - import string + # Used for ascii_letters and digits constants + import string # pylint: disable=deprecated-module import crypt import random salt = "$6$" salt_len = 16 - salt_chars = string.letters + string.digits + './' + salt_chars = string.ascii_letters + string.digits + './' rand_gen = random.SystemRandom() salt += "".join(rand_gen.choice(salt_chars) for i in range(salt_len)) @@ -1272,16 +1287,28 @@ class GRUB(BootLoader): # add the redundant targets if installing stage1 to a disk that is # a member of the stage2 array. + # Look for both mdraid and btrfs raid if self.stage2_device.type == "mdarray" and \ - self.stage2_device.level == raid.RAID1 and \ + self.stage2_device.level == raid.RAID1: + stage2_raid = True + # Set parents to the list of partitions in the RAID + stage2_parents = self.stage2_device.parents + elif self.stage2_device.type == "btrfs subvolume" and \ + self.stage2_device.parents[0].dataLevel == raid.RAID1: + stage2_raid = True + # Set parents to the list of partitions in the parent volume + stage2_parents = self.stage2_device.parents[0].parents + else: + stage2_raid = False + + if stage2_raid and \ self.stage1_device.isDisk and \ self.stage2_device.dependsOn(self.stage1_device): - for stage2dev in self.stage2_device.parents: + for stage2dev in stage2_parents: # if target disk contains any of /boot array's member # partitions, set up stage1 on each member's disk - # and stage2 on each member partition stage1dev = stage2dev.disk - targets.append((stage1dev, stage2dev)) + targets.append((stage1dev, self.stage2_device)) else: targets.append((self.stage1_device, self.stage2_device)) @@ -1290,7 +1317,7 @@ class GRUB(BootLoader): def install(self, args=None): rc = iutil.execInSysroot("grub-install", ["--just-copy"]) if rc: - raise BootLoaderError("bootloader install failed") + raise BootLoaderError("boot loader install failed") for (stage1dev, stage2dev) in self.install_targets: cmd = ("root %(stage2dev)s\n" @@ -1311,7 +1338,7 @@ class GRUB(BootLoader): rc = iutil.execInSysroot("grub", args, stdin=pread) iutil.eintr_retry_call(os.close, pread) if rc: - raise BootLoaderError("bootloader install failed") + raise BootLoaderError("boot loader install failed") def update(self): self.install() @@ -1342,14 +1369,14 @@ class GRUB(BootLoader): self.stage2_device.level == raid.RAID1 and \ self.stage1_device.type != "mdarray": if not self.stage1_device.isDisk: - msg = _("bootloader stage2 device %(stage2dev)s is on a multi-disk array, but bootloader stage1 device %(stage1dev)s is not. " \ + msg = _("boot loader stage2 device %(stage2dev)s is on a multi-disk array, but boot loader stage1 device %(stage1dev)s is not. " \ "A drive failure in %(stage2dev)s could render the system unbootable.") % \ {"stage1dev" : self.stage1_device.name, "stage2dev" : self.stage2_device.name} self.warnings.append(msg) elif not self.stage2_device.dependsOn(self.stage1_device): - msg = _("bootloader stage2 device %(stage2dev)s is on a multi-disk array, but bootloader stage1 device %(stage1dev)s is not part of this array. " \ - "The stage1 bootloader will only be installed to a single drive.") % \ + msg = _("boot loader stage2 device %(stage2dev)s is on a multi-disk array, but boot loader stage1 device %(stage1dev)s is not part of this array. " \ + "The stage1 boot loader will only be installed to a single drive.") % \ {"stage1dev" : self.stage1_device.name, "stage2dev" : self.stage2_device.name} self.warnings.append(msg) @@ -1373,7 +1400,7 @@ class GRUB2(GRUB): - BIOS boot partition (GPT) - parted /dev/sda set <partition_number> bios_grub on - - can't contain a filesystem + - can't contain a file system - 31KiB min, 1MiB recommended """ @@ -1381,15 +1408,11 @@ class GRUB2(GRUB): packages = ["grub2"] _config_file = "grub.cfg" _config_dir = "grub2" - config_file_mode = 0o600 defaults_file = "/etc/default/grub" - can_dual_boot = True - can_update = True terminal_type = "gfxterm" # requirements for boot devices - stage2_device_types = ["partition", "mdarray", "lvmlv", "btrfs volume", - "btrfs subvolume"] + stage2_device_types = ["partition", "mdarray", "lvmlv"] stage2_raid_levels = [raid.RAID0, raid.RAID1, raid.RAID4, raid.RAID5, raid.RAID6, raid.RAID10] stage2_raid_metadata = ["0", "0.90", "1.0", "1.2"] @@ -1511,7 +1534,7 @@ class GRUB2(GRUB): iutil.eintr_retry_call(os.close, pread) self.encrypted_password = buf.split()[-1].strip() if not self.encrypted_password.startswith("grub.pbkdf2."): - raise BootLoaderError("failed to encrypt bootloader password") + raise BootLoaderError("failed to encrypt boot loader password") def write_password_config(self): if not self.password and not self.encrypted_password: @@ -1544,22 +1567,23 @@ class GRUB2(GRUB): try: self.write_password_config() except (BootLoaderError, OSError, RuntimeError) as e: - log.error("bootloader password setup failed: %s", e) + log.error("boot loader password setup failed: %s", e) # disable non-xen entries os.chmod("%s/etc/grub.d/10_linux" % iutil.getSysroot(), 0644) # make sure the default entry is the OS we are installing - entry_title = "0" - rc = iutil.execInSysroot("grub2-set-default", [entry_title]) - if rc: - log.error("failed to set default menu entry to %s", productName) + if self.default is not None: + entry_title = "0" + rc = iutil.execInSysroot("grub2-set-default", [entry_title]) + if rc: + log.error("failed to set default menu entry to %s", productName) # now tell grub2 to generate the main configuration file rc = iutil.execInSysroot("grub2-mkconfig", ["-o", self.config_file]) if rc: - raise BootLoaderError("failed to write bootloader configuration") + raise BootLoaderError("failed to write boot loader configuration") # # installation @@ -1587,7 +1611,7 @@ class GRUB2(GRUB): root=iutil.getSysroot(), env_prune=['MALLOC_PERTURB_']) if rc: - raise BootLoaderError("bootloader install failed") + raise BootLoaderError("boot loader install failed") def write(self): """ Write the bootloader configuration and install the bootloader. """ @@ -1679,6 +1703,10 @@ class EFIGRUB(GRUB2): log.info("Skipping efibootmgr for image/directory install.") return "" + if "noefi" in flags.cmdline: + log.info("Skipping efibootmgr for noefi") + return "" + if kwargs.pop("capture", False): exec_func = iutil.execWithCapture else: @@ -1724,7 +1752,7 @@ class EFIGRUB(GRUB2): rc = self.efibootmgr("-b", slot_id, "-B") if rc: - raise BootLoaderError("failed to remove old efi boot entry. This is most likely a kernel bug.") + raise BootLoaderError("failed to remove old efi boot entry. This is most likely a kernel or firmware bug.") @property def efi_dir_as_efifs_dir(self): @@ -1741,7 +1769,7 @@ class EFIGRUB(GRUB2): self.efi_dir_as_efifs_dir + self._efi_binary, root=iutil.getSysroot()) if rc: - raise BootLoaderError("failed to set new efi boot target. This is most likely a kernel bug.") + raise BootLoaderError("failed to set new efi boot target. This is most likely a kernel or firmware bug.") def add_efi_boot_target(self): if self.stage1_device.type == "partition": @@ -1780,6 +1808,7 @@ class EFIGRUB(GRUB2): def check(self): return True + class XenEFI(EFIGRUB): packages = ["efibootmgr"] _config_file = 'xen.cfg' @@ -1852,22 +1881,15 @@ class XenEFI(EFIGRUB): write_config = BootLoader.write_config -# FIXME: We need to include grubby, and omit the shim package -# on aarch64 until we get all the EFI bits in place. class Aarch64EFIGRUB(EFIGRUB): - packages = ["grub2-efi", "efibootmgr", "grubby"] - _serial_consoles = ["ttyAMA", "ttyS"] - _efi_binary = "\\grubaa64.efi" - - class MacEFIGRUB(EFIGRUB): def mactel_config(self): if os.path.exists(iutil.getSysroot() + "/usr/libexec/mactel-boot-setup"): rc = iutil.execInSysroot("/usr/libexec/mactel-boot-setup", []) if rc: - log.error("failed to configure Mac bootloader") + log.error("failed to configure Mac boot loader") def install(self, args=None): super(MacEFIGRUB, self).install() @@ -2016,7 +2038,7 @@ class Yaboot(YabootBase): args = ["-f", "-C", self.config_file] rc = iutil.execInSysroot(self.prog, args) if rc: - raise BootLoaderError("bootloader installation failed") + raise BootLoaderError("boot loader installation failed") class IPSeriesYaboot(Yaboot): @@ -2040,6 +2062,8 @@ class IPSeriesYaboot(Yaboot): super(IPSeriesYaboot, self).install() def updatePowerPCBootList(self): + if not can_touch_runtime_system("updatePowerPCBootList", touch_live=True): + return log.debug("updatePowerPCBootList: self.stage1_device.path = %s", self.stage1_device.path) @@ -2065,7 +2089,7 @@ class IPSeriesYaboot(Yaboot): # Place the disk containing the PReP partition first. # Remove all other occurances of it. - boot_list = [boot_disk] + filter(lambda x: x != boot_disk, boot_list) + boot_list = [boot_disk] + [x for x in boot_list if x != boot_disk] log.debug("updatePowerPCBootList: updated boot_list = %s", boot_list) @@ -2100,6 +2124,8 @@ class IPSeriesGRUB2(GRUB2): # This will update the PowerPC's (ppc) bios boot devive order list def updateNVRAMBootList(self): + if not can_touch_runtime_system("updateNVRAMBootList", touch_live=True): + return log.debug("updateNVRAMBootList: self.stage1_device.path = %s", self.stage1_device.path) @@ -2124,7 +2150,7 @@ class IPSeriesGRUB2(GRUB2): # Place the disk containing the PReP partition first. # Remove all other occurances of it. - boot_list = [boot_disk] + filter(lambda x: x != boot_disk, boot_list) + boot_list = [boot_disk] + [x for x in boot_list if x != boot_disk] update_value = "boot-device=%s" % " ".join(boot_list) @@ -2357,7 +2383,7 @@ class EXTLINUX(BootLoader): rc = iutil.execInSysroot("extlinux", args) if rc: - raise BootLoaderError("bootloader install failed") + raise BootLoaderError("boot loader install failed") # every platform that wants a bootloader needs to be in this dict @@ -2450,21 +2476,31 @@ def writeBootLoader(storage, payload, instClass, ksdata): """ if not storage.bootloader.skip_bootloader: stage1_device = storage.bootloader.stage1_device - log.info("bootloader stage1 target device is %s", stage1_device.name) + log.info("boot loader stage1 target device is %s", stage1_device.name) stage2_device = storage.bootloader.stage2_device - log.info("bootloader stage2 target device is %s", stage2_device.name) + log.info("boot loader stage2 target device is %s", stage2_device.name) + + # Bridge storage EFI configuration to bootloader + if hasattr(storage.bootloader, 'efi_dir'): + storage.bootloader.efi_dir = instClass.efi_dir if isinstance(payload, RPMOSTreePayload): if storage.bootloader.skip_bootloader: - log.info("skipping bootloader install per user request") + log.info("skipping boot loader install per user request") return writeBootLoaderFinal(storage, payload, instClass, ksdata) return # get a list of installed kernel packages - kernel_versions = payload.kernelVersionList + payload.rescueKernelList + # add whatever rescue kernels we can find to the end + kernel_versions = list(payload.kernelVersionList) + + rescue_versions = glob(iutil.getSysroot() + "/boot/vmlinuz-*-rescue-*") + rescue_versions += glob(iutil.getSysroot() + "/boot/efi/EFI/%s/vmlinuz-*-rescue-*" % instClass.efi_dir) + kernel_versions += (f.split("/")[-1][8:] for f in rescue_versions) + if not kernel_versions: - log.warning("no kernel was installed -- bootloader config unchanged") + log.warning("no kernel was installed -- boot loader config unchanged") return # all the linux images' labels are based on the default image's @@ -2480,14 +2516,12 @@ def writeBootLoader(storage, payload, instClass, ksdata): short=base_short_label) storage.bootloader.add_image(default_image) storage.bootloader.default = default_image - if hasattr(storage.bootloader, 'efi_dir'): - storage.bootloader.efi_dir = instClass.efi_dir # write out /etc/sysconfig/kernel writeSysconfigKernel(storage, version, instClass) if storage.bootloader.skip_bootloader: - log.info("skipping bootloader install per user request") + log.info("skipping boot loader install per user request") return # now add an image for each of the other kernels diff --git a/anaconda/pyanaconda/constants.py b/anaconda/pyanaconda/constants.py index 8128537..a046d82 100644 --- a/anaconda/pyanaconda/constants.py +++ b/anaconda/pyanaconda/constants.py @@ -19,7 +19,8 @@ # Author(s): Erik Troan <ewt@redhat.com> # -import string +# Used for digits, ascii_letters, punctuation constants +import string # pylint: disable=deprecated-module from pyanaconda.i18n import N_ # Use -1 to indicate that the selinux configuration is unset @@ -51,7 +52,7 @@ DD_RPMS = "/tmp/DD-*" TRANSLATIONS_UPDATE_DIR="/tmp/updates/po" ANACONDA_CLEANUP = "anaconda-cleanup" -MOUNT_DIR = "/mnt/install" +MOUNT_DIR = "/run/install" DRACUT_REPODIR = "/run/install/repo" DRACUT_ISODIR = "/run/install/source" ISO_DIR = MOUNT_DIR + "/isodir" @@ -62,7 +63,7 @@ BASE_REPO_NAME = "anaconda" # NOTE: this should be LANG_TERRITORY.CODESET, e.g. en_US.UTF-8 DEFAULT_LANG = "en_US.UTF-8" -DEFAULT_VC_FONT = "latarcyrheb-sun16" +DEFAULT_VC_FONT = "eurlatgr" DEFAULT_KEYBOARD = "us" @@ -133,15 +134,16 @@ FIRSTBOOT_ENVIRON = "firstboot" UNSUPPORTED_HW = 1 << 28 # Password validation -PASSWORD_MIN_LEN = 6 +PASSWORD_MIN_LEN = 8 PASSWORD_EMPTY_ERROR = N_("The password is empty.") PASSWORD_CONFIRM_ERROR_GUI = N_("The passwords do not match.") PASSWORD_CONFIRM_ERROR_TUI = N_("The passwords you entered were different. Please try again.") -PASSWORD_WEAK = N_("The password you have provided is weak. You will have to press Done twice to confirm it.") -PASSWORD_WEAK_WITH_ERROR = N_("The password you have provided is weak: %s. You will have to press Done twice to confirm it.") +PASSWORD_WEAK = N_("The password you have provided is weak. %s") +PASSWORD_WEAK_WITH_ERROR = N_("The password you have provided is weak: %s. %s") PASSWORD_WEAK_CONFIRM = N_("You have provided a weak password. Press Done again to use anyway.") PASSWORD_WEAK_CONFIRM_WITH_ERROR = N_("You have provided a weak password: %s. Press Done again to use anyway.") PASSWORD_ASCII = N_("The password you have provided contains non-ASCII characters. You may not be able to switch between keyboard layouts to login. Press Done to continue.") +PASSWORD_DONE_TWICE = N_("You will have to press Done twice to confirm it.") PASSWORD_STRENGTH_DESC = [N_("Empty"), N_("Weak"), N_("Fair"), N_("Good"), N_("Strong")] @@ -159,9 +161,22 @@ CMDLINE_APPEND = ["modprobe.blacklist"] DEFAULT_AUTOPART_TYPE = AUTOPART_TYPE_LVM +# Default to these units when reading user input when no units given +SIZE_UNITS_DEFAULT = "MiB" + import logging LOGLVL_LOCK = logging.DEBUG-1 +# Constants for reporting status to IPMI. These are from the IPMI spec v2 rev1.1, page 512. +IPMI_STARTED = 0x7 # installation started +IPMI_FINISHED = 0x8 # installation finished successfully +IPMI_ABORTED = 0x9 # installation finished unsuccessfully, due to some non-exn error +IPMI_FAILED = 0xA # installation hit an exception + + # for how long (in seconds) we try to wait for enough entropy for LUKS # keep this a multiple of 60 (minutes) -MAX_ENTROPY_WAIT = 10 * 60 \ No newline at end of file +MAX_ENTROPY_WAIT = 10 * 60 + +# X display number to use +X_DISPLAY_NUMBER = 1 diff --git a/anaconda/pyanaconda/errors.py b/anaconda/pyanaconda/errors.py index 02704f9..72c4488 100644 --- a/anaconda/pyanaconda/errors.py +++ b/anaconda/pyanaconda/errors.py @@ -55,6 +55,11 @@ class CmdlineError(Exception): class RemovedModuleError(ImportError): pass +class PasswordCryptError(Exception): + def __init__(self, algo): + Exception.__init__(self) + self.algo = algo + class ZIPLError(Exception): pass @@ -122,20 +127,11 @@ class ErrorHandler(object): self.ui.showError(message) return ERROR_RAISE - def _dirtyFSHandler(self, exn): - message = _("The following file systems for your Linux system were " - "not unmounted cleanly. Would you like to mount them " - "anyway?\n%s") % exn.devices - if self.ui.showYesNoQuestion(message): - return ERROR_CONTINUE - else: - return ERROR_RAISE - def _fstabTypeMismatchHandler(self, exn): # FIXME: include the two types in the message instead of including # the raw exception text message = _("There is an entry in your /etc/fstab file that contains " - "an invalid or incorrect filesystem type:\n\n") + "an invalid or incorrect file system type:\n\n") message += " " + str(exn) self.ui.showError(message) @@ -245,7 +241,7 @@ class ErrorHandler(object): return ERROR_RAISE def _bootLoaderErrorHandler(self, exn): - message = _("The following error occurred while installing the bootloader. " + message = _("The following error occurred while installing the boot loader. " "The system will not be bootable. " "Would you like to ignore this and continue with " "installation?") @@ -256,6 +252,12 @@ class ErrorHandler(object): else: return ERROR_RAISE + def _passwordCryptErrorHandler(self, exn): + message = _("Unable to encrypt password: unsupported algorithm %s") % exn.algo + + self.ui.showError(message) + return ERROR_RAISE + def _ziplErrorHandler(self, *args, **kwargs): details = kwargs["exception"] message = _("Installation was stopped due to an error installing the " @@ -283,7 +285,6 @@ class ErrorHandler(object): _map = {"PartitioningError": self._partitionErrorHandler, "FSResizeError": self._fsResizeHandler, "NoDisksError": self._noDisksHandler, - "DirtyFSError": self._dirtyFSHandler, "FSTabTypeMismatchError": self._fstabTypeMismatchHandler, "InvalidImageSizeError": self._invalidImageSizeHandler, "MissingImageError": self._missingImageHandler, @@ -295,6 +296,7 @@ class ErrorHandler(object): "PayloadInstallError": self._payloadInstallHandler, "DependencyError": self._dependencyErrorHandler, "BootLoaderError": self._bootLoaderErrorHandler, + "PasswordCryptError": self._passwordCryptErrorHandler, "ZIPLError": self._ziplErrorHandler} if exn.__class__.__name__ in _map: diff --git a/anaconda/pyanaconda/exception.py b/anaconda/pyanaconda/exception.py index b18c6cc..c1b27a7 100644 --- a/anaconda/pyanaconda/exception.py +++ b/anaconda/pyanaconda/exception.py @@ -28,7 +28,6 @@ from pyanaconda import iutil, kickstart import sys import os import shutil -import signal import time import re import errno @@ -37,7 +36,7 @@ import traceback import blivet.errors from pyanaconda.errors import CmdlineError from pyanaconda.ui.communication import hubQ -from pyanaconda.constants import THREAD_EXCEPTION_HANDLING_TEST +from pyanaconda.constants import THREAD_EXCEPTION_HANDLING_TEST, IPMI_FAILED from pyanaconda.threads import threadMgr from pyanaconda.i18n import _ from pyanaconda import flags @@ -50,7 +49,7 @@ log = logging.getLogger("anaconda") class AnacondaExceptionHandler(ExceptionHandler): - def __init__(self, confObj, intfClass, exnClass, tty_num, gui_lock): + def __init__(self, confObj, intfClass, exnClass, tty_num, gui_lock, interactive): """ :see: python-meh's ExceptionHandler :param tty_num: the number of tty the interface is running on @@ -60,8 +59,9 @@ class AnacondaExceptionHandler(ExceptionHandler): ExceptionHandler.__init__(self, confObj, intfClass, exnClass) self._gui_lock = gui_lock self._intf_tty_num = tty_num + self._interactive = interactive - def run_handleException(self, dump_info): + def _main_loop_handleException(self, dump_info): """ Helper method with one argument only so that it can be registered with GLib.idle_add() to run on idle or called from a handler. @@ -70,8 +70,21 @@ class AnacondaExceptionHandler(ExceptionHandler): """ - super(AnacondaExceptionHandler, self).handleException(dump_info) - return False + ty = dump_info.exc_info.type + value = dump_info.exc_info.value + + if (issubclass(ty, blivet.errors.StorageError) and value.hardware_fault) \ + or (issubclass(ty, OSError) and value.errno == errno.EIO): + # hardware fault or '[Errno 5] Input/Output error' + hw_error_msg = _("The installation was stopped due to what " + "seems to be a problem with your hardware. " + "The exact error message is:\n\n%s.\n\n " + "The installer will now terminate.") % str(value) + self.intf.messageWindow(_("Hardware error occured"), hw_error_msg) + sys.exit(0) + else: + super(AnacondaExceptionHandler, self).handleException(dump_info) + return False def handleException(self, dump_info): """ @@ -90,78 +103,70 @@ class AnacondaExceptionHandler(ExceptionHandler): ty = dump_info.exc_info.type value = dump_info.exc_info.value - if (issubclass(ty, blivet.errors.StorageError) and value.hardware_fault) \ - or (issubclass(ty, OSError) and value.errno == errno.EIO): - # hardware fault or '[Errno 5] Input/Output error' - hw_error_msg = _("The installation was stopped due to what " - "seems to be a problem with your hardware. " - "The exact error message is:\n\n%s.\n\n " - "The installer will now terminate.") % str(value) - self.intf.messageWindow(_("Hardware error occured"), hw_error_msg) - sys.exit(0) - else: - try: - from gi.repository import Gtk + try: + from gi.repository import Gtk - # XXX: Gtk stopped raising RuntimeError if it fails to - # initialize. Horay! But will it stay like this? Let's be - # cautious and raise the exception on our own to work in both - # cases - initialized = Gtk.init_check(None)[0] - if not initialized: - raise RuntimeError() + # XXX: Gtk stopped raising RuntimeError if it fails to + # initialize. Horay! But will it stay like this? Let's be + # cautious and raise the exception on our own to work in both + # cases + initialized = Gtk.init_check(None)[0] + if not initialized: + raise RuntimeError() - # Attempt to grab the GUI initializing lock, do not block - if not self._gui_lock.acquire(False): - # the graphical interface is running, don't crash it by - # running another one potentially from a different thread - log.debug("Gtk running, queuing exception handler to the " - "main loop") - GLib.idle_add(self.run_handleException, dump_info) - else: - log.debug("Gtk not running, starting Gtk and running " - "exception handler in it") - super(AnacondaExceptionHandler, self).handleException( - dump_info) - - except (RuntimeError, ImportError): - log.debug("Gtk cannot be initialized") - # X not running (Gtk cannot be initialized) - if threadMgr.in_main_thread(): - log.debug("In the main thread, running exception handler") - if (issubclass (ty, CmdlineError)): + # Attempt to grab the GUI initializing lock, do not block + if not self._gui_lock.acquire(False): + # the graphical interface is running, don't crash it by + # running another one potentially from a different thread + log.debug("Gtk running, queuing exception handler to the " + "main loop") + GLib.idle_add(self._main_loop_handleException, dump_info) + else: + log.debug("Gtk not running, starting Gtk and running " + "exception handler in it") + self._main_loop_handleException(dump_info) + except (RuntimeError, ImportError): + log.debug("Gtk cannot be initialized") + # X not running (Gtk cannot be initialized) + if threadMgr.in_main_thread(): + log.debug("In the main thread, running exception handler") + if issubclass(ty, CmdlineError) or not self._interactive: + if issubclass(ty, CmdlineError): cmdline_error_msg = _("\nThe installation was stopped due to " "incomplete spokes detected while running " "in non-interactive cmdline mode. Since there " - "can not be any questions in cmdline mode, " + "cannot be any questions in cmdline mode, " "edit your kickstart file and retry " "installation.\nThe exact error message is: " "\n\n%s.\n\nThe installer will now terminate.") % str(value) - - # since there is no UI in cmdline mode and it is completely - # non-interactive, we can't show a message window asking the user - # to acknowledge the error; instead, print the error out and sleep - # for a few seconds before exiting the installer - print(cmdline_error_msg) - time.sleep(10) - sys.exit(0) else: - print("\nAn unknown error has occured, look at the " - "/tmp/anaconda-tb* file(s) for more details") - # in the main thread, run exception handler - super(AnacondaExceptionHandler, self).handleException( - dump_info) + cmdline_error_msg = _("\nRunning in cmdline mode, no interactive debugging " + "allowed.\nThe exact error message is: " + "\n\n%s.\n\nThe installer will now terminate.") % str(value) + + # since there is no UI in cmdline mode and it is completely + # non-interactive, we can't show a message window asking the user + # to acknowledge the error; instead, print the error out and sleep + # for a few seconds before exiting the installer + print(cmdline_error_msg) + time.sleep(10) + sys.exit(1) else: - log.debug("In a non-main thread, sending a message with " - "exception data") - # not in the main thread, just send message with exception - # data and let message handler run the exception handler in - # the main thread - exc_info = dump_info.exc_info - hubQ.send_exception((exc_info.type, - exc_info.value, - exc_info.stack)) + print("\nAn unknown error has occured, look at the " + "/tmp/anaconda-tb* file(s) for more details") + # in the main thread, run exception handler + self._main_loop_handleException(dump_info) + else: + log.debug("In a non-main thread, sending a message with " + "exception data") + # not in the main thread, just send message with exception + # data and let message handler run the exception handler in + # the main thread + exc_info = dump_info.exc_info + hubQ.send_exception((exc_info.type, + exc_info.value, + exc_info.stack)) def postWriteHook(self, dump_info): anaconda = dump_info.object @@ -181,19 +186,13 @@ class AnacondaExceptionHandler(ExceptionHandler): except: pass + iutil.ipmi_report(IPMI_FAILED) + def runDebug(self, exc_info): if flags.can_touch_runtime_system("switch console") \ and self._intf_tty_num != 1: iutil.vtActivate(1) - pidfl = "/tmp/vncshell.pid" - if os.path.exists(pidfl) and os.path.isfile(pidfl): - pf = open(pidfl, "r") - for pid in pf.readlines(): - if not int(pid) == os.getpid(): - os.kill(int(pid), signal.SIGKILL) - pf.close() - iutil.eintr_retry_call(os.open, "/dev/console", os.O_RDWR) # reclaim stdin iutil.eintr_retry_call(os.dup2, 0, 1) # reclaim stdout iutil.eintr_retry_call(os.dup2, 0, 2) # reclaim stderr @@ -266,9 +265,10 @@ def initExceptionHandling(anaconda): # anaconda-tb file conf.register_callback("journalctl", journalctl_callback, attchmnt_only=False) + interactive = not anaconda.displayMode == 'c' handler = AnacondaExceptionHandler(conf, anaconda.intf.meh_interface, ReverseExceptionDump, anaconda.intf.tty_num, - anaconda.gui_initialized) + anaconda.gui_initialized, interactive) handler.install(anaconda) return conf diff --git a/anaconda/pyanaconda/flags.py b/anaconda/pyanaconda/flags.py index 55b4944..207f132 100644 --- a/anaconda/pyanaconda/flags.py +++ b/anaconda/pyanaconda/flags.py @@ -65,11 +65,12 @@ class Flags(object): self.gpt = False self.leavebootorder = False self.testing = False - self.dnf = False + self.dnf = True self.mpathFriendlyNames = True # ksprompt is whether or not to prompt for missing ksdata self.ksprompt = True self.rescue_mode = False + self.noefi = False # parse the boot commandline self.cmdline = BootArgs() # Lock it down: no more creating new flags! @@ -79,7 +80,7 @@ class Flags(object): def read_cmdline(self): for f in ("selinux", "debug", "leavebootorder", "testing", "extlinux", - "nombr", "gpt", "dnf"): + "nombr", "gpt", "dnf", "noefi"): self.set_cmdline_bool(f) if not selinux.is_selinux_enabled(): diff --git a/anaconda/pyanaconda/geoloc.py b/anaconda/pyanaconda/geoloc.py index 7b8dc0d..b61aec1 100644 --- a/anaconda/pyanaconda/geoloc.py +++ b/anaconda/pyanaconda/geoloc.py @@ -99,10 +99,8 @@ in a couple seconds * cell tower geolocation """ - +import requests import urllib -import urllib2 -import json import dbus import threading import time @@ -525,10 +523,9 @@ class FedoraGeoIPProvider(GeolocationBackend): def _refresh(self): try: - reply = urllib2.urlopen(self.API_URL, timeout= - constants.NETWORK_CONNECTION_TIMEOUT) - if reply: - json_reply = json.load(reply) + reply = requests.get(self.API_URL, timeout=constants.NETWORK_CONNECTION_TIMEOUT, verify=True) + if reply.status_code == requests.codes.ok: + json_reply = reply.json() territory = json_reply.get("country_code", None) timezone_source = "GeoIP" timezone_code = json_reply.get("time_zone", None) @@ -549,10 +546,10 @@ class FedoraGeoIPProvider(GeolocationBackend): territory_code=territory, timezone=timezone_code, timezone_source=timezone_source)) - except urllib2.HTTPError as e: - log.debug("Geoloc: HTTPError for Fedora GeoIP API lookup:\n%s", e) - except urllib2.URLError as e: - log.debug("Geoloc: URLError for Fedora GeoIP API lookup:\n%s", e) + else: + log.error("Geoloc: Fedora GeoIP API lookup failed with status code: %s", reply.status_code) + except requests.exceptions.RequestException as e: + log.debug("Geoloc: RequestException for Fedora GeoIP API lookup:\n%s", e) except ValueError as e: log.debug("Geoloc: Unable to decode GeoIP JSON:\n%s", e) @@ -570,10 +567,9 @@ class HostipGeoIPProvider(GeolocationBackend): def _refresh(self): try: - reply = urllib2.urlopen(self.API_URL, timeout= - constants.NETWORK_CONNECTION_TIMEOUT) - if reply: - reply_dict = json.load(reply) + reply = requests.get(self.API_URL, timeout=constants.NETWORK_CONNECTION_TIMEOUT, verify=True) + if reply.status_code == requests.codes.ok: + reply_dict = reply.json() territory = reply_dict.get("country_code", None) # unless at least country_code is available, @@ -584,8 +580,13 @@ class HostipGeoIPProvider(GeolocationBackend): public_ip_address=reply_dict.get("ip", None), city=reply_dict.get("city", None) )) - except urllib2.URLError as e: - log.debug("Geoloc: URLError during Hostip lookup:\n%s", e) + else: + log.error("Geoloc: Hostip lookup failed with status code: %s", reply.status_code) + except requests.exceptions.RequestException as e: + log.debug("Geoloc: RequestException during Hostip lookup:\n%s", e) + except ValueError as e: + log.debug("Geoloc: Unable to decode Hostip JSON:\n%s", e) + class GoogleWiFiLocationProvider(GeolocationBackend): @@ -607,9 +608,8 @@ class GoogleWiFiLocationProvider(GeolocationBackend): if access_points: try: url = self._get_url(access_points) - reply = urllib2.urlopen(url, timeout= - constants.NETWORK_CONNECTION_TIMEOUT) - result_dict = json.load(reply) + reply = requests.get(url, timeout=constants.NETWORK_CONNECTION_TIMEOUT, verify=True) + result_dict = reply.json() status = result_dict.get('status', 'NOT OK') if status == 'OK': lat = result_dict['location']['lat'] @@ -623,12 +623,13 @@ class GoogleWiFiLocationProvider(GeolocationBackend): t_code = geocoding_result.territory_code self._set_result(LocationResult(territory_code=t_code)) else: - log.info("Service couldn't find current location.") - except urllib2.URLError as e: - log.debug("Geoloc: URLError during Google" - " Wifi lookup:\n%s", e) + log.info("Geoloc: Service couldn't find current location.") + except requests.exceptions.RequestException as e: + log.debug("Geoloc: RequestException during Google Wifi lookup:\n%s", e) + except ValueError as e: + log.debug("Geoloc: Unable to decode Google Wifi JSON:\n%s", e) else: - log.info("No WiFi access points found - can't detect location.") + log.info("Geoloc: No WiFi access points found - can't detect location.") def _get_url(self, access_points): """Generate Google API URL for the given access points @@ -697,18 +698,19 @@ class Geocoder(object): coordinates.latitude, coordinates.longitude) try: - reply = urllib2.urlopen(url, timeout= - constants.NETWORK_CONNECTION_TIMEOUT) - if reply: - reply_dict = json.load(reply) + reply = requests.get(url, timeout=constants.NETWORK_CONNECTION_TIMEOUT, verify=True) + if reply.status_code == requests.codes.ok: + reply_dict = reply.json() territory_code = reply_dict['address']['country_code'].upper() return GeocodingResult(coordinates=coordinates, territory_code=territory_code) else: + log.error("Geoloc: Nominatim reverse geocoding failed with status code: %s", reply.status_code) return None - except urllib2.URLError as e: - log.debug("Geoloc: URLError during Nominatim reverse geocoding" - " :\n%s", e) + except requests.exceptions.RequestException as e: + log.debug("Geoloc: RequestException during Nominatim reverse geocoding:\n%s", e) + except ValueError as e: + log.debug("Geoloc: Unable to decode Nominatim reverse geocoding JSON:\n%s", e) class GeocodingResult(object): diff --git a/anaconda/pyanaconda/i18n.py b/anaconda/pyanaconda/i18n.py index 3e6c248..2e80957 100644 --- a/anaconda/pyanaconda/i18n.py +++ b/anaconda/pyanaconda/i18n.py @@ -23,9 +23,9 @@ __all__ = ["_", "N_", "P_", "C_", "CN_", "CP_"] import gettext -_ = lambda x: gettext.ldgettext("anaconda", x) if x else "" +_ = lambda x: gettext.translation("anaconda", fallback=True).ugettext(x) if x != "" else u"" N_ = lambda x: x -P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z) +P_ = lambda x, y, z: gettext.translation("anaconda", fallback=True).ungettext(x, y, z) # This is equivalent to "pgettext" in GNU gettext. The pgettext functions # are not exported by Python, but all they really do is a stick a EOT diff --git a/anaconda/pyanaconda/ihelp.py b/anaconda/pyanaconda/ihelp.py index cb02a84..470f300 100644 --- a/anaconda/pyanaconda/ihelp.py +++ b/anaconda/pyanaconda/ihelp.py @@ -21,11 +21,11 @@ Anaconda built-in help module """ import os -import subprocess from pyanaconda.flags import flags from pyanaconda.localization import find_best_locale_match from pyanaconda.constants import DEFAULT_LANG +from pyanaconda.iutil import startProgram import logging log = logging.getLogger("anaconda") @@ -124,7 +124,7 @@ def start_yelp(help_path): # under some extreme circumstances (placeholders missing) # the help path can be None and we need to prevent Popen # receiving None as an argument instead of a string - yelp_process = subprocess.Popen(["yelp", help_path or ""]) + yelp_process = startProgram(["yelp", help_path or ""], reset_lang=False) def kill_yelp(): """Try to kill any existing yelp processes""" diff --git a/anaconda/pyanaconda/indexed_dict.py b/anaconda/pyanaconda/indexed_dict.py deleted file mode 100644 index eb5050c..0000000 --- a/anaconda/pyanaconda/indexed_dict.py +++ /dev/null @@ -1,46 +0,0 @@ -# indexed_dict.py -# Implements IndexedDictionary class. -# -# Copyright (C) 2009 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# - -class IndexedDict(dict): - """ Indexed dictionary that remembers order of the inserted elements. - - Values can be inserted with string keys only, but referenced by both - string keys or index. - - There's a unit test for the class, please maintain it along. - """ - def __init__(self): - super(IndexedDict, self).__init__() - self._indexes = [] - - def __getitem__(self, key): - if type(key) is int: - key = self._indexes[key] - return super(IndexedDict, self).__getitem__(key) - - def __setitem__(self, key, value): - if type(key) is int: - raise TypeError("IndexedDict only accepts strings as new keys") - assert(len(self) == len(self._indexes)) - self._indexes.append(key) - return super(IndexedDict, self).__setitem__(key, value) - - def index(self, string_key): - return self._indexes.index(string_key) diff --git a/anaconda/pyanaconda/install.py b/anaconda/pyanaconda/install.py index 9c2d724..fccbf0a 100644 --- a/anaconda/pyanaconda/install.py +++ b/anaconda/pyanaconda/install.py @@ -20,13 +20,16 @@ # Red Hat Author(s): Chris Lumens <clumens@redhat.com> # -from blivet import turnOnFilesystems, callbacks +from blivet import callbacks +from blivet.osinstall import turnOnFilesystems +from blivet.devices import BTRFSDevice from pyanaconda.bootloader import writeBootLoader from pyanaconda.progress import progress_report, progress_message, progress_step, progress_complete, progress_init from pyanaconda.users import createLuserConf, getPassAlgo, Users from pyanaconda import flags from pyanaconda import iutil from pyanaconda import timezone +from pyanaconda import network from pyanaconda.i18n import _ from pyanaconda.threads import threadMgr from pyanaconda.ui.lib.entropy import wait_for_entropy @@ -97,12 +100,20 @@ def doConfiguration(storage, payload, ksdata, instClass): ksdata.rootpw.execute(storage, ksdata, instClass, u) ksdata.group.execute(storage, ksdata, instClass, u) ksdata.user.execute(storage, ksdata, instClass, u) + ksdata.sshkey.execute(storage, ksdata, instClass, u) with progress_report(_("Configuring addons")): ksdata.addons.execute(storage, ksdata, instClass, u) with progress_report(_("Generating initramfs")): - payload.recreateInitrds(force=True) + payload.recreateInitrds() + + # Work around rhbz#1200539, grubby doesn't handle grub2 missing initrd with /boot on btrfs + # So rerun writing the bootloader if this is live and /boot is on btrfs + boot_on_btrfs = isinstance(storage.mountpoints.get("/boot", storage.mountpoints.get("/")), BTRFSDevice) + if flags.flags.livecdInstall and boot_on_btrfs \ + and (not ksdata.bootloader.disabled and ksdata.bootloader != "none"): + writeBootLoader(storage, payload, instClass, ksdata) if willRunRealmd: with progress_report(_("Joining realm: %s") % ksdata.realm.discovered): @@ -117,17 +128,6 @@ def doConfiguration(storage, payload, ksdata, instClass): progress_complete() -def moveBootMntToPhysical(storage): - """Move the /boot mount to /mnt/sysimage/boot.""" - if iutil.getSysroot() == iutil.getTargetPhysicalRoot(): - return - bootmnt = storage.mountpoints.get('/boot') - if bootmnt is None: - return - bootmnt.format.teardown() - bootmnt.teardown() - bootmnt.format.setup(options=bootmnt.format.options, chroot=iutil.getTargetPhysicalRoot()) - def doInstall(storage, payload, ksdata, instClass): """Perform an installation. This method takes the ksdata as prepared by the UI (the first hub, in graphical mode) and applies it to the disk. @@ -147,6 +147,11 @@ def doInstall(storage, payload, ksdata, instClass): steps = len(storage.devicetree.findActions(action_type="create", object_type="format")) + \ len(storage.devicetree.findActions(action_type="resize", object_type="format")) + # Update every 10% of packages installed. We don't know how many packages + # we are installing until it's too late (see realmd later on) so this is + # the best we can do. + steps += 10 + # pre setup phase, post install steps += 2 @@ -190,9 +195,8 @@ def doInstall(storage, payload, ksdata, instClass): turnOnFilesystems(storage, mountOnly=flags.flags.dirInstall, callbacks=callbacks_reg) write_storage_late = (flags.flags.livecdInstall or ksdata.ostreesetup.seen - or ksdata.method.method == "liveimg" - and not flags.flags.dirInstall) - if not write_storage_late: + or ksdata.method.method == "liveimg") + if not write_storage_late and not flags.flags.dirInstall: storage.write() # Do packaging. @@ -207,6 +211,10 @@ def doInstall(storage, payload, ksdata, instClass): ksdata.authconfig.setup() ksdata.firewall.setup() + # make name resolution work for rpm scripts in chroot + if flags.can_touch_runtime_system("copy /etc/resolv.conf to sysroot"): + network.copyFileToPath("/etc/resolv.conf", iutil.getSysroot()) + # anaconda requires storage packages in order to make sure the target # system is bootable and configurable, and some other packages in order # to finish setting up the system. @@ -216,6 +224,9 @@ def doInstall(storage, payload, ksdata, instClass): if willInstallBootloader: packages += storage.bootloader.packages + if network.is_using_team_device(): + packages.append("teamd") + # don't try to install packages from the install class' ignored list and the # explicitly excluded ones (user takes the responsibility) packages = [p for p in packages @@ -223,7 +234,7 @@ def doInstall(storage, payload, ksdata, instClass): payload.preInstall(packages=packages, groups=payload.languageGroups()) payload.install() - if write_storage_late: + if write_storage_late and not flags.flags.dirInstall: if iutil.getSysroot() != iutil.getTargetPhysicalRoot(): blivet.setSysroot(iutil.getTargetPhysicalRoot(), iutil.getSysroot()) @@ -251,13 +262,10 @@ def doInstall(storage, payload, ksdata, instClass): # Do bootloader. if willInstallBootloader: - with progress_report(_("Installing bootloader")): + with progress_report(_("Installing boot loader")): writeBootLoader(storage, payload, instClass, ksdata) with progress_report(_("Performing post-installation setup tasks")): - # Now, let's reset the state here so that the payload has - # /boot in the system root. - moveBootMntToPhysical(storage) payload.postInstall() progress_complete() diff --git a/anaconda/pyanaconda/installclass.py b/anaconda/pyanaconda/installclass.py index d6d0a8a..d7ec1db 100644 --- a/anaconda/pyanaconda/installclass.py +++ b/anaconda/pyanaconda/installclass.py @@ -26,7 +26,7 @@ import os, sys import imputil from blivet.partspec import PartSpec -from blivet.devicelibs import swap +from blivet.autopart import swapSuggestion from blivet.platform import platform from blivet.size import Size @@ -37,7 +37,7 @@ from pyanaconda.kickstart import getAvailableDiskSpace class BaseInstallClass(object): # default to not being hidden - hidden = 0 + hidden = False name = "base" bootloaderTimeoutDefault = None bootloaderExtraArgs = [] @@ -58,15 +58,18 @@ class BaseInstallClass(object): # Blivet uses by default. defaultFS = None - # don't select this class by default - default = 0 - # help help_folder = "/usr/share/anaconda/help" help_main_page = "Installation_Guide.xml" help_placeholder = None help_placeholder_with_links = None + # path to the installclass stylesheet, if any + stylesheet = None + + # comps environment id to select by default + defaultPackageEnvironment = None + @property def l10n_domain(self): if self._l10n_domain is None: @@ -101,7 +104,7 @@ class BaseInstallClass(object): disk_space = getAvailableDiskSpace(storage) - swp = swap.swapSuggestion(disk_space=disk_space) + swp = swapSuggestion(disk_space=disk_space) autorequests.append(PartSpec(fstype="swap", size=swp, grow=False, lv=True, encrypted=True)) @@ -128,8 +131,8 @@ class BaseInstallClass(object): allClasses = [] allClasses_hidden = [] -# returns ( className, classObject, classLogo ) tuples -def availableClasses(showHidden=0): +# returns ( className, classObject ) tuples +def availableClasses(showHidden=False): global allClasses global allClasses_hidden @@ -194,7 +197,7 @@ def availableClasses(showHidden=0): try: found = imputil.imp.find_module(mainName) except ImportError: - log.warning ("module import of %s failed: %s", mainName, sys.exc_type) + log.warning ("module import of %s failed: %s", mainName, sys.exc_info()[0]) continue try: @@ -204,10 +207,10 @@ def availableClasses(showHidden=0): # If it's got these two methods, it's an InstallClass. if hasattr(obj, "setDefaultPartitioning") and hasattr(obj, "setPackageSelection"): sortOrder = getattr(obj, "sortPriority", 0) - if obj.hidden == 0 or showHidden == 1: + if not obj.hidden or showHidden: lst.append(((obj.name, obj), sortOrder)) except (ImportError, AttributeError): - log.warning ("module import of %s failed: %s", mainName, sys.exc_type) + log.warning ("module import of %s failed: %s", mainName, sys.exc_info()[0]) lst.sort(_ordering) for (item, _) in lst: @@ -223,8 +226,8 @@ def availableClasses(showHidden=0): def getBaseInstallClass(): # figure out what installclass we should base on. - allavail = availableClasses(showHidden = 1) - avail = availableClasses(showHidden = 0) + allavail = availableClasses(showHidden=True) + avail = availableClasses(showHidden=False) if len(avail) == 1: (cname, cobject) = avail[0] @@ -240,8 +243,6 @@ def getBaseInstallClass(): elif len(allavail) > 1: (cname, cobject) = allavail.pop() log.info('%s is the highest priority installclass, using it', cname) - - # Default to the base installclass if nothing else is found. else: raise RuntimeError("Unable to find an install class to use!!!") diff --git a/anaconda/pyanaconda/isys/Makefile.am b/anaconda/pyanaconda/isys/Makefile.am index c787f60..dbf0f62 100644 --- a/anaconda/pyanaconda/isys/Makefile.am +++ b/anaconda/pyanaconda/isys/Makefile.am @@ -33,7 +33,7 @@ _isys_la_SOURCES = isys.c auditddir = $(libexecdir)/$(PACKAGE_NAME) auditd_PROGRAMS = auditd auditd_SOURCES = auditd.c -auditd_CFLAGS = -DSTANDALONE $(SELINUX_CFLAGS) -auditd_LDADD = $(SELINUX_LIBS) $(LIBNL_LIBS) +auditd_CFLAGS = $(SELINUX_CFLAGS) +auditd_LDFLAGS = -laudit MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/pyanaconda/isys/__init__.py b/anaconda/pyanaconda/isys/__init__.py index 44f6144..f91520b 100644 --- a/anaconda/pyanaconda/isys/__init__.py +++ b/anaconda/pyanaconda/isys/__init__.py @@ -29,18 +29,10 @@ except ImportError: # up PYTHONPATH and just do this basic import. import _isys -import os -import os.path -import socket -import stat -import sys -from pyanaconda import iutil import blivet.arch -import re -import struct -import dbus import time import datetime +import pytz import logging log = logging.getLogger("anaconda") @@ -117,34 +109,39 @@ def set_system_time(secs): """ _isys.set_system_time(secs) - log.info("System time set to %s", time.ctime(secs)) + log.info("System time set to %s UTC", time.asctime(time.gmtime(secs))) def set_system_date_time(year=None, month=None, day=None, hour=None, minute=None, - second=None, utc=False): + second=None, tz=None): """ Set system date and time given by the parameters as numbers. If some parameter is missing or None, the current system date/time field is used instead (i.e. the value is not changed by this function). :type year, month, ..., second: int - :param utc: wheter the other parameters specify UTC or local time - :type utc: bool """ - # get the right values - local = 0 if utc else 1 - now = datetime.datetime.now() - year = year or now.year - month = month or now.month - day = day or now.day - hour = hour or now.hour - minute = minute or now.minute - second = second or now.second + # If no timezone is set, use UTC + if not tz: + tz = pytz.UTC - # struct fields -> year, month, day, hour, minute, second, week_day, year_day, local - time_struct = time.struct_time((year, month, day, hour, minute, second, 0, 0, local)) - set_system_time(int(time.mktime(time_struct))) + # get the right values + now = datetime.datetime.now(tz) + year = year if year is not None else now.year + month = month if month is not None else now.month + day = day if day is not None else now.day + hour = hour if hour is not None else now.hour + minute = minute if minute is not None else now.minute + second = second if second is not None else now.second + + set_date = datetime.datetime(year, month, day, hour, minute, second, tzinfo=tz) + + # Calculate the number of seconds between this time and timestamp 0 + epoch = datetime.datetime.fromtimestamp(0, pytz.UTC) + timestamp = (set_date - epoch).total_seconds() + + set_system_time(timestamp) def total_memory(): """Returns total system memory in kB (given to us by /proc/meminfo)""" @@ -180,4 +177,4 @@ def total_memory(): log.error("MemTotal: line not found in /proc/meminfo") raise RuntimeError("MemTotal: line not found in /proc/meminfo") -handleSegv = _isys.handleSegv +installSyncSignalHandlers = _isys.installSyncSignalHandlers diff --git a/anaconda/pyanaconda/isys/auditd.c b/anaconda/pyanaconda/isys/auditd.c index 24cefa8..dfe6ad4 100644 --- a/anaconda/pyanaconda/isys/auditd.c +++ b/anaconda/pyanaconda/isys/auditd.c @@ -38,7 +38,6 @@ #include "auditd.h" -#ifdef USESELINUX static int done; static void sig_done(int sig) @@ -92,43 +91,38 @@ static void do_auditd(int fd) { } return; } -#endif /* USESELINUX */ int audit_daemonize(void) { -#ifdef USESELINUX int fd; pid_t child; + +/* I guess we should actually do something with the output of AC_FUNC_FORK */ +#ifndef HAVE_WORKING_FORK +#error "Autoconf could not find a working fork. Please fix this." +#endif + if ((child = fork()) > 0) return 0; if (child < 0) return -1; -#ifndef STANDALONE - for (fd = 0; fd < getdtablesize(); fd++) - close(fd); - signal(SIGTTOU, SIG_IGN); - signal(SIGTTIN, SIG_IGN); - signal(SIGTSTP, SIG_IGN); -#endif /* !defined(STANDALONE) */ + /* Close stdin and friends */ + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); - if ((fd = open("/proc/self/oom_adj", O_RDWR)) >= 0) { - write(fd, "-17", 3); + if ((fd = open("/proc/self/oom_score_adj", O_RDWR)) >= 0) { + write(fd, "-1000", 5); close(fd); } fd = audit_open(); do_auditd(fd); audit_close(fd); -#ifndef STANDALONE - exit(0); -#endif /* !defined(STANDALONE) */ - -#endif /* USESELINUX */ return 0; } -#ifdef STANDALONE int main(void) { if (audit_daemonize() < 0) { @@ -138,7 +132,6 @@ int main(void) { return 0; } -#endif /* STANDALONE */ /* * vim:ts=8:sw=4:sts=4:et diff --git a/anaconda/pyanaconda/isys/isys.c b/anaconda/pyanaconda/isys/isys.c index 23fb77f..cd699df 100644 --- a/anaconda/pyanaconda/isys/isys.c +++ b/anaconda/pyanaconda/isys/isys.c @@ -28,14 +28,15 @@ #include <signal.h> #include <execinfo.h> #include <stdlib.h> +#include <string.h> static PyObject * doSync(PyObject * s, PyObject * args); -static PyObject * doSegvHandler(PyObject *s, PyObject *args); +static PyObject * doSignalHandlers(PyObject *s, PyObject *args); static PyObject * doSetSystemTime(PyObject *s, PyObject *args); static PyMethodDef isysModuleMethods[] = { - { "sync", (PyCFunction) doSync, METH_VARARGS, NULL}, - { "handleSegv", (PyCFunction) doSegvHandler, METH_VARARGS, NULL }, + { "sync", (PyCFunction) doSync, METH_NOARGS, NULL}, + { "installSyncSignalHandlers", (PyCFunction) doSignalHandlers, METH_NOARGS, NULL}, { "set_system_time", (PyCFunction) doSetSystemTime, METH_VARARGS, NULL}, { NULL, NULL, 0, NULL } } ; @@ -46,27 +47,22 @@ void init_isys(void) { } static PyObject * doSync(PyObject * s, PyObject * args) { - int fd; - - if (!PyArg_ParseTuple(args, "", &fd)) return NULL; sync(); Py_INCREF(Py_None); return Py_None; } -static PyObject * doSegvHandler(PyObject *s, PyObject *args) { +static void sync_signal_handler(int signum) { void *array[20]; size_t size; char **strings; size_t i; - signal(SIGSEGV, SIG_DFL); /* back to default */ - size = backtrace (array, 20); strings = backtrace_symbols (array, size); - printf ("Anaconda received SIGSEGV!. Backtrace:\n"); + printf ("Anaconda received signal %d!. Backtrace:\n", signum); for (i = 0; i < size; i++) printf ("%s\n", strings[i]); @@ -74,6 +70,43 @@ static PyObject * doSegvHandler(PyObject *s, PyObject *args) { exit(1); } +static PyObject * doSignalHandlers(PyObject *s, PyObject *args) { + /* Install a signal handler for all synchronous signals */ + struct sigaction sa; + + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = sync_signal_handler; + + /* Use these flags to ensure that a crash within the signal handler will + * just crash anaconda and not get stuck in a loop. RESETHAND resets the + * handler to SIG_DFL when the handler is entered, so that further signals + * will exit the program, and NODEFER ensures that the signal is not blocked + * during the signal handler, so a SIGSEGV triggered by handling a SIGSEGV will + * be processed and will use the default handler. The Linux kernel forces + * both of these things during a signal handler crash, but this makes it + * explicit. + * + * These flags also mean that a SIGSEGV from a second thread could abort + * the processing of a SIGSEGV from a first, but too bad. + */ + sa.sa_flags = SA_RESETHAND | SA_NODEFER; + + if (sigaction(SIGILL, &sa, NULL) != 0) { + return PyErr_SetFromErrno(PyExc_SystemError); + } + + if (sigaction(SIGFPE, &sa, NULL) != 0) { + return PyErr_SetFromErrno(PyExc_SystemError); + } + + if (sigaction(SIGSEGV, &sa, NULL) != 0) { + return PyErr_SetFromErrno(PyExc_SystemError); + } + + Py_INCREF(Py_None); + return Py_None; +} + static PyObject * doSetSystemTime(PyObject *s, PyObject *args) { struct timeval tv; tv.tv_usec = 0; @@ -82,7 +115,7 @@ static PyObject * doSetSystemTime(PyObject *s, PyObject *args) { return NULL; if (settimeofday(&tv, NULL) != 0) - PyErr_SetFromErrno(PyExc_SystemError); + return PyErr_SetFromErrno(PyExc_SystemError); Py_INCREF(Py_None); return Py_None; diff --git a/anaconda/pyanaconda/iutil.py b/anaconda/pyanaconda/iutil.py index 1b04c18..b163091 100644 --- a/anaconda/pyanaconda/iutil.py +++ b/anaconda/pyanaconda/iutil.py @@ -1,7 +1,7 @@ # # iutil.py - generic install utility functions # -# Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 +# Copyright (C) 1999-2014 # Red Hat, Inc. All rights reserved. # # This program is free software; you can redistribute it and/or modify @@ -27,12 +27,16 @@ import os.path import errno import subprocess import unicodedata -import string +# Used for ascii_lowercase, ascii_uppercase constants +import string # pylint: disable=deprecated-module +import tempfile import types import re -from threading import Thread -from Queue import Queue, Empty from urllib import quote, unquote +import gettext +import signal + +from gi.repository import GLib from pyanaconda.flags import flags from pyanaconda.constants import DRACUT_SHUTDOWN_EJECT, TRANSLATIONS_UPDATE_DIR, UNSUPPORTED_HW @@ -46,11 +50,25 @@ program_log = logging.getLogger("program") from pyanaconda.anaconda_log import program_log_lock +_child_env = {} + +def setenv(name, value): + """ Set an environment variable to be used by child processes. + + This method does not modify os.environ for the running process, which + is not thread-safe. If setenv has already been called for a particular + variable name, the old value is overwritten. + + :param str name: The name of the environment variable + :param str value: The value of the environment variable + """ + + _child_env[name] = value + def augmentEnv(): env = os.environ.copy() - env.update({"LC_ALL": "C", - "ANA_INSTALL_PATH": getSysroot() - }) + env.update({"ANA_INSTALL_PATH": getSysroot()}) + env.update(_child_env) return env _root_path = "/mnt/sysimage" @@ -97,16 +115,26 @@ def setSysroot(path): global _sysroot _sysroot = path -def _run_program(argv, root='/', stdin=None, stdout=None, env_prune=None, log_output=True, binary_output=False): - """ Run an external program, log the output and return it to the caller +def startProgram(argv, root='/', stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env_prune=None, env_add=None, reset_handlers=True, reset_lang=True, **kwargs): + """ Start an external program and return the Popen object. + + The root and reset_handlers arguments are handled by passing a + preexec_fn argument to subprocess.Popen, but an additional preexec_fn + can still be specified and will be run. The user preexec_fn will be run + last. + :param argv: The command to run and argument :param root: The directory to chroot to before running command. :param stdin: The file object to read stdin from. - :param stdout: Optional file object to write stdout and stderr to. - :param env_prune: environment variable to remove before execution - :param log_output: whether to log the output of command - :param binary_output: whether to treat the output of command as binary data - :return: The return code of the command and the output + :param stdout: The file object to write stdout to. + :param stderr: The file object to write stderr to. + :param env_prune: environment variables to remove before execution + :param env_add: environment variables to add before execution + :param reset_handlers: whether to reset to SIG_DFL any signal handlers set to SIG_IGN + :param reset_lang: whether to set the locale of the child process to C + :param kwargs: Additional parameters to pass to subprocess.Popen + :return: A Popen object for the running command. """ if env_prune is None: env_prune = [] @@ -117,45 +145,152 @@ def _run_program(argv, root='/', stdin=None, stdout=None, env_prune=None, log_ou if target_root == _root_path: target_root = getSysroot() - def chroot(): + # Check for and save a preexec_fn argument + preexec_fn = kwargs.pop("preexec_fn", None) + + def preexec(): + # If a target root was specificed, chroot into it if target_root and target_root != '/': os.chroot(target_root) os.chdir("/") + # Signal handlers set to SIG_IGN persist across exec. Reset + # these to SIG_DFL if requested. In particular this will include the + # SIGPIPE handler set by python. + if reset_handlers: + for signum in range(1, signal.NSIG): + if signal.getsignal(signum) == signal.SIG_IGN: + signal.signal(signum, signal.SIG_DFL) + + # If the user specified an additional preexec_fn argument, run it + if preexec_fn is not None: + preexec_fn() + with program_log_lock: program_log.info("Running... %s", " ".join(argv)) - env = augmentEnv() - for var in env_prune: - env.pop(var, None) + env = augmentEnv() + for var in env_prune: + env.pop(var, None) - try: - proc = subprocess.Popen(argv, - stdin=stdin, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - preexec_fn=chroot, cwd=root, env=env) + if reset_lang: + env.update({"LC_ALL": "C"}) - output_string = proc.communicate()[0] - if output_string: - if binary_output: - output_lines = [output_string] - else: - if output_string[-1] != "\n": - output_string = output_string + "\n" - output_lines = output_string.splitlines(True) + if env_add: + env.update(env_add) - for line in output_lines: - if log_output: + return subprocess.Popen(argv, + stdin=stdin, + stdout=stdout, + stderr=stderr, + close_fds=True, + preexec_fn=preexec, cwd=root, env=env, **kwargs) + +def startX(argv, output_redirect=None): + """ Start X and return once X is ready to accept connections. + + X11, if SIGUSR1 is set to SIG_IGN, will send SIGUSR1 to the parent + process once it is ready to accept client connections. This method + sets that up and waits for the signal or bombs out if nothing happens + for a minute. The process will also be added to the list of watched + processes. + + :param argv: The command line to run, as a list + :param output_redirect: file or file descriptor to redirect stdout and stderr to + """ + # Use a list so the value can be modified from the handler function + x11_started = [False] + def sigusr1_handler(num, frame): + log.debug("X server has signalled a successful start.") + x11_started[0] = True + + # Fail after, let's say a minute, in case something weird happens + # and we don't receive SIGUSR1 + def sigalrm_handler(num, frame): + # Check that it didn't make it under the wire + if x11_started[0]: + return + log.error("Timeout trying to start %s", argv[0]) + raise ExitError("Timeout trying to start %s" % argv[0]) + + # preexec_fn to add the SIGUSR1 handler in the child + def sigusr1_preexec(): + signal.signal(signal.SIGUSR1, signal.SIG_IGN) + + try: + old_sigusr1_handler = signal.signal(signal.SIGUSR1, sigusr1_handler) + old_sigalrm_handler = signal.signal(signal.SIGALRM, sigalrm_handler) + + # Start the timer + signal.alarm(60) + + childproc = startProgram(argv, stdout=output_redirect, stderr=output_redirect, + preexec_fn=sigusr1_preexec) + watchProcess(childproc, argv[0]) + + # Wait for SIGUSR1 + while not x11_started[0]: + signal.pause() + + finally: + # Put everything back where it was + signal.alarm(0) + signal.signal(signal.SIGUSR1, old_sigusr1_handler) + signal.signal(signal.SIGALRM, old_sigalrm_handler) + +def _run_program(argv, root='/', stdin=None, stdout=None, env_prune=None, log_output=True, + binary_output=False, filter_stderr=False): + """ Run an external program, log the output and return it to the caller + :param argv: The command to run and argument + :param root: The directory to chroot to before running command. + :param stdin: The file object to read stdin from. + :param stdout: Optional file object to write the output to. + :param env_prune: environment variable to remove before execution + :param log_output: whether to log the output of command + :param binary_output: whether to treat the output of command as binary data + :param filter_stderr: whether to exclude the contents of stderr from the returned output + :return: The return code of the command and the output + """ + try: + if filter_stderr: + stderr = subprocess.PIPE + else: + stderr = subprocess.STDOUT + + proc = startProgram(argv, root=root, stdin=stdin, stdout=subprocess.PIPE, stderr=stderr, + env_prune=env_prune) + + (output_string, err_string) = proc.communicate() + if output_string: + if binary_output: + output_lines = [output_string] + else: + if output_string[-1] != "\n": + output_string = output_string + "\n" + output_lines = output_string.splitlines(True) + + if log_output: + with program_log_lock: + for line in output_lines: program_log.info(line.strip()) - if stdout: - stdout.write(line) + if stdout: + stdout.write(output_string) - except OSError as e: + # If stderr was filtered, log it separately + if filter_stderr and err_string and log_output: + err_lines = err_string.splitlines(True) + + with program_log_lock: + for line in err_lines: + program_log.info(line.strip()) + + except OSError as e: + with program_log_lock: program_log.error("Error running %s: %s", argv[0], e.strerror) - raise + raise + with program_log_lock: program_log.debug("Return code: %d", proc.returncode) return (proc.returncode, output_string) @@ -192,13 +327,14 @@ def execWithRedirect(command, argv, stdin=None, stdout=None, return _run_program(argv, stdin=stdin, stdout=stdout, root=root, env_prune=env_prune, log_output=log_output, binary_output=binary_output)[0] -def execWithCapture(command, argv, stdin=None, root='/', log_output=True): +def execWithCapture(command, argv, stdin=None, root='/', log_output=True, filter_stderr=False): """ Run an external program and capture standard out and err. :param command: The command to run :param argv: The argument list :param stdin: The file object to read stdin from. :param root: The directory to chroot to before running command. :param log_output: Whether to log the output of command + :param filter_stderr: Whether stderr should be excluded from the returned output :return: The output of the command """ if flags.testing: @@ -207,12 +343,17 @@ def execWithCapture(command, argv, stdin=None, root='/', log_output=True): return "" argv = [command] + argv - return _run_program(argv, stdin=stdin, root=root, log_output=log_output)[1] + return _run_program(argv, stdin=stdin, root=root, log_output=log_output, + filter_stderr=filter_stderr)[1] def execReadlines(command, argv, stdin=None, root='/', env_prune=None): """ Execute an external command and return the line output of the command in real-time. + This method assumes that there is a reasonably low delay between the + end of output and the process exiting. If the child process closes + stdout and then keeps on truckin' there will be problems. + :param command: The command to run :param argv: The argument list :param stdin: The file object to read stdin from. @@ -222,66 +363,210 @@ def execReadlines(command, argv, stdin=None, root='/', env_prune=None): :param env_prune: environment variable to remove before execution Output from the file is not logged to program.log - This returns a generator with the lines from the command until it has finished + This returns an iterator with the lines from the command until it has finished """ - if env_prune is None: - env_prune = [] - # Return the lines from stdout via a Queue - def queue_lines(out, queue): - for line in iter(out.readline, b''): - queue.put(line.strip()) - out.close() + class ExecLineReader(object): + """Iterator class for returning lines from a process and cleaning + up the process when the output is no longer needed. + """ - def chroot(): - if root and root != '/': - os.chroot(root) - os.chdir("/") + def __init__(self, proc, argv): + self._proc = proc + self._argv = argv + + def __iter__(self): + return self + + def __del__(self): + # See if the process is still running + if self._proc.poll() is None: + # Stop the process and ignore any problems that might arise + try: + self._proc.terminate() + except OSError: + pass + + def next(self): + # Read the next line, blocking if a line is not yet available + line = self._proc.stdout.readline() + if line == '': + # Output finished, wait for the process to end + self._proc.communicate() + + # Check for successful exit + if self._proc.returncode < 0: + raise OSError("process '%s' was killed by signal %s" % + (self._argv, -self._proc.returncode)) + elif self._proc.returncode > 0: + raise OSError("process '%s' exited with status %s" % + (self._argv, self._proc.returncode)) + raise StopIteration + + return line.strip() argv = [command] + argv - with program_log_lock: - program_log.info("Running... %s", " ".join(argv)) - env = augmentEnv() - for var in env_prune: - env.pop(var, None) try: - proc = subprocess.Popen(argv, - stdin=stdin, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - preexec_fn=chroot, cwd=root, env=env) + proc = startProgram(argv, root=root, stdin=stdin, env_prune=env_prune, bufsize=1) except OSError as e: - program_log.error("Error running %s: %s", argv[0], e.strerror) + with program_log_lock: + program_log.error("Error running %s: %s", argv[0], e.strerror) raise - q = Queue() - t = Thread(target=queue_lines, args=(proc.stdout, q)) - t.daemon = True # thread dies with the program - t.start() - - while True: - try: - line = q.get(timeout=.1) - yield line - q.task_done() - except Empty: - if proc.poll() is not None: - if os.WIFSIGNALED(proc.returncode): - raise OSError("process '%s' was killed" % argv) - break - q.join() - + return ExecLineReader(proc, argv) ## Run a shell. def execConsole(): try: - proc = subprocess.Popen(["/bin/sh"]) + proc = startProgram(["/bin/sh"], stdout=None, stderr=None, reset_lang=False) proc.wait() except OSError as e: raise RuntimeError("Error running /bin/sh: " + e.strerror) +# Dictionary of processes to watch in the form {pid: [name, GLib event source id], ...} +_forever_pids = {} +# Set to True if process watching is handled by GLib +_watch_process_glib = False +_watch_process_handler_set = False + +class ExitError(RuntimeError): + pass + +# Raise an error on process exit. The argument is a list of tuples +# of the form [(name, status), ...] with statuses in the subprocess +# format (>=0 is return codes, <0 is signal) +def _raise_exit_error(statuses): + exn_message = [] + + for proc_name, status in statuses: + if status >= 0: + status_str = "with status %s" % status + else: + status_str = "on signal %s" % -status + + exn_message.append("%s exited %s" % (proc_name, status_str)) + + raise ExitError(", ".join(exn_message)) + +# Signal handler used with watchProcess +def _sigchld_handler(num=None, frame=None): + # Check whether anything in the list of processes being watched has + # exited. We don't want to call waitpid(-1), since that would break + # anything else using wait/waitpid (like the subprocess module). + exited_pids = [] + exit_statuses = [] + + for child_pid in _forever_pids: + try: + pid_result, status = eintr_retry_call(os.waitpid, child_pid, os.WNOHANG) + except OSError as e: + if e.errno == errno.ECHILD: + continue + + if pid_result: + proc_name = _forever_pids[child_pid][0] + exited_pids.append(child_pid) + + # Convert the wait-encoded status to the format used by subprocess + if os.WIFEXITED(status): + sub_status = os.WEXITSTATUS(status) + else: + # subprocess uses negative return codes to indicate signal exit + sub_status = -os.WTERMSIG(status) + + exit_statuses.append((proc_name, sub_status)) + + for child_pid in exited_pids: + if _forever_pids[child_pid][1]: + GLib.source_remove(_forever_pids[child_pid][1]) + del _forever_pids[child_pid] + + if exit_statuses: + _raise_exit_error(exit_statuses) + +# GLib callback used with watchProcess +def _watch_process_cb(pid, status, proc_name): + # Convert the wait-encoded status to the format used by subprocess + if os.WIFEXITED(status): + sub_status = os.WEXITSTATUS(status) + else: + # subprocess uses negative return codes to indicate signal exit + sub_status = -os.WTERMSIG(status) + + _raise_exit_error([(proc_name, sub_status)]) + +def watchProcess(proc, name): + """Watch for a process exit, and raise a ExitError when it does. + + This method installs a SIGCHLD signal handler and thus interferes + the child_watch_add methods in GLib. Use watchProcessGLib to convert + to GLib mode if using a GLib main loop. + + Since the SIGCHLD handler calls wait() on the watched process, this call + cannot be combined with Popen.wait() or Popen.communicate, and also + doing so wouldn't make a whole lot of sense. + + :param proc: The Popen object for the process + :param name: The name of the process + """ + global _watch_process_handler_set + + if not _watch_process_glib and not _watch_process_handler_set: + signal.signal(signal.SIGCHLD, _sigchld_handler) + _watch_process_handler_set = True + + # Add the PID to the dictionary + # The second item in the list is for the GLib event source id and will be + # replaced with the id once we have one. + _forever_pids[proc.pid] = [name, None] + + # If GLib is watching processes, add a watcher. child_watch_add checks if + # the process has already exited. + if _watch_process_glib: + _forever_pids[proc.id][1] = GLib.child_watch_add(proc.pid, _watch_process_cb, name) + else: + # Check that the process didn't already exit + if proc.poll() is not None: + del _forever_pids[proc.pid] + _raise_exit_error([(name, proc.returncode)]) + +def watchProcessGLib(): + """Convert process watching to GLib mode. + + This allows anaconda modes that use GLib main loops to use + GLib.child_watch_add and continue to watch processes started before the + main loop. + """ + + global _watch_process_glib + + # The first call to child_watch_add will replace our SIGCHLD handler, and + # child_watch_add checks if the process has already exited before it returns, + # which will handle processes that exit while we're in the loop. + + _watch_process_glib = True + for child_pid in _forever_pids: + _forever_pids[child_pid][1] = GLib.child_watch_add(child_pid, _watch_process_cb, + _forever_pids[child_pid]) + +def unwatchProcess(proc): + """Unwatch a process watched by watchProcess. + + :param proc: The Popen object for the process. + """ + if _forever_pids[proc.pid][1]: + GLib.source_remove(_forever_pids[proc.pid][1]) + del _forever_pids[proc.pid] + +def unwatchAllProcesses(): + """Clear the watched process list.""" + global _forever_pids + for child_pid in _forever_pids: + if _forever_pids[child_pid][1]: + GLib.source_remove(_forever_pids[child_pid][1]) + _forever_pids = {} + def getDirSize(directory): """ Get the size of a directory and all its subdirectories. :param dir: The name of the directory to find the size of. @@ -396,7 +681,7 @@ def parseNfsUrl(nfsurl): return (options, host, path) -def add_po_path(module, directory): +def add_po_path(directory): """ Looks to see what translations are under a given path and tells the gettext module to use that path as the base dir """ for d in os.listdir(directory): @@ -408,27 +693,12 @@ def add_po_path(module, directory): if not basename.endswith(".mo"): continue log.info("setting %s as translation source for %s", directory, basename[:-3]) - module.bindtextdomain(basename[:-3], directory) + gettext.bindtextdomain(basename[:-3], directory) -def setup_translations(module): +def setup_translations(): if os.path.isdir(TRANSLATIONS_UPDATE_DIR): - add_po_path(module, TRANSLATIONS_UPDATE_DIR) - module.textdomain("anaconda") - -def fork_orphan(): - """Forks an orphan. - - Returns 1 in the parent and 0 in the orphaned child. - """ - intermediate = os.fork() - if not intermediate: - if os.fork(): - # the intermediate child dies - os._exit(0) - return 0 - # the original process waits for the intermediate child - eintr_retry_call(os.waitpid, intermediate, 0) - return 1 + add_po_path(TRANSLATIONS_UPDATE_DIR) + gettext.textdomain("anaconda") def _run_systemctl(command, service): """ @@ -756,9 +1026,9 @@ def is_unsupported_hw(): :rtype: bool """ try: - tainted = long(open("/proc/sys/kernel/tainted").read()) + tainted = int(open("/proc/sys/kernel/tainted").read()) except (IOError, ValueError): - tainted = 0L + tainted = 0 status = bool(tainted & UNSUPPORTED_HW) if status: @@ -888,6 +1158,25 @@ def xprogressive_delay(): yield 0.25*(2**counter) counter += 1 +def get_platform_groupid(): + """ Return a platform group id string + + This runs systemd-detect-virt and if the result is not 'none' it + prefixes the lower case result with "platform-" for use as a group id. + + :returns: Empty string or a group id for the detected platform + :rtype: str + """ + try: + platform = execWithCapture("systemd-detect-virt", []).strip() + except (IOError, AttributeError): + return "" + + if platform == "none": + return "" + + return "platform-" + platform.lower() + def persistent_root_image(): """:returns: whether we are running from a persistent (not in RAM) root.img""" @@ -901,6 +1190,30 @@ def persistent_root_image(): return True +_supports_ipmi = None + +def ipmi_report(event): + global _supports_ipmi + if _supports_ipmi is None: + _supports_ipmi = os.path.exists("/dev/ipmi0") and os.path.exists("/usr/bin/ipmitool") + + if not _supports_ipmi: + return + + (fd, path) = tempfile.mkstemp() + + # EVM revision - always 0x4 + # Sensor type - always 0x1F for Base OS Boot/Installation Status + # Sensor num - passed in event + # Event dir & type - always 0x0 for anaconda's purposes + # Event data 1, 2, 3 - 0x0 for now + eintr_retry_call(os.write, fd, "0x4 0x1F %#x 0x0 0x0 0x0 0x0\n" % event) + eintr_retry_call(os.close, fd) + + execWithCapture("ipmitool", ["sel", "add", path]) + + os.remove(path) + # Copied from python's subprocess.py def eintr_retry_call(func, *args): """Retry an interruptible system call if interrupted.""" @@ -911,3 +1224,7 @@ def eintr_retry_call(func, *args): if e.errno == errno.EINTR: continue raise + +def parent_dir(directory): + """Return the parent's path""" + return "/".join(os.path.normpath(directory).split("/")[:-1]) diff --git a/anaconda/pyanaconda/keyboard.py b/anaconda/pyanaconda/keyboard.py index 1f3e572..0de3111 100644 --- a/anaconda/pyanaconda/keyboard.py +++ b/anaconda/pyanaconda/keyboard.py @@ -395,12 +395,7 @@ class LocaledWrapper(object): # if there are more layouts than variants, empty strings should be appended diff = len(layouts) - len(variants) variants.extend(diff * [""]) - - # if there are more variants than layouts, throw the trailing ones away - variants = variants[:len(layouts)] - - # map can be used with multiple lists and works like zipWith (Haskell) - return map(join_layout_variant, layouts, variants) + return [join_layout_variant(layout, variant) for layout, variant in zip(layouts, variants)] @property def options(self): diff --git a/anaconda/pyanaconda/kickstart.py b/anaconda/pyanaconda/kickstart.py index 21f4875..37b72a1 100644 --- a/anaconda/pyanaconda/kickstart.py +++ b/anaconda/pyanaconda/kickstart.py @@ -21,15 +21,16 @@ from pyanaconda.errors import ScriptError, errorHandler from blivet.deviceaction import ActionCreateFormat, ActionDestroyFormat, ActionResizeDevice, ActionResizeFormat from blivet.devices import LUKSDevice -from blivet.devicelibs.lvm import getPossiblePhysicalExtents, LVM_PE_SIZE, KNOWN_THPOOL_PROFILES +from blivet.devices.lvm import LVMVolumeGroupDevice +from blivet.devicelibs.lvm import LVM_PE_SIZE, KNOWN_THPOOL_PROFILES from blivet.devicelibs.crypto import MIN_CREATE_ENTROPY -from blivet.devicelibs import swap as swap_lib from blivet.formats import getFormat from blivet.partitioning import doPartitioning from blivet.partitioning import growLVM -from blivet.errors import PartitioningError -from blivet.size import Size +from blivet.errors import PartitioningError, StorageError, BTRFSValueError +from blivet.size import Size, KiB from blivet import udev +from blivet import autopart from blivet.platform import platform import blivet.iscsi import blivet.fcoe @@ -41,9 +42,8 @@ from pyanaconda import iutil import os import os.path import tempfile -import subprocess from pyanaconda.flags import flags, can_touch_runtime_system -from pyanaconda.constants import ADDON_PATHS +from pyanaconda.constants import ADDON_PATHS, IPMI_ABORTED import shlex import sys import urlgrabber @@ -62,12 +62,15 @@ from pyanaconda.i18n import _ from pyanaconda.ui.common import collect from pyanaconda.addons import AddonSection, AddonData, AddonRegistry, collect_addon_paths from pyanaconda.bootloader import GRUB2, get_bootloader +from pyanaconda.pwpolicy import F22_PwPolicy, F22_PwPolicyData from pykickstart.constants import CLEARPART_TYPE_NONE, FIRSTBOOT_SKIP, FIRSTBOOT_RECONFIG, KS_SCRIPT_POST, KS_SCRIPT_PRE, \ KS_SCRIPT_TRACEBACK, SELINUX_DISABLED, SELINUX_ENFORCING, SELINUX_PERMISSIVE +from pykickstart.base import BaseHandler from pykickstart.errors import formatErrorMsg, KickstartError, KickstartValueError from pykickstart.parser import KickstartParser from pykickstart.parser import Script as KSScript +from pykickstart.sections import Section from pykickstart.sections import NullSection, PackageSection, PostScriptSection, PreScriptSection, TracebackScriptSection from pykickstart.version import returnClassForVersion @@ -76,8 +79,7 @@ log = logging.getLogger("anaconda") stderrLog = logging.getLogger("anaconda.stderr") storage_log = logging.getLogger("blivet") stdoutLog = logging.getLogger("anaconda.stdout") -from pyanaconda.anaconda_log import logger, logLevelMap, setHandlersLevel,\ - DEFAULT_TTY_LEVEL +from pyanaconda.anaconda_log import logger, logLevelMap, setHandlersLevel, DEFAULT_LEVEL class AnacondaKSScript(KSScript): """ Execute a kickstart script @@ -96,6 +98,9 @@ class AnacondaKSScript(KSScript): else: scriptRoot = "/" + # Environment variables that cause problems for %post scripts + env_prune = ["LIBUSER_CONF"] + (fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp") iutil.eintr_retry_call(os.write, fd, self.script) @@ -122,7 +127,8 @@ class AnacondaKSScript(KSScript): with open(messages, "w") as fp: rc = iutil.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)], stdout=fp, - root = scriptRoot) + root = scriptRoot, + env_prune = env_prune) if rc != 0: log.error("Error code %s running the kickstart script at line %s", rc, self.lineno) @@ -132,6 +138,7 @@ class AnacondaKSScript(KSScript): err = "".join(fp.readlines()) errorHandler.cb(ScriptError(self.lineno, err)) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(0) class AnacondaInternalScript(AnacondaKSScript): @@ -251,7 +258,7 @@ def refreshAutoSwapSize(storage): for request in storage.autoPartitionRequests: if request.fstype == "swap": disk_space = getAvailableDiskSpace(storage) - request.size = swap_lib.swapSuggestion(disk_space=disk_space) + request.size = autopart.swapSuggestion(disk_space=disk_space) break ### @@ -270,7 +277,7 @@ class Authconfig(commands.authconfig.FC3_Authconfig): def execute(self, *args): cmd = "/usr/sbin/authconfig" if not os.path.lexists(iutil.getSysroot()+cmd): - if self.seen: + if flags.automatedInstall and self.seen: msg = _("%s is missing. Cannot setup authentication.") % cmd raise KickstartError(msg) else: @@ -306,7 +313,7 @@ class AutoPart(commands.autopart.F21_AutoPart): return retval def execute(self, storage, ksdata, instClass): - from blivet.partitioning import doAutoPartition + from blivet.autopart import doAutoPartition from pyanaconda.storage_utils import sanity_check if not self.autopart: @@ -351,6 +358,11 @@ class Bootloader(commands.bootloader.F21_Bootloader): raise KickstartValueError(formatErrorMsg(self.lineno, msg=_("GRUB2 does not support installation to a partition."))) + if self.isCrypted and isinstance(get_bootloader(), GRUB2): + if not self.password.startswith("grub.pbkdf2."): + raise KickstartValueError(formatErrorMsg(self.lineno, + msg="GRUB2 encrypted password must be in grub.pbkdf2 format.")) + return self def execute(self, storage, ksdata, instClass): @@ -449,12 +461,12 @@ class BTRFSData(commands.btrfs.F17_BTRFSData): if dev and dev.format.type != "btrfs": raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("BTRFS partition \"%(device)s\" has a format of \"%(format)s\", but should have a format of \"btrfs\".") % + msg=_("Btrfs partition \"%(device)s\" has a format of \"%(format)s\", but should have a format of \"btrfs\".") % {"device": member, "format": dev.format.type})) if not dev: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("Tried to use undefined partition \"%s\" in BTRFS volume specification.") % member)) + msg=_("Tried to use undefined partition \"%s\" in Btrfs volume specification.") % member)) members.append(dev) @@ -467,7 +479,7 @@ class BTRFSData(commands.btrfs.F17_BTRFSData): if len(members) == 0 and not self.preexist: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("BTRFS volume defined without any member devices. Either specify member devices or use --useexisting."))) + msg=_("Btrfs volume defined without any member devices. Either specify member devices or use --useexisting."))) # allow creating btrfs vols/subvols without specifying mountpoint if self.mountpoint in ("none", "None"): @@ -491,16 +503,19 @@ class BTRFSData(commands.btrfs.F17_BTRFSData): device = devicetree.resolveDevice(self.name) if not device: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("BTRFS volume \"%s\" specified with --useexisting does not exist.") % self.name)) + msg=_("Btrfs volume \"%s\" specified with --useexisting does not exist.") % self.name)) device.format.mountpoint = self.mountpoint else: - request = storage.newBTRFS(name=name, + try: + request = storage.newBTRFS(name=name, subvol=self.subvol, mountpoint=self.mountpoint, metaDataLevel=self.metaDataLevel, dataLevel=self.dataLevel, parents=members) + except BTRFSValueError as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) storage.createDevice(request) @@ -515,17 +530,13 @@ class Realm(commands.realm.F19_Realm): return try: - argv = ["realm", "discover", "--verbose"] + \ + argv = ["discover", "--verbose"] + \ self.discover_options + [self.join_realm] - proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, stderr = proc.communicate() - # might contain useful information for users who use - # use the realm kickstart command - log.info("Realm discover stderr:\n%s", stderr) - except OSError as msg: + output = iutil.execWithCapture("realm", argv, filter_stderr=True) + except OSError: # TODO: A lousy way of propagating what will usually be # 'no such realm' - log.error("Error running realm %s: %s", argv, msg) + # The error message is logged by iutil return # Now parse the output for the required software. First line is the @@ -557,25 +568,16 @@ class Realm(commands.realm.F19_Realm): # no explicit password arg using implicit --no-password pw_args = ["--no-password"] - argv = ["realm", "join", "--install", iutil.getSysroot(), "--verbose"] + \ + argv = ["join", "--install", iutil.getSysroot(), "--verbose"] + \ pw_args + self.join_args rc = -1 try: - proc = subprocess.Popen(argv, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stderr = proc.communicate()[1] - # might contain useful information for users who use - # use the realm kickstart command - log.info("Realm join stderr:\n%s", stderr) - rc = proc.returncode - except OSError as msg: - log.error("Error running %s: %s", argv, msg) + rc = iutil.execWithRedirect("realm", argv) + except OSError: + pass - if rc != 0: - log.error("Command failure: %s: %d", argv, rc) - return - - log.info("Joined realm %s", self.join_realm) + if rc == 0: + log.info("Joined realm %s", self.join_realm) class ClearPart(commands.clearpart.F21_ClearPart): @@ -820,6 +822,11 @@ class LogVolData(commands.logvol.F21_LogVolData): storage.doAutoPart = False + # FIXME: we should be running sanityCheck on partitioning that is not ks + # autopart, but that's likely too invasive for #873135 at this moment + if self.mountpoint == "/boot" and blivet.arch.isS390(): + raise KickstartValueError(formatErrorMsg(self.lineno, msg="/boot can not be of type 'lvmlv' on s390x")) + # we might have truncated or otherwise changed the specified vg name vgname = ksdata.onPart.get(self.vgname, self.vgname) @@ -833,7 +840,7 @@ class LogVolData(commands.logvol.F21_LogVolData): self.mountpoint = "" if self.recommended or self.hibernation: disk_space = getAvailableDiskSpace(storage) - size = swap_lib.swapSuggestion(hibernation=self.hibernation, disk_space=disk_space) + size = autopart.swapSuggestion(hibernation=self.hibernation, disk_space=disk_space) self.grow = False else: if self.fstype != "": @@ -841,7 +848,7 @@ class LogVolData(commands.logvol.F21_LogVolData): else: ty = storage.defaultFSType - if size is None: + if size is None and not self.preexist: if not self.size: raise KickstartValueError(formatErrorMsg(self.lineno, msg="Size can not be decided on from kickstart nor obtained from device.")) @@ -921,18 +928,10 @@ class LogVolData(commands.logvol.F21_LogVolData): msg=_("Logical volume name \"%(logvol)s\" is already in use in volume group \"%(volgroup)s\".") % {"logvol": self.name, "volgroup": vg.name})) - # Size specification checks - if not self.percent: - if not size: - raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("No size given for logical volume \"%s\". Use one of --useexisting, --size, or --percent.") % self.name)) - elif not self.grow and size < vg.peSize: - raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("Logical volume size \"%(logvolSize)s\" must be larger than the volume group extent size of \"%(extentSize)s\".") % - {"logvolSize": size, "extentSize": vg.peSize})) - elif self.percent <= 0 or self.percent > 100: + if not self.percent and size and not self.grow and size < vg.peSize: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("Percentage must be between 0 and 100."))) + msg=_("Logical volume size \"%(logvolSize)s\" must be larger than the volume group extent size of \"%(extentSize)s\".") % + {"logvolSize": size, "extentSize": vg.peSize})) # Now get a format to hold a lot of these extra values. fmt = getFormat(ty, @@ -942,8 +941,9 @@ class LogVolData(commands.logvol.F21_LogVolData): mountopts=self.fsopts) if not fmt.type and not self.thin_pool: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("The \"%s\" filesystem type is not supported.") % ty)) + msg=_("The \"%s\" file system type is not supported.") % ty)) + add_fstab_swap = None # If we were given a pre-existing LV to create a filesystem on, we need # to verify it and its VG exists and then schedule a new format action # to take place there. Also, we only support a subset of all the @@ -967,7 +967,7 @@ class LogVolData(commands.logvol.F21_LogVolData): devicetree.registerAction(ActionCreateFormat(device, fmt)) if ty == "swap": - storage.addFstabSwap(device) + add_fstab_swap = device else: # If a previous device has claimed this mount point, delete the # old one. @@ -1006,7 +1006,8 @@ class LogVolData(commands.logvol.F21_LogVolData): else: maxsize = None - request = storage.newLV(fmt=fmt, + try: + request = storage.newLV(fmt=fmt, name=self.name, parents=parents, size=size, @@ -1016,15 +1017,26 @@ class LogVolData(commands.logvol.F21_LogVolData): maxsize=maxsize, percent=self.percent, **pool_args) + except (StorageError, ValueError) as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) storage.createDevice(request) if ty == "swap": - storage.addFstabSwap(request) + add_fstab_swap = request if self.encrypted: if self.passphrase and not storage.encryptionPassphrase: storage.encryptionPassphrase = self.passphrase + # try to use the global passphrase if available + # XXX: we require the LV/part with --passphrase to be processed + # before this one to setup the storage.encryptionPassphrase + self.passphrase = self.passphrase or storage.encryptionPassphrase + + if not self.passphrase: + raise KickstartValueError(formatErrorMsg(self.lineno, + msg=_("No passphrase given for encrypted LV"))) + cert = getEscrowCertificate(storage.escrowCertificates, self.escrowcert) if self.preexist: luksformat = fmt @@ -1034,8 +1046,7 @@ class LogVolData(commands.logvol.F21_LogVolData): add_backup_passphrase=self.backuppassphrase) luksdev = LUKSDevice("luks%d" % storage.nextID, fmt=luksformat, - parents=device, - min_luks_entropy=MIN_CREATE_ENTROPY) + parents=device) else: luksformat = request.format request.format = getFormat("luks", passphrase=self.passphrase, @@ -1046,14 +1057,22 @@ class LogVolData(commands.logvol.F21_LogVolData): luksdev = LUKSDevice("luks%d" % storage.nextID, fmt=luksformat, parents=request) + if ty == "swap": + # swap is on the LUKS device not on the LUKS' parent device, + # override the info here + add_fstab_swap = luksdev + storage.createDevice(luksdev) + if add_fstab_swap: + storage.addFstabSwap(add_fstab_swap) + class Logging(commands.logging.FC6_Logging): def execute(self, *args): - if logger.tty_loglevel == DEFAULT_TTY_LEVEL: + if logger.loglevel == DEFAULT_LEVEL: # not set from the command line level = logLevelMap[self.level] - logger.tty_loglevel = level + logger.loglevel = level setHandlersLevel(log, level) setHandlersLevel(storage_log, level) @@ -1064,7 +1083,7 @@ class Logging(commands.logging.FC6_Logging): remote_server = "%s:%s" %(self.host, self.port) logger.updateRemote(remote_server) -class Network(commands.network.F21_Network): +class Network(commands.network.F22_Network): def execute(self, storage, ksdata, instClass): network.write_network_config(storage, ksdata, instClass, iutil.getSysroot()) @@ -1092,7 +1111,7 @@ class PartitionData(commands.partition.F18_PartData): storage.doAutoPart = False if self.onbiosdisk != "": - for (disk, biosdisk) in storage.eddDict.iteritems(): + for (disk, biosdisk) in storage.eddDict.items(): if "%x" % biosdisk == self.onbiosdisk: self.disk = disk break @@ -1108,7 +1127,7 @@ class PartitionData(commands.partition.F18_PartData): self.mountpoint = "" if self.recommended or self.hibernation: disk_space = getAvailableDiskSpace(storage) - size = swap_lib.swapSuggestion(hibernation=self.hibernation, disk_space=disk_space) + size = autopart.swapSuggestion(hibernation=self.hibernation, disk_space=disk_space) self.grow = False # if people want to specify no mountpoint for some reason, let them # this is really needed for pSeries boot partitions :( @@ -1156,7 +1175,7 @@ class PartitionData(commands.partition.F18_PartData): if devicetree.getDeviceByName(kwargs["name"]): raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("BTRFS partition \"%s\" is defined multiple times.") % kwargs["name"])) + msg=_("Btrfs partition \"%s\" is defined multiple times.") % kwargs["name"])) if self.onPart: ksdata.onPart[kwargs["name"]] = self.onPart @@ -1229,7 +1248,7 @@ class PartitionData(commands.partition.F18_PartData): size=size) if not kwargs["fmt"].type: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("The \"%s\" filesystem type is not supported.") % ty)) + msg=_("The \"%s\" file system type is not supported.") % ty)) # If we were given a specific disk to create the partition on, verify # that it exists first. If it doesn't exist, see if it exists with @@ -1275,6 +1294,7 @@ class PartitionData(commands.partition.F18_PartData): kwargs["primary"] = self.primOnly + add_fstab_swap = None # If we were given a pre-existing partition to create a filesystem on, # we need to verify it exists and then schedule a new format action to # take place there. Also, we only support a subset of all the options @@ -1297,11 +1317,14 @@ class PartitionData(commands.partition.F18_PartData): devicetree.registerAction(ActionCreateFormat(device, kwargs["fmt"])) if ty == "swap": - storage.addFstabSwap(device) + add_fstab_swap = device # tmpfs mounts are not disks and don't occupy a disk partition, # so handle them here elif self.fstype == "tmpfs": - request = storage.newTmpFS(**kwargs) + try: + request = storage.newTmpFS(**kwargs) + except (StorageError, ValueError) as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) storage.createDevice(request) else: # If a previous device has claimed this mount point, delete the @@ -1313,15 +1336,28 @@ class PartitionData(commands.partition.F18_PartData): except KeyError: pass - request = storage.newPartition(**kwargs) + try: + request = storage.newPartition(**kwargs) + except (StorageError, ValueError) as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) + storage.createDevice(request) if ty == "swap": - storage.addFstabSwap(request) + add_fstab_swap = request if self.encrypted: if self.passphrase and not storage.encryptionPassphrase: storage.encryptionPassphrase = self.passphrase + # try to use the global passphrase if available + # XXX: we require the LV/part with --passphrase to be processed + # before this one to setup the storage.encryptionPassphrase + self.passphrase = self.passphrase or storage.encryptionPassphrase + + if not self.passphrase: + raise KickstartValueError(formatErrorMsg(self.lineno, + msg=_("No passphrase given for encrypted part"))) + cert = getEscrowCertificate(storage.escrowCertificates, self.escrowcert) if self.onPart: luksformat = kwargs["fmt"] @@ -1343,8 +1379,17 @@ class PartitionData(commands.partition.F18_PartData): luksdev = LUKSDevice("luks%d" % storage.nextID, fmt=luksformat, parents=request) + + if ty == "swap": + # swap is on the LUKS device not on the LUKS' parent device, + # override the info here + add_fstab_swap = luksdev + storage.createDevice(luksdev) + if add_fstab_swap: + storage.addFstabSwap(add_fstab_swap) + class Raid(commands.raid.F20_Raid): def execute(self, storage, ksdata, instClass): for r in self.raidList: @@ -1384,7 +1429,7 @@ class RaidData(commands.raid.F18_RaidData): if devicetree.getDeviceByName(kwargs["name"]): raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("BTRFS partition \"%s\" is defined multiple times.") % kwargs["name"])) + msg=_("Btrfs partition \"%s\" is defined multiple times.") % kwargs["name"])) self.mountpoint = "" else: @@ -1449,7 +1494,7 @@ class RaidData(commands.raid.F18_RaidData): mountopts=self.fsopts) if not kwargs["fmt"].type: raise KickstartValueError(formatErrorMsg(self.lineno, - msg=_("The \"%s\" filesystem type is not supported.") % ty)) + msg=_("The \"%s\" file system type is not supported.") % ty)) kwargs["name"] = devicename kwargs["level"] = self.level @@ -1485,8 +1530,8 @@ class RaidData(commands.raid.F18_RaidData): try: request = storage.newMDArray(**kwargs) - except ValueError as e: - raise KickstartValueError(formatErrorMsg(self.lineno, msg=str(e))) + except (StorageError, ValueError) as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) storage.createDevice(request) @@ -1515,7 +1560,7 @@ class RaidData(commands.raid.F18_RaidData): parents=request) storage.createDevice(luksdev) -class RepoData(commands.repo.F15_RepoData): +class RepoData(commands.repo.F21_RepoData): def __init__(self, *args, **kwargs): """ Add enabled kwarg @@ -1524,7 +1569,7 @@ class RepoData(commands.repo.F15_RepoData): """ self.enabled = kwargs.pop("enabled", True) - commands.repo.F15_RepoData.__init__(self, *args, **kwargs) + commands.repo.F21_RepoData.__init__(self, *args, **kwargs) class RootPw(commands.rootpw.F18_RootPw): def __init__(self, writePriority=100, *args, **kwargs): @@ -1574,6 +1619,11 @@ class Services(commands.services.FC6_Services): iutil.execInSysroot("systemctl", ["enable", svc]) +class SshKey(commands.sshkey.F22_SshKey): + def execute(self, storage, ksdata, instClass, users): + for usr in self.sshUserList: + users.setUserSshKey(usr.username, usr.key) + class Timezone(commands.timezone.F18_Timezone): def __init__(self, *args): commands.timezone.F18_Timezone.__init__(self, *args) @@ -1637,9 +1687,9 @@ class Timezone(commands.timezone.F18_Timezone): # write out NTP configuration (if set) if not self.nontp and self.ntpservers: chronyd_conf_path = os.path.normpath(iutil.getSysroot() + ntp.NTP_CONFIG_FILE) + pools, servers = ntp.internal_to_pools_and_servers(self.ntpservers) try: - ntp.save_servers_to_config(self.ntpservers, - conf_file_path=chronyd_conf_path) + ntp.save_servers_to_config(pools, servers, conf_file_path=chronyd_conf_path) except ntp.NTPconfigError as ntperr: log.warning("Failed to save NTP configuration: %s", ntperr) @@ -1702,10 +1752,10 @@ class VolGroupData(commands.volgroup.F21_VolGroupData): if self.pesize == 0: # default PE size requested -- we use blivet's default in KiB - self.pesize = LVM_PE_SIZE.convertTo(spec="KiB") + self.pesize = LVM_PE_SIZE.convertTo(KiB) pesize = Size("%d KiB" % self.pesize) - possible_extents = getPossiblePhysicalExtents() + possible_extents = LVMVolumeGroupDevice.get_supported_pe_sizes() if pesize not in possible_extents: raise KickstartValueError(formatErrorMsg(self.lineno, msg=_("Volume group given physical extent size of \"%(extentSize)s\", but must be one of:\n%(validExtentSizes)s.") % @@ -1725,9 +1775,12 @@ class VolGroupData(commands.volgroup.F21_VolGroupData): raise KickstartValueError(formatErrorMsg(self.lineno, msg=_("The volume group name \"%s\" is already in use.") % self.vgname)) else: - request = storage.newVG(parents=pvs, + try: + request = storage.newVG(parents=pvs, name=self.vgname, peSize=pesize) + except (StorageError, ValueError) as e: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=e.message)) storage.createDevice(request) if self.reserved_space: @@ -1777,8 +1830,64 @@ class Upgrade(commands.upgrade.F20_Upgrade): def parse(self, *args): log.error("The upgrade kickstart command is no longer supported. Upgrade functionality is provided through fedup.") sys.stderr.write(_("The upgrade kickstart command is no longer supported. Upgrade functionality is provided through fedup.")) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) +### +### %anaconda Section +### + +class AnacondaSectionHandler(BaseHandler): + """A handler for only the anaconda ection's commands.""" + commandMap = { + "pwpolicy": F22_PwPolicy + } + + dataMap = { + "PwPolicyData": F22_PwPolicyData + } + + def __init__(self): + BaseHandler.__init__(self, mapping=self.commandMap, dataMapping=self.dataMap) + + def __str__(self): + """Return the %anaconda section""" + retval = "" + lst = sorted(self._writeOrder.keys()) + for prio in lst: + for obj in self._writeOrder[prio]: + retval += str(obj) + + if retval: + retval = "\n%anaconda\n" + retval + "%end\n" + return retval + +class AnacondaSection(Section): + """A section for anaconda specific commands.""" + sectionOpen = "%anaconda" + + def __init__(self, *args, **kwargs): + Section.__init__(self, *args, **kwargs) + self.cmdno = 0 + + def handleLine(self, line): + if not self.handler: + return + + self.cmdno += 1 + args = shlex.split(line, comments=True) + self.handler.currentCmd = args[0] + self.handler.currentLine = self.cmdno + return self.handler.dispatcher(args, self.cmdno) + + def handleHeader(self, lineno, args): + """Process the arguments to the %anaconda header.""" + Section.handleHeader(self, lineno, args) + + def finalize(self): + """Let %anaconda know no additional data will come.""" + Section.finalize(self) + ### ### HANDLERS ### @@ -1814,6 +1923,7 @@ commandMap = { "rootpw": RootPw, "selinux": SELinux, "services": Services, + "sshkey": SshKey, "skipx": SkipX, "timezone": Timezone, "upgrade": Upgrade, @@ -1870,8 +1980,11 @@ class AnacondaKSHandler(superclass): # Prepare the final structures for 3rd party addons self.addons = AddonRegistry(addons) + # The %anaconda section uses its own handler for a limited set of commands + self.anaconda = AnacondaSectionHandler() + def __str__(self): - return superclass.__str__(self) + "\n" + str(self.addons) + return superclass.__str__(self) + "\n" + str(self.addons) + str(self.anaconda) class AnacondaPreParser(KickstartParser): # A subclass of KickstartParser that only looks for %pre scripts and @@ -1889,6 +2002,7 @@ class AnacondaPreParser(KickstartParser): self.registerSection(NullSection(self.handler, sectionOpen="%traceback")) self.registerSection(NullSection(self.handler, sectionOpen="%packages")) self.registerSection(NullSection(self.handler, sectionOpen="%addon")) + self.registerSection(NullSection(self.handler.anaconda, sectionOpen="%anaconda")) class AnacondaKSParser(KickstartParser): @@ -1909,6 +2023,7 @@ class AnacondaKSParser(KickstartParser): self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass)) self.registerSection(PackageSection(self.handler)) self.registerSection(AddonSection(self.handler)) + self.registerSection(AnacondaSection(self.handler.anaconda)) def preScriptPass(f): # The first pass through kickstart file processing - look for %pre scripts @@ -1922,6 +2037,7 @@ def preScriptPass(f): # We do not have an interface here yet, so we cannot use our error # handling callback. print(e) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) # run %pre scripts @@ -1949,6 +2065,7 @@ def parseKickstart(f): # We do not have an interface here yet, so we cannot use our error # handling callback. print(e) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) return handler @@ -1970,22 +2087,17 @@ def appendPostScripts(ksdata): ksparser.readKickstartFromString(scripts, reset=False) def runPostScripts(scripts): - postScripts = filter (lambda s: s.type == KS_SCRIPT_POST, scripts) + postScripts = [s for s in scripts if s.type == KS_SCRIPT_POST] if len(postScripts) == 0: return - # Remove environment variables that cause problems for %post scripts. - for var in ["LIBUSER_CONF"]: - if var in os.environ: - del(os.environ[var]) - log.info("Running kickstart %%post script(s)") map (lambda s: s.run(iutil.getSysroot()), postScripts) log.info("All kickstart %%post script(s) have been run") def runPreScripts(scripts): - preScripts = filter (lambda s: s.type == KS_SCRIPT_PRE, scripts) + preScripts = [s for s in scripts if s.type == KS_SCRIPT_PRE] if len(preScripts) == 0: return diff --git a/anaconda/pyanaconda/localization.py b/anaconda/pyanaconda/localization.py index a7cc908..a305941 100644 --- a/anaconda/pyanaconda/localization.py +++ b/anaconda/pyanaconda/localization.py @@ -29,7 +29,7 @@ import glob from collections import namedtuple from pyanaconda import constants -from pyanaconda.iutil import upcase_first_letter +from pyanaconda.iutil import upcase_first_letter, setenv import logging log = logging.getLogger("anaconda") @@ -152,7 +152,7 @@ def find_best_locale_match(locale, langcodes): if not locale_parts or not langcode_parts: return score - for part, part_score in score_map.iteritems(): + for part, part_score in score_map.items(): if locale_parts[part] and langcode_parts[part]: if locale_parts[part] == langcode_parts[part]: # match @@ -187,6 +187,9 @@ def setup_locale(locale, lang=None): ksdata.lang object (if given). DOES NOT PERFORM ANY CHECKS OF THE GIVEN LOCALE. + $LANG must be set by the caller in order to set the language used by gettext. + Doing this in a thread-safe way is up to the caller. + :param locale: locale to setup :type locale: str :param lang: ksdata.lang object or None @@ -198,7 +201,7 @@ def setup_locale(locale, lang=None): if lang: lang.lang = locale - os.environ["LANG"] = locale + setenv("LANG", locale) locale_mod.setlocale(locale_mod.LC_ALL, locale) def get_english_name(locale): @@ -395,7 +398,7 @@ def get_xlated_timezone(tz_spec_part): territoryIdQuery=parts.get("territory", ""), scriptIdQuery=parts.get("script", "")) - return xlated.encode("utf-8") + return xlated def write_language_configuration(lang, root): """ @@ -420,6 +423,8 @@ def load_firmware_language(lang): information in the given ksdata.lang object and sets the $LANG environment variable. + This method must be run before any other threads are started. + :param lang: ksdata.lang object :return: None :rtype: None @@ -471,6 +476,8 @@ def load_firmware_language(lang): log.debug("Using UEFI PlatformLang '%s' ('%s') as our language.", d, locales[0]) setup_locale(locales[0], lang) + os.environ["LANG"] = locales[0] # pylint: disable=environment-modify + _DateFieldSpec = namedtuple("DateFieldSpec", ["format", "suffix"]) def resolve_date_format(year, month, day, fail_safe=True): diff --git a/anaconda/pyanaconda/network.py b/anaconda/pyanaconda/network.py index 1fe62a6..4c568bd 100644 --- a/anaconda/pyanaconda/network.py +++ b/anaconda/pyanaconda/network.py @@ -71,11 +71,6 @@ def setup_ifcfg_log(): logger = logging.getLogger("ifcfg") logger.setLevel(logging.DEBUG) anaconda_log.logger.addFileHandler(ifcfgLogFile, logger, logging.DEBUG) - if os.access("/dev/tty3", os.W_OK): - anaconda_log.logger.addFileHandler("/dev/tty3", logger, - anaconda_log.DEFAULT_TTY_LEVEL, - anaconda_log.TTY_FORMAT, - autoLevel=True) anaconda_log.logger.forwardToSyslog(logger) ifcfglog = logging.getLogger("ifcfg") @@ -100,13 +95,13 @@ def sanityCheckHostname(hostname): """ if not hostname: - return (False, _("Hostname cannot be None or an empty string.")) + return (False, _("Host name cannot be None or an empty string.")) if len(hostname) > 255: - return (False, _("Hostname must be 255 or fewer characters in length.")) + return (False, _("Host name must be 255 or fewer characters in length.")) if not (re.match('^' + HOSTNAME_PATTERN_WITHOUT_ANCHORS + '$', hostname)): - return (False, _("Hostnames can only contain the characters 'a-z', " + return (False, _("Host names can only contain the characters 'a-z', " "'A-Z', '0-9', '-', or '.', parts between periods " "must contain something and cannot start or end with " "'-'.")) @@ -363,6 +358,11 @@ def dracutBootArguments(devname, ifcfg, storage_ipaddr, hostname=None): if hwaddr: netargs.add("ifname=%s:%s" % (devname, hwaddr.lower())) + if ifcfg.get("TYPE") == "Team" or ifcfg.get("DEVICETYPE") == "Team": + slaves = get_team_slaves([devname, ifcfg.get("UUID")]) + netargs.add("team=%s:%s" % (devname, + ",".join(dev for dev, _cfg in slaves))) + nettype = ifcfg.get("NETTYPE") subchannels = ifcfg.get("SUBCHANNELS") if blivet.arch.isS390() and nettype and subchannels: @@ -463,31 +463,8 @@ def add_connection_for_ksdata(networkdata, devname): values.append(['bond', 'interface-name', devname, 's']) options = bond_options_ksdata_to_dbus(networkdata.bondopts) values.append(['bond', 'options', options, 'a{ss}']) - for _i, slave in enumerate(networkdata.bondslaves.split(","), 1): - - #slave_name = "%s slave %d" % (devname, i) - slave_name = slave - - svalues = [] - suuid = str(uuid4()) - svalues.append(['connection', 'uuid', suuid, 's']) - svalues.append(['connection', 'id', slave_name, 's']) - svalues.append(['connection', 'slave-type', 'bond', 's']) - svalues.append(['connection', 'master', devname, 's']) - svalues.append(['connection', 'type', '802-3-ethernet', 's']) - mac = nm.nm_device_perm_hwaddress(slave) - mac = [int(b, 16) for b in mac.split(":")] - svalues.append(['802-3-ethernet', 'mac-address', mac, 'ay']) - - # disconnect slaves - if networkdata.activate: - nm.nm_disconnect_device(slave) - # remove ifcfg file - ifcfg_path = find_ifcfg_file_of_device(slave) - if ifcfg_path and os.access(ifcfg_path, os.R_OK): - os.unlink(ifcfg_path) - - nm.nm_add_connection(svalues) + for slave in networkdata.bondslaves.split(","): + suuid = _add_slave_connection('bond', slave, devname, networkdata.activate) added_connections.append((suuid, slave)) dev_spec = None # type "team" @@ -496,33 +473,9 @@ def add_connection_for_ksdata(networkdata, devname): values.append(['connection', 'id', devname, 's']) values.append(['team', 'interface-name', devname, 's']) values.append(['team', 'config', networkdata.teamconfig, 's']) - for _i, (slave, cfg) in enumerate(networkdata.teamslaves): - - # assume ethernet, TODO: infiniband, wifi, vlan - #slave_name = "%s slave %d" % (devname, i) - slave_name = slave - - svalues = [] - suuid = str(uuid4()) - svalues.append(['connection', 'uuid', suuid, 's']) - svalues.append(['connection', 'id', slave_name, 's']) - svalues.append(['connection', 'slave-type', 'team', 's']) - svalues.append(['connection', 'master', devname, 's']) - svalues.append(['connection', 'type', '802-3-ethernet', 's']) - mac = nm.nm_device_perm_hwaddress(slave) - mac = [int(b, 16) for b in mac.split(":")] - svalues.append(['802-3-ethernet', 'mac-address', mac, 'ay']) - svalues.append(['team-port', 'config', cfg, 's']) - - # disconnect slaves - if networkdata.activate: - nm.nm_disconnect_device(slave) - # remove ifcfg file - ifcfg_path = find_ifcfg_file_of_device(slave) - if ifcfg_path and os.access(ifcfg_path, os.R_OK): - os.unlink(ifcfg_path) - - nm.nm_add_connection(svalues) + for (slave, cfg) in networkdata.teamslaves: + values = [['team-port', 'config', cfg, 's']] + suuid = _add_slave_connection('team', slave, devname, networkdata.activate, values) added_connections.append((suuid, slave)) dev_spec = None # type "vlan" @@ -533,19 +486,84 @@ def add_connection_for_ksdata(networkdata, devname): values.append(['vlan', 'interface-name', devname, 's']) values.append(['vlan', 'id', int(networkdata.vlanid), 'u']) dev_spec = None + # type "bridge" + elif networkdata.bridgeslaves: + # bridge connection is autoactivated + values.append(['connection', 'type', 'bridge', 's']) + values.append(['connection', 'id', devname, 's']) + values.append(['bridge', 'interface-name', devname, 's']) + for opt in networkdata.bridgeopts.split(","): + key, _sep, value = opt.partition("=") + if key == "stp": + if value == "yes": + values.append(['bridge', key, True, 'b']) + elif value == "no": + values.append(['bridge', key, False, 'b']) + continue + try: + value = int(value) + except ValueError: + log.error("Invalid bridge option %s", opt) + continue + values.append(['bridge', key, int(value), 'u']) + for slave in networkdata.bridgeslaves.split(","): + suuid = _add_slave_connection('bridge', slave, devname, networkdata.activate) + added_connections.append((suuid, slave)) + dev_spec = None # type "802-3-ethernet" else: + mac = nm.nm_device_perm_hwaddress(devname) + if flags.cmdline.get("ifname", "").upper() == "{0}:{1}".format(devname, mac).upper(): + mac = [int(b, 16) for b in mac.split(":")] + values.append(['802-3-ethernet', 'mac-address', mac, 'ay']) + else: + values.append(['802-3-ethernet', 'name', devname, 's']) values.append(['connection', 'type', '802-3-ethernet', 's']) values.append(['connection', 'id', devname, 's']) - mac = nm.nm_device_perm_hwaddress(devname) - mac = [int(b, 16) for b in mac.split(":")] - values.append(['802-3-ethernet', 'mac-address', mac, 'ay']) + values.append(['connection', 'interface-name', devname, 's']) + dev_spec = devname - nm.nm_add_connection(values) + try: + nm.nm_add_connection(values) + except nm.BondOptionsError as e: + log.error(e) + return [] added_connections.insert(0, (con_uuid, dev_spec)) return added_connections +def _add_slave_connection(slave_type, slave, master, activate, values=None): + values = values or [] + #slave_name = "%s slave %d" % (devname, slave_idx) + slave_name = slave + + values = [] + suuid = str(uuid4()) + # assume ethernet, TODO: infiniband, wifi, vlan + values.append(['connection', 'uuid', suuid, 's']) + values.append(['connection', 'id', slave_name, 's']) + values.append(['connection', 'slave-type', slave_type, 's']) + values.append(['connection', 'master', master, 's']) + values.append(['connection', 'type', '802-3-ethernet', 's']) + mac = nm.nm_device_perm_hwaddress(slave) + mac = [int(b, 16) for b in mac.split(":")] + values.append(['802-3-ethernet', 'mac-address', mac, 'ay']) + + # disconnect slaves + if activate: + try: + nm.nm_disconnect_device(slave) + except nm.DeviceNotActiveError: + pass + # remove ifcfg file + ifcfg_path = find_ifcfg_file_of_device(slave) + if ifcfg_path and os.access(ifcfg_path, os.R_OK): + os.unlink(ifcfg_path) + + nm.nm_add_connection(values) + + return suuid + def ksdata_from_ifcfg(devname, uuid=None): if devname not in nm.nm_devices(): @@ -586,6 +604,8 @@ def ksdata_from_ifcfg(devname, uuid=None): nd.device = devname elif nm.nm_device_type_is_team(devname): nd.device = devname + elif nm.nm_device_type_is_bridge(devname): + nd.device = devname elif nm.nm_device_type_is_vlan(devname): if devname != default_ks_vlan_interface_name(nd.device, nd.vlanid): nd.interfacename = devname @@ -604,6 +624,9 @@ def ifcfg_to_ksdata(ifcfg, devname): # no network command for team slaves if ifcfg.get("TEAM_MASTER"): return None + # no network command for bridge slaves + if ifcfg.get("BRIDGE"): + return None # ipv4 and ipv6 if ifcfg.get("ONBOOT") and ifcfg.get("ONBOOT" ) == "no": @@ -682,7 +705,7 @@ def ifcfg_to_ksdata(ifcfg, devname): # bonding # FIXME: dracut has only BOND_OPTS if ifcfg.get("BONDING_MASTER") == "yes" or ifcfg.get("TYPE") == "Bond": - slaves = get_bond_slaves_from_ifcfgs([devname, ifcfg.get("UUID")]) + slaves = get_slaves_from_ifcfgs("MASTER", [devname, ifcfg.get("UUID")]) if slaves: kwargs["bondslaves"] = ",".join(slaves) bondopts = ifcfg.get("BONDING_OPTS") @@ -697,6 +720,20 @@ def ifcfg_to_ksdata(ifcfg, devname): kwargs["device"] = ifcfg.get("PHYSDEV") kwargs["vlanid"] = ifcfg.get("VLAN_ID") + # bridging + if ifcfg.get("TYPE") == "Bridge": + slaves = get_slaves_from_ifcfgs("BRIDGE", [devname, ifcfg.get("UUID")]) + if slaves: + kwargs["bridgeslaves"] = ",".join(slaves) + + bridgeopts = ifcfg.get("BRIDGING_OPTS").replace('_', '-').split() + if ifcfg.get("STP"): + bridgeopts.append("%s=%s" % ("stp", ifcfg.get("STP"))) + if ifcfg.get("DELAY"): + bridgeopts.append("%s=%s" % ("forward-delay", ifcfg.get("DELAY"))) + if bridgeopts: + kwargs["bridgeopts"] = ",".join(bridgeopts) + # pylint: disable=no-member nd = handler.NetworkData(**kwargs) @@ -734,6 +771,8 @@ def find_ifcfg_file_of_device(devname, root_path=""): ifcfg_path = find_ifcfg_file([("DEVICE", devname)]) elif nm.nm_device_type_is_vlan(devname): ifcfg_path = find_ifcfg_file([("DEVICE", devname)]) + elif nm.nm_device_type_is_bridge(devname): + ifcfg_path = find_ifcfg_file([("DEVICE", devname)]) elif nm.nm_device_type_is_ethernet(devname): try: hwaddr = nm.nm_device_perm_hwaddress(devname) @@ -750,6 +789,10 @@ def find_ifcfg_file_of_device(devname, root_path=""): ifcfg_path = find_ifcfg_file([("HWADDR", hwaddr_check), ("TEAM_MASTER", nonempty)], root_path) + if not ifcfg_path: + ifcfg_path = find_ifcfg_file([("HWADDR", hwaddr_check), + ("BRIDGE", nonempty)], + root_path) if not ifcfg_path: ifcfg_path = find_ifcfg_file([("HWADDR", hwaddr_check)], root_path) if not ifcfg_path: @@ -772,9 +815,10 @@ def find_ifcfg_file(values, root_path=""): return filepath return None -def get_bond_slaves_from_ifcfgs(master_specs): - """List of slave device names of master specified by master_specs. +def get_slaves_from_ifcfgs(master_option, master_specs): + """List of slaves of master specified by master_specs in master_option. + master_option is ifcfg option containing spec of master master_specs is a list containing device name of master (dracut) and/or master's connection uuid """ @@ -783,7 +827,7 @@ def get_bond_slaves_from_ifcfgs(master_specs): for filepath in _ifcfg_files(netscriptsDir): ifcfg = IfcfgFile(filepath) ifcfg.read() - master = ifcfg.get("MASTER") + master = ifcfg.get(master_option) if master in master_specs: device = ifcfg.get("DEVICE") if device: @@ -894,6 +938,15 @@ def copyDhclientConfFiles(destPath): copyFileToPath(dhclientfile, destPath) def ks_spec_to_device_name(ksspec=""): + """ + Find the first network device which matches the kickstart specification. + Will not match derived types such as bonds and vlans. + + :param ksspec: kickstart-specified device name + :returns: a string naming a physical device, or "" meaning none matched + :rtype: str + + """ bootif_mac = '' if ksspec == 'bootif' and "BOOTIF" in flags.cmdline: bootif_mac = flags.cmdline["BOOTIF"][3:].replace("-", ":").upper() @@ -901,7 +954,7 @@ def ks_spec_to_device_name(ksspec=""): # "eth0" if ksspec == dev: break - # "link" + # "link" - match the first device which is plugged (has a carrier) elif ksspec == 'link': try: link_up = nm.nm_device_carrier(dev) @@ -936,7 +989,7 @@ def ks_spec_to_device_name(ksspec=""): def set_hostname(hn): if can_touch_runtime_system("set hostname", touch_live=True): - log.info("setting installation environment hostname to %s", hn) + log.info("setting installation environment host name to %s", hn) iutil.execWithRedirect("hostnamectl", ["set-hostname", hn]) def write_hostname(rootpath, ksdata, overwrite=False): @@ -1040,7 +1093,7 @@ def update_hostname_data(ksdata, hostname=None): if not hostname: # Default to 'dom0' in Qubes hostname = 'dom0' - log.debug("updating hostname %s", hostname) + log.debug("updating host name %s", hostname) hostname_found = False for nd in ksdata.network.network: if nd.hostname: @@ -1051,13 +1104,20 @@ def update_hostname_data(ksdata, hostname=None): ksdata.network.network.append(nd) def get_device_name(network_data): + """ + Find the first network device which matches the kickstart specification. + :param network_data: A pykickstart NetworkData object + :returns: a string naming a physical device, or "" meaning none matched + :rtype: str + + """ ksspec = network_data.device or flags.cmdline.get('ksdevice') or "" dev_name = ks_spec_to_device_name(ksspec) if not dev_name: return "" if dev_name not in nm.nm_devices(): - if not any((network_data.vlanid, network_data.bondslaves, network_data.teamslaves)): + if not any((network_data.vlanid, network_data.bondslaves, network_data.teamslaves, network_data.bridgeslaves)): return "" if network_data.vlanid: network_data.parent = dev_name @@ -1134,9 +1194,9 @@ def apply_kickstart(ksdata): for con_uuid, dev_name in added_connections: try: nm.nm_activate_device_connection(dev_name, con_uuid) - except nm.UnknownConnectionError: - log.warning("network: pre kickstart: can't activate connection %s on %s", - con_uuid, dev_name) + except (nm.UnknownConnectionError, nm.UnknownDeviceError) as e: + log.warning("network: pre kickstart: can't activate connection %s on %s: %s", + con_uuid, dev_name, e) return applied_devices def networkInitialize(ksdata): @@ -1181,7 +1241,7 @@ def _get_ntp_servers_from_dhcp(ksdata): hostname = socket.gethostbyaddr(server_address)[0] except socket.error: # getting hostname failed, just use the address returned from DHCP - log.debug("getting NTP server hostname failed for address: %s", + log.debug("getting NTP server host name failed for address: %s", server_address) hostname = server_address hostnames.append(hostname) @@ -1301,10 +1361,14 @@ def status_message(): msg = _("Team%(interface_name)s (%(list_of_slaves)s) connected") \ % {"interface_name": devname, \ "list_of_slaves": ",".join(slaves[devname])} + elif nm.nm_device_type_is_bridge(devname): + msg = _("Bridge%(interface_name)s (%(list_of_slaves)s) connected") \ + % {"interface_name": devname, \ + "list_of_slaves": ",".join(slaves[devname])} elif nm.nm_device_type_is_vlan(devname): parent = nm.nm_device_setting_value(devname, "vlan", "parent") vlanid = nm.nm_device_setting_value(devname, "vlan", "id") - msg = _("Vlan %(interface_name)s (%(parent_device)s, ID %(vlanid)s) connected") \ + msg = _("VLAN %(interface_name)s (%(parent_device)s, ID %(vlanid)s) connected") \ % {"interface_name": devname, "parent_device": parent, "vlanid": vlanid} elif len(nonslaves) > 1: devlist = [] @@ -1317,6 +1381,8 @@ def status_message(): devlist.append("%s (%s)" % (devname, ",".join(slaves[devname]))) elif nm.nm_device_type_is_team(devname): devlist.append("%s (%s)" % (devname, ",".join(slaves[devname]))) + elif nm.nm_device_type_is_bridge(devname): + devlist.append("%s (%s)" % (devname, ",".join(slaves[devname]))) elif nm.nm_device_type_is_vlan(devname): devlist.append("%s" % devname) msg = _("Connected: %(list_of_interface_names)s") \ @@ -1362,3 +1428,6 @@ def update_onboot_value(devname, value, ksdata): if nd.device == devname: nd.onboot = True break + +def is_using_team_device(): + return any(nm.nm_device_type_is_team(d) for d in nm.nm_devices()) diff --git a/anaconda/pyanaconda/nm.py b/anaconda/pyanaconda/nm.py index 6e1c8e2..183cd96 100644 --- a/anaconda/pyanaconda/nm.py +++ b/anaconda/pyanaconda/nm.py @@ -21,10 +21,8 @@ from gi.repository import Gio, GLib from gi.repository import NetworkManager -import IPy import struct import socket -import re import logging log = logging.getLogger("anaconda") @@ -77,6 +75,15 @@ class UnknownConnectionError(Exception): def __str__(self): return self.__repr__() +class AddConnectionError(Exception): + """Connection is not available for the device""" + def __str__(self): + return self.__repr__() + +# bug #1039006 +class BondOptionsError(AddConnectionError): + pass + def _get_proxy(bus_type=Gio.BusType.SYSTEM, proxy_flags=Gio.DBusProxyFlags.NONE, info=None, @@ -312,6 +319,18 @@ def nm_device_type_is_team(name): """ return nm_device_type(name) == NetworkManager.DeviceType.TEAM +def nm_device_type_is_bridge(name): + """Is the type of device bridge? + + :param name: name of device + :type name: str + :return: True if type of device is BRIDGE, False otherwise + :rtype: bool + :raise UnknownDeviceError: if device is not found + :raise PropertyNotFoundError: if property is not found + """ + return nm_device_type(name) == NetworkManager.DeviceType.BRIDGE + def nm_device_type_is_vlan(name): """Is the type of device vlan? @@ -483,7 +502,8 @@ def nm_device_ip_config(name, version=4): addr_list = [] for addr, prefix, gateway in addresses: - # TODO - look for a library function (could have used IPy but byte order!) + # NOTE: There is IPy for python2, ipaddress for python3 but + # byte order of dbus value would need to be switched if version == 4: addr_str = nm_dbus_int_to_ipv4(addr) gateway_str = nm_dbus_int_to_ipv4(gateway) @@ -584,6 +604,10 @@ def _device_settings(name): settings = _find_settings(name, 'bond', 'interface-name') elif devtype == NetworkManager.DeviceType.VLAN: settings = _find_settings(name, 'vlan', 'interface-name') + if not settings: + # connections generated by NM from iBFT + _parent, _sep, vlanid = name.partition(".") + settings = _find_settings(int(vlanid), 'vlan', 'id') else: settings = _find_settings(name, 'connection', 'interface-name') if not settings: @@ -796,6 +820,8 @@ def nm_activate_device_connection(dev_name, con_uuid): raise UnmanagedDeviceError(dev_name, e) elif "org.freedesktop.NetworkManager.UnknownConnection" in e.message: raise UnknownConnectionError(dev_name, e) + if "org.freedesktop.NetworkManager.UnknownDevice" in e.message: + raise UnknownDeviceError(dev_name, e) raise def nm_add_connection(values): @@ -823,7 +849,12 @@ def nm_add_connection(values): proxy = _get_proxy(object_path="/org/freedesktop/NetworkManager/Settings", interface_name="org.freedesktop.NetworkManager.Settings") - connection = proxy.AddConnection('(a{sa{sv}})', settings) + try: + connection = proxy.AddConnection('(a{sa{sv}})', settings) + except GLib.GError as e: + if "bond.options: invalid option" in e.message: + raise BondOptionsError(e) + raise return connection def nm_delete_connection(uuid): @@ -973,7 +1004,7 @@ def nm_ipv6_to_dbus_ay(address): :return: address in format 'ay' for NM dbus setting :rtype: list of bytes """ - return [int(byte, 16) for byte in re.findall('.{1,2}', IPy.IP(address).strFullsize().replace(':', ''))] + return [int(byte) for byte in bytearray(socket.inet_pton(socket.AF_INET6, address))] def nm_ipv4_to_dbus_int(address): """Convert ipv4 address from string to int for dbus (switched endianess). diff --git a/anaconda/pyanaconda/ntp.py b/anaconda/pyanaconda/ntp.py index 40263ae..ac710c4 100644 --- a/anaconda/pyanaconda/ntp.py +++ b/anaconda/pyanaconda/ntp.py @@ -23,6 +23,8 @@ Module facilitating the work with NTP servers and NTP daemon's configuration """ +from __future__ import division + import re import os import tempfile @@ -39,7 +41,10 @@ NTP_CONFIG_FILE = "/etc/chrony.conf" #example line: #server 0.fedora.pool.ntp.org iburst -SRV_LINE_REGEXP = re.compile(r"^\s*server\s*([-a-zA-Z.0-9]+)\s*[a-zA-Z]+\s*$") +SRV_LINE_REGEXP = re.compile(r"^\s*(server|pool)\s*([-a-zA-Z.0-9]+)\s*[a-zA-Z]+\s*$") + +#treat pools as four servers with the same name +SERVERS_PER_POOL = 4 class NTPconfigError(Exception): """Exception class for NTP related problems""" @@ -72,6 +77,31 @@ def ntp_server_working(server): return True +def pools_servers_to_internal(pools, servers): + ret = [] + for pool in pools: + ret.extend(SERVERS_PER_POOL * [pool]) + ret.extend(servers) + + return ret + +def internal_to_pools_and_servers(pools_servers): + server_nums = dict() + pools = [] + servers = [] + + for item in pools_servers: + server_nums[item] = server_nums.get(item, 0) + 1 + + for item in server_nums.keys(): + if server_nums[item] >= SERVERS_PER_POOL: + pools.extend((server_nums[item] // SERVERS_PER_POOL) * [item]) + servers.extend((server_nums[item] % SERVERS_PER_POOL) * [item]) + else: + servers.extend(server_nums[item] * [item]) + + return (pools, servers) + def get_servers_from_config(conf_file_path=NTP_CONFIG_FILE, srv_regexp=SRV_LINE_REGEXP): """ @@ -83,29 +113,34 @@ def get_servers_from_config(conf_file_path=NTP_CONFIG_FILE, """ - ret = list() + pools = list() + servers = list() try: with open(conf_file_path, "r") as conf_file: for line in conf_file: match = srv_regexp.match(line) if match: - ret.append(match.group(1)) + if match.group(1) == "pool": + pools.append(match.group(2)) + else: + servers.append(match.group(2)) except IOError as ioerr: msg = "Cannot open config file %s for reading (%s)" % (conf_file_path, ioerr.strerror) raise NTPconfigError(msg) - return ret + return (pools, servers) -def save_servers_to_config(servers, conf_file_path=NTP_CONFIG_FILE, +def save_servers_to_config(pools, servers, conf_file_path=NTP_CONFIG_FILE, srv_regexp=SRV_LINE_REGEXP, out_file_path=None): """ - Replaces the servers defined in the chronyd's configuration file with - the given ones. If the out_file is not None, then it is used for the + Replaces the pools and servers defined in the chronyd's configuration file + with the given ones. If the out_file is not None, then it is used for the resulting config. + :type pools: iterable :type servers: iterable :param out_file_path: path to the file used for the resulting config @@ -141,7 +176,10 @@ def save_servers_to_config(servers, conf_file_path=NTP_CONFIG_FILE, #write info about the origin of the following lines new_conf_file.write(heading) - #write new servers + #write new servers and pools + for pool in pools: + new_conf_file.write("pool " + pool + " iburst\n") + for server in servers: new_conf_file.write("server " + server + " iburst\n") diff --git a/anaconda/pyanaconda/packaging/__init__.py b/anaconda/pyanaconda/packaging/__init__.py index cb78cf1..8cbeb13 100644 --- a/anaconda/pyanaconda/packaging/__init__.py +++ b/anaconda/pyanaconda/packaging/__init__.py @@ -27,16 +27,15 @@ - document all methods """ -from __future__ import print_function -import os, sys +import os from urlgrabber.grabber import URLGrabber from urlgrabber.grabber import URLGrabError import ConfigParser import shutil -import time from glob import glob -import re +from fnmatch import fnmatch import threading +import re if __name__ == "__main__": from pyanaconda import anaconda_log @@ -53,10 +52,10 @@ from pyanaconda import iutil from pyanaconda import isys from pyanaconda.image import findFirstIsoImage from pyanaconda.image import mountImage -from pyanaconda.image import opticalInstallMedia +from pyanaconda.image import opticalInstallMedia, verifyMedia from pyanaconda.iutil import ProxyString, ProxyStringError -from pyanaconda.regexes import VERSION_DIGITS from pyanaconda.threads import threadMgr, AnacondaThread +from pyanaconda.regexes import VERSION_DIGITS from pykickstart.parser import Group @@ -72,8 +71,15 @@ from pyanaconda.product import productName, productVersion import urlgrabber urlgrabber.grabber.default_grabber.opts.user_agent = "%s (anaconda)/%s" %(productName, productVersion) +from distutils.version import LooseVersion + REPO_NOT_SET = False +def versionCmp(v1, v2): + """ Compare two version number strings. """ + + return LooseVersion(v1).__cmp__(LooseVersion(v2)) + ### ### ERROR HANDLING ### @@ -128,9 +134,6 @@ class Payload(object): self.data = data self.storage = None self.instclass = None - self._kernelVersionList = [] - self._rescueVersionList = [] - self._createdInitrds = False self.txID = None def setup(self, storage, instClass): @@ -156,7 +159,7 @@ class Payload(object): """ pass - def reset(self, root=None, releasever=None): + def reset(self): """ Reset the instance, not including ksdata. """ pass @@ -222,18 +225,7 @@ class Payload(object): def needsNetwork(self): return any(self._repoNeedsNetwork(r) for r in self.data.repo.dataList()) - def _resetMethod(self): - self.data.method.method = "" - self.data.method.url = None - self.data.method.server = None - self.data.method.dir = None - self.data.method.partition = None - self.data.method.biospart = None - self.data.method.noverifyssl = False - self.data.method.proxy = "" - self.data.method.opts = None - - def updateBaseRepo(self, fallback=True, root=None, checkmount=True): + def updateBaseRepo(self, fallback=True, checkmount=True): """ Update the base repository from ksdata.method. """ pass @@ -313,60 +305,6 @@ class Payload(object): self.data.packages.excludedGroupList.append(grp) - ### - ### METHODS FOR WORKING WITH PACKAGES - ### - def packageSelected(self, pkgid): - return pkgid in self.data.packages.packageList - - def selectPackage(self, pkgid): - """Mark a package for installation. - - pkgid - The name of a package to be installed. This could include - a version or architecture component. - """ - if pkgid in self.data.packages.packageList: - return - - if pkgid in self.data.packages.excludedList: - self.data.packages.excludedList.remove(pkgid) - - self.data.packages.packageList.append(pkgid) - - def deselectPackage(self, pkgid): - """Mark a package to be excluded from installation. - - pkgid - The name of a package to be excluded. This could include - a version or architecture component. - """ - if pkgid in self.data.packages.excludedList: - return - - if pkgid in self.data.packages.packageList: - self.data.packages.packageList.remove(pkgid) - - self.data.packages.excludedList.append(pkgid) - - def _updateKernelVersionList(self): - try: - import yum - except ImportError: - cmpfunc = cmp - else: - cmpfunc = yum.rpmUtils.miscutils.compareVerOnly - - files = glob(iutil.getSysroot() + "/boot/vmlinuz-*") - files.extend(glob(iutil.getSysroot() + "/boot/efi/EFI/%s/vmlinuz-*" % self.instclass.efi_dir)) - - versions = sorted((f.split("/")[-1][8:] for f in files if os.path.isfile(f)), cmp=cmpfunc) - log.debug("kernel versions: %s", versions) - - # Store regular and rescue kernels separately - self._kernelVersionList = ( - [v for v in versions if "-rescue-" not in v], - [v for v in versions if "-rescue-" in v] - ) - ### ### METHODS FOR QUERYING STATE ### @@ -377,16 +315,8 @@ class Payload(object): @property def kernelVersionList(self): - if not self._kernelVersionList: - self._updateKernelVersionList() - - return self._kernelVersionList[0] - - @property - def rescueKernelList(self): - # do re-scan if looking for rescue kernel - self._updateKernelVersionList() - return self._kernelVersionList[1] + """ An iterable of the kernel versions installed by the payload. """ + raise NotImplementedError() ## ## METHODS FOR TREE VERIFICATION @@ -398,7 +328,7 @@ class Payload(object): :type baseurl: string :param proxy_url: Optional full proxy URL of or "" :type proxy_url: string - :param sslverify: True if SSL certificate should be varified + :param sslverify: True if SSL certificate should be verified :type sslverify: bool :returns: Path to retrieved .treeinfo file or None :rtype: string or None @@ -463,9 +393,6 @@ class Payload(object): except ConfigParser.Error: pass - if version.startswith(time.strftime("%Y")): - version = "rawhide" - log.debug("got a release version of %s", version) return version @@ -583,19 +510,14 @@ class Payload(object): # XXX TODO: real error handling, as this is probably going to # prevent boot on some systems - def recreateInitrds(self, force=False): - """ Recreate the initrds by calling kernel-install + def recreateInitrds(self): + """ Recreate the initrds by calling new-kernel-pkg This needs to be done after all configuration files have been written, since dracut depends on some of them. - :param force: Always recreate, default is to only do it on first call - :type force: bool :returns: None """ - if not force and self._createdInitrds: - return - for kernel in self.kernelVersionList: log.info("recreating initrd for %s", kernel) if not flags.imageInstall: @@ -611,9 +533,6 @@ class Payload(object): "-f", "/boot/initramfs-%s.img" % kernel, kernel]) - self._createdInitrds = True - - def _setDefaultBootTarget(self): """ Set the default systemd target for the system. """ if not os.path.exists(iutil.getSysroot() + "/etc/systemd/system"): @@ -741,6 +660,17 @@ class PackagePayload(Payload): else: self.rpmMacros.append(('__file_context_path', '%{nil}')) + # Add platform specific group + groupid = iutil.get_platform_groupid() + if groupid and groupid in self.groups: + if isinstance(groups, list): + log.info("Adding platform group %s", groupid) + groups.append(groupid) + else: + log.warning("Could not add %s to groups, not a list.", groupid) + elif groupid: + log.warning("Platform group %s not available.", groupid) + @property def kernelPackages(self): if "kernel" in self.data.packages.excludedList: @@ -761,6 +691,28 @@ class PackagePayload(Payload): return kernels + @property + def kernelVersionList(self): + # Find all installed rpms that provide 'kernel' + + # If a PackagePayload is in use, rpm needs to be available + try: + import rpm + except ImportError: + raise PayloadError("failed to import rpm-python, cannot determine kernel versions") + + files = [] + + ts = rpm.TransactionSet(iutil.getSysroot()) + mi = ts.dbMatch('providename', 'kernel') + for hdr in mi: + # Find all /boot/vmlinuz- files and strip off vmlinuz- + files.extend((f.split("/")[-1][8:] for f in hdr.filenames + if fnmatch(f, "/boot/vmlinuz-*") or + fnmatch(f, "/boot/efi/EFI/%s/vmlinuz-*" % self.instclass.efi_dir))) + + return sorted(files, cmp=versionCmp) + @property def rpmMacros(self): """A list of (name, value) pairs to define as macros in the rpm transaction.""" @@ -770,7 +722,11 @@ class PackagePayload(Payload): def rpmMacros(self, value): self._rpm_macros = value - def reset(self, root=None, releasever=None): + def reset(self): + self.reset_install_device() + + def reset_install_device(self): + """ Unmount the previous base repo and reset the install_device """ # cdrom: install_device.teardown (INSTALL_TREE) # hd: umount INSTALL_TREE, install_device.teardown (ISO_DIR) # nfs: umount INSTALL_TREE @@ -865,8 +821,12 @@ class PackagePayload(Payload): needmount = False # We don't setup an install_device here # because we can't tear it down + isodevice = storage.devicetree.resolveDevice(devspec) if needmount: + if not isodevice: + raise PayloadSetupError("device for HDISO install %s does not exist" % devspec) + self._setupMedia(isodevice) url = "file://" + INSTALL_TREE self.install_device = isodevice @@ -893,7 +853,8 @@ class PackagePayload(Payload): needmount = True if device: _options, host, path = iutil.parseNfsUrl('nfs:%s' % device) - if path and path in device: + if method.server and method.server == host and \ + method.dir and method.dir == path: needmount = False path = DRACUT_REPODIR if needmount: @@ -945,6 +906,11 @@ class PackagePayload(Payload): elif method.method == "cdrom" or (checkmount and not method.method): # Did dracut leave the DVD or NFS mounted for us? device = blivet.util.get_mount_device(DRACUT_REPODIR) + + # Check for valid optical media if we didn't boot from one + if not verifyMedia(DRACUT_REPODIR): + self.install_device = opticalInstallMedia(storage.devicetree) + # Only look at the dracut mount if we don't already have a cdrom if device and not self.install_device: self.install_device = storage.devicetree.getDeviceByPath(device) @@ -962,9 +928,6 @@ class PackagePayload(Payload): else: method.method = "cdrom" else: - # cdrom or no method specified -- check for media - if not self.install_device: - self.install_device = opticalInstallMedia(storage.devicetree) if self.install_device: if not method.method: method.method = "cdrom" @@ -1030,6 +993,21 @@ class PackagePayload(Payload): self.requiredPackages.append(package) log.debug("required packages = %s", self.requiredPackages) + @property + def ISOImage(self): + """ The location of a mounted ISO repo, or None. """ + if not self.data.method.method == "harddrive": + return None + # This could either be mounted to INSTALL_TREE or on + # DRACUT_ISODIR if dracut did the mount. + dev = blivet.util.get_mount_device(INSTALL_TREE) + if dev: + return dev[len(ISO_DIR)+1:] + dev = blivet.util.get_mount_device(DRACUT_ISODIR) + if dev: + return dev[len(DRACUT_ISODIR)+1:] + return None + ### ### METHODS FOR WORKING WITH ENVIRONMENTS ### @@ -1047,9 +1025,14 @@ class PackagePayload(Payload): raise NotImplementedError() def selectEnvironment(self, environmentid): - raise NotImplementedError() + if environmentid not in self.environments: + raise NoSuchGroup(environmentid) - def environmentGroups(self, environmentid): + # Select each group within the environment + for groupid in self.environmentGroups(environmentid, optional=False): + self.selectGroup(groupid) + + def environmentGroups(self, environmentid, optional=True): raise NotImplementedError() @property @@ -1285,42 +1268,3 @@ class PayloadManager(object): # Initialize the PayloadManager instance payloadMgr = PayloadManager() - -def show_groups(payload): - #repo = ksdata.RepoData(name="anaconda", baseurl="http://cannonball/install/rawhide/os/") - #obj.addRepo(repo) - - desktops = [] - addons = [] - - for grp in payload.groups: - if grp.endswith("-desktop"): - desktops.append(payload.description(grp)) - elif not grp.endswith("-support"): - addons.append(payload.description(grp)) - - import pprint - - print("==== DESKTOPS ====") - pprint.pprint(desktops) - print("==== ADDONS ====") - pprint.pprint(addons) - - print(payload.groups) - -def print_txmbrs(payload, f=None): - if f is None: - f = sys.stdout - - print("###########", file=f) - for txmbr in payload._yum.tsInfo.getMembers(): - print(txmbr, file=f) - print("###########", file=f) - -def write_txmbrs(payload, filename): - if os.path.exists(filename): - os.unlink(filename) - - f = open(filename, 'w') - print_txmbrs(payload, f) - f.close() diff --git a/anaconda/pyanaconda/packaging/dnfpayload.py b/anaconda/pyanaconda/packaging/dnfpayload.py index 64affc9..7e7d262 100644 --- a/anaconda/pyanaconda/packaging/dnfpayload.py +++ b/anaconda/pyanaconda/packaging/dnfpayload.py @@ -19,8 +19,10 @@ # # Red Hat Author(s): Ales Kozumplik <akozumpl@redhat.com> # +import os from blivet.size import Size +import blivet.arch from pyanaconda.flags import flags from pyanaconda.i18n import _ from pyanaconda.progress import progressQ @@ -31,27 +33,24 @@ import itertools import logging import multiprocessing import operator -import pyanaconda.constants as constants +from pyanaconda import constants from pykickstart.constants import GROUP_ALL, GROUP_DEFAULT, KS_MISSING_IGNORE import pyanaconda.errors as errors import pyanaconda.iutil import pyanaconda.localization import pyanaconda.packaging as packaging +import shutil import sys import time +from pyanaconda.iutil import ProxyString, ProxyStringError log = logging.getLogger("packaging") -try: - import dnf - import dnf.exceptions - import dnf.repo - import dnf.callback - import rpm -except ImportError as e: - log.error("dnfpayload: component import failed: %s", e) - dnf = None - rpm = None +import dnf +import dnf.exceptions +import dnf.repo +import dnf.callback +import rpm DNF_CACHE_DIR = '/tmp/dnf.cache' DNF_PACKAGE_CACHE_DIR_SUFFIX = 'dnf.package.cache' @@ -66,6 +65,7 @@ REPO_DIRS = ['/etc/yum.repos.d', '/etc/anaconda.repos.d', '/tmp/updates/anaconda.repos.d', '/tmp/product/anaconda.repos.d'] +YUM_REPOS_DIR = "/etc/yum.repos.d/" def _failure_limbo(): progressQ.send_quit(1) @@ -110,7 +110,7 @@ def _pick_mpoint(df, requested): if not len(sufficients): return None # default to the biggest one: - return sorted(sufficients.iteritems(), key=operator.itemgetter(1), + return sorted(sufficients.items(), key=operator.itemgetter(1), reverse=True)[0][0] class PayloadRPMDisplay(dnf.callback.LoggingTransactionDisplay): @@ -183,15 +183,34 @@ def do_transaction(base, queue): class DNFPayload(packaging.PackagePayload): def __init__(self, data): packaging.PackagePayload.__init__(self, data) - if rpm is None or dnf is None: - raise packaging.PayloadError("unsupported payload type") self._base = None + self._download_location = None self._configure() def unsetup(self): super(DNFPayload, self).unsetup() self._base = None + self._configure() + + def _replace_vars(self, url): + """ Replace url variables with their values + + :param url: url string to do replacement on + :type url: string + :returns: string with variables substituted + :rtype: string or None + + Currently supports $releasever and $basearch + """ + if not url: + return url + + url = url.replace("$releasever", self._base.conf.releasever) + url = url.replace("$basearch", blivet.arch.getArch()) + + return url + def _add_repo(self, ksrepo): """Add a repo to the dnf repo object @@ -201,15 +220,38 @@ class DNFPayload(packaging.PackagePayload): :returns: None """ repo = dnf.repo.Repo(ksrepo.name, DNF_CACHE_DIR) - url = ksrepo.baseurl - mirrorlist = ksrepo.mirrorlist + url = self._replace_vars(ksrepo.baseurl) + mirrorlist = self._replace_vars(ksrepo.mirrorlist) if url: repo.baseurl = [url] if mirrorlist: repo.mirrorlist = mirrorlist repo.sslverify = not (ksrepo.noverifyssl or flags.noverifyssl) - repo.enable() - self._base.repos.add(repo) + if ksrepo.proxy: + try: + repo.proxy = ProxyString(ksrepo.proxy).url + except ProxyStringError as e: + log.error("Failed to parse proxy for _add_repo %s: %s", + ksrepo.proxy, e) + + # If this repo is already known, it's one of two things: + # (1) The user is trying to do "repo --name=updates" in a kickstart file + # and we should just know to enable the already existing on-disk + # repo config. + # (2) It's a duplicate, and we need to delete the existing definition + # and use this new one. The highest profile user of this is livecd + # kickstarts. + if repo.id in self._base.repos: + if not url and not mirrorlist: + self._base.repos[repo.id].enable() + else: + self._base.repos.pop(repo.id) + self._base.repos.add(repo) + repo.enable() + # If the repo's not already known, we've got to add it. + else: + self._base.repos.add(repo) + repo.enable() # Load the metadata to verify that the repo is valid try: @@ -217,7 +259,7 @@ class DNFPayload(packaging.PackagePayload): except dnf.exceptions.RepoError as e: raise packaging.MetadataError(e) - log.info("added repo: '%s'", ksrepo.name) + log.info("added repo: '%s' - %s", ksrepo.name, url or mirrorlist) def addRepo(self, ksrepo): """Add a repo to dnf and kickstart repo lists @@ -233,34 +275,74 @@ class DNFPayload(packaging.PackagePayload): if self.data.packages.nocore: log.info("skipping core group due to %%packages --nocore; system may not be complete") else: - self._select_group('core') - - for pkg_name in self.data.packages.packageList: - log.info("selecting package: '%s'", pkg_name) try: - self._install_package(pkg_name) - except packaging.NoSuchPackage as e: + self._select_group('core', required=True) + log.info("selected group: core") + except packaging.NoSuchGroup as e: + self._miss(e) + + env = None + + if self.data.packages.default and self.environments: + env = self.environments[0] + elif self.data.packages.environment: + env = self.data.packages.environment + + if env: + try: + self.selectEnvironment(env) + log.info("selected env: %s", env) + except packaging.NoSuchGroup as e: self._miss(e) for group in self.data.packages.groupList: if group.name == 'core': continue + default = group.include in (GROUP_ALL, + GROUP_DEFAULT) + optional = group.include == GROUP_ALL try: - default = group.include in (GROUP_ALL, - GROUP_DEFAULT) - optional = group.include == GROUP_ALL self._select_group(group.name, default=default, optional=optional) + log.info("selected group: %s", group.name) except packaging.NoSuchGroup as e: self._miss(e) - for package in self.requiredPackages: - self._install_package(package, required=True) + for group in self.data.packages.excludedGroupList: + try: + self._deselect_group(group.name) + log.info("deselected group: %s", group.name) + except packaging.NoSuchGroup: + log.info("skipped removing nonexistant group: %s", group.name) - for group in self.requiredGroups: - self._select_group(group, required=True) + for pkg_name in self.data.packages.packageList: + try: + self._install_package(pkg_name) + log.info("selected package: '%s'", pkg_name) + except packaging.NoSuchPackage as e: + self._miss(e) + + for pkg_name in self.data.packages.excludedList: + try: + self._remove_package(pkg_name) + log.info("removed package: %s", pkg_name) + except packaging.NoSuchPackage: + log.info("skipped removing nonexistant package: %s", pkg_name) self._select_kernel_package() - self._install_package('dnf') + + for pkg_name in self.requiredPackages: + try: + self._install_package(pkg_name, required=True) + log.debug("selected required package: %s", pkg_name) + except packaging.NoSuchPackage as e: + self._miss(e) + + for group in self.requiredGroups: + try: + self._select_group(group, required=True) + log.debug("selected required group: %s", group) + except packaging.NoSuchGroup as e: + self._miss(e) def _bump_tx_id(self): if self.txID is None: @@ -287,6 +369,25 @@ class DNFPayload(packaging.PackagePayload): # transaction, disable it in RPM: conf.tsflags.append('nocrypto') + if hasattr(self.data.method, "proxy") and self.data.method.proxy: + try: + proxy = ProxyString(self.data.method.proxy) + conf.proxy = proxy.noauth_url + if proxy.username: + conf.proxy_username = proxy.username + if proxy.password: + conf.proxy_password = proxy.password + log.info("Using %s as proxy", self.data.method.proxy) + except ProxyStringError as e: + log.error("Failed to parse proxy for dnf configure %s: %s", + self.data.method.proxy, e) + + # Start with an empty comps so we can go ahead and use the environment + # and group properties. Unset reposdir to ensure dnf has nothing it can + # check automatically + conf.reposdir = [] + self._base.read_comps() + conf.reposdir = REPO_DIRS @property @@ -304,6 +405,12 @@ class DNFPayload(packaging.PackagePayload): except dnf.exceptions.MarkingError: raise packaging.NoSuchPackage(pkg_name, required=required) + def _remove_package(self, pkg_name): + try: + return self._base.remove(pkg_name) + except dnf.exceptions.PackagesNotInstalledError: + raise packaging.NoSuchPackage(pkg_name) + def _miss(self, exn): if self.data.packages.handleMissing == KS_MISSING_IGNORE: return @@ -315,6 +422,7 @@ class DNFPayload(packaging.PackagePayload): # Doing a sys.exit also ensures the running thread quits before # it can do anything else. progressQ.send_quit(1) + pyanaconda.iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) def _pick_download_location(self): @@ -331,6 +439,8 @@ class DNFPayload(packaging.PackagePayload): for repo in self._base.repos.iter_enabled(): repo.pkgdir = pkgdir + return pkgdir + def _select_group(self, group_id, default=True, optional=False, required=False): grp = self._base.comps.group_by_pattern(group_id) if grp is None: @@ -341,7 +451,21 @@ class DNFPayload(packaging.PackagePayload): if optional: types.add('optional') exclude = self.data.packages.excludedList - self._base.group_install(grp, types, exclude=exclude) + try: + self._base.group_install(grp, types, exclude=exclude) + except dnf.exceptions.CompsError as e: + # DNF raises this when it is already selected + log.debug(e) + + def _deselect_group(self, group_id): + grp = self._base.comps.group_by_pattern(group_id) + if grp is None: + raise packaging.NoSuchGroup(group_id) + try: + self._base.group_remove(grp) + except dnf.exceptions.CompsError as e: + # DNF raises this when it is already not selected + log.debug(e) def _select_kernel_package(self): kernels = self.kernelPackages @@ -361,8 +485,6 @@ class DNFPayload(packaging.PackagePayload): dnf_repo.load() except dnf.exceptions.RepoError as e: id_ = dnf_repo.id - if id_ == self.baseRepo: - raise packaging.MetadataError(str(e)) log.info('_sync_metadata: addon repo error: %s', e) self.disableRepo(id_) @@ -456,13 +578,16 @@ class DNFPayload(packaging.PackagePayload): raise packaging.NoSuchGroup(environmentid) return (env.ui_name, env.ui_description) - def environmentGroups(self, environmentid): + def environmentGroups(self, environmentid, optional=True): env = self._base.comps.environment_by_pattern(environmentid) if env is None: raise packaging.NoSuchGroup(environmentid) group_ids = (id_.name for id_ in env.group_ids) option_ids = (id_.name for id_ in env.option_ids) - return list(itertools.chain(group_ids, option_ids)) + if optional: + return list(itertools.chain(group_ids, option_ids)) + else: + return list(group_ids) def environmentHasOption(self, environmentid, grpid): env = self._base.comps.environment_by_pattern(environmentid) @@ -474,7 +599,10 @@ class DNFPayload(packaging.PackagePayload): env = self._base.comps.environment_by_pattern(environmentid) if env is None: raise packaging.NoSuchGroup(environmentid) - return False + + # Look for a group in the optionlist that matches the group_id and has + # default set + return any(grp for grp in env.option_ids if grp.name == grpid and grp.default) def groupDescription(self, grpid): """ Return name/description tuple for the group specified by id. """ @@ -500,13 +628,13 @@ class DNFPayload(packaging.PackagePayload): self._setupMedia(self.install_device) try: self.checkSoftwareSelection() - self._pick_download_location() + self._download_location = self._pick_download_location() except packaging.PayloadError as e: if errors.errorHandler.cb(e) == errors.ERROR_RAISE: _failure_limbo() pkgs_to_download = self._base.transaction.install_set - log.info('Downloading pacakges.') + log.info('Downloading packages.') progressQ.send_message(_('Downloading packages')) progress = DownloadProgress() try: @@ -540,6 +668,19 @@ class DNFPayload(packaging.PackagePayload): progressQ.send_message(post_msg) process.join() self._base.close() + if os.path.exists(self._download_location): + log.info("Cleaning up downloaded packages: %s", self._download_location) + shutil.rmtree(self._download_location) + else: + # Some installation sources, such as NFS, don't need to download packages to + # local storage, so the download location might not always exist. So for now + # warn about this, at least until the RFE in bug 1193121 is implemented and + # we don't have to care about clearing the download location ourselves. + log.warning("Can't delete nonexistent download location: %s", self._download_location) + + def getRepo(self, repo_id): + """ Return the yum repo object. """ + return self._base.repos[repo_id] def isRepoEnabled(self, repo_id): try: @@ -560,30 +701,41 @@ class DNFPayload(packaging.PackagePayload): return list(gids) def preInstall(self, packages=None, groups=None): - super(DNFPayload, self).preInstall() - self.requiredPackages = packages + super(DNFPayload, self).preInstall(packages, groups) + self.requiredPackages = ["dnf"] + if packages: + self.requiredPackages += packages self.requiredGroups = groups self.addDriverRepos() - def release(self): - pass - - def reset(self, root=None, releasever=None): + def reset(self): super(DNFPayload, self).reset() + shutil.rmtree(DNF_CACHE_DIR, ignore_errors=True) self.txID = None self._base.reset(sack=True, repos=True) - def selectEnvironment(self, environmentid): - env = self._base.comps.environment_by_pattern(environmentid) - map(self.selectGroup, (id_.name for id_ in env.group_ids)) - - def updateBaseRepo(self, fallback=True, root=None, checkmount=True): + def updateBaseRepo(self, fallback=True, checkmount=True): log.info('configuring base repo') self.reset() url, mirrorlist, sslverify = self._setupInstallDevice(self.storage, checkmount) method = self.data.method + # Read in all the repos from the installation environment, make a note of which + # are enabled, and then disable them all. If the user gave us a method, we want + # to use that instead of the default repos. + self._base.read_all_repos() + + enabled = [] + for repo in self._base.repos.iter_enabled(): + enabled.append(repo.id) + repo.disable() + + # If askmethod was specified on the command-line, leave all the repos + # disabled and return + if flags.askmethod: + return + if method.method: try: self._base.conf.releasever = self._getReleaseVersion(url) @@ -593,9 +745,10 @@ class DNFPayload(packaging.PackagePayload): method.method, e) try: + proxy = getattr(method, "proxy", None) base_ksrepo = self.data.RepoData( name=constants.BASE_REPO_NAME, baseurl=url, - mirrorlist=mirrorlist, noverifyssl=not sslverify) + mirrorlist=mirrorlist, noverifyssl=not sslverify, proxy=proxy) self._add_repo(base_ksrepo) except (packaging.MetadataError, packaging.PayloadError) as e: log.error("base repo (%s/%s) not valid -- removing it", @@ -610,11 +763,16 @@ class DNFPayload(packaging.PackagePayload): method.method = None self.install_device = None + # We need to check this again separately in case method.method was unset above. if not method.method: - # only when there's no repo set via method use the repos from the - # install image itself: - log.info('Loading repositories config on the filesystem.') - self._base.read_all_repos() + # If this is a kickstart install, just return now + if flags.automatedInstall: + return + + # Otherwise, fall back to the default repos that we disabled above + for (id_, repo) in self._base.repos.items(): + if id_ in enabled: + repo.enable() for ksrepo in self.data.repo.dataList(): self._add_repo(ksrepo) @@ -627,3 +785,67 @@ class DNFPayload(packaging.PackagePayload): self.disableRepo(id_) elif constants.isFinal and 'rawhide' in id_: self.disableRepo(id_) + + def _writeDNFRepo(self, repo, repo_path): + """ Write a repo object to a DNF repo.conf file + + :param repo: DNF repository object + :param string repo_path: Path to write the repo to + :raises: PayloadSetupError if the repo doesn't have a url + """ + with open(repo_path, "w") as f: + f.write("[%s]\n" % repo.id) + f.write("name=%s\n" % repo.id) + if self.isRepoEnabled(repo.id): + f.write("enabled=1\n") + else: + f.write("enabled=0\n") + + if repo.mirrorlist: + f.write("mirrorlist=%s\n" % repo.mirrorlist) + elif repo.metalink: + f.write("metalink=%s\n" % repo.metalink) + elif repo.baseurl: + f.write("baseurl=%s\n" % repo.baseurl[0]) + else: + f.close() + os.unlink(repo_path) + raise packaging.PayloadSetupError("repo %s has no baseurl, mirrorlist or metalink", repo.id) + + # kickstart repo modifiers + ks_repo = self.getAddOnRepo(repo.id) + if not ks_repo: + return + + if ks_repo.noverifyssl: + f.write("sslverify=0\n") + + if ks_repo.proxy: + try: + proxy = ProxyString(ks_repo.proxy) + f.write("proxy=%s\n" % proxy.url) + except ProxyStringError as e: + log.error("Failed to parse proxy for _writeInstallConfig %s: %s", + ks_repo.proxy, e) + + if ks_repo.cost: + f.write("cost=%d\n" % ks_repo.cost) + + def postInstall(self): + """ Perform post-installation tasks. """ + # Write selected kickstart repos to target system + for ks_repo in (ks for ks in (self.getAddOnRepo(r) for r in self.addOns) if ks.install): + try: + repo = self.getRepo(ks_repo.name) + if not repo: + continue + except (dnf.exceptions.RepoError, KeyError): + continue + repo_path = pyanaconda.iutil.getSysroot() + YUM_REPOS_DIR + "%s.repo" % repo.id + try: + log.info("Writing %s.repo to target system.", repo.id) + self._writeDNFRepo(repo, repo_path) + except packaging.PayloadSetupError as e: + log.error(e) + + super(DNFPayload, self).postInstall() diff --git a/anaconda/pyanaconda/packaging/livepayload.py b/anaconda/pyanaconda/packaging/livepayload.py index f8051ff..4cc9d12 100644 --- a/anaconda/pyanaconda/packaging/livepayload.py +++ b/anaconda/pyanaconda/packaging/livepayload.py @@ -56,6 +56,7 @@ from blivet.size import Size import blivet.util from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.i18n import _ +from pyanaconda.packaging import versionCmp class LiveImagePayload(ImagePayload): """ A LivePayload copies the source image onto the target system. """ @@ -67,6 +68,8 @@ class LiveImagePayload(ImagePayload): self.pct_lock = None self.source_size = 1 + self._kernelVersionList = [] + def setup(self, storage, instClass): super(LiveImagePayload, self).setup(storage, instClass) @@ -80,6 +83,9 @@ class LiveImagePayload(ImagePayload): if rc != 0: raise PayloadInstallError("Failed to mount the install tree") + # Grab the kernel version list now so it's available after umount + self._updateKernelVersionList() + source = iutil.eintr_retry_call(os.statvfs, INSTALL_TREE) self.source_size = source.f_frsize * (source.f_blocks - source.f_bfree) @@ -180,6 +186,17 @@ class LiveImagePayload(ImagePayload): def spaceRequired(self): return Size(iutil.getDirSize("/")*1024) + def _updateKernelVersionList(self): + files = glob.glob(INSTALL_TREE + "/boot/vmlinuz-*") + files.extend(glob.glob(INSTALL_TREE + "/boot/efi/EFI/%s/vmlinuz-*" % self.instclass.efi_dir)) + + self._kernelVersionList = sorted((f.split("/")[-1][8:] for f in files + if os.path.isfile(f) and "-rescue-" not in f), cmp=versionCmp) + + @property + def kernelVersionList(self): + return self._kernelVersionList + class URLGrabberProgress(object): """ Provide methods for urlgrabber progress.""" def start(self, filename, url, basename, size, text): @@ -264,7 +281,7 @@ class LiveImageKSPayload(LiveImagePayload): # 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 + self._min_size = int(req.info().get('content-length')) * 4 except IOError as e: log.error("Error opening liveimg: %s", e) error = e @@ -304,6 +321,10 @@ class LiveImageKSPayload(LiveImagePayload): log.debug("liveimg size is %s", self._min_size) + def unsetup(self): + # Skip LiveImagePayload's unsetup method + ImagePayload.unsetup(self) + def _preInstall_url_image(self): """ Download the image using urlgrabber """ # Setup urlgrabber and call back to download image to sysroot @@ -326,10 +347,6 @@ class LiveImageKSPayload(LiveImagePayload): error = "Failed to download %s, file doesn't exist" % self.data.method.url log.error(error) - def unsetup(self): - # Skip LiveImagePayload's unsetup method - ImagePayload.unsetup(self) - def preInstall(self, *args, **kwargs): """ Get image and loopback mount it. @@ -373,29 +390,53 @@ class LiveImageKSPayload(LiveImagePayload): raise exn # If this looks like a tarfile, skip trying to mount it - if not self.is_tarfile: - # 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]) + if self.is_tarfile: + return - # move the mount to IMAGE_DIR - os.makedirs(IMAGE_DIR, 0o755) - # 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") + # 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 + rc = blivet.util.mount(self.image_path, INSTALL_TREE, fstype="auto", options="ro") + if rc != 0: + log.error("mount error (%s) with %s", rc, self.image_path) + exn = PayloadInstallError("mount error %s" % rc) + if errorHandler.cb(exn) == ERROR_RAISE: + raise exn - source = iutil.eintr_retry_call(os.statvfs, INSTALL_TREE) - self.source_size = source.f_frsize * (source.f_blocks - source.f_bfree) + # Nothing more to mount + if not os.path.exists(INSTALL_TREE+"/LiveOS"): + self._updateKernelVersionList() + return + + # Mount the first .img in the directory on INSTALL_TREE + img_files = glob.glob(INSTALL_TREE+"/LiveOS/*.img") + if img_files: + # move the mount to IMAGE_DIR + os.makedirs(IMAGE_DIR, 0o755) + # work around inability to move shared filesystems + rc = iutil.execWithRedirect("mount", + ["--make-rprivate", "/"]) + if rc == 0: + rc = iutil.execWithRedirect("mount", + ["--move", INSTALL_TREE, IMAGE_DIR]) + if rc != 0: + log.error("error %s moving mount", rc) + exn = PayloadInstallError("mount error %s" % rc) + if errorHandler.cb(exn) == ERROR_RAISE: + raise exn + + img_file = IMAGE_DIR+"/LiveOS/"+os.path.basename(sorted(img_files)[0]) + rc = blivet.util.mount(img_file, INSTALL_TREE, fstype="auto", options="ro") + if rc != 0: + log.error("mount error (%s) with %s", rc, img_file) + exn = PayloadInstallError("mount error %s with %s" % (rc, img_file)) + if errorHandler.cb(exn) == ERROR_RAISE: + raise exn + + self._updateKernelVersionList() + + source = iutil.eintr_retry_call(os.statvfs, INSTALL_TREE) + self.source_size = source.f_frsize * (source.f_blocks - source.f_bfree) def install(self): """ Install the payload if it is a tar. @@ -442,6 +483,12 @@ class LiveImageKSPayload(LiveImagePayload): self.pct = 100 threadMgr.wait(THREAD_LIVE_PROGRESS) + # Live needs to create the rescue image before bootloader is written + for kernel in self.kernelVersionList: + log.info("Generating rescue image for %s", kernel) + iutil.execInSysroot("new-kernel-pkg", + ["--rpmposttrans", kernel]) + def postInstall(self): """ Unmount and remove image @@ -466,3 +513,17 @@ class LiveImageKSPayload(LiveImagePayload): return Size(self._min_size) else: return Size(1024*1024*1024) + + @property + def kernelVersionList(self): + # If it doesn't look like a tarfile use the super's kernelVersionList + if not self.is_tarfile: + return super(LiveImageKSPayload, self).kernelVersionList + + import tarfile + with tarfile.open(self.image_path) as archive: + names = archive.getnames() + + # Strip out vmlinuz- from the names + return sorted((n.split("/")[-1][8:] for n in names if "boot/vmlinuz-" in n), + cmp=versionCmp) diff --git a/anaconda/pyanaconda/packaging/rpmostreepayload.py b/anaconda/pyanaconda/packaging/rpmostreepayload.py index 1dca94a..0df7dc5 100644 --- a/anaconda/pyanaconda/packaging/rpmostreepayload.py +++ b/anaconda/pyanaconda/packaging/rpmostreepayload.py @@ -21,9 +21,11 @@ # import os -import shutil +import sys +from pyanaconda import constants from pyanaconda import iutil +from pyanaconda.flags import flags from pyanaconda.i18n import _ from pyanaconda.progress import progressQ from gi.repository import GLib @@ -41,6 +43,8 @@ class RPMOSTreePayload(ArchivePayload): """ A RPMOSTreePayload deploys a tree (possibly with layered packages) onto the target system. """ def __init__(self, data): super(RPMOSTreePayload, self).__init__(data) + self._remoteOptions = None + self._sysroot_path = None @property def handlesBootloaderConfiguration(self): @@ -84,7 +88,45 @@ class RPMOSTreePayload(ArchivePayload): else: progressQ.send_message("Writing objects") + def _copyBootloaderData(self): + # Copy bootloader data files from the deployment + # checkout to the target root. See + # https://bugzilla.gnome.org/show_bug.cgi?id=726757 This + # happens once, at installation time. + # extlinux ships its modules directly in the RPM in /boot. + # For GRUB2, Anaconda installs device.map there. We may need + # to add other bootloaders here though (if they can't easily + # be fixed to *copy* data into /boot at install time, instead + # of shipping it in the RPM). + physboot = iutil.getTargetPhysicalRoot() + '/boot' + ostree_boot_source = iutil.getSysroot() + '/usr/lib/ostree-boot' + if not os.path.isdir(ostree_boot_source): + ostree_boot_source = iutil.getSysroot() + '/boot' + for fname in os.listdir(ostree_boot_source): + srcpath = os.path.join(ostree_boot_source, fname) + destpath = os.path.join(physboot, fname) + + # We're only copying directories + if not os.path.isdir(srcpath): + continue + + # Special handling for EFI, as it's a mount point that's + # expected to already exist (so if we used copytree, we'd + # traceback). If it doesn't, we're not on a UEFI system, + # so we don't want to copy the data. + if fname == 'efi' and os.path.isdir(destpath): + for subname in os.listdir(srcpath): + sub_srcpath = os.path.join(srcpath, subname) + sub_destpath = os.path.join(destpath, subname) + self._safeExecWithRedirect('cp', ['-r', '-p', sub_srcpath, sub_destpath]) + else: + log.info("Copying bootloader data: " + fname) + self._safeExecWithRedirect('cp', ['-r', '-p', srcpath, destpath]) + def install(self): + mainctx = GLib.MainContext.new() + mainctx.push_thread_default() + cancellable = None from gi.repository import OSTree ostreesetup = self.data.ostreesetup @@ -95,32 +137,55 @@ class RPMOSTreePayload(ArchivePayload): ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), "init-fs", iutil.getTargetPhysicalRoot()]) - repo_arg = "--repo=" + iutil.getTargetPhysicalRoot() + '/ostree/repo' + self._sysroot_path = Gio.File.new_for_path(iutil.getTargetPhysicalRoot()) - # Set up the chosen remote - remote_args = [repo_arg, "remote", "add"] + sysroot = OSTree.Sysroot.new(self._sysroot_path) + sysroot.load(cancellable) + repo = sysroot.get_repo(None)[1] + # We don't support resuming from interrupted installs + repo.set_disable_fsync(True) + + self._remoteOptions = {} + + # Handle variations in pykickstart if ((hasattr(ostreesetup, 'noGpg') and ostreesetup.noGpg) or (hasattr(ostreesetup, 'nogpg') and ostreesetup.nogpg)): - remote_args.append("--set=gpg-verify=false") - remote_args.extend([ostreesetup.remote, - ostreesetup.url]) - self._safeExecWithRedirect("ostree", remote_args) + self._remoteOptions['gpg-verify'] = GLib.Variant('b', False) - sysroot_path = Gio.File.new_for_path(iutil.getTargetPhysicalRoot()) - sysroot = OSTree.Sysroot.new(sysroot_path) - sysroot.load(cancellable) + if flags.noverifyssl: + self._remoteOptions['tls-permissive'] = GLib.Variant('b', True) + + repo.remote_change(None, OSTree.RepoRemoteChange.ADD_IF_NOT_EXISTS, + ostreesetup.remote, ostreesetup.url, + GLib.Variant('a{sv}', self._remoteOptions), + cancellable) - repo = sysroot.get_repo(None)[1] - repo.set_disable_fsync(True) progressQ.send_message(_("Starting pull of %(branchName)s from %(source)s") % \ {"branchName": ostreesetup.ref, "source": ostreesetup.remote}) progress = OSTree.AsyncProgress.new() progress.connect('changed', self._pullProgressCb) - repo.pull(ostreesetup.remote, [ostreesetup.ref], 0, progress, cancellable) + + try: + repo.pull(ostreesetup.remote, [ostreesetup.ref], 0, progress, cancellable) + except GLib.GError as e: + exn = PayloadInstallError("Failed to pull from repository: %s" % e) + log.error(str(exn)) + if errors.errorHandler.cb(exn) == errors.ERROR_RAISE: + progressQ.send_quit(1) + iutil.ipmi_report(constants.IPMI_ABORTED) + sys.exit(1) progressQ.send_message(_("Preparing deployment of %s") % (ostreesetup.ref, )) + # Now that we have the data pulled, delete the remote for now. + # This will allow a remote configuration defined in the tree + # (if any) to override what's in the kickstart. Otherwise, + # we'll re-add it in post. Ideally, ostree would support a + # pull without adding a remote, but that would get quite + # complex. + repo.remote_delete(self.data.ostreesetup.remote, None) + self._safeExecWithRedirect("ostree", ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), "os-init", ostreesetup.osname]) @@ -144,22 +209,17 @@ class RPMOSTreePayload(ArchivePayload): deployment_path = sysroot.get_deployment_directory(deployment) iutil.setSysroot(deployment_path.get_path()) - # Copy specific bootloader data files from the deployment - # checkout to the target root. See - # https://bugzilla.gnome.org/show_bug.cgi?id=726757 This - # happens once, at installation time. - # extlinux ships its modules directly in the RPM in /boot. - # For GRUB2, Anaconda installs device.map there. We may need - # to add other bootloaders here though (if they can't easily - # be fixed to *copy* data into /boot at install time, instead - # of shipping it in the RPM). - physboot = iutil.getTargetPhysicalRoot() + '/boot' - sysboot = iutil.getSysroot() + '/boot' - for fname in ['extlinux', 'grub2']: - srcpath = os.path.join(sysboot, fname) - if os.path.isdir(srcpath): - log.info("Copying bootloader data: " + fname) - shutil.copytree(srcpath, os.path.join(physboot, fname)) + try: + self._copyBootloaderData() + except (OSError, RuntimeError) as e: + exn = PayloadInstallError("Failed to copy bootloader data: %s" % e) + log.error(str(exn)) + if errors.errorHandler.cb(exn) == errors.ERROR_RAISE: + progressQ.send_quit(1) + iutil.ipmi_report(constants.IPMI_ABORTED) + sys.exit(1) + + mainctx.pop_thread_default() def prepareMountTargets(self, storage): ostreesetup = self.data.ostreesetup @@ -194,7 +254,7 @@ class RPMOSTreePayload(ArchivePayload): ["--create", "--boot", "--root=" + iutil.getSysroot(), "--prefix=/var/" + varsubdir]) - def recreateInitrds(self, force=False): + def recreateInitrds(self): # For rpmostree payloads, we're replicating an initramfs from # a compose server, and should never be regenerating them # per-machine. @@ -203,32 +263,41 @@ class RPMOSTreePayload(ArchivePayload): def postInstall(self): super(RPMOSTreePayload, self).postInstall() - physboot = iutil.getTargetPhysicalRoot() + '/boot' + from gi.repository import OSTree + cancellable = None - # If we're using extlinux, rename extlinux.conf to - # syslinux.cfg, since that's what OSTree knows about. - # syslinux upstream supports both, but I'd say that upstream - # using syslinux.cfg is somewhat preferred. - physboot_extlinux = physboot + '/extlinux' - if os.path.isdir(physboot_extlinux): - physboot_syslinux = physboot + '/syslinux' - physboot_loader = physboot + '/loader' - assert os.path.isdir(physboot_loader) - orig_extlinux_conf = physboot_extlinux + '/extlinux.conf' - target_syslinux_cfg = physboot_loader + '/syslinux.cfg' - log.info("Moving %s -> %s", orig_extlinux_conf, target_syslinux_cfg) - os.rename(orig_extlinux_conf, target_syslinux_cfg) - # A compatibility bit for OSTree - os.mkdir(physboot_syslinux) - os.symlink('../loader/syslinux.cfg', physboot_syslinux + '/syslinux.cfg') - # And *also* tell syslinux that the config is really in /boot/loader - os.symlink('loader/syslinux.cfg', physboot + '/syslinux.cfg') + # Following up on the "remote delete" above, we removed the + # remote from /ostree/repo/config. But we want it in /etc, so + # re-add it to /etc/ostree/remotes.d, using the sysroot path. + # + # However, we ignore the case where the remote already exists, + # which occurs when the content itself provides the remote + # config file. + sysroot = OSTree.Sysroot.new(self._sysroot_path) + sysroot.load(cancellable) + repo = sysroot.get_repo(None)[1] + repo.remote_change(Gio.File.new_for_path(iutil.getSysroot()), + OSTree.RepoRemoteChange.ADD_IF_NOT_EXISTS, + self.data.ostreesetup.remote, self.data.ostreesetup.url, + GLib.Variant('a{sv}', self._remoteOptions), + cancellable) + + boot = iutil.getSysroot() + '/boot' + + # If we're using GRUB2, move its config file, also with a + # compatibility symlink. + boot_grub2_cfg = boot + '/grub2/grub.cfg' + if os.path.isfile(boot_grub2_cfg): + boot_loader = boot + '/loader' + target_grub_cfg = boot_loader + '/grub.cfg' + log.info("Moving %s -> %s", boot_grub2_cfg, target_grub_cfg) + os.rename(boot_grub2_cfg, target_grub_cfg) + os.symlink('../loader/grub.cfg', boot_grub2_cfg) # OSTree owns the bootloader configuration, so here we give it # the argument list we computed from storage, architecture and # such. - set_kargs_args = ["admin", "--sysroot=" + iutil.getTargetPhysicalRoot(), - "instutil", "set-kargs"] + set_kargs_args = ["admin", "instutil", "set-kargs"] set_kargs_args.extend(self.storage.bootloader.boot_args) set_kargs_args.append("root=" + self.storage.rootDevice.fstabSpec) - self._safeExecWithRedirect("ostree", set_kargs_args) + self._safeExecWithRedirect("ostree", set_kargs_args, root=iutil.getSysroot()) diff --git a/anaconda/pyanaconda/packaging/tarpayload.py b/anaconda/pyanaconda/packaging/tarpayload.py index b6328e0..805515d 100644 --- a/anaconda/pyanaconda/packaging/tarpayload.py +++ b/anaconda/pyanaconda/packaging/tarpayload.py @@ -36,7 +36,7 @@ except ImportError: log.error("import of tarfile failed") tarfile = None -from pyanaconda.packaging import ArchivePayload, PayloadError +from pyanaconda.packaging import ArchivePayload, PayloadError, versionCmp from pyanaconda import iutil # TarPayload is not yet fully implemented @@ -73,8 +73,10 @@ class TarPayload(ArchivePayload): @property def kernelVersionList(self): names = self.archive.getnames() - kernels = [n for n in names if "boot/vmlinuz-" in n] - return kernels + + # Strip out vmlinuz- from the names + return sorted((n.split("/")[-1][8:] for n in names if "boot/vmlinuz-" in n), + cmp=versionCmp) def install(self): try: diff --git a/anaconda/pyanaconda/packaging/yumpayload.py b/anaconda/pyanaconda/packaging/yumpayload.py index 36e314f..aa9019f 100644 --- a/anaconda/pyanaconda/packaging/yumpayload.py +++ b/anaconda/pyanaconda/packaging/yumpayload.py @@ -58,12 +58,12 @@ try: # handler setup. We already set one up so we don't need it to run. # yum may give us an API to fiddle this at a later time. yum.logginglevels._added_handlers = True + from yum.Errors import RepoError, RepoMDError, GroupsError except ImportError: log.error("import of yum failed") yum = None -from pyanaconda.constants import BASE_REPO_NAME, DRACUT_ISODIR, INSTALL_TREE, ISO_DIR, MOUNT_DIR, \ - LOGLVL_LOCK +from pyanaconda.constants import BASE_REPO_NAME, MOUNT_DIR, LOGLVL_LOCK, IPMI_ABORTED from pyanaconda.flags import flags from pyanaconda import iutil @@ -87,6 +87,7 @@ from pykickstart.constants import GROUP_ALL, GROUP_DEFAULT, KS_MISSING_IGNORE YUM_PLUGINS = ["fastestmirror", "langpacks"] BASE_REPO_NAMES = [BASE_REPO_NAME] + PackagePayload.DEFAULT_REPOS +YUM_REPOS_DIR = "/etc/yum.repos.d/" import inspect import threading @@ -172,13 +173,16 @@ class YumPayload(PackagePayload): self._yum = None self._setup = False + self._groups = None + self._packages = [] + # base repo caching self._base_repo = None self._base_repo_lock = threading.RLock() self.reset() - def reset(self, root=None, releasever=None): + def reset(self): """ Reset this instance to its initial (unconfigured) state. """ super(YumPayload, self).reset() @@ -187,10 +191,7 @@ class YumPayload(PackagePayload): # available we can use that as a better value. self._space_required = Size("3000 MB") - self._groups = None - self._packages = [] - - self._resetYum(root=root, releasever=releasever) + self._resetYum() def setup(self, storage, instClass): super(YumPayload, self).setup(storage, instClass) @@ -209,6 +210,9 @@ class YumPayload(PackagePayload): NOTE: This is enforced by tests/pylint/preconf.py. If the name of this method changes, change it there too. """ + self._groups = None + self._packages = [] + if root is None: root = self._root_dir @@ -229,9 +233,8 @@ class YumPayload(PackagePayload): # Set some configuration parameters that don't get set through a config # file. yum will know what to do with these. - # Enable all types of yum plugins. We're somewhat careful about what - # plugins we put in the environment. - self._yum.preconf.plugin_types = yum.plugins.ALL_TYPES + # Only enable non-interactive yum plugins + self._yum.preconf.plugin_types = yum.plugins.TYPE_CORE self._yum.preconf.enabled_plugins = YUM_PLUGINS self._yum.preconf.fn = "/tmp/anaconda-yum.conf" self._yum.preconf.root = root @@ -306,6 +309,71 @@ reposdir=%s root = self._yum.conf.installroot self._yum.conf.cachedir = self._yum.conf.cachedir[len(root):] + def _writeYumRepo(self, repo, repo_path): + """ Write a repo object to a yum repo.conf file + + :param repo: Yum repository object + :param string repo_path: Path to write the repo to + :raises: PayloadSetupError if the repo doesn't have a url + """ + with open(repo_path, "w") as f: + f.write("[%s]\n" % repo.id) + f.write("name=%s\n" % repo.id) + if self.isRepoEnabled(repo.id): + f.write("enabled=1\n") + else: + f.write("enabled=0\n") + + if repo.mirrorlist: + f.write("mirrorlist=%s\n" % repo.mirrorlist) + elif repo.metalink: + f.write("metalink=%s\n" % repo.metalink) + elif repo.baseurl: + f.write("baseurl=%s\n" % repo.baseurl[0]) + else: + f.close() + os.unlink(repo_path) + raise PayloadSetupError("repo %s has no baseurl, mirrorlist or metalink", repo.id) + + # kickstart repo modifiers + ks_repo = self.getAddOnRepo(repo.id) + + if not ks_repo and not repo.sslverify: + f.write("sslverify=0\n") + + if not ks_repo: + return + + if ks_repo.noverifyssl: + f.write("sslverify=0\n") + + if ks_repo.proxy: + try: + proxy = ProxyString(ks_repo.proxy) + f.write("proxy=%s\n" % (proxy.noauth_url,)) + if proxy.username: + f.write("proxy_username=%s\n" % (proxy.username,)) + if proxy.password: + f.write("proxy_password=%s\n" % (proxy.password,)) + except ProxyStringError as e: + log.error("Failed to parse proxy for _writeInstallConfig %s: %s", + ks_repo.proxy, e) + + if ks_repo.cost: + f.write("cost=%d\n" % ks_repo.cost) + + if ks_repo.includepkgs: + f.write("includepkgs=%s\n" + % ",".join(ks_repo.includepkgs)) + + if ks_repo.excludepkgs: + f.write("exclude=%s\n" + % ",".join(ks_repo.excludepkgs)) + + if ks_repo.ignoregroups: + f.write("enablegroups=0\n") + + def _writeInstallConfig(self): """ Write out the yum config that will be used to install packages. @@ -322,53 +390,10 @@ reposdir=%s for repo in self._yum.repos.listEnabled(): cfg_path = "%s/%s.repo" % (self._repos_dir, repo.id) log.debug("writing repository file %s for repository %s", cfg_path, repo.id) - ks_repo = self.getAddOnRepo(repo.id) - with open(cfg_path, "w") as f: - f.write("[%s]\n" % repo.id) - f.write("name=Install - %s\n" % repo.id) - f.write("enabled=1\n") - if repo.mirrorlist: - f.write("mirrorlist=%s" % repo.mirrorlist) - elif repo.metalink: - f.write("metalink=%s" % repo.metalink) - elif repo.baseurl: - f.write("baseurl=%s\n" % repo.baseurl[0]) - else: - log.error("repo %s has no baseurl, mirrorlist or metalink", repo.id) - f.close() - os.unlink(cfg_path) - continue - - # kickstart repo modifiers - if ks_repo: - if ks_repo.noverifyssl: - f.write("sslverify=0\n") - - if ks_repo.proxy: - try: - proxy = ProxyString(ks_repo.proxy) - f.write("proxy=%s\n" % (proxy.noauth_url,)) - if proxy.username: - f.write("proxy_username=%s\n" % (proxy.username,)) - if proxy.password: - f.write("proxy_password=%s\n" % (proxy.password,)) - except ProxyStringError as e: - log.error("Failed to parse proxy for _writeInstallConfig %s: %s", - ks_repo.proxy, e) - - if ks_repo.cost: - f.write("cost=%d\n" % ks_repo.cost) - - if ks_repo.includepkgs: - f.write("includepkgs=%s\n" - % ",".join(ks_repo.includepkgs)) - - if ks_repo.excludepkgs: - f.write("exclude=%s\n" - % ",".join(ks_repo.excludepkgs)) - - if ks_repo.ignoregroups: - f.write("enablegroups=0\n") + try: + self._writeYumRepo(repo, cfg_path) + except PayloadSetupError as e: + log.error(e) releasever = self._yum.conf.yumvar['releasever'] self._writeYumConfig() @@ -468,15 +493,13 @@ reposdir=%s def isRepoEnabled(self, repo_id): """ Return True if repo is enabled. """ - from yum.Errors import RepoError - try: return self.getRepo(repo_id).enabled except RepoError: return super(YumPayload, self).isRepoEnabled(repo_id) @refresh_base_repo() - def updateBaseRepo(self, fallback=True, root=None, checkmount=True): + def updateBaseRepo(self, fallback=True, checkmount=True): """ Update the base repo based on self.data.method. - Tear down any previous base repo devices, symlinks, &c. @@ -489,7 +512,12 @@ reposdir=%s retrieval fails. """ log.info("configuring base repo") - url, mirrorlist, sslverify = self._setupInstallDevice(self.storage, checkmount) + + self.reset_install_device() + try: + url, mirrorlist, sslverify = self._setupInstallDevice(self.storage, checkmount) + except PayloadSetupError: + self.data.method.method = None releasever = None method = self.data.method @@ -502,12 +530,9 @@ reposdir=%s method.method, e) # start with a fresh YumBase instance & tear down old install device - self.reset(root=root, releasever=releasever) + self._resetYum(releasever=releasever) self._yumCacheDirHack() - # This needs to be done again, reset tore it down. - url, mirrorlist, sslverify = self._setupInstallDevice(self.storage, checkmount) - # If this is a kickstart install and no method has been set up, or # askmethod was given on the command line, we don't want to do # anything. Just disable all repos and return. This should avoid @@ -625,27 +650,13 @@ reposdir=%s log.info("metadata retrieval complete") - @property - def ISOImage(self): - if not self.data.method.method == "harddrive": - return None - # This could either be mounted to INSTALL_TREE or on - # DRACUT_ISODIR if dracut did the mount. - dev = blivet.util.get_mount_device(INSTALL_TREE) - if dev: - return dev[len(ISO_DIR)+1:] - dev = blivet.util.get_mount_device(DRACUT_ISODIR) - if dev: - return dev[len(DRACUT_ISODIR)+1:] - return None - @refresh_base_repo() def _configureAddOnRepo(self, repo): """ Configure a single ksdata repo. """ url = repo.baseurl if url and url.startswith("nfs://"): # Let the assignment throw ValueError for bad NFS urls from kickstart - (server, path) = url.strip("nfs://").split(":", 1) + (server, path) = url[6:].split(":", 1) mountpoint = "%s/%s.nfs" % (MOUNT_DIR, repo.name) self._setupNFS(mountpoint, server, path, None) @@ -735,8 +746,6 @@ reposdir=%s def _getRepoMetadata(self, yumrepo): """ Retrieve repo metadata if we don't already have it. """ - from yum.Errors import RepoError, RepoMDError - # And try to grab its metadata. We do this here so it can be done # on a per-repo basis, so we can then get some finer grained error # handling and recovery. @@ -778,8 +787,6 @@ reposdir=%s def _addYumRepo(self, name, baseurl, mirrorlist=None, proxyurl=None, **kwargs): """ Add a yum repo to the YumBase instance. """ - from yum.Errors import RepoError - needsAdding = True # First, delete any pre-existing repo with the same name. @@ -966,35 +973,7 @@ reposdir=%s return (environment.ui_name, environment.ui_description) - def selectEnvironment(self, environmentid): - groups = self._yumGroups - if not groups: - return - - with _yum_lock: - if not groups.has_environment(environmentid): - raise NoSuchGroup(environmentid) - - environment = groups.return_environment(environmentid) - for group in environment.groups: - self.selectGroup(group) - - def deselectEnvironment(self, environmentid): - groups = self._yumGroups - if not groups: - return - - with _yum_lock: - if not groups.has_environment(environmentid): - raise NoSuchGroup(environmentid) - - environment = groups.return_environment(environmentid) - for group in environment.groups: - self.deselectGroup(group) - for group in environment.options: - self.deselectGroup(group) - - def environmentGroups(self, environmentid): + def environmentGroups(self, environmentid, optional=True): groups = self._yumGroups if not groups: return [] @@ -1004,7 +983,10 @@ reposdir=%s raise NoSuchGroup(environmentid) environment = groups.return_environment(environmentid) - return environment.groups + environment.options + if optional: + return environment.groups + environment.options + else: + return environment.groups ### ### METHODS FOR WORKING WITH GROUPS @@ -1015,7 +997,6 @@ reposdir=%s if not self._setup: return [] - from yum.Errors import RepoError, GroupsError with _yum_lock: if not self._groups: if not self.needsNetwork or nm_is_connected(): @@ -1125,8 +1106,6 @@ reposdir=%s ### @property def packages(self): - from yum.Errors import RepoError - with _yum_lock: if not self._packages: if self.needsNetwork and not nm_is_connected(): @@ -1348,10 +1327,12 @@ reposdir=%s def preInstall(self, packages=None, groups=None): """ Perform pre-installation tasks. """ - super(YumPayload, self).preInstall() + super(YumPayload, self).preInstall(packages, groups) progressQ.send_message(_("Starting package installation process")) - self.requiredPackages = packages + self.requiredPackages = ["yum"] + if packages: + self.requiredPackages += packages self.requiredGroups = groups self.addDriverRepos() @@ -1417,6 +1398,7 @@ reposdir=%s log.info("Running anaconda-yum to install packages") # Watch output for progress, debug and error information install_errors = [] + prev = 0 try: for line in execReadlines("/usr/libexec/anaconda/anaconda-yum", args): if line.startswith("PROGRESS_"): @@ -1424,6 +1406,12 @@ reposdir=%s msg = progress_map[key] + text progressQ.send_message(msg) log.debug(msg) + elif line.startswith("PERCENT:"): + _key, pct = line.split(":", 1) + + if float(pct) // 10 > prev // 10: + progressQ.send_step() + prev = float(pct) elif line.startswith("DEBUG:"): log.debug(line[6:]) elif line.startswith("INFO:"): @@ -1440,6 +1428,7 @@ reposdir=%s exn = PayloadInstallError(str(e)) if errorHandler.cb(exn) == ERROR_RAISE: progressQ.send_quit(1) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) finally: # log the contents of the scriptlet logfile if any @@ -1455,6 +1444,7 @@ reposdir=%s exn = PayloadInstallError("\n".join(install_errors)) if errorHandler.cb(exn) == ERROR_RAISE: progressQ.send_quit(1) + iutil.ipmi_report(IPMI_ABORTED) sys.exit(1) def writeMultiLibConfig(self): @@ -1506,6 +1496,21 @@ reposdir=%s self.writeMultiLibConfig() self._copyLangpacksConfigToTarget() + # Write selected kickstart repos to target system + for ks_repo in (ks for ks in (self.getAddOnRepo(r) for r in self.addOns) if ks.install): + try: + repo = self.getRepo(ks_repo.name) + if not repo: + continue + except RepoError: + continue + repo_path = iutil.getSysroot() + YUM_REPOS_DIR + "%s.repo" % repo.id + try: + log.info("Writing %s.repo to target system.", repo.id) + self._writeYumRepo(repo, repo_path) + except PayloadSetupError as e: + log.error(e) + super(YumPayload, self).postInstall() # Make sure yum is really done and gone and lets go of the yum.log diff --git a/anaconda/pyanaconda/pwpolicy.py b/anaconda/pyanaconda/pwpolicy.py new file mode 100644 index 0000000..c061e1a --- /dev/null +++ b/anaconda/pyanaconda/pwpolicy.py @@ -0,0 +1,140 @@ +# +# Brian C. Lane <bcl@redhat.com> +# +# Copyright 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2. This program is distributed in the hope that it +# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the +# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat +# trademarks that are incorporated in the source code or documentation are not +# subject to the GNU General Public License and may only be used or replicated +# with the express permission of Red Hat, Inc. +# +from pykickstart.base import BaseData, KickstartCommand +from pykickstart.errors import KickstartValueError, formatErrorMsg +from pykickstart.options import KSOptionParser + +import warnings +from pyanaconda.i18n import _ + +class F22_PwPolicyData(BaseData): + """ Kickstart Data object to hold information about pwpolicy. """ + removedKeywords = BaseData.removedKeywords + removedAttrs = BaseData.removedAttrs + + def __init__(self, *args, **kwargs): + BaseData.__init__(self, *args, **kwargs) + self.name = kwargs.get("name", "") + self.minlen = kwargs.get("minlen", 8) + self.minquality = kwargs.get("minquality", 50) + self.strict = kwargs.get("strict", True) + self.changesok = kwargs.get("changesok", False) + self.emptyok = kwargs.get("emptyok", True) + + def __eq__(self, y): + if not y: + return False + + return self.name == y.name + + def __ne__(self, y): + return not self == y + + def __str__(self): + retval = BaseData.__str__(self) + + if self.name != "": + retval += "pwpolicy" + retval += self._getArgsAsStr() + "\n" + + return retval + + def _getArgsAsStr(self): + retval = "" + + retval += " %s" % self.name + retval += " --minlen=%d" % self.minlen + retval += " --minquality=%d" % self.minquality + + if self.strict: + retval += " --strict" + else: + retval += " --notstrict" + if self.changesok: + retval += " --changesok" + else: + retval += " --nochanges" + if self.emptyok: + retval += " --emptyok" + else: + retval += " --notempty" + + return retval + +class F22_PwPolicy(KickstartCommand): + """ Kickstart command implementing password policy. """ + removedKeywords = KickstartCommand.removedKeywords + removedAttrs = KickstartCommand.removedAttrs + + def __init__(self, writePriority=0, *args, **kwargs): + KickstartCommand.__init__(self, writePriority, *args, **kwargs) + self.op = self._getParser() + + self.policyList = kwargs.get("policyList", []) + + def __str__(self): + retval = "" + for policy in self.policyList: + retval += policy.__str__() + + return retval + + def _getParser(self): + op = KSOptionParser() + op.add_option("--minlen", type="int") + op.add_option("--minquality", type="int") + op.add_option("--strict", action="store_true") + op.add_option("--notstrict", dest="strict", action="store_false") + op.add_option("--changesok", action="store_true") + op.add_option("--nochanges", dest="changesok", action="store_false") + op.add_option("--emptyok", action="store_true") + op.add_option("--notempty", dest="emptyok", action="store_false") + return op + + def parse(self, args): + (opts, extra) = self.op.parse_args(args=args, lineno=self.lineno) + if len(extra) != 1: + raise KickstartValueError(formatErrorMsg(self.lineno, msg=_("policy name required for %s") % "pwpolicy")) + + pd = self.handler.PwPolicyData() + self._setToObj(self.op, opts, pd) + pd.lineno = self.lineno + pd.name = extra[0] + + # Check for duplicates in the data list. + if pd in self.dataList(): + warnings.warn(_("A %s with the name %s has already been defined.") % ("pwpolicy", pd.name)) + + return pd + + def dataList(self): + return self.policyList + + def get_policy(self, name): + """ Get the policy by name + + :param str name: Name of the policy to return. + + """ + policy = [p for p in self.policyList if p.name == name] + if policy: + return policy[0] + else: + return None diff --git a/anaconda/pyanaconda/rescue.py b/anaconda/pyanaconda/rescue.py index 63bc0c1..6fba78d 100644 --- a/anaconda/pyanaconda/rescue.py +++ b/anaconda/pyanaconda/rescue.py @@ -25,7 +25,6 @@ from pyanaconda import iutil import shutil import time import re -import subprocess from snack import ButtonChoiceWindow, ListboxChoiceWindow,SnackScreen @@ -37,11 +36,13 @@ from pyanaconda.installinterfacebase import InstallInterfaceBase from pyanaconda.i18n import _ from pyanaconda.kickstart import runPostScripts -from blivet import mountExistingSystem -from blivet.errors import StorageError, DirtyFSError +from blivet import osinstall +from blivet.errors import StorageError from blivet.devices import LUKSDevice +from blivet.osinstall import storageInitialize, mountExistingSystem from pykickstart.constants import KS_REBOOT, KS_SHUTDOWN +from gi.repository import BlockDev as blockdev import meh.ui.text @@ -190,8 +191,7 @@ def runShell(screen = None, msg=""): proc = None if os.path.exists("/usr/bin/firstaidkit-qs"): - proc = subprocess.Popen(["/usr/bin/firstaidkit-qs"]) - proc.wait() + iutil.execWithRedirect("/usr/bin/firstaidkit-qs", []) if proc is None or proc.returncode!=0: if os.path.exists("/bin/bash"): @@ -246,8 +246,8 @@ def _unlock_devices(intf, storage): unlocked = True # try to use the same passhprase for other devices try_passphrase = passphrase - except StorageError as serr: - log.error("Failed to unlock %s: %s", device.name, serr) + except (StorageError, blockdev.CryptoError) as err: + log.error("Failed to unlock %s: %s", device.name, err) device.teardown(recursive=True) device.format.passphrase = None try_passphrase = None @@ -307,9 +307,9 @@ def doRescue(intf, rescue_mount, ksdata): break sto = blivet.Blivet(ksdata=ksdata) - blivet.storageInitialize(sto, ksdata, []) + storageInitialize(sto, ksdata, []) _unlock_devices(intf, sto) - roots = blivet.findExistingInstallations(sto.devicetree) + roots = osinstall.findExistingInstallations(sto.devicetree) if not roots: root = None @@ -350,84 +350,71 @@ def doRescue(intf, rescue_mount, ksdata): msg = _("Run %s to unmount the system " "when you are finished.") % ANACONDA_CLEANUP - try: - mountExistingSystem(sto.fsset, root.device, - allowDirty = True, - readOnly = readOnly) - except DirtyFSError: - if flags.automatedInstall: - log.error("System had dirty file systems which you chose not to mount") - else: - ButtonChoiceWindow(intf.screen, _("Rescue"), - _("Your system had dirty file systems which you chose not " - "to mount. Press return to get a shell from which " - "you can fsck and mount your partitions. %s") % msg, - [_("OK")], width = 50) - rootmounted = False + mountExistingSystem(sto.fsset, root.device, readOnly=readOnly) + + if flags.automatedInstall: + log.info("System has been mounted under: %s", iutil.getSysroot()) else: - if flags.automatedInstall: - log.info("System has been mounted under: %s", iutil.getSysroot()) - else: - ButtonChoiceWindow(intf.screen, _("Rescue"), - _("Your system has been mounted under %(rootPath)s.\n\n" - "Press <return> to get a shell. If you would like to " - "make your system the root environment, run the command:\n\n" - "\tchroot %(rootPath)s\n\n%(msg)s") % - {'rootPath': iutil.getSysroot(), - 'msg': msg}, - [_("OK")] ) - rootmounted = True + ButtonChoiceWindow(intf.screen, _("Rescue"), + _("Your system has been mounted under %(rootPath)s.\n\n" + "Press <return> to get a shell. If you would like to " + "make your system the root environment, run the command:\n\n" + "\tchroot %(rootPath)s\n\n%(msg)s") % + {'rootPath': iutil.getSysroot(), + 'msg': msg}, + [_("OK")] ) + rootmounted = True - # now turn on swap - if not readOnly: - try: - sto.turnOnSwap() - except StorageError: - log.error("Error enabling swap") - - # and selinux too - if flags.selinux: - # we have to catch the possible exception - # because we support read-only mounting - try: - fd = open("%s/.autorelabel" % iutil.getSysroot(), "w+") - fd.close() - except IOError: - log.warning("cannot touch /.autorelabel") - - # set a library path to use mounted fs - libdirs = os.environ.get("LD_LIBRARY_PATH", "").split(":") - mounted = map(lambda dir: "/mnt/sysimage%s" % dir, libdirs) - os.environ["LD_LIBRARY_PATH"] = ":".join(libdirs + mounted) - - # find groff data dir - gversion = None + # now turn on swap + if not readOnly: try: - glst = os.listdir("/mnt/sysimage/usr/share/groff") - except OSError: - pass - else: - # find a directory which is a numeral, its where - # data files are - for gdir in glst: - if re.match(r'\d[.\d]+\d$', gdir): - gversion = gdir - break + sto.turnOnSwap() + except StorageError: + log.error("Error enabling swap") - if gversion is not None: - gpath = "/mnt/sysimage/usr/share/groff/"+gversion - os.environ["GROFF_FONT_PATH"] = gpath + '/font' - os.environ["GROFF_TMAC_PATH"] = "%s:/mnt/sysimage/usr/share/groff/site-tmac" % (gpath + '/tmac',) - - # do we have bash? + # and selinux too + if flags.selinux: + # we have to catch the possible exception + # because we support read-only mounting try: - if os.access("/usr/bin/bash", os.R_OK): - os.symlink ("/usr/bin/bash", "/bin/bash") - except OSError: - pass + fd = open("%s/.autorelabel" % iutil.getSysroot(), "w+") + fd.close() + except IOError: + log.warning("cannot touch /.autorelabel") + + # set a library path to use mounted fs + libdirs = os.environ.get("LD_LIBRARY_PATH", "").split(":") + mounted = ["/mnt/sysimage%s" % mdir for mdir in libdirs] + iutil.setenv("LD_LIBRARY_PATH", ":".join(libdirs + mounted)) + + # find groff data dir + gversion = None + try: + glst = os.listdir("/mnt/sysimage/usr/share/groff") + except OSError: + pass + else: + # find a directory which is a numeral, its where + # data files are + for gdir in glst: + if re.match(r'\d[.\d]+\d$', gdir): + gversion = gdir + break + + if gversion is not None: + gpath = "/mnt/sysimage/usr/share/groff/"+gversion + iutil.setenv("GROFF_FONT_PATH", gpath + '/font') + iutil.setenv("GROFF_TMAC_PATH", "%s:/mnt/sysimage/usr/share/groff/site-tmac" % (gpath + '/tmac',)) + + # do we have bash? + try: + if os.access("/usr/bin/bash", os.R_OK): + os.symlink ("/usr/bin/bash", "/bin/bash") + except OSError: + pass except (ValueError, LookupError, SyntaxError, NameError): raise - except Exception as e: + except Exception as e: # pylint: disable=broad-except log.error("doRescue caught exception: %s", e) if flags.automatedInstall: log.error("An error occurred trying to mount some or all of your system") diff --git a/anaconda/pyanaconda/storage_utils.py b/anaconda/pyanaconda/storage_utils.py index 410dfea..091c7a9 100644 --- a/anaconda/pyanaconda/storage_utils.py +++ b/anaconda/pyanaconda/storage_utils.py @@ -51,7 +51,7 @@ DEVICE_TEXT_LVM = N_("LVM") DEVICE_TEXT_LVM_THINP = N_("LVM Thin Provisioning") DEVICE_TEXT_MD = N_("RAID") DEVICE_TEXT_PARTITION = N_("Standard Partition") -DEVICE_TEXT_BTRFS = N_("BTRFS") +DEVICE_TEXT_BTRFS = N_("Btrfs") DEVICE_TEXT_DISK = N_("Disk") DEVICE_TEXT_MAP = {DEVICE_TYPE_LVM: DEVICE_TEXT_LVM, @@ -74,11 +74,11 @@ MOUNTPOINT_DESCRIPTIONS = {"Swap": N_("The 'swap' area on your computer is used "BIOS Boot": N_("The BIOS boot partition is required to enable booting\n" "from GPT-partitioned disks on BIOS hardware."), "PReP Boot": N_("The PReP boot partition is required as part of the\n" - "bootloader configuration on some PPC platforms.") + "boot loader configuration on some PPC platforms.") } AUTOPART_CHOICES = ((N_("Standard Partition"), AUTOPART_TYPE_PLAIN), - (N_("BTRFS"), AUTOPART_TYPE_BTRFS), + (N_("Btrfs"), AUTOPART_TYPE_BTRFS), (N_("LVM"), AUTOPART_TYPE_LVM), (N_("LVM Thin Provisioning"), AUTOPART_TYPE_LVM_THINP)) @@ -90,26 +90,30 @@ AUTOPART_DEVICE_TYPES = {AUTOPART_TYPE_LVM: DEVICE_TYPE_LVM, NAMED_DEVICE_TYPES = (DEVICE_TYPE_BTRFS, DEVICE_TYPE_LVM, DEVICE_TYPE_MD, DEVICE_TYPE_LVM_THINP) CONTAINER_DEVICE_TYPES = (DEVICE_TYPE_LVM, DEVICE_TYPE_BTRFS, DEVICE_TYPE_LVM_THINP) -def size_from_input(input_str): - """Get size from user's input""" +def size_from_input(input_str, units=None): + """ Get a Size object from an input string. + + :param str input_str: a string forming some representation of a size + :param units: use these units if none specified in input_str + :type units: str or NoneType + :returns: a Size object corresponding to input_str + :rtype: :class:`blivet.size.Size` or NoneType + + Units default to bytes if no units in input_str or units. + """ if not input_str: # Nothing to parse return None - # if no unit was specified, default to MiB. Assume that a string - # ending with anything other than a digit has a unit suffix + # A string ending with a digit contains no units information. if re.search(r'[\d.%s]$' % locale.nl_langinfo(locale.RADIXCHAR), input_str): - input_str += "MiB" + input_str += units or "" try: size = Size(input_str) except ValueError: return None - else: - # Minimium size for ui-created partitions is 1MiB. - if size.convertTo(spec="MiB") < 1: - size = Size("1 MiB") return size @@ -231,7 +235,7 @@ def sanity_check(storage, min_ram=isys.MIN_RAM): stage1 = storage.bootloader.stage1_device if not stage1: exns.append( - SanityError(_("No valid bootloader target device found. " + SanityError(_("No valid boot loader target device found. " "See below for details."))) pe = _platform.stage1MissingError if pe: @@ -334,4 +338,110 @@ def verify_LUKS_devices_have_key(storage): d.format.type == "luks" and \ not d.format.exists and \ not d.format.hasKey): - yield LUKSDeviceWithoutKeyError(_("LUKS device %s has no encryption key") % (dev.name,)) + yield LUKSDeviceWithoutKeyError(_("Encryption requested for LUKS device %s but no encryption key specified for this device.") % (dev.name,)) + + +def bound_size(size, device, old_size): + """ Returns a size bounded by the maximum and minimum size for + the device. + + :param size: the candidate size + :type size: :class:`blivet.size.Size` + :param device: the device being displayed + :type device: :class:`blivet.devices.StorageDevice` + :param old_size: the fallback size + :type old_size: :class:`blivet.size.Size` + :returns: a size to which to set the device + :rtype: :class:`blivet.size.Size` + + If size is 0, interpreted as set size to maximum possible. + If no maximum size is available, reset size to old_size, but + log a warning. + """ + max_size = device.maxSize + min_size = device.minSize + if not size: + if max_size: + log.info("No size specified, using maximum size for this device (%d).", max_size) + size = max_size + else: + log.warning("No size specified and no maximum size available, setting size back to original size (%d).", old_size) + size = old_size + else: + if max_size: + if size > max_size: + log.warning("Size specified (%d) is greater than the maximum size for this device (%d), using maximum size.", size, max_size) + size = max_size + else: + log.warning("Unknown upper bound on size. Using requested size (%d).", size) + + if size < min_size: + log.warning("Size specified (%d) is less than the minimum size for this device (%d), using minimum size.", size, min_size) + size = min_size + + return size + +class StorageSnapshot(object): + """R/W snapshot of storage (i.e. a :class:`blivet.Blivet` instance)""" + + def __init__(self, storage=None): + """ + Create new instance of the class + + :param storage: if given, its snapshot is created + :type storage: :class:`blivet.Blivet` + """ + if storage: + self._storage_snap = storage.copy() + else: + self._storage_snap = None + + @property + def storage(self): + return self._storage_snap + + @property + def created(self): + return bool(self._storage_snap) + + def create_snapshot(self, storage): + """Create (and save) snapshot of storage""" + + self._storage_snap = storage.copy() + + def dispose_snapshot(self): + """ + Dispose (unref) the snapshot + + .. note:: + + In order to free the memory taken by the snapshot, all references + returned by :property:`self.storage` have to be unrefed too. + """ + self._storage_snap = None + + def reset_to_snapshot(self, storage, dispose=False): + """ + Reset storage to snapshot (**modifies :param:`storage` in place**) + + :param storage: :class:`blivet.Blivet` instance to reset to the created snapshot + :param bool dispose: whether to dispose the snapshot after reset or not + :raises ValueError: if no snapshot is available (was not created before) + """ + if not self.created: + raise ValueError("No snapshot created, cannot reset") + + # we need to create a new copy from the snapshot first -- simple + # assignment from the snapshot would result in snapshot being modified + # by further changes of 'storage' + new_copy = self._storage_snap.copy() + storage.devicetree = new_copy.devicetree + storage.roots = new_copy.roots + storage.fsset = new_copy.fsset + + if dispose: + self.dispose_snapshot() + +# a snapshot of early storage as we got it from scanning disks without doing any +# changes +on_disk_storage = StorageSnapshot() diff --git a/anaconda/pyanaconda/threads.py b/anaconda/pyanaconda/threads.py index eea1da6..3a8d162 100644 --- a/anaconda/pyanaconda/threads.py +++ b/anaconda/pyanaconda/threads.py @@ -97,12 +97,15 @@ class ThreadManager(object): """Wait for the thread to exit and if the thread exited with an error re-raise it here. """ + + ret_val = True + # we don't need a lock here, # because get() acquires it itself try: self.get(name).join() except AttributeError: - pass + ret_val = False # - if there is a thread object for the given name, # we join it # - if there is not a thread object for the given name, @@ -111,6 +114,9 @@ class ThreadManager(object): self.raise_if_error(name) + # return True if we waited for the thread, False otherwise + return ret_val + def wait_all(self): """Wait for all threads to exit and if there was an error re-raise it. """ @@ -121,7 +127,7 @@ class ThreadManager(object): self.wait(name) if self.any_errors: - thread_names = ", ".join(thread_name for thread_name in self._errors.iterkeys() + thread_names = ", ".join(thread_name for thread_name in self._errors.keys() if self._errors[thread_name]) msg = "Unhandled errors from the following threads detected: %s" % thread_names raise RuntimeError(msg) @@ -182,6 +188,17 @@ class ThreadManager(object): with self._objs_lock: return self._objs.keys() + def wait_for_error_threads(self): + """ + Waits for all threads that caused exceptions. In other words, waits for + exception handling (possibly interactive) to be finished. + + """ + + for thread_name in self._errors.keys(): + thread = self._objs[thread_name] + thread.join() + class AnacondaThread(threading.Thread): """A threading.Thread subclass that exists only for a couple purposes: diff --git a/anaconda/pyanaconda/timezone.py b/anaconda/pyanaconda/timezone.py index 9cc2c53..956c33e 100644 --- a/anaconda/pyanaconda/timezone.py +++ b/anaconda/pyanaconda/timezone.py @@ -215,3 +215,12 @@ def is_valid_timezone(timezone): return timezone in pytz.common_timezones + etc_zones +def get_timezone(timezone): + """ + Return a tzinfo object for a given timezone name. + + :param str timezone: the timezone name + :rtype: datetime.tzinfo + """ + + return pytz.timezone(timezone) diff --git a/anaconda/pyanaconda/ui/__init__.py b/anaconda/pyanaconda/ui/__init__.py index b1eb060..9bf7959 100644 --- a/anaconda/pyanaconda/ui/__init__.py +++ b/anaconda/pyanaconda/ui/__init__.py @@ -22,14 +22,13 @@ __all__ = ["UserInterface"] import copy -import os from pyanaconda.ui.common import collect class PathDict(dict): """Dictionary class supporting + operator""" def __add__(self, ext): new_dict = copy.copy(self) - for key, value in ext.iteritems(): + for key, value in ext.items(): try: new_dict[key].extend(value) except KeyError: @@ -72,9 +71,6 @@ class UserInterface(object): from pyanaconda.errors import errorHandler errorHandler.ui = self - - basepath = os.path.dirname(__file__) - basemask = "pyanaconda.ui" paths = PathDict({}) @property @@ -86,7 +82,7 @@ class UserInterface(object): def update_paths(cls, pathdict): """Receives pathdict and appends it's contents to the current class defined search path dictionary.""" - for k,v in pathdict.iteritems(): + for k,v in pathdict.items(): cls.paths.setdefault(k, []) cls.paths[k].extend(v) @@ -176,10 +172,10 @@ class UserInterface(object): actionClasses = [] for hub in hubs: - actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "preForHub", None) == hub, spokes), + actionClasses.extend(sorted(filter(lambda obj, h=hub: getattr(obj, "preForHub", None) == h, spokes), key=lambda obj: obj.priority)) actionClasses.append(hub) - actionClasses.extend(sorted(filter(lambda obj: getattr(obj, "postForHub", None) == hub, spokes), + actionClasses.extend(sorted(filter(lambda obj, h=hub: getattr(obj, "postForHub", None) == h, spokes), key=lambda obj: obj.priority)) return actionClasses diff --git a/anaconda/pyanaconda/ui/categories/__init__.py b/anaconda/pyanaconda/ui/categories/__init__.py index 6a0e270..5fa3ccd 100644 --- a/anaconda/pyanaconda/ui/categories/__init__.py +++ b/anaconda/pyanaconda/ui/categories/__init__.py @@ -19,7 +19,6 @@ # Red Hat Author(s): Chris Lumens <clumens@redhat.com> # -import os.path from pyanaconda.i18n import N_ __all__ = ["SpokeCategory"] diff --git a/anaconda/pyanaconda/ui/gui/Makefile.am b/anaconda/pyanaconda/ui/gui/Makefile.am index 013c550..1a618f4 100644 --- a/anaconda/pyanaconda/ui/gui/Makefile.am +++ b/anaconda/pyanaconda/ui/gui/Makefile.am @@ -15,7 +15,7 @@ # # Author: Chris Lumens <clumens@redhat.com> -SUBDIRS = hubs spokes tools +SUBDIRS = hubs spokes MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/pyanaconda/ui/gui/__init__.py b/anaconda/pyanaconda/ui/gui/__init__.py index 38d64a5..232070e 100644 --- a/anaconda/pyanaconda/ui/gui/__init__.py +++ b/anaconda/pyanaconda/ui/gui/__init__.py @@ -18,7 +18,7 @@ # # Red Hat Author(s): Chris Lumens <clumens@redhat.com> # -import inspect, os, sys, time, site +import inspect, os, sys, time, site, signal import meh.ui.gui from contextlib import contextmanager @@ -26,10 +26,12 @@ from contextlib import contextmanager from gi.repository import Gdk, Gtk, AnacondaWidgets, Keybinder, GdkPixbuf, GLib, GObject from pyanaconda.i18n import _ -from pyanaconda import product +from pyanaconda.constants import IPMI_ABORTED +from pyanaconda import product, iutil +from pyanaconda import threads from pyanaconda.ui import UserInterface, common -from pyanaconda.ui.gui.utils import gtk_action_wait, busyCursor, unbusyCursor +from pyanaconda.ui.gui.utils import gtk_action_wait, unbusyCursor from pyanaconda import ihelp import os.path @@ -44,13 +46,11 @@ SCREENSHOT_DELAY = 1 # in seconds ANACONDA_WINDOW_GROUP = Gtk.WindowGroup() -# Stylesheet priorities to use for product-specific stylesheets and our -# missing icon overrides. The missing icon rules should be higher than -# the regular stylesheet, applied at GTK_STYLE_PROVIDER_PRIORITY_APPLICATION, -# and stylesheets from updates.img and product.img should be higher than that, -# so that they can override background images and not get re-overriden by us. -# Both should be lower than GTK_STYLE_PROVIDER_PRIORITY_USER. -STYLE_PROVIDER_PRIORITY_MISSING_ICON = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 10 +# Stylesheet priorities to use for product-specific stylesheets. +# installclass stylesheets should be higher than our base stylesheet, and +# stylesheets from updates.img and product.img should be higher than that. All +# levels should be lower than GTK_STYLE_PROVIDER_PRIORITY_USER. +STYLE_PROVIDER_PRIORITY_INSTALLCLASS = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 15 STYLE_PROVIDER_PRIORITY_UPDATES = Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 20 assert STYLE_PROVIDER_PRIORITY_UPDATES < Gtk.STYLE_PROVIDER_PRIORITY_USER @@ -269,9 +269,24 @@ class ErrorDialog(GUIObject): class MainWindow(Gtk.Window): """This is a top-level, full size window containing the Anaconda screens.""" - def __init__(self): + def __init__(self, fullscreen): + """Create a new anaconda main window. + + :param bool fullscreen: if True, fullscreen the window, if false maximize + """ Gtk.Window.__init__(self) + # Hide the titlebar when maximized if the window manager allows it. + # This makes anaconda look full-screenish but without covering parts + # needed to interact with the window manager, like the GNOME top bar. + self.set_hide_titlebar_when_maximized(True) + + # The Anaconda and Initial Setup windows might sometimes get decorated with + # a titlebar which contains the __init__.py header text by default. + # As all Anaconda and Initial Setup usually have a very distinct title text + # inside the window, the titlebar text is redundant and should be disabled. + self.set_title("") + # Treat an attempt to close the window the same as hitting quit self.connect("delete-event", self._on_delete_event) @@ -292,19 +307,20 @@ class MainWindow(Gtk.Window): self._overlay_depth = 0 # Create a stack and a list of what's been added to the stack - self._stack = Gtk.Stack() + # Double the stack transition duration since the default 200ms is too + # quick to get the point across + self._stack = Gtk.Stack(transition_duration=400) self._stack_contents = set() # Create an accel group for the F12 accelerators added after window transitions self._accel_group = Gtk.AccelGroup() self.add_accel_group(self._accel_group) - # Connect to window-state-event changes to catch when the user - # maxmizes/unmaximizes the window. - self.connect("window-state-event", self._on_window_state_event) - - # Start the window as full screen - self.fullscreen() + # Make the window big + if fullscreen: + self.fullscreen() + else: + self.maximize() self._overlay.add(self._stack) self.add(self._overlay) @@ -312,17 +328,10 @@ class MainWindow(Gtk.Window): self._current_action = None - def _on_window_state_event(self, window, event, user_data=None): - # If the window is being maximized, fullscreen it instead - if (Gdk.WindowState.MAXIMIZED & event.changed_mask) and \ - (Gdk.WindowState.MAXIMIZED & event.new_window_state): - self.fullscreen() - - # Return true to stop the signal handler since we're changing - # state mid-stream here - return True - - return False + # Help button mnemonics handling + self._mnemonic_signal = None + # we have a sensible initial value, just in case + self._saved_help_button_label = _("Help!") def _on_delete_event(self, widget, event, user_data=None): # Use the quit-clicked signal on the the current standalone, even if the @@ -340,13 +349,22 @@ class MainWindow(Gtk.Window): overlayed_widget.set_from_pixbuf(self._transparent_base.scale_simple( overlay_allocation.width, overlay_allocation.height, GdkPixbuf.InterpType.NEAREST)) - # Set the allocation for the overlayed image to the full size of the GtkOverlay - allocation.x = 0 - allocation.y = 0 - allocation.width = overlay_allocation.width - allocation.height = overlay_allocation.height + # Return False to indicate that the child allocation is not yet set + return False - return True + def _on_mnemonics_visible_changed(self, window, property_type, obj): + # mnemonics display has been activated or deactivated, + # add or remove the F1 mnemonics display from the help button + help_button = obj.window.get_help_button() + if window.props.mnemonics_visible: + # save current label + old_label = help_button.get_label() + self._saved_help_button_label = old_label + # add the (F1) "mnemonics" to the help button + help_button.set_label("%s (F1)" % old_label) + else: + # restore the old label + help_button.set_label(self._saved_help_button_label) @property def current_action(self): @@ -371,17 +389,20 @@ class MainWindow(Gtk.Window): if isinstance(child.window, AnacondaWidgets.BaseStandalone): child.window.add_accelerator("continue-clicked", self._accel_group, Gdk.KEY_F12, 0, 0) - child.window.add_accelerator("help-button-clicked", self._accel_group, - Gdk.KEY_F1, 0, 0) - child.window.add_accelerator("help-button-clicked", self._accel_group, - Gdk.KEY_F1, Gdk.ModifierType.MOD1_MASK, 0) elif isinstance(child.window, AnacondaWidgets.SpokeWindow): child.window.add_accelerator("button-clicked", self._accel_group, Gdk.KEY_F12, 0, 0) - child.window.add_accelerator("help-button-clicked", self._accel_group, - Gdk.KEY_F1, 0, 0) - child.window.add_accelerator("help-button-clicked", self._accel_group, - Gdk.KEY_F1, Gdk.ModifierType.MOD1_MASK, 0) + + # Configure the help button + child.window.add_accelerator("help-button-clicked", self._accel_group, + Gdk.KEY_F1, 0, 0) + child.window.add_accelerator("help-button-clicked", self._accel_group, + Gdk.KEY_F1, Gdk.ModifierType.MOD1_MASK, 0) + + # Connect to mnemonics-visible to add the (F1) mnemonic to the button label + if self._mnemonic_signal: + self.disconnect(self._mnemonic_signal) + self._mnemonic_signal = self.connect("notify::mnemonics-visible", self._on_mnemonics_visible_changed, child) self._stack.set_visible_child(child.window) @@ -396,6 +417,9 @@ class MainWindow(Gtk.Window): :param AnacondaWidgets.BaseStandalone standalone: the new standalone action """ + # Slide the old hub/standalone off of the new one + self._stack.set_transition_type(Gtk.StackTransitionType.UNDER_LEFT) + self._current_action = standalone self._setVisibleChild(standalone) @@ -407,10 +431,16 @@ class MainWindow(Gtk.Window): :param AnacondaWidgets.SpokeWindow spoke: a spoke to enter """ + # Slide up, as if the spoke is under the hub + self._stack.set_transition_type(Gtk.StackTransitionType.UNDER_UP) + self._setVisibleChild(spoke) def returnToHub(self): """Exit a spoke and return to a hub.""" + # Slide back down over the spoke + self._stack.set_transition_type(Gtk.StackTransitionType.OVER_DOWN) + self._setVisibleChild(self._current_action) def lightbox_on(self): @@ -451,7 +481,7 @@ class GraphicalUserInterface(UserInterface): """ def __init__(self, storage, payload, instclass, distributionText = product.distributionText, isFinal = product.isFinal, - quitDialog = QuitDialog, gui_lock = None): + quitDialog = QuitDialog, gui_lock = None, fullscreen=False): UserInterface.__init__(self, storage, payload, instclass) @@ -462,7 +492,7 @@ class GraphicalUserInterface(UserInterface): self.data = None - self.mainWindow = MainWindow() + self.mainWindow = MainWindow(fullscreen=fullscreen) self._distributionText = distributionText self._isFinal = isFinal @@ -471,8 +501,6 @@ class GraphicalUserInterface(UserInterface): self.mainWindow.lightbox_on) ANACONDA_WINDOW_GROUP.add_window(self.mainWindow) - # we have a sensible initial value, just in case - self._saved_help_button_label = _("Help!") basemask = "pyanaconda.ui" basepath = os.path.dirname(__file__) @@ -493,48 +521,6 @@ class GraphicalUserInterface(UserInterface): for path in pathlist] } - def _assureLogoImage(self): - # make sure there is a logo image present, - # otherwise the console will get spammed by errors - replacement_image_path = None - logo_path = "/usr/share/anaconda/pixmaps/sidebar-logo.png" - header_path = "/usr/share/anaconda/pixmaps/anaconda_header.png" - sad_smiley_path = "/usr/share/icons/Adwaita/48x48/emotes/face-crying.png" - if not os.path.exists(logo_path): - # first try to replace the missing logo with the Anaconda header image - if os.path.exists(header_path): - replacement_image_path = header_path - # if the header image is not present, use a sad smiley from GTK icons - elif os.path.exists(sad_smiley_path): - replacement_image_path = sad_smiley_path - - if replacement_image_path: - log.warning("logo image is missing, using a substitute") - - # Add a new stylesheet overriding the background-image for .logo - provider = Gtk.CssProvider() - provider.load_from_data(".logo { background-image: url('%s'); }" % replacement_image_path) - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, - STYLE_PROVIDER_PRIORITY_MISSING_ICON) - else: - log.warning("logo image is missing") - - # Look for the top and sidebar images. If missing remove the background-image - topbar_path = "/usr/share/anaconda/pixmaps/topbar-bg.png" - sidebar_path = "/usr/share/anaconda/pixmaps/sidebar-bg.png" - if not os.path.exists(topbar_path): - provider = Gtk.CssProvider() - provider.load_from_data("AnacondaSpokeWindow #nav-box { background-image: none; }") - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, - STYLE_PROVIDER_PRIORITY_MISSING_ICON) - - if not os.path.exists(sidebar_path): - provider = Gtk.CssProvider() - provider.load_from_data(".logo-sidebar { background-image: none; }") - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, - STYLE_PROVIDER_PRIORITY_MISSING_ICON) - - def _widgetScale(self): # First, check if the GDK_SCALE environment variable is already set. If so, # leave it alone. @@ -576,7 +562,39 @@ class GraphicalUserInterface(UserInterface): if monitor_height_px >= 1200 and monitor_dpi_x > 192 and monitor_dpi_y > 192: display.set_window_scale(2) # Export the scale so that Gtk programs launched by anaconda are also scaled - os.environ["GDK_SCALE"] = "2" + iutil.setenv("GDK_SCALE", "2") + + def _convertSignals(self): + # What tends to happen when we receive a signal is that the signal will + # be received by the python interpreter's C handler, python will do + # what it needs to do to set the python handler we registered to run, + # the C handler returns, and then nothing happens because Gtk is + # holding the global interpreter lock. The signal then gets delivered + # to our python code when you move the mouse or something. We can get + # around this by doing signals the GLib way. The conversion assumes + # that none of our signal handlers care about the frame parameter, + # which is generally true. + # + # After the unix_signal_add call, signal.getsignal will tell a half + # truth: the method returned will still be called, by way of + # _signal_converter, but GLib will have replaced the actual signal + # handler for that signal. + + # Convert everything except SIGCHLD, because that's a different can of worms + + def _signal_converter(user_data): + (handler, signum) = user_data + handler(signum, None) + + for signum in (s for s in range(1, signal.NSIG) if s != signal.SIGCHLD): + handler = signal.getsignal(signum) + if handler and handler not in (signal.SIG_DFL, signal.SIG_IGN): + # NB: if you are looking at the glib documentation you are in for + # some surprises because gobject-introspection is a minefield. + # g_unix_signal_add_full comes out as GLib.unix_signal_add, and + # g_unix_signal_add doesn't come out at all. + GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signum, + _signal_converter, (handler, signum)) @property def tty_num(self): @@ -598,8 +616,6 @@ class GraphicalUserInterface(UserInterface): return isinstance(obj, StandaloneSpoke) def setup(self, data): - busyCursor() - self._actions = self.getActionClasses(self._list_hubs()) self.data = data @@ -635,7 +651,6 @@ class GraphicalUserInterface(UserInterface): # Use connect_after so classes can add actions before we change screens obj.window.connect_after("continue-clicked", self._on_continue_clicked) obj.window.connect_after("help-button-clicked", self._on_help_clicked, obj) - self.mainWindow.connect("notify::mnemonics-visible", self._on_mnemonics_visible_changed, obj) obj.window.connect_after("quit-clicked", self._on_quit_clicked) return obj @@ -655,55 +670,64 @@ class GraphicalUserInterface(UserInterface): log.error("Unhandled exception caught, waiting for python-meh to "\ "exit") - # Loop forever, meh will call sys.exit() when it's done - while True: - time.sleep(10000) + threads.threadMgr.wait_for_error_threads() + sys.exit(1) - # Apply a widget-scale to hidpi monitors - self._widgetScale() + try: + # Apply a widget-scale to hidpi monitors + self._widgetScale() - while not self._currentAction: - self._currentAction = self._instantiateAction(self._actions[0]) - if not self._currentAction: - self._actions.pop(0) + while not self._currentAction: + self._currentAction = self._instantiateAction(self._actions[0]) + if not self._currentAction: + self._actions.pop(0) - if not self._actions: - return + if not self._actions: + return - self._currentAction.initialize() - self._currentAction.entry_logger() - self._currentAction.refresh() + self._currentAction.initialize() + self._currentAction.entry_logger() + self._currentAction.refresh() - self._currentAction.window.set_beta(not self._isFinal) - self._currentAction.window.set_property("distribution", self._distributionText().upper()) + self._currentAction.window.set_beta(not self._isFinal) + self._currentAction.window.set_property("distribution", self._distributionText().upper()) - # Set some program-wide settings. - settings = Gtk.Settings.get_default() - settings.set_property("gtk-font-name", "Cantarell") - settings.set_property("gtk-icon-theme-name", "gnome") + # Set some program-wide settings. + settings = Gtk.Settings.get_default() + settings.set_property("gtk-font-name", "Cantarell") + settings.set_property("gtk-icon-theme-name", "gnome") - # Apply the application stylesheet - provider = Gtk.CssProvider() - provider.load_from_path("/usr/share/anaconda/anaconda-gtk.css") - Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + # Apply the application stylesheet + provider = Gtk.CssProvider() + provider.load_from_path("/usr/share/anaconda/anaconda-gtk.css") + Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - # Look for updates to the stylesheet and apply them at a higher priority - for updates_dir in ("updates", "product"): - updates_css = "/run/install/%s/anaconda-gtk.css" % updates_dir - if os.path.exists(updates_css): + # Apply the installclass stylesheet + if self.instclass.stylesheet: provider = Gtk.CssProvider() - provider.load_from_path(updates_css) + provider.load_from_path(self.instclass.stylesheet) Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, - STYLE_PROVIDER_PRIORITY_UPDATES) + STYLE_PROVIDER_PRIORITY_INSTALLCLASS) - # try to make sure a logo image is present - self._assureLogoImage() + # Look for updates to the stylesheet and apply them at a higher priority + for updates_dir in ("updates", "product"): + updates_css = "/run/install/%s/anaconda-gtk.css" % updates_dir + if os.path.exists(updates_css): + provider = Gtk.CssProvider() + provider.load_from_path(updates_css) + Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, + STYLE_PROVIDER_PRIORITY_UPDATES) - self.mainWindow.setCurrentAction(self._currentAction) + self.mainWindow.setCurrentAction(self._currentAction) - # Do this at the last possible minute. - unbusyCursor() + # Do this at the last possible minute. + unbusyCursor() + # If anything went wrong before we start the Gtk main loop, release + # the gui lock and re-raise the exception so that meh can take over + except Exception: + self._gui_lock.release() + raise Gtk.main() @@ -730,7 +754,7 @@ class GraphicalUserInterface(UserInterface): with self.mainWindow.enlightbox(dlg.window): dlg.refresh(details) - rc = dlg.run() + dlg.run() dlg.window.destroy() @gtk_action_wait @@ -753,7 +777,7 @@ class GraphicalUserInterface(UserInterface): ### SIGNAL HANDLING METHODS ### def _on_continue_clicked(self, win, user_data=None): - if not win.get_may_continue(): + if not win.get_may_continue() or win != self._currentAction.window: return # If we're on the last screen, clicking Continue quits. @@ -818,20 +842,6 @@ class GraphicalUserInterface(UserInterface): # content for the current screen ihelp.start_yelp(ihelp.get_help_path(obj.helpFile, self.instclass)) - def _on_mnemonics_visible_changed(self, window, property, obj): - # mnemonics display has been activated or deactivated, - # add or remove the F1 mnemonics display from the help button - help_button = obj.window.get_help_button() - if window.props.mnemonics_visible: - # save current label - old_label = help_button.get_label() - self._saved_help_button_label = old_label - # add the (F1) "mnemonics" to the help button - help_button.set_label("%s (F1)" % old_label) - else: - # restore the old label - help_button.set_label(self._saved_help_button_label) - def _on_quit_clicked(self, win, userData=None): if not win.get_quit_button(): return @@ -843,6 +853,7 @@ class GraphicalUserInterface(UserInterface): if rc == 1: self._currentAction.exit_logger() + iutil.ipmi_report(IPMI_ABORTED) sys.exit(0) class GraphicalExceptionHandlingIface(meh.ui.gui.GraphicalIntf): diff --git a/anaconda/pyanaconda/ui/gui/helpers.py b/anaconda/pyanaconda/ui/gui/helpers.py index 11b4b15..320f933 100644 --- a/anaconda/pyanaconda/ui/gui/helpers.py +++ b/anaconda/pyanaconda/ui/gui/helpers.py @@ -90,7 +90,6 @@ class GUISpokeInputCheckHandler(GUIInputCheckHandler): self.clear_info() if failed_check: self.set_warning(failed_check.check_status) - self.window.show_all() # Implemented by GUIObject @abstractmethod diff --git a/anaconda/pyanaconda/ui/gui/hubs/__init__.py b/anaconda/pyanaconda/ui/gui/hubs/__init__.py index 88c52bb..4e5c71b 100644 --- a/anaconda/pyanaconda/ui/gui/hubs/__init__.py +++ b/anaconda/pyanaconda/ui/gui/hubs/__init__.py @@ -19,8 +19,6 @@ # Red Hat Author(s): Chris Lumens <clumens@redhat.com> # -import os - from gi.repository import GLib from pyanaconda.flags import flags @@ -29,9 +27,7 @@ from pyanaconda.product import distributionText from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.spokes import StandaloneSpoke from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup -from pyanaconda.constants import ANACONDA_ENVIRON import logging log = logging.getLogger("anaconda") @@ -97,7 +93,6 @@ class Hub(GUIObject, common.Hub): def _createBox(self): from gi.repository import Gtk, AnacondaWidgets - from pyanaconda.ui.gui.utils import setViewportBackground cats_and_spokes = self._collectCategoriesAndSpokes() categories = cats_and_spokes.keys() @@ -197,7 +192,6 @@ class Hub(GUIObject, common.Hub): viewport.add(grid) spokeArea.add(viewport) - setViewportBackground(viewport) self._updateContinue() def _updateCompleteness(self, spoke, update_continue=True): @@ -226,12 +220,10 @@ class Hub(GUIObject, common.Hub): if len(self._incompleteSpokes) == 0: if self._checker and not self._checker.check(): self.set_warning(self._checker.error_message) - self.window.show_all() else: msg = _("Please complete items marked with this icon before continuing to the next step.") self.set_warning(msg) - self.window.show_all() self._updateContinueButton() @@ -250,7 +242,7 @@ class Hub(GUIObject, common.Hub): if not self._spokes and self.window.get_may_continue(): # no spokes, move on - log.info("no spokes available on %s, continuing automatically", self) + log.debug("no spokes available on %s, continuing automatically", self) gtk_call_once(self.window.emit, "continue-clicked") click_continue = False @@ -264,7 +256,7 @@ class Hub(GUIObject, common.Hub): # The first argument to all codes is the name of the spoke we are # acting on. If no such spoke exists, throw the message away. spoke = self._spokes.get(args[0], None) - if not spoke: + if not spoke or spoke.__class__.__name__ not in self._spokes: q.task_done() continue @@ -275,7 +267,7 @@ class Hub(GUIObject, common.Hub): self._notReadySpokes.append(spoke) self._updateContinueButton() - log.info("spoke is not ready: %s", spoke) + log.debug("spoke is not ready: %s", spoke) elif code == hubQ.HUB_CODE_READY: self._updateCompleteness(spoke) @@ -283,13 +275,18 @@ class Hub(GUIObject, common.Hub): self._notReadySpokes.remove(spoke) self._updateContinueButton() - log.info("spoke is ready: %s", spoke) + log.debug("spoke is ready: %s", spoke) # If this is a real kickstart install (the kind with an input ks file) # and all spokes are now completed, we should skip ahead to the next # hub automatically. Take into account the possibility the user is # viewing a spoke right now, though. if flags.automatedInstall: + # Users might find it helpful to know why a kickstart install + # went interactive. Log that here. + if not spoke.completed: + log.info("kickstart installation stopped for info: %s", spoke.title.replace("_", "")) + # Spokes that were not initially ready got the execute call in # _createBox skipped. Now that it's become ready, do it. Note # that we also provide a way to skip this processing (see comments @@ -306,14 +303,14 @@ class Hub(GUIObject, common.Hub): elif code == hubQ.HUB_CODE_MESSAGE: spoke.selector.set_property("status", args[1]) - log.info("setting %s status to: %s", spoke, args[1]) + log.debug("setting %s status to: %s", spoke, args[1]) q.task_done() # queue is now empty, should continue be clicked? if self._autoContinue and click_continue and self.window.get_may_continue(): # enqueue the emit to the Gtk message queue - log.info("_autoContinue clicking continue button") + log.debug("_autoContinue clicking continue button") gtk_call_once(self.window.emit, "continue-clicked") return True @@ -357,6 +354,10 @@ class Hub(GUIObject, common.Hub): self.main_window.enterSpoke(spoke) def spoke_done(self, spoke): + # Ignore if not in a spoke + if not self._inSpoke: + return + spoke.visitedSinceApplied = True # Don't take visitedSinceApplied into account here. It will always be @@ -371,7 +372,7 @@ class Hub(GUIObject, common.Hub): self._inSpoke = False # Now update the selector with the current status and completeness. - for sp in self._spokes.itervalues(): + for sp in self._spokes.values(): if not sp.indirect: self._updateCompleteness(sp, update_continue=False) diff --git a/anaconda/pyanaconda/ui/gui/hubs/progress.glade b/anaconda/pyanaconda/ui/gui/hubs/progress.glade index f459ce0..b072e32 100644 --- a/anaconda/pyanaconda/ui/gui/hubs/progress.glade +++ b/anaconda/pyanaconda/ui/gui/hubs/progress.glade @@ -23,6 +23,9 @@ <child> <placeholder/> </child> + <child> + <placeholder/> + </child> </object> </child> </object> @@ -36,7 +39,8 @@ <object class="GtkAlignment" id="AnacondaHubWindow-alignment1"> <property name="can_focus">False</property> <property name="yalign">0</property> - <property name="xscale">0.5</property> + <property name="left_padding">12</property> + <property name="right_padding">6</property> <child internal-child="action_area"> <object class="GtkBox" id="progressWindow-actionArea"> <property name="can_focus">False</property> diff --git a/anaconda/pyanaconda/ui/gui/hubs/progress.py b/anaconda/pyanaconda/ui/gui/hubs/progress.py index 95883ba..3f91513 100644 --- a/anaconda/pyanaconda/ui/gui/hubs/progress.py +++ b/anaconda/pyanaconda/ui/gui/hubs/progress.py @@ -32,7 +32,8 @@ from pyanaconda.i18n import _, C_ from pyanaconda.localization import find_best_locale_match from pyanaconda.product import productName from pyanaconda.flags import flags -from pyanaconda.constants import THREAD_INSTALL, THREAD_CONFIGURATION, DEFAULT_LANG +from pyanaconda import iutil +from pyanaconda.constants import THREAD_INSTALL, THREAD_CONFIGURATION, DEFAULT_LANG, IPMI_FINISHED from pykickstart.constants import KS_SHUTDOWN, KS_REBOOT from pyanaconda.ui.gui.hubs import Hub @@ -132,6 +133,8 @@ class ProgressHub(Hub): GLib.source_remove(self._rnotes_id) self._progressNotebook.set_current_page(1) + iutil.ipmi_report(IPMI_FINISHED) + # kickstart install, continue automatically if reboot or shutdown selected if flags.automatedInstall and self.data.reboot.action in [KS_REBOOT, KS_SHUTDOWN]: self.window.emit("continue-clicked") diff --git a/anaconda/pyanaconda/ui/gui/hubs/summary.glade b/anaconda/pyanaconda/ui/gui/hubs/summary.glade index 8588791..88313c2 100644 --- a/anaconda/pyanaconda/ui/gui/hubs/summary.glade +++ b/anaconda/pyanaconda/ui/gui/hubs/summary.glade @@ -24,6 +24,9 @@ <child> <placeholder/> </child> + <child> + <placeholder/> + </child> </object> </child> </object> @@ -37,7 +40,8 @@ <object class="GtkAlignment" id="AnacondaHubWindow-alignment1"> <property name="can_focus">False</property> <property name="yalign">0</property> - <property name="xscale">0.5</property> + <property name="left_padding">12</property> + <property name="right_padding">6</property> <child internal-child="action_area"> <object class="GtkBox" id="summaryWindow-actionArea"> <property name="can_focus">False</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/__init__.py b/anaconda/pyanaconda/ui/gui/spokes/__init__.py index f17b71c..1227d44 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/__init__.py +++ b/anaconda/pyanaconda/ui/gui/spokes/__init__.py @@ -21,7 +21,6 @@ from pyanaconda.ui import common from pyanaconda.ui.gui import GUIObject -import os.path from pyanaconda import ihelp __all__ = ["StandaloneSpoke", "NormalSpoke"] diff --git a/anaconda/pyanaconda/ui/gui/spokes/advanced_user.glade b/anaconda/pyanaconda/ui/gui/spokes/advanced_user.glade index 0063d9c..4bed411 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/advanced_user.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/advanced_user.glade @@ -270,7 +270,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="margin_top">3</property> - <property name="invisible_char">●</property> </object> <packing> <property name="expand">False</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/dasd.glade b/anaconda/pyanaconda/ui/gui/spokes/advstorage/dasd.glade index 791af79..2246829 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/advstorage/dasd.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/dasd.glade @@ -109,7 +109,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">?</property> </object> <packing> <property name="left_attach">1</property> @@ -208,7 +207,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">1</property> - <property name="icon_name">gtk-dialog-error</property> + <property name="icon_name">dialog-error</property> </object> <packing> <property name="left_attach">0</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/fcoe.glade b/anaconda/pyanaconda/ui/gui/spokes/advstorage/fcoe.glade index 90221ab..cec4717 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/advstorage/fcoe.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/fcoe.glade @@ -105,7 +105,7 @@ </child> <child> <object class="GtkCheckButton" id="autoCheckbox"> - <property name="label" translatable="yes" context="GUI|Advanced Storage|FCoE">Use auto _vlan</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|FCoE">Use auto _VLAN</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.glade b/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.glade index 1fb2f08..203a66f 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.glade @@ -12,6 +12,8 @@ <column type="gchararray"/> <!-- column-name nodeIface --> <column type="gchararray"/> + <!-- column-name nodePortal --> + <column type="gchararray"/> </columns> </object> <object class="GtkTreeModelFilter" id="nodeStoreFiltered"> @@ -124,7 +126,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_discover_field_changed" swapped="no"/> </object> <packing> @@ -152,7 +153,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_discover_field_changed" swapped="no"/> </object> <packing> @@ -259,7 +259,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|CHAP">CHAP _Username:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|CHAP">CHAP _User name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">chapUsernameEntry</property> </object> @@ -287,7 +287,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_discover_field_changed" swapped="no"/> </object> <packing> @@ -328,7 +327,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_discover_field_changed" swapped="no"/> </object> <packing> @@ -355,7 +353,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_discover_field_changed" swapped="no"/> </object> <packing> @@ -382,7 +379,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP">CHAP _Username:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP">CHAP _User name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">rchapUsernameEntry</property> </object> @@ -410,7 +407,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP">Reverse CHAP User_name:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Configure|Reverse CHAP">Reverse CHAP User _name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">rchapReverseUsername</property> </object> @@ -646,7 +643,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label">The following nodes were discovered using the iSCSI initiator <b>%(initiatorName)s</b> using the target IP address <b>%(targetAddress)s</b>. Please select which nodes you wish to log into:</property> + <property name="label">The following nodes have been discovered using the iSCSI initiator <b>%(initiatorName)s</b> using the target IP address <b>%(targetAddress)s</b>. Please select which nodes you wish to log into:</property> <property name="use_markup">True</property> <property name="wrap">True</property> </object> @@ -706,6 +703,17 @@ </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="nodePortal"> + <property name="title" translatable="yes">Portal</property> + <child> + <object class="GtkCellRendererText" id="nodePortalRenderer"/> + <attributes> + <attribute name="text">4</attribute> + </attributes> + </child> + </object> + </child> </object> </child> </object> @@ -726,7 +734,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login">_Node login authentication type:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login">_Node Login Authentication Type:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">loginAuthTypeCombo</property> </object> @@ -778,7 +786,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|CHAP">CHAP _Username:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|CHAP">CHAP _User name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">loginChapUsernameEntry</property> </object> @@ -806,7 +814,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_login_field_changed" swapped="no"/> </object> <packing> @@ -847,7 +854,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_login_field_changed" swapped="no"/> </object> <packing> @@ -874,7 +880,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_login_field_changed" swapped="no"/> </object> <packing> @@ -901,7 +906,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|Reverse CHAP">CHAP _Username:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|Reverse CHAP">CHAP _User name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">loginRchapUsernameEntry</property> </object> @@ -929,7 +934,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|Reverse CHAP">_Reverse CHAP Username:</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|iSCSI|Login|Reverse CHAP">_Reverse CHAP User name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">loginRchapReverseUsername</property> </object> diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.py b/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.py index 987b0eb..29d738d 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.py +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/iscsi.py @@ -40,7 +40,7 @@ Credentials = namedtuple("Credentials", ["style", "targetIP", "initiator", "username", "password", "rUsername", "rPassword"]) -NodeStoreRow = namedtuple("NodeStoreRow", ["selected", "notLoggedIn", "name", "iface"]) +NodeStoreRow = namedtuple("NodeStoreRow", ["selected", "notLoggedIn", "name", "iface", "portal"]) def discover_no_credentials(builder): return Credentials(STYLE_NONE, @@ -234,7 +234,6 @@ class ISCSIDialog(GUIObject): # that subscreen. self._add_nodes(self._discoveredNodes) self._iscsiNotebook.set_current_page(1) - self._okButton.set_sensitive(True) # If some form of login credentials were used for discovery, # default to using the same for login. @@ -303,7 +302,8 @@ class ISCSIDialog(GUIObject): text = widget.get_text() stripped = text.strip() - return "." in stripped and ":" in stripped + #iSCSI Naming Standards: RFC 3720 and RFC 3721 + return "." in stripped def on_discover_field_changed(self, *args): # Make up a credentials object so we can test if it's valid. @@ -318,7 +318,8 @@ class ISCSIDialog(GUIObject): def _add_nodes(self, nodes): for node in nodes: iface = self.iscsi.ifaces.get(node.iface, node.iface) - self._store.append([False, True, node.name, iface]) + portal = "%s:%s" % (node.address, node.port) + self._store.append([False, True, node.name, iface, portal]) # We should select the first node by default. self._store[0][0] = True @@ -347,7 +348,8 @@ class ISCSIDialog(GUIObject): continue for node in self._discoveredNodes: - if obj.notLoggedIn and node.name == obj.name: + if obj.notLoggedIn and node.name == obj.name \ + and obj.portal == "%s:%s" % (node.address, node.port): # when binding interfaces match also interface if self.iscsi.ifaces and \ obj.iface != self.iscsi.ifaces[node.iface]: diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.glade b/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.glade new file mode 100644 index 0000000..56b95a5 --- /dev/null +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.glade @@ -0,0 +1,445 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <object class="GtkListStore" id="nodeStore"> + <columns> + <!-- column-name nodeSelected --> + <column type="gboolean"/> + <!-- column-name nodeNotLoggedIn --> + <column type="gboolean"/> + <!-- column-name nodeName --> + <column type="gchararray"/> + <!-- column-name nodeIface --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkTreeModelFilter" id="nodeStoreFiltered"> + <property name="child_model">nodeStore</property> + </object> + <object class="GtkDialog" id="zfcpDialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <property name="decorated">False</property> + <child internal-child="vbox"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkButtonBox" id="dialog-action_area1"> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="cancelButton"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="okButton"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="box1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">ADD zFCP STORAGE TARGET</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="zfcpNotebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="show_tabs">False</property> + <property name="show_border">False</property> + <child> + <object class="GtkGrid" id="configureGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkLabel" id="label5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_bottom">6</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">To use zFCP disks, you must provide the device number, WWPN, and LUN configured for the device.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + <property name="width">3</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="deviceEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes" context="GUI|Advanced Storage|zFCP|Device Number">_Device number:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">deviceEntry</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="wwpnEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">WWPN:</property> + <property name="use_underline">False</property> + <property name="mnemonic_widget">wwpnEntry</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="conditionNotebook"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="valign">center</property> + <property name="show_tabs">False</property> + <property name="show_border">False</property> + <child> + <object class="GtkButton" id="startButton"> + <property name="label" translatable="yes" context="GUI|Advanced Storage|zFCP|Start Discovery">_Start Discovery</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_start_clicked" swapped="no"/> + </object> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="waitBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="spacing">5</property> + <child> + <object class="GtkSpinner" id="waitSpinner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Discovering zFCP devices. This may take a moment...</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkGrid" id="errorBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkImage" id="image3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="icon_name">dialog-error</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Device discovery failed.</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">The following error occurred discovering zFCP devices. Please double check your configuration information and try again.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="deviceErrorLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label">Error message goes here.</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="retryButton"> + <property name="label" translatable="yes" context="GUI|Advanced Storage|zFCP|Retry Discovery">_Retry Discovery</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_start_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">7</property> + <property name="width">3</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">LUN:</property> + <property name="use_underline">False</property> + <property name="mnemonic_widget">lunEntry</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + <property name="width">1</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="lunEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">cancelButton</action-widget> + <action-widget response="1">okButton</action-widget> + </action-widgets> + </object> +</interface> diff --git a/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.py b/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.py new file mode 100644 index 0000000..0674b1f --- /dev/null +++ b/anaconda/pyanaconda/ui/gui/spokes/advstorage/zfcp.py @@ -0,0 +1,148 @@ +# zFCP configuration dialog +# +# Copyright (C) 2013 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Samantha N. Bueno <sbueno@redhat.com> +# + +from pyanaconda.ui.gui import GUIObject +from pyanaconda.ui.gui.utils import gtk_action_nowait + +from blivet.zfcp import ZFCPDevice + +__all__ = ["ZFCPDialog"] + +class ZFCPDialog(GUIObject): + """ Gtk dialog which allows users to manually add zFCP devices without + having previously specified them in a parm file. + """ + builderObjects = ["zfcpDialog"] + mainWidgetName = "zfcpDialog" + uiFile = "spokes/advstorage/zfcp.glade" + + def __init__(self, data, storage): + GUIObject.__init__(self, data) + self.storage = storage + self.zfcp = self.storage.zfcp() + + self._discoveryError = None + + self._update_devicetree = False + + # grab all of the ui objects + self._zfcpNotebook = self.builder.get_object("zfcpNotebook") + + self._configureGrid = self.builder.get_object("configureGrid") + self._conditionNotebook = self.builder.get_object("conditionNotebook") + + self._startButton = self.builder.get_object("startButton") + self._okButton = self.builder.get_object("okButton") + self._cancelButton = self.builder.get_object("cancelButton") + + self._deviceEntry = self.builder.get_object("deviceEntry") + self._wwpnEntry = self.builder.get_object("wwpnEntry") + self._lunEntry = self.builder.get_object("lunEntry") + + def refresh(self): + self._deviceEntry.set_text("") + self._deviceEntry.set_sensitive(True) + self._startButton.set_sensitive(True) + + def run(self): + rc = self.window.run() + self.window.destroy() + # We need to call this to get the device nodes to show up + # in our devicetree. + if self._update_devicetree: + self.storage.devicetree.populate() + return rc + + def _set_configure_sensitive(self, sensitivity): + """ Set entries to a given sensitivity. """ + for child in self._configureGrid.get_children(): + child.set_sensitive(sensitivity) + + def on_start_clicked(self, *args): + """ Go through the process of validating entry contents and then + attempt to add the device. + """ + # First update widgets + self._startButton.hide() + self._cancelButton.set_sensitive(False) + self._okButton.set_sensitive(False) + + self._conditionNotebook.set_current_page(1) + self._set_configure_sensitive(False) + self._deviceEntry.set_sensitive(False) + + # Make a zFCP object with some dummy credentials so we can validate our + # actual input + self._conditionNotebook.set_current_page(1) + dev = ZFCPDevice("0.0.0000", "0x0000000000000000", "0x0000000000000000") + # below really, really is ugly and needs to be re-factored, but this + # should give a good base idea as far as expected behavior should go + try: + device = dev.sanitizeDeviceInput(self._deviceEntry.get_text()) + wwpn = dev.sanitizeWWPNInput(self._wwpnEntry.get_text()) + lun = dev.sanitizeFCPLInput(self._lunEntry.get_text()) + except ValueError as e: + _config_error = str(e) + self.builder.get_object("deviceErrorLabel").set_text(_config_error) + self._conditionNotebook.set_current_page(2) + + spinner = self.builder.get_object("waitSpinner") + spinner.start() + + self._discover(device, wwpn, lun) + self._check_discover() + + @gtk_action_nowait + def _check_discover(self, *args): + """ After the zFCP discover thread runs, check to see whether a valid + device was discovered. Display an error message if not. + """ + + spinner = self.builder.get_object("waitSpinner") + spinner.stop() + + if self._discoveryError: + # Failure, display a message and leave the user on the dialog so + # they can try again (or cancel) + self.builder.get_object("deviceErrorLabel").set_text(self._discoveryError) + self._discoveryError = None + self._conditionNotebook.set_current_page(2) + self._set_configure_sensitive(True) + else: + # Great success. Just return to the advanced storage window and let the + # UI update with the newly-added device + self.window.response(1) + return True + + self._cancelButton.set_sensitive(True) + return False + + def _discover(self, *args): + """ Given the configuration options from a user, attempt to discover + a zFCP device. This includes searching black-listed devices. + """ + # attempt to add the device + try: + self.zfcp.addFCP(args[0], args[1], args[2]) + self._update_devicetree = True + except ValueError as e: + self._discoveryError = str(e) + return diff --git a/anaconda/pyanaconda/ui/gui/spokes/custom.glade b/anaconda/pyanaconda/ui/gui/spokes/custom.glade index 2cf6e70..8eaf30c 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/custom.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/custom.glade @@ -108,9 +108,6 @@ <property name="margin_left">6</property> <property name="margin_right">6</property> <property name="margin_top">6</property> - <child> - <placeholder/> - </child> </object> </child> </object> @@ -376,7 +373,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">start</property> - <property name="invisible_char">●</property> <property name="width_chars">20</property> <property name="completion">mountPointCompletion</property> <signal name="changed" handler="on_value_changed" swapped="no"/> @@ -424,7 +420,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">start</property> - <property name="invisible_char">●</property> <property name="width_chars">10</property> <signal name="changed" handler="on_value_changed" swapped="no"/> </object> @@ -477,7 +472,6 @@ <property name="can_focus">True</property> <property name="halign">start</property> <property name="max_length">20</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_value_changed" swapped="no"/> </object> <packing> @@ -586,7 +580,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">start</property> - <property name="invisible_char">●</property> <property name="width_chars">20</property> <signal name="changed" handler="on_value_changed" swapped="no"/> </object> @@ -645,7 +638,9 @@ <signal name="changed" handler="on_device_type_changed" swapped="no"/> <signal name="changed" handler="on_value_changed" swapped="no"/> <child> - <object class="GtkCellRendererText" id="deviceTypeRenderer"/> + <object class="GtkCellRendererText" id="deviceTypeRenderer"> + <property name="ellipsize">end</property> + </object> <attributes> <attribute name="text">0</attribute> </attributes> @@ -817,7 +812,9 @@ <signal name="changed" handler="on_container_changed" swapped="no"/> <signal name="changed" handler="on_value_changed" swapped="no"/> <child> - <object class="GtkCellRendererText" id="descRenderer"/> + <object class="GtkCellRendererText" id="descRenderer"> + <property name="ellipsize">middle</property> + </object> <attributes> <attribute name="text">0</attribute> </attributes> @@ -900,7 +897,9 @@ <property name="model">raidStoreFiltered</property> <signal name="changed" handler="on_value_changed" swapped="no"/> <child> - <object class="GtkCellRendererText" id="raidLevelRenderer"/> + <object class="GtkCellRendererText" id="raidLevelRenderer"> + <property name="ellipsize">end</property> + </object> <attributes> <attribute name="markup">0</attribute> </attributes> diff --git a/anaconda/pyanaconda/ui/gui/spokes/custom.py b/anaconda/pyanaconda/ui/gui/spokes/custom.py index 77f3414..d0aee9c 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/custom.py +++ b/anaconda/pyanaconda/ui/gui/spokes/custom.py @@ -35,6 +35,7 @@ from pyanaconda.i18n import _, N_, CP_ from pyanaconda.product import productName, productVersion, translated_new_install_name from pyanaconda.threads import AnacondaThread, threadMgr from pyanaconda.constants import THREAD_EXECUTE_STORAGE, THREAD_STORAGE, THREAD_CUSTOM_STORAGE_INIT +from pyanaconda.constants import SIZE_UNITS_DEFAULT from pyanaconda.iutil import lowerASCII from pyanaconda.bootloader import BootLoaderError from pyanaconda.kickstart import refreshAutoSwapSize @@ -45,7 +46,6 @@ from blivet.formats import device_formats from blivet.formats import getFormat from blivet.formats.fs import FS from blivet.size import Size -from blivet import Root from blivet.devicefactory import DEVICE_TYPE_LVM from blivet.devicefactory import DEVICE_TYPE_BTRFS from blivet.devicefactory import DEVICE_TYPE_PARTITION @@ -53,8 +53,8 @@ from blivet.devicefactory import DEVICE_TYPE_MD from blivet.devicefactory import DEVICE_TYPE_DISK from blivet.devicefactory import DEVICE_TYPE_LVM_THINP from blivet.devicefactory import SIZE_POLICY_AUTO -from blivet import findExistingInstallations -from blivet.partitioning import doAutoPartition +from blivet.osinstall import findExistingInstallations, Root +from blivet.autopart import doAutoPartition from blivet.errors import StorageError from blivet.errors import NoDisksError from blivet.errors import NotEnoughFreeSpaceError @@ -80,14 +80,15 @@ from pyanaconda.ui.gui.spokes.lib.summary import ActionSummaryDialog from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import size_from_entry from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import validate_label, validate_mountpoint, get_raid_level from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import selectedRaidLevel, raidLevelSelection, defaultRaidLevel, requiresRaidSelection, containerRaidLevelsSupported, raidLevelsSupported, defaultContainerRaidLevel -from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import get_container_type_name, RAID_NOT_ENOUGH_DISKS +from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import get_container_type, RAID_NOT_ENOUGH_DISKS from pyanaconda.ui.gui.spokes.lib.custom_storage_helpers import AddDialog, ConfirmDeleteDialog, DisksDialog, ContainerDialog from pyanaconda.ui.gui.utils import setViewportBackground, fancy_set_sensitive, ignoreEscape -from pyanaconda.ui.gui.utils import really_hide, really_show, GtkActionList, timed_action +from pyanaconda.ui.gui.utils import really_hide, really_show, timed_action from pyanaconda.ui.categories.system import SystemCategory from gi.repository import Gdk, Gtk +from gi.repository import BlockDev as blockdev from gi.repository.AnacondaWidgets import MountpointSelector from functools import wraps @@ -112,6 +113,13 @@ UNRECOVERABLE_ERROR_MSG = N_("Storage configuration reset due to unrecoverable " "error. Click for details.") def dev_type_from_const(dev_type_const): + """ Return integer corresponding to name for device type defined as + a constant in blivet.devicefactory. + + :param str dev_type_const: the name of a DEVICE_TYPE_* + :returns: the corresponding integer code, if there is one + :rtype: int or NoneType + """ return getattr(devicefactory, dev_type_const, None) def ui_storage_logged(func): @@ -134,6 +142,12 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): category = SystemCategory title = N_("MANUAL PARTITIONING") + # The maximum number of places to show when displaying a size + MAX_SIZE_PLACES = 2 + + # If the user enters a smaller size, the GUI changes it to this value + MIN_SIZE_ENTRY = Size("1 MiB") + def __init__(self, data, storage, payload, instclass): StorageChecker.__init__(self, min_ram=isys.MIN_GUI_RAM) NormalSpoke.__init__(self, data, storage, payload, instclass) @@ -147,10 +161,9 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._devices = [] self._error = None self._hidden_disks = [] - self._fs_types = [] # list of supported fstypes + self._fs_types = set() # set of supported fstypes self._free_space = Size(0) - self._device_size_text = None self._device_disks = [] self._device_container_name = None self._device_container_raid_level = None @@ -201,6 +214,10 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._partitionsViewport = self.builder.get_object("partitionsViewport") self._partitionsNotebook = self.builder.get_object("partitionsNotebook") + # Connect partitionsNotebook focus events to scrolling in the parent viewport + partitionsNotebookViewport = self.builder.get_object("partitionsNotebookViewport") + self._partitionsNotebook.set_focus_vadjustment(partitionsNotebookViewport.get_vadjustment()) + self._whenCreateLabel = self.builder.get_object("whenCreateLabel") self._availableSpaceLabel = self.builder.get_object("availableSpaceLabel") @@ -231,6 +248,15 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._containerStore = self.builder.get_object("containerStore") self._deviceDescLabel = self.builder.get_object("deviceDescLabel") + # Set the fixed-size properties on the volume group ComboBox renderers to + # False so that the "Create a new..." row can overlap with the free space + # on the other rows. These properties are not accessible from glade. + cell_area = self._containerCombo.get_area() + descRenderer = self.builder.get_object("descRenderer") + freeSpaceRenderer = self.builder.get_object("freeSpaceRenderer") + cell_area.cell_set_property(descRenderer, "fixed-size", False) + cell_area.cell_set_property(freeSpaceRenderer, "fixed-size", False) + self._passphraseEntry = self.builder.get_object("passphraseEntry") # Stores @@ -260,17 +286,19 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._accordion = Accordion() self._partitionsViewport.add(self._accordion) - # Populate the list of valid filesystem types from the format classes. - # Unfortunately, we have to narrow them down a little bit more because - # this list will include things like PVs and RAID members. - self._fsCombo.remove_all() + # Connect viewport scrolling with accordion focus events + self._accordion.set_focus_hadjustment(self._partitionsViewport.get_hadjustment()) + self._accordion.set_focus_vadjustment(self._partitionsViewport.get_vadjustment()) threadMgr.add(AnacondaThread(name=THREAD_CUSTOM_STORAGE_INIT, target=self._initialize)) def _initialize(self): - self._fs_types = [] - actions = GtkActionList() - for cls in device_formats.itervalues(): + """ Populate the set of valid filesystem types from the format classes. + + Restrict the set to ones that we might allow users to select. + """ + _fs_types = [] + for cls in device_formats.values(): obj = cls() # btrfs is always handled by on_device_type_changed @@ -280,10 +308,9 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): (isinstance(obj, FS) or obj.type in ["biosboot", "prepboot", "swap"])) if supported_fs: - actions.add_action(self._fsCombo.append_text, obj.name) - self._fs_types.append(obj.name) + _fs_types.append(obj.name) - actions.fire() + self._fs_types = set(_fs_types) @property def _clearpartDevices(self): @@ -293,7 +320,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): def unusedDevices(self): unused_devices = [d for d in self._storage_playground.unusedDevices if d.disks and d.mediaPresent and - not d.partitioned and d.direct] + not d.partitioned and (d.direct or d.isleaf)] # add incomplete VGs and MDs incomplete = [d for d in self._storage_playground.devicetree._devices if not getattr(d, "complete", True)] @@ -492,7 +519,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): page = Page(root.name) - for (mountpoint, device) in root.mounts.iteritems(): + for (mountpoint, device) in root.mounts.items(): if device not in self._devices or \ not device.disks or \ (root.name != translated_new_install_name() and not device.format.exists): @@ -636,7 +663,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): elif encrypted and new_fs_type in PARTITION_ONLY_FORMAT_TYPES: error = _("%s cannot be encrypted") % new_fs_type elif mountpoint == "/" and device.format.exists and not reformat: - error = _("You must create a new filesystem on the root device.") + error = _("You must create a new file system on the root device.") if not error and \ (raid_level is not None or requiresRaidSelection(device_type)) and \ @@ -672,7 +699,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._update_all_devices_in_selectors() self._error = e self.set_warning(_(DEVICE_CONFIGURATION_ERROR_MSG)) - self.window.show_all() if not removed_device: # nothing more to do @@ -692,11 +718,17 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self.refresh() # this calls self.clear_errors self._error = e self.set_warning(_(UNRECOVERABLE_ERROR_MSG)) - self.window.show_all() return False @ui_storage_logged - def _revert_reformat(self, device, use_dev): + def _revert_reformat(self, device): + """ Revert reformat. + + :param device: the device being displayed + :type device: :class:`blivet.devices.StorageDevice` + """ + use_dev = device.raw_device + # figure out the existing device and reset it if not use_dev.format.exists: original_device = use_dev @@ -706,54 +738,62 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): log.debug("resetting device %s", original_device.name) self._storage_playground.resetDevice(original_device) - def _bound_size(self, size, device): - # If no size was specified, we just want to grow to the maximum. - # But resizeDevice doesn't take None for a value. - if not size: - size = device.maxSize - elif size < device.minSize: - size = device.minSize - elif size > device.maxSize: - size = device.maxSize - - return size - @ui_storage_logged - def _handle_size_change(self, size, old_size, device, use_dev): + def _handle_size_change(self, size, old_size, device): + """ Handle size change. + + :param device: the device being displayed + :type device: :class:`blivet.devices.StorageDevice` + """ + use_dev = device.raw_device + + # If a LUKS device is being displayed, adjust the size + # to the appropriate size for the raw device. + use_size = size + use_old_size = old_size + if use_dev is not device: + use_size = size + crypto.LUKS_METADATA_SIZE + use_old_size = use_dev.size + # bound size to boundaries given by the device - size = self._bound_size(size, device) - size = device.alignTargetSize(size) + use_size = use_dev.alignTargetSize(use_size) + use_size = storage_utils.bound_size(use_size, use_dev, use_old_size) + use_size = use_dev.alignTargetSize(use_size) # And then we need to re-check that the max size is actually # different from the current size. _changed_size = False - if size != device.size and size == device.currentSize: + if use_size != device.size and size == device.currentSize: # size has been set back to its original value - actions = self._storage_playground.devicetree.findActions(action_type="resize", - devid=device.id) + actions = self._storage_playground.devicetree.findActions( + action_type="resize", + devid=use_dev.id + ) for action in reversed(actions): self._storage_playground.devicetree.cancelAction(action) _changed_size = True - elif size != device.size: - log.debug("scheduling resize of device %s to %s", device.name, size) + elif use_size != use_dev.size: + log.debug("scheduling resize of device %s to %s", use_dev.name, use_size) try: - self._storage_playground.resizeDevice(device, size) + self._storage_playground.resizeDevice(use_dev, use_size) except StorageError as e: log.error("failed to schedule device resize: %s", e) - device.size = old_size + use_dev.size = use_old_size self._error = e self.set_warning(_("Device resize request failed. " "Click for details.")) - self.window.show_all() else: _changed_size = True if _changed_size: - log.debug("new size: %s", device.size) - log.debug("target size: %s", device.targetSize) + log.debug("new size: %s", use_dev.size) + log.debug("target size: %s", use_dev.targetSize) # update the selector's size property + # The selector shows the visible disk, so it is necessary + # to use device and size, which are the values visible to + # the user. for s in self._accordion.allSelectors: if s._device == device: s.size = str(device.size) @@ -816,7 +856,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._error = e self.set_warning(_("Device reformat request failed. " "Click for details.")) - self.window.show_all() else: # first, remove this selector from any old install page(s) new_selector = None @@ -846,6 +885,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): This method must never trigger a call to self._do_refresh. """ + self.clear_errors() + # check if initialized and have something to operate on if not self._initialized or not selector: return @@ -893,15 +934,18 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # SIZE old_size = device.size - # we are interested in size human readable representation change because - # that's what the user sees - same_size = self._device_size_text == self._sizeEntry.get_text() - if same_size: + # If the size text hasn't changed at all from that displayed, + # assume no change intended. + if old_size.humanReadable(max_places=self.MAX_SIZE_PLACES) == self._sizeEntry.get_text(): size = old_size else: - size = size_from_entry(self._sizeEntry) + size = size_from_entry( + self._sizeEntry, + lower_bound=self.MIN_SIZE_ENTRY, + units=SIZE_UNITS_DEFAULT + ) changed_size = ((use_dev.resizable or not use_dev.exists) and - not same_size) + size != old_size) old_device_info["size"] = old_size new_device_info["size"] = size @@ -928,7 +972,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # ENCRYPTION old_encrypted = isinstance(device, LUKSDevice) - encrypted = self._encryptCheckbox.get_active() + encrypted = self._encryptCheckbox.get_active() and self._encryptCheckbox.is_sensitive() changed_encryption = (old_encrypted != encrypted) old_device_info["encrypted"] = old_encrypted new_device_info["encrypted"] = encrypted @@ -944,7 +988,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if error: self._error = error self.set_warning(self._error) - self.window.show_all() self._populate_right_side(selector) return @@ -966,7 +1009,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if error: self._error = error self.set_warning(self._error) - self.window.show_all() self._populate_right_side(selector) return @@ -982,7 +1024,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): changed_raid_level = (old_device_type == device_type and device_type in (DEVICE_TYPE_MD, DEVICE_TYPE_BTRFS) and - old_raid_level != raid_level) + old_raid_level is not raid_level) old_device_info["raid_level"] = old_raid_level new_device_info["raid_level"] = raid_level @@ -994,14 +1036,13 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): raid_level) if error: self.set_warning(error) - self.window.show_all() self._populate_right_side(selector) return # If the device is a btrfs volume, the only things we can set/update # are mountpoint and container-wide settings. if device_type == DEVICE_TYPE_BTRFS and hasattr(use_dev, "subvolumes"): - size = 0 + size = Size(0) changed_size = False encrypted = False changed_encryption = False @@ -1092,9 +1133,10 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): changed_container or changed_container_encrypted or changed_container_raid_level or changed_container_size) - ## - ## Handle changes to an existing device - ## + # If something has changed but the device does not exist, + # there is no need to schedule actions on the device. + # It is only necessary to create a new device object + # which reflects the current choices. if not use_dev.exists: if not changed: log.debug("nothing changed for new device") @@ -1138,11 +1180,11 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # a reformat. if not reformat and (not use_dev.format.exists or not device.format.exists): - self._revert_reformat(device, use_dev) + self._revert_reformat(device) # Handle size change - if changed_size and device.resizable: - self._handle_size_change(size, old_size, device, use_dev) + if changed_size: + self._handle_size_change(size, old_size, device) # it's possible that reformat is active but fstype is unchanged, in # which case we're not going to schedule another reformat unless @@ -1179,14 +1221,19 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # if changed_name: self.clear_errors() - use_dev._name = name - new_name = use_dev.name - log.debug("changing name of %s to %s", old_name, new_name) - if new_name in self._storage_playground.names: - use_dev._name = old_name - self.set_info(_("Specified name %s already in use.") % new_name) + try: + use_dev.name = name + except ValueError as e: + self._error = e + self.set_error(_("Invalid device name: %s") % name) else: - updateSelectorFromDevice(selector, device) + new_name = use_dev.name + log.debug("changing name of %s to %s", old_name, new_name) + if new_name in self._storage_playground.names: + use_dev.name = old_name + self.set_info(_("Specified name %s already in use.") % new_name) + else: + updateSelectorFromDevice(selector, device) self._populate_right_side(selector) @@ -1219,13 +1266,28 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): map(really_show, [self._raidLevelLabel, self._raidLevelCombo]) - def _get_current_device_type(self): + def _get_current_device_type_name(self): + """ Return name for type combo selection. + + :returns: the corresponding name extracted from the combo + :rtype: str or NoneType + """ itr = self._typeCombo.get_active_iter() if not itr: return None # we have the constant name in the second column of the store - return dev_type_from_const(self._typeStore[itr][1]) + return self._typeStore[itr][1] + + def _get_current_device_type(self): + """ Return integer for type combo selection. + + :returns: the corresponding integer code, a constant in + blivet.devicefactory. + :rtype: int or NoneType + """ + device_type_name = self._get_current_device_type_name() + return dev_type_from_const(device_type_name) if device_type_name else None def _update_container_info(self, use_dev): if hasattr(use_dev, "vg"): @@ -1249,34 +1311,66 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): or defaultContainerRaidLevel(devicefactory.get_device_type(use_dev)) def _setup_fstype_combo(self, device): - # remove any fs types that aren't supported - remove_indices = [] - for idx, row in enumerate(self._fsCombo.get_model()): - fs_type = row[0] - if fs_type not in self._fs_types: - remove_indices.append(idx) - continue + """ Setup the filesystem combo box. - if fs_type == device.format.name: - self._fsCombo.set_active(idx) + :param device: blivet.devices.Device instance + """ + type_name = device.format.name - # remove items from the combobox in reversed order so that item 3 - # doesn't become item 2 by removing item 1 etc. - map(self._fsCombo.remove, reversed(remove_indices)) - - # if the current device has unsupported formatting, add an entry for it - if device.format.name not in self._fs_types: - self._fsCombo.append_text(device.format.name) - self._fsCombo.set_active(len(self._fsCombo.get_model()) - 1) - - # Give them a way to reset to original formatting. Whenever we add a - # "reformat this" widget this will need revisiting. + # Possibly unsupported but still required filesystem names if device.exists and \ device.format.type != device.originalFormat.type and \ device.originalFormat.type not in self._fs_types: - self._fsCombo.append_text(device.originalFormat.name) + extra_names = (type_name, device.originalFormat.name) + else: + extra_names = (type_name,) + + names = list(self._fs_types.union(extra_names)) + names.sort() + + # Add all desired fileystem type names to the box, sorted alphabetically + self._fsCombo.remove_all() + for ty in names: + self._fsCombo.append_text(ty) + + # set the active filesystem type + idx = next(i for i, data in enumerate(self._fsCombo.get_model()) if data[0] == type_name) + self._fsCombo.set_active(idx) + + # do additional updating handled by other method + self._update_fstype_combo(devicefactory.get_device_type(device)) + + def _btrfs_in_typecombo(self, device): + """ Whether BTRFS should appear in device type combo box. + + :param device: the device being displayed + :type device: :class:`blivet.devices.StorageDevice` + :rtype: bool + :returns: True if BTRFS should appear, otherwise False + """ + device = device.raw_device + + # The device is btrfs, so btrfs must be shown. + if device.format.type == "btrfs": + return True + + # Return True if btrfs filesystem is both allowed and supported. + fmt = getFormat("btrfs") + return fmt.supported and fmt.formattable and \ + device.format.type not in PARTITION_ONLY_FORMAT_TYPES + ("swap",) + + def _setup_device_type_combo(self, device, device_name): + """ Set up device type combo. + + :param device: the device + :type device: :class:`blivet.devices.StorageDevice` + :param str device_name: the device name + + :returns: the device type that was decided on + :rtype: int (an enumeration defined in blivet.devicefactory) + """ + use_dev = device.raw_device - def _setup_device_type_combo(self, device, use_dev, device_name): # these device types should always be listed should_appear = {"DEVICE_TYPE_PARTITION", "DEVICE_TYPE_LVM", "DEVICE_TYPE_LVM_THINP"} @@ -1284,8 +1378,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if (use_dev.type == "mdarray" or len(self._clearpartDevices) > 1): should_appear.add("DEVICE_TYPE_MD") - # if the format is swap the device type can't be btrfs - if use_dev.format.type not in PARTITION_ONLY_FORMAT_TYPES + ("swap",): + if self._btrfs_in_typecombo(device): should_appear.add("DEVICE_TYPE_BTRFS") # only include disk if the current device is a disk @@ -1311,7 +1404,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): device_type = devicefactory.get_device_type(device) - for _type in self._device_name_dict.iterkeys(): + for _type in self._device_name_dict.keys(): if _type == device_type: self._device_name_dict[_type] = device_name continue @@ -1391,8 +1484,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._labelEntry.set_text("") fancy_set_sensitive(self._labelEntry, True) - self._device_size_text = device.size.humanReadable(max_places=2) - self._sizeEntry.set_text(self._device_size_text) + self._sizeEntry.set_text(device.size.humanReadable(max_places=self.MAX_SIZE_PLACES)) self._reformatCheckbox.set_active(not device.format.exists) fancy_set_sensitive(self._reformatCheckbox, @@ -1406,12 +1498,16 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # The encryption checkbutton should not be sensitive if there is # existing encryption below the leaf layer. self._encryptCheckbox.set_sensitive(False) + self._encryptCheckbox.set_active(True) + self._encryptCheckbox.set_tooltip_text(_("The container is encrypted.")) + else: + self._encryptCheckbox.set_tooltip_text("") # Set up the filesystem type combo. self._setup_fstype_combo(device) # Set up the device type combo. - device_type = self._setup_device_type_combo(device, use_dev, device_name) + device_type = self._setup_device_type_combo(device, device_name) fancy_set_sensitive(self._fsCombo, self._reformatCheckbox.get_active() and device_type != DEVICE_TYPE_BTRFS) @@ -1439,7 +1535,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if self._sizeEntry.get_sensitive(): self._sizeEntry.props.has_tooltip = False elif device.format.type == "btrfs": - self._sizeEntry.set_tooltip_text(_("The space available to this mountpoint can be changed by modifying the volume below.")) + self._sizeEntry.set_tooltip_text(_("The space available to this mount point can be changed by modifying the volume below.")) else: self._sizeEntry.set_tooltip_text(_("This file system may not be resized.")) @@ -1501,7 +1597,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # on_info_bar_clicked requires self._error to be set, so set it to the # list of all errors and warnings that storage checking found. - self.window.show_all() self._error = "\n".join(self.errors + self.warnings) return self._error == "" @@ -1512,6 +1607,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # Save anything from the currently displayed mountpoint. self._save_right_side(self._current_selector) + self._applyButton.set_sensitive(False) # And then display the summary screen. From there, the user will either # head back to the hub, or stay on the custom screen. @@ -1606,7 +1702,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self.set_info(_("Added new %(type)s to existing " "container %(name)s.") % {"type" : type_str, "name" : container.name}) - self.window.show_all() e = None # the factory's error handling has replaced all of the devices @@ -1617,12 +1712,10 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._error = e self.set_error(_("Failed to add new device. Click for " "details.")) - self.window.show_all() except OverflowError as e: log.error("invalid size set for partition") self._error = e self.set_error(_("Invalid partition size set. Use a valid integer.")) - self.window.show_all() def on_add_clicked(self, button): self._save_right_side(self._current_selector) @@ -1651,8 +1744,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): dev_info["mountpoint"] = dialog.mountpoint log.debug("requested size = %s ; available space = %s", dialog.size, self._free_space) - # if no size was entered, request as much of the free space as possible - if dialog.size is not None and dialog.size.convertTo(spec="mb") < 1: + # if no requested size, or size less than 1 MB, request maximum size + if dialog.size is None or dialog.size < Size("1 MB"): dev_info["size"] = None else: dev_info["size"] = dialog.size @@ -1731,7 +1824,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._error = e self.set_warning(_("Device removal request failed. Click " "for details.")) - self.window.show_all() else: if is_logical_partition: self._storage_playground.removeEmptyExtendedPartitions() @@ -1898,7 +1990,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if not disks: self._error = "No disks selected. Keeping previous disk set." self.set_info(self._error) - self.window.show_all() return if set(disks) != self._device_disks: @@ -1957,7 +2048,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if not disks: self._error = "No disks selected. Not saving changes." self.set_info(self._error) - self.window.show_all() return log.debug("new container name: %s", name) @@ -1965,7 +2055,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._error = _("Volume Group name %s is already in use. Not " "saving changes.") % name self.set_info(self._error) - self.window.show_all() return if (set(disks) != set(self._device_disks) or @@ -2008,6 +2097,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): log.debug("%s -> %s", container_name, self._device_container_name) if container_name == self._device_container_name: + self.on_update_settings_clicked(None) return log.debug("renaming container %s to %s", container_name, self._device_container_name) @@ -2017,10 +2107,17 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._storage_playground.devicetree.names.remove(container.name) self._storage_playground.devicetree.names.append(self._device_container_name) - # until there's a setter for btrfs volume name - container._name = self._device_container_name - if container.format.type == "btrfs": - container.format.label = self._device_container_name + try: + container.name = self._device_container_name + except ValueError as e: + self._error = e + self.set_error(_("Invalid device name: %s") % self._device_container_name) + self._device_container_name = container_name + self.on_update_settings_clicked(None) + return + else: + if container.format.type == "btrfs": + container.format.label = self._device_container_name container_exists = getattr(container, "exists", False) @@ -2032,6 +2129,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): else: # no match found, just update selectors and return self._update_selectors() + self.on_update_settings_clicked(None) return c = self._storage_playground.devicetree.getDeviceByName(self._device_container_name) @@ -2045,6 +2143,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._containerStore.remove(self._containerStore.get_iter_from_string("%s" % (idx + 1))) self._update_selectors() + self.on_update_settings_clicked(None) def on_container_changed(self, combo): ndx = combo.get_active() @@ -2061,8 +2160,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): return device_type = self._get_current_device_type() - container_type = get_container_type_name(device_type).lower() - new_text = _(NEW_CONTAINER_TEXT) % {"container_type": container_type} + container_type_name = get_container_type(device_type).name.lower() + new_text = _(NEW_CONTAINER_TEXT) % {"container_type": container_type_name} create_new_container = container_name == new_text if create_new_container: # run the vg editor dialog with a default name and disk set @@ -2220,20 +2319,17 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): log.error("doAutoPartition failed: %s", e) self._error = e self.set_error(_("No disks selected.")) - self.window.show_all() except NotEnoughFreeSpaceError as e: # No handling should be required for this. log.error("doAutoPartition failed: %s", e) self._error = e self.set_error(_("Not enough free space on selected disks.")) - self.window.show_all() except (StorageError, BootLoaderError) as e: log.error("doAutoPartition failed: %s", e) self._reset_storage() self._error = e self.set_error(_("Automatic partitioning failed. Click " "for details.")) - self.window.show_all() else: self._devices = self._storage_playground.devices # mark all new containers for automatic size management @@ -2259,7 +2355,6 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._error = messages self.set_error(_("Automatic partitioning failed. Click " "for details.")) - self.window.show_all() def on_create_clicked(self, button, autopartTypeCombo): # Then do autopartitioning. We do not do any clearpart first. This is @@ -2278,7 +2373,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): def on_reformat_toggled(self, widget): active = widget.get_active() - self._encryptCheckbox.set_sensitive(active) + encrypt_sensitive = active if self._current_selector: device = self._current_selector.device.raw_device @@ -2287,14 +2382,14 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if any(a.format.type == "luks" and a.format.exists for a in ancestors): # The encryption checkbutton should not be sensitive if there is # existing encryption below the leaf layer. - self._encryptCheckbox.set_sensitive(False) + encrypt_sensitive = False # you can't encrypt a btrfs subvolume -- only the volume/container device_type = self._get_current_device_type() if device_type == DEVICE_TYPE_BTRFS: self._encryptCheckbox.set_active(False) - self._encryptCheckbox.set_sensitive(device_type != DEVICE_TYPE_BTRFS) + self._encryptCheckbox.set_sensitive(encrypt_sensitive and device_type != DEVICE_TYPE_BTRFS) fancy_set_sensitive(self._fsCombo, active) def on_fs_type_changed(self, combo): @@ -2341,8 +2436,9 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): if container: container_size_policy = container.size_policy - container_type_text = get_container_type_name(device_type) - self._containerLabel.set_text(container_type_text.title()) + container_type = get_container_type(device_type) + self._containerLabel.set_text(container_type.label.title()) + self._containerLabel.set_use_underline(True) self._containerStore.clear() if device_type == DEVICE_TYPE_BTRFS: containers = self._storage_playground.btrfsVolumes @@ -2368,8 +2464,8 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._containerStore.append(self._container_store_row(default_container_name)) self._containerCombo.set_active(len(self._containerStore) - 1) - self._containerStore.append(self._container_store_row(_(NEW_CONTAINER_TEXT) % {"container_type": container_type_text.lower()})) - self._containerCombo.set_tooltip_text(_(CONTAINER_TOOLTIP) % {"container_type": container_type_text.lower()}) + self._containerStore.append(self._container_store_row(_(NEW_CONTAINER_TEXT) % {"container_type": container_type.name.lower()})) + self._containerCombo.set_tooltip_text(_(CONTAINER_TOOLTIP) % {"container_type": container_type.name.lower()}) if default_container_name is None: self._containerCombo.set_active(len(self._containerStore) - 1) @@ -2382,55 +2478,79 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): container_exists = getattr(container, "exists", False) self._modifyContainerButton.set_sensitive(not container_exists) - def _resolve_btrfs_restrictions(self, should_be_btrfs): - model = self._fsCombo.get_model() - btrfs_pos = None - for idx, data in enumerate(model): - if data[0] == "btrfs": - btrfs_pos = idx + def _update_fstype_combo(self, device_type): + """ Set up device type dependent portion of filesystem combo. - active_index = self._fsCombo.get_active() - current_fstype = self._fsCombo.get_active_text() - if btrfs_pos and not should_be_btrfs: - self._fsCombo.remove(btrfs_pos) - if current_fstype == "btrfs": - for i, row in enumerate(model): - if row[0] == self.storage.defaultFSType: - active_index = i - break - elif should_be_btrfs and not btrfs_pos: - self._fsCombo.append_text("btrfs") - active_index = len(self._fsCombo.get_model()) - 1 + :param int device_type: an int representing the device type + + Generally speaking, the filesystem combo can be set up without + reference to the device type because the choice of filesystem + combo and of device type is orthogonal. + + However, choice of btrfs device type requires choice of btrfs + filesystem type, and choice of any other device type precludes + choice of btrfs filesystem type. + + Preconditions are: + * the filesystem combo contains at least the default filesystem + * the default filesystem is not the same as btrfs + * if device_type is DEVICE_TYPE_BTRFS, btrfs is supported + + This method is idempotent, and must remain so. + """ + # Find unique instance of btrfs in fsCombo, if any. + btrfs_idx = next((idx for idx, data in enumerate(self._fsCombo.get_model()) if data[0] == "btrfs"), None) + + if device_type == DEVICE_TYPE_BTRFS: + # If no btrfs entry, add one, and select the new entry + if btrfs_idx is None: + self._fsCombo.append_text("btrfs") + active_index = len(self._fsCombo.get_model()) - 1 + # Otherwise, select the already located btrfs entry + else: + active_index = btrfs_idx + else: + # Get the currently active index + active_index = self._fsCombo.get_active() + + # If there is a btrfs entry, remove and adjust active_index + if btrfs_idx is not None: + self._fsCombo.remove(btrfs_idx) + + # If btrfs previously selected, select default filesystem + if active_index == btrfs_idx: + active_index = next(idx for idx, data in enumerate(self._fsCombo.get_model()) if data[0] == self.storage.defaultFSType) + # Otherwise, shift index left by one if after removed entry + elif active_index > btrfs_idx: + active_index = active_index - 1 + # If there is no btrfs entry, stick with user's previous choice + else: + pass self._fsCombo.set_active(active_index) + fancy_set_sensitive(self._fsCombo, self._reformatCheckbox.get_active() and device_type != DEVICE_TYPE_BTRFS) def on_device_type_changed(self, combo): + if combo is not self._typeCombo: + return + if not self._initialized: return - new_type = self._get_current_device_type() - itr = combo.get_active_iter() - if not itr: - return + # The name of the device type is more informative than the numeric id + new_type = self._get_current_device_type_name() + log.debug("device_type_changed: %s", new_type) - active_dev_type = self._typeStore[itr] - log.debug("device_type_changed: %s %s", new_type, active_dev_type) + # Quit if no device type is selected. if new_type is None: return - # if device type is not btrfs we want to make sure btrfs is not in the - # fstype combo - should_be_btrfs = False - fs_type_sensitive = True + # The numeric id of the device is what is needed by blivet. + new_type = dev_type_from_const(new_type) - raid_level = None - if new_type == DEVICE_TYPE_BTRFS: - # add btrfs to the fstype combo and lock it in - test_fmt = getFormat("btrfs") - should_be_btrfs = test_fmt.supported and test_fmt.formattable - fs_type_sensitive = False - elif new_type == DEVICE_TYPE_MD: - raid_level = defaultRaidLevel(new_type) + # Quit if device type name is unrecognized by blivet. + if new_type is None: + return # lvm uses the RHS to set disk set. no foolish minds here. exists = self._current_selector and self._current_selector.device.exists @@ -2441,17 +2561,14 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): # device type self._raidStoreFilter.refilter() - self._populate_raid(raid_level) + self._populate_raid(defaultRaidLevel(new_type)) self._populate_container() fancy_set_sensitive(self._nameEntry, new_type in NAMED_DEVICE_TYPES) self._nameEntry.set_text(self._device_name_dict[new_type]) fancy_set_sensitive(self._sizeEntry, new_type != DEVICE_TYPE_BTRFS) - fancy_set_sensitive(self._fsCombo, self._reformatCheckbox.get_active() and - fs_type_sensitive) - - self._resolve_btrfs_restrictions(should_be_btrfs) + self._update_fstype_combo(new_type) def clear_errors(self): self._error = None @@ -2537,7 +2654,7 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): try: device.setup() device.format.setup() - except StorageError as e: + except (StorageError, blockdev.CryptoError) as e: log.error("failed to unlock %s: %s", device.name, e) device.teardown(recursive=True) self._error = e @@ -2545,9 +2662,11 @@ class CustomPartitioningSpoke(NormalSpoke, StorageChecker): self._passphraseEntry.set_text("") self.set_warning(_("Failed to unlock encrypted block device. " "Click for details")) - self.window.show_all() return + # set the passphrase also to the originalFormat of the device (a + # different object than '.format', but the same contents) + device.originalFormat.passphrase = passphrase log.info("unlocked %s, now going to populate devicetree...", device.name) with ui_storage_logger(): luks_dev = LUKSDevice(device.format.mapName, diff --git a/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.glade b/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.glade index 236433b..fd5a5ec 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.glade @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.18.3 --> <interface> - <requires lib="gtk+" version="3.2"/> + <requires lib="gtk+" version="3.12"/> <requires lib="AnacondaWidgets" version="1.0"/> <requires lib="TimezoneMap" version="0.4"/> <object class="GtkImage" id="addImage"> @@ -83,6 +83,8 @@ <columns> <!-- column-name hostname --> <column type="gchararray"/> + <!-- column-name pool --> + <column type="gboolean"/> <!-- column-name working --> <column type="gint"/> <!-- column-name use --> @@ -163,7 +165,6 @@ <object class="GtkEntry" id="serverEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <signal name="activate" handler="on_entry_activated" swapped="no"/> <child internal-child="accessible"> <object class="AtkObject" id="serverEntry-atkobject"> @@ -204,6 +205,21 @@ <property name="position">1</property> </packing> </child> + <child> + <object class="GtkCheckButton" id="poolCheckButton"> + <property name="label" translatable="yes">This URL refers to a pool of NTP servers</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="xalign">0</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> <child> <object class="GtkScrolledWindow" id="scrolledWindow"> <property name="visible">True</property> @@ -222,7 +238,7 @@ </child> <child> <object class="GtkTreeViewColumn" id="hostnameColumn"> - <property name="title" translatable="yes">Hostname</property> + <property name="title" translatable="yes">Host Name</property> <property name="expand">True</property> <child> <object class="GtkCellRendererText" id="hostnameRenderer"> @@ -235,6 +251,19 @@ </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="pool"> + <property name="title" translatable="yes">Pool</property> + <child> + <object class="GtkCellRendererToggle" id="poolRenderer"> + <signal name="toggled" handler="on_pool_toggled" swapped="no"/> + </object> + <attributes> + <attribute name="active">1</attribute> + </attributes> + </child> + </object> + </child> <child> <object class="GtkTreeViewColumn" id="workingColumn"> <property name="title" translatable="yes">Working</property> @@ -251,7 +280,7 @@ <signal name="toggled" handler="on_use_server_toggled" swapped="no"/> </object> <attributes> - <attribute name="active">2</attribute> + <attribute name="active">3</attribute> </attributes> </child> </object> @@ -262,7 +291,7 @@ <packing> <property name="expand">True</property> <property name="fill">True</property> - <property name="position">2</property> + <property name="position">4</property> </packing> </child> </object> @@ -294,7 +323,7 @@ </object> <object class="AnacondaSpokeWindow" id="datetimeWindow"> <property name="can_focus">False</property> - <property name="window_name" translatable="yes">DATE & TIME</property> + <property name="window_name" translatable="yes">TIME & DATE</property> <signal name="button-clicked" handler="on_back_clicked" swapped="no"/> <child internal-child="main_box"> <object class="GtkBox" id="AnacondaSpokeWindow-main_box1"> @@ -632,24 +661,6 @@ <property name="top_attach">2</property> </packing> </child> - <child> - <object class="GtkButton" id="amPmUpButton"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="image">upImage1</property> - <signal name="clicked" handler="on_updown_ampm_clicked" swapped="no"/> - <child internal-child="accessible"> - <object class="AtkObject" id="amPmUpButton-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">AM/PM Up</property> - </object> - </child> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">0</property> - </packing> - </child> <child> <object class="GtkButton" id="minutesUpButton"> <property name="visible">True</property> @@ -668,24 +679,6 @@ <property name="top_attach">0</property> </packing> </child> - <child> - <object class="GtkButton" id="amPmDownButton"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="image">downImage1</property> - <signal name="clicked" handler="on_updown_ampm_clicked" swapped="no"/> - <child internal-child="accessible"> - <object class="AtkObject" id="amPmDownButton-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">AM/PM Down</property> - </object> - </child> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">2</property> - </packing> - </child> <child> <object class="GtkButton" id="minutesDownButton"> <property name="visible">True</property> @@ -719,17 +712,79 @@ </packing> </child> <child> - <object class="GtkLabel" id="amPmLabel"> + <object class="GtkRevealer" id="amPmRevealer"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes">PM</property> - <attributes> - <attribute name="scale" value="1.5"/> - </attributes> + <property name="transition_type">crossfade</property> + <property name="reveal_child">True</property> + <child> + <object class="GtkBox" id="amPmBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkButton" id="amPmUpButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="image">upImage1</property> + <signal name="clicked" handler="on_updown_ampm_clicked" swapped="no"/> + <child internal-child="accessible"> + <object class="AtkObject" id="amPmUpButton-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes">AM/PM Up</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="amPmLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">4</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">PM</property> + <attributes> + <attribute name="scale" value="1.5"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="amPmDownButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="image">downImage1</property> + <signal name="clicked" handler="on_updown_ampm_clicked" swapped="no"/> + <child internal-child="accessible"> + <object class="AtkObject" id="amPmDownButton-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes">AM/PM Down</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> </object> <packing> <property name="left_attach">4</property> - <property name="top_attach">1</property> + <property name="top_attach">0</property> + <property name="height">3</property> </packing> </child> <child> @@ -876,7 +931,7 @@ </child> <child internal-child="accessible"> <object class="AtkObject" id="datetimeWindow-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">DATE & TIME</property> + <property name="AtkObject::accessible-name" translatable="yes">TIME & DATE</property> </object> </child> </object> diff --git a/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.py b/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.py index e892a5f..c9fe1c8 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.py +++ b/anaconda/pyanaconda/ui/gui/spokes/datetime_spoke.py @@ -29,12 +29,12 @@ from pyanaconda.ui.common import FirstbootSpokeMixIn from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.categories.localization import LocalizationCategory -from pyanaconda.ui.gui.utils import gtk_action_nowait, gtk_call_once, override_cell_property +from pyanaconda.ui.gui.utils import gtk_action_nowait, gtk_action_wait, gtk_call_once, override_cell_property from pyanaconda.ui.gui.helpers import GUIDialogInputCheckHandler from pyanaconda.ui.helpers import InputCheck from pyanaconda.i18n import _, CN_ -from pyanaconda.timezone import NTP_SERVICE, get_all_regions_and_timezones, is_valid_timezone +from pyanaconda.timezone import NTP_SERVICE, get_all_regions_and_timezones, get_timezone, is_valid_timezone from pyanaconda.localization import get_xlated_timezone, resolve_date_format from pyanaconda import iutil from pyanaconda import isys @@ -46,7 +46,6 @@ from pyanaconda import constants from pyanaconda.threads import threadMgr, AnacondaThread import datetime -import os import re import threading import locale as locale_mod @@ -57,6 +56,11 @@ SERVER_OK = 0 SERVER_NOK = 1 SERVER_QUERY = 2 +SERVER_HOSTNAME = 0 +SERVER_POOL = 1 +SERVER_WORKING = 2 +SERVER_USE = 3 + DEFAULT_TZ = "America/New_York" SPLIT_NUMBER_SUFFIX_RE = re.compile(r'([^0-9]*)([-+])([0-9]+)') @@ -150,26 +154,27 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): @property def working_server(self): for row in self._serversStore: - if row[1] == SERVER_OK and row[2]: + if row[SERVER_WORKING] == SERVER_OK and row[SERVER_USE]: #server is checked and working - return row[0] + return row[SERVER_HOSTNAME] return None @property - def servers(self): - ret = list() + def pools_servers(self): + pools = list() + servers = list() - for row in self._serversStore: - if row[2]: - #server checked - ret.append(row[0]) + for used_row in (row for row in self._serversStore if row[SERVER_USE]): + if used_row[SERVER_POOL]: + pools.append(used_row[SERVER_HOSTNAME]) + else: + servers.append(used_row[SERVER_HOSTNAME]) - return ret + return (pools, servers) def _render_working(self, column, renderer, model, itr, user_data=None): - #get the value in the second column - value = model[itr][1] + value = model[itr][SERVER_WORKING] if value == SERVER_QUERY: return "dialog-question" @@ -191,6 +196,8 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): self._addButton = self.builder.get_object("addButton") + self._poolCheckButton = self.builder.get_object("poolCheckButton") + # Validate the server entry box self._serverCheck = self.add_check(self._serverEntry, self._validateServer) self._serverCheck.update_check_status() @@ -201,14 +208,19 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): self._serversStore.clear() if self.data.timezone.ntpservers: - for server in self.data.timezone.ntpservers: - self._add_server(server) + pools, servers = ntp.internal_to_pools_and_servers(self.data.timezone.ntpservers) else: try: - for server in ntp.get_servers_from_config(): - self._add_server(server) + pools, servers = ntp.get_servers_from_config() except ntp.NTPconfigError: log.warning("Failed to load NTP servers configuration") + return + + for pool in pools: + self._add_server(pool, True) + for server in servers: + self._add_server(server, False) + def _validateServer(self, inputcheck): server = self.get_input(inputcheck.input_obj) @@ -247,15 +259,10 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): #OK clicked if rc == 1: - new_servers = list() - - for row in self._serversStore: - #if server checked - if row[2]: - new_servers.append(row[0]) + new_pools, new_servers = self.pools_servers if flags.can_touch_runtime_system("save NTP servers configuration"): - ntp.save_servers_to_config(new_servers) + ntp.save_servers_to_config(new_pools, new_servers) iutil.restart_service(NTP_SERVICE) #Cancel clicked, window destroyed... @@ -291,8 +298,8 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): (store, itr, column, value) = arg_tuple store.set_value(itr, column, value) - orig_hostname = self._serversStore[itr][0] - server_working = ntp.ntp_server_working(self._serversStore[itr][0]) + orig_hostname = self._serversStore[itr][SERVER_HOSTNAME] + server_working = ntp.ntp_server_working(self._serversStore[itr][SERVER_HOSTNAME]) #do not let dialog change epoch while we are modifying data self._epoch_lock.acquire() @@ -300,27 +307,27 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): #check if we are in the same epoch as the dialog (and the serversStore) #and if the server wasn't changed meanwhile if epoch_started == self._epoch: - actual_hostname = self._serversStore[itr][0] + actual_hostname = self._serversStore[itr][SERVER_HOSTNAME] if orig_hostname == actual_hostname: if server_working: set_store_value((self._serversStore, - itr, 1, SERVER_OK)) + itr, SERVER_WORKING, SERVER_OK)) else: set_store_value((self._serversStore, - itr, 1, SERVER_NOK)) + itr, SERVER_WORKING, SERVER_NOK)) self._epoch_lock.release() @gtk_action_nowait def _refresh_server_working(self, itr): """ Runs a new thread with _set_server_ok_nok(itr) as a taget. """ - self._serversStore.set_value(itr, 1, SERVER_QUERY) + self._serversStore.set_value(itr, SERVER_WORKING, SERVER_QUERY) threadMgr.add(AnacondaThread(prefix="AnaNTPserver", target=self._set_server_ok_nok, args=(itr, self._epoch))) - def _add_server(self, server): + def _add_server(self, server, pool=False): """ Checks if a given server is a valid hostname and if yes, adds it to the list of servers. @@ -329,12 +336,7 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): """ - for row in self._serversStore: - if row[0] == server: - #do not add duplicate items - return - - itr = self._serversStore.append([server, SERVER_QUERY, True]) + itr = self._serversStore.append([server, pool, SERVER_QUERY, True]) #do not block UI while starting thread (may take some time) self._refresh_server_working(itr) @@ -342,17 +344,24 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): def on_entry_activated(self, entry, *args): # Check that the input check has passed if self._serverCheck.check_status == InputCheck.CHECK_OK: - self._add_server(entry.get_text()) + self._add_server(entry.get_text(), self._poolCheckButton.get_active()) entry.set_text("") + self._poolCheckButton.set_active(False) def on_add_clicked(self, *args): self._serverEntry.emit("activate") def on_use_server_toggled(self, renderer, path, *args): itr = self._serversStore.get_iter(path) - old_value = self._serversStore[itr][2] + old_value = self._serversStore[itr][SERVER_USE] - self._serversStore.set_value(itr, 2, not old_value) + self._serversStore.set_value(itr, SERVER_USE, not old_value) + + def on_pool_toggled(self, renderer, path, *args): + itr = self._serversStore.get_iter(path) + old_value = self._serversStore[itr][SERVER_POOL] + + self._serversStore.set_value(itr, SERVER_POOL, not old_value) def on_server_edited(self, renderer, path, new_text, *args): if not path: @@ -365,11 +374,11 @@ class NTPconfigDialog(GUIObject, GUIDialogInputCheckHandler): itr = self._serversStore.get_iter(path) - if self._serversStore[itr][0] == new_text: + if self._serversStore[itr][SERVER_HOSTNAME] == new_text: return - self._serversStore.set_value(itr, 0, new_text) - self._serversStore.set_value(itr, 1, SERVER_QUERY) + self._serversStore.set_value(itr, SERVER_HOSTNAME, new_text) + self._serversStore.set_value(itr, SERVER_WORKING, SERVER_QUERY) self._refresh_server_working(itr) @@ -389,7 +398,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): category = LocalizationCategory icon = "preferences-system-time-symbolic" - title = CN_("GUI|Spoke", "DATE & _TIME") + title = CN_("GUI|Spoke", "_TIME & DATE") # Hack to get libtimezonemap loaded for GtkBuilder # see https://bugzilla.gnome.org/show_bug.cgi?id=712184 @@ -404,6 +413,8 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): self._update_datetime_timer_id = None self._start_updating_timer_id = None + self._shown = False + self._tz = None def initialize(self): NormalSpoke.initialize(self) @@ -434,6 +445,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): self._amPmDown = self.builder.get_object("amPmDownButton") self._amPmLabel = self.builder.get_object("amPmLabel") self._radioButton24h = self.builder.get_object("timeFormatRB") + self._amPmRevealer = self.builder.get_object("amPmRevealer") # create widgets for displaying/configuring date day_box, self._dayCombo, day_label = _new_date_field_box(self._daysFilter) @@ -488,7 +500,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): cities = set() xlated_regions = ((region, get_xlated_timezone(region)) - for region in self._regions_zones.iterkeys()) + for region in self._regions_zones.keys()) for region, xlated in sorted(xlated_regions, cmp=_compare_regions): self.add_to_store_xlated(self._regionsStore, region, xlated) for city in self._regions_zones[region]: @@ -529,6 +541,8 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): return _("Nothing selected") def apply(self): + self._shown = False + # we could use self._tzmap.get_timezone() here, but it returns "" if # Etc/XXXXXX timezone is selected region = self._get_active_region() @@ -573,13 +587,15 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): return True def refresh(self): + self._shown = True + #update the displayed time self._update_datetime_timer_id = GLib.timeout_add_seconds(1, self._update_datetime) self._start_updating_timer_id = None if is_valid_timezone(self.data.timezone.timezone): - self._set_timezone(self.data.timezone.timezone) + self._tzmap.set_timezone(self.data.timezone.timezone) self._update_datetime() @@ -597,7 +613,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): self._ntpSwitch.set_active(ntp_working) - @gtk_action_nowait + @gtk_action_wait def _set_timezone(self, timezone): """ Sets timezone to the city/region comboboxes and the timezone map. @@ -712,7 +728,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): return (hours + correction) % 24 def _update_datetime(self): - now = datetime.datetime.now() + now = datetime.datetime.now(self._tz) if self._radioButton24h.get_active(): self._hoursLabel.set_text("%0.2d" % now.hour) else: @@ -738,6 +754,8 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): """ + self._start_updating_timer_id = None + if not flags.can_touch_runtime_system("save system time"): return False @@ -758,10 +776,10 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): day = self._get_combo_selection(self._dayCombo)[0] #day may be None if there is no such in the selected year and month if day: - isys.set_system_date_time(year, month, day, hours, minutes) + isys.set_system_date_time(year, month, day, hours, minutes, tz=self._tz) #start the timer only when the spoke is shown - if self._update_datetime_timer_id is not None: + if self._shown and not self._update_datetime_timer_id: self._update_datetime_timer_id = GLib.timeout_add_seconds(1, self._update_datetime) @@ -783,13 +801,15 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): """ #do not start timers if the spoke is not shown - if self._update_datetime_timer_id is None: + if not self._shown: self._update_datetime() self._save_system_time() return #stop time updating - GLib.source_remove(self._update_datetime_timer_id) + if self._update_datetime_timer_id: + GLib.source_remove(self._update_datetime_timer_id) + self._update_datetime_timer_id = None #stop previous $interval seconds timer (see below) if self._start_updating_timer_id: @@ -1012,7 +1032,7 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): timezone = location.get_property('zone') if self._set_timezone(timezone): # timezone successfully set - os.environ["TZ"] = timezone + self._tz = get_timezone(timezone) self._update_datetime() def on_timeformat_changed(self, button24h, *args): @@ -1023,11 +1043,12 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): if button24h.get_active(): self._set_amPm_part_sensitive(False) new_hours = self._to_24h(hours, amPm) - + self._amPmRevealer.set_reveal_child(False) else: self._set_amPm_part_sensitive(True) new_hours, new_amPm = self._to_amPm(hours) self._amPmLabel.set_text(new_amPm) + self._amPmRevealer.set_reveal_child(True) self._hoursLabel.set_text("%0.2d" % new_hours) @@ -1039,11 +1060,9 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): def _show_no_network_warning(self): self.set_warning(_("You need to set up networking first if you "\ "want to use NTP")) - self.window.show_all() def _show_no_ntp_server_warning(self): self.set_warning(_("You have no working NTP server configured")) - self.window.show_all() def on_ntp_switched(self, switch, *args): if switch.get_active(): @@ -1098,7 +1117,8 @@ class DatetimeSpoke(FirstbootSpokeMixIn, NormalSpoke): response = self._config_dialog.run() if response == 1: - self.data.timezone.ntpservers = self._config_dialog.servers + pools, servers = self._config_dialog.pools_servers + self.data.timezone.ntpservers = ntp.pools_servers_to_internal(pools, servers) if self._config_dialog.working_server is None: self._show_no_ntp_server_warning() diff --git a/anaconda/pyanaconda/ui/gui/spokes/filter.glade b/anaconda/pyanaconda/ui/gui/spokes/filter.glade index 9cfd145..3e94e46 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/filter.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/filter.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.1 --> +<!-- Generated with glade 3.18.3 --> <interface> <requires lib="gtk+" version="3.0"/> <requires lib="AnacondaWidgets" version="1.0"/> @@ -37,6 +37,8 @@ <column type="gchararray"/> <!-- column-name diskCCW --> <column type="gchararray"/> + <!-- column-name diskWWPN --> + <column type="gchararray"/> </columns> </object> <object class="GtkTreeModelFilter" id="multipathModel"> @@ -110,7 +112,7 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> - <object class="GtkViewport" id="viewport1"> + <object class="GtkViewport" id="searchScrolledViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="shadow_type">none</property> @@ -233,7 +235,8 @@ <object class="GtkEntry" id="searchTargetEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> @@ -245,6 +248,7 @@ <object class="GtkLabel" id="label15"> <property name="visible">True</property> <property name="can_focus">False</property> + <!-- check_accelerators: refresh --> <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Search|Port Target LUN">_LUN:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">searchLUNEntry</property> @@ -259,7 +263,8 @@ <object class="GtkEntry" id="searchLUNEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> </object> <packing> <property name="expand">False</property> @@ -299,7 +304,8 @@ <object class="GtkEntry" id="searchWWIDEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> <property name="width_chars">30</property> </object> <packing> @@ -355,6 +361,20 @@ </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="searchNameCol"> + <property name="title" translatable="yes">Name</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="searchNameRenderer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">3</attribute> + </attributes> + </child> + </object> + </child> <child> <object class="GtkTreeViewColumn" id="searchWWIDCol"> <property name="title" translatable="yes">WWID</property> @@ -474,20 +494,6 @@ <property name="width">5</property> </packing> </child> - <child> - <object class="GtkButton" id="searchClearButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Search">_Clear</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_clear_clicked" swapped="no"/> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">0</property> - </packing> - </child> <child> <object class="GtkButton" id="searchFindButton"> <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Search">_Find</property> @@ -525,7 +531,7 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> - <object class="GtkViewport" id="viewport2"> + <object class="GtkViewport" id="multipathViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="shadow_type">none</property> @@ -686,7 +692,8 @@ <object class="GtkEntry" id="multipathWWIDEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> <property name="width_chars">30</property> </object> <packing> @@ -814,20 +821,6 @@ <property name="width">5</property> </packing> </child> - <child> - <object class="GtkButton" id="multipathClearButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Multipath">_Clear</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_clear_clicked" swapped="no"/> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">0</property> - </packing> - </child> <child> <object class="GtkButton" id="multipathFindButton"> <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Multipath">_Find</property> @@ -869,7 +862,7 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> - <object class="GtkViewport" id="viewport4"> + <object class="GtkViewport" id="otherViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="shadow_type">none</property> @@ -913,6 +906,116 @@ <property name="top_attach">0</property> </packing> </child> + <child> + <object class="GtkTreeView" id="otherTreeView"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="model">otherModel</property> + <property name="headers_clickable">False</property> + <property name="rules_hint">True</property> + <property name="enable_search">False</property> + <property name="search_column">0</property> + <property name="enable_tree_lines">True</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="treeview-selection3"/> + </child> + <child> + <object class="GtkTreeViewColumn" id="otherSelectedCol"> + <child> + <object class="GtkCellRendererToggle" id="otherSelectedRenderer"> + <signal name="toggled" handler="on_row_toggled" swapped="no"/> + </object> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="active">1</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="otherIDCol"> + <property name="title" translatable="yes">Identifier</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="otherIDRendererer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">10</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="otherCapacityCol"> + <property name="title" translatable="yes">Capacity</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="otherCapacityRenderer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">6</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="otherVendorCol"> + <property name="title" translatable="yes">Vendor</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="otherVendorRenderer"/> + <attributes> + <attribute name="yalign">2</attribute> + <attribute name="sensitive">1</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">7</attribute> + </attributes> + </child> + </object> + </child> + <child> + <object class="GtkTreeViewColumn" id="otherInterconnectCol"> + <property name="title" translatable="yes">Interconnect</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="otherInterconnectRenderer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">8</attribute> + </attributes> + </child> + </object> + </child> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + <property name="width">5</property> + <property name="height">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="otherFindButton"> + <property name="label">gtk-find</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + <signal name="clicked" handler="on_find_clicked" swapped="no"/> + </object> + <packing> + <property name="left_attach">3</property> + <property name="top_attach">0</property> + <property name="width">2</property> + <property name="height">1</property> + </packing> + </child> <child> <object class="GtkNotebook" id="otherTypeNotebook"> <property name="visible">True</property> @@ -1030,7 +1133,8 @@ <object class="GtkEntry" id="otherIDEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> <property name="width_chars">30</property> </object> <packing> @@ -1053,127 +1157,6 @@ <property name="top_attach">0</property> </packing> </child> - <child> - <object class="GtkTreeView" id="otherTreeView"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="vexpand">True</property> - <property name="model">otherModel</property> - <property name="headers_clickable">False</property> - <property name="rules_hint">True</property> - <property name="enable_search">False</property> - <property name="search_column">0</property> - <property name="enable_tree_lines">True</property> - <child internal-child="selection"> - <object class="GtkTreeSelection" id="treeview-selection3"/> - </child> - <child> - <object class="GtkTreeViewColumn" id="otherSelectedCol"> - <child> - <object class="GtkCellRendererToggle" id="otherSelectedRenderer"> - <signal name="toggled" handler="on_row_toggled" swapped="no"/> - </object> - <attributes> - <attribute name="sensitive">2</attribute> - <attribute name="visible">0</attribute> - <attribute name="active">1</attribute> - </attributes> - </child> - </object> - </child> - <child> - <object class="GtkTreeViewColumn" id="otherIDCol"> - <property name="title" translatable="yes">Identifier</property> - <property name="clickable">True</property> - <child> - <object class="GtkCellRendererText" id="otherIDRenderer"/> - <attributes> - <attribute name="sensitive">2</attribute> - <attribute name="visible">0</attribute> - <attribute name="text">10</attribute> - </attributes> - </child> - </object> - </child> - <child> - <object class="GtkTreeViewColumn" id="otherCapacityCol"> - <property name="title" translatable="yes">Capacity</property> - <property name="clickable">True</property> - <child> - <object class="GtkCellRendererText" id="otherCapacityRenderer"/> - <attributes> - <attribute name="sensitive">2</attribute> - <attribute name="visible">0</attribute> - <attribute name="text">6</attribute> - </attributes> - </child> - </object> - </child> - <child> - <object class="GtkTreeViewColumn" id="otherVendorCol"> - <property name="title" translatable="yes">Vendor</property> - <property name="clickable">True</property> - <child> - <object class="GtkCellRendererText" id="otherVendorRenderer"/> - <attributes> - <attribute name="yalign">2</attribute> - <attribute name="sensitive">1</attribute> - <attribute name="visible">0</attribute> - <attribute name="text">7</attribute> - </attributes> - </child> - </object> - </child> - <child> - <object class="GtkTreeViewColumn" id="otherInterconnectCol"> - <property name="title" translatable="yes">Interconnect</property> - <property name="clickable">True</property> - <child> - <object class="GtkCellRendererText" id="otherInterconnectRenderer"/> - <attributes> - <attribute name="sensitive">2</attribute> - <attribute name="visible">0</attribute> - <attribute name="text">8</attribute> - </attributes> - </child> - </object> - </child> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - <property name="width">5</property> - </packing> - </child> - <child> - <object class="GtkButton" id="otherClearButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Other">_Clear</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_clear_clicked" swapped="no"/> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="otherFindButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|Other">_Find</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - <signal name="clicked" handler="on_find_clicked" swapped="no"/> - </object> - <packing> - <property name="left_attach">3</property> - <property name="top_attach">0</property> - </packing> - </child> </object> </child> </object> @@ -1201,7 +1184,7 @@ <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> - <object class="GtkViewport" id="viewport3"> + <object class="GtkViewport" id="zViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="shadow_type">none</property> @@ -1219,9 +1202,8 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">_Filter By:</property> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">Filter B_y:</property> <property name="use_underline">True</property> - <property name="mnemonic_widget">zFilterCombo</property> </object> <packing> <property name="left_attach">0</property> @@ -1229,68 +1211,19 @@ </packing> </child> <child> - <object class="GtkLabel" id="zFilterTypeLabel"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">_Label</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">zFilterEntry</property> - </object> - <packing> - <property name="left_attach">2</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="zGoButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">_Go</property> + <object class="GtkButton" id="zFindButton"> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">_Find</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">4</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="zClearButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries">_Clear</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="left_attach">5</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="zFilterEntry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="hexpand">True</property> - <property name="invisible_char">●</property> + <signal name="clicked" handler="on_find_clicked" swapped="no"/> </object> <packing> <property name="left_attach">3</property> <property name="top_attach">0</property> </packing> </child> - <child> - <object class="GtkComboBox" id="zFilterCombo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="hexpand">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> <child> <object class="GtkTreeView" id="zTreeView"> <property name="visible">True</property> @@ -1301,9 +1234,6 @@ <property name="rules_hint">True</property> <property name="enable_search">False</property> <property name="enable_tree_lines">True</property> - <child internal-child="selection"> - <object class="GtkTreeSelection" id="treeview-selection4"/> - </child> <child> <object class="GtkTreeViewColumn" id="zSelectedCol"> <child> @@ -1332,6 +1262,20 @@ </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="zNameCol"> + <property name="title" translatable="yes">Name</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="zNameRenderer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">3</attribute> + </attributes> + </child> + </object> + </child> <child> <object class="GtkTreeViewColumn" id="zTypeCol"> <property name="title" translatable="yes">Type</property> @@ -1369,7 +1313,7 @@ <attributes> <attribute name="sensitive">2</attribute> <attribute name="visible">0</attribute> - <attribute name="text">10</attribute> + <attribute name="text">16</attribute> </attributes> </child> </object> @@ -1388,11 +1332,197 @@ </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="zPortCol"> + <property name="title" translatable="yes">Port</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="zPortRenderer"/> + <attributes> + <attribute name="sensitive">2</attribute> + <attribute name="visible">0</attribute> + <attribute name="text">12</attribute> + </attributes> + </child> + </object> + </child> </object> <packing> <property name="left_attach">0</property> <property name="top_attach">1</property> - <property name="width">6</property> + <property name="width">5</property> + </packing> + </child> + <child> + <object class="GtkNotebook" id="zTypeNotebook"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="no_show_all">True</property> + <property name="hexpand">True</property> + <property name="show_tabs">False</property> + <child> + <object class="GtkLabel" id="label19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + </object> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="zCCWBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label23"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries|CCW">_CCW:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">zCCWEntry</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="zCCWEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> + <property name="width_chars">30</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="zWWPNBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label24"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries|WWPN">_WWPN:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">zWWPNEntry</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="zWWPNEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> + <property name="width_chars">30</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <placeholder/> + </child> + <child> + <object class="GtkBox" id="zLUNBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkLabel" id="label22"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <!-- check_accelerators: refresh --> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter|zSeries|LUN">_LUN:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">zLUNEntry</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="zLUNEntry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="secondary_icon_name">edit-clear-symbolic</property> + <signal name="icon-release" handler="on_clear_icon_clicked" swapped="no"/> + <property name="width_chars">30</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + <property name="width">1</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="zTypeCombo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="entry_text_column">0</property> + <property name="id_column">1</property> + <items> + <item translatable="yes">None</item> + <item translatable="yes">CCW</item> + <item translatable="yes">WWPN</item> + <item translatable="yes">LUN</item> + </items> + <signal name="changed" handler="on_z_type_combo_changed" swapped="no"/> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + <property name="width">1</property> + <property name="height">1</property> </packing> </child> </object> @@ -1431,7 +1561,7 @@ <property name="layout_style">end</property> <child> <object class="GtkButton" id="addZFCPButton"> - <property name="label" translatable="yes" context="GUI|Installation Destination|Filter">_Add ZFCP LUN...</property> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter">_Add zFCP LUN...</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> @@ -1446,7 +1576,7 @@ </child> <child> <object class="GtkButton" id="addDASDButton"> - <property name="label" translatable="yes">Add EC_KD DASD...</property> + <property name="label" translatable="yes" context="GUI|Installation Destination|Filter">Add EC_KD DASD...</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> @@ -1489,6 +1619,21 @@ <property name="position">3</property> </packing> </child> + <child> + <object class="GtkButton" id="refresh"> + <property name="label" translatable="yes">Refresh _List</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="on_refresh_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> </object> <packing> <property name="expand">False</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/filter.py b/anaconda/pyanaconda/ui/gui/spokes/filter.py index 21e9737..c31ce3e 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/filter.py +++ b/anaconda/pyanaconda/ui/gui/spokes/filter.py @@ -19,8 +19,9 @@ # Red Hat Author(s): Chris Lumens <clumens@redhat.com> # +from gi.repository import Gtk + from collections import namedtuple -import itertools from blivet import arch from blivet.devices import DASDDevice, FcoeDiskDevice, iScsiDiskDevice, MultipathDevice, ZFCPDiskDevice @@ -29,10 +30,12 @@ from blivet.fcoe import has_fcoe from pyanaconda.flags import flags from pyanaconda.i18n import CN_, CP_ -from pyanaconda.ui.lib.disks import getDisks, isLocalDisk +from pyanaconda.ui.lib.disks import getDisks +from pyanaconda.ui.gui.utils import timed_action from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.spokes.advstorage.fcoe import FCoEDialog from pyanaconda.ui.gui.spokes.advstorage.iscsi import ISCSIDialog +from pyanaconda.ui.gui.spokes.advstorage.zfcp import ZFCPDialog from pyanaconda.ui.gui.spokes.advstorage.dasd import DASDDialog from pyanaconda.ui.gui.spokes.lib.cart import SelectedDisksDialog from pyanaconda.ui.categories.system import SystemCategory @@ -43,7 +46,7 @@ DiskStoreRow = namedtuple("DiskStoreRow", ["visible", "selected", "mutable", "name", "type", "model", "capacity", "vendor", "interconnect", "serial", "wwid", "paths", "port", "target", - "lun", "ccw"]) + "lun", "ccw", "wwpn"]) class FilterPage(object): """A FilterPage is the logic behind one of the notebook tabs on the filter @@ -195,6 +198,8 @@ class SearchPage(FilterPage): return int(active) == device.node.tpgt except ValueError: return True + elif active and hasattr(device, "fcp_lun"): + return active in device.fcp_lun else: return True @@ -210,8 +215,6 @@ class SearchPage(FilterPage): return self._port_equal(device) and self._target_equal(device) and self._lun_equal(device) elif filterBy == 2: return self._wwidEntry.get_text() in getattr(device, "wwid", self._long_identifier(device)) - elif filterBy == 3: - return hasattr(device, "fcp_lun") and self._lunEntry.get_text() in device.fcp_lun def visible_func(self, model, itr, *args): obj = DiskStoreRow(*model[itr]) @@ -244,7 +247,7 @@ class MultipathPage(FilterPage): disk.name, "", disk.model, str(disk.size), disk.vendor, disk.bus, disk.serial, disk.wwid, "\n".join(paths), "", "", - "", ""]) + "", "", ""]) if not disk.vendor in vendors: vendors.append(disk.vendor) @@ -304,6 +307,7 @@ class OtherPage(FilterPage): interconnects = [] for disk in disks: + paths = [d.name for d in disk.parents] selected = disk.name in selectedNames if hasattr(disk, "node"): @@ -316,8 +320,8 @@ class OtherPage(FilterPage): store.append([True, selected, not disk.protected, disk.name, "", disk.model, str(disk.size), disk.vendor, disk.bus, disk.serial, - self._long_identifier(disk), "", port, getattr(disk, "initiator", ""), - lun, ""]) + self._long_identifier(disk), "\n".join(paths), port, getattr(disk, "initiator", ""), + lun, "", ""]) if not disk.vendor in vendors: vendors.append(disk.vendor) @@ -366,19 +370,74 @@ class ZPage(FilterPage): self.model = self.builder.get_object("zModel") self.model.set_visible_func(self.visible_func) + self._ccwEntry = self.builder.get_object("zCCWEntry") + self._wwpnEntry = self.builder.get_object("zWWPNEntry") + self._lunEntry = self.builder.get_object("zLUNEntry") + self._combo = self.builder.get_object("zTypeCombo") + self._isS390 = arch.isS390() + def clear(self): + self._lunEntry.set_text("") + self._ccwEntry.set_text("") + self._wwpnEntry.set_text("") + def ismember(self, device): return isinstance(device, ZFCPDiskDevice) or isinstance(device, DASDDevice) def setup(self, store, selectedNames, disks): + """ Set up our Z-page, but only if we're running on s390x. """ if not self._isS390: return + else: + ccws = [] + wwpns = [] + luns = [] + + self._combo.set_active(0) + self._combo.emit("changed") + + for disk in disks: + paths = [d.name for d in disk.parents] + selected = disk.name in selectedNames + + if getattr(disk, "type") == "zfcp": + # remember to store all of the zfcp-related junk so we can + # see it in the UI + if not disk.fcp_lun in luns: + luns.append(disk.fcp_lun) + if not disk.wwpn in wwpns: + wwpns.append(disk.wwpn) + if not disk.hba_id in ccws: + ccws.append(disk.hba_id) + + # now add it to our store + store.append([True, selected, not disk.protected, + disk.name, "", disk.model, str(disk.size), + disk.vendor, disk.bus, disk.serial, "", "\n".join(paths), + "", "", disk.fcp_lun, disk.hba_id, disk.wwpn]) + + def _filter_func(self, device): + if not self.filterActive: + return True + + filterBy = self._combo.get_active() + + if filterBy == 0: + return True + elif filterBy == 1: + return self._ccwEntry.get_text() in device.hba_id + elif filterBy == 2: + return self._wwpnEntry.get_text() in device.wwpn + elif filterBy == 3: + return self._lunEntry.get_text() in device.fcp_lun + + return False def visible_func(self, model, itr, *args): obj = DiskStoreRow(*model[itr]) device = self.storage.devicetree.getDeviceByName(obj.name, hidden=True) - return self.ismember(device) + return self.ismember(device) and self._filter_func(device) class FilterSpoke(NormalSpoke): builderObjects = ["diskStore", "filterWindow", @@ -438,6 +497,27 @@ class FilterSpoke(NormalSpoke): self._store = self.builder.get_object("diskStore") self._addDisksButton = self.builder.get_object("addDisksButton") + # Connect focus events in scrolled viewport children to scrolling on the viewport + searchScrolledViewport = self.builder.get_object("searchScrolledViewport") + searchGrid = self.builder.get_object("searchGrid") + searchGrid.set_focus_hadjustment(searchScrolledViewport.get_hadjustment()) + searchGrid.set_focus_vadjustment(searchScrolledViewport.get_vadjustment()) + + multipathViewport = self.builder.get_object("multipathViewport") + multipathGrid = self.builder.get_object("multipathGrid") + multipathGrid.set_focus_hadjustment(multipathViewport.get_hadjustment()) + multipathGrid.set_focus_vadjustment(multipathViewport.get_vadjustment()) + + otherViewport = self.builder.get_object("otherViewport") + otherGrid = self.builder.get_object("otherGrid") + otherGrid.set_focus_hadjustment(otherViewport.get_hadjustment()) + otherGrid.set_focus_vadjustment(otherViewport.get_vadjustment()) + + zViewport = self.builder.get_object("zViewport") + zGrid = self.builder.get_object("zGrid") + zGrid.set_focus_hadjustment(zViewport.get_hadjustment()) + zGrid.set_focus_vadjustment(zViewport.get_vadjustment()) + def _real_ancestors(self, disk): # Return a list of all the ancestors of a disk, but remove the disk # itself from this list. @@ -449,8 +529,7 @@ class FilterSpoke(NormalSpoke): self.disks = getDisks(self.storage.devicetree) self.selected_disks = self.data.ignoredisk.onlyuse[:] - self.ancestors = itertools.chain(*map(self._real_ancestors, self.disks)) - self.ancestors = map(lambda d: d.name, self.ancestors) + self.ancestors = [d.name for disk in self.disks for d in self._real_ancestors(disk)] self._store.clear() @@ -464,7 +543,7 @@ class FilterSpoke(NormalSpoke): # these lists of disks, then call setup on each individual page. This is # because there could be page-specific setup to do that requires a complete # view of all the disks on that page. - for disk in itertools.ifilterfalse(isLocalDisk, self.disks): + for disk in self.disks: if self.pages[1].ismember(disk): multipathDisks.append(disk) elif self.pages[2].ismember(disk): @@ -522,11 +601,9 @@ class FilterSpoke(NormalSpoke): self.pages[n].filterActive = True self.pages[n].model.refilter() - def on_clear_clicked(self, button): - n = self._notebook.get_current_page() - self.pages[n].filterActive = False - self.pages[n].model.refilter() - self.pages[n].clear() + def on_clear_icon_clicked(self, entry, icon_pos, event): + if icon_pos == Gtk.EntryIconPosition.SECONDARY: + entry.set_text("") def on_page_switched(self, notebook, newPage, newPageNum, *args): self.pages[newPageNum].model.refilter() @@ -549,6 +626,11 @@ class FilterSpoke(NormalSpoke): self._update_summary() + @timed_action(delay=50, threshold=100) + def on_refresh_clicked(self, widget, *args): + self.storage.devicetree.populate() + self.refresh() + def on_add_iscsi_clicked(self, widget, *args): dialog = ISCSIDialog(self.data, self.storage) @@ -572,7 +654,15 @@ class FilterSpoke(NormalSpoke): self.refresh() def on_add_zfcp_clicked(self, widget, *args): - pass + dialog = ZFCPDialog(self.data, self.storage) + + with self.main_window.enlightbox(dialog.window): + dialog.refresh() + dialog.run() + + # We now need to refresh so any new disks picked up by adding advanced + # storage are displayed in the UI. + self.refresh() def on_add_dasd_clicked(self, widget, *args): dialog = DASDDialog(self.data, self.storage) @@ -593,10 +683,8 @@ class FilterSpoke(NormalSpoke): notebook = self.builder.get_object("searchTypeNotebook") findButton = self.builder.get_object("searchFindButton") - clearButton = self.builder.get_object("searchClearButton") findButton.set_sensitive(ndx != 0) - clearButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx) ## @@ -607,10 +695,8 @@ class FilterSpoke(NormalSpoke): notebook = self.builder.get_object("multipathTypeNotebook") findButton = self.builder.get_object("multipathFindButton") - clearButton = self.builder.get_object("multipathClearButton") findButton.set_sensitive(ndx != 0) - clearButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx) ## @@ -621,8 +707,18 @@ class FilterSpoke(NormalSpoke): notebook = self.builder.get_object("otherTypeNotebook") findButton = self.builder.get_object("otherFindButton") - clearButton = self.builder.get_object("otherClearButton") findButton.set_sensitive(ndx != 0) - clearButton.set_sensitive(ndx != 0) + notebook.set_current_page(ndx) + + ## + ## Z TAB SIGNAL HANDLERS + ## + def on_z_type_combo_changed(self, combo): + ndx = combo.get_active() + + notebook = self.builder.get_object("zTypeNotebook") + findButton = self.builder.get_object("zFindButton") + + findButton.set_sensitive(ndx != 0) notebook.set_current_page(ndx) diff --git a/anaconda/pyanaconda/ui/gui/spokes/keyboard.glade b/anaconda/pyanaconda/ui/gui/spokes/keyboard.glade index 149b2c8..4df7951 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/keyboard.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/keyboard.glade @@ -311,7 +311,6 @@ <object class="GtkTextView" id="layoutTextView"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="border_width">6</property> <property name="buffer">layoutTestBuffer</property> <property name="accepts_tab">False</property> </object> @@ -547,7 +546,6 @@ <object class="GtkEntry" id="addLayoutEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <property name="secondary_icon_name">edit-clear-symbolic</property> <signal name="changed" handler="on_entry_changed" swapped="no"/> <signal name="icon-press" handler="on_entry_icon_clicked" swapped="no"/> diff --git a/anaconda/pyanaconda/ui/gui/spokes/langsupport.glade b/anaconda/pyanaconda/ui/gui/spokes/langsupport.glade index 420cd56..2777769 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/langsupport.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/langsupport.glade @@ -189,7 +189,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="valign">start</property> - <property name="invisible_char">●</property> <property name="secondary_icon_name">edit-clear-symbolic</property> <property name="placeholder_text" translatable="yes">Type here to search.</property> <signal name="changed" handler="on_entry_changed" swapped="no"/> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/accordion.py b/anaconda/pyanaconda/ui/gui/spokes/lib/accordion.py index 844f725..4aee64c 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/accordion.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/accordion.py @@ -157,13 +157,17 @@ class Page(Gtk.Box): self._dataLabel = self._make_category_label(_("DATA")) really_hide(self._dataLabel) self._dataBox.add(self._dataLabel) - self._dataBox.connect("add", self._onDataAdded) - self._dataBox.connect("remove", self._onDataRemoved) + self._dataBox.connect("add", self._onSelectorAdded, self._dataLabel) + self._dataBox.connect("remove", self._onSelectorRemoved, self._dataLabel) self.add(self._dataBox) # Create the System label and a box to store all its members in. self._systemBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self._systemBox.add(self._make_category_label(_("SYSTEM"))) + self._systemLabel = self._make_category_label(_("SYSTEM")) + really_hide(self._systemLabel) + self._systemBox.add(self._systemLabel) + self._systemBox.connect("add", self._onSelectorAdded, self._systemLabel) + self._systemBox.connect("remove", self._onSelectorRemoved, self._systemLabel) self.add(self._systemBox) self.members = [] @@ -227,14 +231,14 @@ class Page(Gtk.Box): # _onSelectorClicked cb(selector) - def _onDataAdded(self, container, widget): - really_show(self._dataLabel) + def _onSelectorAdded(self, container, widget, label): + really_show(label) - def _onDataRemoved(self, container, widget): + def _onSelectorRemoved(self, container, widget, label): # This runs before widget is removed from container, so if it's the last # item then the container will still not be empty. if len(container.get_children()) == 1: - really_hide(self._dataLabel) + really_hide(label) class UnknownPage(Page): def __init__(self, title): diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/cart.glade b/anaconda/pyanaconda/ui/gui/spokes/lib/cart.glade index 819633c..1aa2ca4 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/cart.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/cart.glade @@ -19,7 +19,7 @@ </columns> </object> <object class="GtkDialog" id="selected_disks_dialog"> - <property name="width_request">500</property> + <property name="width_request">600</property> <property name="height_request">400</property> <property name="can_focus">False</property> <property name="border_width">6</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/cart.py b/anaconda/pyanaconda/ui/gui/spokes/lib/cart.py index fe0499d..6c70db7 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/cart.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/cart.py @@ -129,9 +129,9 @@ class SelectedDisksDialog(GUIObject): # pylint: disable=unescaped-markup text = P_("<b>%(count)d disk; %(size)s capacity; %(free)s free space</b> " - "(unpartitioned and in filesystems)", + "(unpartitioned and in file systems)", "<b>%(count)d disks; %(size)s capacity; %(free)s free space</b> " - "(unpartitioned and in filesystems)", + "(unpartitioned and in file systems)", count) % {"count" : count, "size" : escape_markup(size), "free" : escape_markup(free)} @@ -187,7 +187,7 @@ class SelectedDisksDialog(GUIObject): def _toggle_button_text(self, row): if row[IS_BOOT_COL]: self._set_button.set_label(C_("GUI|Selected Disks Dialog", - "_Do not install bootloader")) + "_Do not install boot loader")) else: self._set_button.set_label(C_("GUI|Selected Disks Dialog", "_Set as Boot Device")) diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade b/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade index e81ef3e..d90f853 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.glade @@ -96,20 +96,20 @@ </object> <object class="GtkListStore" id="disk_store"> <columns> - <!-- column-name disk_col --> + <!-- column-name description_col --> <column type="gchararray"/> <!-- column-name capacity_col --> <column type="gchararray"/> <!-- column-name free_col --> <column type="gchararray"/> - <!-- column-name serial_col --> + <!-- column-name name_col --> <column type="gchararray"/> <!-- column-name id_col --> <column type="gint"/> </columns> </object> <object class="GtkDialog" id="disks_dialog"> - <property name="width_request">400</property> + <property name="width_request">550</property> <property name="height_request">200</property> <property name="can_focus">False</property> <property name="border_width">6</property> @@ -217,18 +217,31 @@ </object> </child> <child> - <object class="GtkTreeViewColumn" id="disk_column"> + <object class="GtkTreeViewColumn" id="description_column"> <property name="spacing">6</property> - <property name="title" translatable="yes">Disk</property> + <property name="title" translatable="yes">Description</property> <property name="clickable">True</property> <child> - <object class="GtkCellRendererText" id="disk_renderer"/> + <object class="GtkCellRendererText" id="description_renderer"/> <attributes> <attribute name="text">0</attribute> </attributes> </child> </object> </child> + <child> + <object class="GtkTreeViewColumn" id="name_column"> + <property name="spacing">6</property> + <property name="title" translatable="yes">Name</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererText" id="name_renderer"/> + <attributes> + <attribute name="text">3</attribute> + </attributes> + </child> + </object> + </child> <child> <object class="GtkTreeViewColumn" id="capacity_column"> <property name="spacing">6</property> @@ -255,19 +268,6 @@ </child> </object> </child> - <child> - <object class="GtkTreeViewColumn" id="id_column"> - <property name="spacing">6</property> - <property name="title" translatable="yes">ID</property> - <property name="clickable">True</property> - <child> - <object class="GtkCellRendererText" id="id_renderer"/> - <attributes> - <attribute name="text">3</attribute> - </attributes> - </child> - </object> - </child> </object> </child> </object> @@ -478,7 +478,6 @@ use. Try something else?</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="tooltip_text" translatable="yes">eg: "20 GB", "500mb" (minus the quotation marks)</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -674,7 +673,6 @@ use. Try something else?</property> <object class="GtkEntry" id="container_name_entry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="expand">False</property> @@ -910,7 +908,6 @@ use. Try something else?</property> <object class="GtkEntry" id="containerSizeEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="expand">False</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py b/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py index 5370a8b..3275d6f 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/custom_storage_helpers.py @@ -27,13 +27,14 @@ __all__ = ["size_from_entry", "populate_mountpoint_store", "validate_label", "validate_mountpoint", "get_raid_level", "selectedRaidLevel", "raidLevelSelection", "defaultRaidLevel", "requiresRaidSelection", "defaultContainerRaidLevel", - "containerRaidLevelsSupported", "raidLevelsSupported", "get_container_type_name", - "AddDialog", "ConfirmDeleteDialog", "DisksDialog", "ContainerDialog", - "HelpDialog"] + "containerRaidLevelsSupported", "raidLevelsSupported", "get_container_type", + "AddDialog", "ConfirmDeleteDialog", "DisksDialog", "ContainerDialog"] +from collections import namedtuple import functools import re +from pyanaconda.constants import SIZE_UNITS_DEFAULT from pyanaconda.product import productName from pyanaconda.iutil import lowerASCII from pyanaconda.storage_utils import size_from_input @@ -68,16 +69,38 @@ CONTAINER_DIALOG_TITLE = N_("CONFIGURE %(container_type)s") CONTAINER_DIALOG_TEXT = N_("Please create a name for this %(container_type)s " "and select at least one disk below.") -CONTAINER_TYPE_NAMES = {DEVICE_TYPE_LVM: N_("Volume Group"), - DEVICE_TYPE_LVM_THINP: N_("Volume Group"), - DEVICE_TYPE_BTRFS: N_("Volume")} +ContainerType = namedtuple("ContainerType", ["name", "label"]) + +CONTAINER_TYPES = {DEVICE_TYPE_LVM: ContainerType(N_("Volume Group"), N_("_Volume Group:")), + DEVICE_TYPE_LVM_THINP: ContainerType(N_("Volume Group"), N_("_Volume Group:")), + DEVICE_TYPE_BTRFS: ContainerType(N_("Volume"), N_("_Volume:"))} # These cannot be specified as mountpoints system_mountpoints = ["/dev", "/proc", "/run", "/sys"] -def size_from_entry(entry): +def size_from_entry(entry, lower_bound=None, units=None): + """ Get a Size object from an entry field. + + :param lower_bound: lower bound for size returned, + :type lower_bound: :class:`blivet.size.Size` or NoneType + :param units: units to use if none obtained from entry + :type units: str or NoneType + :returns: a Size object corresponding to the text in the entry field + :rtype: :class:`blivet.size.Size` or NoneType + + Units default to bytes if no units specified in entry or units. + + Rounds up to lower_bound, if value in entry field corresponds + to a smaller value. The default for lower_bound is None, yielding + no rounding. + """ size_text = entry.get_text().decode("utf-8").strip() - return size_from_input(size_text) + size = size_from_input(size_text, units=units) + if size is None: + return None + if lower_bound is not None and size < lower_bound: + return lower_bound + return size def populate_mountpoint_store(store, used_mountpoints): # sure, add whatever you want to this list. this is just a start. @@ -99,9 +122,9 @@ def populate_mountpoint_store(store, used_mountpoints): def validate_label(label, fmt): """Returns a code indicating either that the given label can be set for - this filesystem or the reason why it can not. + this filesystem or the reason why it cannot. - In the case where the format can not assign a label, the empty string + In the case where the format cannot assign a label, the empty string stands for accept the default, but in the case where the format can assign a label the empty string represents itself. @@ -110,14 +133,14 @@ def validate_label(label, fmt): """ if fmt.exists: - return _("Can not relabel already existing filesystem.") + return _("Cannot relabel already existing file system.") if not fmt.labeling(): if label == "": return "" else: - return _("Can not set label on filesystem.") + return _("Cannot set label on file system.") if not fmt.labelFormatOK(label): - return _("Unacceptable label format for filesystem.") + return _("Unacceptable label format for file system.") return "" def validate_mountpoint(mountpoint, used_mountpoints, strict=True): @@ -129,7 +152,7 @@ def validate_mountpoint(mountpoint, used_mountpoints, strict=True): if mountpoint in used_mountpoints: return _("That mount point is already in use. Try something else?") elif not mountpoint: - return _("Please enter a valid mountpoint.") + return _("Please enter a valid mount point.") elif mountpoint in system_mountpoints: return _("That mount point is invalid. Try something else?") elif (lowerASCII(mountpoint) not in fake_mountpoints and @@ -283,14 +306,17 @@ def containerRaidLevelsSupported(device_type): return get_supported_raid_levels(DEVICE_TYPE_BTRFS).intersection(supported) return set() -def get_container_type_name(device_type): - return CONTAINER_TYPE_NAMES.get(device_type, _("container")) +def get_container_type(device_type): + return CONTAINER_TYPES.get(device_type, ContainerType(_("container"), _("container"))) class AddDialog(GUIObject): builderObjects = ["addDialog", "mountPointStore", "mountPointCompletion", "mountPointEntryBuffer"] mainWidgetName = "addDialog" uiFile = "spokes/lib/custom_storage_helpers.glade" + # If the user enters a smaller size, the GUI changes it to this value + MIN_SIZE_ENTRY = Size("1 MiB") + def __init__(self, *args, **kwargs): self.mountpoints = kwargs.pop("mountpoints", []) GUIObject.__init__(self, *args, **kwargs) @@ -317,7 +343,11 @@ class AddDialog(GUIObject): if self._error: return - self.size = size_from_entry(self.builder.get_object("addSizeEntry")) + self.size = size_from_entry( + self.builder.get_object("addSizeEntry"), + lower_bound=self.MIN_SIZE_ENTRY, + units=SIZE_UNITS_DEFAULT + ) self.window.destroy() def refresh(self): @@ -356,7 +386,7 @@ class ConfirmDeleteDialog(GUIObject): rootName = rootName.replace("_", "__") self._removeAll.set_label( C_("GUI|Custom Partitioning|Confirm Delete Dialog", - "Delete _all other filesystems in the %s root as well.") + "Delete _all other file systems in the %s root as well.") % rootName) self._removeAll.set_sensitive(rootName is not None) @@ -388,10 +418,10 @@ class DisksDialog(GUIObject): self._store = self.builder.get_object("disk_store") # populate the store for disk in self._disks: - self._store.append([disk.description, + self._store.append(["%s (%s)" % (disk.description, disk.serial), str(disk.size), str(free[disk.name][0]), - disk.serial, + disk.name, disk.id]) treeview = self.builder.get_object("disk_view") @@ -437,6 +467,9 @@ class ContainerDialog(GUIObject, GUIDialogInputCheckHandler): mainWidgetName = "container_dialog" uiFile = "spokes/lib/custom_storage_helpers.glade" + # If the user enters a smaller size, the GUI changes it to this value + MIN_SIZE_ENTRY = Size("1 MiB") + def __init__(self, *args, **kwargs): GUIDialogInputCheckHandler.__init__(self) @@ -462,11 +495,11 @@ class ContainerDialog(GUIObject, GUIDialogInputCheckHandler): self._grabObjects() # set up the dialog labels with device-type-specific text - container_type = get_container_type_name(self.device_type) - title_text = _(CONTAINER_DIALOG_TITLE) % {"container_type": container_type.upper()} + container_type = get_container_type(self.device_type) + title_text = _(CONTAINER_DIALOG_TITLE) % {"container_type": container_type.name.upper()} self._title_label.set_text(title_text) - dialog_text = _(CONTAINER_DIALOG_TEXT) % {"container_type": container_type.lower()} + dialog_text = _(CONTAINER_DIALOG_TEXT) % {"container_type": container_type.name.lower()} self._dialog_label.set_text(dialog_text) # populate the dialog widgets @@ -502,7 +535,7 @@ class ContainerDialog(GUIObject, GUIDialogInputCheckHandler): self._populate_raid() self._original_size = self.size - self._original_size_text = self.size.humanReadable(max_places=None) + self._original_size_text = self.size.humanReadable(max_places=2) self._sizeEntry.set_text(self._original_size_text) if self.size_policy == SIZE_POLICY_AUTO: self._sizeCombo.set_active(0) @@ -572,7 +605,11 @@ class ContainerDialog(GUIObject, GUIDialogInputCheckHandler): size = SIZE_POLICY_MAX elif idx == 2: if self._original_size_text != self._sizeEntry.get_text(): - size = size_from_entry(self._sizeEntry) + size = size_from_entry( + self._sizeEntry, + lower_bound=self.MIN_SIZE_ENTRY, + units=SIZE_UNITS_DEFAULT + ) if size is None: size = SIZE_POLICY_MAX else: diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/dasdfmt.glade b/anaconda/pyanaconda/ui/gui/spokes/lib/dasdfmt.glade index d5280c3..f93bcbc 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/dasdfmt.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/dasdfmt.glade @@ -4,7 +4,7 @@ <requires lib="gtk+" version="3.6"/> <object class="GtkDialog" id="unformattedDasdDialog"> <property name="can_focus">False</property> - <property name="border_width">5</property> + <property name="border_width">6</property> <property name="title" translatable="yes">UNFORMATTED DASDS</property> <property name="modal">True</property> <property name="type_hint">dialog</property> @@ -67,7 +67,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="valign">start</property> - <property name="label" translatable="yes">The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs can not be used during installation.</property> + <property name="label" translatable="yes">The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs cannot be used during installation.</property> <property name="wrap">True</property> <attributes> <attribute name="font-desc" value="Cantarell 12"/> @@ -254,19 +254,6 @@ installation options while formatting completes.</property> <property name="top_attach">0</property> </packing> </child> - <child> - <object class="GtkLabel" id="label19"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="xalign">0</property> - <property name="label" translatable="yes">Pressing 'OK' below will take you to the disk selection screen where you will need to re-select your disks.</property> - <property name="wrap">True</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> <child> <object class="GtkImage" id="image3"> <property name="visible">True</property> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/lang_locale_handler.py b/anaconda/pyanaconda/ui/gui/spokes/lib/lang_locale_handler.py index 1874647..9eaaeb7 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/lang_locale_handler.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/lang_locale_handler.py @@ -85,9 +85,13 @@ class LangLocaleHandler(object): # Otherwise, filter the list showing only what is matched by the # text entry. Either the English or native names can match. - lowered = entry.lower() - translit = strip_accents(unicode(native, "utf-8")).lower() - if lowered in native.lower() or lowered in english.lower() or lowered in translit: + # Convert strings to unicode so lower() works. + lowered = entry.decode('utf-8').lower() + native = native.decode('utf-8').lower() + english = english.decode('utf-8').lower() + translit = strip_accents(native).lower() + + if lowered in native or lowered in english or lowered in translit: return True else: return False diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/passphrase.py b/anaconda/pyanaconda/ui/gui/spokes/lib/passphrase.py index 685d711..c104194 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/passphrase.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/passphrase.py @@ -28,6 +28,7 @@ from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.helpers import GUIInputCheckHandler from pyanaconda.constants import PW_ASCII_CHARS from pyanaconda.i18n import _, N_ +from pyanaconda.ui.gui.utils import really_hide, really_show __all__ = ["PassphraseDialog"] @@ -59,6 +60,11 @@ class PassphraseDialog(GUIObject, GUIInputCheckHandler): self._strength_bar.add_offset_value("medium", 3) self._strength_bar.add_offset_value("high", 4) + # Configure the password policy, if available. Otherwise use defaults. + self.policy = self.data.anaconda.pwpolicy.get_policy("luks") + if not self.policy: + self.policy = self.data.anaconda.PwPolicyData() + # These will be set up later. self._pwq = None self._pwq_error = None @@ -79,6 +85,7 @@ class PassphraseDialog(GUIObject, GUIInputCheckHandler): # set up passphrase quality checker self._pwq = pwquality.PWQSettings() self._pwq.read_config() + self._pwq.minlen = self.policy.minlen # initialize with the previously set passphrase self.passphrase = self.data.autopart.passphrase @@ -111,7 +118,7 @@ class PassphraseDialog(GUIObject, GUIInputCheckHandler): try: strength = self._pwq.check(passphrase, None, None) except pwquality.PWQError as e: - self._pwq_error = e[1] + self._pwq_error = e.args[1] if strength < 50: val = 1 @@ -137,15 +144,16 @@ class PassphraseDialog(GUIObject, GUIInputCheckHandler): result_icon, result_message = failed_check.check_status self._passphrase_warning_image.set_from_icon_name(result_icon, Gtk.IconSize.BUTTON) self._passphrase_warning_label.set_text(result_message) - self._passphrase_warning_image.set_visible(True) - self._passphrase_warning_label.set_visible(True) + really_show(self._passphrase_warning_image) + really_show(self._passphrase_warning_label) else: - self._passphrase_warning_image.set_visible(False) - self._passphrase_warning_label.set_visible(False) + really_hide(self._passphrase_warning_image) + really_hide(self._passphrase_warning_label) # The save button should only be sensitive if the match check passes if self._passphrase_match_check.check_status == InputCheck.CHECK_OK and \ - self._confirm_match_check.check_status == InputCheck.CHECK_OK: + self._confirm_match_check.check_status == InputCheck.CHECK_OK and \ + (not self.policy.strict or self._strength_check.check_status == InputCheck.CHECK_OK): self._save_button.set_sensitive(True) else: self._save_button.set_sensitive(False) diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/refresh.py b/anaconda/pyanaconda/ui/gui/spokes/lib/refresh.py index 396c38f..639ef45 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/refresh.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/refresh.py @@ -25,7 +25,7 @@ from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.ui.gui import GUIObject from pyanaconda import constants -from blivet import storageInitialize +from blivet.osinstall import storageInitialize __all__ = ["RefreshDialog"] diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/resize.glade b/anaconda/pyanaconda/ui/gui/spokes/lib/resize.glade index 147f22e..4bb8f0e 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/resize.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/resize.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.16.1 --> +<!-- Generated with glade 3.18.3 --> <interface> <requires lib="gtk+" version="3.0"/> <object class="GtkTreeStore" id="diskStore"> @@ -171,7 +171,7 @@ </child> <child> <object class="GtkTreeViewColumn" id="fsColumn"> - <property name="title" translatable="yes">Filesystem</property> + <property name="title" translatable="yes">File System</property> <child> <object class="GtkCellRendererText" id="fsRenderer"/> <attributes> @@ -310,7 +310,7 @@ <property name="can_focus">False</property> <property name="halign">start</property> <property name="margin_bottom">6</property> - <property name="label"><b>%s disks; %s reclaimable space</b> (in filesystems)</property> + <property name="label"><b>%s disks; %s reclaimable space</b> (in file systems)</property> <property name="use_markup">True</property> </object> <packing> @@ -360,7 +360,12 @@ </child> <action-widgets> <action-widget response="0">cancelButton</action-widget> - <action-widget response="2">resizeButton</action-widget> + <action-widget response="1">resizeButton</action-widget> </action-widgets> + <child internal-child="accessible"> + <object class="AtkObject" id="resizeDialog-atkobject"> + <property name="AtkObject::accessible-name" translatable="yes">Reclaim</property> + </object> + </child> </object> </interface> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/resize.py b/anaconda/pyanaconda/ui/gui/spokes/lib/resize.py index e71ee92..740e40e 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/resize.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/resize.py @@ -26,7 +26,7 @@ from gi.repository import Gdk, Gtk from pyanaconda.i18n import _, C_, N_, P_ from pyanaconda.ui.gui import GUIObject -from pyanaconda.ui.gui.utils import escape_markup, timed_action +from pyanaconda.ui.gui.utils import blockedHandler, escape_markup, timed_action from blivet.size import Size __all__ = ["ResizeDialog"] @@ -95,7 +95,7 @@ class ResizeDialog(GUIObject): # First, try to find the partition in some known Root. If we find # it, return the mountpoint as the description. for root in self.storage.roots: - for (mount, device) in root.mounts.iteritems(): + for (mount, device) in root.mounts.items(): if device == part: return "%s (%s)" % (mount, root.name) @@ -210,13 +210,13 @@ class ResizeDialog(GUIObject): self._update_labels(totalDisks, totalReclaimableSpace, 0) - description = _("You can remove existing filesystems you no longer need to free up space " - "for this installation. Removing a filesystem will permanently delete all " + description = _("You can remove existing file systems you no longer need to free up space " + "for this installation. Removing a file system will permanently delete all " "of the data it contains.") if canShrinkSomething: description += "\n\n" - description += _("There is also free space available in pre-existing filesystems. " + description += _("There is also free space available in pre-existing file systems. " "While it's risky and we recommend you back up your data first, you " "can recover that free disk space and make it available for this " "installation below.") @@ -226,8 +226,8 @@ class ResizeDialog(GUIObject): def _update_labels(self, nDisks=None, totalReclaimable=None, selectedReclaimable=None): if nDisks is not None and totalReclaimable is not None: - text = P_("<b>%(count)s disk; %(size)s reclaimable space</b> (in filesystems)", - "<b>%(count)s disks; %(size)s reclaimable space</b> (in filesystems)", + text = P_("<b>%(count)s disk; %(size)s reclaimable space</b> (in file systems)", + "<b>%(count)s disks; %(size)s reclaimable space</b> (in file systems)", nDisks) % {"count" : escape_markup(str(nDisks)), "size" : escape_markup(totalReclaimable)} self._reclaimable_label.set_markup(text) @@ -261,9 +261,9 @@ class ResizeDialog(GUIObject): fivePercent = int(distance / 20) twentyPercent = int(distance / 5) - self._resizeSlider.handler_block_by_func(self.on_resize_value_changed) - self._resizeSlider.set_range(minSize, size) - self._resizeSlider.handler_unblock_by_func(self.on_resize_value_changed) + with blockedHandler(self._resizeSlider, self.on_resize_value_changed): + self._resizeSlider.set_range(minSize, size) + self._resizeSlider.set_value(default_value) adjustment = self.builder.get_object("resizeAdjustment") diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/summary.glade b/anaconda/pyanaconda/ui/gui/spokes/lib/summary.glade index e1584f6..de78509 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/summary.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/summary.glade @@ -14,6 +14,8 @@ <column type="gchararray"/> <!-- column-name actionMountpoint --> <column type="gchararray"/> + <!-- column-name diskTooltip --> + <column type="gchararray"/> </columns> </object> <object class="GtkDialog" id="summaryDialog"> @@ -120,6 +122,7 @@ <property name="can_focus">True</property> <property name="model">actionStore</property> <property name="headers_clickable">False</property> + <property name="tooltip_column">5</property> <child internal-child="selection"> <object class="GtkTreeSelection" id="treeview-selection1"> <property name="mode">none</property> @@ -160,7 +163,7 @@ </child> <child> <object class="GtkTreeViewColumn" id="deviceNameColumn"> - <property name="title" translatable="yes">Device Name</property> + <property name="title" translatable="yes">Device</property> <child> <object class="GtkCellRendererText" id="deviceNameRenderer"/> <attributes> @@ -171,7 +174,7 @@ </child> <child> <object class="GtkTreeViewColumn" id="mountpointColumn"> - <property name="title" translatable="yes">Mountpoint</property> + <property name="title" translatable="yes">Mount point</property> <child> <object class="GtkCellRendererText" id="mountpointRenderer"/> <attributes> diff --git a/anaconda/pyanaconda/ui/gui/spokes/lib/summary.py b/anaconda/pyanaconda/ui/gui/spokes/lib/summary.py index f98db86..ea41734 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/lib/summary.py +++ b/anaconda/pyanaconda/ui/gui/spokes/lib/summary.py @@ -1,6 +1,6 @@ # Action summary dialog # -# Copyright (C) 2013 Red Hat, Inc. +# Copyright (C) 2013-2014 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of @@ -21,6 +21,7 @@ from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.utils import escape_markup +from pyanaconda.i18n import _ from blivet.deviceaction import ACTION_TYPE_DESTROY, ACTION_TYPE_RESIZE, ACTION_OBJECT_FORMAT @@ -49,11 +50,24 @@ class ActionSummaryDialog(GUIObject): if action.obj == ACTION_OBJECT_FORMAT: mountpoint = getattr(action.device.format, "mountpoint", "") + if hasattr(action.device, "description"): + desc = _("%(description)s (%(deviceName)s)") % {"deviceName": action.device.name, + "description": action.device.description} + serial = action.device.serial + elif hasattr(action.device, "disk"): + desc = _("%(deviceName)s on %(container)s") % {"deviceName": action.device.name, + "container": action.device.disk.description} + serial = action.device.disk.serial + else: + desc = action.device.name + serial = action.device.serial + self._store.append([i, typeString, action.objectTypeString, - action.device.name, - mountpoint]) + desc, + mountpoint, + serial]) # pylint: disable=arguments-differ def refresh(self, actions): diff --git a/anaconda/pyanaconda/ui/gui/spokes/network.glade b/anaconda/pyanaconda/ui/gui/spokes/network.glade index c39f768..2a7c19a 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/network.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/network.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.1 --> +<!-- Generated with glade 3.18.3 --> <interface> <requires lib="gtk+" version="3.0"/> <requires lib="AnacondaWidgets" version="1.0"/> @@ -13,15 +13,19 @@ <data> <row> <col id="0" translatable="yes">Bond</col> - <col id="1" translatable="yes">bond</col> + <col id="1">bond</col> + </row> + <row> + <col id="0" translatable="yes">Bridge</col> + <col id="1">bridge</col> </row> <row> <col id="0" translatable="yes">Team</col> - <col id="1" translatable="yes">team</col> + <col id="1">team</col> </row> <row> - <col id="0" translatable="yes">Vlan</col> - <col id="1" translatable="yes">vlan</col> + <col id="0" translatable="yes">VLAN</col> + <col id="1">vlan</col> </row> </data> </object> @@ -158,7 +162,7 @@ </object> <object class="AnacondaSpokeWindow" id="networkWindow"> <property name="can_focus">False</property> - <property name="window_name" translatable="yes">NETWORK & HOSTNAME</property> + <property name="window_name" translatable="yes">NETWORK & HOST NAME</property> <signal name="button-clicked" handler="on_back_clicked" swapped="no"/> <child internal-child="main_box"> <object class="GtkBox" id="AnacondaSpokeWindow-main_box1"> @@ -197,7 +201,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">start</property> - <property name="label" translatable="yes">Please use the live desktop environment's tools for customizing your network configuration. You can set the hostname here.</property> + <property name="label" translatable="yes">Please use the live desktop environment's tools for customizing your network configuration. You can set the host name here.</property> <property name="ellipsize">start</property> </object> <packing> @@ -617,7 +621,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">1</property> - <property name="label" translatable="yes">Vlan ID</property> + <property name="label" translatable="yes">VLAN ID</property> </object> <packing> <property name="left_attach">0</property> @@ -692,7 +696,7 @@ <property name="vexpand">True</property> <child> <object class="GtkButton" id="button_wired_options"> - <property name="label" translatable="yes" context="GUI|Network|Wired">C_onfigure...</property> + <property name="label" translatable="yes" context="GUI|Network|Wired">_Configure...</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> @@ -1051,7 +1055,6 @@ <child internal-child="entry"> <object class="GtkEntry" id="combobox-entry4"> <property name="can_focus">False</property> - <property name="invisible_char">●</property> </object> </child> </object> @@ -1623,7 +1626,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">1</property> - <property name="label" translatable="yes">Username</property> + <property name="label" translatable="yes">User Name</property> </object> <packing> <property name="left_attach">0</property> @@ -1891,7 +1894,6 @@ <object class="GtkEntry" id="entry_proxy_url"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -1974,7 +1976,6 @@ <object class="GtkEntry" id="entry_proxy_http"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -1997,7 +1998,6 @@ <object class="GtkEntry" id="entry_proxy_https"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -2008,7 +2008,6 @@ <object class="GtkEntry" id="entry_proxy_ftp"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -2019,7 +2018,6 @@ <object class="GtkEntry" id="entry_proxy_socks"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -2215,7 +2213,7 @@ <object class="GtkLabel" id="label_hostname"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes" context="GUI|Network">_Hostname:</property> + <property name="label" translatable="yes" context="GUI|Network">_Host Name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">entry_hostname</property> </object> @@ -2229,11 +2227,10 @@ <object class="GtkEntry" id="entry_hostname"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <property name="width_chars">35</property> <child internal-child="accessible"> <object class="AtkObject" id="entry_hostname-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">Hostname</property> + <property name="AtkObject::accessible-name" translatable="yes">Host Name</property> </object> </child> </object> @@ -2270,7 +2267,7 @@ </child> <child internal-child="accessible"> <object class="AtkObject" id="networkWindow-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">NETWORK & HOSTNAME</property> + <property name="AtkObject::accessible-name" translatable="yes">NETWORK & HOST NAME</property> </object> </child> </object> diff --git a/anaconda/pyanaconda/ui/gui/spokes/password.py b/anaconda/pyanaconda/ui/gui/spokes/password.py index c8e6d0d..8bd7f01 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/password.py +++ b/anaconda/pyanaconda/ui/gui/spokes/password.py @@ -32,7 +32,8 @@ from pyanaconda.ui.helpers import InputCheck from pyanaconda.constants import PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GUI,\ PASSWORD_STRENGTH_DESC, PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR,\ - PASSWORD_WEAK_CONFIRM, PASSWORD_WEAK_CONFIRM_WITH_ERROR, PW_ASCII_CHARS, PASSWORD_ASCII + PASSWORD_WEAK_CONFIRM, PASSWORD_WEAK_CONFIRM_WITH_ERROR, PASSWORD_DONE_TWICE,\ + PW_ASCII_CHARS, PASSWORD_ASCII __all__ = ["PasswordSpoke"] @@ -108,6 +109,11 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) + # Configure the password policy, if available. Otherwise use defaults. + self.policy = self.data.anaconda.pwpolicy.get_policy("root") + if not self.policy: + self.policy = self.data.anaconda.PwPolicyData() + def refresh(self): # Enable the input checks in case they were disabled on the last exit for check in self.checks: @@ -173,7 +179,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) """Check whether a password has been specified at all.""" # If the password was set by kickstart, skip this check - if self._kickstarted: + if self._kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK if self.lock.get_active(): return InputCheck.CHECK_OK @@ -233,7 +239,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) self._waiveStrengthClicks = 0 self._waiveASCIIClicks = 0 - self._pwq_valid, strength, self._pwq_error = validatePassword(pwtext, "root") + self._pwq_valid, strength, self._pwq_error = validatePassword(pwtext, "root", minlen=self.policy.minlen) if not pwtext: val = 0 @@ -267,9 +273,10 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) if (not self._pwq_valid) and (self._pwq_error): return self._pwq_error - pwstrength = self.pw_bar.get_value() + # use strength from policy, not bars + _valid, pwstrength, _error = validatePassword(pw, "root", minlen=self.policy.minlen) - if pwstrength < 2: + if pwstrength < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK @@ -279,10 +286,16 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) else: return _(PASSWORD_WEAK_CONFIRM) else: - if self._pwq_error: - return _(PASSWORD_WEAK_WITH_ERROR) % self._pwq_error + # non-strict allows done to be clicked twice + if self.policy.strict: + done_msg = "" else: - return _(PASSWORD_WEAK) + done_msg = _(PASSWORD_DONE_TWICE) + + if self._pwq_error: + return _(PASSWORD_WEAK_WITH_ERROR) % (self._pwq_error, done_msg) + else: + return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK @@ -304,10 +317,10 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler) return InputCheck.CHECK_OK def on_back_clicked(self, button): - # If the failed check is for password strenght or non-ASCII + # If the failed check is for password strength or non-ASCII # characters, add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) - if failed_check == self._pwStrengthCheck: + if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: diff --git a/anaconda/pyanaconda/ui/gui/spokes/software.py b/anaconda/pyanaconda/ui/gui/spokes/software.py index 8fa17cc..cdd3f3f 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/software.py +++ b/anaconda/pyanaconda/ui/gui/spokes/software.py @@ -25,18 +25,18 @@ from pyanaconda.flags import flags from pyanaconda.i18n import _, C_, CN_ from pyanaconda.packaging import PackagePayload, payloadMgr from pyanaconda.threads import threadMgr, AnacondaThread -from pyanaconda import constants +from pyanaconda import constants, iutil from pyanaconda.ui.communication import hubQ from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.gui.spokes.lib.detailederror import DetailedErrorDialog -from pyanaconda.ui.gui.utils import gtk_action_wait, escape_markup +from pyanaconda.ui.gui.utils import blockedHandler, gtk_action_wait, escape_markup from pyanaconda.ui.categories.software import SoftwareCategory import logging log = logging.getLogger("anaconda") -import sys +import sys, copy __all__ = ["SoftwareSelectionSpoke"] @@ -72,6 +72,12 @@ class SoftwareSelectionSpoke(NormalSpoke): self._environmentListBox = self.builder.get_object("environmentListBox") self._addonListBox = self.builder.get_object("addonListBox") + # Connect viewport scrolling with listbox focus events + environmentViewport = self.builder.get_object("environmentViewport") + addonViewport = self.builder.get_object("addonViewport") + self._environmentListBox.set_focus_vadjustment(environmentViewport.get_vadjustment()) + self._addonListBox.set_focus_vadjustment(addonViewport.get_vadjustment()) + # Used to store how the user has interacted with add-ons for the default add-on # selection logic. The dictionary keys are group IDs, and the values are selection # state constants. See refreshAddons for how the values are used. @@ -105,21 +111,23 @@ class SoftwareSelectionSpoke(NormalSpoke): if not env: return - addons = self._get_selected_addons() - for group in addons: - if group not in self.selectedGroups: - self.selectedGroups.append(group) + # Not a kickstart with packages, setup the environment and groups + if not (flags.automatedInstall and self.data.packages.seen): + addons = self._get_selected_addons() + for group in addons: + if group not in self.selectedGroups: + self.selectedGroups.append(group) - self._selectFlag = False - self.payload.data.packages.groupList = [] - self.payload.selectEnvironment(env) - self.environment = env - for group in self.selectedGroups: - self.payload.selectGroup(group) + self._selectFlag = False + self.payload.data.packages.groupList = [] + self.payload.selectEnvironment(env) + self.environment = env + for group in self.selectedGroups: + self.payload.selectGroup(group) - # And then save these values so we can check next time. - self._origAddons = addons - self._origEnvironment = self.environment + # And then save these values so we can check next time. + self._origAddons = addons + self._origEnvironment = self.environment hubQ.send_not_ready(self.__class__.__name__) hubQ.send_not_ready("SourceSpoke") @@ -275,6 +283,13 @@ class SoftwareSelectionSpoke(NormalSpoke): if self.environment not in self.payload.environments: self.environment = None + # If no environment is selected, use the default from the instclass. + # If nothing is set in the instclass, the first environment will be + # selected below. + if not self.environment and self.payload.instclass and \ + self.payload.instclass.defaultPackageEnvironment in self.payload.environments: + self.environment = self.payload.instclass.defaultPackageEnvironment + firstEnvironment = True firstRadio = None @@ -322,6 +337,12 @@ class SoftwareSelectionSpoke(NormalSpoke): check.set_active(selected) self._add_row(self._addonListBox, name, desc, check, self.on_checkbox_toggled) + @property + def _addSep(self): + """ Whether the addon list contains a separator. """ + return len(self.payload.environmentAddons[self.environment][0]) > 0 and \ + len(self.payload.environmentAddons[self.environment][1]) > 0 + def refreshAddons(self): if self.environment and (self.environment in self.payload.environmentAddons): self._clear_listbox(self._addonListBox) @@ -336,15 +357,12 @@ class SoftwareSelectionSpoke(NormalSpoke): # state will be used. Otherwise, the add-on will be selected if it is a default # for this environment. - addSep = len(self.payload.environmentAddons[self.environment][0]) > 0 and \ - len(self.payload.environmentAddons[self.environment][1]) > 0 - for grp in self.payload.environmentAddons[self.environment][0]: self._addAddon(grp) # This marks a separator in the view - only add it if there's both environment # specific and generic addons. - if addSep: + if self._addSep: self._addonListBox.insert(Gtk.Separator(), -1) for grp in self.payload.environmentAddons[self.environment][1]: @@ -358,9 +376,11 @@ class SoftwareSelectionSpoke(NormalSpoke): self.clear_info() def _allAddons(self): - return self.payload.environmentAddons[self.environment][0] + \ - [""] + \ - self.payload.environmentAddons[self.environment][1] + addons = copy.copy(self.payload.environmentAddons[self.environment][0]) + if self._addSep: + addons.append('') + addons += self.payload.environmentAddons[self.environment][1] + return addons def _get_selected_addons(self): retval = [] @@ -411,9 +431,8 @@ class SoftwareSelectionSpoke(NormalSpoke): box = row.get_children()[0] button = box.get_children()[0] - button.handler_block_by_func(self.on_radio_button_toggled) - button.set_active(True) - button.handler_unblock_by_func(self.on_radio_button_toggled) + with blockedHandler(button, self.on_radio_button_toggled): + button.set_active(True) # Remove all the groups that were selected by the previously # selected environment. @@ -438,9 +457,8 @@ class SoftwareSelectionSpoke(NormalSpoke): wasActive = group in self.selectedGroups - button.handler_block_by_func(self.on_checkbox_toggled) - button.set_active(not wasActive) - button.handler_unblock_by_func(self.on_checkbox_toggled) + with blockedHandler(button, self.on_checkbox_toggled): + button.set_active(not wasActive) if wasActive: self.selectedGroups.remove(group) @@ -474,6 +492,7 @@ class SoftwareSelectionSpoke(NormalSpoke): if rc == 0: # Quit. + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(0) elif rc == 1: # Send the user to the installation source spoke. diff --git a/anaconda/pyanaconda/ui/gui/spokes/source.glade b/anaconda/pyanaconda/ui/gui/spokes/source.glade index a3969ea..fe81465 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/source.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/source.glade @@ -236,7 +236,7 @@ </child> <child> <object class="GtkButton" id="proxyOkButton"> - <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">_Ok</property> + <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">_OK</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> @@ -297,7 +297,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">_Proxy URL</property> + <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">_Proxy URL:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyURLEntry</property> <attributes> @@ -326,7 +326,6 @@ <object class="GtkEntry" id="proxyURLEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <property name="width_chars">40</property> </object> <packing> @@ -373,7 +372,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">User_name</property> + <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">User _name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyUsernameEntry</property> <attributes> @@ -390,7 +389,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">Pass_word</property> + <property name="label" translatable="yes" context="GUI|Software Source|Proxy Dialog">Pass_word:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">proxyPasswordEntry</property> <attributes> @@ -406,7 +405,6 @@ <object class="GtkEntry" id="proxyUsernameEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_proxyUsernameEntry_changed" swapped="no"/> </object> <packing> @@ -508,18 +506,18 @@ <property name="vexpand">True</property> <property name="orientation">vertical</property> <child> - <object class="GtkScrolledWindow" id="scrolledwindow1"> + <object class="GtkScrolledWindow" id="mainScrolledWindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscrollbar_policy">never</property> <child> - <object class="GtkViewport" id="viewport1"> + <object class="GtkViewport" id="mainViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_right">24</property> <property name="shadow_type">none</property> <child> - <object class="GtkBox" id="box4"> + <object class="GtkBox" id="mainBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> @@ -770,7 +768,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> </object> <packing> <property name="left_attach">1</property> @@ -849,7 +846,6 @@ <property name="has_tooltip">True</property> <property name="tooltip_markup" translatable="yes">This field is optional.</property> <property name="tooltip_text" translatable="yes">This field is optional.</property> - <property name="invisible_char">●</property> </object> <packing> <property name="expand">True</property> @@ -1084,6 +1080,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="hexpand">True</property> + <property name="row_spacing">6</property> <property name="column_spacing">6</property> <child> <object class="GtkEntry" id="repoUrlEntry"> @@ -1091,7 +1088,6 @@ <property name="can_focus">True</property> <property name="tooltip_markup" translatable="yes">URL for the repository, without protocol.</property> <property name="tooltip_text" translatable="yes">URL for the repository, without protocol.</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_repoUrl_changed" swapped="no"/> </object> <packing> @@ -1105,7 +1101,6 @@ <property name="can_focus">True</property> <property name="tooltip_markup" translatable="yes">URL of proxy in the form of protocol://host:[port]</property> <property name="tooltip_text" translatable="yes">URL of proxy in the form of protocol://host:[port]</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_repoProxy_changed" swapped="no"/> </object> <packing> @@ -1117,9 +1112,8 @@ <object class="GtkEntry" id="repoProxyUsernameEntry"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="tooltip_markup" translatable="yes">Optional proxy username.</property> - <property name="tooltip_text" translatable="yes">Optional proxy username.</property> - <property name="invisible_char">●</property> + <property name="tooltip_markup" translatable="yes">Optional proxy user name.</property> + <property name="tooltip_text" translatable="yes">Optional proxy user name.</property> <signal name="changed" handler="on_repoProxy_changed" swapped="no"/> </object> <packing> @@ -1218,7 +1212,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes" context="GUI|Software Source">U_sername:</property> + <property name="label" translatable="yes" context="GUI|Software Source">U_ser name:</property> <property name="use_underline">True</property> <property name="mnemonic_widget">repoProxyUsernameEntry</property> <attributes> @@ -1254,7 +1248,6 @@ <property name="tooltip_markup" translatable="yes">Name of the repository.</property> <property name="tooltip_text" translatable="yes">Name of the repository.</property> <property name="hexpand">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="on_repoNameEntry_changed" swapped="no"/> </object> <packing> diff --git a/anaconda/pyanaconda/ui/gui/spokes/source.py b/anaconda/pyanaconda/ui/gui/spokes/source.py index 67e968c..28e3dc4 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/source.py +++ b/anaconda/pyanaconda/ui/gui/spokes/source.py @@ -38,7 +38,7 @@ from pyanaconda.ui.gui import GUIObject from pyanaconda.ui.gui.helpers import GUIDialogInputCheckHandler, GUISpokeInputCheckHandler from pyanaconda.ui.gui.spokes import NormalSpoke from pyanaconda.ui.categories.software import SoftwareCategory -from pyanaconda.ui.gui.utils import fire_gtk_action +from pyanaconda.ui.gui.utils import blockedHandler, fire_gtk_action from pyanaconda.iutil import ProxyString, ProxyStringError, cmp_obj_attrs from pyanaconda.ui.gui.utils import gtk_call_once, really_hide, really_show, fancy_set_sensitive from pyanaconda.threads import threadMgr, AnacondaThread @@ -46,7 +46,7 @@ from pyanaconda.packaging import PackagePayload, payloadMgr from pyanaconda.regexes import REPO_NAME_VALID, URL_PARSE, HOSTNAME_PATTERN_WITHOUT_ANCHORS from pyanaconda import constants -from blivet.util import get_mount_paths +from blivet.util import get_mount_device, get_mount_paths __all__ = ["SourceSpoke"] @@ -406,8 +406,8 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): # The / gets stripped off by payload.ISOImage self.data.method.dir = "/" + self._currentIsoFile if (old_source.method == "harddrive" and - old_source.partition == self.data.method.partition and - old_source.dir == self.data.method.dir): + self.storage.devicetree.resolveDevice(old_source.partition) == part and + old_source.dir in [self._currentIsoFile, "/" + self._currentIsoFile]): return False # Make sure anaconda doesn't touch this device. @@ -605,6 +605,11 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): self._proxyButton = self.builder.get_object("proxyButton") self._nfsOptsBox = self.builder.get_object("nfsOptsBox") + # Connect scroll events on the viewport with focus events on the box + mainViewport = self.builder.get_object("mainViewport") + mainBox = self.builder.get_object("mainBox") + mainBox.set_focus_vadjustment(mainViewport.get_vadjustment()) + def initialize(self): NormalSpoke.initialize(self) @@ -736,6 +741,10 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): added = False active = 0 idx = 0 + + if self.data.method.method == "harddrive": + methodDev = self.storage.devicetree.resolveDevice(self.data.method.partition) + for dev in potentialHdisoSources(self.storage.devicetree): # path model size format type uuid of format dev_info = { "model" : self._sanitize_model(dev.disk.model), @@ -750,8 +759,8 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): if dev_info["label"] != "": dev_info["label"] = "\n" + dev_info["label"] - store.append([dev, "%(model)s %(path)s (%(size)s MB) %(format)s %(label)s" % dev_info]) - if self.data.method.method == "harddrive" and self.data.method.partition in [dev.path, dev.name]: + store.append([dev, "%(model)s %(path)s (%(size)s) %(format)s %(label)s" % dev_info]) + if self.data.method.method == "harddrive" and dev == methodDev: active = idx added = True idx += 1 @@ -830,10 +839,20 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): # Setup the addon repos self._reset_repoStore() - # Then, some widgets get enabled/disabled/greyed out depending on - # how others are set up. We can use the signal handlers to handle - # that condition here too. - self.on_protocol_changed(self._protocolComboBox) + if self.data.method.method == "harddrive" and \ + get_mount_device(constants.DRACUT_ISODIR) == get_mount_device(constants.DRACUT_REPODIR): + # If the stage2 image is mounted from an HDISO source, there's really + # no way we can tear down that source to allow the user to change it. + # Thus, this portion of the spoke should be insensitive. + for widget in [self._autodetectButton, self._autodetectBox, self._isoButton, + self._isoBox, self._networkButton, self._networkBox]: + widget.set_sensitive(False) + widget.set_tooltip_text(_("The installation source is in use by the installer and cannot be changed.")) + else: + # Then, some widgets get enabled/disabled/greyed out depending on + # how others are set up. We can use the signal handlers to handle + # that condition here too. + self.on_protocol_changed(self._protocolComboBox) def _setup_no_updates(self): """ Setup the state of the No Updates checkbox. @@ -1160,6 +1179,33 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): self._clear_repo_info() self._repoEntryBox.set_sensitive(False) + def _unique_repo_name(self, name): + """ Return a unique variation of the name if it already + exists in the repo store. + + :param str name: Name to check + :returns: name or name with _%d appended + + The returned name will be 1 greater than any other entry in the store + with a _%d at the end of it. + """ + # Does this name exist in the store? If not, return it. + if not any(r[REPO_NAME_COL] == name for r in self._repoStore): + return name + + # If the name already ends with a _\d+ it needs to be stripped. + match = re.match(r"(.*)_\d+$", name) + if match: + name = match.group(1) + + # Find all of the names with _\d+ at the end + name_re = re.compile(r"("+re.escape(name)+r")_(\d+)") + matches = (name_re.match(r[REPO_NAME_COL]) for r in self._repoStore) + matches = [int(m.group(2)) for m in matches if m is not None] + + # Get the highest number, add 1, append to name + highest_index = max(matches) if matches else 0 + return name + ("_%d" % (highest_index + 1)) def on_repoSelection_changed(self, *args): """ Called when the selection changed. @@ -1184,9 +1230,10 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): and reset the checkbox and combobox. """ self._repoNameEntry.set_text("") - self._repoMirrorlistCheckbox.handler_block_by_func(self.on_repoMirrorlistCheckbox_toggled) - self._repoMirrorlistCheckbox.set_active(False) - self._repoMirrorlistCheckbox.handler_unblock_by_func(self.on_repoMirrorlistCheckbox_toggled) + + with blockedHandler(self._repoMirrorlistCheckbox, self.on_repoMirrorlistCheckbox_toggled): + self._repoMirrorlistCheckbox.set_active(False) + self._repoUrlEntry.set_text("") self._repoProtocolComboBox.set_active(0) self._repoProxyUrlEntry.set_text("") @@ -1201,17 +1248,16 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): """ self._repoNameEntry.set_text(repo.name) - self._repoMirrorlistCheckbox.handler_block_by_func(self.on_repoMirrorlistCheckbox_toggled) - if repo.mirrorlist: - url = repo.mirrorlist - self._repoMirrorlistCheckbox.set_active(True) - else: - url = repo.baseurl - self._repoMirrorlistCheckbox.set_active(False) - self._repoMirrorlistCheckbox.handler_unblock_by_func(self.on_repoMirrorlistCheckbox_toggled) + with blockedHandler(self._repoMirrorlistCheckbox, self.on_repoMirrorlistCheckbox_toggled): + if repo.mirrorlist: + url = repo.mirrorlist + self._repoMirrorlistCheckbox.set_active(True) + else: + url = repo.baseurl + self._repoMirrorlistCheckbox.set_active(False) if url: - for idx, proto in REPO_PROTO.iteritems(): + for idx, proto in REPO_PROTO.items(): if url.startswith(proto): self._repoProtocolComboBox.set_active_id(idx) self._repoUrlEntry.set_text(url[len(proto):]) @@ -1255,7 +1301,8 @@ class SourceSpoke(NormalSpoke, GUISpokeInputCheckHandler): def on_addRepo_clicked(self, button): """ Add a new repository """ - repo = self.data.RepoData(name="New_Repository") + name = self._unique_repo_name("New_Repository") + repo = self.data.RepoData(name=name) repo.ks_repo = True repo.orig_name = "" diff --git a/anaconda/pyanaconda/ui/gui/spokes/storage.glade b/anaconda/pyanaconda/ui/gui/spokes/storage.glade index 6074750..0acdd3a 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/storage.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/storage.glade @@ -24,7 +24,7 @@ <property name="layout_style">end</property> <child> <object class="GtkButton" id="need_space_cancel_button"> - <property name="label" translatable="yes" context="GUI|Storage|Need Space Dialog">Cancel & _add more disks</property> + <property name="label" translatable="yes" context="GUI|Storage|Need Space Dialog">_Cancel & add more disks</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> @@ -225,8 +225,6 @@ <property name="border_width">6</property> <property name="modal">True</property> <property name="window_position">center-on-parent</property> - <property name="default_width">400</property> - <property name="default_height">300</property> <property name="destroy_with_parent">True</property> <property name="type_hint">dialog</property> <property name="decorated">False</property> @@ -484,18 +482,18 @@ <property name="orientation">vertical</property> <property name="spacing">6</property> <child> - <object class="GtkScrolledWindow" id="scrolledwindow1"> + <object class="GtkScrolledWindow" id="storageScrolledWindow"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="vexpand">True</property> <property name="hscrollbar_policy">never</property> <child> - <object class="GtkViewport" id="viewport1"> + <object class="GtkViewport" id="storageViewport"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="shadow_type">none</property> <child> - <object class="GtkBox" id="box3"> + <object class="GtkBox" id="storageMainBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> @@ -785,8 +783,8 @@ <property name="margin_left">18</property> <property name="margin_right">18</property> <property name="vexpand">False</property> - <property name="column_spacing">12</property> <property name="row_spacing">3</property> + <property name="column_spacing">12</property> <child> <object class="GtkLabel" id="label10"> <property name="visible">True</property> @@ -987,7 +985,7 @@ <object class="GtkLabel" id="label5"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes" context="GUI|Storage">_Full disk summary and bootloader...</property> + <property name="label" translatable="yes" context="GUI|Storage">_Full disk summary and boot loader...</property> <property name="use_underline">True</property> <attributes> <attribute name="underline" value="True"/> diff --git a/anaconda/pyanaconda/ui/gui/spokes/storage.py b/anaconda/pyanaconda/ui/gui/spokes/storage.py index fc65a0f..42138b3 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/storage.py +++ b/anaconda/pyanaconda/ui/gui/spokes/storage.py @@ -56,19 +56,19 @@ from pyanaconda.ui.helpers import StorageChecker from pyanaconda.kickstart import doKickstartStorage, refreshAutoSwapSize, resetCustomStorageData from blivet import arch +from blivet import autopart from blivet.size import Size -from blivet.devices import MultipathDevice +from blivet.devices import MultipathDevice, ZFCPDiskDevice from blivet.errors import StorageError, DasdFormatError from blivet.platform import platform -from blivet.devicelibs import swap as swap_lib from blivet.devicelibs.dasd import make_unformatted_dasd_list, format_dasd from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.product import productName from pyanaconda.flags import flags from pyanaconda.i18n import _, C_, CN_, P_ -from pyanaconda import constants -from pyanaconda import isys +from pyanaconda import constants, iutil, isys from pyanaconda.bootloader import BootLoaderError +from pyanaconda.storage_utils import on_disk_storage from pykickstart.constants import CLEARPART_TYPE_NONE, AUTOPART_TYPE_LVM from pykickstart.errors import KickstartValueError @@ -82,9 +82,13 @@ __all__ = ["StorageSpoke"] # Response ID codes for all the various buttons on all the dialogs. RESPONSE_CANCEL = 0 +RESPONSE_OK = 1 RESPONSE_MODIFY_SW = 2 RESPONSE_RECLAIM = 3 RESPONSE_QUIT = 4 +DASD_FORMAT_NO_CHANGE = -1 +DASD_FORMAT_REFRESH = 1 +DASD_FORMAT_RETURN_TO_HUB = 2 class InstallOptionsDialogBase(GUIObject): uiFile = "spokes/storage.glade" @@ -111,15 +115,26 @@ class InstallOptionsDialogBase(GUIObject): def _get_sw_needs_text(self, required_space, auto_swap): tooltip = _("Please wait... software metadata still loading.") - sw_text = (_("Your current <a href=\"\" title=\"%(tooltip)s\"><b>%(product)s</b> software " - "selection</a> requires <b>%(total)s</b> of available " - "space, including <b>%(software)s</b> for software and " - "<b>%(swap)s</b> for swap space.") - % {"tooltip": escape_markup(tooltip), - "product": escape_markup(productName), - "total": escape_markup(str(required_space + auto_swap)), - "software": escape_markup(str(required_space)), - "swap": escape_markup(str(auto_swap))}) + + if flags.livecdInstall: + sw_text = (_("Your current <b>%(product)s</b> software " + "selection requires <b>%(total)s</b> of available " + "space, including <b>%(software)s</b> for software and " + "<b>%(swap)s</b> for swap space.") + % {"product": escape_markup(productName), + "total": escape_markup(str(required_space + auto_swap)), + "software": escape_markup(str(required_space)), + "swap": escape_markup(str(auto_swap))}) + else: + sw_text = (_("Your current <a href=\"\" title=\"%(tooltip)s\"><b>%(product)s</b> software " + "selection</a> requires <b>%(total)s</b> of available " + "space, including <b>%(software)s</b> for software and " + "<b>%(swap)s</b> for swap space.") + % {"tooltip": escape_markup(tooltip), + "product": escape_markup(productName), + "total": escape_markup(str(required_space + auto_swap)), + "software": escape_markup(str(required_space)), + "swap": escape_markup(str(auto_swap))}) return sw_text # Methods to handle sensitivity of the modify button. @@ -165,7 +180,9 @@ class NeedSpaceDialog(InstallOptionsDialogBase): "amounts of free space:") % sw_text label = self.builder.get_object("need_space_desc_label") label.set_markup(label_text) - label.connect("activate-link", self._modify_sw_link_clicked) + + if not flags.livecdInstall: + label.connect("activate-link", self._modify_sw_link_clicked) self._set_free_space_labels(disk_free, fs_free) @@ -192,15 +209,16 @@ class NoSpaceDialog(InstallOptionsDialogBase): # pylint: disable=arguments-differ def refresh(self, required_space, auto_swap, disk_free, fs_free): - sw_text = self._get_sw_needs_text(required_space, auto_swap) - label_text = (_("%(sw_text)s You don't have enough space available to install " - "<b>%(product)s</b>, even if you used all of the free space " - "available on the selected disks.") - % {"sw_text": escape_markup(sw_text), - "product": escape_markup(productName)}) + label_text = self._get_sw_needs_text(required_space, auto_swap) + label_text += (_(" You don't have enough space available to install " + "<b>%(product)s</b>, even if you used all of the free space " + "available on the selected disks.") + % {"product": escape_markup(productName)}) label = self.builder.get_object("no_space_desc_label") label.set_markup(label_text) - label.connect("activate-link", self._modify_sw_link_clicked) + + if not flags.livecdInstall: + label.connect("activate-link", self._modify_sw_link_clicked) self._set_free_space_labels(disk_free, fs_free) @@ -237,6 +255,8 @@ class StorageSpoke(NormalSpoke, StorageChecker): self.encrypted = False self.passphrase = "" self.selected_disks = self.data.ignoredisk.onlyuse[:] + self._last_selected_disks = None + self._back_clicked = False # This list contains all possible disks that can be included in the install. # All types of advanced disks should be set up for us ahead of time, so @@ -360,7 +380,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): msg = _("No disks selected") if flags.automatedInstall and not self.storage.rootDevice: - return msg + msg = _("Kickstart insufficient") elif threadMgr.get(constants.THREAD_DASDFMT): msg = _("Formatting DASDs") elif self.data.ignoredisk.onlyuse: @@ -385,8 +405,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): @property def advancedOverviews(self): - return filter(lambda child: isinstance(child, AnacondaWidgets.DiskOverview), - self.specialized_disks_box.get_children()) + return [child for child in self.specialized_disks_box.get_children() if isinstance(child, AnacondaWidgets.DiskOverview)] def _on_disk_clicked(self, overview, event): # This handler only runs for these two kinds of events, and only for @@ -447,13 +466,19 @@ class StorageSpoke(NormalSpoke, StorageChecker): self._cur_clicked_overview = overview def refresh(self): + self._back_clicked = False + self.disks = getDisks(self.storage.devicetree) # synchronize our local data store with the global ksdata disk_names = [d.name for d in self.disks] - # don't put disks with hidden formats in selected_disks self.selected_disks = [d for d in self.data.ignoredisk.onlyuse - if d in disk_names] + if d in disk_names] + + # unhide previously hidden disks so that they don't look like being + # empty (because of all child devices hidden) + self._unhide_disks() + self.autopart = self.data.autopart.autopart self.autoPartType = self.data.autopart.type if self.autoPartType is None: @@ -471,7 +496,10 @@ class StorageSpoke(NormalSpoke, StorageChecker): # handled here instead of refresh to take into account the user pressing # the rescan button on custom partitioning. for disk in filter(isLocalDisk, self.disks): - self._add_disk_overview(disk, self.local_disks_box) + # While technically local disks, zFCP devices are specialized + # storage and should not be shown here. + if disk.type is not "zfcp": + self._add_disk_overview(disk, self.local_disks_box) # Advanced disks are different. Because there can potentially be a lot # of them, we do not display them in the box by default. Instead, only @@ -481,7 +509,11 @@ class StorageSpoke(NormalSpoke, StorageChecker): if name not in disk_names: continue obj = self.storage.devicetree.getDeviceByName(name, hidden=True) - if isLocalDisk(obj): + # since zfcp devices may be detected as local disks when added + # manually, specifically check the disk type here to make sure + # we won't accidentally bypass adding zfcp devices to the disk + # overview + if isLocalDisk(obj) and obj.type is not "zfcp": continue self._add_disk_overview(obj, self.specialized_disks_box) @@ -499,7 +531,6 @@ class StorageSpoke(NormalSpoke, StorageChecker): self.set_warning(_("Error checking storage configuration. Click for details.")) elif self.warnings: self.set_warning(_("Warning checking storage configuration. Click for details.")) - self.window.show_all() def initialize(self): NormalSpoke.initialize(self) @@ -507,6 +538,17 @@ class StorageSpoke(NormalSpoke, StorageChecker): self.local_disks_box = self.builder.get_object("local_disks_box") self.specialized_disks_box = self.builder.get_object("specialized_disks_box") + # Connect the viewport adjustments to the child widgets + # See also https://bugzilla.gnome.org/show_bug.cgi?id=744721 + localViewport = self.builder.get_object("localViewport") + specializedViewport = self.builder.get_object("specializedViewport") + self.local_disks_box.set_focus_hadjustment(localViewport.get_hadjustment()) + self.specialized_disks_box.set_focus_hadjustment(specializedViewport.get_hadjustment()) + + mainViewport = self.builder.get_object("storageViewport") + mainBox = self.builder.get_object("storageMainBox") + mainBox.set_focus_vadjustment(mainViewport.get_vadjustment()) + threadMgr.add(AnacondaThread(name=constants.THREAD_STORAGE_WATCHER, target=self._initialize)) @@ -526,6 +568,11 @@ class StorageSpoke(NormalSpoke, StorageChecker): if isinstance(disk, MultipathDevice): desc = disk.wwid.split(":") description = ":".join(desc[0:3]) + "..." + ":".join(desc[-4:]) + elif isinstance(disk, ZFCPDiskDevice): + # manually mangle the desc of a zFCP device to be multi-line since + # it's so long it makes the disk selection screen look odd + description = _("FCP device %(hba_id)s\nWWPN %(wwpn)s\nLUN %(lun)s") % \ + {"hba_id": disk.hba_id, "wwpn": disk.wwpn, "lun": disk.fcp_lun} else: description = disk.description @@ -641,10 +688,6 @@ class StorageSpoke(NormalSpoke, StorageChecker): log.error(str(err)) continue - # I really hate doing this, but the way is the way; probably the most - # correct way to kajigger the storage spoke into becoming ready - self.execute() - # signal handlers def on_summary_clicked(self, button): # show the selected disks dialog @@ -669,8 +712,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): self.data.bootloader.seen = True if self.data.bootloader.location == "none": - self.set_warning(_("You have chosen to skip bootloader installation. Your system may not be bootable.")) - self.window.show_all() + self.set_warning(_("You have chosen to skip boot loader installation. Your system may not be bootable.")) else: self.clear_info() @@ -680,12 +722,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): return rc - def _check_encrypted(self): - # even if they're not doing autopart, setting autopart.encrypted - # establishes a default of encrypting new devices - if not self.encrypted: - return True - + def _setup_passphrase(self): dialog = PassphraseDialog(self.data) rc = self.run_lightbox_dialog(dialog) if rc != 1: @@ -700,63 +737,40 @@ class StorageSpoke(NormalSpoke, StorageChecker): return True - def on_back_clicked(self, button): - # We can't exit early if it looks like nothing has changed because the - # user might want to change settings presented in the dialogs shown from - # within this method. + def _remove_nonexistant_partitions(self): + for partition in self.storage.partitions[:]: + # check if it's been removed in a previous iteration + if not partition.exists and \ + partition in self.storage.partitions: + self.storage.recursiveRemove(partition) - # Remove all non-existing devices if autopart was active when we last - # refreshed. - if self._previous_autopart: - self._previous_autopart = False - for partition in self.storage.partitions[:]: - # check if it's been removed in a previous iteration - if not partition.exists and \ - partition in self.storage.partitions: - self.storage.recursiveRemove(partition) - - # hide/unhide disks as requested + def _hide_disks(self): for disk in self.disks: if disk.name not in self.selected_disks and \ disk in self.storage.devices: self.storage.devicetree.hide(disk) - elif disk.name in self.selected_disks and \ - disk not in self.storage.devices: - self.storage.devicetree.unhide(disk) - # show the installation options dialog - disks = [d for d in self.disks if d.name in self.selected_disks] - disks_size = sum((d.size for d in disks), Size(0)) + def _unhide_disks(self): + if self._last_selected_disks: + for disk in self.disks: + if disk.name not in self.selected_disks and \ + disk.name not in self._last_selected_disks: + self.storage.devicetree.unhide(disk) - # No disks selected? The user wants to back out of the storage spoke. - if not disks: - NormalSpoke.on_back_clicked(self, button) - return + def _check_dasd_formats(self): + rc = DASD_FORMAT_NO_CHANGE + dasds = make_unformatted_dasd_list(self.selected_disks) + if len(dasds) > 0: + # We want to apply current selection before running dasdfmt to + # prevent this information from being lost afterward + applyDiskSelection(self.storage, self.data, self.selected_disks) + dialog = DasdFormatDialog(self.data, self.storage, dasds) + ignoreEscape(dialog.window) + rc = self.run_lightbox_dialog(dialog) - if arch.isS390(): - # check for unformatted DASDs and launch dasdfmt if any discovered - dasds = make_unformatted_dasd_list(self.selected_disks) - if len(dasds) > 0: - # We want to apply current selection before running dasdfmt to - # prevent this information from being lost afterward - applyDiskSelection(self.storage, self.data, self.selected_disks) - dialog = DasdFormatDialog(self.data, self.storage, dasds) - ignoreEscape(dialog.window) - rc = self.run_lightbox_dialog(dialog) - if rc == 1: - # User hit OK on the dialog - self.refresh() - elif rc == 2: - # User clicked uri to return to hub. - NormalSpoke.on_back_clicked(self, button) - return - elif rc != 2: - # User either hit cancel on the dialog or closed it via escape, - # there was no formatting done. - # NOTE: rc == 2 means the user clicked on the link that takes t - # back to the hub. - return + return rc + def _check_space_and_get_dialog(self, disks): # Figure out if the existing disk labels will work on this platform # you need to have at least one of the platform's labels in order for # any of the free space to be useful. @@ -771,16 +785,17 @@ class StorageSpoke(NormalSpoke, StorageChecker): else: free_space = self.storage.getFreeSpace(disks=disks, clearPartType=CLEARPART_TYPE_NONE) - disk_free = sum(f[0] for f in free_space.itervalues()) - fs_free = sum(f[1] for f in free_space.itervalues()) + disk_free = sum(f[0] for f in free_space.values()) + fs_free = sum(f[1] for f in free_space.values()) + disks_size = sum((d.size for d in disks), Size(0)) required_space = self.payload.spaceRequired auto_swap = sum((r.size for r in self.storage.autoPartitionRequests if r.fstype == "swap"), Size(0)) if self.autopart and auto_swap == Size(0): # autopartitioning requested, but not applied yet (=> no auto swap # requests), ask user for enough space to fit in the suggested swap - auto_swap = swap_lib.swapSuggestion() + auto_swap = autopart.swapSuggestion() log.debug("disk free: %s fs free: %s sw needs: %s auto swap: %s", disk_free, fs_free, required_space, auto_swap) @@ -788,93 +803,154 @@ class StorageSpoke(NormalSpoke, StorageChecker): if disk_free >= required_space + auto_swap: dialog = None elif disks_size >= required_space: - if self._customPart.get_active() or self._reclaim.get_active(): - dialog = None - else: - dialog = NeedSpaceDialog(self.data, payload=self.payload) - dialog.refresh(required_space, auto_swap, disk_free, fs_free) - rc = self.run_lightbox_dialog(dialog) + dialog = NeedSpaceDialog(self.data, payload=self.payload) + dialog.refresh(required_space, auto_swap, disk_free, fs_free) else: dialog = NoSpaceDialog(self.data, payload=self.payload) dialog.refresh(required_space, auto_swap, disk_free, fs_free) - rc = self.run_lightbox_dialog(dialog) - if not dialog: - # Plenty of room - there's no need to pop up a dialog, so just send - # the user to wherever they asked to go. That's either the custom - # spoke or the hub. - # - OR - - # Not enough room, but the user checked the reclaim button. + # the 'dialog' variable is always set by the if statement above + return dialog - self.encrypted = self._encrypted.get_active() - - if self._customPart.get_active(): - self.autopart = False - self.skipTo = "CustomPartitioningSpoke" - else: - self.autopart = True - - # We might first need to ask about an encryption passphrase. - if not self._check_encrypted(): - return - - # Oh and then we might also want to go to the reclaim dialog. - if self._reclaim.get_active(): - self.apply() - if not self._show_resize_dialog(disks): - # User pressed cancel on the reclaim dialog, so don't leave - # the storage spoke. - return - elif rc == RESPONSE_CANCEL: - # A cancel button was clicked on one of the dialogs. Stay on this - # spoke. Generally, this is because the user wants to add more disks. - return - elif rc == RESPONSE_MODIFY_SW: - # The "Fedora software selection" link was clicked on one of the - # dialogs. Send the user to the software spoke. - self.skipTo = "SoftwareSelectionSpoke" - elif rc == RESPONSE_RECLAIM: - # Not enough space, but the user can make enough if they do some - # work and free up space. - self.encrypted = self._encrypted.get_active() - - if not self._check_encrypted(): - return + def _run_dialogs(self, disks, start_with): + rc = self.run_lightbox_dialog(start_with) + if rc == RESPONSE_RECLAIM: + # we need to run another dialog + # respect disk selection and other choices in the ReclaimDialog self.apply() - if not self._show_resize_dialog(disks): - # User pressed cancel on the reclaim dialog, so don't leave - # the storage spoke. + resize_dialog = ResizeDialog(self.data, self.storage, self.payload) + resize_dialog.refresh(disks) + + return self._run_dialogs(disks, start_with=resize_dialog) + else: + # we are done + return rc + + def on_back_clicked(self, button): + # We can't exit early if it looks like nothing has changed because the + # user might want to change settings presented in the dialogs shown from + # within this method. + + # Do not enter this method multiple times if user clicking multiple times + # on back button + if self._back_clicked: + return + else: + self._back_clicked = True + + # make sure the snapshot of unmodified on-disk-storage model is created + if not on_disk_storage.created: + on_disk_storage.create_snapshot(self.storage) + + # No disks selected? The user wants to back out of the storage spoke. + if not self.selected_disks: + NormalSpoke.on_back_clicked(self, button) + return + + disk_selection_changed = False + if self._last_selected_disks: + disk_selection_changed = (self._last_selected_disks != set(self.selected_disks)) + + # remember the disk selection for future decisions + self._last_selected_disks = set(self.selected_disks) + + if disk_selection_changed: + # Changing disk selection is really, really complicated and has + # always been causing numerous hard bugs. Let's not play the hero + # game and just revert everything and start over again. + on_disk_storage.reset_to_snapshot(self.storage) + self.disks = getDisks(self.storage.devicetree) + else: + # Remove all non-existing devices if autopart was active when we last + # refreshed. + if self._previous_autopart: + self._previous_autopart = False + self._remove_nonexistant_partitions() + + # hide disks as requested + self._hide_disks() + + if arch.isS390(): + # check for unformatted DASDs and launch dasdfmt if any discovered + rc = self._check_dasd_formats() + if rc == DASD_FORMAT_NO_CHANGE: + pass + elif rc == DASD_FORMAT_REFRESH: + # User hit OK on the dialog + self.refresh() + elif rc == DASD_FORMAT_RETURN_TO_HUB: + # User clicked uri to return to hub. + NormalSpoke.on_back_clicked(self, button) + return + else: + # User either hit cancel on the dialog or closed it via escape, + # there was no formatting done. + self._back_clicked = False return - # And then go to the custom partitioning spoke if they chose to - # do so. - if self._customPart.get_active(): - self.autopart = False - self.skipTo = "CustomPartitioningSpoke" - else: - self.autopart = True - elif rc == RESPONSE_QUIT: - # Not enough space, and the user can't do anything about it so - # they chose to quit. - raise SystemExit("user-selected exit") - else: - # I don't know how we'd get here, but might as well have a - # catch-all. Just stay on this spoke. + # even if they're not doing autopart, setting autopart.encrypted + # establishes a default of encrypting new devices + self.encrypted = self._encrypted.get_active() + + # We might first need to ask about an encryption passphrase. + if self.encrypted and not self._setup_passphrase(): + self._back_clicked = False return + # At this point there are three possible states: + # 1) user chose custom part => just send them to the CustomPart spoke + # 2) user wants to reclaim some more space => run the ResizeDialog + # 3) we are just asked to do autopart => check free space and see if we need + # user to do anything more + self.autopart = not self._customPart.get_active() + disks = [d for d in self.disks if d.name in self.selected_disks] + dialog = None + if not self.autopart: + self.skipTo = "CustomPartitioningSpoke" + elif self._reclaim.get_active(): + # HINT: change the logic of this 'if' statement if we are asked to + # support "reclaim before custom partitioning" + + # respect disk selection and other choices in the ReclaimDialog + self.apply() + dialog = ResizeDialog(self.data, self.storage, self.payload) + dialog.refresh(disks) + else: + dialog = self._check_space_and_get_dialog(disks) + + if dialog: + # more dialogs may need to be run based on user choices, but we are + # only interested in the final result + rc = self._run_dialogs(disks, start_with=dialog) + + if rc == RESPONSE_OK: + # nothing special needed + pass + elif rc == RESPONSE_CANCEL: + # A cancel button was clicked on one of the dialogs. Stay on this + # spoke. Generally, this is because the user wants to add more disks. + self._back_clicked = False + return + elif rc == RESPONSE_MODIFY_SW: + # The "Fedora software selection" link was clicked on one of the + # dialogs. Send the user to the software spoke. + self.skipTo = "SoftwareSelectionSpoke" + elif rc == RESPONSE_QUIT: + # Not enough space, and the user can't do anything about it so + # they chose to quit. + raise SystemExit("user-selected exit") + else: + # I don't know how we'd get here, but might as well have a + # catch-all. Just stay on this spoke. + self._back_clicked = False + return + if self.autopart: refreshAutoSwapSize(self.storage) self.applyOnSkip = True NormalSpoke.on_back_clicked(self, button) - def _show_resize_dialog(self, disks): - resizeDialog = ResizeDialog(self.data, self.storage, self.payload) - resizeDialog.refresh(disks) - - rc = self.run_lightbox_dialog(resizeDialog) - return rc - def on_custom_toggled(self, button): # The custom button won't be active until after this handler is run, # so we have to negate everything here. @@ -917,6 +993,7 @@ class StorageSpoke(NormalSpoke, StorageChecker): if rc == 0: # Quit. sys.exit(0) + iutil.ipmi_report(constants.IPMI_ABORTED) elif self.warnings: label = _("The following warnings were encountered when checking your storage " "configuration. These are not fatal, but you may wish to make " diff --git a/anaconda/pyanaconda/ui/gui/spokes/user.glade b/anaconda/pyanaconda/ui/gui/spokes/user.glade index cecbdd1..335c3c7 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/user.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/user.glade @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.18.1 --> +<!-- Generated with glade 3.18.3 --> <interface> <requires lib="gtk+" version="3.6"/> <requires lib="AnacondaWidgets" version="1.0"/> @@ -77,7 +77,7 @@ <property name="can_focus">False</property> <property name="xalign">1</property> <property name="xpad">10</property> - <property name="label" translatable="yes" context="GUI|User">_Username</property> + <property name="label" translatable="yes" context="GUI|User">_User name</property> <property name="use_underline">True</property> <property name="mnemonic_widget">t_username</property> <attributes> @@ -93,7 +93,6 @@ <object class="GtkEntry" id="t_fullname"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <property name="caps_lock_warning">False</property> <signal name="changed" handler="full_name_changed" swapped="no"/> <child internal-child="accessible"> @@ -111,11 +110,10 @@ <object class="GtkEntry" id="t_username"> <property name="visible">True</property> <property name="can_focus">True</property> - <property name="invisible_char">●</property> <signal name="changed" handler="username_changed" swapped="no"/> <child internal-child="accessible"> <object class="AtkObject" id="t_username-atkobject"> - <property name="AtkObject::accessible-name" translatable="yes">Username</property> + <property name="AtkObject::accessible-name" translatable="yes">User Name</property> </object> </child> </object> @@ -200,7 +198,7 @@ <property name="visible">True</property> <property name="can_focus">False</property> <property name="xalign">0</property> - <property name="label" translatable="yes"><b>Tip:</b> Keep your username shorter than 32 characters and do not use spaces.</property> + <property name="label" translatable="yes"><b>Tip:</b> Keep your user name shorter than 32 characters and do not use spaces.</property> <property name="use_markup">True</property> </object> <packing> @@ -210,10 +208,11 @@ </child> <child> <object class="GtkCheckButton" id="c_usepassword"> - <property name="label" translatable="yes" context="GUI|User">Require a password to use this account</property> + <property name="label" translatable="yes" context="GUI|User">_Require a password to use this account</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_underline">True</property> <property name="xalign">0</property> <property name="active">True</property> <property name="draw_indicator">True</property> @@ -268,10 +267,11 @@ </child> <child> <object class="GtkCheckButton" id="c_admin"> - <property name="label" translatable="yes" context="GUI|User">Make this user administrator</property> + <property name="label" translatable="yes" context="GUI|User">_Make this user administrator</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">False</property> + <property name="use_underline">True</property> <property name="xalign">0</property> <property name="draw_indicator">True</property> </object> diff --git a/anaconda/pyanaconda/ui/gui/spokes/user.py b/anaconda/pyanaconda/ui/gui/spokes/user.py index 98e830b..51cf067 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/user.py +++ b/anaconda/pyanaconda/ui/gui/spokes/user.py @@ -21,7 +21,7 @@ # import re - +import os from pyanaconda.flags import flags from pyanaconda.i18n import _, CN_ from pyanaconda.users import cryptPassword, validatePassword, guess_username @@ -37,7 +37,8 @@ from pykickstart.constants import FIRSTBOOT_RECONFIG from pyanaconda.constants import ANACONDA_ENVIRON, FIRSTBOOT_ENVIRON,\ PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GUI, PASSWORD_STRENGTH_DESC,\ PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR, PASSWORD_WEAK_CONFIRM,\ - PASSWORD_WEAK_CONFIRM_WITH_ERROR, PW_ASCII_CHARS, PASSWORD_ASCII + PASSWORD_WEAK_CONFIRM_WITH_ERROR, PASSWORD_DONE_TWICE,\ + PW_ASCII_CHARS, PASSWORD_ASCII from pyanaconda.regexes import GECOS_VALID, USERNAME_VALID, GROUPNAME_VALID, GROUPLIST_FANCY_PARSE __all__ = ["UserSpoke", "AdvancedUserDialog"] @@ -171,6 +172,8 @@ class AdvancedUserDialog(GUIObject, GUIDialogInputCheckHandler): # during any earlier run of the dialog, set homedir to the value # in the input box. homedir = self._tHome.get_text() + if not os.path.isabs(homedir): + homedir = "/" + homedir if self._homeSet or self._origHome != homedir: self._homeSet = True self._user.homedir = homedir @@ -276,6 +279,11 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): self.pw_bar.add_offset_value("medium", 3) self.pw_bar.add_offset_value("high", 4) + # Configure the password policy, if available. Otherwise use defaults. + self.policy = self.data.anaconda.pwpolicy.get_policy("user") + if not self.policy: + self.policy = self.data.anaconda.PwPolicyData() + # indicate when the password was set by kickstart self._user.password_kickstarted = self.data.user.seen if self._user.password_kickstarted: @@ -287,6 +295,13 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): self.usepassword.set_active(True) self.pw.set_placeholder_text(_("The password was set by kickstart.")) self.confirm.set_placeholder_text(_("The password was set by kickstart.")) + elif not self.policy.emptyok: + # Policy is that a non-empty password is required + self.usepassword.set_active(True) + + if not self.policy.emptyok: + # User isn't allowed to change whether password is required or not + self.usepassword.set_sensitive(False) # Password checks, in order of importance: # - if a password is required, is one specified? @@ -314,7 +329,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): # Allow empty usernames so the spoke can be exited without creating a user self.add_re_check(self.username, re.compile(USERNAME_VALID.pattern + r'|^$'), - _("Invalid username")) + _("Invalid user name")) self.add_re_check(self.fullname, GECOS_VALID, _("Full name cannot contain colon characters")) @@ -412,8 +427,10 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): @property def sensitive(self): + # Spoke cannot be entered if a user was set in the kickstart and the user + # policy doesn't allow changes. return not (self.completed and flags.automatedInstall - and self.data.user.seen) + and self.data.user.seen and not self.policy.changesok) @property def completed(self): @@ -499,7 +516,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): """ # If the password was set by kickstart, skip the strength check - if self._user.password_kickstarted: + if self._user.password_kickstarted and not self.policy.changesok: return InputCheck.CHECK_OK # Skip the check if no password is required @@ -557,9 +574,12 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): if (not self._pwq_valid) and (self._pwq_error): return self._pwq_error - pwstrength = self.pw_bar.get_value() + # use strength from policy, not bars + pw = self.pw.get_text() + username = self.username.get_text() + _valid, pwstrength, _error = validatePassword(pw, username, minlen=self.policy.minlen) - if pwstrength < 2: + if pwstrength < self.policy.minquality: # If Done has been clicked twice, waive the check if self._waiveStrengthClicks > 1: return InputCheck.CHECK_OK @@ -569,10 +589,16 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): else: return _(PASSWORD_WEAK_CONFIRM) else: - if self._pwq_error: - return _(PASSWORD_WEAK_WITH_ERROR) % self._pwq_error + # non-strict allows done to be clicked twice + if self.policy.strict: + done_msg = "" else: - return _(PASSWORD_WEAK) + done_msg = _(PASSWORD_DONE_TWICE) + + if self._pwq_error: + return _(PASSWORD_WEAK_WITH_ERROR) % (self._pwq_error, done_msg) + else: + return _(PASSWORD_WEAK) % done_msg else: return InputCheck.CHECK_OK @@ -614,10 +640,10 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler): self.admin.set_active(self._wheel.name in self._user.groups) def on_back_clicked(self, button): - # If the failed check is for password strength or non-ASCII - # characters, add a click to the counter and check again + # If the failed check is for non-ASCII characters, + # add a click to the counter and check again failed_check = next(self.failed_checks_with_message, None) - if failed_check == self._pwStrengthCheck: + if not self.policy.strict and failed_check == self._pwStrengthCheck: self._waiveStrengthClicks += 1 self._pwStrengthCheck.update_check_status() elif failed_check == self._pwASCIICheck: diff --git a/anaconda/pyanaconda/ui/gui/spokes/user.py.orig b/anaconda/pyanaconda/ui/gui/spokes/user.py.orig deleted file mode 100644 index 9370876..0000000 --- a/anaconda/pyanaconda/ui/gui/spokes/user.py.orig +++ /dev/null @@ -1,590 +0,0 @@ -# User creation spoke -# -# Copyright (C) 2013 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -# Red Hat Author(s): Martin Sivak <msivak@redhat.com> -# - -from pyanaconda.i18n import _, N_ -from pyanaconda.users import cryptPassword, validatePassword, guess_username - -from pyanaconda.ui.gui.spokes import NormalSpoke -from pyanaconda.ui.gui import GUIObject, GUIDialog, check_re, GUICheck -from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory -from pyanaconda.ui.common import FirstbootSpokeMixIn -from pyanaconda.ui.gui.utils import enlightbox - -from pykickstart.constants import FIRSTBOOT_RECONFIG -from pyanaconda.constants import ANACONDA_ENVIRON, FIRSTBOOT_ENVIRON,\ - PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GUI, PASSWORD_STRENGTH_DESC,\ - PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR, PASSWORD_WEAK_CONFIRM,\ - PASSWORD_WEAK_CONFIRM_WITH_ERROR -from pyanaconda.regexes import GECOS_VALID, USERNAME_VALID, GROUPNAME_VALID, GROUPLIST_FANCY_PARSE - -__all__ = ["UserSpoke", "AdvancedUserDialog"] - -def _checkUsername(editable, data): - """Validate a username. Allow empty usernames.""" - if not (editable.get_text()): - return GUICheck.CHECK_OK - else: - return check_re(editable, data) - -def _validateGroups(editable, data): - groups_list = editable.get_text().split(",") - - # Check each group name in the list - for group in groups_list: - group_name = GROUPLIST_FANCY_PARSE.match(group).group('name') - if not GROUPNAME_VALID.match(group_name): - return _("Invalid group name: %s") % group_name - - return GUICheck.CHECK_OK - -class AdvancedUserDialog(GUIDialog): - builderObjects = ["advancedUserDialog", "uid", "gid"] - mainWidgetName = "advancedUserDialog" - uiFile = "spokes/advanced_user.glade" - - def __init__(self, user, groupDict, data): - GUIDialog.__init__(self, data) - self._user = user - self._groupDict = groupDict - - def _grabObjects(self): - self._cHome = self.builder.get_object("c_home") - self._cUid = self.builder.get_object("c_uid") - self._cGid = self.builder.get_object("c_gid") - self._tHome = self.builder.get_object("t_home") - self._lHome = self.builder.get_object("l_home") - self._tGroups = self.builder.get_object("t_groups") - self._spinUid = self.builder.get_object("spin_uid") - self._spinGid = self.builder.get_object("spin_gid") - self._uid = self.builder.get_object("uid") - self._gid = self.builder.get_object("gid") - self._groupsError = self.builder.get_object("groups_error") - self._saveButton = self.builder.get_object("save_button") - - def initialize(self): - GUIObject.initialize(self) - - self._grabObjects() - - # Validate the group input box - self.add_check_with_error_label(editable=self._tGroups, - error_label=self._groupsError, - run_check=_validateGroups) - - def update_check(self, check, check_status): - # If there are any errors, disable the save button - self._saveButton.set_sensitive(not next(self.failed_checks, None)) - - return GUIDialog.update_check(self, check, check_status) - - def _apply_checkboxes(self, _editable = None, data = None): - """Update the state of this screen according to the - checkbox states on the screen. It is called from - the toggled Gtk event. - """ - c_home = self._cHome.get_active() - c_uid = self._cUid.get_active() - c_gid = self._cGid.get_active() - - self._tHome.set_sensitive(c_home) - self._lHome.set_sensitive(c_home) - self._spinUid.set_sensitive(c_uid) - self._spinGid.set_sensitive(c_gid) - - def _parse_groups(self): - group_strings = self._tGroups.get_text().split(",") - group_objects = [] - - for group in group_strings: - # Skip empty strings - if not group: - continue - - (group_name, group_id) = GROUPLIST_FANCY_PARSE.match(group).groups() - if group_id: - group_id = int(group_id) - - group_objects.append(self.data.GroupData(name=group_name, gid=group_id)) - - return group_objects - - def refresh(self): - if self._user.homedir: - self._tHome.set_text(self._user.homedir) - elif self._user.name: - homedir = "/home/" + self._user.name - self._tHome.set_text(homedir) - self._user.homedir = homedir - - self._cHome.set_active(bool(self._user.homedir)) - self._cUid.set_active(bool(self._user.uid)) - self._cGid.set_active(bool(self._user.gid)) - self._apply_checkboxes() - - self._spinUid.update() - self._spinGid.update() - - groups = [] - for group_name in self._user.groups: - group = self._groupDict[group_name] - - if group.name and group.gid is not None: - groups.append("%s (%d)" % (group.name, group.gid)) - elif group.name: - groups.append(group.name) - elif group.gid is not None: - groups.append("(%d)" % (group.gid,)) - - self._tGroups.set_text(", ".join(groups)) - - def run(self): - self.window.show() - rc = self.window.run() - self.window.hide() - - #OK clicked - if rc == 1: - if self._cHome.get_active(): - self._user.homedir = self._tHome.get_text() - else: - self._user.homedir = None - - if self._cUid.get_active(): - self._user.uid = int(self._uid.get_value()) - else: - self._user.uid = None - - if self._cGid.get_active(): - self._user.gid = int(self._gid.get_value()) - else: - self._user.gid = None - - groups = self._parse_groups() - self._user.groups = [] - self._groupDict.clear() - for group in groups: - self._groupDict[group.name] = group - self._user.groups.append(group.name) - - #Cancel clicked, window destroyed... - else: - pass - - return rc - -class UserSpoke(FirstbootSpokeMixIn, NormalSpoke): - builderObjects = ["userCreationWindow"] - - mainWidgetName = "userCreationWindow" - uiFile = "spokes/user.glade" - - category = UserSettingsCategory - - icon = "avatar-default-symbolic" - title = N_("_USER CREATION") - - @classmethod - def should_run(cls, environment, data): - # The Qubes installer still uses old firstboot to create users (TODO) - return False - - # the user spoke should run always in the anaconda and in firstboot only - # when doing reconfig or if no user has been created in the installation - if environment == ANACONDA_ENVIRON: - return True - elif environment == FIRSTBOOT_ENVIRON and data is None: - # cannot decide, stay in the game and let another call with data - # available (will come) decide - return True - elif environment == FIRSTBOOT_ENVIRON and data and \ - (data.firstboot.firstboot == FIRSTBOOT_RECONFIG or \ - len(data.user.userList) == 0): - return True - else: - return False - - def __init__(self, *args): - NormalSpoke.__init__(self, *args) - self._oldweak = None - - def initialize(self): - NormalSpoke.initialize(self) - - if self.data.user.userList: - self._user = self.data.user.userList[0] - else: - self._user = self.data.UserData() - self._wheel = self.data.GroupData(name = "wheel") - self._groupDict = {"wheel": self._wheel} - - # placeholders for the text boxes - self.fullname = self.builder.get_object("t_fullname") - self.username = self.builder.get_object("t_username") - self.pw = self.builder.get_object("t_password") - self.confirm = self.builder.get_object("t_verifypassword") - self.admin = self.builder.get_object("c_admin") - self.usepassword = self.builder.get_object("c_usepassword") - self.b_advanced = self.builder.get_object("b_advanced") - - # Counter for the click Done twice check - self._waivePasswordClicks = 0 - - self.guesser = { - self.username: True - } - - # Updated during the password changed event and used by the password - # field validity checker - self._pwq_error = None - self._pwq_valid = True - - self.pw_bar = self.builder.get_object("password_bar") - self.pw_label = self.builder.get_object("password_label") - - # Configure levels for the password bar - self.pw_bar.add_offset_value("low", 2) - self.pw_bar.add_offset_value("medium", 3) - self.pw_bar.add_offset_value("high", 4) - - # indicate when the password was set by kickstart - self._user.password_kickstarted = self.data.user.seen - if self._user.password_kickstarted: - self.usepassword.set_active(self._user.password != "") - if not self._user.isCrypted: - self.pw.set_text(self._user.password) - self.confirm.set_text(self._user.password) - else: - self.usepassword.set_active(True) - self.pw.set_placeholder_text(_("The password was set by kickstart.")) - self.confirm.set_placeholder_text(_("The password was set by kickstart.")) - - # Password checks, in order of importance: - # - if a password is required, is one specified? - # - if a password is specified and there is data in the confirm box, do they match? - # - if a password is specified and the confirm box is empty or match, how strong is it? - # - if a password is required, is there any data in the confirm box? - self.add_check(self.pw, self._checkPasswordEmpty) - - # The password confirmation needs to be checked whenever either of the password - # fields change. Separate checks are created on each field so that edits on - # either will trigger a check and so that the last edited field will get the focus - # when Done is clicked. Whichever check is run needs to run the other check in - # order to reset the status. The check_data field is used as a flag to prevent - # infinite recursion. - self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) - self._password_check = self.add_check(self.pw, self._checkPasswordConfirm) - - # Keep a reference to this check, since it has to be manually run for the - # click Done twice check. - self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) - - self.add_check(self.confirm, self._checkPasswordEmpty) - - # Allow empty usernames so the spoke can be exited without creating a user - self.add_check(self.username, _checkUsername, - {'regex': USERNAME_VALID, 'message': _("Invalid username")}) - - self.add_re_check(self.fullname, GECOS_VALID, _("Full name cannot contain colon characters")) - - self._advanced = AdvancedUserDialog(self._user, self._groupDict, - self.data) - self._advanced.initialize() - - def refresh(self): - # Enable the input checks in case they were disabled on the last exit - for check in self.checks: - check.enable() - - self.username.set_text(self._user.name) - self.fullname.set_text(self._user.gecos) - self.admin.set_active(self._wheel.name in self._user.groups) - - self.pw.emit("changed") - self.confirm.emit("changed") - - if self.username.get_text() and self.usepassword.get_active() and \ - self._user.password == "": - self.pw.grab_focus() - elif self.fullname.get_text(): - self.username.grab_focus() - else: - self.fullname.grab_focus() - - self.b_advanced.set_sensitive(bool(self._user.name)) - - @property - def status(self): - if len(self.data.user.userList) == 0: - return _("No user will be created") - elif self._wheel.name in self.data.user.userList[0].groups: - return _("Administrator %s will be created") % self.data.user.userList[0].name - else: - return _("User %s will be created") % self.data.user.userList[0].name - - @property - def mandatory(self): - # mandatory only if root account is disabled - return (not self.data.rootpw.password) or self.data.rootpw.lock - - def apply(self): - # set the password only if the user enters anything to the text entry - # this should preserve the kickstart based password - if self.usepassword.get_active(): - if self.pw.get_text(): - self._user.password_kickstarted = False - self._user.password = cryptPassword(self.pw.get_text()) - self._user.isCrypted = True - self.pw.set_placeholder_text("") - self.confirm.set_placeholder_text("") - - # reset the password when the user unselects it - else: - self.pw.set_placeholder_text("") - self.confirm.set_placeholder_text("") - self._user.password = "" - self._user.isCrypted = False - self._user.password_kickstarted = False - - self._user.name = self.username.get_text() - self._user.gecos = self.fullname.get_text() - - # Remove any groups that were created in a previous visit to this spoke - self.data.group.groupList = [g for g in self.data.group.groupList \ - if not hasattr(g, 'anaconda_group')] - - # the user will be created only if the username is set - if self._user.name: - if self.admin.get_active() and \ - self._wheel.name not in self._user.groups: - self._user.groups.append(self._wheel.name) - elif not self.admin.get_active() and \ - self._wheel.name in self._user.groups: - self._user.groups.remove(self._wheel.name) - - anaconda_groups = [self._groupDict[g] for g in self._user.groups - if g != self._wheel.name] - - self.data.group.groupList += anaconda_groups - - # Flag the groups as being created in this spoke - for g in anaconda_groups: - g.anaconda_group = True - - if self._user not in self.data.user.userList: - self.data.user.userList.append(self._user) - - elif self._user in self.data.user.userList: - self.data.user.userList.remove(self._user) - - @property - def completed(self): - return len(self.data.user.userList) > 0 - - def _updatePwQuality(self): - """This method updates the password indicators according - to the password entered by the user. - """ - pwtext = self.pw.get_text() - username = self.username.get_text() - - # Reset the counter used for the "press Done twice" logic - self._waivePasswordClicks = 0 - - self._pwq_valid, strength, self._pwq_error = validatePassword(pwtext, username) - - if not pwtext: - val = 0 - elif strength < 50: - val = 1 - elif strength < 75: - val = 2 - elif strength < 90: - val = 3 - else: - val = 4 - text = _(PASSWORD_STRENGTH_DESC[val]) - - self.pw_bar.set_value(val) - self.pw_label.set_text(text) - - def usepassword_toggled(self, togglebutton = None, data = None): - """Called by Gtk callback when the "Use password" check - button is toggled. It will make password entries in/sensitive.""" - - self.pw.set_sensitive(self.usepassword.get_active()) - self.confirm.set_sensitive(self.usepassword.get_active()) - - # Re-check the password - self.pw.emit("changed") - self.confirm.emit("changed") - - def password_changed(self, editable=None, data=None): - """Update the password strength level bar""" - self._updatePwQuality() - - def username_changed(self, editable = None, data = None): - """Called by Gtk callback when the username or hostname - entry changes. It disables the guess algorithm if the - user added his own text there and reenable it when the - user deletes the whole text.""" - - if editable.get_text() == "": - self.guesser[editable] = True - self.b_advanced.set_sensitive(False) - else: - self.guesser[editable] = False - self.b_advanced.set_sensitive(True) - - # Re-run the password checks against the new username - self.pw.emit("changed") - self.confirm.emit("changed") - - def full_name_changed(self, editable = None, data = None): - """Called by Gtk callback when the full name field changes. - It guesses the username and hostname, strips diacritics - and make those lowercase. - """ - - # after the text is updated in guesser, the guess has to be reenabled - if self.guesser[self.username]: - fullname = self.fullname.get_text() - username = guess_username(fullname) - self.username.set_text(username) - self.guesser[self.username] = True - - def _checkPasswordEmpty(self, editable, data): - """Check whether a password has been specified at all. - - This check is used for both the password and the confirmation. - """ - - # If the password was set by kickstart, skip the strength check - if self._user.password_kickstarted: - return GUICheck.CHECK_OK - - # Skip the check if no password is required - if (not self.usepassword.get_active()) or self._user.password_kickstarted: - return GUICheck.CHECK_OK - elif not editable.get_text(): - if editable == self.pw: - return _(PASSWORD_EMPTY_ERROR) - else: - return _(PASSWORD_CONFIRM_ERROR_GUI) - else: - return GUICheck.CHECK_OK - - def _checkPasswordConfirm(self, editable=None, reset_status=None): - """If the user has entered confirmation data, check whether it matches the password.""" - - # This check is triggered by changes to either the password field or the - # confirmation field. If this method is being run from a successful check - # to reset the status, just return success - if reset_status: - return GUICheck.CHECK_OK - - # Skip the check if no password is required - if (not self.usepassword.get_active()) or self._user.password_kickstarted: - result = GUICheck.CHECK_OK - elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()): - result = _(PASSWORD_CONFIRM_ERROR_GUI) - else: - result = GUICheck.CHECK_OK - - # If the check succeeded, reset the status of the other check object - if result == GUICheck.CHECK_OK: - if editable == self.confirm: - self._password_check.update_check_status(check_data=True) - else: - self._confirm_check.update_check_status(check_data=True) - - return result - - def _checkPasswordStrength(self, editable=None, data=None): - """Update the error message based on password strength. - - The password strength has already been checked in _updatePwQuality, called - previously in the signal chain. This method converts the data set from there - into an error message. - - The password strength check can be waived by pressing "Done" twice. This - is controlled through the self._waivePasswordClicks counter. The counter - is set in on_back_clicked, which also re-runs this check manually. - """ - - # Skip the check if no password is required - if (not self.usepassword.get_active()) or \ - ((not self.pw.get_text()) and (self._user.password_kickstarted)): - return GUICheck.CHECK_OK - - # If the password failed the validity check, fail this check - if (not self._pwq_valid) and (self._pwq_error): - return self._pwq_error - - pwstrength = self.pw_bar.get_value() - - if pwstrength < 2: - # If Done has been clicked twice, waive the check - if self._waivePasswordClicks > 1: - return GUICheck.CHECK_OK - elif self._waivePasswordClicks == 1: - if self._pwq_error: - return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error - else: - return _(PASSWORD_WEAK_CONFIRM) - else: - if self._pwq_error: - return _(PASSWORD_WEAK_WITH_ERROR) % self._pwq_error - else: - return _(PASSWORD_WEAK) - else: - return GUICheck.CHECK_OK - - def on_advanced_clicked(self, _button, data=None): - """Handler for the Advanced.. button. It starts the Advanced dialog - for setting homedit, uid, gid and groups. - """ - - self._user.name = self.username.get_text() - - if self.admin.get_active() and \ - self._wheel.name not in self._user.groups: - self._user.groups.append(self._wheel.name) - elif not self.admin.get_active() and \ - self._wheel.name in self._user.groups: - self._user.groups.remove(self._wheel.name) - - self._advanced.refresh() - with enlightbox(self.window, self._advanced.window): - self._advanced.run() - - self.admin.set_active(self._wheel.name in self._user.groups) - - def on_back_clicked(self, button): - # Add a click and re-check the password strength - self._waivePasswordClicks += 1 - self._pwStrengthCheck.update_check_status() - - # If there is no user set, skip the checks - if not self.username.get_text(): - for check in self.checks: - check.disable() - NormalSpoke.on_back_clicked(self, button) - diff --git a/anaconda/pyanaconda/ui/gui/spokes/welcome.glade b/anaconda/pyanaconda/ui/gui/spokes/welcome.glade index 0180e63..bdd5308 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/welcome.glade +++ b/anaconda/pyanaconda/ui/gui/spokes/welcome.glade @@ -332,7 +332,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="valign">start</property> - <property name="invisible_char">●</property> <property name="secondary_icon_name">edit-clear-symbolic</property> <property name="placeholder_text" translatable="yes">Type here to search.</property> <signal name="changed" handler="on_entry_changed" swapped="no"/> diff --git a/anaconda/pyanaconda/ui/gui/spokes/welcome.py b/anaconda/pyanaconda/ui/gui/spokes/welcome.py index 7e997bf..1dbe527 100644 --- a/anaconda/pyanaconda/ui/gui/spokes/welcome.py +++ b/anaconda/pyanaconda/ui/gui/spokes/welcome.py @@ -23,10 +23,11 @@ import sys import re import langtable +import os from pyanaconda.ui.gui.hubs.summary import SummaryHub from pyanaconda.ui.gui.spokes import StandaloneSpoke -from pyanaconda.ui.gui.utils import setup_gtk_direction, escape_markup +from pyanaconda.ui.gui.utils import setup_gtk_direction, escape_markup, gtk_action_wait from pyanaconda.ui.gui.xkl_wrapper import XklWrapper from pyanaconda.ui.gui.spokes.lib.lang_locale_handler import LangLocaleHandler @@ -36,8 +37,8 @@ from pyanaconda import keyboard from pyanaconda import flags from pyanaconda import geoloc from pyanaconda.i18n import _, C_ -from pyanaconda.iutil import is_unsupported_hw -from pyanaconda.constants import DEFAULT_LANG, DEFAULT_KEYBOARD +from pyanaconda.iutil import is_unsupported_hw, ipmi_report +from pyanaconda.constants import DEFAULT_LANG, DEFAULT_KEYBOARD, IPMI_ABORTED import logging log = logging.getLogger("anaconda") @@ -65,6 +66,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): (store, itr) = self._localeSelection.get_selected() locale = store[itr][1] + self._set_lang(locale) localization.setup_locale(locale, self.data.lang) # Skip timezone and keyboard default setting for kickstart installs. @@ -194,6 +196,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): # if there are no translations for the given locales, # use default if not langs_with_translations: + self._set_lang(DEFAULT_LANG) localization.setup_locale(DEFAULT_LANG, self.data.lang) lang_itr, _locale_itr = self._select_locale(self.data.lang.lang) langs_with_translations[DEFAULT_LANG] = lang_itr @@ -218,6 +221,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): store.set(newItr, 0, "", 1, "", 2, "", 3, True) # setup the "best" locale + self._set_lang(locales[0]) localization.setup_locale(locales[0], self.data.lang) self._select_locale(self.data.lang.lang) @@ -235,7 +239,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): else: widget.set_label(_(before)) - def retranslate(self, lang): + def retranslate(self): # Change the translations on labels and buttons that do not have # substitution text. for name in ["pickLanguageLabel", "betaWarnTitle", "betaWarnDesc"]: @@ -263,7 +267,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): # And of course, don't forget the underlying window. self.window.set_property("distribution", distributionText().upper()) - self.window.retranslate(lang) + self.window.retranslate() def refresh(self): self._select_locale(self.data.lang.lang) @@ -297,8 +301,9 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): if selected: lang = store[selected[0]][1] + self._set_lang(lang) localization.setup_locale(lang) - self.retranslate(lang) + self.retranslate() # Reset the text direction setup_gtk_direction() @@ -316,6 +321,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): rc = dlg.run() dlg.destroy() if rc != 1: + ipmi_report(IPMI_ABORTED) sys.exit(0) if productName.startswith("Red Hat ") and \ @@ -325,6 +331,30 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke): rc = dlg.run() dlg.destroy() if rc != 1: + ipmi_report(IPMI_ABORTED) sys.exit(0) StandaloneSpoke._on_continue_clicked(self, window, user_data) + + @gtk_action_wait + def _set_lang(self, lang): + # This is *hopefully* safe. The only threads that might be running + # outside of the GIL are those doing file operations, the Gio dbus + # proxy thread, and calls from the Gtk main loop. The file operations + # won't be doing things that may access the environment, fingers + # crossed, the GDbus thread shouldn't be doing anything weird since all + # of our dbus calls are from python and synchronous. Using + # gtk_action_wait ensures that this is Gtk main loop thread, and it's + # holding the GIL. + # + # There is a lot of uncertainty and weasliness in those statements. + # This is not good code. + # + # We cannot get around setting $LANG. Python's gettext implementation + # differs from C in that consults only the environment for the current + # language and not the data set via setlocale. If we want translations + # from python modules to work, something needs to be set in the + # environment when the language changes. + + # pylint: disable=environment-modify + os.environ["LANG"] = lang diff --git a/anaconda/pyanaconda/ui/gui/tools/README b/anaconda/pyanaconda/ui/gui/tools/README deleted file mode 100644 index 1b48330..0000000 --- a/anaconda/pyanaconda/ui/gui/tools/README +++ /dev/null @@ -1,50 +0,0 @@ -This directory contains some tools that are helpful in developing the -anaconda UI. They assume that you have an anaconda git tree checked out -(which you should if you're reading this), that you've run make in that -tree, and that you have the anaconda-widgets and anaconda-widgets-devel -packages installed. - -Here's what you've got to work with: - -run-spoke.py ------------- -This program allows you to run a single GUI spoke on an installed system -without having to start up anaconda. You do not have to be root to do this -and if you come across a situation where that ends up being a lie, it's a -bug in anaconda. Please fix it. - -This lets you very quickly make tweaks to your glade file and test it out -to see what things look like when some data is added, or check how buttons -and dialogs behave. Often, there can be UI layout differences between how -things look in glade and how they look in reality. - -To run, first modify the following lines: - - #from pyanaconda.ui.gui.spokes.software import SoftwareSelectionSpoke - #spokeClass = SoftwareSelectionSpoke - spokeClass = None - -Uncomment the first two, changing "software" and "SoftwareSelectionSpoke" to -the names of the things you're testing, and then remove the third line. - -Then, you run it like so (assuming you're in your home directory): - -$ PYTHONPATH=src/anaconda:src/anaconda/pyanaconda/isys/.libs \ - UIPATH=src/anaconda/pyanaconda/ui/gui/ \ - src/anaconda/pyanaconda/ui/gui/tools/run-spoke.py - -The $UIPATH environment variable ensures that you do not need to be anywhere -special when you run the program. anaconda will be able to find your glade -file based on that. - -run-hub.py ----------- -This is the same as run-spoke.py, except it deals with hubs instead of spokes. -Because it deals with hubs, it also provides you with the ability to dive -down into individual spokes. This is useful for making sure that the status, -apply, and setup methods of spokes work correctly. - -Pressing the quit and continue buttons will immediately quit the program. - - -Direct any questions to clumens@redhat.com. diff --git a/anaconda/pyanaconda/ui/gui/tools/run-hub.py b/anaconda/pyanaconda/ui/gui/tools/run-hub.py deleted file mode 100755 index afddbd1..0000000 --- a/anaconda/pyanaconda/ui/gui/tools/run-hub.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/python - -import sys, os - -import gi.overrides - -# We need this so we can tell GI to look for overrides objects -# also in anaconda source directories -for p in os.environ.get("ANACONDA_WIDGETS_OVERRIDES", "").split(":"): - gi.overrides.__path__.insert(0, p) - -from gi.repository import AnacondaWidgets, Gtk - -import ctypes -import os.path - -# Check command line arguments -if len(sys.argv)<2: - print "Usage: $0 <spoke module name> [<spoke widget class>]" - sys.exit(1) - -# This is a hack to make sure the AnacondaWidgets library gets loaded -ctypes.CDLL("libAnacondaWidgets.so.0", ctypes.RTLD_GLOBAL) - -# Logging always needs to be set up first thing, or there'll be tracebacks. -from pyanaconda import anaconda_log -anaconda_log.init() - -from pyanaconda.installclass import DefaultInstall -from pyanaconda.storage import Storage -from pyanaconda.threads import initThreading -from pyanaconda.packaging.yumpayload import YumPayload -from pyanaconda.platform import getPlatform -from pykickstart.version import makeVersion - -# Don't worry with fcoe, iscsi, dasd, any of that crud. -from pyanaconda.flags import flags -flags.imageInstall = True -flags.testing = True - -initThreading() - -# Figure out the part we are about to show: hub/spoke? -# And get the name of the module which represents it -if os.path.basename(sys.argv[0]) == "run-spoke.py": - spokeModuleName = "pyanaconda.ui.gui.spokes.%s" % sys.argv[1] - from pyanaconda.ui.common import Spoke - spokeBaseClass = Spoke - spokeText = "spoke" - SpokeText = "Spoke" -elif os.path.basename(sys.argv[0]) == "run-hub.py": - spokeModuleName = "pyanaconda.ui.gui.hubs.%s" % sys.argv[1] - from pyanaconda.ui.common import Hub - spokeBaseClass = Hub - spokeText = "hub" - SpokeText = "Hub" -else: - print "You have to run this command as run-spoke.py or run-hub.py." - sys.exit(1) - -# Set default spoke class -spokeClass = None - -# Load spoke specified on the command line -# If the spoke module was specified, but the spoke class was not, -# try to find it using class hierarchy -try: - spokeClassName = sys.argv[2] - __import__(spokeModuleName, fromlist = [spokeClassName]) - spokeModule = sys.modules[spokeModuleName] -except IndexError: - __import__(spokeModuleName) - spokeModule = sys.modules[spokeModuleName] - for k,v in vars(spokeModule).iteritems(): - try: - if issubclass(v, spokeBaseClass) and v != spokeBaseClass: - spokeClassName = k - spokeClass = v - except TypeError: - pass - -if not spokeClass: - try: - spokeClass = getattr(spokeModule, spokeClassName) - except KeyError: - print "%s %s could not be found in %s" % (SpokeText, spokeClassName, spokeModuleName) - sys.exit(1) - - -print "Running %s %s from %s" % (spokeText, spokeClass, spokeModule) - -platform = getPlatform() -ksdata = makeVersion() -storage = Storage(data=ksdata, platform=platform) -storage.reset() -instclass = DefaultInstall() - -payload = YumPayload(ksdata) -payload.setup(storage) - -spoke = spokeClass(ksdata, storage, payload, instclass) -if hasattr(spoke, "register_event_cb"): - spoke.register_event_cb("continue", lambda: Gtk.main_quit()) - spoke.register_event_cb("quit", lambda: Gtk.main_quit()) -spoke.initialize() - -if not spoke.showable: - print "This %s is not showable, but I'll continue anyway." % spokeText - -spoke.refresh() -spoke.window.set_beta(True) -spoke.window.set_property("distribution", "TEST HARNESS") -spoke.window.show_all() - -Gtk.main() - -if hasattr(spoke, "status"): - print "%s status:\n%s\n" % (SpokeText, spoke.status) -if hasattr(spoke, "completed"): - print "%s completed:\n%s\n" % (SpokeText, spoke.completed) -print "%s kickstart fragment:\n%s" % (SpokeText, ksdata) diff --git a/anaconda/pyanaconda/ui/gui/tools/run-spoke.py b/anaconda/pyanaconda/ui/gui/tools/run-spoke.py deleted file mode 100755 index 4edaacd..0000000 --- a/anaconda/pyanaconda/ui/gui/tools/run-spoke.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/python - -import sys, os - -import gi.overrides - -# We need this so we can tell GI to look for overrides objects -# also in anaconda source directories -for p in os.environ.get("ANACONDA_WIDGETS_OVERRIDES", "").split(":"): - gi.overrides.__path__.insert(0, p) - -from gi.repository import Gtk - -import ctypes -import os.path - -# Check command line arguments -if len(sys.argv)<2: - print("Usage: $0 <spoke module name> [<spoke widget class>]") - sys.exit(1) - -# This is a hack to make sure the AnacondaWidgets library gets loaded -ctypes.CDLL("libAnacondaWidgets.so.1", ctypes.RTLD_GLOBAL) - -# Logging always needs to be set up first thing, or there'll be tracebacks. -from pyanaconda import anaconda_log -anaconda_log.init() - -from pyanaconda.installclass import DefaultInstall -from blivet import Blivet -from pyanaconda.threads import initThreading -from pyanaconda.packaging.yumpayload import YumPayload -from pykickstart.version import makeVersion - -# Don't worry with fcoe, iscsi, dasd, any of that crud. -from pyanaconda.flags import flags -flags.imageInstall = True -flags.testing = True - -initThreading() - -# Figure out the part we are about to show: hub/spoke? -# And get the name of the module which represents it -if os.path.basename(sys.argv[0]) == "run-spoke.py": - spokeModuleName = "pyanaconda.ui.gui.spokes.%s" % sys.argv[1] - from pyanaconda.ui.common import Spoke - spokeBaseClass = Spoke - spokeText = "spoke" - SpokeText = "Spoke" -elif os.path.basename(sys.argv[0]) == "run-hub.py": - spokeModuleName = "pyanaconda.ui.gui.hubs.%s" % sys.argv[1] - from pyanaconda.ui.common import Hub - spokeBaseClass = Hub - spokeText = "hub" - SpokeText = "Hub" -else: - print("You have to run this command as run-spoke.py or run-hub.py.") - sys.exit(1) - -# Set default spoke class -spokeClass = None - -# Load spoke specified on the command line -# If the spoke module was specified, but the spoke class was not, -# try to find it using class hierarchy -try: - spokeClassName = sys.argv[2] - __import__(spokeModuleName, fromlist = [spokeClassName]) - spokeModule = sys.modules[spokeModuleName] -except IndexError: - __import__(spokeModuleName) - spokeModule = sys.modules[spokeModuleName] - for k,v in vars(spokeModule).iteritems(): - try: - if issubclass(v, spokeBaseClass) and v != spokeBaseClass: - spokeClassName = k - spokeClass = v - except TypeError: - pass - -if not spokeClass: - try: - spokeClass = getattr(spokeModule, spokeClassName) - except KeyError: - print("%s %s could not be found in %s" % (SpokeText, spokeClassName, spokeModuleName)) - sys.exit(1) - - -print("Running %s %s from %s" % (spokeText, spokeClass, spokeModule)) - -ksdata = makeVersion() -storage = Blivet(ksdata=ksdata) -storage.reset() -instclass = DefaultInstall() - -payload = YumPayload(ksdata) -payload.setup(storage) - -from pyanaconda.ui.gui.spokes import StandaloneSpoke -spoke = spokeClass(ksdata, storage, payload, instclass) -if isinstance(spoke, StandaloneSpoke): - spoke.window.connect_after("continue-clicked", Gtk.main_quit) - spoke.window.connect("quit-clicked", Gtk.main_quit) - -if hasattr(spoke, "set_path"): - spoke.set_path("categories", [ - ("pyanaconda.ui.categories.%s", - os.path.join(os.path.dirname(__file__),"..", "categories")) - ]) - spoke.set_path("spokes", [ - ("pyanaconda.ui.gui.spokes.%s", - os.path.join(os.path.dirname(__file__), "..", "spokes")) - ]) - -spoke.initialize() - -if not spoke.showable: - print("This %s is not showable, but I'll continue anyway." % spokeText) - -spoke.refresh() -spoke.window.set_beta(True) -spoke.window.set_property("distribution", "TEST HARNESS") -spoke.window.show_all() - -Gtk.main() - -if hasattr(spoke, "status"): - print("%s status:\n%s\n" % (SpokeText, spoke.status)) -if hasattr(spoke, "completed"): - print("%s completed:\n%s\n" % (SpokeText, spoke.completed)) -print("%s kickstart fragment:\n%s" % (SpokeText, ksdata)) diff --git a/anaconda/pyanaconda/ui/gui/utils.py b/anaconda/pyanaconda/ui/gui/utils.py index e6573b8..8f3c328 100644 --- a/anaconda/pyanaconda/ui/gui/utils.py +++ b/anaconda/pyanaconda/ui/gui/utils.py @@ -1,6 +1,6 @@ # Miscellaneous UI functions # -# Copyright (C) 2012, 2013 Red Hat, Inc. +# Copyright (C) 2012-2014 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of @@ -21,6 +21,8 @@ # Vratislav Podzimek <vpodzime@redhat.com> # +from contextlib import contextmanager + from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.constants import NOTICEABLE_FREEZE @@ -174,7 +176,7 @@ def gtk_batch_map(action, items, args=(), pre_func=None, batch_size=1): # process as many batches as user shouldn't notice while tstamp - tstamp_start < NOTICEABLE_FREEZE: - for _i in xrange(batch_size): + for _i in range(batch_size): try: action_item = queue.get_nowait() if action_item is TERMINATOR: @@ -288,12 +290,27 @@ def timed_action(delay=300, threshold=750, busy_cursor=True): return decorator +@contextmanager +def blockedHandler(obj, func): + """Prevent a GLib signal handling function from being called during some + block of code. + """ + obj.handler_block_by_func(func) + yield + obj.handler_unblock_by_func(func) + def busyCursor(): window = Gdk.get_default_root_window() + if not window: + return + window.set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH)) def unbusyCursor(): window = Gdk.get_default_root_window() + if not window: + return + window.set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) def ignoreEscape(dlg): @@ -407,7 +424,9 @@ def escape_markup(value): if isinstance(value, unicode): value = value.encode("utf-8") - return GLib.markup_escape_text(str(value)) + escaped = GLib.markup_escape_text(str(value)) + + return escaped.decode("utf-8") # This will be populated by override_cell_property. Keys are tuples of (column, renderer). # Values are a dict of the form {property-name: (property-func, property-data)}. diff --git a/anaconda/pyanaconda/ui/gui/xkl_wrapper.py b/anaconda/pyanaconda/ui/gui/xkl_wrapper.py index 1f7855b..d970ce1 100644 --- a/anaconda/pyanaconda/ui/gui/xkl_wrapper.py +++ b/anaconda/pyanaconda/ui/gui/xkl_wrapper.py @@ -197,12 +197,12 @@ class XklWrapper(object): def get_available_layouts(self): """A generator yielding layouts (no need to store them as a bunch)""" - return self._layout_infos.iterkeys() + return self._layout_infos.keys() def get_switching_options(self): """Method returning list of available layout switching options""" - return self._switch_opt_infos.iterkeys() + return self._switch_opt_infos.keys() def get_layout_variant_description(self, layout_variant, with_lang=True, xlated=True): """ diff --git a/anaconda/pyanaconda/ui/lib/space.py b/anaconda/pyanaconda/ui/lib/space.py index e556492..a133c8b 100644 --- a/anaconda/pyanaconda/ui/lib/space.py +++ b/anaconda/pyanaconda/ui/lib/space.py @@ -29,12 +29,12 @@ log = logging.getLogger("anaconda") class FileSystemSpaceChecker(object): """This object provides for a way to verify that enough space is available - on configured filesystems to support the current software selections. + on configured file systems to support the current software selections. It is run as part of completeness checking every time a spoke changes, therefore moving this step up out of both the storage and software spokes. """ - error_template = N_("Not enough space in filesystems for the current " + error_template = N_("Not enough space in file systems for the current " "software selection. An additional %s is needed.") def __init__(self, storage, payload): diff --git a/anaconda/pyanaconda/ui/tui/Makefile.am b/anaconda/pyanaconda/ui/tui/Makefile.am index 389638e..5abbf2e 100644 --- a/anaconda/pyanaconda/ui/tui/Makefile.am +++ b/anaconda/pyanaconda/ui/tui/Makefile.am @@ -15,7 +15,7 @@ # # Author: Chris Lumens <clumens@redhat.com> -SUBDIRS = hubs spokes simpleline tools +SUBDIRS = hubs spokes simpleline MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/pyanaconda/ui/tui/__init__.py b/anaconda/pyanaconda/ui/tui/__init__.py index 655913a..0fb1ca7 100644 --- a/anaconda/pyanaconda/ui/tui/__init__.py +++ b/anaconda/pyanaconda/ui/tui/__init__.py @@ -33,6 +33,8 @@ import sys import site import Queue import meh.ui.text +import logging +log = logging.getLogger("anaconda") def exception_msg_handler(event, data): """ @@ -149,7 +151,6 @@ class TextUserInterface(ui.UserInterface): _hubs = self._list_hubs() # First, grab a list of all the standalone spokes. - path = os.path.join(os.path.dirname(__file__), "spokes") spokes = self._collectActionClasses(self.paths["spokes"], StandaloneSpoke) actionClasses = self._orderActionClasses(spokes, _hubs) @@ -259,6 +260,11 @@ class TextUserInterface(ui.UserInterface): def _showError(self, message): """Internal helper function that MUST BE CALLED FROM THE MAIN THREAD""" + if flags.automatedInstall and not flags.ksprompt: + log.error(message) + # If we're in cmdline mode, just exit. + return + error_window = ErrorDialog(self._app, message) self._app.switch_screen_modal(error_window) @@ -289,6 +295,7 @@ class TextUserInterface(ui.UserInterface): """Internal helper function that MUST BE CALLED FROM THE MAIN THREAD""" if flags.automatedInstall and not flags.ksprompt: + log.error(message) # If we're in cmdline mode, just say no. return False diff --git a/anaconda/pyanaconda/ui/tui/hubs/__init__.py b/anaconda/pyanaconda/ui/tui/hubs/__init__.py index e89bf71..7605dca 100644 --- a/anaconda/pyanaconda/ui/tui/hubs/__init__.py +++ b/anaconda/pyanaconda/ui/tui/hubs/__init__.py @@ -87,8 +87,8 @@ class TUIHub(TUIObject, common.Hub): return tui.ColumnWidget([(3, [number]), (None, [w])], 1) # split spokes to two columns - left = [_prep(i, w) for i,w in self._keys.iteritems() if i % 2 == 1] - right = [_prep(i, w) for i,w in self._keys.iteritems() if i % 2 == 0] + left = [_prep(i, w) for i,w in self._keys.items() if i % 2 == 1] + right = [_prep(i, w) for i,w in self._keys.items() if i % 2 == 0] c = tui.ColumnWidget([(39, left), (39, right)], 2) self._window.append(c) diff --git a/anaconda/pyanaconda/ui/tui/hubs/summary.py b/anaconda/pyanaconda/ui/tui/hubs/summary.py index c363d7a..52923c0 100644 --- a/anaconda/pyanaconda/ui/tui/hubs/summary.py +++ b/anaconda/pyanaconda/ui/tui/hubs/summary.py @@ -24,7 +24,7 @@ from pyanaconda.ui.lib.space import FileSystemSpaceChecker, DirInstallSpaceCheck from pyanaconda.ui.tui.hubs import TUIHub from pyanaconda.flags import flags from pyanaconda.errors import CmdlineError -from pyanaconda.i18n import N_, _ +from pyanaconda.i18n import N_, _, C_ import sys import time @@ -78,7 +78,10 @@ class SummaryHub(TUIHub): self.close() return None - if not flags.ksprompt: + if flags.ksprompt: + for spoke in incompleteSpokes: + log.info("kickstart installation stopped for info: %s", spoke.title) + else: errtxt = _("The following mandatory spokes are not completed:") + \ "\n" + "\n".join(spoke.title for spoke in incompleteSpokes) log.error("CmdlineError: %s", errtxt) @@ -101,7 +104,7 @@ class SummaryHub(TUIHub): # If we get a continue, check for unfinished spokes. If unfinished # don't continue # TRANSLATORS: 'b' to begin installation - if key == _('b'): + if key == C_('TUI|Spoke Navigation', 'b'): for spoke in self._spokes.values(): if not spoke.completed and spoke.mandatory: print(_("Please complete all spokes before continuing")) @@ -115,7 +118,7 @@ class SummaryHub(TUIHub): self.app.close_screen() return True # TRANSLATORS: 'c' to continue - elif key == _('c'): + elif key == C_('TUI|Spoke Navigation', 'c'): # Kind of a hack, but we want to ignore if anyone presses 'c' # which is the global TUI key to close the current screen return False diff --git a/anaconda/pyanaconda/ui/tui/simpleline/base.py b/anaconda/pyanaconda/ui/tui/simpleline/base.py index 04e4053..6371b93 100644 --- a/anaconda/pyanaconda/ui/tui/simpleline/base.py +++ b/anaconda/pyanaconda/ui/tui/simpleline/base.py @@ -28,7 +28,7 @@ import threading import functools from pyanaconda.threads import threadMgr, AnacondaThread from pyanaconda.ui.communication import hubQ -from pyanaconda import constants +from pyanaconda import constants, iutil from pyanaconda.i18n import _, N_, C_ RAW_INPUT_LOCK = threading.Lock() @@ -287,7 +287,7 @@ class App(object): self._redraw = False except ExitMainLoop: raise - except Exception: + except Exception: # pylint: disable=broad-except send_exception(self.queue, sys.exc_info()) return False @@ -346,7 +346,7 @@ class App(object): prompt = last_screen.prompt(self._screens[-1][1]) except ExitMainLoop: raise - except Exception: + except Exception: # pylint: disable=broad-except send_exception(self.queue, sys.exc_info()) continue @@ -403,7 +403,7 @@ class App(object): handler(event, data) except ExitMainLoop: raise - except Exception: + except Exception: # pylint: disable=broad-except send_exception(self.queue, sys.exc_info()) def raw_input(self, prompt, hidden=False): @@ -441,7 +441,7 @@ class App(object): return True except ExitMainLoop: raise - except Exception: + except Exception: # pylint: disable=broad-except send_exception(self.queue, sys.exc_info()) return False @@ -465,6 +465,8 @@ class App(object): self.switch_screen_modal(d) if d.answer: raise ExitAllMainLoops() + + iutil.ipmi_report(constants.IPMI_ABORTED) return True return False diff --git a/anaconda/pyanaconda/ui/tui/spokes/__init__.py b/anaconda/pyanaconda/ui/tui/spokes/__init__.py index 4f72600..61c09c3 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/__init__.py +++ b/anaconda/pyanaconda/ui/tui/spokes/__init__.py @@ -27,6 +27,7 @@ from collections import namedtuple from pyanaconda.iutil import setdeepattr, getdeepattr from pyanaconda.i18n import N_, _ from pyanaconda.constants import PASSWORD_CONFIRM_ERROR_TUI, PW_ASCII_CHARS +from pyanaconda.constants import PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR __all__ = ["TUISpoke", "EditTUISpoke", "EditTUIDialog", "EditTUISpokeEntry", "StandaloneSpoke", "NormalTUISpoke"] @@ -102,13 +103,18 @@ class EditTUIDialog(NormalTUISpoke): title = N_("New value") PASSWORD = re.compile(".*") - def __init__(self, app, data, storage, payload, instclass): + def __init__(self, app, data, storage, payload, instclass, policy_name=""): if self.__class__ is EditTUIDialog: raise TypeError("EditTUIDialog is an abstract class") NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self.value = None + # Configure the password policy, if available. Otherwise use defaults. + self.policy = self.data.anaconda.pwpolicy.get_policy(policy_name) + if not self.policy: + self.policy = self.data.anaconda.PwPolicyData() + def refresh(self, args = None): self._window = [] self.value = None @@ -135,22 +141,30 @@ class EditTUIDialog(NormalTUISpoke): self.value = "" return None - valid, strength, message = validatePassword(pw, user=None) + valid, strength, message = validatePassword(pw, user=None, minlen=self.policy.minlen) if not valid: print(message) return None - if strength < 50: - if message: - error = _("You have provided a weak password: %s\n" - "Would you like to use it anyway?") % message + if strength < self.policy.minquality: + if self.policy.strict: + done_msg = "" else: - error = _("You have provided a weak password.\n" - "Would you like to use it anyway?") - question_window = YesNoDialog(self._app, error) - self._app.switch_screen_modal(question_window) - if not question_window.answer: + done_msg = _("\nWould you like to use it anyway?") + + if message: + error = _(PASSWORD_WEAK_WITH_ERROR) % (message, done_msg) + else: + error = _(PASSWORD_WEAK) % done_msg + + if not self.policy.strict: + question_window = YesNoDialog(self._app, error) + self._app.switch_screen_modal(question_window) + if not question_window.answer: + return None + else: + print(error) return None if any(char not in PW_ASCII_CHARS for char in pw): @@ -168,8 +182,8 @@ class EditTUIDialog(NormalTUISpoke): self.close() return True else: - print(_("You have provided an invalid username: %s\n" - "Tip: Keep your username shorter than 32 characters and do not use spaces.\n") % key) + print(_("You have provided an invalid user name: %s\n" + "Tip: Keep your user name shorter than 32 characters and do not use spaces.\n") % key) return NormalTUISpoke.input(self, entry, key) class OneShotEditTUIDialog(EditTUIDialog): @@ -222,12 +236,12 @@ class EditTUISpoke(NormalTUISpoke): edit_fields = [ ] - def __init__(self, app, data, storage, payload, instclass): + def __init__(self, app, data, storage, payload, instclass, policy_name=""): if self.__class__ is EditTUISpoke: raise TypeError("EditTUISpoke is an abstract class") NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) - self.dialog = OneShotEditTUIDialog(app, data, storage, payload, instclass) + self.dialog = OneShotEditTUIDialog(app, data, storage, payload, instclass, policy_name=policy_name) # self.args should hold the object this Spoke is supposed # to edit @@ -301,9 +315,9 @@ class EditTUISpoke(NormalTUISpoke): try: idx = int(key) - 1 if idx >= 0 and idx < len(self.visible_fields): - if self.edit_fields[idx].aux == self.CHECK: + if self.visible_fields[idx].aux == self.CHECK: setdeepattr(self.args, self.visible_fields[idx].attribute, - not getdeepattr(self.args, self.edit_fields[idx][1])) + not getdeepattr(self.args, self.visible_fields[idx][1])) self.app.redraw() self.apply() else: diff --git a/anaconda/pyanaconda/ui/tui/spokes/askvnc.py b/anaconda/pyanaconda/ui/tui/spokes/askvnc.py index 5d815e2..606dbfe 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/askvnc.py +++ b/anaconda/pyanaconda/ui/tui/spokes/askvnc.py @@ -22,12 +22,15 @@ from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget from pyanaconda.ui.tui.tuiobject import YesNoDialog -from pyanaconda.constants import USEVNC, USETEXT +from pyanaconda.constants import USEVNC, USETEXT, IPMI_ABORTED from pyanaconda.constants_text import INPUT_PROCESSED from pyanaconda.i18n import N_, _ from pyanaconda.ui.communication import hubQ from pyanaconda.ui.tui import exception_msg_handler -import getpass, subprocess +from pyanaconda.iutil import execWithRedirect +from pyanaconda.flags import can_touch_runtime_system +from pyanaconda import iutil +import getpass import sys def exception_msg_handler_and_exit(event, data): @@ -104,7 +107,11 @@ class AskVNCSpoke(NormalTUISpoke): d = YesNoDialog(self.app, _(self.app.quit_message)) self.app.switch_screen_modal(d) if d.answer: - subprocess.Popen(["systemctl", "--no-wall", "reboot"]) + iutil.ipmi_report(IPMI_ABORTED) + if can_touch_runtime_system("Quit and Reboot"): + execWithRedirect("systemctl", ["--no-wall", "reboot"]) + else: + exit(1) else: return key diff --git a/anaconda/pyanaconda/ui/tui/spokes/langsupport.py b/anaconda/pyanaconda/ui/tui/spokes/langsupport.py index 98b5250..175840f 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/langsupport.py +++ b/anaconda/pyanaconda/ui/tui/spokes/langsupport.py @@ -24,9 +24,7 @@ from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget from pyanaconda.ui.common import FirstbootSpokeMixIn from pyanaconda import localization -from pyanaconda.i18n import N_, _ - -from pyanaconda import flags +from pyanaconda.i18n import N_, _, C_ class LangSpoke(FirstbootSpokeMixIn, NormalTUISpoke): """ @@ -53,10 +51,7 @@ class LangSpoke(FirstbootSpokeMixIn, NormalTUISpoke): @property def completed(self): - if flags.flags.automatedInstall: - return self.data.lang.lang and self.data.lang.lang != "" - else: - return False + return self.data.lang.lang @property def mandatory(self): @@ -114,7 +109,7 @@ class LangSpoke(FirstbootSpokeMixIn, NormalTUISpoke): pass # TRANSLATORS: 'b' to go back - if key.lower() == _("b"): + if key.lower() == C_("TUI|Spoke Navigation|Language Support", "b"): self.app.switch_screen(self, None) return True else: diff --git a/anaconda/pyanaconda/ui/tui/spokes/password.py b/anaconda/pyanaconda/ui/tui/spokes/password.py index 4fe971a..2478d8d 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/password.py +++ b/anaconda/pyanaconda/ui/tui/spokes/password.py @@ -33,7 +33,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, EditTUIDialog): category = UserSettingsCategory def __init__(self, app, data, storage, payload, instclass): - EditTUIDialog.__init__(self, app, data, storage, payload, instclass) + EditTUIDialog.__init__(self, app, data, storage, payload, instclass, "root") self._password = None @property @@ -42,7 +42,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, EditTUIDialog): @property def showable(self): - return not (self.completed and flags.automatedInstall) + return not (self.completed and flags.automatedInstall and not self.policy.changesok) @property def mandatory(self): diff --git a/anaconda/pyanaconda/ui/tui/spokes/progress.py b/anaconda/pyanaconda/ui/tui/spokes/progress.py index 28376d3..92cb2f4 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/progress.py +++ b/anaconda/pyanaconda/ui/tui/spokes/progress.py @@ -23,7 +23,8 @@ import sys from pyanaconda.flags import flags from pyanaconda.i18n import N_, _ -from pyanaconda.constants import THREAD_INSTALL, THREAD_CONFIGURATION +from pyanaconda import iutil +from pyanaconda.constants import THREAD_INSTALL, THREAD_CONFIGURATION, IPMI_FINISHED from pykickstart.constants import KS_SHUTDOWN, KS_REBOOT from pyanaconda.ui.tui.spokes import StandaloneTUISpoke @@ -124,6 +125,8 @@ class ProgressSpoke(StandaloneTUISpoke): # This will run until we're all done with the configuration thread. self._update_progress() + iutil.ipmi_report(IPMI_FINISHED) + # kickstart install, continue automatically if reboot or shutdown selected if flags.automatedInstall and self.data.reboot.action in [KS_REBOOT, KS_SHUTDOWN]: # Just pretend like we got input, and our input doesn't care diff --git a/anaconda/pyanaconda/ui/tui/spokes/shell_spoke.py b/anaconda/pyanaconda/ui/tui/spokes/shell_spoke.py index e0115ca..0037188 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/shell_spoke.py +++ b/anaconda/pyanaconda/ui/tui/spokes/shell_spoke.py @@ -26,10 +26,9 @@ from pyanaconda.ui.tui.simpleline.widgets import TextWidget from pyanaconda.i18n import N_, _ from pyanaconda.constants import ANACONDA_ENVIRON from pyanaconda.flags import flags +from pyanaconda.iutil import execConsole from blivet import arch -import subprocess - class ShellSpoke(NormalTUISpoke): title = N_("Shell") category = SystemCategory @@ -59,8 +58,7 @@ class ShellSpoke(NormalTUISpoke): def prompt(self, args=None): # run shell instead of printing prompt and close window on shell exit - proc = subprocess.Popen(["bash", "--login"], shell=True, cwd="/") - proc.wait() + execConsole() self.close() # suppress the prompt diff --git a/anaconda/pyanaconda/ui/tui/spokes/software.py b/anaconda/pyanaconda/ui/tui/spokes/software.py index ce069c9..7657648 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/software.py +++ b/anaconda/pyanaconda/ui/tui/spokes/software.py @@ -24,11 +24,12 @@ from pyanaconda.ui.categories.software import SoftwareCategory from pyanaconda.ui.tui.spokes import NormalTUISpoke from pyanaconda.ui.tui.simpleline import TextWidget, ColumnWidget, CheckboxWidget from pyanaconda.threads import threadMgr, AnacondaThread -from pyanaconda.packaging import DependencyError, PackagePayload -from pyanaconda.i18n import N_, _ +from pyanaconda.packaging import DependencyError, PackagePayload, payloadMgr +from pyanaconda.i18n import N_, _, C_ from pyanaconda.constants import THREAD_PAYLOAD from pyanaconda.constants import THREAD_CHECK_SOFTWARE +from pyanaconda.constants import THREAD_SOFTWARE_WATCHER from pyanaconda.constants_text import INPUT_PROCESSED __all__ = ["SoftwareSpoke"] @@ -43,8 +44,7 @@ class SoftwareSpoke(NormalTUISpoke): NormalTUISpoke.__init__(self, app, data, storage, payload, instclass) self.errors = [] self._tx_id = None - # default to first selection (Gnome) in list of environments - self._selection = 0 + self._selection = None self.environment = None # for detecting later whether any changes have been made @@ -53,11 +53,53 @@ class SoftwareSpoke(NormalTUISpoke): # are we taking values (package list) from a kickstart file? self._kickstarted = flags.automatedInstall and self.data.packages.seen + # Register event listeners to update our status on payload events + payloadMgr.addListener(payloadMgr.STATE_START, self._payload_start) + payloadMgr.addListener(payloadMgr.STATE_FINISHED, self._payload_finished) + payloadMgr.addListener(payloadMgr.STATE_ERROR, self._payload_error) + + def initialize(self): + # Start a thread to wait for the payload and run the first, automatic + # dependency check + super(SoftwareSpoke, self).initialize() + threadMgr.add(AnacondaThread(name=THREAD_SOFTWARE_WATCHER, + target=self._initialize)) + + def _initialize(self): + threadMgr.wait(THREAD_PAYLOAD) + + if not self._kickstarted: + # If an environment was specified in the instclass, use that. + # Otherwise, select the first environment. + if self.payload.environments: + environments = self.payload.environments + instclass = self.payload.instclass + + if instclass and instclass.defaultPackageEnvironment and \ + instclass.defaultPackageEnvironment in environments: + self._selection = environments.index(instclass.defaultPackageEnvironment) + else: + self._selection = 0 + + # Apply the initial selection + self._apply() + + def _payload_start(self): + # Source is changing, invalidate the software selection and clear the + # errors + self._selection = None + self.errors = [] + + def _payload_finished(self): + self.environment = self.data.packages.environment + + def _payload_error(self): + self.errors = [payloadMgr.error] + @property def showable(self): return isinstance(self.payload, PackagePayload) - @property def status(self): """ Where we are in the process """ @@ -70,14 +112,6 @@ class SoftwareSpoke(NormalTUISpoke): if not self.txid_valid: return _("Source changed - please verify") - ## FIXME: - # quite ugly, but env isn't getting set to gnome (or anything) by - # default, and it really should be so we can maintain consistency - # with graphical behavior - if self._selection >= 0 and not self.environment \ - and not flags.automatedInstall: - self.apply() - if not self.environment: # Ks installs with %packages will have an env selected, unless # they did an install without a desktop environment. This should @@ -107,6 +141,8 @@ class SoftwareSpoke(NormalTUISpoke): """ Refresh screen. """ NormalTUISpoke.refresh(self, args) + threadMgr.wait(THREAD_PAYLOAD) + if not self.payload.baseRepo: message = TextWidget(_("Installation source needs to be set up first.")) self._window.append(message) @@ -128,7 +164,6 @@ class SoftwareSpoke(NormalTUISpoke): name = self.payload.environmentDescription(env)[0] displayed.append(CheckboxWidget(title="%s" % name, completed=(environments.index(env) == self._selection))) - print(_("Base environment")) def _prep(i, w): """ Do some format magic for display. """ @@ -150,7 +185,9 @@ class SoftwareSpoke(NormalTUISpoke): try: keyid = int(key) - 1 except ValueError: - if key.lower() == "c" and 0 <= self._selection < len(self.payload.environments): + # TRANSLATORS: 'c' to continue + if key.lower() == C_("TUI|Spoke Navigation", "c") and \ + 0 <= self._selection < len(self.payload.environments): self.apply() self.close() return INPUT_PROCESSED @@ -165,7 +202,8 @@ class SoftwareSpoke(NormalTUISpoke): def ready(self): """ If we're ready to move on. """ return (not threadMgr.get(THREAD_PAYLOAD) and - not threadMgr.get(THREAD_CHECK_SOFTWARE)) + not threadMgr.get(THREAD_CHECK_SOFTWARE) and + not threadMgr.get(THREAD_SOFTWARE_WATCHER)) def apply(self): """ Apply our selections """ @@ -175,34 +213,43 @@ class SoftwareSpoke(NormalTUISpoke): self._kickstarted = False self.data.packages.seen = True - threadMgr.add(AnacondaThread(name=THREAD_CHECK_SOFTWARE, - target=self.checkSoftwareSelection)) - def _apply(self): """ Private apply. """ - self.environment = self.payload.environments[self._selection] - if not self.environment: - return - - if not self._origEnv: - # nothing selected before, select the environment - self.payload.selectEnvironment(self.environment) - elif self._origEnv != self.environment: - # environment changed, clear the list of packages and select the new - # one - self.payload.data.packages.groupList = [] - self.payload.selectEnvironment(self.environment) + if 0 <= self._selection < len(self.payload.environments): + self.environment = self.payload.environments[self._selection] else: - # no change + self.environment = None return - self._origEnv = self.environment + changed = False + + # Not a kickstart with packages, setup the selected environment + if not self._kickstarted: + if not self._origEnv: + # nothing selected before, select the environment + self.payload.selectEnvironment(self.environment) + changed = True + elif self._origEnv != self.environment: + # environment changed, clear the list of packages and select the new + # one + self.payload.data.packages.groupList = [] + self.payload.selectEnvironment(self.environment) + changed = True + + self._origEnv = self.environment + + # Check the software selection + if changed: + threadMgr.add(AnacondaThread(name=THREAD_CHECK_SOFTWARE, + target=self.checkSoftwareSelection)) + def checkSoftwareSelection(self): """ Depsolving """ try: self.payload.checkSoftwareSelection() - except DependencyError: + except DependencyError as e: + self.errors = [e.message] self._tx_id = None else: self._tx_id = self.payload.txID diff --git a/anaconda/pyanaconda/ui/tui/spokes/source.py b/anaconda/pyanaconda/ui/tui/spokes/source.py index 7f0c79f..970a77d 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/source.py +++ b/anaconda/pyanaconda/ui/tui/spokes/source.py @@ -32,12 +32,12 @@ from pyanaconda.iutil import DataHolder from pyanaconda.constants import THREAD_SOURCE_WATCHER, THREAD_PAYLOAD from pyanaconda.constants import THREAD_STORAGE_WATCHER -from pyanaconda.constants import THREAD_CHECK_SOFTWARE, ISO_DIR, DRACUT_ISODIR +from pyanaconda.constants import THREAD_CHECK_SOFTWARE, ISO_DIR, DRACUT_ISODIR, DRACUT_REPODIR from pyanaconda.constants_text import INPUT_PROCESSED from pyanaconda.ui.helpers import SourceSwitchHandler -from blivet.util import get_mount_paths +from blivet.util import get_mount_device, get_mount_paths import re import os @@ -63,7 +63,7 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): EditTUISpoke.__init__(self, app, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self._ready = False - self.errors = [] + self._error = False self._cdrom = None def initialize(self): @@ -88,7 +88,7 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): self._ready = True def _payload_error(self): - self.errors.append(payloadMgr.error) + self._error = True def _repo_status(self): """ Return a string describing repo url or lack of one. """ @@ -113,28 +113,19 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): @property def status(self): - if self.errors: + if self._error: return _("Error setting up software source") elif not self.ready: return _("Processing...") else: return self._repo_status() - def _update_summary(self): - """ Update screen with a summary. Show errors if there are any. """ - summary = (_("Repo URL set to: %s") % self._repo_status()) - - if self.errors: - summary = summary + "\n" + "\n".join(self.errors) - - return summary - @property def completed(self): if flags.automatedInstall and self.ready and not self.payload.baseRepo: return False else: - return not self.errors and self.ready and (self.data.method.method or self.payload.baseRepo) + return not self._error and self.ready and (self.data.method.method or self.payload.baseRepo) def refresh(self, args=None): EditTUISpoke.refresh(self, args) @@ -142,6 +133,13 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): threadMgr.wait(THREAD_PAYLOAD) _methods = [_("CD/DVD"), _("local ISO file"), _("Network")] + + if self.data.method.method == "harddrive" and \ + get_mount_device(DRACUT_ISODIR) == get_mount_device(DRACUT_REPODIR): + message = _("The installation source is in use by the installer and cannot be changed.") + self._window += [TextWidget(message), ""] + return True + if args == 3: text = [TextWidget(_(p)) for p in self._protocols] else: @@ -191,7 +189,7 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): # preliminary NFS source switch self.set_source_nfs() newspoke = SpecifyNFSRepoSpoke(self.app, self.data, self.storage, - self.payload, self.instclass, self._selection, self.errors) + self.payload, self.instclass, self._selection, self._error) self.app.switch_screen_modal(newspoke) self.apply() self.close() @@ -233,6 +231,10 @@ class SourceSpoke(EditTUISpoke, SourceSwitchHandler): if flags.askmethod: flags.askmethod = False + # if we had any errors, e.g. from a previous attempt to set the source, + # clear them at this point + self._error = False + payloadMgr.restartThread(self.storage, self.data, self.payload, self.instclass, checkmount=False) @@ -284,11 +286,11 @@ class SpecifyNFSRepoSpoke(EditTUISpoke, SourceSwitchHandler): Entry(N_("NFS mount options"), "opts", re.compile(".*$"), True) ] - def __init__(self, app, data, storage, payload, instclass, selection, errors): + def __init__(self, app, data, storage, payload, instclass, selection, error): EditTUISpoke.__init__(self, app, data, storage, payload, instclass) SourceSwitchHandler.__init__(self) self.selection = selection - self.errors = errors + self._error = error nfs = self.data.method self.args = DataHolder(server="", opts=nfs.opts or "") @@ -309,13 +311,13 @@ class SpecifyNFSRepoSpoke(EditTUISpoke, SourceSwitchHandler): return False if self.args.server.startswith("nfs://"): - self.args.server = self.args.server.strip("nfs://") + self.args.server = self.args.server[6:] try: (self.data.method.server, self.data.method.dir) = self.args.server.split(":", 2) except ValueError as err: LOG.error("ValueError: %s", err) - self.errors.append(_("Failed to set up installation source. Check the source address.")) + self._error = True return opts = self.args.opts or "" diff --git a/anaconda/pyanaconda/ui/tui/spokes/storage.py b/anaconda/pyanaconda/ui/tui/spokes/storage.py index 2df64ed..1c7b600 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/storage.py +++ b/anaconda/pyanaconda/ui/tui/spokes/storage.py @@ -86,8 +86,10 @@ class StorageSpoke(NormalTUISpoke): if self.data.zerombr.zerombr and arch.isS390(): # if zerombr is specified in a ks file and there are unformatted - # dasds, automatically format them - to_format = make_unformatted_dasd_list(self.selected_disks) + # dasds, automatically format them. pass in storage.devicetree here + # instead of storage.disks since mediaPresent is checked on disks; + # a dasd needing dasdfmt will fail this media check though + to_format = make_unformatted_dasd_list(d.name for d in getDisks(self.storage.devicetree)) if to_format: self.run_dasdfmt(to_format) @@ -121,7 +123,7 @@ class StorageSpoke(NormalTUISpoke): msg = _("No disks selected") if flags.automatedInstall and not self.storage.rootDevice: - return msg + msg = _("Kickstart insufficient") elif self.data.ignoredisk.onlyuse: msg = P_(("%d disk selected"), ("%d disks selected"), @@ -302,7 +304,7 @@ class StorageSpoke(NormalTUISpoke): # ask user to verify they want to format if zerombr not in ks file if not self.data.zerombr.zerombr: # prepare our msg strings; copied directly from dasdfmt.glade - summary = _("The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs can not be used during installation.\n\n") + summary = _("The following unformatted DASDs have been detected on your system. You can choose to format them now with dasdfmt or cancel to leave them unformatted. Unformatted DASDs cannot be used during installation.\n\n") warntext = _("Warning: All storage changes made using the installer will be lost when you choose to format.\n\nProceed to run dasdfmt?\n") diff --git a/anaconda/pyanaconda/ui/tui/spokes/time_spoke.py b/anaconda/pyanaconda/ui/tui/spokes/time_spoke.py index a6e59e3..a1a4b4b 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/time_spoke.py +++ b/anaconda/pyanaconda/ui/tui/spokes/time_spoke.py @@ -39,8 +39,7 @@ class TimeZoneSpoke(FirstbootSpokeMixIn, NormalTUISpoke): # needs to be unsorted in order to display in the same order as the GUI # so whatever self._regions = timezone.get_all_regions_and_timezones().keys() - self._timezones = dict((k, sorted(v)) for k,v in timezone.get_all_regions_and_timezones -().iteritems()) + self._timezones = dict((k, sorted(v)) for k,v in timezone.get_all_regions_and_timezones().items()) self._lower_regions = [r.lower() for r in self._timezones] self._zones = ["%s/%s" % (region, z) for region in self._timezones for z in self._timezones[region]] diff --git a/anaconda/pyanaconda/ui/tui/spokes/user.py b/anaconda/pyanaconda/ui/tui/spokes/user.py index 1c24d6d..68a27ad 100644 --- a/anaconda/pyanaconda/ui/tui/spokes/user.py +++ b/anaconda/pyanaconda/ui/tui/spokes/user.py @@ -66,7 +66,7 @@ class UserSpoke(FirstbootSpokeMixIn, EditTUISpoke): def __init__(self, app, data, storage, payload, instclass): FirstbootSpokeMixIn.__init__(self) - EditTUISpoke.__init__(self, app, data, storage, payload, instclass) + EditTUISpoke.__init__(self, app, data, storage, payload, instclass, "user") if self.data.user.userList: self.args = self.data.user.userList[0] @@ -99,7 +99,8 @@ class UserSpoke(FirstbootSpokeMixIn, EditTUISpoke): @property def showable(self): - return not (self.completed and flags.automatedInstall) + return not (self.completed and flags.automatedInstall + and self.data.user.seen and not self.dialog.policy.changesok) @property def mandatory(self): diff --git a/anaconda/pyanaconda/ui/tui/tools/Makefile.am b/anaconda/pyanaconda/ui/tui/tools/Makefile.am deleted file mode 100644 index 408c53a..0000000 --- a/anaconda/pyanaconda/ui/tui/tools/Makefile.am +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2011 Red Hat, Inc. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Author: Chris Lumens <clumens@redhat.com> - -EXTRA_DIST = run-text-hub.py run-text-spoke.py - -MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/pyanaconda/ui/tui/tools/run-text-hub.py b/anaconda/pyanaconda/ui/tui/tools/run-text-hub.py deleted file mode 100755 index b542fcb..0000000 --- a/anaconda/pyanaconda/ui/tui/tools/run-text-hub.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/python - -import sys, os -import os.path - -# Check command line arguments -if len(sys.argv)<2: - print "Usage: $0 <spoke module name> [<spoke widget class>]" - sys.exit(1) - -# Logging always needs to be set up first thing, or there'll be tracebacks. -from pyanaconda import anaconda_log -anaconda_log.init() - -from pyanaconda.installclass import DefaultInstall -from pyanaconda.storage import Storage -from pyanaconda.threads import initThreading -from pyanaconda.packaging.yumpayload import YumPayload -from pyanaconda.platform import getPlatform -from pykickstart.version import makeVersion -from pyanaconda.ui.tui.simpleline import App -from pyanaconda.ui.tui import YesNoDialog - -# Don't worry with fcoe, iscsi, dasd, any of that crud. -from pyanaconda.flags import flags -flags.imageInstall = True -flags.testing = True - -initThreading() - -# Figure out the part we are about to show: hub/spoke? -# And get the name of the module which represents it -if os.path.basename(sys.argv[0]) == "run-text-spoke.py": - spokeModuleName = "pyanaconda.ui.tui.spokes.%s" % sys.argv[1] - from pyanaconda.ui.common import Spoke - spokeBaseClass = Spoke - spokeText = "spoke" - SpokeText = "Spoke" -elif os.path.basename(sys.argv[0]) == "run-text-hub.py": - spokeModuleName = "pyanaconda.ui.tui.hubs.%s" % sys.argv[1] - from pyanaconda.ui.common import Hub - spokeBaseClass = Hub - spokeText = "hub" - SpokeText = "Hub" -else: - print "You have to run this command as run-spoke.py or run-hub.py." - sys.exit(1) - -# Set default spoke class -spokeClass = None -spokeClassName = None - -# Load spoke specified on the command line -# If the spoke module was specified, but the spoke class was not, -# try to find it using class hierarchy -try: - spokeClassName = sys.argv[2] - __import__(spokeModuleName, fromlist = [spokeClassName]) - spokeModule = sys.modules[spokeModuleName] -except IndexError: - __import__(spokeModuleName) - spokeModule = sys.modules[spokeModuleName] - for k,v in vars(spokeModule).iteritems(): - try: - print k,v - if issubclass(v, spokeBaseClass) and v != spokeBaseClass: - spokeClassName = k - spokeClass = v - except TypeError: - pass - -if not spokeClass: - try: - spokeClass = getattr(spokeModule, spokeClassName) - except KeyError: - print "%s %s could not be found in %s" % (SpokeText, spokeClassName, spokeModuleName) - sys.exit(1) - - -print "Running %s %s from %s" % (spokeText, spokeClass, spokeModule) - -platform = getPlatform() -ksdata = makeVersion() -storage = Storage(data=ksdata, platform=platform) -storage.reset() -instclass = DefaultInstall() -app = App("TEST HARNESS", yes_or_no_question = YesNoDialog) - -payload = YumPayload(ksdata) -payload.setup(storage) -payload.install_log = sys.stdout - -spoke = spokeClass(app, ksdata, storage, payload, instclass) - -if not spoke.showable: - print "This %s is not showable, but I'll continue anyway." % spokeText - -app.schedule_screen(spoke) -app.run() - -if hasattr(spoke, "status"): - print "%s status:\n%s\n" % (SpokeText, spoke.status) -if hasattr(spoke, "completed"): - print "%s completed:\n%s\n" % (SpokeText, spoke.completed) -print "%s kickstart fragment:\n%s" % (SpokeText, ksdata) diff --git a/anaconda/pyanaconda/ui/tui/tools/run-text-spoke.py b/anaconda/pyanaconda/ui/tui/tools/run-text-spoke.py deleted file mode 100755 index c503cdc..0000000 --- a/anaconda/pyanaconda/ui/tui/tools/run-text-spoke.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/python - -import sys, os -import os.path - -# Check command line arguments -if len(sys.argv)<2: - print("Usage: $0 <spoke module name> [<spoke widget class>]") - sys.exit(1) - -# Logging always needs to be set up first thing, or there'll be tracebacks. -from pyanaconda import anaconda_log -anaconda_log.init() - -from pyanaconda.installclass import DefaultInstall -from blivet import Blivet -from pyanaconda.threads import initThreading -from pyanaconda.packaging.yumpayload import YumPayload -from pykickstart.version import makeVersion -from pyanaconda.ui.tui.simpleline import App -from pyanaconda.ui.tui import YesNoDialog - -# Don't worry with fcoe, iscsi, dasd, any of that crud. -from pyanaconda.flags import flags -flags.imageInstall = True -flags.testing = True - -initThreading() - -# Figure out the part we are about to show: hub/spoke? -# And get the name of the module which represents it -if os.path.basename(sys.argv[0]) == "run-text-spoke.py": - spokeModuleName = "pyanaconda.ui.tui.spokes.%s" % sys.argv[1] - from pyanaconda.ui.common import Spoke - spokeBaseClass = Spoke - spokeText = "spoke" - SpokeText = "Spoke" -elif os.path.basename(sys.argv[0]) == "run-text-hub.py": - spokeModuleName = "pyanaconda.ui.tui.hubs.%s" % sys.argv[1] - from pyanaconda.ui.common import Hub - spokeBaseClass = Hub - spokeText = "hub" - SpokeText = "Hub" -else: - print("You have to run this command as run-spoke.py or run-hub.py.") - sys.exit(1) - -# Set default spoke class -spokeClass = None -spokeClassName = None - -# Load spoke specified on the command line -# If the spoke module was specified, but the spoke class was not, -# try to find it using class hierarchy -try: - spokeClassName = sys.argv[2] - __import__(spokeModuleName, fromlist = [spokeClassName]) - spokeModule = sys.modules[spokeModuleName] -except IndexError: - __import__(spokeModuleName) - spokeModule = sys.modules[spokeModuleName] - for k,v in vars(spokeModule).iteritems(): - try: - print(k,v) - if issubclass(v, spokeBaseClass) and v != spokeBaseClass: - spokeClassName = k - spokeClass = v - except TypeError: - pass - -if not spokeClass: - try: - spokeClass = getattr(spokeModule, spokeClassName) - except KeyError: - print("%s %s could not be found in %s" % (SpokeText, spokeClassName, spokeModuleName)) - sys.exit(1) - - -print("Running %s %s from %s" % (spokeText, spokeClass, spokeModule)) - -ksdata = makeVersion() -storage = Blivet(ksdata=ksdata) -storage.reset() -instclass = DefaultInstall() -app = App("TEST HARNESS", yes_or_no_question = YesNoDialog) - -payload = YumPayload(ksdata) -payload.setup(storage) -payload.install_log = sys.stdout - -spoke = spokeClass(app, ksdata, storage, payload, instclass) - -if not spoke.showable: - print("This %s is not showable, but I'll continue anyway." % spokeText) - -app.schedule_screen(spoke) -app.run() - -if hasattr(spoke, "status"): - print("%s status:\n%s\n" % (SpokeText, spoke.status)) -if hasattr(spoke, "completed"): - print("%s completed:\n%s\n" % (SpokeText, spoke.completed)) -print("%s kickstart fragment:\n%s" % (SpokeText, ksdata)) diff --git a/anaconda/pyanaconda/ui/tui/tuiobject.py b/anaconda/pyanaconda/ui/tui/tuiobject.py index 0a5243f..855537f 100644 --- a/anaconda/pyanaconda/ui/tui/tuiobject.py +++ b/anaconda/pyanaconda/ui/tui/tuiobject.py @@ -116,15 +116,6 @@ class TUIObject(tui.UIScreen, common.UIObject): def showable(self): return True - def teardown(self): - pass - - def initialize(self): - """This method gets called whenever Hub or UserInterface prepares - all found objects for use. It is called only once and has no direct - connection to rendering.""" - pass - def refresh(self, args = None): """Put everything to display into self.window list.""" tui.UIScreen.refresh(self, args) diff --git a/anaconda/pyanaconda/users.py b/anaconda/pyanaconda/users.py index 68e497a..2d56151 100644 --- a/anaconda/pyanaconda/users.py +++ b/anaconda/pyanaconda/users.py @@ -20,16 +20,19 @@ # import libuser -import string +# Used for ascii_letters and digits constants +import string # pylint: disable=deprecated-module import crypt import random import tempfile import os import os.path +import locale from pyanaconda import iutil import pwquality from pyanaconda.iutil import strip_accents from pyanaconda.constants import PASSWORD_MIN_LEN +from pyanaconda.errors import errorHandler, PasswordCryptError, ERROR_RAISE import logging log = logging.getLogger("anaconda") @@ -40,21 +43,18 @@ def createLuserConf(instPath, algoname='sha512'): This must be called before User() is instantiated the first time so that libuser.admin will use the temporary config file. """ - createTmp = False - try: - fn = os.environ["LIBUSER_CONF"] - if os.access(fn, os.F_OK): - log.info("removing libuser.conf at %s", os.getenv("LIBUSER_CONF")) - os.unlink(fn) - log.info("created new libuser.conf at %s with instPath=\"%s\"", fn, instPath) - fd = open(fn, 'w') - except (OSError, IOError, KeyError): - createTmp = True - if createTmp: + # If LIBUSER_CONF is not set, create a new temporary file + if "LIBUSER_CONF" not in os.environ: (fp, fn) = tempfile.mkstemp(prefix="libuser.") log.info("created new libuser.conf at %s with instPath=\"%s\"", fn, instPath) - fd = os.fdopen(fp, 'w') + fd = os.fdopen(fp, "w") + # This is only ok if createLuserConf is first called before threads are started + os.environ["LIBUSER_CONF"] = fn # pylint: disable=environment-modify + else: + fn = os.environ["LIBUSER_CONF"] + log.info("Clearing libuser.conf at %s", fn) + fd = open(fn, "w") buf = """ [defaults] @@ -79,7 +79,6 @@ login_defs = %(instPath)s/etc/login.defs fd.write(buf) fd.close() - os.environ["LIBUSER_CONF"] = fn return fn @@ -114,12 +113,18 @@ def cryptPassword(password, algo=None): saltstr = salts[algo] for _i in range(saltlen): - saltstr = saltstr + random.choice (string.letters + + saltstr = saltstr + random.choice (string.ascii_letters + string.digits + './') - return crypt.crypt (password, saltstr) + cryptpw = crypt.crypt (password, saltstr) + if cryptpw is None: + exn = PasswordCryptError(algo=algo) + if errorHandler.cb(exn) == ERROR_RAISE: + raise exn -def validatePassword(pw, user="root", settings=None): + return cryptpw + +def validatePassword(pw, user="root", settings=None, minlen=None): """Check the quality of a password. This function does three things: given a password and an optional @@ -144,6 +149,8 @@ def validatePassword(pw, user="root", settings=None): :param settings: an optional PWQSettings object :type settings: pwquality.PWQSettings + :param int minlen: Minimum acceptable password length. If not passed, + use the default length from PASSWORD_MIN_LEN :returns: A tuple containing (bool(valid), int(score), str(message)) :rtype: tuple @@ -161,6 +168,9 @@ def validatePassword(pw, user="root", settings=None): validatePassword.pwqsettings.minlen = PASSWORD_MIN_LEN settings = validatePassword.pwqsettings + if minlen is not None: + settings.minlen = minlen + if valid: try: strength = settings.check(pw, None, user) @@ -168,7 +178,8 @@ def validatePassword(pw, user="root", settings=None): # Leave valid alone here: the password is weak but can still # be accepted. # PWQError values are built as a tuple of (int, str) - message = e[1] + # Convert the str message (encoded to the current locale) to a unicode + message = e.args[1].decode(locale.nl_langinfo(locale.CODESET)) return (valid, strength, message) @@ -202,7 +213,8 @@ class Users: if not root in ["","/"]: os.chroot(root) os.chdir("/") - del(os.environ["LIBUSER_CONF"]) + # This is ok because it's after a fork + del(os.environ["LIBUSER_CONF"]) # pylint: disable=environment-modify self.admin = libuser.admin() @@ -293,16 +305,18 @@ class Users: if kwargs.get("gid", -1) >= 0: groupEnt.set(libuser.GIDNUMBER, kwargs["gid"]) - grpLst = filter(lambda grp: grp, - map(self.admin.lookupGroupByName, kwargs.get("groups", []))) + grpLst = [grp for grp in map(self.admin.lookupGroupByName, kwargs.get("groups", [])) if grp] userEnt.set(libuser.GIDNUMBER, [groupEnt.get(libuser.GIDNUMBER)[0]] + - map(lambda grp: grp.get(libuser.GIDNUMBER)[0], grpLst)) + [grp.get(libuser.GIDNUMBER)[0] for grp in grpLst]) - if kwargs.get("homedir", False): - userEnt.set(libuser.HOMEDIRECTORY, kwargs["homedir"]) - else: - iutil.mkdirChain('/home') - userEnt.set(libuser.HOMEDIRECTORY, "/home/" + user_name) + homedir = kwargs.get("homedir", None) + if not homedir: + homedir = "/home/" + user_name + # libuser expects the parent directory tree to exist. + parent_dir = iutil.parent_dir(homedir) + if parent_dir: + iutil.mkdirChain(parent_dir) + userEnt.set(libuser.HOMEDIRECTORY, homedir) if kwargs.get("shell", False): userEnt.set(libuser.LOGINSHELL, kwargs["shell"]) @@ -415,3 +429,36 @@ class Users: def setRootPassword(self, password, isCrypted=False, isLocked=False, algo=None): return self.setUserPassword("root", password, isCrypted, isLocked, algo) + + def setUserSshKey(self, username, key, **kwargs): + childpid = self._prepareChroot(kwargs.get("root", iutil.getSysroot())) + + if childpid == 0: + user = self.admin.lookupUserByName(username) + if not user: + log.error("setUserSshKey: user %s does not exist", username) + os._exit(1) + + homedir = user.get(libuser.HOMEDIRECTORY)[0] + if not os.path.exists(homedir): + log.error("setUserSshKey: home directory for %s does not exist", username) + os._exit(1) + + sshdir = os.path.join(homedir, ".ssh") + if not os.path.isdir(sshdir): + os.mkdir(sshdir, 0o700) + iutil.eintr_retry_call(os.chown, sshdir, user.get(libuser.UIDNUMBER)[0], user.get(libuser.GIDNUMBER)[0]) + + authfile = os.path.join(sshdir, "authorized_keys") + authfile_existed = os.path.exists(authfile) + with open(authfile, "a") as f: + f.write(key + "\n") + + # Only change mode and ownership if we created it + if not authfile_existed: + iutil.eintr_retry_call(os.chmod, authfile, 0o600) + iutil.eintr_retry_call(os.chown, authfile, user.get(libuser.UIDNUMBER)[0], user.get(libuser.GIDNUMBER)[0]) + iutil.execWithRedirect("restorecon", ["-r", sshdir]) + os._exit(0) + else: + return self._finishChroot(childpid) diff --git a/anaconda/pyanaconda/vnc.py b/anaconda/pyanaconda/vnc.py index 9abee56..473c16c 100644 --- a/anaconda/pyanaconda/vnc.py +++ b/anaconda/pyanaconda/vnc.py @@ -21,7 +21,7 @@ import os, sys import time -from pyanaconda import network, product, iutil +from pyanaconda import constants, network, product, iutil import socket import subprocess import dbus @@ -55,11 +55,10 @@ def shutdownServer(): class VncServer: - def __init__(self, display="1", root="/", ip=None, name=None, + def __init__(self, root="/", ip=None, name=None, password="", vncconnecthost="", vncconnectport="", log_file="/tmp/vncserver.log", pw_file="/tmp/vncpassword"): - self.display = display self.root = root self.ip = ip self.name = name @@ -120,10 +119,14 @@ class VncServer: if self.ip.find(':') != -1: ipstr = "[%s]" % (self.ip,) - if (self.name is not None) and (not self.name.startswith('localhost')) and (ipstr is not None): - self.connxinfo = "%s:%s (%s:%s)" % (socket.getfqdn(name=self.name), self.display, ipstr, self.display) + name_ips = [i[4][0] for i in socket.getaddrinfo(self.name, 0)] + if self.name is not None and not self.name.startswith('localhost') \ + and ipstr is not None and self.ip in name_ips: + self.connxinfo = "%s:%s (%s:%s)" % \ + (socket.getfqdn(name=self.name), constants.X_DISPLAY_NUMBER, + ipstr, constants.X_DISPLAY_NUMBER) elif ipstr is not None: - self.connxinfo = "%s:%s" % (ipstr, self.display,) + self.connxinfo = "%s:%s" % (ipstr, constants.X_DISPLAY_NUMBER) else: self.connxinfo = None @@ -155,10 +158,10 @@ class VncServer: else: hostarg = self.vncconnecthost - vncconfigcommand = [self.root+"/usr/bin/vncconfig", "-display", ":%s"%self.display, "-connect", hostarg] + vncconfigcommand = [self.root+"/usr/bin/vncconfig", "-display", ":%s" % constants.X_DISPLAY_NUMBER, "-connect", hostarg] for _i in range(maxTries): - vncconfp = subprocess.Popen(vncconfigcommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # vncconfig process + vncconfp = iutil.startProgram(vncconfigcommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # vncconfig process err = vncconfp.communicate()[1] if err == '': @@ -170,12 +173,23 @@ class VncServer: continue else: log.critical(err) + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) self.log.error(P_("Giving up attempting to connect after %d try!\n", "Giving up attempting to connect after %d tries!\n", maxTries), maxTries) return False + def startVncConfig(self): + """Attempt to start vncconfig""" + + self.log.info(_("Attempting to start vncconfig")) + + vncconfigcommand = [self.root+"/usr/bin/vncconfig", "-nowin", "-display", ":%s" % constants.X_DISPLAY_NUMBER] + + # Use startProgram to run vncconfig in the background + iutil.startProgram(vncconfigcommand, stdout=self.openlogfile(), stderr=subprocess.STDOUT) + def VNCListen(self): """Put the server in listening mode. @@ -186,7 +200,7 @@ class VncServer: else: self.log.info(_("Please manually connect your vnc client to <IP ADDRESS>:%s " "to begin the install. Switch to the shell (Ctrl-B 2) and " - "run 'ip addr' to find the <IP ADDRESS>."), self.display) + "run 'ip addr' to find the <IP ADDRESS>."), constants.X_DISPLAY_NUMBER) def startServer(self): self.log.info(_("Starting VNC...")) @@ -197,6 +211,7 @@ class VncServer: self.initialize() except (socket.herror, dbus.DBusException, ValueError) as e: stdoutLog.critical("Could not initialize the VNC server: %s", e) + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) if self.password and (len(self.password) < 6 or len(self.password) > 8): @@ -212,26 +227,20 @@ class VncServer: self.setVNCPassword() # Lets start the xvnc. - xvnccommand = [ XVNC_BINARY_NAME, ":%s" % self.display, + xvnccommand = [ XVNC_BINARY_NAME, ":%s" % constants.X_DISPLAY_NUMBER, "-depth", "16", "-br", "IdleTimeout=0", "-auth", "/dev/null", "-once", "DisconnectClients=false", "desktop=%s" % (self.desktop,), "SecurityTypes=%s" % SecurityTypes, "rfbauth=%s" % rfbauth ] try: - xvncp = subprocess.Popen(xvnccommand, stdout=self.openlogfile(), stderr=subprocess.STDOUT) + iutil.startX(xvnccommand, output_redirect=self.openlogfile()) except OSError: stdoutLog.critical("Could not start the VNC server. Aborting.") + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) - # Lets give the xvnc time to initialize - time.sleep(1) - - # Make sure it hasn't blown up - if xvncp.poll() != None: - sys.exit(1) - else: - self.log.info(_("The VNC server is now running.")) + self.log.info(_("The VNC server is now running.")) # Lets tell the user what we are going to do. if self.vncconnecthost != "": @@ -247,6 +256,7 @@ class VncServer: self.log.warning(_("\n\nYou chose to execute vnc with a password. \n\n")) else: self.log.warning(_("\n\nUnknown Error. Aborting. \n\n")) + iutil.ipmi_report(constants.IPMI_ABORTED) sys.exit(1) # Lets try to configure the vnc server to whatever the user specified @@ -257,7 +267,8 @@ class VncServer: else: self.VNCListen() - os.environ["DISPLAY"]=":%s" % self.display + # Start vncconfig for copy/paste + self.startVncConfig() def changeVNCPasswdWindow(self): """ Change the password to a sane parameter. diff --git a/anaconda/scripts/Makefile.am b/anaconda/scripts/Makefile.am index 9bcfdcc..61df441 100644 --- a/anaconda/scripts/Makefile.am +++ b/anaconda/scripts/Makefile.am @@ -21,7 +21,7 @@ scriptsdir = $(libexecdir)/$(PACKAGE_NAME) dist_scripts_SCRIPTS = upd-updates run-anaconda anaconda-yum zramswapon zramswapoff zram-stats dist_noinst_SCRIPTS = upd-kernel makeupdates -dist_bin_SCRIPTS = analog anaconda-cleanup instperf +dist_bin_SCRIPTS = analog anaconda-cleanup instperf anaconda-disable-nm-ibft-plugin stage2scriptsdir = $(datadir)/$(PACKAGE_NAME) dist_stage2scripts_SCRIPTS = restart-anaconda diff --git a/anaconda/scripts/anaconda-cleanup b/anaconda/scripts/anaconda-cleanup index 8664685..ea3ebcd 100755 --- a/anaconda/scripts/anaconda-cleanup +++ b/anaconda/scripts/anaconda-cleanup @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 """ always: - unmount everything under /mnt/sysimage @@ -38,7 +38,7 @@ flags.imageInstall = True import pyanaconda.anaconda_log pyanaconda.anaconda_log.init() -from blivet import StorageDiscoveryConfig +from blivet.blivet import StorageDiscoveryConfig from blivet.devicetree import DeviceTree from blivet import devicelibs diff --git a/anaconda/scripts/anaconda-disable-nm-ibft-plugin b/anaconda/scripts/anaconda-disable-nm-ibft-plugin new file mode 100644 index 0000000..6ddaf46 --- /dev/null +++ b/anaconda/scripts/anaconda-disable-nm-ibft-plugin @@ -0,0 +1,2 @@ +#!/bin/sh +rm -f /etc/NetworkManager/conf.d/10-ibft-plugin.conf diff --git a/anaconda/scripts/anaconda-yum b/anaconda/scripts/anaconda-yum index 77067c7..8cc4d8b 100644 --- a/anaconda/scripts/anaconda-yum +++ b/anaconda/scripts/anaconda-yum @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # pylint: disable=bad-preconf-access # # Copyright (C) 2013 Red Hat, Inc. @@ -27,7 +27,7 @@ import rpm import rpmUtils import yum from urlgrabber.grabber import URLGrabError -from pyanaconda.iutil import xprogressive_delay +from pyanaconda.iutil import xprogressive_delay, eintr_retry_call YUM_PLUGINS = ["fastestmirror", "langpacks"] @@ -82,6 +82,7 @@ def run_yum_transaction(release, arch, yum_conf, install_root, ts_file, script_l from yum.Errors import PackageSackError, RepoError, YumBaseError, YumRPMTransError # remove some environmental variables that can cause problems with package scripts + # pylint: disable=environment-modify env_remove = ('DISPLAY', 'DBUS_SESSION_BUS_ADDRESS') for k in env_remove: if k in os.environ: @@ -127,7 +128,7 @@ def run_yum_transaction(release, arch, yum_conf, install_root, ts_file, script_l print("INFO: populate transaction set") xdelay = xprogressive_delay() - for retry_count in xrange(0, MAX_DOWNLOAD_RETRIES+1): + for retry_count in range(0, MAX_DOWNLOAD_RETRIES+1): # retry count == 0 -> first attempt # retry count > 0 -> retry if retry_count: @@ -156,8 +157,8 @@ def run_yum_transaction(release, arch, yum_conf, install_root, ts_file, script_l yb.ts.clean() # Write scriptlet output to a file to be logged later - logfile = open(script_log, "w") - yb.ts.ts.scriptFd = logfile.fileno() + logfile = eintr_retry_call(os.open, script_log, os.O_WRONLY|os.O_CREAT) + yb.ts.ts.scriptFd = logfile rpm.setLogFile(logfile) # create the install callback @@ -183,7 +184,7 @@ def run_yum_transaction(release, arch, yum_conf, install_root, ts_file, script_l print("INFO: transaction complete") finally: yb.ts.close() - logfile.close() + eintr_retry_call(os.close, logfile) except YumBaseError as e: print("ERROR: transaction error: %s" % e) finally: @@ -214,14 +215,14 @@ class RPMCallback(object): def __init__(self, yb, arch, log, debug=False): """ :param yb: YumBase object :type yb: YumBase - :param log: script logfile - :type log: string + :param log: file-descriptor of script logfile + :type log: int :param statusQ: status communication back to other process :type statusQ: Queue """ self.yb = yb # yum.YumBase self.base_arch = arch - self.install_log = log # logfile for yum script logs + self.install_log = log # fd of logfile for yum script logs self.debug = debug self.package_file = None # file instance (package file management) @@ -277,7 +278,7 @@ class RPMCallback(object): log_msg = msg_format % (txmbr.po, self.completed_actions, self.total_actions) - self.install_log.write(log_msg+"\n") + eintr_retry_call(os.write, self.install_log, log_msg+"\n") print("PROGRESS_INSTALL: %s" % progress_msg) try: @@ -292,7 +293,7 @@ class RPMCallback(object): exception_message = "" xdelay = xprogressive_delay() - for retry_count in xrange(0, MAX_DOWNLOAD_RETRIES+1): + for retry_count in range(0, MAX_DOWNLOAD_RETRIES+1): # retry count == 0 -> first attempt # retry count > 0 -> retry if retry_count and retry_message: @@ -380,6 +381,9 @@ class RPMCallback(object): # display the message. if self.completed_actions == self.total_actions: print("PROGRESS_POST:") + elif self.completed_actions is not None and self.total_actions is not None: + pct = 100 * (self.completed_actions / float(self.total_actions)) + print("PERCENT: %f" % pct) def cpio_error(self, amount, total, key, data): name = self._get_txmbr(key)[0] diff --git a/anaconda/scripts/analog b/anaconda/scripts/analog index eb1962e..d2d68aa 100755 --- a/anaconda/scripts/analog +++ b/anaconda/scripts/analog @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/python2 # # analog: Remote logging manager for the Red Hat Installer # @@ -24,11 +24,9 @@ from __future__ import print_function import getpass -import errno import optparse import os import os.path -import signal import sys DEFAULT_PORT = 6080 @@ -194,9 +192,9 @@ def pid_location(unique_id): os.mkdir(directory) return location -if __name__ == "__main__": +def main(): try: - (options, args) = get_opts() + (options, _args) = get_opts() except OptParserError as exc: exc.parser.error(str(exc)) sys.exit(1) @@ -205,9 +203,9 @@ if __name__ == "__main__": if options.output: options.output = os.path.abspath(options.output) try: - with open(options.output, 'w') as file: - file.write(config) - except IOError as e: + with open(options.output, 'w') as f: + f.write(config) + except IOError: print("Can't write into file %s" % options.output, file=sys.stderr) sys.exit(1) if options.stdout: @@ -218,3 +216,6 @@ if __name__ == "__main__": }) else: print(config) + +if __name__ == "__main__": + main() diff --git a/anaconda/scripts/githooks/pre-commit b/anaconda/scripts/githooks/pre-commit index c8832ef..257155f 100644 --- a/anaconda/scripts/githooks/pre-commit +++ b/anaconda/scripts/githooks/pre-commit @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 """ Run the Anaconda pylint tests on the files changed in this commit Set NOPYLINT env variable to skip this. eg. NOPYLINT= git commit @@ -8,6 +8,7 @@ import os import sys from subprocess import check_output, CalledProcessError +import tempfile def is_python(filename): if filename.endswith(".py"): @@ -30,30 +31,46 @@ if "NOPYLINT" in os.environ: # run pylint on all the python files changed by this commit try: - git_files = check_output("git status --porcelain", shell=True) + git_files = check_output(["git", "status", "--porcelain", "-z"]) except CalledProcessError: sys.exit(1) +# If git status returned nothing, return +if not git_files: + sys.exit(0) + +git_files = git_files.strip('\0').split('\0') + +pylint_names = [] pylint_files = [] # Lines look like: MM tests/pylint/runpylint.sh # The first character is the status in the index (or our side of a merge), # the second character is the status in the tree (or their side of a merge). -for gf in git_files.splitlines(): +# Use an iterator so we can skip lines in the case of renames +git_iter = iter(git_files) +for gf in git_iter: # If the file is being removed, or is not in git, or is not part of this # commit, ignore it if gf[0] in ('D', '?', ' '): continue - # If the file is being renamed, grab the target filename + + # If the file is being renamed, the target filename (which we want) is on + # the current line, and the next line is the source, which we want to skip if gf[0] == 'R': - target = gf[3:].split(' -> ')[1] - if is_python(target): - pylint_files.append(target) - elif is_python(gf[3:]): - # If the file is unmerged or changed locally, raise an error - if gf[0] == 'U' or gf[1] in ('U', 'M'): - print("ERROR: %s in commit does not match tree" % gf[3:]) - sys.exit(1) - pylint_files.append(gf[3:]) + git_iter.next() + + if is_python(gf[3:]): + pylint_names.append(gf[3:]) + + # If the file is unmerged, changed, or deleted locally, create a + # temporary file with the actual content that will be committed + if gf[0] == 'U' or gf[1] in ('U', 'M', 'D'): + tmpfile = tempfile.NamedTemporaryFile(prefix=gf[3:].replace('/', '.')) + tmpfile.write(check_output(["git", "show", ":%s" % gf[3:]])) + tmpfile.flush() + pylint_files.append(tmpfile.name) + else: + pylint_files.append(gf[3:]) if not pylint_files: sys.exit(0) @@ -63,7 +80,7 @@ if not pylint_files: env = os.environ.copy() env["PYTHONPATH"] = OTHER_MODULES_PATH -print("Running pylint on %s" % " ".join(pylint_files)) +print("Running pylint on %s" % " ".join(pylint_names)) try: check_output(["./tests/pylint/runpylint.sh"] + pylint_files, env=env) except CalledProcessError as e: diff --git a/anaconda/scripts/githooks/pre-push b/anaconda/scripts/githooks/pre-push index db8c8ae..ca84b53 100644 --- a/anaconda/scripts/githooks/pre-push +++ b/anaconda/scripts/githooks/pre-push @@ -6,6 +6,11 @@ exec 1>&2 +if ! type parallel 2>&1 > /dev/null; then + echo "parallel must be installed" + exit 99 +fi + # git gives us these on STDIN read more_args local_ref=$(echo ${more_args}|cut -d' ' -f1) @@ -72,11 +77,10 @@ fi ## main ## # fork separate process for every commit, querying bugzilla takes time -num_commits=$(echo ${commits}|wc -w) -echo ${commits}|xargs -n1 --max-procs=${num_commits} .git/hooks/check_commit_msg.sh ${release} -xargs_ret=$? +echo -n ${commits}|parallel --no-notice -d' ' --gnu -j0 .git/hooks/check_commit_msg.sh ${release} {} +ret=$? if [ ${ACK_FATAL} == "0" ]; then - test ${xargs_ret} -eq 0 + test ${ret} -eq 0 exit $? else exit 0 diff --git a/anaconda/scripts/instperf b/anaconda/scripts/instperf index f0d53b2..a4fe28a 100755 --- a/anaconda/scripts/instperf +++ b/anaconda/scripts/instperf @@ -1,7 +1,6 @@ -#!/usr/bin/python +#!/usr/bin/python2 import logging -from string import split from subprocess import Popen, PIPE import time @@ -12,7 +11,7 @@ import time def topProcesses(): output = Popen(["ps", "-eo", "comm,rss", "--sort", "-rss", "--no-headers"], stdout=PIPE).communicate()[0] top5 = output.split("\n")[:5] - return ",".join(map(lambda (a,b): a+":"+b, map(split, top5))) + return ",".join("%s:%s" % (proc[0], proc[1]) for proc in (s.split() for s in top5)) def logit(): buffers = 0 diff --git a/anaconda/scripts/makeupdates b/anaconda/scripts/makeupdates index de3c5dd..e03dd79 100755 --- a/anaconda/scripts/makeupdates +++ b/anaconda/scripts/makeupdates @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # makeupdates - Generate an updates.img containing changes since the last # tag, but only changes to the main anaconda runtime. @@ -31,6 +31,8 @@ import urllib import threading import multiprocessing import argparse +import tempfile +import fnmatch from collections import namedtuple try: from rpmUtils import miscutils # available from the yum-utils package @@ -218,7 +220,7 @@ def create_RPM_cache_folder(): if not os.path.exists(RPM_FOLDER_NAME): os.makedirs(RPM_FOLDER_NAME) -def copyUpdatedFiles(tag, updates, cwd): +def copyUpdatedFiles(tag, updates, cwd, builddir): def install_to_dir(fname, relpath): sys.stdout.write("Including %s\n" % fname) outdir = os.path.join(updates, relpath) @@ -226,6 +228,24 @@ def copyUpdatedFiles(tag, updates, cwd): os.makedirs(outdir) shutil.copy2(fname, outdir) + def install_gschema(): + # Run make install to a temp directory and pull the compiled file out + # of it + tmpdir = tempfile.mkdtemp() + try: + os.system('make -C %s/data/window-manager/config install DESTDIR=%s' % + (builddir,tmpdir)) + # Find the .compiled file + for root, _dirs, files in os.walk(tmpdir): + for f in files: + if f.endswith('.compiled'): + install_to_dir(os.path.join(root, f), + 'usr/share/anaconda/window-manager/glib-2.0/schemas') + finally: + shutil.rmtree(tmpdir) + + + # Updates get overlaid onto the runtime filesystem. Anaconda expects them # to be in /run/install/updates, so put them in # $updatedir/run/install/updates. @@ -308,6 +328,10 @@ def copyUpdatedFiles(tag, updates, cwd): install_to_dir(gitfile, "usr/share/anaconda/pixmaps") elif gitfile.startswith("data/ui/"): install_to_dir(gitfile, "usr/share/anaconda/ui") + elif gitfile.startswith("data/window-manager/config"): + install_gschema() + elif gitfile.startswith("data/window-manager/theme"): + install_to_dir(gitfile, "usr/share/themes/Anaconda/metacity-1") elif gitfile.startswith("data/post-scripts/"): install_to_dir(gitfile, "usr/share/anaconda/post-scripts") elif any(gitfile.endswith(libexec_script) for libexec_script in \ @@ -361,6 +385,10 @@ def isysChanged(tag): def widgetsChanged(tag): return _compilableChanged(tag, 'widgets') +def auditdChanged(tag): + return _compilableChanged(tag, 'pyanaconda/isys/auditd.c') or \ + _compilableChanged(tag, 'pyanaconda/isys/auditd.h') + def checkAutotools(srcdir, builddir): # Assumes that cwd is srcdir if not os.path.isfile(os.path.join(builddir, 'Makefile')): @@ -393,6 +421,23 @@ def copyUpdatedIsys(updates, srcdir, builddir): if os.path.isfile(isysmodule): shutil.copy2(isysmodule, tmpupdates) +def copyUpdatedAuditd(updates, srcdir, builddir): + os.chdir(srcdir) + print("copyUpdatedIsys BUILLDIR %s" % builddir) + auditdir = updates + '/usr/sbin' + + checkAutotools(srcdir, builddir) + + os.system('make -C %s -j %d auditd' % (builddir + '/pyanaconda/isys', multiprocessing.cpu_count())) + + # Copy the auditd binary to /usr/sbin + if not os.path.isdir(auditdir): + os.makedirs(auditdir) + + auditd = builddir + '/pyanaconda/isys/auditd' + if os.path.isfile(auditd): + shutil.copy2(auditd, auditdir) + def copyUpdatedWidgets(updates, srcdir, builddir): os.chdir(srcdir) @@ -457,7 +502,17 @@ def addRpms(updates_path, add_rpms): # relative paths can be used with -a/--add add_rpms = map(os.path.abspath, add_rpms) + # resolve wildcards and also eliminate non-existing RPMs + resolved_rpms = [] for rpm in add_rpms: + resolved_path = glob.glob(rpm) + if not(resolved_path): + print("warning: requested rpm %s does not exist and can't be aded" % rpm) + elif len(resolved_path) > 1: + print("wildcard %s resolved to %d paths" % (rpm, len(resolved_path))) + resolved_rpms.extend(resolved_path) + + for rpm in resolved_rpms: cmd = "cd %s && rpm2cpio %s | cpio -dium" % (updates_path, rpm) sys.stdout.write(cmd+"\n") os.system(cmd) @@ -467,7 +522,7 @@ def createUpdatesImage(cwd, updates): os.system("find . | cpio -c -o | pigz -9cv > %s/updates.img" % (cwd,)) sys.stdout.write("updates.img ready\n") -def check_for_new_packages(tag, arch, added_rpms, specfile_path): +def check_for_new_packages(tag, arch, args, specfile_path): """Download any new packages added to Requires and Defines since the given tag, return list of RPM paths """ @@ -607,6 +662,19 @@ def check_for_new_packages(tag, arch, added_rpms, specfile_path): print("%s %s %s" % (p.name, p.version_request, p.version)) else: print(p.name) + + # remove ignored packages + ignored_count = 0 + for ignored_package in args.ignored_packages: + matches = fnmatch.filter(new_packages, ignored_package) + # the ignored package specifications support glob + for match in matches: + print("the new package %s matches %s and will be ignored" % (match, ignored_package)) + del new_packages[match] + ignored_count += 1 + if ignored_count: + print("%d new packages have been ignored" % ignored_count) + else: print("no new Requires or updated %%defines for Requires found") return [] @@ -616,7 +684,7 @@ def check_for_new_packages(tag, arch, added_rpms, specfile_path): # get package names for RPMs added by the -a/--add flags added_names = {} - for path in added_rpms: + for path in args.add_rpms: try: basename = os.path.basename(path) name = get_pkg_tuple(basename)[0] @@ -738,7 +806,7 @@ def get_RPMs_from_koji(packages, fedora_number, arch): index = 1 print("starting %d worker threads" % len(packages)) - for _package_name, package in packages.iteritems(): + for _package_name, package in packages.items(): thread = threading.Thread(name=index, target=get_rpm_from_Koji_thread, args=(package, fedora_number, arch, rpm_paths, print_lock)) @@ -897,6 +965,15 @@ def get_rpm_from_Koji_thread(package, fedora_number, arch, else: print("%s %s in any version was not found in Koji" % (prefix, package.name)) +class ExtendAction(argparse.Action): + """ A parsing action that extends a list of items instead of appending to + it. Useful where there is an option that can be used multiple times, + and each time the values yielded are a list, and a single list is + desired. + """ + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, getattr(namespace, self.dest, []) + values) + def main(): cwd = os.getcwd() configure = os.path.realpath(os.path.join(cwd, 'configure.ac')) @@ -920,23 +997,17 @@ def main(): parser.add_argument('-p', '--po', action='store_true', help='update translations') - # -a/--add can be both used multiple times and multiple paths can be - # passed to a single -a/--add option, all provided paths will be then added - # to the add_rpm list - # - # Example: - # - # makeupdates -a foo.rpm -a bar.rpm baz.rpm --add spam.rpm - # - # This will add foo.rpm, bar.rpm, baz.rpm and spam.rpm to the - # updates image. - parser.add_argument('-a', '--add', action='append', type=str, nargs='+', + parser.add_argument('-a', '--add', action=ExtendAction, type=str, nargs='+', dest='add_rpms', metavar='PATH_TO_RPM', default=[], - help='add contents of RPMs to the updates image') + help='add contents of RPMs to the updates image (glob supported)') parser.add_argument('-f', '--fetch', action='store', type=str, metavar="ARCH", help='autofetch new dependencies from Koji for ARCH') + parser.add_argument('-i', '--ignore', action=ExtendAction, type=str, metavar="PACKAGE_NAME", + dest="ignored_packages", nargs='+', default=[], + help='ignore this package when autofetching dependencies (glob supported)') + parser.add_argument('-b', '--builddir', action='store', type=str, metavar='BUILDDIR', help='build directory for shared objects') @@ -966,7 +1037,7 @@ def main(): if not os.path.isdir(updates): os.makedirs(updates) - copyUpdatedFiles(args.tag, updates, cwd) + copyUpdatedFiles(args.tag, updates, cwd, builddir) if args.compile: if isysChanged(args.tag): @@ -975,31 +1046,21 @@ def main(): if widgetsChanged(args.tag): copyUpdatedWidgets(updates, cwd, builddir) + if auditdChanged(args.tag): + copyUpdatedAuditd(updates, cwd, builddir) + if args.po: copyTranslations(updates, cwd, builddir) if args.add_rpms: - # as using -a or --add mutltiple times - # adds a new list to the list instead of extending the - # original list, we need to "normalize" to a - # one-level list of strings - normalized_rpm_paths = [] - for item in args.add_rpms: - if isinstance(item, list): - normalized_rpm_paths.extend(item) - else: - normalized_rpm_paths.append(item) - # also remove any duplicated paths - # by converting the list into a set - args.add_rpms = list(set(normalized_rpm_paths)) - + args.add_rpms = list(set(args.add_rpms)) print('%d RPMs added manually:' % len(args.add_rpms)) for item in args.add_rpms: print(os.path.basename(item)) if args.fetch: arch = args.fetch - rpm_paths = check_for_new_packages(args.tag, arch, args.add_rpms, spec) + rpm_paths = check_for_new_packages(args.tag, arch, args, spec) args.add_rpms.extend(rpm_paths) if args.add_rpms: diff --git a/anaconda/tests/Makefile.am b/anaconda/tests/Makefile.am index 2803481..2f29d9d 100644 --- a/anaconda/tests/Makefile.am +++ b/anaconda/tests/Makefile.am @@ -48,8 +48,11 @@ dist_check_SCRIPTS = $(srcdir)/glade/*/*.py \ testenv.sh \ gettext/gettext_warnings.sh \ gettext/gettext_potfiles.py \ + gettext/style_guide.py \ storage/run_storage_tests.py \ - ostree/run_ostree_tests.sh \ + install/run_install_test.sh \ + $(srcdir)/kickstart_tests/*.ks \ + $(srcdir)/kickstart_tests/*.sh \ $(srcdir)/gui/*.ks \ $(srcdir)/gui/*.sh \ $(srcdir)/gui/inside/*.py \ @@ -62,10 +65,10 @@ TESTS = nosetests.sh \ cppcheck/runcppcheck.sh \ gettext/gettext_warnings.sh \ gettext/gettext_potfiles.py \ + gettext/style_guide.py \ storage/run_storage_tests.py \ - gui/run_gui_tests.sh \ glade/run_glade_tests.sh \ - ostree/run_ostree_tests.sh + kickstart_tests/run_kickstart_tests.sh clean-local: -rm -rf pylint/.pylint.d diff --git a/anaconda/tests/gettext/gettext_potfiles.py b/anaconda/tests/gettext/gettext_potfiles.py index 5d80308..05c5ec9 100644 --- a/anaconda/tests/gettext/gettext_potfiles.py +++ b/anaconda/tests/gettext/gettext_potfiles.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 import os, sys, subprocess diff --git a/anaconda/tests/gettext/style_guide.py b/anaconda/tests/gettext/style_guide.py new file mode 100644 index 0000000..f41369b --- /dev/null +++ b/anaconda/tests/gettext/style_guide.py @@ -0,0 +1,104 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Shea <dshea@redhat.com> + +import os, re, sys + +# {'bad re': 'suggestion'} +# (?i) makes the re case-insensitive +bad_strings = {'(?i)bootloader': 'boot loader', + '(?i)filesystem': 'file system', + '(?i)username': 'user name', + '[Vv]lan': 'VLAN', + '(?i)hostname': 'hostname', + 'ZFCP': 'zFCP', + 'zfcp': 'zFCP', + 'BTRFS': 'Btrfs', + 'btrfs': 'Btrfs', + '[Cc]an not': 'cannot', + '(?i)mountpoint': 'mount point', + 'Ok': 'OK'} + +# Sometimes we need to use a bad string, or it's just too much of a pain to +# write a more specific regex. List occurrences here. +# {'filename': {'matched string', occurrences}} +expected_badness = {'pyanaconda/bootloader.py': {'mountpoint': 1}, # format string specifier + 'pyanaconda/kickstart.py': {'btrfs': 1}, # quoted filesystem type + 'pyanaconda/network.py': {'vlan': 1}} # format string specifier + +# Use polib to parse anaconda.pot +try: + import polib +except ImportError: + print("You need to install the python-polib package to read anaconda.pot") + # This return code tells the automake test driver that the test setup failed + sys.exit(99) + +if "top_srcdir" not in os.environ: + sys.stderr.write("$top_srcdir must be defined in the test environment\n") + sys.exit(99) + +if "top_builddir" not in os.environ: + sys.stderr.write("$top_srcdir must be defined in the test environment\n") + sys.exit(99) + +# Update the .pot file with the latest strings +if os.system('make -C %s anaconda.pot-update' % (os.environ['top_builddir'] + "/po")) != 0: + sys.stderr.write("Unable to update anaconda.pot") + sys.exit(1) + +# Parse anaconda.pot and rearrange the POFile object into a dict of {msgid: POEntry} +pofile = polib.pofile(os.environ['top_srcdir'] + "/po/anaconda.pot") +msgs = {e.msgid: e for e in pofile} + +# Look for each of the bad regexes +success = True +for badre in bad_strings.keys(): + regex = re.compile(badre) + for msg in msgs.keys(): + # Remove underscores to avoid trouble with underline-based accelerators + match = re.search(regex, msg.replace('_', '')) + if match: + # If this is something expected, decrement the occurrence count in expected_badness + badstr = match.group(0) + remainder = [] + for occur in msgs[msg].occurrences: + if occur[0] in expected_badness and badstr in expected_badness[occur[0]]: + expected_badness[occur[0]][badstr] -= 1 + if expected_badness[occur[0]][badstr] == 0: + del expected_badness[occur[0]][badstr] + if not expected_badness[occur[0]]: + del expected_badness[occur[0]] + else: + remainder.append(occur) + + if remainder: + print("Bad string %(bad)s found at %(occurrences)s. Try %(suggestion)s instead." % + {"bad": badstr, + "occurrences": " ".join(("%s:%s" % (o[0], o[1]) for o in remainder)), + "suggestion": bad_strings[badre]}) + success = False + +if expected_badness: + for filename in expected_badness.keys(): + for badstr in expected_badness[filename].keys(): + print("Did not find %d occurrences of %s in %s" % + (expected_badness[filename][badstr], badstr, filename)) + success = False + +sys.exit(0 if success else 1) diff --git a/anaconda/tests/glade/accelerators/check_accelerators.py b/anaconda/tests/glade/accelerators/check_accelerators.py index 75f478a..ec44d36 100644 --- a/anaconda/tests/glade/accelerators/check_accelerators.py +++ b/anaconda/tests/glade/accelerators/check_accelerators.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2013 Red Hat, Inc. # diff --git a/anaconda/tests/glade/format_string/check_format_string.py b/anaconda/tests/glade/format_string/check_format_string.py index 81191e1..ea213eb 100644 --- a/anaconda/tests/glade/format_string/check_format_string.py +++ b/anaconda/tests/glade/format_string/check_format_string.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # diff --git a/anaconda/tests/glade/icons/check_icons.py b/anaconda/tests/glade/icons/check_icons.py index e338ea2..4321a5b 100644 --- a/anaconda/tests/glade/icons/check_icons.py +++ b/anaconda/tests/glade/icons/check_icons.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # diff --git a/anaconda/tests/glade/markup/check_markup.py b/anaconda/tests/glade/markup/check_markup.py index f90e15e..b7619f8 100644 --- a/anaconda/tests/glade/markup/check_markup.py +++ b/anaconda/tests/glade/markup/check_markup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # diff --git a/anaconda/tests/glade/pw_visibility/check_invisible_char.py b/anaconda/tests/glade/pw_visibility/check_invisible_char.py new file mode 100644 index 0000000..2eaaa96 --- /dev/null +++ b/anaconda/tests/glade/pw_visibility/check_invisible_char.py @@ -0,0 +1,79 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Shea <dshea@redhat.com> +# + +""" +Check that the invisible_char in glade files is actually a char. + +The invisible char is often non-ASCII and sometimes that gets clobbered. +""" + +import argparse +import sys + +try: + from lxml import etree +except ImportError: + print("You need to install the python-lxml package to use check_pw_visibility.py") + sys.exit(1) + +def check_glade_file(glade_file_path): + succ = True + + with open(glade_file_path, "r") as glade_file: + tree = etree.parse(glade_file) + # Only look for entries with an invisible_char property + for entry in tree.xpath("//object[@class='GtkEntry' and ./property[@name='invisible_char']]"): + # Check the contents of the invisible_char property + invis = entry.xpath("./property[@name='invisible_char']")[0] + if len(invis.text) != 1: + print("invisible_char at %s:%s not a character" % (glade_file_path, invis.sourceline)) + succ = False + + # If the char is '?' that's probably also bad + if invis.text == '?': + print("invisible_char at %s:%s is not what you want" % (glade_file_path, invis.sourceline)) + + # Check that invisible_char even does anything: visibility should be False + if not entry.xpath("./property[@name='visibility' and ./text() = 'False']"): + print("Pointless invisible_char found at %s:%s" % (glade_file_path, invis.sourceline)) + succ = False + + return succ + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Check that invisible character properties are set correctly") + + # Ignore translation arguments + parser.add_argument("-t", "--translate", action='store_true', + help=argparse.SUPPRESS) + parser.add_argument("-p", "--podir", action='store', type=str, + metavar='PODIR', help=argparse.SUPPRESS, default='./po') + + parser.add_argument("glade_files", nargs="+", metavar="GLADE-FILE", + help='The glade file to check') + args = parser.parse_args(args=sys.argv[1:]) + + success = True + for file_path in args.glade_files: + if not check_glade_file(file_path): + success = False + + sys.exit(0 if success else 1) diff --git a/anaconda/tests/glade/pw_visibility/check_pw_visibility.py b/anaconda/tests/glade/pw_visibility/check_pw_visibility.py index 0e9a99f..6cbe8e2 100644 --- a/anaconda/tests/glade/pw_visibility/check_pw_visibility.py +++ b/anaconda/tests/glade/pw_visibility/check_pw_visibility.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2013 Red Hat, Inc. # diff --git a/anaconda/tests/glade/run_glade_tests.sh b/anaconda/tests/glade/run_glade_tests.sh index e197180..8ebcbfe 100644 --- a/anaconda/tests/glade/run_glade_tests.sh +++ b/anaconda/tests/glade/run_glade_tests.sh @@ -1,5 +1,10 @@ #!/bin/sh +if ! type parallel 2>&1 > /dev/null; then + echo "parallel must be installed" + exit 99 +fi + : "${top_srcdir:=$(dirname "$0")/../..}" . "${top_srcdir}/tests/testenv.sh" srcdir="${top_srcdir}/tests/glade" @@ -22,7 +27,7 @@ fi status=0 for check in ${srcdir}/*/check_*.py ; do - findtestfiles -name '*.glade' | xargs "${check}" "$@" + findtestfiles -name '*.glade' | parallel --no-notice --gnu -j0 "${check}" "$@" {} if [ "$?" -ne 0 ]; then status=1 fi diff --git a/anaconda/tests/glade/validity/check_glade_validity.py b/anaconda/tests/glade/validity/check_glade_validity.py index 9a2c179..87445ec 100644 --- a/anaconda/tests/glade/validity/check_glade_validity.py +++ b/anaconda/tests/glade/validity/check_glade_validity.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # diff --git a/anaconda/tests/glade/viewport/check_viewport.py b/anaconda/tests/glade/viewport/check_viewport.py index fd75a1d..337d00f 100644 --- a/anaconda/tests/glade/viewport/check_viewport.py +++ b/anaconda/tests/glade/viewport/check_viewport.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # diff --git a/anaconda/tests/gui/anaconda-autogui-testing.ks b/anaconda/tests/gui/anaconda-autogui-testing.ks index e54e711..21c9348 100644 --- a/anaconda/tests/gui/anaconda-autogui-testing.ks +++ b/anaconda/tests/gui/anaconda-autogui-testing.ks @@ -1,4 +1,4 @@ -%include /usr/share/spin-kickstarts/fedora-livecd-desktop.ks +%include /usr/share/spin-kickstarts/fedora-live-workstation.ks url --mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=rawhide&arch=x86_64 @@ -10,36 +10,42 @@ rootpw qweqwe bootloader --location=mbr zerombr clearpart --all -part / --fstype="ext4" --size=4400 +part / --fstype="ext4" --size=6000 %post cat >> /etc/rc.d/init.d/livesys << EOF -# Mount the attached disk containing test suite information. -mkdir /mnt/anactest -mount -L ANACTEST /mnt/anactest -chown -R liveuser.liveuser /mnt/anactest +# Mount the attached disk containing test suite information, if it exists. +blkid -l -t LABEL=ANACTEST +if [ \$? = 0 ]; then + mkdir /mnt/anactest + mount -L ANACTEST /mnt/anactest + chown -R liveuser.liveuser /mnt/anactest -if [ -f /usr/share/applications/anaconda.desktop ]; then - # Make anaconda start automatically. - if [ -f ~liveuser/.config/autostart/fedora-welcome.desktop ]; then - rm ~liveuser/.config/autostart/fedora-welcome.desktop - fi + if [ -f /usr/share/applications/anaconda.desktop ]; then + # Make anaconda start automatically. + if [ -f ~liveuser/.config/autostart/fedora-welcome.desktop ]; then + rm ~liveuser/.config/autostart/fedora-welcome.desktop + fi - cp /usr/share/applications/anaconda.desktop ~liveuser/.config/autostart/ - sed -i -e '/Exec=/ s|/usr/bin/liveinst|python /mnt/anactest/suite.py|' ~liveuser/.config/autostart/anaconda.desktop + cp /usr/share/applications/anaconda.desktop ~liveuser/.config/autostart/ + sed -i -e '/Exec=/ s|/usr/bin/liveinst|python /mnt/anactest/suite.py|' ~liveuser/.config/autostart/anaconda.desktop - # Enable accessibility needed for testing. - cat >> /usr/share/glib-2.0/schemas/org.gnome.desktop.interface.gschema.override << FOE + # Enable accessibility needed for testing. + cat >> /usr/share/glib-2.0/schemas/org.gnome.desktop.interface.gschema.override << FOE [org.gnome.desktop.interface] toolkit-accessibility=true FOE + fi fi EOF %end %packages +@^workstation-product-environment +-@fedora-release-nonproduct dogtail +syslinux -@libreoffice -java* %end diff --git a/anaconda/tests/gui/inside/__init__.py b/anaconda/tests/gui/inside/__init__.py index cf4cf90..ca29684 100644 --- a/anaconda/tests/gui/inside/__init__.py +++ b/anaconda/tests/gui/inside/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # @@ -22,7 +22,6 @@ from dogtail.tree import SearchError, root from dogtail.utils import doDelay, screenshot import glob -import os import unittest class UITestCase(unittest.TestCase): @@ -92,7 +91,7 @@ class UITestCase(unittest.TestCase): ### METHODS FOR FINDING WIDGETS ### - def find(self, name, roleName=None): + def find(self, name, roleName=None, node=None): """Wrap findChild, returning None if no widget is found instead of raising an exception. This method also allows for checking if anaconda has hit a traceback and if so, fails the test @@ -101,11 +100,20 @@ class UITestCase(unittest.TestCase): if len(glob.glob("/tmp/anaconda-tb-*")) > 0: self.fail("anaconda encountered a traceback") + if not node: + node = self.ana + try: - return self.ana.child(name=name, roleName=roleName) + return node.child(name=name, roleName=roleName) except SearchError: return None + def view_children(self, view): + return [child for child in view.findChildren(GenericPredicate(roleName="table cell"))] + + def selected_view_children(self, view): + return [child for child in self.view_children(view) if child.selected] + ### ### METHODS FOR CHECKING A SINGLE WIDGET ### @@ -115,9 +123,10 @@ class UITestCase(unittest.TestCase): provided name is currently displayed on the screen. If not, the current test case will be failed. """ - w = self.find(name) + w = self.find(name, roleName="panel") self.assertIsNotNone(w, msg="%s not found" % name) self.assertTrue(w.showing, msg="%s is not displayed" % name) + return w def check_dialog_displayed(self, name): """Verify that a dialog given by the provided name is currently @@ -127,36 +136,48 @@ class UITestCase(unittest.TestCase): w = self.find(name, "dialog") self.assertIsNotNone(w, msg="%s not found" % name) self.assertTrue(w.showing, msg="%s is not displayed" % name) + return w - def check_keyboard_layout_indicator(self, layout): + def check_keyboard_layout_indicator(self, layout, node=None): """Verify that the keyboard layout indicator is present and that the currently enabled layout is what we expect. If not, the current test case will be failed. """ - indicator = self.find("Keyboard Layout") + indicator = self.find("Keyboard Layout", node=node) self.assertIsNotNone(indicator, msg="keyboard layout indicator not found") self.assertEqual(indicator.description, layout, msg="keyboard layout indicator not set to %s" % layout) - def check_no_warning_bar(self): - """Verify that the warning bar is not currently displayed.""" - self.assertIsNone(self.find("Warning"), msg="Warning bar should not be displayed") + def check_help_button(self, node=None): + self.click_button("Help!", node=node) - def check_warning_bar(self, msg=None): + try: + yelp = root.application("yelp") + except SearchError: + self.fail("Help view is not displayed.") + + doDelay(2) + yelp.keyCombo("<Alt>F4") + + def check_no_warning_bar(self, node=None): + """Verify that the warning bar is not currently displayed.""" + self.assertIsNone(self.find("Warning", node=node), msg="Warning bar should not be displayed") + + def check_warning_bar(self, msg=None, node=None): """Verify that the warning bar is currently displayed. If msg is given, verify that it is contained in whatever message the warning bar is showing. """ - bar = self.find("Warning") + bar = self.find("Warning", node=node) self.assertTrue(bar.showing, msg="Warning bar should be displayed") if msg: self.assertIn(msg, bar.child(roleName="label").text) - def click_button(self, name): + def click_button(self, name, node=None): """Verify that a button with the given name exists and is sensitive, and then click it. """ - b = self.find(name, "push button") + b = self.find(name, "push button", node=node) self.assertIsNotNone(b, msg="%s button not found" % name) self.assertTrue(b.sensitive, msg="%s button should be sensitive" % name) b.click() @@ -169,13 +190,13 @@ class UITestCase(unittest.TestCase): self.assertIsNotNone(selector, msg="Selector %s not found" % spokeSelectorName) selector.click() - def exit_spoke(self, hubName="INSTALLATION SUMMARY"): + def exit_spoke(self, hubName="INSTALLATION SUMMARY", node=None): """Leave a spoke by clicking the Done button in the upper left corner, then verify we have returned to the proper hub. Since most spokes are off the summary hub, that's the default. If we are not back on the hub, the current test case will be failed. """ - button = self.find("_Done", "push button") + button = self.find("_Done", "push button", node=node) self.assertIsNotNone(button, msg="Done button not found") button.click() doDelay(5) diff --git a/anaconda/tests/gui/inside/date_time.py b/anaconda/tests/gui/inside/date_time.py index b7cc72e..93dbbf3 100644 --- a/anaconda/tests/gui/inside/date_time.py +++ b/anaconda/tests/gui/inside/date_time.py @@ -18,9 +18,9 @@ from . import UITestCase class LiveCDDateTimeTestCase(UITestCase): - def check_region_city(self): + def check_region_city(self, spoke): # FIXME: This encodes default information. - entry = self.find("Region", "text") + entry = self.find("Region", "text", node=spoke) self.assertIsNotNone(entry, "Region entry does not exist") self.assertEqual(entry.text, "Americas", msg="Region should be set to default") @@ -28,32 +28,33 @@ class LiveCDDateTimeTestCase(UITestCase): self.assertIsNotNone(entry, "City entry does not exist") self.assertEqual(entry.text, "New York", msg="City should be set to default") - def check_ntp(self): + def check_ntp(self, spoke): # NTP should be enabled given that we started up with networking. # FIXME: This encodes default information. - button = self.find("Use Network Time", "toggle button") + button = self.find("Use Network Time", "toggle button", node=spoke) self.assertIsNotNone(button, msg="Use Network Time button not found") self.assertTrue(button.checked, msg="NTP should be enabled") - button = self.find("Configure NTP", "push button") + button = self.find("Configure NTP", "push button", node=spoke) self.assertIsNotNone(button, msg="Configure NTP button not found") self.assertTrue(button.sensitive, msg="Configure NTP button should be sensitive") - area = self.find("Set Date & Time") + area = self.find("Set Date & Time", node=spoke) self.assertIsNotNone(area, msg="Set Date & Time not found") self.assertFalse(area.sensitive, msg="Date & Time region should not be sensitive") def _run(self): # First, we need to click on the network spoke selector. - self.enter_spoke("DATE & TIME") + self.enter_spoke("TIME & DATE") # Now verify we are on the right screen. - self.check_window_displayed("DATE & TIME") + w = self.check_window_displayed("TIME & DATE") # And now we can check everything else on the screen. - self.check_region_city() - self.check_ntp() + self.check_help_button(w) + self.check_region_city(w) + self.check_ntp(w) # And then we click the Done button to go back to the hub, verifying # that's where we ended up. - self.exit_spoke() + self.exit_spoke(node=w) diff --git a/anaconda/tests/gui/inside/keyboard.py b/anaconda/tests/gui/inside/keyboard.py index b57773b..dea99ca 100644 --- a/anaconda/tests/gui/inside/keyboard.py +++ b/anaconda/tests/gui/inside/keyboard.py @@ -15,113 +15,175 @@ # # Author: Chris Lumens <clumens@redhat.com> -from dogtail.predicate import GenericPredicate - from . import UITestCase class BasicKeyboardTestCase(UITestCase): - def _get_enabled_layouts(self, view): - return [child.text for child in view.findChildren(GenericPredicate(roleName="table cell"))] + def check_options_dialog(self, spoke): + # No layout switching should be configured yet. + self.assertTrue(self.find("Layout switching not configured.", node=spoke).showing) - def check_options_dialog(self): # Click the options button. - self.click_button("Options") - self.check_dialog_displayed("Layout Options") - self.click_button("Cancel") + self.click_button("Options", node=spoke) + dlg = self.check_dialog_displayed("Layout Options") - def check_num_layouts(self): - # There ought to be only one layout enabled right now. + # Enable a layout switching combo by just clicking on the first checkbox. + view = self.find("Layout Options", node=dlg) + self.assertIsNotNone(view, "Layout Switching Options view not found") + + children = self.view_children(view) + self.assertTrue(len(children) > 0, msg="No layouts found in view") + children[0].click() + + # Leave the dialog and make sure the layout switching hint on the spoke has changed. + self.click_button("OK", node=dlg) + self.assertTrue(self.find("Alt+Caps Lock to switch layouts.", node=spoke).showing) + + def check_num_layouts(self, spoke, n): # FIXME: This encodes default information. - view = self.find("Selected Layouts") + view = self.find("Selected Layouts", node=spoke) self.assertIsNotNone(view, "Selected Layouts view not found") - self.assertEqual(len(self._get_enabled_layouts(view)), 1, msg="An unexpected number of keyboard layouts are enabled") + self.assertEqual(len(self.view_children(view)), n, msg="An unexpected number of keyboard layouts are enabled") - def check_layout_buttons_initial(self): - button = self.find("Add layout", "push button") + def check_layout_buttons_initial(self, spoke): + button = self.find("Add layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Add layout button not found") self.assertTrue(button.sensitive, msg="Add layout button should be sensitive") # When no layouts are selected in the view, none of these buttons mean anything. - button = self.find("Remove layout", "push button") + button = self.find("Remove layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Remove layout button not found") self.assertFalse(button.sensitive, msg="Remove layout button should not be sensitive") - button = self.find("Move selected layout up", "push button") + button = self.find("Move selected layout up", "push button", node=spoke) self.assertIsNotNone(button, msg="Move layout up button not found") self.assertFalse(button.sensitive, msg="Move layout up button should not be sensitive") - button = self.find("Move selected layout down", "push button") + button = self.find("Move selected layout down", "push button", node=spoke) self.assertIsNotNone(button, msg="Move layout down button not found") self.assertFalse(button.sensitive, msg="Move layout down button should not be sensitive") - button = self.find("Preview layout", "push button") + button = self.find("Preview layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Preview layout button not found") self.assertFalse(button.sensitive, msg="Preview layout button should not be sensitive") - def check_add_layout_dialog(self): - self.click_button("Add layout") - self.check_dialog_displayed("Add Layout") - self.click_button("Cancel") + def check_add_layout_dialog(self, spoke): + # Click the Add button to bring up the dialog. + self.click_button("Add layout", node=spoke) + dlg = self.check_dialog_displayed("Add Layout") - def check_layout_buttons_after_click(self): - # Click on the first (and only) layout shown in the view. This - # ensures buttons change sensitivity. - # FIXME: This encodes default information. - view = self.find("Selected Layouts") + # Now on the dialog, the Add button should be insensitive initially. + button = self.find("Add", node=dlg) + self.assertIsNotNone(button, msg="Add button not found") + self.assertFalse(button.sensitive, msg="Add button should not be sensitive") + + # Select the first layout in the dialog - 'af'. + view = self.find("Available Layouts", node=dlg) + self.assertIsNotNone(view, "Available Layouts view not found") + + children = self.view_children(view) + self.assertTrue(len(children) > 0, msg="No layouts found in view") + children[0].click() + + self.assertTrue(button.sensitive, msg="Add button should be sensitive") + + # Leave the dialog and make sure the new layout is visible on the spoke. + # There are now two layouts available - 'us' (default), and 'af'. + self.click_button("Add", node=dlg) + self.check_num_layouts(spoke, 2) + + def check_layout_indicator(self, spoke): + # First, the layout indicator should still show 'us' as the active layout. + self.check_keyboard_layout_indicator("us", node=spoke) + + # Now if we click on it, the layout indicator should change to show 'af'. + self.find("Keyboard Layout", node=spoke).click() + self.check_keyboard_layout_indicator("af", node=spoke) + + # FIXME: The order of keyboard layouts in the view should also change + # to match what happened in the layout indicator. This is an anaconda + # bug. + + # Click on it again, and it should go back to 'us'. + self.find("Keyboard Layout", node=spoke).click() + self.check_keyboard_layout_indicator("us", node=spoke) + + def check_layout_buttons_after_click(self, spoke): + # Click on the first layout shown in the view. This ensures buttons + # change sensitivity. + view = self.find("Selected Layouts", node=spoke) self.assertIsNotNone(view, msg="Selected Layouts view not found") view.children[1].click() - button = self.find("Add layout", "push button") + button = self.find("Add layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Add layout button not found") self.assertTrue(button.sensitive, msg="Add layout button should be sensitive") - button = self.find("Remove layout", "push button") + button = self.find("Remove layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Remove layout button not found") self.assertTrue(button.sensitive, msg="Remove layout button should be sensitive") - # These two should still not be sensitive - we've only got one layout. - # We ensured that with check_num_layouts. - # FIXME: This encodes default information. - button = self.find("Move selected layout up", "push button") + button = self.find("Move selected layout down", "push button", node=spoke) + self.assertIsNotNone(button, msg="Move layout down button not found") + self.assertTrue(button.sensitive, msg="Move layout down button should be sensitive") + + # This should still not be sensitive - we selected the first layout, so + # it's impossible to move up. + button = self.find("Move selected layout up", "push button", node=spoke) self.assertIsNotNone(button, msg="Move layout up button not found") self.assertFalse(button.sensitive, msg="Move layout up button should not be sensitive") - button = self.find("Move selected layout down", "push button") - self.assertIsNotNone(button, msg="Move layout down button not found") - self.assertFalse(button.sensitive, msg="Move layout down button should not be sensitive") - - button = self.find("Preview layout", "push button") + button = self.find("Preview layout", "push button", node=spoke) self.assertIsNotNone(button, msg="Preview layout button not found") self.assertTrue(button.sensitive, msg="Preview layout button should be sensitive") - def check_preview_dialog(self): - self.click_button("Preview layout") + # Now that we've just checked the sensitivity of everything, do something + # with the buttons. First, move the default 'us' layout down and then back + # up to verify those two buttons work. + self.click_button("Move selected layout down", node=spoke) + self.click_button("Move selected layout up", node=spoke) + + # Click on the second layout and remove it. This should leave only the initial + # layout in the view. + view = self.find("Selected Layouts", node=spoke) + self.assertIsNotNone(view, "Selected Layouts view not found") + + children = self.view_children(view) + children[1].click() + self.click_button("Remove layout", node=spoke) + self.check_num_layouts(spoke, 1) + + def check_preview_dialog(self, spoke): + self.click_button("Preview layout", node=spoke) # Verify the preview dialog is displayed. The dialog out to be titled # with the name of the current layout. We happen to know that - it's # the default. # FIXME: This encodes default information. - self.check_dialog_displayed("English (US)") + dlg = self.check_dialog_displayed("English (US)") - self.click_button("Close") + self.click_button("Close", node=dlg) def _run(self): # First, we need to click on the network spoke selector. self.enter_spoke("KEYBOARD") # Now verify we are on the right screen. - self.check_window_displayed("KEYBOARD") + w = self.check_window_displayed("KEYBOARD LAYOUT") # And now we can check everything else on the screen. - self.check_options_dialog() - self.check_num_layouts() - self.check_layout_buttons_initial() - self.check_add_layout_dialog() + self.check_help_button(w) + self.check_options_dialog(w) + self.check_num_layouts(w, 1) + self.check_layout_buttons_initial(w) + + # Once a layout has been added, we can test the keyboard layout indicator. + self.check_add_layout_dialog(w) + self.check_layout_indicator(w) # Once a layout has been selected in the view, we can test these other buttons. - self.check_layout_buttons_after_click() - self.check_preview_dialog() + self.check_layout_buttons_after_click(w) + self.check_preview_dialog(w) # And then we click the Done button to go back to the hub, verifying # that's where we ended up. - self.exit_spoke() + self.exit_spoke(node=w) diff --git a/anaconda/tests/gui/inside/network.py b/anaconda/tests/gui/inside/network.py index 6398230..fa7a660 100644 --- a/anaconda/tests/gui/inside/network.py +++ b/anaconda/tests/gui/inside/network.py @@ -18,32 +18,33 @@ from . import UITestCase class LiveCDNetworkTestCase(UITestCase): - def check_hostname_entry(self): + def check_hostname_entry(self, spoke): # Only the live install hint and hostname box should be visible. - self.assertTrue(self.find("Please use the live desktop environment's tools for customizing your network configuration. You can set the hostname here.").showing) + self.assertTrue(self.find("Please use the live desktop environment's tools for customizing your network configuration. You can set the host name here.", node=spoke).showing) - box = self.find("Network Config Box") + box = self.find("Network Config Box", node=spoke) self.assertIsNotNone(box, "Network Config box not found") self.assertFalse(box.showing, msg="Network Config box should not be displayed") - box = self.find("More Network Config Box") + box = self.find("More Network Config Box", node=spoke) self.assertIsNotNone(box, "More Network Config box not found") self.assertFalse(box.showing, msg="More Network Config box should not be displayed") - entry = self.find("Hostname", "text") + entry = self.find("Host Name", "text", node=spoke) self.assertIsNotNone(entry , "Hostname entry not found") self.assertTrue(entry.showing, msg="Hostname entry should be displayed") def _run(self): # First, we need to click on the network spoke selector. - self.enter_spoke("NETWORK & HOSTNAME") + self.enter_spoke("NETWORK & HOST NAME") # Now verify we are on the right screen. - self.check_window_displayed("NETWORK & HOSTNAME") + w = self.check_window_displayed("NETWORK & HOST NAME") # And now we can check everything else on the screen. - self.check_hostname_entry() + self.check_help_button(w) + self.check_hostname_entry(w) # And then we click the Done button to go back to the hub, verifying # that's where we ended up. - self.exit_spoke() + self.exit_spoke(node=w) diff --git a/anaconda/tests/gui/inside/progress.py b/anaconda/tests/gui/inside/progress.py index 5d9976b..3767e27 100644 --- a/anaconda/tests/gui/inside/progress.py +++ b/anaconda/tests/gui/inside/progress.py @@ -22,15 +22,15 @@ from dogtail.utils import doDelay from . import UITestCase class LiveCDProgressTestCase(UITestCase): - def check_begin_installation_button(self): - button = self.find("Begin Installation", "push button") + def check_begin_installation_button(self, hub): + button = self.find("Begin Installation", "push button", node=hub) self.assertIsNotNone(button, msg="Begin Installation button does not exist") self.assertTrue(button.sensitive, msg="Begin Installation button should be sensitive") - def check_shown_spoke_selectors(self): + def check_shown_spoke_selectors(self, hub): # FIXME: This forces English. validSelectors = ["ROOT PASSWORD", "USER CREATION"] - selectors = self.ana.findChildren(GenericPredicate(roleName="spoke selector")) + selectors = hub.findChildren(GenericPredicate(roleName="spoke selector")) self.assertEqual(len(selectors), len(validSelectors), msg="Incorrect number of spoke selectors shown") @@ -52,18 +52,19 @@ class LiveCDProgressTestCase(UITestCase): def _run(self): # Before doing anything, verify we are still on the summary hub. - self.check_window_displayed("INSTALLATION SUMMARY") + w = self.check_window_displayed("INSTALLATION SUMMARY") # All spokes should have been visited and satisfied now. - self.check_no_warning_bar() - self.check_begin_installation_button() + self.check_no_warning_bar(w) + self.check_begin_installation_button(w) # Click the begin installation button, wait a moment, and now we should # be on the progress hub. - self.click_button("Begin Installation") - self.check_window_displayed("CONFIGURATION") + self.click_button("Begin Installation", node=w) - self.check_shown_spoke_selectors() - self.check_warning_bar() + w = self.check_window_displayed("CONFIGURATION") + self.check_shown_spoke_selectors(w) + self.check_warning_bar(node=w) + self.check_help_button(w) # Now we need to wait for installation to finish. We're doing that two ways: # (1) Set a 30 minute timeout. Should we hit that, anaconda's clearly not @@ -75,7 +76,7 @@ class LiveCDProgressTestCase(UITestCase): signal.alarm(30*60) while True: - label = self.find("Complete!") + label = self.find("Complete!", node=w) if label: signal.alarm(0) break @@ -85,25 +86,25 @@ class LiveCDProgressTestCase(UITestCase): # If we got here, installation completed successfully. Since we've not # done a password or created a user yet, we still have to do that. The # finish configuration button should still be insensitive. - button = self.find("Finish configuration", "push button") + button = self.find("Finish configuration", "push button", node=w) self.assertIsNotNone(button, msg="Finish configuration button not found") self.assertFalse(button.sensitive, msg="Finish Configuration button should not be sensitive") class LiveCDFinishTestCase(UITestCase): - def check_finish_config_button(self): + def check_finish_config_button(self, hub): # Click the Finish Configuration button. - self.click_button("Finish configuration") + self.click_button("Finish configuration", node=hub) def _timer_expired(self, signum, frame): self.fail("anaconda did not finish in 5 minutes") def _run(self): # Before doing anything, verify we are still on the progress hub. - self.check_window_displayed("CONFIGURATION") + w = self.check_window_displayed("CONFIGURATION") - self.check_finish_config_button() + self.check_finish_config_button(w) # We've completed configuration, so the warning bar should be gone. - self.check_no_warning_bar() + self.check_no_warning_bar(node=w) # And now we wait for configuration to finish. Then we check the # reboot button, but don't click it. The end of the test case shuts @@ -112,11 +113,11 @@ class LiveCDFinishTestCase(UITestCase): signal.alarm(5*60) while True: - button = self.find("Quit", "push button") - if button: + button = self.find("Quit", "push button", node=w) + if button and button.showing: signal.alarm(0) break doDelay(20) - self.click_button("Quit") + self.click_button("Quit", node=w) diff --git a/anaconda/tests/gui/inside/rootpassword.py b/anaconda/tests/gui/inside/rootpassword.py index c703e8a..9077133 100644 --- a/anaconda/tests/gui/inside/rootpassword.py +++ b/anaconda/tests/gui/inside/rootpassword.py @@ -20,11 +20,11 @@ from dogtail.utils import doDelay from . import UITestCase class BasicRootPasswordTestCase(UITestCase): - def check_enter_password(self): + def check_enter_password(self, spoke): # The warning bar starts off telling us there's no password set. - self.check_warning_bar("The password is empty") + self.check_warning_bar("The password is empty", node=spoke) - entry = self.find("Password", "text") + entry = self.find("Password", "password text", node=spoke) self.assertIsNotNone(entry, msg="Password entry should be displayed") entry.grabFocus() entry.text = "asdfasdf" @@ -32,10 +32,10 @@ class BasicRootPasswordTestCase(UITestCase): # That wasn't a very good password and we haven't confirmed it, so the # bar is still displayed at the bottom. doDelay(1) - self.check_warning_bar("it does not contain enough DIFFERENT characters") + self.check_warning_bar("The password you have provided is weak.", node=spoke) # Let's confirm that terrible password. - entry = self.find("Confirm Password", "text") + entry = self.find("Confirm Password", "password text", node=spoke) self.assertIsNotNone(entry, msg="Confirm password should be displayed") entry.grabFocus() entry.text = "asdfasdf" @@ -43,24 +43,25 @@ class BasicRootPasswordTestCase(UITestCase): # But of course it's still a terrible password, so the bar is still # displayed at the bottom. doDelay(1) - self.check_warning_bar("it does not contain enough DIFFERENT characters") + self.check_warning_bar("The password you have provided is weak.", node=spoke) - def check_click_done(self): + def check_click_done(self, spoke): # Press the Done button once, which won't take us anywhere but will change the # warning label at the bottom. - self.click_button("_Done") - self.check_warning_bar("Press Done again") + self.click_button("_Done", node=spoke) + self.check_warning_bar("Press Done again", node=spoke) # Pressing Done again should take us back to the progress hub. - self.exit_spoke(hubName="CONFIGURATION") + self.exit_spoke(hubName="CONFIGURATION", node=spoke) def _run(self): # First, we need to click on the spoke selector. self.enter_spoke("ROOT PASSWORD") # Now, verify we are on the right screen. - self.check_window_displayed("ROOT PASSWORD") + w = self.check_window_displayed("ROOT PASSWORD") # And now we can check everything else on the screen. - self.check_enter_password() - self.check_click_done() + self.check_help_button(w) + self.check_enter_password(w) + self.check_click_done(w) diff --git a/anaconda/tests/gui/inside/storage.py b/anaconda/tests/gui/inside/storage.py index 28768dd..660484b 100644 --- a/anaconda/tests/gui/inside/storage.py +++ b/anaconda/tests/gui/inside/storage.py @@ -16,11 +16,12 @@ # Author: Chris Lumens <clumens@redhat.com> from dogtail.predicate import GenericPredicate +from dogtail.utils import doDelay from . import UITestCase class BasicStorageTestCase(UITestCase): - def check_select_disks(self): + def check_select_disks(self, spoke): # FIXME: This is a really roundabout way of determining whether a disk is # selected or not. For some reason when a DiskOverview is selected, its icon # loses the name "Hard Disk". For now, we can use this to check. @@ -33,13 +34,13 @@ class BasicStorageTestCase(UITestCase): # size. def _real_disk(do): for child in do.findChildren(GenericPredicate()): - if child.name.startswith("10."): + if child.name == "10 MiB": return False return True # There should be some disks displayed on the screen. - overviews = self.ana.findChildren(GenericPredicate(roleName="disk overview")) + overviews = spoke.findChildren(GenericPredicate(roleName="disk overview")) self.assertGreater(len(overviews), 0, msg="No disks are displayed") if len(overviews) == 1: @@ -55,47 +56,149 @@ class BasicStorageTestCase(UITestCase): overview.click() self.assertTrue(_selected(overview)) - def check_shopping_cart(self): + def check_shopping_cart(self, spoke): pass - def check_storage_options(self): - button = self.find("Automatically configure partitioning.", "radio button") + def check_storage_options(self, spoke): + button = self.find("Automatically configure partitioning.", "radio button", node=spoke) self.assertIsNotNone(button, msg="Autopart button not found") self.assertTrue(button.checked, msg="Autopart should be selected") - button = self.find("I would like to make additional space available.", "check box") + button = self.find("I would like to make additional space available.", "check box", node=spoke) self.assertIsNotNone(button, msg="Reclaim button not found") self.assertFalse(button.checked, msg="Reclaim button should not be selected") - button = self.find("Encrypt my data.", "check box") + button = self.find("Encrypt my data.", "check box", node=spoke) self.assertIsNotNone(button, msg="Encrypt button not found") self.assertFalse(button.checked, msg="Encrypt button should not be selected") - def _run(self): - # First, we need to click on the network spoke selector. + def _common_run(self): + # First, we need to click on the storage spoke selector. self.enter_spoke("INSTALLATION DESTINATION") # Now verify we are on the right screen. - self.check_window_displayed("INSTALLATION DESTINATION") + w = self.check_window_displayed("INSTALLATION DESTINATION") + self.check_help_button(w) # Given that we attach a second disk to the system (for storing the test # suite and results), anaconda will not select disks by default. Thus, # the storage options panel should currently be insensitive. - area = self.find("Storage Options") + area = self.find("Storage Options", node=w) self.assertIsNotNone(area, "Storage Options not found") self.assertFalse(area.sensitive, msg="Storage options should be insensitive") # Select disk overviews. In the basic case, this means uninitialized # disks that we're going to do autopart on. - self.check_select_disks() + self.check_select_disks(w) # And now with disks selected, the storage options should be sensitive. self.assertTrue(area.sensitive, msg="Storage options should be sensitive") - self.check_shopping_cart() - self.check_storage_options() + self.check_shopping_cart(w) + self.check_storage_options(w) + return w + + def _run(self): + w = self._common_run() # And then we click the Done button which should take the user right back to # the hub. There's no need to display any other dialogs given that this is # an install against empty disks and no other options were checked. - self.exit_spoke() + self.exit_spoke(node=w) + +class BasicReclaimTestCase(BasicStorageTestCase): + def check_reclaim_buttons_before(self, dlg): + # Test initial sensitivity of widgets upon entering the reclaim dialog. A + # partition should be selected in the view, the only way out should be via + # the Cancel button, and the only operation available on the selected partition + # should be deleting. + button = self.find("Preserve", "push button", node=dlg) + self.assertIsNotNone(button, msg="Preserve button not found") + self.assertFalse(button.sensitive, msg="Preserve button should be insensitive") + + button = self.find("Delete", "push button", node=dlg) + self.assertIsNotNone(button, msg="Delete button not found") + self.assertTrue(button.sensitive, msg="Delete button should be sensitive") + + button = self.find("Shrink", "push button", node=dlg) + self.assertIsNotNone(button, msg="Shrink button not found") + self.assertFalse(button.sensitive, msg="Shrink button should be insensitive") + + button = self.find("Delete all", "push button", node=dlg) + self.assertIsNotNone(button, msg="Delete all button not found") + self.assertTrue(button.sensitive, msg="Delete all button should be sensitive") + + button = self.find("Cancel", "push button", node=dlg) + self.assertIsNotNone(button, msg="Cancel button not found") + self.assertTrue(button.sensitive, msg="Cancel button should be sensitive") + + button = self.find("Reclaim space", "push button", node=dlg) + self.assertIsNotNone(button, msg="Reclaim button not found") + self.assertFalse(button.sensitive, msg="Reclaim button should be insensitive") + + def check_reclaim_buttons_after(self, dlg): + # Test sensitivity of widgets now that enough space has been freed up on disks + # to continue. The Preserve buttons should be the only available operations, + # Delete all should have been renamed to Preserve All, and there should now be + # two ways out of the dialog. + button = self.find("Preserve", "push button", node=dlg) + self.assertIsNotNone(button, msg="Preserve button not found") + self.assertTrue(button.sensitive, msg="Preserve button should be sensitive") + + button = self.find("Delete", "push button", node=dlg) + self.assertIsNotNone(button, msg="Delete button not found") + self.assertFalse(button.sensitive, msg="Delete button should be insensitive") + + button = self.find("Shrink", "push button", node=dlg) + self.assertIsNotNone(button, msg="Shrink button not found") + self.assertFalse(button.sensitive, msg="Shrink button should be insensitive") + + button = self.find("Preserve all", "push button", node=dlg) + self.assertIsNotNone(button, msg="Preserve all button not found") + self.assertTrue(button.sensitive, msg="Preserve all button should be sensitive") + + button = self.find("Cancel", "push button", node=dlg) + self.assertIsNotNone(button, msg="Cancel button not found") + self.assertTrue(button.sensitive, msg="Cancel button should be sensitive") + + button = self.find("Reclaim space", "push button", node=dlg) + self.assertIsNotNone(button, msg="Reclaim button not found") + self.assertTrue(button.sensitive, msg="Reclaim button should be sensitive") + + def check_reclaim(self, optionsDlg): + self.click_button("Reclaim space", node=optionsDlg) + + # Verify we are on the reclaim dialog. + reclaimDlg = self.check_dialog_displayed("Reclaim") + self.check_reclaim_buttons_before(reclaimDlg) + + # Click the Delete all button to free up enough space. + self.click_button("Delete all", node=reclaimDlg) + self.check_reclaim_buttons_after(reclaimDlg) + + # Click on Reclaim space, which should take us all the way back to the hub. + self.click_button("Reclaim space", node=reclaimDlg) + doDelay(5) + self.check_window_displayed("INSTALLATION SUMMARY") + + def _run(self): + w = self._common_run() + + # Clicking the Done button should bring up the installation options dialog + # indicating there's not currently enough space to install, but more space + # can be made by going to the reclaim dialog. + self.click_button("_Done", node=w) + optionsDlg = self.check_dialog_displayed("Need Space") + self.check_reclaim(optionsDlg) + +class CantReclaimTestCase(BasicStorageTestCase): + def _run(self): + w = self._common_run() + + # Clicking the Done button should bring up the installation options dialog + # indicating there's never going to be enough space to install. There's nothing + # to do now but quit. + self.click_button("_Done", node=w) + doDelay(5) + optionsDlg = self.check_dialog_displayed("No Space") + self.click_button("Quit installer", node=optionsDlg) diff --git a/anaconda/tests/gui/inside/summary.py b/anaconda/tests/gui/inside/summary.py index 46c25ab..bee717a 100644 --- a/anaconda/tests/gui/inside/summary.py +++ b/anaconda/tests/gui/inside/summary.py @@ -30,21 +30,21 @@ from . import UITestCase # spoke selectors are visible. class LiveCDSummaryTestCase(UITestCase): - def check_quit_button(self): - self.click_button("Quit") - self.check_dialog_displayed("Quit") - self.click_button("No") + def check_quit_button(self, spoke): + self.click_button("Quit", node=spoke) + dlg = self.check_dialog_displayed("Quit") + self.click_button("No", node=dlg) - def check_begin_installation_button(self): - button = self.find("Begin Installation", "push button") + def check_begin_installation_button(self, spoke): + button = self.find("Begin Installation", "push button", node=spoke) self.assertIsNotNone(button, msg="Begin Installation button not found") self.assertTrue(button.showing, msg="Begin Installation button should be displayed") self.assertFalse(button.sensitive, msg="Begin Installation button should not be sensitive") - def check_shown_spoke_selectors(self): + def check_shown_spoke_selectors(self, spoke): # FIXME: This forces English. - validSelectors = ["DATE & TIME", "KEYBOARD", "INSTALLATION DESTINATION", "NETWORK & HOSTNAME"] - selectors = self.ana.findChildren(GenericPredicate(roleName="spoke selector")) + validSelectors = ["TIME & DATE", "KEYBOARD", "INSTALLATION DESTINATION", "NETWORK & HOST NAME"] + selectors = spoke.findChildren(GenericPredicate(roleName="spoke selector")) self.assertEqual(len(selectors), len(validSelectors), msg="Incorrect number of spoke selectors shown") @@ -54,7 +54,7 @@ class LiveCDSummaryTestCase(UITestCase): # going to change. # FIXME: This encodes default information. for selector in selectors: - if selector.name == "DATE & TIME": + if selector.name == "TIME & DATE": self.assertEqual(selector.description, "Americas/New York timezone") elif selector.name == "KEYBOARD": self.assertEqual(selector.description, "English (US)") @@ -64,18 +64,19 @@ class LiveCDSummaryTestCase(UITestCase): # one, it selects none. self.assertIn(selector.description, ["Automatic partitioning selected", "No disks selected"]) - elif selector.name == "NETWORK & HOSTNAME": - self.assertRegexpMatches(selector.description, "Wired (.+) connected") + elif selector.name == "NETWORK & HOST NAME": + self.assertTrue(selector.description.startswith("Connected:")) else: self.fail("Invalid spoke selector shown on livecd: %s" % selector.name) def _run(self): # Before doing anything, verify we are on the right screen. doDelay(5) - self.check_window_displayed("INSTALLATION SUMMARY") + w = self.check_window_displayed("INSTALLATION SUMMARY") # And now we can check everything else on the screen. - self.check_quit_button() - self.check_begin_installation_button() - self.check_shown_spoke_selectors() - self.check_warning_bar() + self.check_help_button(w) + self.check_quit_button(w) + self.check_begin_installation_button(w) + self.check_shown_spoke_selectors(w) + self.check_warning_bar(node=w) diff --git a/anaconda/tests/gui/inside/user.py b/anaconda/tests/gui/inside/user.py index 137ed90..866b890 100644 --- a/anaconda/tests/gui/inside/user.py +++ b/anaconda/tests/gui/inside/user.py @@ -18,57 +18,58 @@ from . import UITestCase class BasicUserTestCase(UITestCase): - def check_enter_name(self): - entry = self.find("Full Name", "text") + def check_enter_name(self, spoke): + entry = self.find("Full Name", "text", node=spoke) self.assertIsNotNone(entry, msg="Full name entry not found") entry.grabFocus() entry.text = "Bobby Good User" # We base the username on the full name entered - entry = self.find("Username", "text") + entry = self.find("Username", "text", node=spoke) self.assertIsNotNone(entry, msg="Username entry not found") self.assertEqual(entry.text, "buser", msg="Generated username does not match expectation") - def check_enter_password(self): + def check_enter_password(self, spoke): # The warning bar starts off telling us there's no password set. - self.check_warning_bar("The password is empty") + self.check_warning_bar("The password is empty", node=spoke) - entry = self.find("Password", "text") + entry = self.find("Password", "password text", node=spoke) self.assertIsNotNone(entry, msg="Password entry should be displayed") entry.grabFocus() entry.text = "asdfasdf" # That wasn't a very good password and we haven't confirmed it, so the # bar is still displayed at the bottom. - self.check_warning_bar("it does not contain enough DIFFERENT characters") + self.check_warning_bar("The password you have provided is weak.", node=spoke) # Let's confirm that terrible password. - entry = self.find("Confirm Password", "text") + entry = self.find("Confirm Password", "password text", node=spoke) self.assertIsNotNone(entry, msg="Confirm password should be displayed") entry.grabFocus() entry.text = "asdfasdf" # But of course it's still a terrible password, so the bar is still # displayed at the bottom. - self.check_warning_bar("it does not contain enough DIFFERENT characters") + self.check_warning_bar("The password you have provided is weak.", node=spoke) - def check_click_done(self): + def check_click_done(self, spoke): # Press the Done button once, which won't take us anywhere but will change the # warning label at the bottom. - self.click_button("_Done") - self.check_warning_bar("Press Done again") + self.click_button("_Done", node=spoke) + self.check_warning_bar("Press Done again", node=spoke) # Pressing Done again should take us back to the progress hub. - self.exit_spoke(hubName="CONFIGURATION") + self.exit_spoke(hubName="CONFIGURATION", node=spoke) def _run(self): # First, we need to click on the spoke selector. self.enter_spoke("USER CREATION") # Now, verify we are on the right screen. - self.check_window_displayed("CREATE USER") + w = self.check_window_displayed("CREATE USER") # And now we can check everything else on the screen. - self.check_enter_name() - self.check_enter_password() - self.check_click_done() + self.check_help_button(w) + self.check_enter_name(w) + self.check_enter_password(w) + self.check_click_done(w) diff --git a/anaconda/tests/gui/inside/welcome.py b/anaconda/tests/gui/inside/welcome.py index 86dae38..6281691 100644 --- a/anaconda/tests/gui/inside/welcome.py +++ b/anaconda/tests/gui/inside/welcome.py @@ -32,33 +32,41 @@ from . import UITestCase # contents of the left hand view. class BasicWelcomeTestCase(UITestCase): - def check_lang_locale_views(self): + def check_lang_locale_views(self, spoke): # FIXME: This encodes default information. -# lang = "English" -# locale = "English (United States)" + lang = "English" + locale = "English (United States)" - return -# self.assertEqual(ldtp.verifytablecell("frmWelcome", "tblLanguages", 0, 0, lang), 1, -# msg="Default language (%s) is not selected" % lang) -# self.assertEqual(ldtp.verifytablecell("frmWelcome", "tblLocales", 0, 0, locale), 1, -# msg="Default locale (%s) is not selected") + view = self.find("Languages", node=spoke) + self.assertIsNotNone(view, "Language view not found") + enabled = self.selected_view_children(view) + # We get back a list of [native name, english name, language setting] for each actual language. + self.assertEqual(len(enabled), 3, msg="An unexpected number of languages are selected") + self.assertEqual(enabled[0].text, lang) - def check_quit_button(self): - self.click_button("_Quit") - self.check_dialog_displayed("Quit") - self.click_button("No") + view = self.find("Locales", node=spoke) + self.assertIsNotNone(view, "Locale view not found") + enabled = self.selected_view_children(view) + self.assertEqual(len(enabled), 1, msg="An unexpected number of locales are selected") + self.assertEqual(enabled[0].text, locale) - def check_continue_button(self): - self.click_button("_Continue") - self.check_dialog_displayed("Beta Warn") - self.click_button("I accept my fate.") + def check_quit_button(self, spoke): + self.click_button("_Quit", node=spoke) + dlg = self.check_dialog_displayed("Quit") + self.click_button("No", node=dlg) + + def check_continue_button(self, spoke): + self.click_button("_Continue", node=spoke) + dlg = self.check_dialog_displayed("Beta Warn") + self.click_button("I accept my fate.", dlg) def _run(self): # Before doing anything, verify we are on the right screen. - self.check_window_displayed("WELCOME") + w = self.check_window_displayed("WELCOME") # And now we can check everything else on the screen. - self.check_keyboard_layout_indicator("us") - self.check_lang_locale_views() - self.check_quit_button() - self.check_continue_button() + self.check_help_button(w) + self.check_keyboard_layout_indicator("us", node=w) + self.check_lang_locale_views(w) + self.check_quit_button(w) + self.check_continue_button(w) diff --git a/anaconda/tests/gui/make_livecd.sh b/anaconda/tests/gui/make_livecd.sh index 0709a11..5412fd3 100644 --- a/anaconda/tests/gui/make_livecd.sh +++ b/anaconda/tests/gui/make_livecd.sh @@ -84,6 +84,8 @@ livemedia-creator --make-iso \ --vnc vnc \ --lorax-templates ${TEMPLATES} \ --ram 2048 \ - --vcpus 2 + --vcpus 2 \ + --kernel-args nomodeset \ + --timeout 90 rm livecd.ks rm -r ${TEMPLATES} diff --git a/anaconda/tests/gui/outside/__init__.py b/anaconda/tests/gui/outside/__init__.py index 4a1d9fc..c1e293a 100644 --- a/anaconda/tests/gui/outside/__init__.py +++ b/anaconda/tests/gui/outside/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # @@ -19,14 +19,14 @@ __all__ = ["Creator", "OutsideMixin"] -import blivet +from blivet.size import MiB from contextlib import contextmanager +from nose.plugins.multiprocess import TimedOutException import os import shutil import subprocess import tempfile -import unittest import errno # Copied from python's subprocess.py @@ -49,16 +49,13 @@ class Creator(object): attributes: drives -- A list of tuples describing disk images to create. Each - tuple is the name and size, in GB. + tuple is the name of the drive and its size as a blivet.Size. environ -- A dictionary of environment variables that should be added to the environment the test suite will run under. name -- A unique string that names a Creator. This name will be used in creating the results directory (and perhaps other places in the future) so make sure it doesn't conflict with another object. - reqMemory -- The amount of memory this VM needs, in MB. The default - is 2048, which seems to be the minimum required to - make things run quickly. Redefine if you need more. tests -- A list of tuples describing which test cases make up this test. Each tuple is the name of the module containing the test case (minus the leading "inside." @@ -68,7 +65,6 @@ class Creator(object): drives = [] environ = {} name = "Creator" - reqMemory = 2048 tests = [] def __init__(self): @@ -77,6 +73,11 @@ class Creator(object): self._proc = None self._tempdir = None + self._reqMemory = 1536 + + def _call(self, args): + subprocess.call(args, stdout=open("/dev/null", "w"), stderr=open("/dev/null", "w")) + def archive(self): """Copy all log files and other test results to a subdirectory of the given resultsdir. If logs are no longer available, this method @@ -95,7 +96,6 @@ class Creator(object): directory they were stored in. """ shutil.rmtree(self.tempdir, ignore_errors=True) - os.unlink(self._drivePaths["suite"]) def die(self): """Kill any running qemu process previously started by this test.""" @@ -113,8 +113,10 @@ class Creator(object): (fd, diskimage) = tempfile.mkstemp(dir=self.tempdir) eintr_retry_call(os.close, fd) - subprocess.call(["/usr/bin/qemu-img", "create", "-f", "qcow2", diskimage, "%sG" % size], - stdout=open("/dev/null", "w")) + # For now we are using qemu-img to create these files but specifying + # sizes in blivet Size objects. Unfortunately, qemu-img wants sizes + # as xM or xG, not xMB or xGB. That's what the conversion here is for. + self._call(["/usr/bin/qemu-img", "create", "-f", "raw", diskimage, "%sM" % size.convertTo(MiB)]) self._drivePaths[drive] = diskimage @property @@ -141,79 +143,48 @@ class Creator(object): """ from testconfig import config - # First, create a disk image and put a filesystem on it. - b = blivet.Blivet() + self._call(["/usr/bin/qemu-img", "create", "-f", "raw", self.suitepath, "10M"]) + self._call(["/sbin/mkfs.ext4", "-F", self.suitepath, "-L", "ANACTEST"]) + self._call(["/usr/bin/mount", "-o", "loop", self.suitepath, self.mountpoint]) - # pylint: disable=undefined-variable - disk1_path = blivet.util.create_sparse_tempfile("suite", blivet.size.Size("11 MB")) - b.config.diskImages["suite"] = disk1_path + # Create the directory structure needed for storing results. + os.makedirs(self.mountpoint + "/result/anaconda") - b.reset() + # Copy all the inside stuff into the mountpoint. + shutil.copytree("inside", self.mountpoint + "/inside") - try: - disk1 = b.devicetree.getDeviceByName("suite") - b.initializeDisk(disk1) + # Create the suite file, which contains all the test cases to run and is how + # the VM will figure out what to run. + with open(self.mountpoint + "/suite.py", "w") as f: + imports = map(lambda (path, cls): " from inside.%s import %s" % (path, cls), self.tests) + addtests = map(lambda (path, cls): " s.addTest(%s())" % cls, self.tests) - part = b.newPartition(size=blivet.size.Size("10 MB"), parents=[disk1]) - b.createDevice(part) + f.write(self.template % {"environ": " os.environ.update(%s)" % self.environ, + "imports": "\n".join(imports), + "addtests": "\n".join(addtests), + "anacondaArgs": config.get("anacondaArgs", "").strip('"')}) - fmt = blivet.formats.getFormat("ext4", label="ANACTEST", mountpoint=self.mountpoint) - b.formatDevice(part, fmt) - - blivet.partitioning.doPartitioning(b) - b.doIt() - - fmt.mount() - - # Create the directory structure needed for storing results. - os.makedirs(self.mountpoint + "/result/anaconda") - - # Copy all the inside stuff into the mountpoint. - shutil.copytree("inside", self.mountpoint + "/inside") - - # Create the suite file, which contains all the test cases to run and is how - # the VM will figure out what to run. - with open(self.mountpoint + "/suite.py", "w") as f: - imports = map(lambda (path, cls): " from inside.%s import %s" % (path, cls), self.tests) - addtests = map(lambda (path, cls): " s.addTest(%s())" % cls, self.tests) - - f.write(self.template % {"environ": " os.environ.update(%s)" % self.environ, - "imports": "\n".join(imports), - "addtests": "\n".join(addtests), - "anacondaArgs": config.get("anacondaArgs", "").strip('"')}) - finally: - # pylint: disable=undefined-variable - b.devicetree.teardownDiskImages() - shutil.rmtree(self.mountpoint) + self._call(["/usr/bin/umount", self.mountpoint]) # This ensures it gets passed to qemu-kvm as a disk arg. - self._drivePaths["suite"] = disk1_path + self._drivePaths[self.suitename] = self.suitepath @contextmanager def suiteMounted(self): """This context manager allows for wrapping code that needs to access the suite. It mounts the disk image beforehand and unmounts it afterwards. """ - if self._drivePaths.get("suite", "") == "": + if self._drivePaths.get(self.suitename, "") == "": return - b = blivet.Blivet() - b.config.diskImages["suite"] = self._drivePaths["suite"] - b.reset() - - disk = b.devicetree.getDeviceByName("suite") - part = b.devicetree.getChildren(disk)[0] - part.format.mountpoint = self.mountpoint - part.format.mount() + self._call(["/usr/bin/mount", "-o", "loop", self.suitepath, self.mountpoint]) try: yield except: raise finally: - part.format.unmount() - # pylint: disable=undefined-variable - b.devicetree.teardownDiskImages() + self._call(["/usr/bin/umount", self.mountpoint]) def run(self): """Given disk images previously created by Creator.makeDrives and @@ -222,10 +193,10 @@ class Creator(object): from testconfig import config args = ["/usr/bin/qemu-kvm", - "-vnc", "localhost:2", - "-m", str(self.reqMemory), + "-vnc", "none", + "-m", str(self._reqMemory), "-boot", "d", - "-drive", "file=%s,media=cdrom" % config["liveImage"]] + "-drive", "file=%s,media=cdrom,readonly" % config["liveImage"]] for drive in self._drivePaths.values(): args += ["-drive", "file=%s,media=disk" % drive] @@ -234,8 +205,15 @@ class Creator(object): # it if necessary. For now, the only reason we'd want to kill it is # an expired timer. self._proc = subprocess.Popen(args) - self._proc.wait() - self._proc = None + + try: + self._proc.wait() + except TimedOutException: + self.die() + self.cleanup() + raise + finally: + self._proc = None @property def mountpoint(self): @@ -259,6 +237,14 @@ class Creator(object): return self._tempdir + @property + def suitename(self): + return self.name + "_suite" + + @property + def suitepath(self): + return self.tempdir + "/" + self.suitename + class OutsideMixin(object): """A BaseOutsideTestCase subclass is the interface between the unittest framework and a running VM. It interfaces with an associated Creator object to create diff --git a/anaconda/tests/gui/outside/template.py b/anaconda/tests/gui/outside/template.py index 9ef47b3..4ae631d 100644 --- a/anaconda/tests/gui/outside/template.py +++ b/anaconda/tests/gui/outside/template.py @@ -63,6 +63,9 @@ if __name__ == "__main__": for log in glob.glob("/tmp/*.log"): shutil.copy(log, "/mnt/anactest/result/anaconda/") + if os.path.exists("/tmp/memory.dat"): + shutil.copy("/tmp/memory.dat", "/mnt/anactest/result/anaconda/") + # anaconda writes out traceback files with restricted permissions, so # we have to go out of our way to grab them. for tb in glob.glob("/tmp/anaconda-tb-*"): diff --git a/anaconda/tests/gui/outside/test_reclaim.py b/anaconda/tests/gui/outside/test_reclaim.py new file mode 100644 index 0000000..af21ffb --- /dev/null +++ b/anaconda/tests/gui/outside/test_reclaim.py @@ -0,0 +1,71 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Chris Lumens <clumens@redhat.com> + +__all__ = ["BasicReclaimLiveCDCreator", "BasicReclaimLiveCD_OutsideTest", + "CantReclaimLiveCDCreator", "CantReclaimLiveCD_OutsideTest"] + +from . import Creator, OutsideMixin +import subprocess +import unittest + +from blivet.size import Size + +class BasicReclaimLiveCDCreator(Creator): + drives = [("one", Size("8 GiB"))] + name = "basicreclaimlivecd" + + # This does not test every spoke, as we only need to do enough to satisfy anaconda + # and get us onto the progress hub. + tests = [("welcome", "BasicWelcomeTestCase"), + ("summary", "LiveCDSummaryTestCase"), + ("storage", "BasicReclaimTestCase"), + ("progress", "LiveCDProgressTestCase"), + ("rootpassword", "BasicRootPasswordTestCase"), + ("progress", "LiveCDFinishTestCase")] + + def makeDrives(self): + Creator.makeDrives(self) + + # Put a partition and filesystem across the whole disk, which will + # force anaconda to display the reclaim dialog. + for (drive, size) in self.drives: + subprocess.call(["/sbin/parted", "-s", self._drivePaths[drive], "mklabel", "msdos"], + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w")) + subprocess.call(["/sbin/parted", "-s", self._drivePaths[drive], "mkpart", "p", "ext2", "0", str(size)], + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w")) + subprocess.call(["/sbin/mkfs.ext4", "-F", self._drivePaths[drive]], + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w")) + +class BasicReclaimLiveCD_OutsideTest(OutsideMixin, unittest.TestCase): + creatorClass = BasicReclaimLiveCDCreator + +class CantReclaimLiveCDCreator(BasicReclaimLiveCDCreator): + drives = [("one", Size("2 GiB"))] + name = "cantreclaimlivecd" + + # We don't get to test much here, since the reclaim test shuts down anaconda. + tests = [("welcome", "BasicWelcomeTestCase"), + ("summary", "LiveCDSummaryTestCase"), + ("storage", "CantReclaimTestCase")] + +class CantReclaimLiveCD_OutsideTest(OutsideMixin, unittest.TestCase): + creatorClass = CantReclaimLiveCDCreator diff --git a/anaconda/tests/gui/outside/test_simplelivecd.py b/anaconda/tests/gui/outside/test_simplelivecd.py index 7f9c485..6d333da 100644 --- a/anaconda/tests/gui/outside/test_simplelivecd.py +++ b/anaconda/tests/gui/outside/test_simplelivecd.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # @@ -22,8 +22,10 @@ __all__ = ["SimpleLiveCDCreator", "SimpleLiveCD_OutsideTest"] from . import Creator, OutsideMixin import unittest +from blivet.size import Size + class SimpleLiveCDCreator(Creator): - drives = [("one", 8)] + drives = [("one", Size("8 GiB"))] name = "simplelivecd" tests = [("welcome", "BasicWelcomeTestCase"), ("summary", "LiveCDSummaryTestCase"), diff --git a/anaconda/tests/gui/run_gui_tests.sh b/anaconda/tests/gui/run_gui_tests.sh index b170f4b..ee78b5d 100644 --- a/anaconda/tests/gui/run_gui_tests.sh +++ b/anaconda/tests/gui/run_gui_tests.sh @@ -22,24 +22,28 @@ function doit() { ARGS="-s \ -v \ --nologcapture \ + --process-timeout=1200 \ + --processes=2 \ --tc=resultsdir:$(mktemp -d --tmpdir=$(pwd) autogui-results-XXXXXX) \ --tc=liveImage:$1" if [ -z "$2" ]; then - nosetests ${ARGS} outside + nosetests ${ARGS} ${GUI_TESTS:-outside} else - nosetests ${ARGS} "${2}" outside + nosetests ${ARGS} "${2}" ${GUI_TESTS:-outside} fi } # We require the test_config plugin for nose, which is not currently packaged # but is installable via pip. if [ -z "$(nosetests -p | grep test_config)" ]; then + echo "test_config plugin is not available; exiting." exit 99 fi # Have to be root to run this test, as it requires creating disk iamges. if [ ${EUID} != 0 ]; then + echo "You must be root to run the GUI tests; skipping." exit 77 fi diff --git a/anaconda/tests/install/run_install_test.sh b/anaconda/tests/install/run_install_test.sh new file mode 100644 index 0000000..e137953 --- /dev/null +++ b/anaconda/tests/install/run_install_test.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +# Have to be root to run this test, as yum is stupid. +if [ ${EUID} != 0 ]; then + exit 77 +fi + +tmpdir=$(mktemp -d -p /var/tmp) + +# The anaconda repo can come from one of two different places: +# (1) $TEST_ANACONDA_REPO, if this script is being called from "make check" +# (2) The command line, if this script is being called directly. +if [[ "${TEST_ANACONDA_REPO}" != "" ]]; then + REPO=${TEST_ANACONDA_REPO} +elif [[ $# != 0 ]]; then + REPO=$1 + shift +else + echo "usage: $0 <anaconda repo>" + exit 1 +fi + +status=0 + +cat <<EOF > ${tmpdir}/yum.conf +[anaconda] +name=anaconda \$releasever - \$basearch +baseurl=${REPO} +enabled=1 + +[anaconda-rawhide] +name=Fedora - Rawhide - Developmental packages for the next Fedora release +failovermethod=priority +baseurl=http://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/\$basearch/os/ +enabled=1 +gpgcheck=0 +EOF + +yum install -y -c ${tmpdir}/yum.conf --installroot=${tmpdir} --releasever=rawhide \ + --disablerepo=\* --enablerepo=anaconda --enablerepo=anaconda-rawhide \ + anaconda +status=$? + +# yum's return value is not especially helpful (it can return 0 even on error) +# but just in case it told us it failed, exit out here. +if [ $? != 0 ]; then + rm -r ${tmpdir} + exit $status +fi + +# Did anaconda actually get installed? At least rpm will set $?=1 if not. +rpm --root=${tmpdir} -q anaconda +status=$? + +rm -r ${tmpdir} +exit $status diff --git a/anaconda/tests/ostree/basic.ks b/anaconda/tests/kickstart_tests/basic-ostree.ks similarity index 85% rename from anaconda/tests/ostree/basic.ks rename to anaconda/tests/kickstart_tests/basic-ostree.ks index 3767366..b2dc374 100644 --- a/anaconda/tests/ostree/basic.ks +++ b/anaconda/tests/kickstart_tests/basic-ostree.ks @@ -1,7 +1,9 @@ # Substitute something in for REPO or this will all come crashing down. ostreesetup --nogpg --osname=fedora-atomic --remote=fedora-atomic --url=REPO --ref=fedora-atomic/rawhide/x86_64/base/core +install +network --bootproto=dhcp -bootloader --timeout=1 --extlinux +bootloader --timeout=1 zerombr clearpart --all part --fstype=ext4 --size=4400 / @@ -9,12 +11,13 @@ part --fstype=ext4 --size=500 /boot part --fstype=swap --size=500 swap keyboard us +lang en timezone America/New_York rootpw qweqwe shutdown %post -cat <<EOF > /etc/systemd/system/default.target.wants/run-test.service +cat <<EOF > /lib/systemd/system/default.target.wants/run-test.service [Unit] Description=Run a test to see if anaconda+ostree worked After=basic.target diff --git a/anaconda/tests/kickstart_tests/basic-ostree.sh b/anaconda/tests/kickstart_tests/basic-ostree.sh new file mode 100644 index 0000000..0416f4f --- /dev/null +++ b/anaconda/tests/kickstart_tests/basic-ostree.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc +} + +prepare() { + ks=$1 + tmpdir=$2 + + if [[ "${TEST_OSTREE_REPO}" == "" ]]; then + echo \$TEST_OSTREE_REPO is not set. + return 1 + fi + + sed -e "/ostreesetup/ s|REPO|${TEST_OSTREE_REPO}|" ${ks} > ${tmpdir}/kickstart.ks + echo ${tmpdir}/kickstart.ks +} + +validate() { + img=$1 + + # Now attempt to boot the resulting VM and see if the install + # actually worked. The VM will shut itself down so there's no + # need to worry with that here. + timeout 5m /usr/bin/qemu-kvm -m 2048 \ + -smp 2 \ + -hda ${img} \ + -vnc localhost:3 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /ostree/deploy/fedora-atomic/var/roothome/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-1.ks b/anaconda/tests/kickstart_tests/groups-and-envs-1.ks new file mode 100644 index 0000000..3ff575d --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-1.ks @@ -0,0 +1,38 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=4400 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +@core +@c-development +@^web-server-environment +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. +rpm -q httpd +if [[ $? != 0 ]]; then + echo '*** web-server-environment was not installed' > /root/RESULT +else + rpm -q gcc + if [[ $? != 0 ]]; then + echo '*** c-development was not installed' > /root/RESULT + else + echo SUCCESS > /root/RESULT + fi +fi +%end diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-1.sh b/anaconda/tests/kickstart_tests/groups-and-envs-1.sh new file mode 100644 index 0000000..e3ed01a --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-1.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-2.ks b/anaconda/tests/kickstart_tests/groups-and-envs-2.ks new file mode 100644 index 0000000..a767008 --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-2.ks @@ -0,0 +1,82 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=4400 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +# (1) Test that you can remove a group that's part of an environment. +@^xfce-desktop-environment +-@dial-up + +# (2) Test that you can add and then remove a group. +@3d-printing +-@3d-printing + +# (3) Test that --optional works. +@container-management --optional + +# (4) Test that --nodefaults works. +@rpm-development-tools --nodefaults +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. + +# Testing #1 - lrzsz is only part of dial-up, and should not be installed. +rpm -q lrzsz +if [[ $? == 0 ]]; then + echo '*** dial-up group should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #2 - RepetierHost is only part of 3d-printing, and should not +# be installed. +rpm -q RepetierHost +if [[ $? == 0 ]]; then + echo '*** 3d-printing group should not have been installed' > /root/RESULT + exit 1 +f + +# Testing #3 - docker-registry is only part of container-management, where +# it is optional, so it should be installed. +rpm -q docker-registry +if [[ $? != 0 ]]; then + echo '*** docker-registry was not installed' > /root/RESULT + exit 1 +fi + +# Testing #4 - rpm-build is mandatory so it should be installed. rpmdevtools is +# default so it should not. rpmlint is optional so it should not. +rpm -q rpm-build +if [[ $? != 0 ]]; then + echo '*** Mandatory package from rpm-development-tools was not installed' > /root/RESULT + exit 1 +else + rpm -q rpmdevtools + if [[ $? == 0 ]]; then + echo '*** Default package from rpm-development-tools should not have been installed' > /root/RESULT + exit 1 + else + rpm -q rpmlint + if [[ $? == 0 ]]; then + echo '*** Optional package from rpm-development-tools should not have been installed' > /root/RESULT + exit 1 + fi + fi +fi + +echo SUCCESS > /root/RESULT +%end diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-2.sh b/anaconda/tests/kickstart_tests/groups-and-envs-2.sh new file mode 100644 index 0000000..549d07a --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-2.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.ks b/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.ks new file mode 100644 index 0000000..3ff575d --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.ks @@ -0,0 +1,38 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=4400 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +@core +@c-development +@^web-server-environment +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. +rpm -q httpd +if [[ $? != 0 ]]; then + echo '*** web-server-environment was not installed' > /root/RESULT +else + rpm -q gcc + if [[ $? != 0 ]]; then + echo '*** c-development was not installed' > /root/RESULT + else + echo SUCCESS > /root/RESULT + fi +fi +%end diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.sh b/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.sh new file mode 100644 index 0000000..31fe340 --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-yum-1.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc inst.nodnf +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.ks b/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.ks new file mode 100644 index 0000000..fc87cc6 --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.ks @@ -0,0 +1,82 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=4400 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +# (1) Test that you can remove a group that's part of an environment. +@^xfce-desktop-environment +-@dial-up + +# (2) Test that you can add and then remove a group. +@3d-printing +-@3d-printing + +# (3) Test that --optional works. +@container-management --optional + +# (4) Test that --nodefaults works. +@rpm-development-tools --nodefaults +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. + +# Testing #1 - lrzsz is only part of dial-up, and should not be installed. +rpm -q lrzsz +if [[ $? == 0 ]]; then + echo '*** dial-up group should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #2 - RepetierHost is only part of 3d-printing, and should not +# be installed. +rpm -q RepetierHost +if [[ $? == 0 ]]; then + echo '*** 3d-printing group should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #3 - docker-registry is only part of container-management, where +# it is optional, so it should be installed. +rpm -q docker-registry +if [[ $? != 0 ]]; then + echo '*** docker-registry was not installed' > /root/RESULT + exit 1 +fi + +# Testing #4 - rpm-build is mandatory so it should be installed. rpmdevtools is +# default so it should not. rpmlint is optional so it should not. +rpm -q rpm-build +if [[ $? != 0 ]]; then + echo '*** Mandatory package from rpm-development-tools was not installed' > /root/RESULT + exit 1 +else + rpm -q rpmdevtools + if [[ $? == 0 ]]; then + echo '*** Default package from rpm-development-tools should not have been installed' > /root/RESULT + exit 1 + else + rpm -q rpmlint + if [[ $? == 0 ]]; then + echo '*** Optional package from rpm-development-tools should not have been installed' > /root/RESULT + exit 1 + fi + fi +fi + +echo SUCCESS > /root/RESULT +%end diff --git a/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.sh b/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.sh new file mode 100644 index 0000000..113c927 --- /dev/null +++ b/anaconda/tests/kickstart_tests/groups-and-envs-yum-2.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc inst.nodnf +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/packages-and-groups-1.ks b/anaconda/tests/kickstart_tests/packages-and-groups-1.ks new file mode 100644 index 0000000..7477297 --- /dev/null +++ b/anaconda/tests/kickstart_tests/packages-and-groups-1.ks @@ -0,0 +1,82 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=6500 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +@^xfce-desktop-environment + +# (1) Test that you can remove a package that's part of a group +@c-development +-valgrind + +# (2) Test that you can add and then remove the same package. +qemu-kvm +-qemu-kvm + +# (3) Test that you can add packages with a glob. +emacs* + +# (4) Test that you can remove packages with a glob. +-ibus* +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. + +# Testing #1 - gcc should be installed, but not valgrind +rpm -q gcc +if [[ $? != 0 ]]; then + echo '*** c-development group was not installed' > /root/RESULT + exit 1 +fi + +rpm -q valgrind +if [[ $? == 0 ]]; then + echo '*** valgrind package should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #2 - qemu-kvm should not be installed. +rpm -q qemu-kvm +if [[ $? == 0 ]]; then + echo '*** 3d-printing group should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #3 - emacs stuff should be installed. +rpm -qa emacs\* +if [[ $? != 0 ]]; then + echo '*** emacs glob was not installed' > /root/RESULT + exit 1 +fi + +# Make sure that more than just emacs and its dependencies were installed. +count=$(rpm -qa emacs\* | wc -l) +if [[ $count -lt 50 ]]; then + echo '*** emacs glob was not fully installed' > /root/RESULT + exit 1 +fi + +# Testing #4 - ibus stuff should not be installed. +rpm -qa ibus\* +if [[ $? == 0 ]]; then + echo '*** ibus glob should not have been installed' > /root/RESULT + exit 1 +fi + +echo SUCCESS > /root/RESULT +%end diff --git a/anaconda/tests/kickstart_tests/packages-and-groups-1.sh b/anaconda/tests/kickstart_tests/packages-and-groups-1.sh new file mode 100644 index 0000000..549d07a --- /dev/null +++ b/anaconda/tests/kickstart_tests/packages-and-groups-1.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.ks b/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.ks new file mode 100644 index 0000000..7477297 --- /dev/null +++ b/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.ks @@ -0,0 +1,82 @@ +url --url=http://dl.fedoraproject.org/pub/fedora/linux/development/$releasever/$basearch/os/ +install +network --bootproto=dhcp + +bootloader --timeout=1 +zerombr +clearpart --all +part --fstype=ext4 --size=6500 / +part --fstype=ext4 --size=500 /boot +part --fstype=swap --size=500 swap + +keyboard us +lang en +timezone America/New_York +rootpw qweqwe +shutdown + +%packages +@^xfce-desktop-environment + +# (1) Test that you can remove a package that's part of a group +@c-development +-valgrind + +# (2) Test that you can add and then remove the same package. +qemu-kvm +-qemu-kvm + +# (3) Test that you can add packages with a glob. +emacs* + +# (4) Test that you can remove packages with a glob. +-ibus* +%end + +%post +# We don't have a way of determining if a group/env is installed or not. +# These sentinel packages will have to do. + +# Testing #1 - gcc should be installed, but not valgrind +rpm -q gcc +if [[ $? != 0 ]]; then + echo '*** c-development group was not installed' > /root/RESULT + exit 1 +fi + +rpm -q valgrind +if [[ $? == 0 ]]; then + echo '*** valgrind package should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #2 - qemu-kvm should not be installed. +rpm -q qemu-kvm +if [[ $? == 0 ]]; then + echo '*** 3d-printing group should not have been installed' > /root/RESULT + exit 1 +fi + +# Testing #3 - emacs stuff should be installed. +rpm -qa emacs\* +if [[ $? != 0 ]]; then + echo '*** emacs glob was not installed' > /root/RESULT + exit 1 +fi + +# Make sure that more than just emacs and its dependencies were installed. +count=$(rpm -qa emacs\* | wc -l) +if [[ $count -lt 50 ]]; then + echo '*** emacs glob was not fully installed' > /root/RESULT + exit 1 +fi + +# Testing #4 - ibus stuff should not be installed. +rpm -qa ibus\* +if [[ $? == 0 ]]; then + echo '*** ibus glob should not have been installed' > /root/RESULT + exit 1 +fi + +echo SUCCESS > /root/RESULT +%end diff --git a/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.sh b/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.sh new file mode 100644 index 0000000..113c927 --- /dev/null +++ b/anaconda/tests/kickstart_tests/packages-and-groups-yum-1.sh @@ -0,0 +1,48 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +kernel_args() { + echo vnc inst.nodnf +} + +prepare() { + ks=$1 + tmpdir=$2 + + echo ${ks} +} + +validate() { + img=$1 + + # There should be a /root/RESULT file with results in it. Check + # its contents and decide whether the test finally succeeded or + # not. + result=$(virt-cat -a ${img} -m /dev/sda2 /root/RESULT) + if [[ $? != 0 ]]; then + status=1 + echo '*** /root/RESULT does not exist in VM image.' + elif [[ "${result}" != "SUCCESS" ]]; then + status=1 + echo "${result}" + fi + + return ${status} +} + diff --git a/anaconda/tests/kickstart_tests/run_kickstart_tests.sh b/anaconda/tests/kickstart_tests/run_kickstart_tests.sh new file mode 100644 index 0000000..2024cdd --- /dev/null +++ b/anaconda/tests/kickstart_tests/run_kickstart_tests.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# +# Copyright (C) 2014, 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +# This script runs the entire kickstart_tests suite. It is an interface +# between "make check" (which is why it takes environment variables instead +# of arguments) and livemedia-creator. Each test consists of a kickstart +# file that specifies most everything about the installation, and a shell +# script that does validation and specifies kernel boot parameters. lmc +# then fires up a VM and watches for tracebacks or stuck installs. +# +# A boot ISO is required, which should be specified with TEST_BOOT_ISO=. +# +# The number of jobs corresponds to the number of VMs that will be started +# simultaneously. Each one wants about 2 GB of memory. The default is +# two simultaneous jobs, but you can control this with TEST_JOBS=. It is +# suggested you not run out of memory. +# +# You can control what logs are held onto after the test is complete via the +# KEEPIT= variable, explained below. By default, nothing is kept. +# +# Finally, you can run tests across multiple computers at the same time by +# putting all the hostnames into TEST_REMOTES= as a space separated list. +# Do not add localhost manually, as it will always be added for you. You +# must create a user named kstest on each remote system, allow that user to +# sudo to root for purposes of running livemedia-creator, and have ssh keys +# set up so that the user running this script can login to the remote systems +# as kstest without a password. TEST_JOBS= applies on a per-system basis. +# KEEPIT= controls how much will be kept on the master system (where "make +# check" is run). All results will be removed from the slave systems. + +# The boot.iso location can come from one of two different places: +# (1) $TEST_BOOT_ISO, if this script is being called from "make check" +# (2) The command line, if this script is being called directly. +IMAGE="" +if [[ "${TEST_BOOT_ISO}" != "" ]]; then + IMAGE=${TEST_BOOT_ISO} +elif [[ $# != 0 ]]; then + IMAGE=$1 + shift +fi + +if [[ ! -e "${IMAGE}" ]]; then + echo "Required boot.iso does not exist; skipping." + exit 77 +fi + +# Possible values for this parameter: +# 0 - Keep nothing (the default) +# 1 - Keep log files +# 2 - Keep log files and disk images (will take up a lot of space) +KEEPIT=${KEEPIT:-0} + +# This is for environment variables that parallel needs to pass to +# remote systems. Put anything here that test cases care about or +# they won't work when run on some systems. +# +# NOTE: You will also need to add these to the list in /etc/sudoers +# if you are using env_reset there, or they will not get passed from +# this script to parallel. +env_args="--env TEST_OSTREE_REPO" + +# Round up all the kickstart tests we want to run, skipping those that are not +# executable as well as this file itself. +find kickstart_tests -name '*sh' -a -perm -o+x -a \! -wholename 'kickstart_tests/run_*.sh' | \ +if [[ "$TEST_REMOTES" != "" ]]; then + _IMAGE=kickstart_tests/$(basename ${IMAGE}) + + # (1) Copy everything to the remote systems. We do this ourselves because + # parallel doesn't like globs, and we need to put the boot image somewhere + # that qemu on the remote systems can read. + for remote in ${TEST_REMOTES}; do + scp -r kickstart_tests kstest@${remote}: + scp ${IMAGE} kstest@${remote}:kickstart_tests/ + done + + # (1a) We also need to copy the provided image to under kickstart_tests/ on + # the local system too. This is because parallel will attempt to run the + # same command line on every system and that requires the image to also be + # in the same location. + cp ${IMAGE} ${_IMAGE} + + # (2) Run parallel. We always add the local system to the list of machines + # being passed to parallel. Don't add it yourself. + remote_args="--sshlogin :" + for remote in ${TEST_REMOTES}; do + remote_args="${remote_args} --sshlogin kstest@${remote}" + done + + parallel --no-notice ${remote_args} \ + ${env_args} --jobs ${TEST_JOBS:-2} \ + sudo kickstart_tests/run_one_ks.sh -i ${_IMAGE} -k ${KEEPIT} {} + rc=$? + + # (3) Get all the results back from the remote systems, which will have already + # applied the KEEPIT setting. However if KEEPIT is 0 (meaning, don't save + # anything) there's no point in trying. We do this ourselves because, again, + # parallel doesn't like globs. + # + # We also need to clean up the stuff we copied over in step 1, and then clean up + # the results from the remotes too. We don't want to keep things scattered all + # over the place. + for remote in ${TEST_REMOTES}; do + if [[ ${KEEPIT} > 0 ]]; then + scp -r kstest@${remote}:/var/tmp/kstest-\* /var/tmp/ + fi + + ssh kstest@${remote} rm -rf kickstart_tests /var/tmp/kstest-\* + done + + # (3a) And then also remove the copy of the image we made earlier. + rm ${_IMAGE} + + # (4) Exit the subshell defined by "find ... | " way up at the top. The exit + # code will be caught outside and converted into the overall exit code. + exit ${rc} +else + parallel --no-notice ${env_args} --jobs ${TEST_JOBS:-2} \ + sudo kickstart_tests/run_one_ks.sh -i ${IMAGE} -k ${KEEPIT} {} + + # For future expansion - any cleanup code can go in between the variable + # setting and the exit, like in the other branch of the if-else above. + rc=$? + exit ${rc} +fi + +# Catch the exit code of the subshell and return it. This is structured for +# future expansion, too. Any extra global cleanup code can go in between the +# variable setting and the exit. +rc=$? +exit ${rc} diff --git a/anaconda/tests/kickstart_tests/run_one_ks.sh b/anaconda/tests/kickstart_tests/run_one_ks.sh new file mode 100644 index 0000000..f7b1ea0 --- /dev/null +++ b/anaconda/tests/kickstart_tests/run_one_ks.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Chris Lumens <clumens@redhat.com> + +# This script runs a single kickstart test on a single system. It takes +# command line arguments instead of environment variables because it is +# designed to be driven by run_kickstart_tests.sh via parallel. It is +# not for direct use, though as long as you pass the right arguments there's +# no reason it couldn't work. + +IMAGE= +KEEPIT=0 + +cleanup() { + d=$1 + + # Always remove the copy of the boot.iso. + rm ${d}/$(basename ${IMAGE}) + + if [[ ${KEEPIT} == 2 ]]; then + return + elif [[ ${KEEPIT} == 1 ]]; then + rm -f ${d}/*img ${d}/*ks + elif [[ ${KEEPIT} == 0 ]]; then + rm -rf ${d} + fi +} + +runone() { + t=$1 + + ks=${t/.sh/.ks} + . $t + + name=$(basename ${t%.sh}) + + echo + echo =========================================================================== + echo ${ks} on $(hostname) + echo =========================================================================== + + # qemu user needs to be able to read the directory and the boot.iso, so put that + # into this directory as well. It will get deleted later, regardless of the + # KEEPIT setting. + tmpdir=$(mktemp -d --tmpdir=/var/tmp kstest-${name}.XXXXXXXX) + chmod 755 ${tmpdir} + cp ${IMAGE} ${tmpdir} + + ksfile=$(prepare ${ks} ${tmpdir}) + if [[ $? != 0 ]]; then + echo Test prep failed: ${ksfile} + cleanup ${tmpdir} + return 1 + fi + + kargs=$(kernel_args) + if [[ "${kargs}" != "" ]]; then + kargs="--kernel-args \"$kargs\"" + fi + + eval livemedia-creator ${kargs} \ + --make-disk \ + --iso "${tmpdir}/$(basename ${IMAGE})" \ + --ks ${ksfile} \ + --tmp ${tmpdir} \ + --logfile ${tmpdir}/livemedia.log \ + --title Fedora \ + --project Fedora \ + --releasever 22 \ + --ram 2048 \ + --vcpus 2 \ + --vnc vnc \ + --timeout 60 + if [[ $? != 0 ]]; then + echo $(grep CRIT ${tmpdir}/virt-install.log) + cleanup ${tmpdir} + return 1 + elif [[ -f ${tmpdir}/livemedia.log ]]; then + img=$(grep disk_img ${tmpdir}/livemedia.log | cut -d= -f2) + trimmed=${img## } + + if [[ $(grep "due to timeout" ${tmpdir}/livemedia.log) != "" ]]; then + echo FAILED - Test timed out. + cleanup ${tmpdir} + return 1 + elif [[ ! -f ${trimmed} ]]; then + echo FAILED - Disk image ${trimmed} does not exist. + cleanup ${tmpdir} + return 1 + fi + + result=$(validate ${trimmed}) + if [[ $? != 0 ]]; then + echo FAILED - "${result}" + cleanup ${tmpdir} + return 1 + fi + fi + + echo SUCCESS + cleanup ${tmpdir} + return 0 +} + +# Have to be root to run this test, as it requires creating disk images. +if [[ ${EUID} != 0 ]]; then + echo "You must be root to run this test." + exit 77 +fi + +while getopts ":i:k:" opt; do + case $opt in + i) + IMAGE=$OPTARG + ;; + + k) + KEEPIT=$OPTARG + ;; + + *) + echo "Usage: run_one_ks.sh -i ISO [-k KEEPIT] ks.cfg" + exit 1 + ;; + esac +done + +shift $((OPTIND - 1)) + +if [[ ! -e "${IMAGE}" ]]; then + echo "Required boot.iso does not exist." + exit 77 +fi + +if [[ $# == 0 || ! -x $1 ]]; then + echo "Test not provided or is not executable." + exit 1 +fi + +runone $1 diff --git a/anaconda/tests/lib/regexcheck.py b/anaconda/tests/lib/regexcheck.py new file mode 100644 index 0000000..57307d9 --- /dev/null +++ b/anaconda/tests/lib/regexcheck.py @@ -0,0 +1,71 @@ +# +# regexcheck.py: check a regular expression against lists of matching and non-matching strings +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Shea <dshea@redhat.com> + +def regex_match(expression, goodlist, badlist): + """ Check that a regular expression matches and does not match lists of strings. + + This method will print a message for any failure and return a a bool + to indicate whether the testcase passed. + + :param expression: a compiled regular expression object + :param goodlist: a list of strings expected to be matched by expression + :param badlist: a list of strings expected not to be matched by expression + :returns: True if all strings in the lists matched or did not match as expected + :rtype: bool + """ + + success = True + + for good in goodlist: + if expression.match(good) is None: + success = False + print("Good string %s did not match expression" % good) + + for bad in badlist: + if expression.match(bad) is not None: + success = False + print("Bad string %s matched expression" % bad) + + return success + +def regex_group(expression, test_cases): + """ Check that a regex parses strings into expected groups. + + Test cases is a list of tuples of the form (test_string, expected_result) + where expected_result is the groups tuples that should be returned by + regex.match. + + :param expression: a compiled expression object + :param test_cases: a list of test strings and expected results + :returns: True if all test cases return the expected groups + :rtype: bool + """ + + success = True + for test_str, result in test_cases: + match = expression.match(test_str) + if match is not None: + match = match.groups() + + if match != result: + print("Test case `%s' did not parse as `%s': %s" % (test_str, result, match)) + success = False + + return success diff --git a/anaconda/tests/lib/timer.py b/anaconda/tests/lib/timer.py new file mode 100644 index 0000000..f7f62c6 --- /dev/null +++ b/anaconda/tests/lib/timer.py @@ -0,0 +1,41 @@ +# +# timer.py: timer decorator for unittest functions +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: David Shea <dshea@redhat.com> + +from contextlib import contextmanager +import signal + +@contextmanager +def timer(seconds): + """Return a timer context manager. + + If the code within the context does not finish within the given number + of seconds, it will raise an AssertionError. + """ + def _handle_sigalrm(signum, frame): + raise AssertionError("Test failed to complete within %d seconds" % seconds) + + old_handler = signal.signal(signal.SIGALRM, _handle_sigalrm) + try: + signal.alarm(seconds) + yield + finally: + # Put everything back + signal.alarm(0) + signal.signal(signal.SIGALRM, old_handler) diff --git a/anaconda/tests/ostree/run_ostree_tests.sh b/anaconda/tests/ostree/run_ostree_tests.sh deleted file mode 100644 index b2d57eb..0000000 --- a/anaconda/tests/ostree/run_ostree_tests.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -# Red Hat Author(s): Chris Lumens <clumens@redhat.com> - -# Have to be root to run this test, as it requires creating disk iamges. -if [ ${EUID} != 0 ]; then - exit 77 -fi - -# The boot.iso location can come from one of two different places: -# (1) $TEST_BOOT_ISO, if this script is being called from "make check" -# (2) The command line, if this script is being called directly. -if [[ "${TEST_BOOT_ISO}" != "" ]]; then - IMAGE=${TEST_BOOT_ISO} -elif [[ $# != 0 ]]; then - IMAGE=$1 - shift -fi - -# The same with the ostree repo. -if [[ "${TEST_OSTREE_REPO}" != "" ]]; then - REPO=${TEST_OSTREE_REPO} -elif [[ $# != 0 ]]; then - REPO=$1 - shift -else - echo "usage: $0 <boot.iso> <ostree repo>" - exit 1 -fi - -if [ ! -e "${IMAGE}" ]; then - echo "Required boot.iso does not exist." - exit 2 -fi - -logdir=$(mktemp -d) - -status=0 -for ks in ostree/*ks; do - # Substitute in the location of an ostree repo here. This could be one - # publically accessible, or a very local and private one that happens - # to be fast. - ksfile=$(mktemp) - sed -e "/ostreesetup/ s|REPO|${REPO}|" ${ks} > ${ksfile} - - echo ${ks} - echo ==================== - - livemedia-creator --make-disk \ - --iso "${IMAGE}" \ - --ks ${ksfile} \ - --tmp /var/tmp \ - --logfile ${logdir}/livemedia.log \ - --title Fedora \ - --project Fedora \ - --releasever 21 \ - --ram 2048 \ - --vcpus 2 \ - --vnc vnc - if [ $? != 0 ]; then - status=1 - echo $(grep CRIT ${logdir}/virt-install.log) - fi - - rm ${ksfile} - - if [ -f ostree/run_ostree_tests.log ]; then - img=$(grep disk_img ostree/run_ostree_tests.log | cut -d= -f2) - trimmed=${img## } - - if [ ! -f ${trimmed} ]; then - status=1 - echo Disk image ${trimmed} does not exist. - continue - fi - - # Now attempt to boot the resulting VM and see if the install - # actually worked. The VM will shut itself down so there's no - # need to worry with that here. - /usr/bin/qemu-kvm -m 2048 \ - -smp 2 \ - -hda ${trimmed} - - # There should be a /root/RESULT file with results in it. Check - # its contents and decide whether the test finally succeeded or - # not. - result=$(virt-cat -a ${trimmed} -m /dev/sda2 /ostree/deploy/fedora-atomic/var/roothome/RESULT) - if [ $? != 0 ]; then - status=1 - echo /root/RESULT does not exist in VM image. - elif [ "${result}" != "SUCCESS" ]; then - status=1 - echo ${result} - fi - fi - - # Clean it up for the next go around. - if [ -f ${trimmed} ]; then - rm ${trimmed} - fi -done - -rm -r ${logdir} -exit $status diff --git a/anaconda/tests/pyanaconda_tests/grub_raid_test.py b/anaconda/tests/pyanaconda_tests/grub_raid_test.py index 6690073..0e4188e 100644 --- a/anaconda/tests/pyanaconda_tests/grub_raid_test.py +++ b/anaconda/tests/pyanaconda_tests/grub_raid_test.py @@ -20,6 +20,8 @@ # These tests do not write anything to the disk and do not require root from blivet.devices import DiskDevice, PartitionDevice, MDRaidArrayDevice +from blivet.devices import BTRFSVolumeDevice, BTRFSSubVolumeDevice +from blivet.devicelibs.raid import RAID1 from blivet.formats import getFormat from blivet.size import Size @@ -46,29 +48,40 @@ class GRUBRaidSimpleTest(unittest.TestCase): self.sdb = DiskDevice(name="sdb", size=Size("100 GiB")) self.sdb.format = getFormat("disklabel") - # Set up biosboot partitions and an array for /boot on sda + sdb. + # Set up biosboot partitions, an mdarray for /boot, and a btrfs array on sda + sdb. # Start with the partitions self.sda1 = PartitionDevice(name="sda1", parents=[self.sda], size=Size("1 MiB")) self.sda1.format = getFormat("biosboot") self.sda2 = PartitionDevice(name="sda2", parents=[self.sda], size=Size("500 MiB")) self.sda2.format = getFormat("mdmember") + self.sda4 = PartitionDevice(name="sda4", parents=[self.sda], size=Size("500 MiB")) + self.sda4.format = getFormat("btrfs") self.sdb1 = PartitionDevice(name="sdb1", parents=[self.sdb], size=Size("1 MiB")) self.sdb1.format = getFormat("biosboot") self.sdb2 = PartitionDevice(name="sdb2", parents=[self.sdb], size=Size("500 MiB")) self.sdb2.format = getFormat("mdmember") + self.sdb4 = PartitionDevice(name="sdb4", parents=[self.sdb], size=Size("4 GiB")) + self.sdb4.format = getFormat("btrfs") # Add an extra partition for /boot on not-RAID self.sda3 = PartitionDevice(name="sda3", parents=[self.sda], size=Size("500 MiB")) self.sda3.format = getFormat("ext4", mountpoint="/boot") # Pretend that the partitions are real with real parent disks - for part in (self.sda1, self.sda2, self.sdb1, self.sdb2): + for part in (self.sda1, self.sda2, self.sda3, self.sda4, self.sdb1, self.sdb2, self.sdb4): part.parents = part.req_disks self.boot_md = MDRaidArrayDevice(name="md1", parents=[self.sda2, self.sdb2], level=1) self.boot_md.format = getFormat("ext4", mountpoint="/boot") + # Set up the btrfs raid1 volume with a subvolume for /boot + self.btrfs_volume = BTRFSVolumeDevice(parents=[self.sda4, self.sdb4], dataLevel=RAID1) + self.btrfs_volume.format = getFormat("btrfs") + + self.boot_btrfs = BTRFSSubVolumeDevice(parents=[self.btrfs_volume]) + self.boot_btrfs.format = getFormat("btrfs", mountpoint="/boot") + self.grub = GRUB() def grub_mbr_partition_test(self): @@ -103,12 +116,12 @@ class GRUBRaidSimpleTest(unittest.TestCase): # Test stage1 on sda (MBR), stage2 on /boot RAID # install_targets should return two grub installs, one for each disk - # in the raid. stage1 will be the disk, stage2 will be the partition. + # in the raid. stage1 will be the disk, stage2 will be the raid device. self.grub.stage1_device = self.sda self.grub.stage2_device = self.boot_md install_targets = set(self.grub.install_targets) - expected_targets = set([(self.sda, self.sda2), (self.sdb, self.sdb2)]) + expected_targets = set([(self.sda, self.boot_md), (self.sdb, self.boot_md)]) self.assertEquals(install_targets, expected_targets) @@ -125,3 +138,32 @@ class GRUBRaidSimpleTest(unittest.TestCase): expected_targets = set([(self.sda1, self.boot_md)]) self.assertEquals(install_targets, expected_targets) + + def grub_btrfs_test(self): + """Test installing GRUB to a MBR stage1 and btrfs RAID stage2""" + + # Test stage1 on sda (MBR), stage2 on btrfs /boot RAID + # install_targets should return two grub installs, one for each disk + # in the btrfs volume. stage1 will be the disk, stage2 will be the + # btrfs subvolume. + self.grub.stage1_device = self.sda + self.grub.stage2_device = self.boot_btrfs + + install_targets = set(self.grub.install_targets) + expected_targets = set([(self.sda, self.boot_btrfs), (self.sdb, self.boot_btrfs)]) + + self.assertEquals(install_targets, expected_targets) + + def grub_partition_btrfs_test(self): + """Test installing GRUB to a partition stage1 and MBR stage2""" + + # Test stage1 on sda1 (biosboot), stage2 on btrfs /boot RAID + # since stage1 is a non-raid partition, install_targets should return + # the original (stage1, stage2) and not add any targets + self.grub.stage1_device = self.sda1 + self.grub.stage2_device = self.boot_btrfs + + install_targets = set(self.grub.install_targets) + expected_targets = set([(self.sda1, self.boot_btrfs)]) + + self.assertEquals(install_targets, expected_targets) diff --git a/anaconda/tests/pyanaconda_tests/iutil_test.py b/anaconda/tests/pyanaconda_tests/iutil_test.py index 957cd16..e6c3afd 100644 --- a/anaconda/tests/pyanaconda_tests/iutil_test.py +++ b/anaconda/tests/pyanaconda_tests/iutil_test.py @@ -21,11 +21,14 @@ from pyanaconda import iutil import unittest -import types import os +import tempfile +import signal import shutil from test_constants import ANACONDA_TEST_DIR +from timer import timer + class UpcaseFirstLetterTests(unittest.TestCase): def setUp(self): @@ -90,6 +93,25 @@ class RunProgramTests(unittest.TestCase): # check no output is returned self.assertEqual(len(iutil.execWithCapture('true', [])), 0) + def exec_with_capture_no_stderr_test(self): + """Test execWithCapture with no stderr""" + + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "output" +echo "error" >&2 +""") + testscript.flush() + + # check that only the output is captured + self.assertEqual( + iutil.execWithCapture("/bin/sh", [testscript.name], filter_stderr=True), + "output\n") + + # check that both output and error are captured + self.assertEqual(iutil.execWithCapture("/bin/sh", [testscript.name]), + "output\nerror\n") + def exec_readlines_test(self): """Test execReadlines.""" @@ -99,13 +121,290 @@ class RunProgramTests(unittest.TestCase): # test some lines are returned self.assertGreater(len(list(iutil.execReadlines("ls", ["--help"]))), 0) - # check that it always returns a generator for both + # check that it always returns an iterator for both # if there is some output and if there isn't any - self.assertIsInstance(iutil.execReadlines("ls", ["--help"]), - types.GeneratorType) - self.assertIsInstance(iutil.execReadlines("true", []), - types.GeneratorType) + self.assertTrue(hasattr(iutil.execReadlines("ls", ["--help"]), "__iter__")) + self.assertTrue(hasattr(iutil.execReadlines("true", []), "__iter__")) + def exec_readlines_test_normal_output(self): + """Test the output of execReadlines.""" + + # Test regular-looking output + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo "three" +exit 0 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(StopIteration, rl_iterator.next) + + # Test output with no end of line + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo -n "three" +exit 0 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(StopIteration, rl_iterator.next) + + def exec_readlines_test_exits(self): + """Test execReadlines in different child exit situations.""" + + # Tests that exit on signal will raise OSError once output + # has been consumed, otherwise the test will exit normally. + + # Test a normal, non-0 exit + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo "three" +exit 1 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(OSError, rl_iterator.next) + + # Test exit on signal + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo "three" +kill -TERM $$ +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(OSError, rl_iterator.next) + + # Repeat the above two tests, but exit before a final newline + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo -n "three" +exit 1 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(OSError, rl_iterator.next) + + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +echo "two" +echo -n "three" +kill -TERM $$ +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(OSError, rl_iterator.next) + + def exec_readlines_test_signals(self): + """Test execReadlines and signal receipt.""" + + # ignored signal + old_HUP_handler = signal.signal(signal.SIGHUP, signal.SIG_IGN) + try: + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +kill -HUP $PPID +echo "two" +echo -n "three" +exit 0 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(StopIteration, rl_iterator.next) + finally: + signal.signal(signal.SIGHUP, old_HUP_handler) + + # caught signal + def _hup_handler(signum, frame): + pass + old_HUP_handler = signal.signal(signal.SIGHUP, _hup_handler) + try: + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +echo "one" +kill -HUP $PPID +echo "two" +echo -n "three" +exit 0 +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + self.assertEqual(rl_iterator.next(), "one") + self.assertEqual(rl_iterator.next(), "two") + self.assertEqual(rl_iterator.next(), "three") + self.assertRaises(StopIteration, rl_iterator.next) + finally: + signal.signal(signal.SIGHUP, old_HUP_handler) + + def start_program_preexec_fn_test(self): + """Test passing preexec_fn to startProgram.""" + + marker_text = "yo wassup man" + # Create a temporary file that will be written before exec + with tempfile.NamedTemporaryFile() as testfile: + + # Write something to testfile to show this method was run + def preexec(): + # Open a copy of the file here since close_fds has already closed the descriptor + testcopy = open(testfile.name, 'w') + testcopy.write(marker_text) + testcopy.close() + + with timer(5): + # Start a program that does nothing, with a preexec_fn + proc = iutil.startProgram(["/bin/true"], preexec_fn=preexec) + proc.communicate() + + # Rewind testfile and look for the text + testfile.seek(0, os.SEEK_SET) + self.assertEqual(testfile.read(), marker_text) + + def start_program_stdout_test(self): + """Test redirecting stdout with startProgram.""" + + marker_text = "yo wassup man" + # Create a temporary file that will be written by the program + with tempfile.NamedTemporaryFile() as testfile: + # Open a new copy of the file so that the child doesn't close and + # delete the NamedTemporaryFile + stdout = open(testfile.name, 'w') + with timer(5): + proc = iutil.startProgram(["/bin/echo", marker_text], stdout=stdout) + proc.communicate() + + # Rewind testfile and look for the text + testfile.seek(0, os.SEEK_SET) + self.assertEqual(testfile.read().strip(), marker_text) + + def start_program_reset_handlers_test(self): + """Test the reset_handlers parameter of startProgram.""" + + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +# Just hang out and do nothing, forever +while true ; do sleep 1 ; done +""") + testscript.flush() + + # Start a program with reset_handlers + proc = iutil.startProgram(["/bin/sh", testscript.name]) + + with timer(5): + # Kill with SIGPIPE and check that the python's SIG_IGN was not inheritted + # The process should die on the signal. + proc.send_signal(signal.SIGPIPE) + proc.communicate() + self.assertEqual(proc.returncode, -(signal.SIGPIPE)) + + # Start another copy without reset_handlers + proc = iutil.startProgram(["/bin/sh", testscript.name], reset_handlers=False) + + with timer(5): + # Kill with SIGPIPE, then SIGTERM, and make sure SIGTERM was the one + # that worked. + proc.send_signal(signal.SIGPIPE) + proc.terminate() + proc.communicate() + self.assertEqual(proc.returncode, -(signal.SIGTERM)) + + def exec_readlines_auto_kill_test(self): + """Test execReadlines with reading only part of the output""" + + with tempfile.NamedTemporaryFile() as testscript: + testscript.write("""#!/bin/sh +# Output forever +while true; do +echo hey +done +""") + testscript.flush() + + with timer(5): + rl_iterator = iutil.execReadlines("/bin/sh", [testscript.name]) + + # Save the process context + proc = rl_iterator._proc + + # Read two lines worth + self.assertEqual(rl_iterator.next(), "hey") + self.assertEqual(rl_iterator.next(), "hey") + + # Delete the iterator and wait for the process to be killed + del rl_iterator + proc.communicate() + + # Check that the process is gone + self.assertIsNotNone(proc.poll()) + + def watch_process_test(self): + """Test watchProcess""" + + def test_still_running(): + with timer(5): + # Run something forever so we can kill it + proc = iutil.startProgram(["/bin/sh", "-c", "while true; do sleep 1; done"]) + iutil.watchProcess(proc, "test1") + proc.kill() + # Wait for the SIGCHLD + signal.pause() + self.assertRaises(iutil.ExitError, test_still_running) + + # Make sure watchProcess checks that the process has not already exited + with timer(5): + proc = iutil.startProgram(["true"]) + proc.communicate() + self.assertRaises(iutil.ExitError, iutil.watchProcess, proc, "test2") + +class MiscTests(unittest.TestCase): def get_dir_size_test(self): """Test the getDirSize.""" @@ -219,11 +518,16 @@ class RunProgramTests(unittest.TestCase): def raise_os_error(*args, **kwargs): raise OSError - # chvt does not exist on all platforms - # and the function needs to correctly survie that - iutil.vtActivate.func_globals['execWithRedirect'] = raise_os_error + _execWithRedirect = iutil.vtActivate.func_globals['execWithRedirect'] - self.assertEqual(iutil.vtActivate(2), False) + try: + # chvt does not exist on all platforms + # and the function needs to correctly survie that + iutil.vtActivate.func_globals['execWithRedirect'] = raise_os_error + + self.assertEqual(iutil.vtActivate(2), False) + finally: + iutil.vtActivate.func_globals['execWithRedirect'] = _execWithRedirect def get_deep_attr_test(self): """Test getdeepattr.""" @@ -438,3 +742,12 @@ class RunProgramTests(unittest.TestCase): # Compare unicode and str and make sure nothing crashes self.assertTrue(iutil.have_word_match("fête", u"fête champêtre")) self.assertTrue(iutil.have_word_match(u"fête", "fête champêtre")) + + def parent_dir_test(self): + """Test the parent_dir function""" + dirs = [("", ""), ("/", ""), ("/home/", ""), ("/home/bcl", "/home"), ("home/bcl", "home"), + ("/home/bcl/", "/home"), ("/home/extra/bcl", "/home/extra"), + ("/home/extra/bcl/", "/home/extra"), ("/home/extra/../bcl/", "/home")] + + for d, r in dirs: + self.assertEquals(iutil.parent_dir(d), r) diff --git a/anaconda/tests/kickstart_tests/version_test.py b/anaconda/tests/pyanaconda_tests/ks_version_test.py similarity index 95% rename from anaconda/tests/kickstart_tests/version_test.py rename to anaconda/tests/pyanaconda_tests/ks_version_test.py index 84fa384..26ab04a 100644 --- a/anaconda/tests/kickstart_tests/version_test.py +++ b/anaconda/tests/pyanaconda_tests/ks_version_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2013 Red Hat, Inc. # @@ -40,7 +40,7 @@ class BaseTestCase(unittest.TestCase): class CommandVersionTestCase(BaseTestCase): def commands_test(self): """Test that anaconda uses the right versions of kickstart commands""" - for (commandName, commandObj) in self._commandMap.iteritems(): + for (commandName, commandObj) in self._commandMap.items(): pykickstartClass = self.handler.commands[commandName].__class__ self.assertIsInstance(commandObj(), pykickstartClass) @@ -48,7 +48,7 @@ class CommandVersionTestCase(BaseTestCase): class DataVersionTestCase(BaseTestCase): def data_test(self): """Test that anaconda uses the right versions of kickstart data""" - for (dataName, dataObj) in self._dataMap.iteritems(): + for (dataName, dataObj) in self._dataMap.items(): # pykickstart does not expose data objects as a mapping the way # it does command objects. pykickstartClass = eval("self.handler.%s" % dataName) diff --git a/anaconda/tests/pyanaconda_tests/localization_test.py b/anaconda/tests/pyanaconda_tests/localization_test.py index 521fb4a..34672b2 100644 --- a/anaconda/tests/pyanaconda_tests/localization_test.py +++ b/anaconda/tests/pyanaconda_tests/localization_test.py @@ -153,6 +153,6 @@ class LangcodeLocaleMatchingTests(unittest.TestCase): # cannot set locale (a bug in the locale module?) continue - (order, formats) = localization.resolve_date_format(1, 2, 3, fail_safe=False) + order = localization.resolve_date_format(1, 2, 3, fail_safe=False)[0] for i in (1, 2, 3): self.assertIn(i, order) diff --git a/anaconda/tests/pyanaconda_tests/pwpolicy.py b/anaconda/tests/pyanaconda_tests/pwpolicy.py new file mode 100644 index 0000000..54dda51 --- /dev/null +++ b/anaconda/tests/pyanaconda_tests/pwpolicy.py @@ -0,0 +1,51 @@ +# +# Brian C. Lane <bcl@redhat.com> +# +# Copyright 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2. This program is distributed in the hope that it +# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the +# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat +# trademarks that are incorporated in the source code or documentation are not +# subject to the GNU General Public License and may only be used or replicated +# with the express permission of Red Hat, Inc. +# +from mock import Mock +import unittest + +class BaseTestCase(unittest.TestCase): + def setUp(self): + import sys + + sys.modules["anaconda_log"] = Mock() + sys.modules["block"] = Mock() + + from pyanaconda import kickstart + self.kickstart = kickstart + self.handler = kickstart.AnacondaKSHandler() + self.ksparser = kickstart.AnacondaKSParser(self.handler) + +class PwPolicyTestCase(BaseTestCase): + ks = """ +%anaconda +pwpolicy root --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy user --strict --minlen=8 --minquality=50 --nochanges --emptyok +pwpolicy luks --strict --minlen=8 --minquality=50 --nochanges --emptyok +%end +""" + def pwpolicy_test(self): + self.ksparser.readKickstartFromString(self.ks) + + self.assertIsInstance(self.handler, self.kickstart.AnacondaKSHandler) + self.assertIsInstance(self.handler.anaconda, self.kickstart.AnacondaSectionHandler) + + eq_template = "pwpolicy %s --minlen=8 --minquality=50 --strict --nochanges --emptyok\n" + for name in ["root", "user", "luks"]: + self.assertEqual(str(self.handler.anaconda.pwpolicy.get_policy(name)), eq_template % name) # pylint: disable=no-member diff --git a/anaconda/tests/pyanaconda_tests/timezone_test.py b/anaconda/tests/pyanaconda_tests/timezone_test.py index 26084d5..00c1520 100644 --- a/anaconda/tests/pyanaconda_tests/timezone_test.py +++ b/anaconda/tests/pyanaconda_tests/timezone_test.py @@ -25,7 +25,7 @@ import mock class TimezonesListings(unittest.TestCase): def string_timezones_test(self): """Check if returned timezones are plain strings, not unicode objects.""" - for (region, zones) in timezone.get_all_regions_and_timezones().iteritems(): + for (region, zones) in timezone.get_all_regions_and_timezones().items(): self.assertIsInstance(region, str) for zone in zones: @@ -34,7 +34,7 @@ class TimezonesListings(unittest.TestCase): def all_timezones_valid_test(self): """Check if all returned timezones are considered valid timezones.""" - for (region, zones) in timezone.get_all_regions_and_timezones().iteritems(): + for (region, zones) in timezone.get_all_regions_and_timezones().items(): for zone in zones: self.assertTrue(timezone.is_valid_timezone(region + "/" + zone)) diff --git a/anaconda/tests/pylint/environ.py b/anaconda/tests/pylint/environ.py new file mode 100644 index 0000000..ffbed49 --- /dev/null +++ b/anaconda/tests/pylint/environ.py @@ -0,0 +1,107 @@ +# setenv pylint module +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): David Shea <dshea@redhat.com> +# + +import astroid + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages, safe_infer +from pylint.interfaces import IAstroidChecker + +import os + +class EnvironChecker(BaseChecker): + __implements__ = (IAstroidChecker,) + name = "environ" + msgs = {"W9940" : ("Found potentially unsafe modification of environment", + "environment-modify", + "Potentially thread-unsafe modification of environment")} + + def _is_environ(self, node): + # Guess whether a node being modified is os.environ + + if isinstance(node, astroid.Getattr): + if node.attrname == "environ": + expr_node = safe_infer(node.expr) + if isinstance(expr_node, astroid.Module) and expr_node.name == "os": + return True + + # If the node being modified is just "environ" assume that it's os.environ + if isinstance(node, astroid.Name): + if node.name == "environ": + return True + + return False + + @check_messages("environment-modify") + def visit_assign(self, node): + if not isinstance(node, astroid.Assign): + return + + # Look for os.environ["WHATEVER"] = something + for target in node.targets: + if not isinstance(target, astroid.Subscript): + continue + + if self._is_environ(target.value): + self.add_message("environment-modify", node=node) + + @check_messages("environment-modify") + def visit_callfunc(self, node): + # Check both for uses of os.putenv and os.setenv and modifying calls + # to the os.environ object, such as os.environ.update + + if not isinstance(node, astroid.CallFunc): + return + + function_node = safe_infer(node.func) + if not isinstance(function_node, (astroid.Function, astroid.BoundMethod)): + return + + # If the function is from the os or posix modules, look for calls that + # modify the environment + if function_node.root().name in ("os", os.name) and \ + function_node.name in ("putenv", "unsetenv"): + self.add_message("environment-modify", node=node) + + # Look for methods bound to the environ dict + if isinstance(function_node, astroid.BoundMethod) and \ + isinstance(function_node.bound, astroid.Dict) and \ + function_node.bound.root().name in ("os", os.name) and \ + function_node.bound.name == "environ" and \ + function_node.name in ("clear", "pop", "popitem", "setdefault", "update"): + self.add_message("environment-modify", node=node) + + @check_messages("environment-modify") + def visit_delete(self, node): + if not isinstance(node, astroid.Delete): + return + + # Look for del os.environ["WHATEVER"] + for target in node.targets: + if not isinstance(target, astroid.Subscript): + continue + + if self._is_environ(target.value): + self.add_message("environment-modify", node=node) + +def register(linter): + """required method to auto register this checker """ + linter.register_checker(EnvironChecker(linter)) diff --git a/anaconda/tests/pylint/intl.py b/anaconda/tests/pylint/intl.py index 4a2a84d..ac27bb6 100644 --- a/anaconda/tests/pylint/intl.py +++ b/anaconda/tests/pylint/intl.py @@ -129,8 +129,6 @@ class IntlStringFormatChecker(StringFormatChecker): string format messages extended for translated strings") } - options = () - @check_messages('translated-format') def visit_binop(self, node): if node.op != '%': diff --git a/anaconda/tests/pylint/pointless-override.py b/anaconda/tests/pylint/pointless-override.py new file mode 100644 index 0000000..c8d4776 --- /dev/null +++ b/anaconda/tests/pylint/pointless-override.py @@ -0,0 +1,252 @@ +# Pylint checker for pointless class attributes overrides. +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern <amulhern@redhat.com> +# + +import abc + +from six import add_metaclass + +import astroid + +from pylint.checkers import BaseChecker +from pylint.checkers.utils import check_messages +from pylint.interfaces import IAstroidChecker + +@add_metaclass(abc.ABCMeta) +class PointlessData(object): + + _DEF_CLASS = abc.abstractproperty(doc="Class of interesting definitions.") + message_id = abc.abstractproperty(doc="Pylint message identifier.") + + @classmethod + @abc.abstractmethod + def _retain_node(cls, node, restrict=True): + """ Determines whether to retain a node for the analysis. + + :param node: an AST node + :type node: astroid.Class + :param restrict bool: True if results returned should be restricted + :returns: True if the node should be kept, otherwise False + :rtype: bool + + Restricted nodes are candidates for being marked as overridden. + Only restricted nodes are put into the initial pool of candidates. + """ + raise NotImplementedError() + + @staticmethod + @abc.abstractmethod + def _extract_value(node): + """ Return the node that contains the assignment's value. + + :param node: an AST node + :type node: astroid.Class + :returns: the node corresponding to the value + :rtype: bool + """ + raise NotImplementedError() + + @staticmethod + @abc.abstractmethod + def _extract_targets(node): + """ Generates the names being assigned to. + + :param node: an AST node + :type node: astroid.Class + :returns: a list of assignment target names + :rtype: generator of str + """ + raise NotImplementedError() + + @classmethod + def get_data(cls, node, restrict=True): + """ Find relevant nodes for this analysis. + + :param node: an AST node + :type node: astroid.Class + :param restrict bool: True if results returned should be restricted + + :rtype: generator of astroid.Class + :returns: a generator of interesting nodes. + + Note that all nodes returned are guaranteed to be instances of + some class in self._DEF_CLASS. + """ + nodes = (n for n in node.body if isinstance(n, cls._DEF_CLASS)) + for n in nodes: + if cls._retain_node(n, restrict): + for name in cls._extract_targets(n): + yield (name, cls._extract_value(n)) + + @classmethod + @abc.abstractmethod + def check_equal(cls, node, other): + """ Check whether the two nodes are considered equal. + + :param node: some ast node + :param other: some ast node + + :rtype: bool + :returns: True if the nodes are considered equal, otherwise False + + If the method returns True, the nodes are actually equal, but it + may return False when the nodes are equal. + """ + raise NotImplementedError() + +class PointlessFunctionDefinition(PointlessData): + """ Looking for pointless function definitions. """ + + _DEF_CLASS = astroid.Function + message_id = "W9952" + + @classmethod + def _retain_node(cls, node, restrict=True): + return not restrict or \ + (len(node.body) == 1 and isinstance(node.body[0], astroid.Pass)) + + @classmethod + def check_equal(cls, node, other): + return len(node.body) == 1 and isinstance(node.body[0], astroid.Pass) and \ + len(other.body) == 1 and isinstance(other.body[0], astroid.Pass) + + @staticmethod + def _extract_value(node): + return node + + @staticmethod + def _extract_targets(node): + yield node.name + +class PointlessAssignment(PointlessData): + + _DEF_CLASS = astroid.Assign + message_id = "W9951" + + _VALUE_CLASSES = ( + astroid.Const, + astroid.Dict, + astroid.List, + astroid.Tuple + ) + + @classmethod + def _retain_node(cls, node, restrict=True): + return not restrict or isinstance(node.value, cls._VALUE_CLASSES) + + @classmethod + def check_equal(cls, node, other): + if type(node) != type(other): + return False + if isinstance(node, astroid.Const): + return node.value == other.value + if isinstance(node, (astroid.List, astroid.Tuple)): + return len(node.elts) == len(other.elts) and \ + all(cls.check_equal(n, o) for (n, o) in zip(node.elts, other.elts)) + if isinstance(node, astroid.Dict): + return len(node.items) == len(other.items) + return False + + @staticmethod + def _extract_value(node): + return node.value + + @staticmethod + def _extract_targets(node): + for target in node.targets: + yield target.name + +class PointlessClassAttributeOverrideChecker(BaseChecker): + """ If the nearest definition of the class attribute in the MRO assigns + it the same value, then the overriding definition is said to be + pointless. + + The algorithm for detecting a pointless attribute override is the following. + + * For each class, C: + - For each attribute assignment, + name_1 = name_2 ... name_n = l (where l is a literal): + * For each n in (n_1, n_2): + - Traverse the linearization of the MRO until the first + matching assignment n = l' is identified. If l is equal to l', + then consider that the assignment to l in C is a + pointless override. + + The algorithm for detecting a pointless method override has the same + general structure, and the same defects discussed below. + + Note that this analysis is neither sound nor complete. It is unsound + under multiple inheritance. Consider the following class hierarchy:: + + class A(object): + _attrib = False + + class B(A): + _attrib = False + + class C(A): + _attrib = True + + class D(B,C): + pass + + In this case, starting from B, B._attrib = False would be considered + pointless. However, for D the MRO is B, C, A, and removing the assignment + B._attrib = False would change the inherited value of D._attrib from + False to True. + + The analysis is incomplete because it will find some values unequal when + actually they are equal. + + The analysis is both incomplete and unsound because it expects that + assignments will always be made by means of the same syntax. + """ + + __implements__ = (IAstroidChecker,) + + name = "pointless class attribute override checker" + msgs = { + "W9951": + ( + "Assignment to class attribute %s overrides identical assignment in ancestor.", + "pointless-class-attribute-override", + "Assignment to class attribute that overrides assignment in ancestor that assigns identical value has no effect." + ), + "W9952": + ( + "definition of %s method overrides identical method definition in ancestor", + "pointless-method-definition-override", + "Overriding empty method definition with another empty method definition has no effect." + ) + } + + @check_messages("W9951", "W9952") + def visit_class(self, node): + for checker in (PointlessAssignment, PointlessFunctionDefinition): + for (name, value) in checker.get_data(node): + for a in node.ancestors(): + match = next((v for (n, v) in checker.get_data(a, False) if n == name), None) + if match is not None: + if checker.check_equal(value, match): + self.add_message(checker.message_id, node=value, args=(name,)) + break + +def register(linter): + linter.register_checker(PointlessClassAttributeOverrideChecker(linter)) diff --git a/anaconda/tests/pylint/pylint-false-positives b/anaconda/tests/pylint/pylint-false-positives index bc020fd..ef96849 100644 --- a/anaconda/tests/pylint/pylint-false-positives +++ b/anaconda/tests/pylint/pylint-false-positives @@ -1,7 +1,5 @@ ^E1101:[ 0-9]*,[0-9]*:.*: Instance of '.*' has no 'get_property' member$ ^E1101:[ 0-9]*,[0-9]*:.*: Instance of '.*' has no 'set_property' member$ -^E0611:[ 0-9]*,[0-9]*: No name 'GLib' in module 'gi.repository'$ -^E0611:[ 0-9]*,[0-9]*:.*: No name 'GLib' in module 'gi.repository'$ ^E0611:[ 0-9]*,[0-9]*: No name '_isys' in module 'pyanaconda'$ ^E0611:[ 0-9]*,[0-9]*:.*: No name '_isys' in module 'pyanaconda'$ ^E0712:[ 0-9]*,[0-9]*:.*: Catching an exception which doesn't inherit from BaseException: GError$ diff --git a/anaconda/tests/pylint/pylint-one.sh b/anaconda/tests/pylint/pylint-one.sh index 4b155b8..8a88f59 100644 --- a/anaconda/tests/pylint/pylint-one.sh +++ b/anaconda/tests/pylint/pylint-one.sh @@ -12,20 +12,20 @@ if grep -q '# pylint: skip-file' $1; then exit 0 fi -file_suffix="$(eval echo \$$#|sed s?/?_?g)" - pylint_output="$(pylint \ --msg-template='{msg_id}:{line:3d},{column}: {obj}: {msg}' \ -r n --disable=C,R --rcfile=/dev/null \ --dummy-variables-rgx=_ \ --ignored-classes=DefaultInstall,Popen,QueueFactory,TransactionSet \ --defining-attr-methods=__init__,_grabObjects,initialize,reset,start,setUp \ - --load-plugins=intl,preconf,markup,eintr \ + --load-plugins=intl,preconf,markup,eintr,pointless-override,environ \ + --init-import=y \ --init-hook=\ 'import gi.overrides, os; gi.overrides.__path__[0:0] = (os.environ["ANACONDA_WIDGETS_OVERRIDES"].split(":") if "ANACONDA_WIDGETS_OVERRIDES" in os.environ else [])' \ $DISABLED_WARN_OPTIONS \ $DISABLED_ERR_OPTIONS \ + $EXTRA_OPTIONS \ $NON_STRICT_OPTIONS "$@" 2>&1 | \ egrep -v -f "$FALSE_POSITIVES" \ )" @@ -33,6 +33,8 @@ gi.overrides.__path__[0:0] = (os.environ["ANACONDA_WIDGETS_OVERRIDES"].split(":" if [ -n "$(echo "$pylint_output" | fgrep -v '************* Module ')" ]; then # Replace the Module line with the actual filename pylint_output="$(echo "$pylint_output" | sed "s|\* Module .*|* Module $(eval echo \$$#)|")" - echo "$pylint_output" > pylint-out_$file_suffix - touch "pylint-$file_suffix-failed" + echo "$pylint_output" + exit 1 +else + exit 0 fi diff --git a/anaconda/tests/pylint/runpylint.sh b/anaconda/tests/pylint/runpylint.sh index 3615052..936fa5f 100755 --- a/anaconda/tests/pylint/runpylint.sh +++ b/anaconda/tests/pylint/runpylint.sh @@ -8,6 +8,11 @@ # to stdout and this script will exit with a status of 1, if no (non filtered) # warnings are found it exits with a status of 0 +if ! type parallel 2>&1 > /dev/null; then + echo "parallel must be installed" + exit 99 +fi + # XDG_RUNTIME_DIR is "required" to be set, so make one up in case something # actually tries to do something with it if [ -z "$XDG_RUNTIME_DIR" ]; then @@ -72,6 +77,10 @@ export DISABLED_ERR_OPTIONS="--disable=E1103" # I0013 - Ignoring entire file (i.e., pylint: skip-file) export DISABLED_WARN_OPTIONS="--disable=W0110,W0123,W0141,W0142,W0511,W0603,W0613,W0614,I0011,I0012,I0013" +# string is about half-deprecated, so add it to the list of deprecated modules +# and handle the valid cases with pragma comments. +export EXTRA_OPTIONS="--deprecated-modules=string,regsub,TERMIOS,Bastion,rexec" + usage () { echo "usage: `basename $0` [--strict] [--help] [files...]" exit $1 @@ -107,34 +116,20 @@ fi # run pylint one file / module at a time, otherwise it sometimes gets # confused if [ -z "$FILES" ]; then - # Test any file that either ends in .py or contains #!/usr/bin/python in + # Test any file that either ends in .py or contains #!/usr/bin/python2 in # the first line. Scan everything except old_tests FILES=$(findtestfiles \( -name '*.py' -o \ - -exec /bin/sh -c "head -1 {} | grep -q '#!/usr/bin/python'" \; \) -print | \ + -exec /bin/sh -c "head -1 {} | grep -q '#!/usr/bin/python2'" \; \) -print | \ egrep -v '(|/)old_tests/') fi -num_cpus=$(getconf _NPROCESSORS_ONLN) # run pylint in paralel -echo $FILES | xargs --max-procs=$num_cpus -n 1 "$srcdir"/pylint-one.sh $ARGS || exit 1 +output=$(echo -n $FILES | parallel --no-notice -d' ' --gnu "$srcdir"/pylint-one.sh $ARGS {}) +exit_status=$? -for file in $(find -name 'pylint-out*'); do - cat "$file" >> pylint-log - rm "$file" -done - -fails=$(find -name 'pylint*failed' -print -exec rm '{}' \;) -if [ -z "$fails" ]; then - exit_status=0 -else - exit_status=1 -fi - -if [ -s pylint-log ]; then +if [ "$output" != '\n' -a "$output" != "" ]; then echo "pylint reports the following issues:" - cat pylint-log -elif [ -e pylint-log ]; then - rm pylint-log + echo "$output" | tee pylint-log fi exit "$exit_status" diff --git a/anaconda/tests/regex_tests/groupparse_test.py b/anaconda/tests/regex_tests/groupparse_test.py index 6d5b793..80e7187 100644 --- a/anaconda/tests/regex_tests/groupparse_test.py +++ b/anaconda/tests/regex_tests/groupparse_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2010-2013 Red Hat, Inc. # @@ -20,6 +20,7 @@ # import unittest +from regexcheck import regex_group from pyanaconda.regexes import GROUPLIST_FANCY_PARSE class GroupParseTestCase(unittest.TestCase): @@ -49,13 +50,5 @@ class GroupParseTestCase(unittest.TestCase): ("", ("", None)), ] - got_error = False - for group, result in tests: - try: - self.assertEqual(GROUPLIST_FANCY_PARSE.match(group).groups(), result) - except AssertionError: - got_error = True - print("Group parse error: `%s' did not not parse as `%s'" % (group, result)) - - if got_error: + if not regex_group(GROUPLIST_FANCY_PARSE, tests): self.fail() diff --git a/anaconda/tests/regex_tests/hostname_test.py b/anaconda/tests/regex_tests/hostname_test.py index fa2bfc6..df04327 100644 --- a/anaconda/tests/regex_tests/hostname_test.py +++ b/anaconda/tests/regex_tests/hostname_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # vim:set fileencoding=utf-8 # # Copyright (C) 2014 Red Hat, Inc. @@ -22,28 +22,10 @@ import unittest import re +from regexcheck import regex_match from pyanaconda.regexes import HOSTNAME_PATTERN_WITHOUT_ANCHORS, IPV4_PATTERN_WITHOUT_ANCHORS,\ IPV6_PATTERN_WITHOUT_ANCHORS -def _run_tests(testcase, expression, goodlist, badlist): - got_error = False - for good in goodlist: - try: - testcase.assertIsNotNone(expression.match(good)) - except AssertionError: - got_error = True - print("Good string %s did not match expression" % good) - - for bad in badlist: - try: - testcase.assertIsNone(expression.match(bad)) - except AssertionError: - got_error = True - print("Bad string %s matched expression" % bad) - - if got_error: - testcase.fail() - class HostnameRegexTestCase(unittest.TestCase): def hostname_test(self): good_tests = [ @@ -76,7 +58,8 @@ class HostnameRegexTestCase(unittest.TestCase): ] hostname_re = re.compile('^' + HOSTNAME_PATTERN_WITHOUT_ANCHORS + '$') - _run_tests(self, hostname_re, good_tests, bad_tests) + if not regex_match(hostname_re, good_tests, bad_tests): + self.fail() class IPv4RegexTestCase(unittest.TestCase): def ipv4_test(self): @@ -99,7 +82,8 @@ class IPv4RegexTestCase(unittest.TestCase): ] ipv4_re = re.compile('^(' + IPV4_PATTERN_WITHOUT_ANCHORS + ')$') - _run_tests(self, ipv4_re, good_tests, bad_tests) + if not regex_match(ipv4_re, good_tests, bad_tests): + self.fail() class IPv6RegexTestCase(unittest.TestCase): def ipv6_test(self): @@ -187,4 +171,5 @@ class IPv6RegexTestCase(unittest.TestCase): ] ipv6_re = re.compile('^(' + IPV6_PATTERN_WITHOUT_ANCHORS + ')$') - _run_tests(self, ipv6_re, good_tests, bad_tests) + if not regex_match(ipv6_re, good_tests, bad_tests): + self.fail() diff --git a/anaconda/tests/regex_tests/repo_name_test.py b/anaconda/tests/regex_tests/repo_name_test.py index 10f55a8..76ac0f9 100644 --- a/anaconda/tests/regex_tests/repo_name_test.py +++ b/anaconda/tests/regex_tests/repo_name_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # vim:set fileencoding=utf-8 # # Copyright (C) 2014 Red Hat, Inc. @@ -21,27 +21,9 @@ # import unittest +from regexcheck import regex_match from pyanaconda.regexes import REPO_NAME_VALID -def _run_tests(testcase, expression, goodlist, badlist): - got_error = False - for good in goodlist: - try: - testcase.assertIsNotNone(expression.match(good)) - except AssertionError: - got_error = True - print("Good string %s did not match expression" % good) - - for bad in badlist: - try: - testcase.assertIsNone(expression.match(bad)) - except AssertionError: - got_error = True - print("Bad string %s matched expression" % bad) - - if got_error: - testcase.fail() - class RepoNameTestCase(unittest.TestCase): def reponame_test(self): good_tests = [ @@ -63,4 +45,5 @@ class RepoNameTestCase(unittest.TestCase): '[reponame]' ] - _run_tests(self, REPO_NAME_VALID, good_tests, bad_tests) + if not regex_match(REPO_NAME_VALID, good_tests, bad_tests): + self.fail() diff --git a/anaconda/tests/regex_tests/url_test.py b/anaconda/tests/regex_tests/url_test.py index 3c77f7d..0f40ea2 100644 --- a/anaconda/tests/regex_tests/url_test.py +++ b/anaconda/tests/regex_tests/url_test.py @@ -21,6 +21,7 @@ import unittest +from regexcheck import regex_group from pyanaconda.regexes import URL_PARSE class URLRegexTestCase(unittest.TestCase): @@ -165,19 +166,5 @@ class URLRegexTestCase(unittest.TestCase): ] - got_error = False - for proxy, result in tests: - match = URL_PARSE.match(proxy) - if match: - match= match.groups() - else: - match = None - - try: - self.assertEqual(match, result) - except AssertionError: - got_error = True - print("Proxy parse error: `%s' did not parse as `%s': %s" % (proxy, result, match)) - - if got_error: + if not regex_group(URL_PARSE, tests): self.fail() diff --git a/anaconda/tests/regex_tests/username_test.py b/anaconda/tests/regex_tests/username_test.py index 0c0d5d2..b205945 100644 --- a/anaconda/tests/regex_tests/username_test.py +++ b/anaconda/tests/regex_tests/username_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # vim:set fileencoding=utf-8 # # Copyright (C) 2010-2013 Red Hat, Inc. @@ -21,28 +21,10 @@ # import unittest +from regexcheck import regex_match from pyanaconda.regexes import GECOS_VALID, USERNAME_VALID, GROUPNAME_VALID, GROUPLIST_SIMPLE_VALID class UsernameRegexTestCase(unittest.TestCase): - def _run_tests(self, expression, goodlist, badlist): - got_error = False - for good in goodlist: - try: - self.assertIsNotNone(expression.match(good)) - except AssertionError: - got_error = True - print("Good string %s did not match expression" % good) - - for bad in badlist: - try: - self.assertIsNone(expression.match(bad)) - except AssertionError: - got_error = True - print("Bad string %s matched expression" % bad) - - if got_error: - self.fail() - def gecos_test(self): """Test a list of possible Full Name values.""" # These are valid full names @@ -59,7 +41,8 @@ class UsernameRegexTestCase(unittest.TestCase): # These are invalid full names bad_tests = ['George:Burdell'] - self._run_tests(GECOS_VALID, good_tests, bad_tests) + if not regex_match(GECOS_VALID, good_tests, bad_tests): + self.fail() def username_test(self): """Test a list of possible username values.""" @@ -94,10 +77,12 @@ class UsernameRegexTestCase(unittest.TestCase): '-' ] - self._run_tests(USERNAME_VALID, good_tests, bad_tests) + if not regex_match(USERNAME_VALID, good_tests, bad_tests): + self.fail() # The group name checks for the same thing as the user name - self._run_tests(GROUPNAME_VALID, good_tests, bad_tests) + if not regex_match(GROUPNAME_VALID, good_tests, bad_tests): + self.fail() def grouplist_simple_test(self): good_tests = [ @@ -129,4 +114,5 @@ class UsernameRegexTestCase(unittest.TestCase): 'gburdell, wheel,' ] - self._run_tests(GROUPLIST_SIMPLE_VALID, good_tests, bad_tests) + if not regex_match(GROUPLIST_SIMPLE_VALID, good_tests, bad_tests): + self.fail() diff --git a/anaconda/tests/storage/cases/__init__.py b/anaconda/tests/storage/cases/__init__.py index 87882a9..74b9467 100644 --- a/anaconda/tests/storage/cases/__init__.py +++ b/anaconda/tests/storage/cases/__init__.py @@ -1,6 +1,6 @@ -#!/usr/bin/python +#!/usr/bin/python2 # -# Copyright (C) 2013 Red Hat, Inc. +# Copyright (C) 2013-2014 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published @@ -17,6 +17,8 @@ # # Author: Chris Lumens <clumens@redhat.com> +from __future__ import print_function + import logging import os, sys import re @@ -27,6 +29,7 @@ blivet.util.set_up_logging() blivet_log = logging.getLogger("blivet") blivet_log.info(sys.argv[0]) +from pyanaconda.bootloader import BootLoaderError from pyanaconda.installclass import DefaultInstall from pyanaconda.kickstart import AnacondaKSHandler, AnacondaKSParser, doKickstartStorage from pykickstart.errors import KickstartError @@ -45,7 +48,6 @@ class TestCase(object): Class attributes: - components -- A list of TestCaseComponent classes. desc -- A description of what this test is supposed to be testing. name -- An identifying string given to this TestCase. @@ -54,13 +56,12 @@ class TestCase(object): matching platforms. If the list is empty, it is assumed to be valid for all platforms. """ - components = [] desc = "" name = "" platforms = [] def __init__(self): - pass + self.components = [] def run(self): """Iterate over all components, running each, and collecting the @@ -70,25 +71,23 @@ class TestCase(object): failures = 0 if self.platforms and blivet.platform.getPlatform().__class__.__name__ not in self.platforms: - print("Test %s skipped: not valid for this platform" % self.name) + print("Test %s skipped: not valid for this platform" % self.name, file=sys.stderr) return - for c in self.components: - obj = c() - + for obj in self.components: try: obj._run() except FailedTest as e: - print("Test %s-%s failed:\n\tExpected: %s\n\tGot: %s" % (self.name, obj.name, e.expected, e.got)) + print("Test %s-%s failed:\n\tExpected: %s\n\tGot: %s" % (self.name, obj.name, e.expected, e.got), file=sys.stderr) failures += 1 continue - print("Test %s-%s succeeded" % (self.name, obj.name)) + print("Test %s-%s succeeded" % (self.name, obj.name), file=sys.stderr) successes += 1 - print("Test %s summary:" % self.name) - print("\tSuccesses: %s" % successes) - print("\tFailures: %s" % failures) + print("Test %s summary:" % self.name, file=sys.stderr) + print("\tSuccesses: %s" % successes, file=sys.stderr) + print("\tFailures: %s\n" % failures, file=sys.stderr) return failures class TestCaseComponent(object): @@ -132,12 +131,12 @@ class TestCaseComponent(object): """ return "" - def setupDisks(self): + def setupDisks(self, ksdata): """Create all disk images given by self.disksToCreate and initialize the storage module. Subclasses may override this method, but they should be sure to call the base method as well. """ - self._blivet = blivet.Blivet() + self._blivet = blivet.Blivet(ksdata=ksdata) # blivet only sets up the bootloader in installer_mode. We don't # want installer_mode, though, because that involves running lots @@ -197,8 +196,6 @@ class TestCaseComponent(object): # Set up disks/blivet. try: - self.setupDisks() - # Parse the kickstart using anaconda's parser, since it has more # advanced error detection. This also requires having storage set # up first. @@ -207,10 +204,13 @@ class TestCaseComponent(object): instClass = DefaultInstall() + self.setupDisks(parser.handler) + doKickstartStorage(self._blivet, parser.handler, instClass) self._blivet.updateKSData() + self._blivet.devicetree.teardownAll() self._blivet.doIt() - except (KickstartError, StorageError) as e: + except (BootLoaderError, KickstartError, StorageError) as e: # anaconda handles expected kickstart errors (like parsing busted # input files) by printing the error and quitting. For testing, an # error might be expected so we should compare the result here with @@ -234,4 +234,78 @@ class TestCaseComponent(object): if self.expectedExceptionType: raise FailedTest(None, self.expectedExceptionType) - return +class ReusableTestCaseComponent(TestCaseComponent): + """A version of TestCaseComponent that does not remove its disk images + after use. In this way, a later TestCaseComponent can reuse them. + This is handy for test cases that need pre-existing partitioning. + + See further comments in ReusingTestCaseComponent. + """ + + def tearDownDisks(self): + # Don't destroy disks here, since a later component will want to + # use them. + self._blivet.devicetree.teardownDiskImages() + +class ReusingTestCaseComponent(TestCaseComponent): + """A version of TestCaseComponent that reuses existing disk images + rather than create its own. It will, however, delete these disk images + after use. + + This class knows which disk images to reuse by the reusedComponents + parameter passed in at object instantiation. This is a list of other + TestCaseComponent instances. This class will reuse all disk images + from each instance in that list, in the order given. + + A typical pipeline of components would thus look like this: + + class ComponentA(ReusableTestCaseComponent): + ... + + class ComponentB(ReusingTestCaseComponent): + ... + + ComponentA -> ComponentB + + A component may also derive from both, if it's in the middle of the + pipeline: + + class ComponentA(ReusableTestCaseComponent): + ... + + class ComponentB(ReusableTestCaseComponent, ReusingTestCaseComponent): + ... + + class ComponentC(ReusingTestCaseComponent): + ... + + ComponentA -> ComponentB -> ComponentC + """ + + def __init__(self, reusedComponents=None): + """Create a new ReusingTestCaseComponent. reusedComponents is a list + of other TestCaseComponent objects that this instance should make + use of. All disk images in that list will be used by this instance, + and all will be cleaned up at the end of the test. + """ + TestCaseComponent.__init__(self) + + if reusedComponents is None: + self._reusedComponents = [] + else: + self._reusedComponents = reusedComponents + + def setupDisks(self, ksdata): + self._blivet = blivet.Blivet(ksdata=ksdata) + + # See comment in super class's method. + from pyanaconda.bootloader import get_bootloader + self._blivet._bootloader = get_bootloader() + + for component in self._reusedComponents: + self._disks.update(component._disks) + + for (name, image) in self._disks.items(): + self._blivet.config.diskImages[name] = image + + self._blivet.reset() diff --git a/anaconda/tests/storage/cases/bz1014545.py b/anaconda/tests/storage/cases/bz1014545.py index 6aa8ba1..680b604 100644 --- a/anaconda/tests/storage/cases/bz1014545.py +++ b/anaconda/tests/storage/cases/bz1014545.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2013-2014 Red Hat, Inc. # @@ -17,6 +17,8 @@ # # Author: Chris Lumens <clumens@redhat.com> +__all__ = ["BZ1014545_TestCase"] + from . import TestCase, TestCaseComponent from blivet.size import Size @@ -27,14 +29,15 @@ class BTRFSOnNonBTRFSComponent(TestCaseComponent): def __init__(self, *args, **kwargs): TestCaseComponent.__init__(self, *args, **kwargs) - self.disksToCreate = [("anatest-disk1", Size("1GiB"))] + self.disksToCreate = [("btrfs-on-non-btrfs-disk1", Size("1GiB"))] @property def ks(self): return """ +bootloader --location=none zerombr clearpart --all --initlabel -btrfs none --data=0 --metadata=1 anatest-disk1 +btrfs none --data=0 --metadata=1 btrfs-on-non-btrfs-disk1 """ @property @@ -43,21 +46,22 @@ btrfs none --data=0 --metadata=1 anatest-disk1 @property def expectedExceptionText(self): - return "BTRFS partition .* has incorrect format" + return "Btrfs partition .* has a format of \"disklabel\", but should have a format of \"btrfs\"" class VolGroupOnNonPVsComponent(TestCaseComponent): name = "VolGroupOnNonPVs" def __init__(self, *args, **kwargs): TestCaseComponent.__init__(self, *args, **kwargs) - self.disksToCreate = [("anatest-disk1", Size("1GiB"))] + self.disksToCreate = [("volgroup-on-non-pv-disk1", Size("1GiB"))] @property def ks(self): return """ +bootloader --location=none zerombr clearpart --all --initlabel -volgroup myvg anatest-disk1 +volgroup myvg volgroup-on-non-pv-disk1 """ @property @@ -66,22 +70,23 @@ volgroup myvg anatest-disk1 @property def expectedExceptionText(self): - return "Physical Volume .* has incorrect format" + return "Physical volume .* has a format of \"disklabel\", but should have a format of \"lvmpv\"" class RaidOnNonRaidMembersComponent(TestCaseComponent): name = "RaidOnNonRaidMembers" def __init__(self, *args, **kwargs): TestCaseComponent.__init__(self, *args, **kwargs) - self.disksToCreate = [("anatest-disk1", Size("1GiB")), - ("anatest-disk2", Size("1GiB"))] + self.disksToCreate = [("raid-on-non-raid-disk1", Size("1GiB")), + ("raid-on-non-raid-disk2", Size("1GiB"))] @property def ks(self): return """ +bootloader --location=none zerombr clearpart --all --initlabel -raid / --level=1 --device=md0 anatest-disk1 anatest-disk2 +raid / --level=1 --device=md0 raid-on-non-raid-disk1 raid-on-non-raid-disk2 """ @property @@ -90,12 +95,9 @@ raid / --level=1 --device=md0 anatest-disk1 anatest-disk2 @property def expectedExceptionText(self): - return "RAID member .* has incorrect format" + return "RAID device .* has a format of \"disklabel\", but should have a format of \"mdmember\"" class BZ1014545_TestCase(TestCase): - components = [BTRFSOnNonBTRFSComponent, - RaidOnNonRaidMembersComponent, - VolGroupOnNonPVsComponent] name = "1014545" desc = """The members of various commands must have the correct format. For instance, raid members must have mdmember, and volgroup members must have @@ -108,3 +110,9 @@ much further in installation - during bootloader installation. The real bug is that this condition should be detected when the kickstart storage commands are being converted to actions. """ + + def __init__(self): + TestCase.__init__(self) + self.components = [BTRFSOnNonBTRFSComponent(), + RaidOnNonRaidMembersComponent(), + VolGroupOnNonPVsComponent()] diff --git a/anaconda/tests/storage/cases/bz1067707.py b/anaconda/tests/storage/cases/bz1067707.py index e49e4fc..29a13b7 100644 --- a/anaconda/tests/storage/cases/bz1067707.py +++ b/anaconda/tests/storage/cases/bz1067707.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # Copyright (C) 2014 Red Hat, Inc. # @@ -17,6 +17,8 @@ # # Author: Chris Lumens <clumens@redhat.com> +__all__ = ["BZ1067707_TestCase"] + from . import TestCase, TestCaseComponent from blivet.size import Size @@ -25,21 +27,25 @@ class SwapWithRecommendedSizeComponent(TestCaseComponent): def __init__(self, *args, **kwargs): TestCaseComponent.__init__(self, *args, **kwargs) - self.disksToCreate = [("anatest-disk1", Size("1GiB"))] + self.disksToCreate = [("recommended-swap-disk1", Size("1GiB"))] @property def ks(self): return """ +bootloader --location=none zerombr clearpart --all --initlabel part swap --recommended """ class BZ1067707_TestCase(TestCase): - components = [SwapWithRecommendedSizeComponent] name = "1067707" desc = """Partition lines do not necessarily require a size to be specified. There are plenty of reasons why a user might not do this: putting a filesystem on a given pre-existing partition, giving a resize or maxsize option, or (in this case) asking for the recommended swap size. """ + + def __init__(self): + TestCase.__init__(self) + self.components = [SwapWithRecommendedSizeComponent()] diff --git a/anaconda/tests/storage/cases/reuse.py b/anaconda/tests/storage/cases/reuse.py new file mode 100644 index 0000000..b0007dd --- /dev/null +++ b/anaconda/tests/storage/cases/reuse.py @@ -0,0 +1,187 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Author: Chris Lumens <clumens@redhat.com> + +__all__ = ["PartitionReuse_TestCase", "LVMReuse_TestCase", "BTRFSReuse_TestCase", "ThinpReuse_TestCase"] + +from . import TestCase, ReusableTestCaseComponent, ReusingTestCaseComponent +from blivet.size import Size + +class FirstPartitionAutopartComponent(ReusableTestCaseComponent): + name = "FirstPartitionAutopart" + + def __init__(self, *args, **kwargs): + ReusableTestCaseComponent.__init__(self, *args, **kwargs) + self.disksToCreate = [("part-autopart-disk1", Size("8GiB"))] + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=plain +""" + +class SecondPartitionAutopartComponent(ReusingTestCaseComponent): + name = "SecondPartitionAutopart" + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=plain +""" + +class PartitionReuse_TestCase(TestCase): + name = "PartitionReuse" + desc = """Test that a disk with pre-existing partitioning as a +result of a previous installation with partition-based autopart works. +""" + + def __init__(self): + TestCase.__init__(self) + first = FirstPartitionAutopartComponent() + second = SecondPartitionAutopartComponent(reusedComponents=[first]) + + self.components = [first, second] + +class FirstLVMAutopartComponent(ReusableTestCaseComponent): + name = "FirstLVMAutopart" + + def __init__(self, *args, **kwargs): + ReusableTestCaseComponent.__init__(self, *args, **kwargs) + self.disksToCreate = [("lvm-autopart-disk1", Size("8GiB"))] + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=lvm +""" + +class SecondLVMAutopartComponent(ReusingTestCaseComponent): + name = "SecondLVMAutopart" + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=lvm +""" + +class LVMReuse_TestCase(TestCase): + name = "LVMReuse" + desc = """Test that a disk with pre-existing LVM partitioning as a +result of a previous installation with LVM-based autopart works. +""" + + def __init__(self): + TestCase.__init__(self) + first = FirstLVMAutopartComponent() + second = SecondLVMAutopartComponent(reusedComponents=[first]) + + self.components = [first, second] + +class FirstBTRFSAutopartComponent(ReusableTestCaseComponent): + name = "FirstBTRFSAutopart" + + def __init__(self, *args, **kwargs): + ReusableTestCaseComponent.__init__(self, *args, **kwargs) + self.disksToCreate = [("btrfs-autopart-disk1", Size("8GiB"))] + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=btrfs +""" + +class SecondBTRFSAutopartComponent(ReusingTestCaseComponent): + name = "SecondBTRFSAutopart" + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=btrfs +""" + +class BTRFSReuse_TestCase(TestCase): + name = "BTRFSReuse" + desc = """Test that a disk with pre-existing BTRFS partitioning as a +result of a previous installation with BTRFS-based autopart works. +""" + + def __init__(self): + TestCase.__init__(self) + first = FirstBTRFSAutopartComponent() + second = SecondBTRFSAutopartComponent(reusedComponents=[first]) + + self.components = [first, second] + +class FirstThinpAutopartComponent(ReusableTestCaseComponent): + name = "FirstThinpAutopart" + + def __init__(self, *args, **kwargs): + ReusableTestCaseComponent.__init__(self, *args, **kwargs) + self.disksToCreate = [("thinp-autopart-disk1", Size("8GiB"))] + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=thinp +""" + +class SecondThinpAutopartComponent(ReusingTestCaseComponent): + name = "SecondThinpAutopart" + + @property + def ks(self): + return """ +bootloader --location=none +zerombr +clearpart --all --initlabel +autopart --type=thinp +""" + +class ThinpReuse_TestCase(TestCase): + name = "ThinpReuse" + desc = """Test that a disk with pre-existing thinp partitioning as a +result of a previous installation with thinp autopart works. +""" + + def __init__(self): + TestCase.__init__(self) + first = FirstThinpAutopartComponent() + second = SecondThinpAutopartComponent(reusedComponents=[first]) + + self.components = [first, second] diff --git a/anaconda/tests/storage/run_storage_tests.py b/anaconda/tests/storage/run_storage_tests.py index ef155e0..be0d199 100644 --- a/anaconda/tests/storage/run_storage_tests.py +++ b/anaconda/tests/storage/run_storage_tests.py @@ -1,17 +1,31 @@ -#!/usr/bin/python +#!/usr/bin/python2 import os, sys +from pyanaconda.ui.common import collect + +# FIXME: Storage tests don't want to work right now, so they are disabled while +# I debug them so we can get useful data from other tests. +os._exit(77) + if os.geteuid() != 0: sys.stderr.write("You must be root to run the storage tests; skipping.\n") # This return code tells the automake test driver that this test was skipped. os._exit(77) -from cases.bz1014545 import BZ1014545_TestCase -from cases.bz1067707 import BZ1067707_TestCase +if "top_srcdir" not in os.environ: + sys.stderr.write("$top_srcdir must be defined in the test environment\n") + # This return code tells the automake test driver that the test setup failed + sys.exit(99) -for tc in [BZ1014545_TestCase(), - BZ1067707_TestCase()]: - failures = tc.run() +failures = 0 + +classes = collect("cases.%s", + os.path.abspath(os.path.join(os.environ["top_srcdir"], "tests/storage/cases/")), + lambda obj: getattr(obj, "desc", None) is not None) + +for tc in classes: + obj = tc() + failures += obj.run() os._exit(failures) diff --git a/anaconda/tests/testenv.sh b/anaconda/tests/testenv.sh index 017559d..cdc7c41 100644 --- a/anaconda/tests/testenv.sh +++ b/anaconda/tests/testenv.sh @@ -20,7 +20,7 @@ else LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${top_builddir}/widgets/src/.libs" fi -ANACONDA_INSTALL_CLASSES="${top_builddir}/pyanaconda/installclasses" +ANACONDA_INSTALL_CLASSES="${top_srcdir}/pyanaconda/installclasses" GI_TYPELIB_PATH="${top_builddir}/widgets/src" export ANACONDA_INSTALL_CLASSES diff --git a/anaconda/utils/dd/dd_extract.c b/anaconda/utils/dd/dd_extract.c index 46c8426..d0a386e 100644 --- a/anaconda/utils/dd/dd_extract.c +++ b/anaconda/utils/dd/dd_extract.c @@ -17,6 +17,9 @@ * Author(s): Martin Sivak <msivak@redhat.com> * Brian C. Lane <bcl@redhat.com> */ + +#include "config.h" + #include <ctype.h> #include <stdio.h> #include <stdlib.h> diff --git a/anaconda/utils/dd/dd_list.c b/anaconda/utils/dd/dd_list.c index 28ce8d5..7412644 100644 --- a/anaconda/utils/dd/dd_list.c +++ b/anaconda/utils/dd/dd_list.c @@ -17,6 +17,9 @@ * Author(s): Martin Sivak <msivak@redhat.com> * Brian C. Lane <bcl@redhat.com> */ + +#include "config.h" + #include <ctype.h> #include <stdio.h> #include <stdlib.h> diff --git a/anaconda/utils/dd/rpmutils.c b/anaconda/utils/dd/rpmutils.c index 7c77973..4f06d25 100644 --- a/anaconda/utils/dd/rpmutils.c +++ b/anaconda/utils/dd/rpmutils.c @@ -22,6 +22,8 @@ * * */ +#include "config.h" + #include <stdlib.h> #include <stdio.h> #include <string.h> diff --git a/anaconda/utils/handle-sshpw b/anaconda/utils/handle-sshpw index 8965572..281efe7 100755 --- a/anaconda/utils/handle-sshpw +++ b/anaconda/utils/handle-sshpw @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2 # # handle-sshpw: Code processing sshpw lines in kickstart files for the # install environment. @@ -26,6 +26,7 @@ import os import sys from pykickstart.parser import KickstartParser from pykickstart.version import makeVersion +from pykickstart.sections import NullSection import pyanaconda.users as users ksfile = '/run/install/ks.cfg' @@ -34,7 +35,10 @@ ksfile = '/run/install/ks.cfg' if not os.path.exists(ksfile): sys.exit() -ksparser = KickstartParser(makeVersion(), missingIncludeIsFatal=False) +handler = makeVersion() +ksparser = KickstartParser(handler, missingIncludeIsFatal=False) +ksparser.registerSection(NullSection(handler, sectionOpen="%addon")) +ksparser.registerSection(NullSection(handler, sectionOpen="%anaconda")) ksparser.readKickstart(ksfile) # we need to have a libuser.conf that points to the installer root for @@ -52,5 +56,3 @@ for ud in userdata: kwargs = ud.__dict__ kwargs.update({"root": "/", "mkmailspool": False}) u.createUser(ud.username, **kwargs) - -del(os.environ["LIBUSER_CONF"]) diff --git a/anaconda/widgets/Makefile.am b/anaconda/widgets/Makefile.am index ad80f87..7afcf19 100644 --- a/anaconda/widgets/Makefile.am +++ b/anaconda/widgets/Makefile.am @@ -24,7 +24,7 @@ ACLOCAL_AMFLAGS = -I m4 SUBDIRS = src python glade doc data MAINTAINERCLEANFILES = Makefile.in aclocal.m4 config.guess config.h.in config.sub \ - configure depcomp gtk-doc.make install-sh ltmain.sh missing py-compile \ + configure depcomp install-sh ltmain.sh missing py-compile \ m4/*.m4 run-glade: diff --git a/anaconda/widgets/acinclude.m4 b/anaconda/widgets/acinclude.m4 new file mode 120000 index 0000000..d84c32a --- /dev/null +++ b/anaconda/widgets/acinclude.m4 @@ -0,0 +1 @@ +../acinclude.m4 \ No newline at end of file diff --git a/anaconda/widgets/autogen.sh b/anaconda/widgets/autogen.sh index e18b17c..55ed5de 100755 --- a/anaconda/widgets/autogen.sh +++ b/anaconda/widgets/autogen.sh @@ -1,7 +1,6 @@ #!/bin/bash -e [ -d m4 ] || mkdir m4 libtoolize --copy --force -gtkdocize --copy aclocal -I m4 autoconf autoheader --force diff --git a/anaconda/widgets/configure.ac b/anaconda/widgets/configure.ac index 83c197d..e86e6cf 100644 --- a/anaconda/widgets/configure.ac +++ b/anaconda/widgets/configure.ac @@ -31,38 +31,45 @@ AC_CONFIG_HEADERS([config.h]) AC_CONFIG_MACRO_DIR([m4]) +AC_TYPE_SIZE_T + AC_PROG_CC -AC_PROG_CXX AC_PROG_LIBTOOL -AC_PROG_LN_S AM_GNU_GETTEXT([external]) dnl Make sure that autoconf fails if the gobject-introspection macro dnl is not expanded. -m4_pattern_forbid([GOBJECT_INTROSPECTION_CHECK])dnl -GOBJECT_INTROSPECTION_CHECK([0.6.7]) +m4_ifdef([GOBJECT_INTROSPECTION_CHECK], +[GOBJECT_INTROSPECTION_CHECK([0.6.7])], +[found_introspection=no +AM_CONDITIONAL(HAVE_INTROSPECTION, false)]) -# Fail if introspection was not enabled +# Complain if introspection was not enabled AS_IF([test "x$found_introspection" = xyes], [:], - [AC_MSG_ERROR([*** GObject introspection must be enabled])]) + [ANACONDA_SOFT_FAILURE([GObject introspection must be enabled])]) -dnl A missing GTK_DOC_CHECK should fail in autogen.sh running gtkdocize, but -dnl might as well check it here too -m4_pattern_forbid([GTK_DOC_CHECK])dnl -GTK_DOC_CHECK([1.14], [--flavour no-tmpl]) +# Add gtk-doc's html-dir option without pulling in the rest of gtk-doc +AC_ARG_WITH([html-dir], + AS_HELP_STRING([--with-html-dir=PATH], [path to installed docs]),, + [with_html_dir='${datadir}/gtk-doc/html']) +HTML_DIR="$with_html_dir" +AC_SUBST([HTML_DIR]) -PKG_CHECK_MODULES([GLADEUI], [gladeui-2.0 >= 3.10]) -PKG_CHECK_MODULES([GTK], [gtk+-x11-3.0 >= 3.11.3]) -PKG_CHECK_MODULES([GLIB], [glib-2.0]) -PKG_CHECK_MODULES([LIBXKLAVIER], [libxklavier >= 5.2.1]) -PKG_CHECK_EXISTS([gobject-introspection-1.0 >= 1.30]) +ANACONDA_PKG_CHECK_MODULES([GLADEUI], [gladeui-2.0 >= 3.10]) +ANACONDA_PKG_CHECK_MODULES([GTK], [gtk+-x11-3.0 >= 3.11.3]) +ANACONDA_PKG_CHECK_MODULES([LIBXKLAVIER], [libxklavier >= 5.2.1]) +ANACONDA_PKG_CHECK_EXISTS([gobject-introspection-1.0 >= 1.30]) -AC_CHECK_HEADERS([libintl.h stdlib.h string.h unistd.h]) +AC_CHECK_HEADERS([libintl.h stdlib.h string.h unistd.h locale.h], + [], + [ANACONDA_SOFT_FAILURE([Header file $ac_header not found.])], + []) -AC_STRUCT_TIMEZONE +AC_CHECK_FUNCS([setlocale], + [], + [ANACONDA_SOFT_FAILURE([Function $ac_func not found.])]) -AC_CHECK_FUNCS([pow setenv setlocale strchr]) AC_CONFIG_FILES([Makefile doc/Makefile glade/Makefile @@ -71,3 +78,6 @@ AC_CONFIG_FILES([Makefile data/Makefile data/pixmaps/Makefile]) AC_OUTPUT + +# Gently advise the user about the build failures they are about to encounter +ANACONDA_FAILURES diff --git a/anaconda/widgets/doc/Makefile.am b/anaconda/widgets/doc/Makefile.am index b148f39..dac52b0 100644 --- a/anaconda/widgets/doc/Makefile.am +++ b/anaconda/widgets/doc/Makefile.am @@ -28,7 +28,7 @@ DOC_MODULE=AnacondaWidgets DOC_MAIN_SGML_FILE=$(DOC_MODULE)-docs.xml # Directories containing the source code. -DOC_SOURCE_DIR=$(top_srcdir) +DOC_SOURCE_DIR=$(top_srcdir)/src # Header files to ignore while scanning. IGNORE_HFILES = config.h \ @@ -43,17 +43,85 @@ MKDB_OPTIONS=--sgml-mode # Only needed if you are using gtkdoc-scangobj to dynamically query widget # signals and properties (which we are). -GTKDOC_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS) -GTKDOC_LIBS=$(GLIB_LIBS) $(GTK_LIBS) $(top_builddir)/src/libAnacondaWidgets.la +GTKDOC_CFLAGS=$(GTK_CFLAGS) +GTKDOC_LIBS=$(GTK_LIBS) $(top_builddir)/src/libAnacondaWidgets.la -include $(top_srcdir)/gtk-doc.make +# Extra junk copied from gtk-doc.make +GTKDOC_CC = $(LIBTOOL) --tag=CC --mode=compile $(CC) $(INCLUDES) $(GTKDOC_DEPS_CFLAGS) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) +GTKDOC_LD = $(LIBTOOL) --tag=CC --mode=link $(CC) $(GTKDOC_DEPS_LIBS) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) +GTKDOC_RUN = $(LIBTOOL) --mode=execute -if ENABLE_GTK_DOC -TESTS_ENVIRONMENT = cd $(srcdir) && \ - DOC_MODULE=$(DOC_MODULE) DOC_MAIN_SGML_FILE=$(DOC_MAIN_SGML_FILE) \ - SRCDIR=$(abs_srcdir) BUILDDIR=$(abs_builddir) -#TESTS = $(GTKDOC_CHECK) -endif +# gtk-doc is terrible at integrating with autotools, so build things by hand. -MAINTAINERCLEANFILES = AnacondaWidgets-overrides.txt AnacondaWidgets-sections.txt \ - AnacondaWidgets.types Makefile.in +# Copy the files we need into $builddir +SETUP_FILES = $(DOC_MAIN_SGML_FILE) +GTK_DOC_V_SETUP=$(GTK_DOC_V_SETUP_$(V)) +GTK_DOC_V_SETUP_=$(GTK_DOC_V_SETUP_$(AM_DEFAULT_VERBOSITY)) +GTK_DOC_V_SETUP_0=@echo " DOC Preparing build"; + +setup-build.stamp: + $(GTK_DOC_V_SETUP)if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \ + test -d $(abs_builddir) || $(MKDIR_P) $(abs_builddir) ; \ + cp -pf $(abs_srcdir)/$(DOC_MAIN_SGML_FILE) $(abs_builddir)/$(DOC_MAIN_SGML_FILE) ; \ + fi + $(AM_V_at)touch setup-build.stamp + +GTK_DOC_V_BUILD=$(GTK_DOC_V_BUILD_$(V)) +GTK_DOC_V_BUILD_=$(GTK_DOC_V_BUILD_$(AM_DEFAULT_VERBOSITY)) +GTK_DOC_V_BUILD_0=@echo " DOC Building docs"; + +noinst_DATA = gtkdoc.stamp +dist_noinst_SCRIPTS = run-gtkdoc.sh +dist_noinst_DATA = AnacondaWidgets-docs.xml + +# Copy a bunch of make variables to the environment and run the gtk-doc script +gtkdoc.stamp: run-gtkdoc.sh $(DOC_SOURCE_DIR)/*.c $(DOC_SOURCE_DIR)/*.h \ + $(top_builddir)/src/libAnacondaWidgets.la \ + $(DOC_MAIN_SGML_FILE) \ + setup-build.stamp + $(GTK_DOC_V_BUILD)DOC_MODULE="$(DOC_MODULE)" \ + DOC_MAIN_SGML_FILE="$(DOC_MAIN_SGML_FILE)" \ + DOC_SOURCE_DIR="$(DOC_SOURCE_DIR)" \ + IGNORE_HFILES="$(IGNORE_HFILES)" \ + SCAN_OPTIONS="$(SCAN_OPTIONS)" \ + MKDB_OPTIONS="$(MKDB_OPTIONS)" \ + HTML_DIR="$(HTML_DIR)" \ + GTKDOC_CC="$(GTKDOC_CC)" \ + GTKDOC_LD="$(GTKDOC_LD)" \ + GTKDOC_RUN="$(GTKDOC_RUN)" \ + GTKDOC_CFLAGS="$(GTKDOC_CFLAGS)" \ + GTKDOC_LIBS="$(GTKDOC_LIBS)" \ + V=$(V) \ + $(srcdir)/run-gtkdoc.sh + +# install and uninstall targets adapted from gtk-doc.make +install-data-local: gtkdoc.stamp + @installfiles=`echo $(builddir)/html/*`; \ + installdir="$(DESTDIR)$(HTML_DIR)/$(DOC_MODULE)" ; \ + $(mkinstalldirs) $${installdir} ; \ + for i in $$installfiles; do \ + echo ' $(INSTALL_DATA) '$$i ; \ + $(INSTALL_DATA) $$i $${installdir}; \ + done ; \ + gtkdoc-rebase --relative --dest-dir=$(DESTDIR) --html-dir=$${installdir} + +uninstall-local: + @installdir="$(DESTDIR)$(HTML_DIR)/$(DOC_MODULE)" ; \ + rm -rf $${installdir} + +# Clean up the mess +CLEANFILES = AnacondaWidgets-decl-list.txt AnacondaWidgets-decl.txt \ + AnacondaWidgets-overrides.txt AnacondaWidgets-sections.txt \ + AnacondaWidgets.types AnacondaWidgets.args AnacondaWidgets.hierarchy \ + AnacondaWidgets.interfaces AnacondaWidgets.prerequisites AnacondaWidgets.signals \ + AnacondaWidgets-doc.bottom AnacondaWidgets-doc.top AnacondaWidgets-undeclared.txt \ + AnacondaWidgets-undocumented.txt AnacondaWidgets-unused.txt \ + sgml.stamp html.stamp gtkdoc.stamp setup-build.stamp + +clean-local: + @rm -rf xml html + @if test "$(abs_srcdir)" != "$(abs_builddir)" ; then \ + rm -f $(abs_builddir)/$(DOC_MAIN_SGML_FILE) ; \ + fi + +MAINTAINERCLEANFILES = Makefile.in diff --git a/anaconda/widgets/doc/run-gtkdoc.sh b/anaconda/widgets/doc/run-gtkdoc.sh new file mode 100755 index 0000000..40e6d1d --- /dev/null +++ b/anaconda/widgets/doc/run-gtkdoc.sh @@ -0,0 +1,116 @@ +#!/bin/sh -e +# Generate HTML documentation +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): David Shea <dshea@redhat.com> +# + +# Using make to run tools with multiple outputs is a recipe for disaster, so +# use make to run this instead. It's not like any of it can be parallelized +# anyway, and if anything changes we more than likely have to start at the +# beginning. This script will run all of the various gtkdoc programs and create +# a single output file, $DOC_OUTPUT_DIR/gtkdoc.stamp + +# ---- ENVIRONMENT VARIABLES ----- +# DOC_MODULE: the name of the module +# DOC_MAIN_SGML_FILE: the top-level SGML file +# DOC_SOURCE_DIR: directories containing the source code +# IGNORE_HFILES: header files to ignore while scanning +# SCAN_OPTIONS: Extra options to supply to gtkdoc-scan +# MKDB_OPTIONS: Extra options to supply to gtkdoc-mkdb +# HTML_DIR: The gtk-doc html directory +# GTKDOC_CC: compiler to use with gtkdoc-scangobj (e.g., $(LTCOMPILE)) +# GTKDOC_LD: linker to use with gtkdoc-scangobj (e.g., $(LINK)) +# GTKDOC_RUN: Wrapper to run whatever gtdkco-scangobj made (e.g., $(LIBTOOL) --mode=execute) +# GTKDOC_CFLAGS: CFLAGS to pass to gtkdoc-scangobj (e.g., $(GTK_CFLAGS)) +# GTKDOC_LIBS: LIBS to pass to gtkdoc-scangobj (e.g., $(GTK_LIB) plus the .la file to scan) + +# Everything will be output in the current directory because these tools are +# awful at communicating paths to each other, and all of the paths in +# $DOC_MAIN_SGML_FILE need to be relative to the output directory. Which +# also means that $DOC_MAIN_SGML_FILE needs to be copied into $builddir. + +# This script should be run any time the source files or the main SGML file +# change. + +if [ -n "$V" ]; then + set -x +fi + +# Remove the old stamp file +rm -f gtkdoc.stamp + +# Generate --source-dir args for gtkdoc-scan and gtkdoc-mkdb +# Use realpath to convert to absolute paths because whoever wrote gtkdoc-mkdb +# couldn't imagine a world in which input file aren't in the current directory +_source_dir= +for d in $DOC_SOURCE_DIR ; do + _source_dir="${_source_dir} --source-dir=$(realpath $d)" +done + +# Convert DOC_MAIN_SGML_FILE to an absolute path because gtkdoc-mkhtml can't +# specify an output directory +DOC_MAIN_SGML_FILE=$(realpath ${DOC_MAIN_SGML_FILE}) + +# Clean up files from gtkdoc-scan and run +rm -f ${DOC_MODULE}-decl-list.txt ${DOC_MODULE}-decl.txt ${DOC_MODULE}-overrides.txt \ + ${DOC_MODULE}-sections.txt ${DOC_MODULE}.types + +gtkdoc-scan \ + --module=${DOC_MODULE} \ + --ignore-headers="${IGNORE_HFILES}" ${_source_dir} \ + --output-dir=. \ + ${SCAN_OPTIONS} + +# Clean up files from gtkdoc-scangobj and run +rm -f ${DOC_MODULE}.args ${DOC_MODULE}.hierarchy ${DOC_MODULE}.interfaces \ + ${DOC_MODULE}.prerequisites ${DOC_MODULE}.signals + +gtkdoc-scangobj \ + --module=${DOC_MODULE} \ + --types=${DOC_MODULE}.types \ + --output-dir=. \ + --cc="${GTKDOC_CC}" \ + --run="${GTKDOC_RUN}" \ + --ld="${GTKDOC_LD}" \ + --cflags="${GTKDOC_CFLAGS}" \ + --ldflags="${GTKDOC_LIBS}" + +# Clean up files from gtkdoc-mkdb and run +rm -rf ${DOC_MODULE}-doc.bottom ${DOC_MODULE}-doc.top \ + ${DOC_MODULE}-undeclared.txt ${DOC_MODULE}-undocumented.txt \ + ${DOC_MODULE}-unused.txt sgml.stamp xml + +gtkdoc-mkdb --module=${DOC_MODULE} \ + --source-suffixes=c,h \ + --ignore-files="${IGNORE_HFILES}" \ + --output-dir=xml \ + --main-sgml-file=${DOC_MAIN_SGML_FILE} \ + --output-format=xml \ + ${_source_dir} \ + ${MKDB_OPTIONS} + +# We almost have something useful! gtkdoc-mkhtml is next +rm -rf html +mkdir html +( cd html && gtkdoc-mkhtml ${DOC_MODULE} ${DOC_MAIN_SGML_FILE} ) + +gtkdoc-fixxref --module=${DOC_MODULE} --module-dir=html --html-dir=${HTML_DIR} + +# All done, new stamp file +touch gtkdoc.stamp diff --git a/anaconda/widgets/src/BaseStandalone.c b/anaconda/widgets/src/BaseStandalone.c index c822cf4..ffc5018 100644 --- a/anaconda/widgets/src/BaseStandalone.c +++ b/anaconda/widgets/src/BaseStandalone.c @@ -17,6 +17,8 @@ * Author: David Shea <dshea@redhat.com> */ +#include "config.h" + #include "BaseWindow.h" #include "BaseStandalone.h" #include "intl.h" diff --git a/anaconda/widgets/src/BaseWindow.c b/anaconda/widgets/src/BaseWindow.c index b739701..1074e56 100644 --- a/anaconda/widgets/src/BaseWindow.c +++ b/anaconda/widgets/src/BaseWindow.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include <libintl.h> #include <locale.h> #include <stdlib.h> @@ -234,6 +236,7 @@ GtkWidget *anaconda_base_window_new() { static void anaconda_base_window_init(AnacondaBaseWindow *win) { char *markup; AtkObject *atk; + GtkStyleContext *context; win->priv = G_TYPE_INSTANCE_GET_PRIVATE(win, ANACONDA_TYPE_BASE_WINDOW, @@ -261,6 +264,13 @@ static void anaconda_base_window_init(AnacondaBaseWindow *win) { win->priv->main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); gtk_container_add(GTK_CONTAINER(win), win->priv->main_box); + /* GtkBoxes don't draw a background by default, which causes issues during + * transitions in a GtkStack. Work around this by forcing a "background" + * style class. See https://bugzilla.gnome.org/show_bug.cgi?id=742552 + */ + context = gtk_widget_get_style_context(win->priv->main_box); + gtk_style_context_add_class(context, "background"); + /* Then the navigation area that sits as the first item in the main box * for every Window class. */ @@ -588,6 +598,7 @@ static void anaconda_base_window_set_info_bar(AnacondaBaseWindow *win, GtkMessag gtk_container_add(GTK_CONTAINER(content_area), label); gtk_info_bar_set_message_type(GTK_INFO_BAR(win->priv->info_bar), ty); gtk_widget_show(win->priv->info_bar); + gtk_widget_show(win->priv->event_box); win->priv->info_shown = TRUE; } @@ -670,7 +681,6 @@ void anaconda_base_window_clear_info(AnacondaBaseWindow *win) { /** * anaconda_base_window_retranslate: * @win: a #AnacondaBaseWindow - * @lang: target language * * Reload translations for this widget as needed. Generally, this is not * needed. However when changing the language during installation, we need @@ -679,12 +689,9 @@ void anaconda_base_window_clear_info(AnacondaBaseWindow *win) { * * Since: 1.0 */ -void anaconda_base_window_retranslate(AnacondaBaseWindow *win, const char *lang) { +void anaconda_base_window_retranslate(AnacondaBaseWindow *win) { GValue distro = G_VALUE_INIT; - setenv("LANGUAGE", lang, 1); - setlocale(LC_ALL, ""); - /* This bit is internal gettext magic. */ { extern int _nl_msg_cat_cntr; diff --git a/anaconda/widgets/src/BaseWindow.h b/anaconda/widgets/src/BaseWindow.h index 0e29df9..0cc0876 100644 --- a/anaconda/widgets/src/BaseWindow.h +++ b/anaconda/widgets/src/BaseWindow.h @@ -56,7 +56,7 @@ struct _AnacondaBaseWindow { * pointer to be cast to a #GtkBin pointer. * @info_bar_clicked : Function pointer called when the #AnacondaBaseWindow::info-bar-clicked * signal is emitted. - * @help_button_clicked: Function pointer called when the #AnacondaSpokeWindow::help-button-clicked + * @help_button_clicked: Function pointer called when the #AnacondaBaseWindow::help-button-clicked * signal is emitted. */ struct _AnacondaBaseWindowClass { @@ -69,7 +69,7 @@ struct _AnacondaBaseWindowClass { GType anaconda_base_window_get_type (void); GtkWidget *anaconda_base_window_new (); -void anaconda_base_window_retranslate (AnacondaBaseWindow *win, const char *lang); +void anaconda_base_window_retranslate (AnacondaBaseWindow *win); gboolean anaconda_base_window_get_beta (AnacondaBaseWindow *win); void anaconda_base_window_set_beta (AnacondaBaseWindow *win, gboolean is_beta); diff --git a/anaconda/widgets/src/DiskOverview.c b/anaconda/widgets/src/DiskOverview.c index f558e7f..e7ff64b 100644 --- a/anaconda/widgets/src/DiskOverview.c +++ b/anaconda/widgets/src/DiskOverview.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include <atk/atk.h> #include <gdk/gdk.h> #include <gio/gio.h> @@ -273,7 +275,7 @@ static void anaconda_disk_overview_init(AnacondaDiskOverview *widget) { g_signal_connect(widget, "focus-out-event", G_CALLBACK(anaconda_disk_overview_focus_changed), NULL); /* Set "hand" cursor shape when over the selector */ - widget->priv->cursor = gdk_cursor_new(GDK_HAND2); + widget->priv->cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_HAND2); g_signal_connect(widget, "realize", G_CALLBACK(anaconda_disk_overview_realize), NULL); /* Set some properties. */ @@ -414,6 +416,7 @@ static void anaconda_disk_overview_set_property(GObject *object, guint prop_id, char *markup = g_markup_printf_escaped("<span weight='bold' size='large'>%s</span>", g_value_get_string(value)); gtk_label_set_markup(GTK_LABEL(priv->description_label), markup); g_free(markup); + gtk_label_set_justify(GTK_LABEL(priv->description_label), GTK_JUSTIFY_CENTER); break; } diff --git a/anaconda/widgets/src/HubWindow.c b/anaconda/widgets/src/HubWindow.c index 3b5cdea..a093bce 100644 --- a/anaconda/widgets/src/HubWindow.c +++ b/anaconda/widgets/src/HubWindow.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include "BaseStandalone.h" #include "HubWindow.h" #include "intl.h" @@ -131,8 +133,8 @@ static void anaconda_hub_window_init(AnacondaHubWindow *win) { /* The hub has different alignment requirements than a spoke. */ G_GNUC_BEGIN_IGNORE_DEPRECATIONS - gtk_alignment_set(GTK_ALIGNMENT(anaconda_base_window_get_alignment(ANACONDA_BASE_WINDOW(win))), - 0.5, 0.0, 0.5, 1.0); + gtk_alignment_set_padding(GTK_ALIGNMENT(anaconda_base_window_get_alignment(ANACONDA_BASE_WINDOW(win))), + 0, 0, 12, 6); G_GNUC_END_IGNORE_DEPRECATIONS } diff --git a/anaconda/widgets/src/LayoutIndicator.c b/anaconda/widgets/src/LayoutIndicator.c index cebc21d..9aac858 100644 --- a/anaconda/widgets/src/LayoutIndicator.c +++ b/anaconda/widgets/src/LayoutIndicator.c @@ -19,6 +19,8 @@ * Author: Vratislav Podzimek <vpodzime@redhat.com> */ +#include "config.h" + #include <atk/atk.h> #include <glib.h> #include <gdk/gdk.h> @@ -141,7 +143,6 @@ GtkWidget *anaconda_layout_indicator_new() { static void anaconda_layout_indicator_init(AnacondaLayoutIndicator *self) { AtkObject *atk; GdkDisplay *display; - GdkRGBA background_color = { 0.0, 0.0, 0.0, 0.0 }; AnacondaLayoutIndicatorClass *klass = ANACONDA_LAYOUT_INDICATOR_GET_CLASS(self); if (!klass->engine) { @@ -163,8 +164,6 @@ static void anaconda_layout_indicator_init(AnacondaLayoutIndicator *self) { gdk_window_add_filter(NULL, (GdkFilterFunc) handle_xevent, klass->engine); } - g_return_if_fail(gdk_rgba_parse(&background_color, "#fdfdfd")); - self->priv = G_TYPE_INSTANCE_GET_PRIVATE(self, ANACONDA_TYPE_LAYOUT_INDICATOR, AnacondaLayoutIndicatorPrivate); @@ -184,16 +183,11 @@ static void anaconda_layout_indicator_init(AnacondaLayoutIndicator *self) { NULL); /* layout indicator should have a hand cursor so that looks like clickable widget */ - self->priv->cursor = gdk_cursor_new(GDK_HAND2); + self->priv->cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_HAND2); g_signal_connect(self, "realize", G_CALLBACK(anaconda_layout_indicator_realize), NULL); - /* layout indicator should have a different background color - TODO: should be "exported" to allow changes in glade from code? */ - gtk_widget_override_background_color(GTK_WIDGET(self), - GTK_STATE_FLAG_NORMAL, &background_color); - /* initialize XklConfigRec instance providing data */ self->priv->config_rec = xkl_config_rec_new(); xkl_config_rec_get_from_server(self->priv->config_rec, klass->engine); diff --git a/anaconda/widgets/src/Makefile.am b/anaconda/widgets/src/Makefile.am index 42615c2..f6584dd 100644 --- a/anaconda/widgets/src/Makefile.am +++ b/anaconda/widgets/src/Makefile.am @@ -56,14 +56,12 @@ SOURCES = $(GISOURCES) $(NONGISOURCES) HDRS = $(GIHDRS) $(NONGIHDRS) WIDGETSDATA = '"$(datadir)/anaconda"' -TZMAPDATA = '"tzmapdata"' noinst_HEADERS = gettext.h intl.h lib_LTLIBRARIES = libAnacondaWidgets.la libAnacondaWidgets_la_CFLAGS = $(GTK_CFLAGS) $(GLADEUI_CFLAGS) $(LIBXKLAVIER_CFLAGS) -Wall -g\ - -DWIDGETS_DATADIR=$(WIDGETSDATA)\ - -DTZMAP_DATADIR=$(TZMAPDATA) + -DWIDGETS_DATADIR=$(WIDGETSDATA) libAnacondaWidgets_la_LIBADD = $(GTK_LIBS) $(GLADEUI_LIBS) $(LIBXKLAVIER_LIBS) libAnacondaWidgets_la_LDFLAGS = $(LTLIBINTL) -version-info 3:0:1 libAnacondaWidgets_la_SOURCES = $(SOURCES) $(HDRS) \ diff --git a/anaconda/widgets/src/MountpointSelector.c b/anaconda/widgets/src/MountpointSelector.c index b2323b4..d7c835f 100644 --- a/anaconda/widgets/src/MountpointSelector.c +++ b/anaconda/widgets/src/MountpointSelector.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include <unistd.h> #include <gdk/gdk.h> #include <glib.h> @@ -182,7 +184,7 @@ static void anaconda_mountpoint_selector_init(AnacondaMountpointSelector *mountp gtk_widget_add_events(GTK_WIDGET(mountpoint), GDK_FOCUS_CHANGE_MASK|GDK_KEY_RELEASE_MASK); /* Set "hand" cursor shape when over the selector */ - mountpoint->priv->cursor = gdk_cursor_new(GDK_HAND2); + mountpoint->priv->cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_HAND2); g_signal_connect(mountpoint, "realize", G_CALLBACK(anaconda_mountpoint_selector_realize), NULL); /* Create the grid. */ @@ -297,13 +299,9 @@ static void anaconda_mountpoint_selector_set_property(GObject *object, guint pro static void anaconda_mountpoint_selector_toggle_background(AnacondaMountpointSelector *widget) { if (widget->priv->chosen) { gtk_widget_set_state_flags(GTK_WIDGET(widget), GTK_STATE_FLAG_SELECTED, FALSE); - gtk_widget_override_color(GTK_WIDGET(widget->priv->mountpoint_label), GTK_STATE_FLAG_SELECTED, NULL); } else { - GdkRGBA color; gtk_widget_unset_state_flags(GTK_WIDGET(widget), GTK_STATE_FLAG_SELECTED); - gdk_rgba_parse(&color, "#555555"); - gtk_widget_override_color(GTK_WIDGET(widget->priv->mountpoint_label), GTK_STATE_FLAG_NORMAL, &color); } } diff --git a/anaconda/widgets/src/SpokeSelector.c b/anaconda/widgets/src/SpokeSelector.c index 492a6e2..cd83efd 100644 --- a/anaconda/widgets/src/SpokeSelector.c +++ b/anaconda/widgets/src/SpokeSelector.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include <atk/atk.h> #include <gdk/gdk.h> #include <gio/gio.h> @@ -282,7 +284,7 @@ static void anaconda_spoke_selector_init(AnacondaSpokeSelector *spoke) { g_signal_connect(spoke, "focus-out-event", G_CALLBACK(anaconda_spoke_selector_focus_changed), NULL); /* Set "hand" cursor shape when over the selector */ - spoke->priv->cursor = gdk_cursor_new(GDK_HAND2); + spoke->priv->cursor = gdk_cursor_new_for_display(gdk_display_get_default(), GDK_HAND2); g_signal_connect(spoke, "realize", G_CALLBACK(anaconda_spoke_selector_realize), NULL); /* Set property defaults. */ diff --git a/anaconda/widgets/src/SpokeWindow.c b/anaconda/widgets/src/SpokeWindow.c index a1a46f1..f213177 100644 --- a/anaconda/widgets/src/SpokeWindow.c +++ b/anaconda/widgets/src/SpokeWindow.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include "BaseWindow.h" #include "SpokeWindow.h" #include "intl.h" diff --git a/anaconda/widgets/src/StandaloneWindow.c b/anaconda/widgets/src/StandaloneWindow.c index 8281184..c952a1f 100644 --- a/anaconda/widgets/src/StandaloneWindow.c +++ b/anaconda/widgets/src/StandaloneWindow.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + #include "BaseStandalone.h" #include "StandaloneWindow.h" #include "intl.h" @@ -173,7 +175,6 @@ static void anaconda_standalone_window_init(AnacondaStandaloneWindow *win) { /** * anaconda_standalone_window_retranslate: * @win: a #AnacondaStandaloneWindow - * @lang: target language * * Reload translations for this widget as needed. Generally, this is not * needed. However when changing the language during installation, we need @@ -182,8 +183,8 @@ static void anaconda_standalone_window_init(AnacondaStandaloneWindow *win) { * * Since: 1.0 */ -void anaconda_standalone_window_retranslate(AnacondaStandaloneWindow *win, const char *lang) { - anaconda_base_window_retranslate(ANACONDA_BASE_WINDOW(win), lang); +void anaconda_standalone_window_retranslate(AnacondaStandaloneWindow *win) { + anaconda_base_window_retranslate(ANACONDA_BASE_WINDOW(win)); gtk_button_set_label(GTK_BUTTON(win->priv->quit_button), _(QUIT_TEXT)); gtk_button_set_label(GTK_BUTTON(win->priv->continue_button), _(CONTINUE_TEXT)); diff --git a/anaconda/widgets/src/StandaloneWindow.h b/anaconda/widgets/src/StandaloneWindow.h index 63a9e9e..85671f2 100644 --- a/anaconda/widgets/src/StandaloneWindow.h +++ b/anaconda/widgets/src/StandaloneWindow.h @@ -63,7 +63,7 @@ struct _AnacondaStandaloneWindowClass { GType anaconda_standalone_window_get_type (void); GtkWidget *anaconda_standalone_window_new (); -void anaconda_standalone_window_retranslate (AnacondaStandaloneWindow *win, const char *lang); +void anaconda_standalone_window_retranslate (AnacondaStandaloneWindow *win); G_END_DECLS diff --git a/anaconda/widgets/src/glade-adaptor.c b/anaconda/widgets/src/glade-adaptor.c index 367e0e8..b5146f1 100644 --- a/anaconda/widgets/src/glade-adaptor.c +++ b/anaconda/widgets/src/glade-adaptor.c @@ -17,6 +17,8 @@ * Author: Chris Lumens <clumens@redhat.com> */ +#include "config.h" + /* This file contains code called by glade when it creates, reads, writes, or * otherwise manipulates anaconda-specific widgets. Each function in this file * that glade should call must be referenced in a glade-widget-class stanza of @@ -60,15 +62,14 @@ void anaconda_standalone_window_post_create(GladeWidgetAdaptor *adaptor, alignment_widget = glade_widget_get_from_gobject(anaconda_base_window_get_alignment(window)); + glade_widget_property_set(alignment_widget, "xalign", 0.5); + glade_widget_property_set(alignment_widget, "yalign", 0.0); + glade_widget_property_set(alignment_widget, "xscale", 1.0); + glade_widget_property_set(alignment_widget, "yscale", 1.0); + + /* Set padding on hubs */ if (ANACONDA_IS_HUB_WINDOW(object)) { - glade_widget_property_set(alignment_widget, "xalign", 0.5); - glade_widget_property_set(alignment_widget, "yalign", 0.0); - glade_widget_property_set(alignment_widget, "xscale", 0.5); - glade_widget_property_set(alignment_widget, "yscale", 1.0); - } else { - glade_widget_property_set(alignment_widget, "xalign", 0.5); - glade_widget_property_set(alignment_widget, "yalign", 0.0); - glade_widget_property_set(alignment_widget, "xscale", 1.0); - glade_widget_property_set(alignment_widget, "yscale", 1.0); + glade_widget_property_set(alignment_widget, "left-padding", 12); + glade_widget_property_set(alignment_widget, "right-padding", 6); } } diff --git a/anaconda/widgets/src/intl.h b/anaconda/widgets/src/intl.h index 18aad9f..f73ce7a 100644 --- a/anaconda/widgets/src/intl.h +++ b/anaconda/widgets/src/intl.h @@ -20,7 +20,7 @@ #ifndef _INTL_H #define _INTL_H -#include "../config.h" +#include "config.h" #include "gettext.h" #define _(x) dgettext("anaconda", x) diff --git a/anaconda/widgets/src/widgets-common.c b/anaconda/widgets/src/widgets-common.c index 60bb0c5..d965d79 100644 --- a/anaconda/widgets/src/widgets-common.c +++ b/anaconda/widgets/src/widgets-common.c @@ -20,6 +20,8 @@ * */ +#include "config.h" + #include "widgets-common.h" #include <stdlib.h> diff --git a/anaconda/zanata.xml b/anaconda/zanata.xml new file mode 100644 index 0000000..a1b24b8 --- /dev/null +++ b/anaconda/zanata.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<config xmlns="http://zanata.org/namespace/config/"> + <url>https://fedora.zanata.org/</url> + <project>anaconda</project> + <project-version>f22-branch</project-version> + <project-type>gettext</project-type> + + <locales> + <locale>sq</locale> + <locale>ar</locale> + <locale>as</locale> + <locale>ast</locale> + <locale>bal</locale> + <locale>eu</locale> + <locale>bn</locale> + <locale>bn-IN</locale> + <locale>brx</locale> + <locale>bs</locale> + <locale>br</locale> + <locale>bg</locale> + <locale>ca</locale> + <locale>zh-CN</locale> + <locale>zh-HK</locale> + <locale>zh-TW</locale> + <locale>kw</locale> + <locale>kw-GB</locale> + <locale>cs</locale> + <locale>da</locale> + <locale>nl</locale> + <locale>en-GB</locale> + <locale>eo</locale> + <locale>et</locale> + <locale>fi</locale> + <locale>fr</locale> + <locale>gl</locale> + <locale>ka</locale> + <locale>de</locale> + <locale>el</locale> + <locale>gu</locale> + <locale>he</locale> + <locale>hi</locale> + <locale>hu</locale> + <locale>is</locale> + <locale>id</locale> + <locale>ia</locale> + <locale>it</locale> + <locale>ja</locale> + <locale>kn</locale> + <locale>kk</locale> + <locale>km</locale> + <locale>ky</locale> + <locale>ko</locale> + <locale>lt</locale> + <locale>nds</locale> + <locale>mk</locale> + <locale>mai</locale> + <locale>ms</locale> + <locale>ml</locale> + <locale>mr</locale> + <locale>mn</locale> + <locale>ne</locale> + <locale>nb</locale> + <locale>nn</locale> + <locale>or</locale> + <locale>pa</locale> + <locale>fa</locale> + <locale>pl</locale> + <locale>pt</locale> + <locale>pt-BR</locale> + <locale>ro</locale> + <locale>ru</locale> + <locale>sr</locale> + <locale>sr@latin</locale> + <locale>si</locale> + <locale>sk</locale> + <locale>sl</locale> + <locale>es</locale> + <locale>sv</locale> + <locale>tg</locale> + <locale>ta</locale> + <locale>te</locale> + <locale>bo</locale> + <locale>tr</locale> + <locale>uk</locale> + <locale>ur</locale> + <locale>wba</locale> + <locale>cy</locale> + <locale>lv</locale> + <locale>kw@uccor</locale> + <locale>kw@kkcor</locale> + <locale>af</locale> + <locale>am</locale> + <locale>be</locale> + <locale>hr</locale> + <locale>de-CH</locale> + <locale>th</locale> + <locale>vi</locale> + <locale>zu</locale> + <locale>ilo</locale> + <locale>nso</locale> + <locale>tw</locale> + <locale>yo</locale> + <locale>anp</locale> + </locales> + +</config>