From ca89a794e3f1e5a4699aaab38be53c1cd5fcc1aa Mon Sep 17 00:00:00 2001 From: Kyle Rankin Date: Mon, 23 Aug 2021 13:43:28 -0700 Subject: [PATCH] Prompt for LUKS password at first boot This set of changes modifies existing Anaconda scripts related to the user dialog to add prompts to set a LUKS passphrase. This approach allows us to re-use some of the same kind of code and structure from the user module which already shows up at first boot in the OEM image. I didn't create an additional add-on and instead opted to modify the existing user script, because I didn't find a good way to register a new add-on in the post-install section of the kickstart. Copying files in the addons directory wasn't sufficient, and adding an %addons section to the ks.cfg only takes effect during the install. It sees the referenced add-on is missing (because it gets copied in post-install) and skips it. Note that this change also sets the initial encryption password to be empty, so the user doesn't have to know about a hard-coded password ahead of time and can just hit Enter instead, since that has the same security value as a hard-coded password in this case. --- ks.cfg | 66 + make-image.sh | 15 +- purism/anaconda/ui/spokes/user.glade | 374 ++++ purism/pyanaconda/constants.py | 222 +++ purism/pyanaconda/kickstart.py | 2280 +++++++++++++++++++++++ purism/pyanaconda/ui/gui/spokes/user.py | 635 +++++++ purism/pyanaconda/users.py | 485 +++++ 7 files changed, 4064 insertions(+), 13 deletions(-) create mode 100644 ks.cfg create mode 100644 purism/anaconda/ui/spokes/user.glade create mode 100755 purism/pyanaconda/constants.py create mode 100644 purism/pyanaconda/kickstart.py create mode 100644 purism/pyanaconda/ui/gui/spokes/user.py create mode 100644 purism/pyanaconda/users.py diff --git a/ks.cfg b/ks.cfg new file mode 100644 index 00000000..7696e938 --- /dev/null +++ b/ks.cfg @@ -0,0 +1,66 @@ +#version=DEVEL +# System authorization information +auth --enableshadow --passalgo=sha512 +# Use graphical install +graphical +# Run the Setup Agent on first boot +firstboot --enable +ignoredisk --only-use=sda +# Keyboard layouts +keyboard --vckeymap=us --xlayouts='us' +# System language +lang en_US.UTF-8 + +# Network information +network --hostname=dom0 +# System timezone +timezone UTC --isUtc +#user --groups=wheel,qubes --name=user +# X Window System configuration information +xconfig --startxonboot +# System bootloader configuration +bootloader --location=mbr --boot-drive=sda +#Root password +rootpw --lock +# Partition clearing information +clearpart --all --initlabel --drives=sda +# Disk partitioning information +autopart --type thinp --encrypted --passphrase="PleaseChangeMe" + +# Poweroff after installation +poweroff + +%packages +@^qubes-xfce +@debian +@whonix + +%end + +%post --nochroot + +set -e + +oem_dir=/run/install/repo/ +mkdir /mnt/sysimage/srv/formulas/base/nitrokey-formula/ +cp -a $oem_dir/nitrokey /mnt/sysimage/srv/formulas/base/nitrokey-formula/ +mkdir -p /mnt/sysimage/srv/salt/_tops/base +ln -s /srv/formulas/base/nitrokey-formula/nitrokey/init.top \ + /mnt/sysimage/srv/salt/_tops/base/nitrokey.top +printf 'file_roots:\n base:\n - %s\n' \ + '/srv/formulas/base/nitrokey-formula' \ + > /mnt/sysimage/etc/salt/minion.d/formula-nitrokey.conf + +cp -a $oem_dir/purism/anaconda/* /mnt/sysimage/usr/share/anaconda/ +cp -a $oem_dir/purism/pyanaconda/* /mnt/sysimage/usr/lib64/python3.5/site-packages/pyanaconda/ +echo -n 'PleaseChangeMe' > /dev/shm/oldpass +echo -n '' > /dev/shm/newpass +cryptsetup luksChangeKey --key-file /dev/shm/oldpass /dev/sda2 /dev/shm/newpass + +%end + +%anaconda +pwpolicy root --minlen=0 --minquality=1 --notstrict --nochanges --emptyok +pwpolicy user --minlen=0 --minquality=1 --notstrict --nochanges --emptyok +pwpolicy luks --minlen=0 --minquality=1 --notstrict --nochanges --emptyok +%end diff --git a/make-image.sh b/make-image.sh index 1cd863ee..201a15f1 100755 --- a/make-image.sh +++ b/make-image.sh @@ -5,22 +5,10 @@ command -v wget >/dev/null 2>&1 || { echo >&2 "Please install 'wget' first. Abo set -xe -if [ "$1" = "de" ]; then - mv ./ks-DE.cfg ./ks.cfg - echo Build DE -elif [ "$1" = "en" ]; then - mv ./ks-EN.cfg ./ks.cfg - echo Build EN -else - echo Select Language: ./make-image.sh en - exit -fi - # Basic parameters QUBES_RELEASE="R4.0.4" -DEVICE="nitropad" RELEASE_ISO_FILENAME="Qubes-${QUBES_RELEASE}-x86_64.iso" -CUSTOM_ISO_FILENAME="Qubes-${QUBES_RELEASE}-${DEVICE}-oem-x86_64-${1}.iso" +CUSTOM_ISO_FILENAME="Qubes-${QUBES_RELEASE}-nitrokey-oem-x86_64.iso" UNPACKED_IMAGE_PATH="./unpacked-iso/" MBR_IMAGE_FILENAME="${RELEASE_ISO_FILENAME}.mbr" @@ -47,6 +35,7 @@ pushd unpacked-iso cp ../isolinux.cfg isolinux/ cp ../ks.cfg ./ cp -r ../nitrokey ./ +cp -r ../purism ./ popd # Build the new ISO diff --git a/purism/anaconda/ui/spokes/user.glade b/purism/anaconda/ui/spokes/user.glade new file mode 100644 index 00000000..bf5eee8f --- /dev/null +++ b/purism/anaconda/ui/spokes/user.glade @@ -0,0 +1,374 @@ + + + + + + + False + True + True + CREATE USER + + + + False + vertical + 6 + + + False + + + False + 6 + 6 + 6 + + + + + False + False + 0 + + + + + False + 12 + 0 + 0 + 0.5 + 48 + 24 + 24 + + + False + vertical + 6 + + + True + False + 8 + 9 + + + True + False + 10 + _User name + True + t_username + 1 + + + + + + 0 + 1 + + + + + True + True + + + + + User Name + + + + + 1 + 1 + + + + + True + False + 10 + _Password + True + t_password + 1 + + + + + + 0 + 5 + + + + + True + False + 10 + _Confirm password + True + t_verifypassword + 1 + + + + + + 0 + 7 + + + + + True + True + False + + + + + + Password + + + + + 1 + 5 + + + + + True + True + False + + + + + Confirm Password + + + + + 1 + 7 + + + + + True + False + 10 + _Disk Encryption Password + True + t_lukspassword + 1 + + + + + + 0 + 9 + + + + + True + False + 10 + _Confirm Disk Encryption password + True + t_verifylukspassword + 1 + + + + + + 0 + 11 + + + + + True + True + False + + + + + + Disk Encryption Password + + + + + 1 + 9 + + + + + True + True + False + + + + + Confirm Disk Encryption Password + + + + + 1 + 11 + + + + + True + False + <b>Tip:</b> Keep your user name shorter than 32 characters and do not use spaces. + True + 0 + + + 1 + 2 + + + + + True + False + + + True + False + center + 4 + discrete + + + True + True + 0 + + + + + True + False + center + 6 + empty password + + + + + + False + True + 1 + + + + + 1 + 6 + + + + + True + False + + + True + False + center + 4 + discrete + + + True + True + 0 + + + + + True + False + center + 6 + empty password + + + + + + False + True + 1 + + + + + 1 + 10 + + + + + + + + + + + + + + + + + + + + True + True + 0 + + + + + + + True + True + 1 + + + + + + + + + + CREATE USER + + + + diff --git a/purism/pyanaconda/constants.py b/purism/pyanaconda/constants.py new file mode 100755 index 00000000..24e09f2b --- /dev/null +++ b/purism/pyanaconda/constants.py @@ -0,0 +1,222 @@ +# +# constants.py: anaconda constants +# +# Copyright (C) 2001 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 . +# + +# 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 +SELINUX_DEFAULT = -1 + +# where to look for 3rd party addons +ADDON_PATHS = ["/usr/share/anaconda/addons"] + +from pykickstart.constants import AUTOPART_TYPE_LVM_THINP + +# common string needs to be easy to change +from pyanaconda import product +productName = product.productName +productVersion = product.productVersion +productArch = product.productArch +bugzillaUrl = product.bugUrl +isFinal = product.isFinal + +# for use in device names, eg: "fedora", "rhel" +shortProductName = productName.lower() # pylint: disable=no-member +if productName.count(" "): # pylint: disable=no-member + shortProductName = ''.join(s[0] for s in shortProductName.split()) + +# DriverDisc Paths +DD_ALL = "/tmp/DD" +DD_FIRMWARE = "/tmp/DD/lib/firmware" +DD_RPMS = "/tmp/DD-*" + +TRANSLATIONS_UPDATE_DIR = "/tmp/updates/po" + +ANACONDA_CLEANUP = "anaconda-cleanup" +MOUNT_DIR = "/run/install" +DRACUT_REPODIR = "/run/install/repo" +DRACUT_ISODIR = "/run/install/source" +ISO_DIR = MOUNT_DIR + "/isodir" +IMAGE_DIR = MOUNT_DIR + "/image" +INSTALL_TREE = MOUNT_DIR + "/source" +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 = "eurlatgr" + +DEFAULT_KEYBOARD = "us" + +DRACUT_SHUTDOWN_EJECT = "/run/initramfs/usr/lib/dracut/hooks/shutdown/99anaconda-eject.sh" + +# VNC questions +USEVNC = N_("Start VNC") +USETEXT = N_("Use text mode") + +# Runlevel files +TEXT_ONLY_TARGET = 'multi-user.target' +GRAPHICAL_TARGET = 'graphical.target' + +# Network +NETWORK_CONNECTION_TIMEOUT = 45 # in seconds +NETWORK_CONNECTED_CHECK_INTERVAL = 0.1 # in seconds + +# DBus +DEFAULT_DBUS_TIMEOUT = -1 # use default + +# Thread names +THREAD_EXECUTE_STORAGE = "AnaExecuteStorageThread" +THREAD_STORAGE = "AnaStorageThread" +THREAD_STORAGE_WATCHER = "AnaStorageWatcher" +THREAD_CHECK_STORAGE = "AnaCheckStorageThread" +THREAD_CUSTOM_STORAGE_INIT = "AnaCustomStorageInit" +THREAD_WAIT_FOR_CONNECTING_NM = "AnaWaitForConnectingNMThread" +THREAD_PAYLOAD = "AnaPayloadThread" +THREAD_PAYLOAD_RESTART = "AnaPayloadRestartThread" +THREAD_INPUT_BASENAME = "AnaInputThread" +THREAD_SYNC_TIME_BASENAME = "AnaSyncTime" +THREAD_EXCEPTION_HANDLING_TEST = "AnaExceptionHandlingTest" +THREAD_LIVE_PROGRESS = "AnaLiveProgressThread" +THREAD_SOFTWARE_WATCHER = "AnaSoftwareWatcher" +THREAD_CHECK_SOFTWARE = "AnaCheckSoftwareThread" +THREAD_SOURCE_WATCHER = "AnaSourceWatcher" +THREAD_INSTALL = "AnaInstallThread" +THREAD_CONFIGURATION = "AnaConfigurationThread" +THREAD_FCOE = "AnaFCOEThread" +THREAD_ISCSI_DISCOVER = "AnaIscsiDiscoverThread" +THREAD_ISCSI_LOGIN = "AnaIscsiLoginThread" +THREAD_GEOLOCATION_REFRESH = "AnaGeolocationRefreshThread" +THREAD_DATE_TIME = "AnaDateTimeThread" +THREAD_TIME_INIT = "AnaTimeInitThread" +THREAD_DASDFMT = "AnaDasdfmtThread" +THREAD_KEYBOARD_INIT = "AnaKeyboardThread" +THREAD_ADD_LAYOUTS_INIT = "AnaAddLayoutsInitThread" +THREAD_NTP_SERVER_CHECK = "AnaNTPserver" + +# Geolocation constants + +# geolocation providers +# - values are used by the geoloc CLI/boot option +GEOLOC_PROVIDER_FEDORA_GEOIP = "provider_fedora_geoip" +GEOLOC_PROVIDER_HOSTIP = "provider_hostip" +GEOLOC_PROVIDER_GOOGLE_WIFI = "provider_google_wifi" +# geocoding provider +GEOLOC_GEOCODER_NOMINATIM = "geocoder_nominatim" +# default providers +GEOLOC_DEFAULT_PROVIDER = GEOLOC_PROVIDER_FEDORA_GEOIP +GEOLOC_DEFAULT_GEOCODER = GEOLOC_GEOCODER_NOMINATIM +# timeout (in seconds) +GEOLOC_TIMEOUT = 3 + + +ANACONDA_ENVIRON = "anaconda" +FIRSTBOOT_ENVIRON = "firstboot" + +# Tainted hardware +UNSUPPORTED_HW = 1 << 28 + +# Password validation +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. %s") +PASSWORD_WEAK_WITH_ERROR = N_("The password you have provided is weak: %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")] + +PASSWORD_HIDE = N_("Hide password.") +PASSWORD_SHOW = N_("Show password.") + +PASSWORD_HIDE_ICON = "anaconda-password-show-off" +PASSWORD_SHOW_ICON = "anaconda-password-show-on" + +# LUKS Password validation +LUKS_PASSWORD_MIN_LEN = 8 +LUKS_PASSWORD_EMPTY_ERROR = N_("The disk encryption password is empty.") +LUKS_PASSWORD_CONFIRM_ERROR_GUI = N_("The disk encryption passwords do not match.") +LUKS_PASSWORD_CONFIRM_ERROR_TUI = N_("The disk encryption passwords you entered were different. Please try again.") +LUKS_PASSWORD_WEAK = N_("The disk encryption password you have provided is weak. %s") +LUKS_PASSWORD_WEAK_WITH_ERROR = N_("The disk encryption password you have provided is weak: %s.") +LUKS_PASSWORD_WEAK_CONFIRM = N_("You have provided a weak disk encryption password. Press Done again to use anyway.") +LUKS_PASSWORD_WEAK_CONFIRM_WITH_ERROR = N_("You have provided a weak disk encryption password: %s. Press Done again to use anyway.") +LUKS_PASSWORD_ASCII = N_("The disk encryption password you have provided contains non-ASCII characters. You may not be able to switch between keyboard layouts to login. Press Done to continue.") +LUKS_PASSWORD_DONE_TWICE = N_("You will have to press Done twice to confirm it.") + +LUKS_PASSWORD_STRENGTH_DESC = [N_("Empty"), N_("Weak"), N_("Fair"), N_("Good"), N_("Strong")] + +LUKS_PASSWORD_HIDE = N_("Hide disk encryption password.") +LUKS_PASSWORD_SHOW = N_("Show disk encryption password.") + +LUKS_PASSWORD_HIDE_ICON = "anaconda-password-show-off" +LUKS_PASSWORD_SHOW_ICON = "anaconda-password-show-on" + +# the number of seconds we consider a noticeable freeze of the UI +NOTICEABLE_FREEZE = 0.1 + +# all ASCII characters +PW_ASCII_CHARS = string.digits + string.ascii_letters + string.punctuation + " " + +# Recognizing a tarfile +TAR_SUFFIX = (".tar", ".tbz", ".tgz", ".txz", ".tar.bz2", "tar.gz", "tar.xz") + +# screenshots +SCREENSHOTS_DIRECTORY = "/tmp/anaconda-screenshots" +SCREENSHOTS_TARGET_DIRECTORY = "/root/anaconda-screenshots" + +# cmdline arguments that append instead of overwrite +CMDLINE_APPEND = ["modprobe.blacklist", "ifname"] + +DEFAULT_AUTOPART_TYPE = AUTOPART_TYPE_LVM_THINP + +# Default to these units when reading user input when no units given +SIZE_UNITS_DEFAULT = "MiB" + +# 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 + +# X display number to use +X_DISPLAY_NUMBER = 1 + +# Payload status messages +PAYLOAD_STATUS_PROBING_STORAGE = N_("Probing storage...") +PAYLOAD_STATUS_PACKAGE_MD = N_("Downloading package metadata...") +PAYLOAD_STATUS_GROUP_MD = N_("Downloading group metadata...") + +# Window title text +WINDOW_TITLE_TEXT = N_("Anaconda Installer") + +# NTP server checking +NTP_SERVER_OK = 0 +NTP_SERVER_NOK = 1 +NTP_SERVER_QUERY = 2 diff --git a/purism/pyanaconda/kickstart.py b/purism/pyanaconda/kickstart.py new file mode 100644 index 00000000..e09131f4 --- /dev/null +++ b/purism/pyanaconda/kickstart.py @@ -0,0 +1,2280 @@ +# +# kickstart.py: kickstart install support +# +# Copyright (C) 1999-2016 +# 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 . +# + +from pyanaconda.errors import ScriptError, errorHandler +from blivet.deviceaction import ActionCreateFormat, ActionResizeDevice, ActionResizeFormat +from blivet.devices import LUKSDevice +from blivet.devices.lvm import LVMVolumeGroupDevice, LVMCacheRequest +from blivet.devicelibs.lvm import LVM_PE_SIZE, KNOWN_THPOOL_PROFILES +from blivet.devicelibs.crypto import MIN_CREATE_ENTROPY +from blivet.formats import get_format +from blivet.partitioning import do_partitioning +from blivet.partitioning import grow_lvm +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 +import blivet.zfcp +import blivet.arch + +import glob +from pyanaconda import iutil +import os +import os.path +import tempfile +from pyanaconda.flags import flags, can_touch_runtime_system +from pyanaconda.constants import ADDON_PATHS, IPMI_ABORTED, TEXT_ONLY_TARGET, GRAPHICAL_TARGET +import shlex +import requests +import sys +import pykickstart.commands as commands +from pyanaconda import keyboard +from pyanaconda import ntp +from pyanaconda import timezone +from pyanaconda.timezone import NTP_PACKAGE, NTP_SERVICE +from pyanaconda import localization +from pyanaconda import network +from pyanaconda import nm +from pyanaconda.simpleconfig import SimpleConfigFile +from pyanaconda.users import getPassAlgo +from pyanaconda.desktop import Desktop +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 pyanaconda.storage_utils import device_matches + +from pykickstart.constants import CLEARPART_TYPE_NONE, FIRSTBOOT_SKIP, FIRSTBOOT_RECONFIG, KS_SCRIPT_POST, KS_SCRIPT_PRE, \ + KS_SCRIPT_TRACEBACK, KS_SCRIPT_PREINSTALL, SELINUX_DISABLED, SELINUX_ENFORCING, SELINUX_PERMISSIVE +from pykickstart.base import BaseHandler +from pykickstart.errors import formatErrorMsg, KickstartError, KickstartParseError +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, PreInstallScriptSection, \ + OnErrorScriptSection, TracebackScriptSection +from pykickstart.version import returnClassForVersion + +import logging +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_LEVEL + +class AnacondaKSScript(KSScript): + """ Execute a kickstart script + + This will write the script to a file named /tmp/ks-script- before + execution. + Output is logged by the program logger, the path specified by --log + or to /tmp/ks-script-\\*.log + """ + def run(self, chroot): + """ Run the kickstart script + @param chroot directory path to chroot into before execution + """ + if self.inChroot: + scriptRoot = chroot + else: + scriptRoot = "/" + + (fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp") + + os.write(fd, self.script.encode("utf-8")) + os.close(fd) + os.chmod(path, 0o700) + + # Always log stdout/stderr from scripts. Using --log just lets you + # pick where it goes. The script will also be logged to program.log + # because of execWithRedirect. + if self.logfile: + if self.inChroot: + messages = "%s/%s" % (scriptRoot, self.logfile) + else: + messages = self.logfile + + d = os.path.dirname(messages) + if not os.path.exists(d): + os.makedirs(d) + else: + # Always log outside the chroot, we copy those logs into the + # chroot later. + messages = "/tmp/%s.log" % os.path.basename(path) + + with open(messages, "w") as fp: + rc = iutil.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)], + stdout=fp, + root=scriptRoot) + + if rc != 0: + log.error("Error code %s running the kickstart script at line %s", rc, self.lineno) + if self.errorOnFail: + err = "" + with open(messages, "r") as fp: + err = "".join(fp.readlines()) + + errorHandler.cb(ScriptError(self.lineno, err)) + iutil.ipmi_report(IPMI_ABORTED) + sys.exit(0) + +class AnacondaInternalScript(AnacondaKSScript): + def __init__(self, *args, **kwargs): + AnacondaKSScript.__init__(self, *args, **kwargs) + self._hidden = True + + def __str__(self): + # Scripts that implement portions of anaconda (copying screenshots and + # log files, setfilecons, etc.) should not be written to the output + # kickstart file. + return "" + +def getEscrowCertificate(escrowCerts, url): + if not url: + return None + + if url in escrowCerts: + return escrowCerts[url] + + needs_net = not url.startswith("/") and not url.startswith("file:") + if needs_net and not nm.nm_is_connected(): + msg = _("Escrow certificate %s requires the network.") % url + raise KickstartError(msg) + + log.info("escrow: downloading %s", url) + + try: + request = iutil.requests_session().get(url, verify=True) + except requests.exceptions.SSLError as e: + msg = _("SSL error while downloading the escrow certificate:\n\n%s") % e + raise KickstartError(msg) + except requests.exceptions.RequestException as e: + msg = _("The following error was encountered while downloading the escrow certificate:\n\n%s") % e + raise KickstartError(msg) + + try: + escrowCerts[url] = request.content + finally: + request.close() + + return escrowCerts[url] + +def lookupAlias(devicetree, alias): + for dev in devicetree.devices: + if getattr(dev, "req_name", None) == alias: + return dev + + return None + +def getAvailableDiskSpace(storage): + """ + Get overall disk space available on disks we may use. + + :param storage: blivet.Blivet instance + :return: overall disk space available + :rtype: :class:`blivet.size.Size` + + """ + + free_space = storage.free_space_snapshot + # blivet creates a new free space dict to instead of modifying the old one, + # so there is no worry about the dictionary changing during iteration. + return sum(disk_free for disk_free, fs_free in free_space.values()) + +def refreshAutoSwapSize(storage): + """ + Refresh size of the auto partitioning request for swap device according to + the current state of the storage configuration. + + :param storage: blivet.Blivet instance + + """ + + for request in storage.autopart_requests: + if request.fstype == "swap": + disk_space = getAvailableDiskSpace(storage) + request.size = autopart.swap_suggestion(disk_space=disk_space) + break + +### +### SUBCLASSES OF PYKICKSTART COMMAND HANDLERS +### + +class Authconfig(commands.authconfig.FC3_Authconfig): + def __init__(self, *args, **kwargs): + commands.authconfig.FC3_Authconfig.__init__(self, *args, **kwargs) + self.packages = [] + + def setup(self): + if self.seen: + self.packages = ["authconfig"] + + def execute(self, *args): + cmd = "/usr/sbin/authconfig" + if not os.path.lexists(iutil.getSysroot()+cmd): + if flags.automatedInstall and self.seen: + msg = _("%s is missing. Cannot setup authentication.") % cmd + raise KickstartError(msg) + else: + return + + args = ["--update", "--nostart"] + shlex.split(self.authconfig) + + if not flags.automatedInstall and \ + (os.path.exists(iutil.getSysroot() + "/lib64/security/pam_fprintd.so") or \ + os.path.exists(iutil.getSysroot() + "/lib/security/pam_fprintd.so")): + args += ["--enablefingerprint"] + + try: + iutil.execInSysroot(cmd, args) + except RuntimeError as msg: + log.error("Error running %s %s: %s", cmd, args, msg) + +class AutoPart(commands.autopart.F21_AutoPart): + def __init__(self, writePriority=100, *args, **kwargs): + if 'encrypted' not in kwargs: + kwargs['encrypted'] = True + super(AutoPart, self).__init__(writePriority=writePriority, *args, **kwargs) + + def parse(self, args): + retval = commands.autopart.F21_AutoPart.parse(self, args) + + if self.fstype: + fmt = blivet.formats.get_format(self.fstype) + if not fmt or fmt.type is None: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("autopart fstype of %s is invalid.") % self.fstype)) + + return retval + + def execute(self, storage, ksdata, instClass): + from blivet.autopart import do_autopart + from pyanaconda.storage_utils import sanity_check + + if not self.autopart: + return + + if self.fstype: + try: + storage.set_default_fstype(self.fstype) + storage.set_default_boot_fstype(self.fstype) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Settings default fstype to %s failed.") % self.fstype)) + + # sets up default autopartitioning. use clearpart separately + # if you want it + instClass.setDefaultPartitioning(storage) + storage.do_autopart = True + + if self.encrypted: + storage.encrypted_autopart = True + storage.encryption_passphrase = self.passphrase + storage.encryption_cipher = self.cipher + storage.autopart_escrow_cert = getEscrowCertificate(storage.escrow_certificates, self.escrowcert) + storage.autoppart_add_backup_passphrase = self.backuppassphrase + + if self.type is not None: + storage.autopart_type = self.type + + do_autopart(storage, ksdata, min_luks_entropy=MIN_CREATE_ENTROPY) + errors = sanity_check(storage) + if errors: + raise PartitioningError("autopart failed:\n" + "\n".join(str(error) for error in errors)) + +class Bootloader(commands.bootloader.F21_Bootloader): + def __init__(self, *args, **kwargs): + commands.bootloader.F21_Bootloader.__init__(self, *args, **kwargs) + self.location = "mbr" + self._useBackup = False + self._origBootDrive = None + + def parse(self, args): + commands.bootloader.F21_Bootloader.parse(self, args) + if self.location == "partition" and isinstance(get_bootloader(), GRUB2): + raise KickstartParseError(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 KickstartParseError(formatErrorMsg(self.lineno, + msg="GRUB2 encrypted password must be in grub.pbkdf2 format.")) + + return self + + def execute(self, storage, ksdata, instClass, dry_run=False): + """ Resolve and execute the bootloader installation. + + :param storage: object storing storage-related information + (disks, partitioning, bootloader, etc.) + :type storage: blivet.Blivet + :param payload: object storing packaging-related information + :type payload: pyanaconda.packaging.Payload + :param instclass: distribution-specific information + :type instclass: pyanaconda.installclass.BaseInstallClass + :param dry_run: flag if this is only dry run before the partitioning + will be resolved + :type dry_run: bool + """ + if flags.imageInstall and blivet.arch.is_s390(): + self.location = "none" + + if dry_run: + self._origBootDrive = self.bootDrive + self._useBackup = True + elif self._useBackup: + self.bootDrive = self._origBootDrive + self._useBackup = False + + if self.location == "none": + location = None + elif self.location == "partition": + location = "boot" + else: + location = self.location + + if not location: + storage.bootloader.skip_bootloader = True + return + + if self.appendLine: + args = self.appendLine.split() + storage.bootloader.boot_args.update(args) + + if self.password: + if self.isCrypted: + storage.bootloader.encrypted_password = self.password + else: + storage.bootloader.password = self.password + + if location: + storage.bootloader.set_preferred_stage1_type(location) + + if self.timeout is not None: + storage.bootloader.timeout = self.timeout + + # Throw out drives specified that don't exist or cannot be used (iSCSI + # device on an s390 machine) + disk_names = [d.name for d in storage.disks + if not d.format.hidden and not d.protected and + (not blivet.arch.is_s390() or not isinstance(d, blivet.devices.iScsiDiskDevice))] + diskSet = set(disk_names) + + valid_disks = [] + # Drive specifications can contain | delimited variant specifications, + # such as for example: "vd*|hd*|sd*" + # So use the resolved disk identifiers returned by the device_matches() function in place + # of the original specification but still remove the specifications that don't match anything + # from the output kickstart to keep existing --driveorder processing behavior. + for drive in self.driveorder[:]: + matches = device_matches(drive, devicetree=storage.devicetree, disks_only=True) + if set(matches).isdisjoint(diskSet): + log.warning("requested drive %s in boot drive order doesn't exist or cannot be used", + drive) + self.driveorder.remove(drive) + else: + valid_disks.extend(matches) + + storage.bootloader.disk_order = valid_disks + + # When bootloader doesn't have --boot-drive parameter then use this logic as fallback: + # 1) If present first valid disk from driveorder parameter + # 2) If present and usable, use disk where /boot partition is placed + # 3) Use first disk from Blivet + if self.bootDrive: + matches = set(device_matches(self.bootDrive, devicetree=storage.devicetree, disks_only=True)) + if len(matches) > 1: + raise KickstartParseError( + formatErrorMsg(self.lineno, + msg=(_("More than one match found for given boot drive \"%s\".") + % self.bootDrive))) + elif matches.isdisjoint(diskSet): + raise KickstartParseError( + formatErrorMsg(self.lineno, + msg=(_("Requested boot drive \"%s\" doesn't exist or cannot be used.") + % self.bootDrive))) + # Take valid disk from --driveorder + elif len(valid_disks) >= 1: + log.debug("Bootloader: use '%s' first disk from driveorder as boot drive, dry run %s", + valid_disks[0], dry_run) + self.bootDrive = valid_disks[0] + else: + # Try to find /boot + # + # This method is executed two times. Before and after partitioning. + # In the first run, the result is used for other partitioning but + # the second will be used. + try: + boot_dev = storage.mountpoints["/boot"] + except KeyError: + log.debug("Bootloader: /boot partition is not present, dry run %s", dry_run) + else: + boot_drive = "" + # Use disk ancestor + if boot_dev.disks: + boot_drive = boot_dev.disks[0].name + + if boot_drive and boot_drive in disk_names: + self.bootDrive = boot_drive + log.debug("Bootloader: use /boot partition's disk '%s' as boot drive, dry run %s", + boot_drive, dry_run) + + # Nothing was found use first disk from Blivet + if not self.bootDrive: + log.debug("Bootloader: fallback use first disk return from Blivet '%s' as boot drive, dry run %s", + disk_names[0], dry_run) + self.bootDrive = disk_names[0] + + drive = storage.devicetree.resolve_device(self.bootDrive) + storage.bootloader.stage1_disk = drive + + if self.leavebootorder: + flags.leavebootorder = True + + if self.nombr: + flags.nombr = True + +class BTRFS(commands.btrfs.F23_BTRFS): + def execute(self, storage, ksdata, instClass): + for b in self.btrfsList: + b.execute(storage, ksdata, instClass) + +class BTRFSData(commands.btrfs.F23_BTRFSData): + def execute(self, storage, ksdata, instClass): + devicetree = storage.devicetree + + storage.do_autopart = False + + members = [] + + # Get a list of all the devices that make up this volume. + for member in self.devices: + dev = devicetree.resolve_device(member) + if not dev: + # if using --onpart, use original device + member_name = ksdata.onPart.get(member, member) + dev = devicetree.resolve_device(member_name) or lookupAlias(devicetree, member) + + if dev and dev.format.type == "luks": + try: + dev = dev.children[0] + except IndexError: + dev = None + + if dev and dev.format.type != "btrfs": + raise KickstartParseError(formatErrorMsg(self.lineno, + 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 KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Tried to use undefined partition \"%s\" in Btrfs volume specification.") % member)) + + members.append(dev) + + if self.subvol: + name = self.name + elif self.label: + name = self.label + else: + name = None + + if len(members) == 0 and not self.preexist: + raise KickstartParseError(formatErrorMsg(self.lineno, + 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"): + self.mountpoint = "" + + # Sanity check mountpoint + if self.mountpoint != "" and self.mountpoint[0] != '/': + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The mount point \"%s\" is not valid. It must start with a /.") % self.mountpoint)) + + # If a previous device has claimed this mount point, delete the + # old one. + try: + if self.mountpoint: + device = storage.mountpoints[self.mountpoint] + storage.destroy_device(device) + except KeyError: + pass + + if self.preexist: + device = devicetree.resolve_device(self.name) + if not device: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Btrfs volume \"%s\" specified with --useexisting does not exist.") % self.name)) + + device.format.mountpoint = self.mountpoint + else: + try: + request = storage.new_btrfs(name=name, + subvol=self.subvol, + mountpoint=self.mountpoint, + metadata_level=self.metaDataLevel, + data_level=self.dataLevel, + parents=members, + create_options=self.mkfsopts) + except BTRFSValueError as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + storage.create_device(request) + +class Realm(commands.realm.F19_Realm): + def __init__(self, *args): + commands.realm.F19_Realm.__init__(self, *args) + self.packages = [] + self.discovered = "" + + def setup(self): + if not self.join_realm: + return + + try: + argv = ["discover", "--verbose"] + \ + self.discover_options + [self.join_realm] + output = iutil.execWithCapture("realm", argv, filter_stderr=True) + except OSError: + # TODO: A lousy way of propagating what will usually be + # 'no such realm' + # The error message is logged by iutil + return + + # Now parse the output for the required software. First line is the + # realm name, and following lines are information as "name: value" + self.packages = ["realmd"] + self.discovered = "" + + lines = output.split("\n") + if not lines: + return + self.discovered = lines.pop(0).strip() + log.info("Realm discovered: %s", self.discovered) + for line in lines: + parts = line.split(":", 1) + if len(parts) == 2 and parts[0].strip() == "required-package": + self.packages.append(parts[1].strip()) + + log.info("Realm %s needs packages %s", + self.discovered, ", ".join(self.packages)) + + def execute(self, *args): + if not self.discovered: + return + for arg in self.join_args: + if arg.startswith("--no-password") or arg.startswith("--one-time-password"): + pw_args = [] + break + else: + # no explicit password arg using implicit --no-password + pw_args = ["--no-password"] + + argv = ["join", "--install", iutil.getSysroot(), "--verbose"] + \ + pw_args + self.join_args + rc = -1 + try: + rc = iutil.execWithRedirect("realm", argv) + except OSError: + pass + + if rc == 0: + log.info("Joined realm %s", self.join_realm) + + +class ClearPart(commands.clearpart.F21_ClearPart): + def parse(self, args): + retval = commands.clearpart.F21_ClearPart.parse(self, args) + + if self.type is None: + self.type = CLEARPART_TYPE_NONE + + if self.disklabel and self.disklabel not in platform.disklabel_types: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disklabel \"%s\" given in clearpart command is not " + "supported on this platform.") % self.disklabel)) + + # Do any glob expansion now, since we need to have the real list of + # disks available before the execute methods run. + drives = [] + for spec in self.drives: + matched = device_matches(spec, disks_only=True) + if matched: + drives.extend(matched) + else: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" given in clearpart command does not exist.") % spec)) + + self.drives = drives + + # Do any glob expansion now, since we need to have the real list of + # devices available before the execute methods run. + devices = [] + for spec in self.devices: + matched = device_matches(spec, disks_only=True) + if matched: + devices.extend(matched) + else: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Device \"%s\" given in clearpart device list does not exist.") % spec)) + + self.devices = devices + + return retval + + def execute(self, storage, ksdata, instClass): + storage.config.clearpart_type = self.type + storage.config.clearpart_disks = self.drives + storage.config.clearpart_devices = self.devices + + if self.initAll: + storage.config.initialize_disks = self.initAll + + if self.disklabel: + if not platform.set_default_disklabel_type(self.disklabel): + log.warning("%s is not a supported disklabel type on this platform. " + "Using default disklabel %s instead.", self.disklabel, platform.default_disklabel_type) + + storage.clear_partitions() + +class Fcoe(commands.fcoe.F13_Fcoe): + def parse(self, args): + fc = commands.fcoe.F13_Fcoe.parse(self, args) + + if fc.nic not in nm.nm_devices(): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("NIC \"%s\" given in fcoe command does not exist.") % fc.nic)) + + if fc.nic in (info[0] for info in blivet.fcoe.fcoe.nics): + log.info("Kickstart fcoe device %s already added from EDD, ignoring", fc.nic) + else: + msg = blivet.fcoe.fcoe.add_san(nic=fc.nic, dcb=fc.dcb, auto_vlan=True) + if not msg: + msg = "Succeeded." + blivet.fcoe.fcoe.added_nics.append(fc.nic) + + log.info("adding FCoE SAN on %s: %s", fc.nic, msg) + + return fc + +class Firewall(commands.firewall.F20_Firewall): + def __init__(self, *args, **kwargs): + commands.firewall.F20_Firewall.__init__(self, *args, **kwargs) + self.packages = [] + + def setup(self): + if self.seen: + self.packages = ["firewalld"] + + def execute(self, storage, ksdata, instClass): + args = [] + # enabled is None if neither --enable or --disable is passed + # default to enabled if nothing has been set. + if self.enabled == False: + args += ["--disabled"] + else: + args += ["--enabled"] + + if "ssh" not in self.services and "ssh" not in self.remove_services \ + and "22:tcp" not in self.ports: + args += ["--service=ssh"] + + for dev in self.trusts: + args += ["--trust=%s" % (dev,)] + + for port in self.ports: + args += ["--port=%s" % (port,)] + + for remove_service in self.remove_services: + args += ["--remove-service=%s" % (remove_service,)] + + for service in self.services: + args += ["--service=%s" % (service,)] + + cmd = "/usr/bin/firewall-offline-cmd" + if not os.path.exists(iutil.getSysroot()+cmd): + if self.enabled: + msg = _("%s is missing. Cannot setup firewall.") % (cmd,) + raise KickstartError(msg) + else: + iutil.execInSysroot(cmd, args) + +class Firstboot(commands.firstboot.FC3_Firstboot): + def setup(self, *args): + # firstboot should be disabled by default after kickstart installations + if flags.automatedInstall and not self.seen: + self.firstboot = FIRSTBOOT_SKIP + + def execute(self, *args): + action = iutil.enable_service + unit_name = "initial-setup.service" + + # find if the unit file for the Initial Setup service is installed + unit_exists = os.path.exists(os.path.join(iutil.getSysroot(), "lib/systemd/system/", unit_name)) + if unit_exists and self.firstboot == FIRSTBOOT_RECONFIG: + # write the reconfig trigger file + f = open(os.path.join(iutil.getSysroot(), "etc/reconfigSys"), "w+") + f.close() + + if self.firstboot == FIRSTBOOT_SKIP: + action = iutil.disable_service + + # enable/disable the Initial Setup service (if its unit is installed) + if unit_exists: + action(unit_name) + +class Group(commands.group.F12_Group): + def execute(self, storage, ksdata, instClass, users): + for grp in self.groupList: + kwargs = grp.__dict__ + kwargs.update({"root": iutil.getSysroot()}) + try: + users.createGroup(grp.name, **kwargs) + except ValueError as e: + log.warning(str(e)) + +class IgnoreDisk(commands.ignoredisk.RHEL6_IgnoreDisk): + def parse(self, args): + retval = commands.ignoredisk.RHEL6_IgnoreDisk.parse(self, args) + + # See comment in ClearPart.parse + drives = [] + for spec in self.ignoredisk: + matched = device_matches(spec, disks_only=True) + if matched: + drives.extend(matched) + else: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" given in ignoredisk command does not exist.") % spec)) + + self.ignoredisk = drives + + drives = [] + for spec in self.onlyuse: + matched = device_matches(spec, disks_only=True) + if matched: + drives.extend(matched) + else: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" given in ignoredisk command does not exist.") % spec)) + + self.onlyuse = drives + + return retval + +class Iscsi(commands.iscsi.F17_Iscsi): + def parse(self, args): + tg = commands.iscsi.F17_Iscsi.parse(self, args) + + if tg.iface: + if not network.wait_for_network_devices([tg.iface]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Network interface \"%(nic)s\" required by iSCSI \"%(iscsiTarget)s\" target is not up.") % + {"nic": tg.iface, "iscsiTarget": tg.target})) + + mode = blivet.iscsi.iscsi.mode + if mode == "none": + if tg.iface: + blivet.iscsi.iscsi.create_interfaces(nm.nm_activated_devices()) + elif ((mode == "bind" and not tg.iface) + or (mode == "default" and tg.iface)): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("iscsi --iface must be specified (binding used) either for all targets or for none"))) + + try: + blivet.iscsi.iscsi.add_target(tg.ipaddr, tg.port, tg.user, + tg.password, tg.user_in, + tg.password_in, + target=tg.target, + iface=tg.iface) + log.info("added iscsi target %s at %s via %s", tg.target, + tg.ipaddr, + tg.iface) + except (IOError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + return tg + +class IscsiName(commands.iscsiname.FC6_IscsiName): + def parse(self, args): + retval = commands.iscsiname.FC6_IscsiName.parse(self, args) + + blivet.iscsi.iscsi.initiator = self.iscsiname + return retval + +class Lang(commands.lang.F19_Lang): + def execute(self, *args, **kwargs): + localization.write_language_configuration(self, iutil.getSysroot()) + +# no overrides needed here +Eula = commands.eula.F20_Eula + +class LogVol(commands.logvol.F23_LogVol): + def execute(self, storage, ksdata, instClass): + for l in self.lvList: + l.execute(storage, ksdata, instClass) + + if self.lvList: + grow_lvm(storage) + +class LogVolData(commands.logvol.F23_LogVolData): + def execute(self, storage, ksdata, instClass): + devicetree = storage.devicetree + + storage.do_autopart = 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.is_s390(): + raise KickstartParseError(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) + + size = None + + if self.percent: + size = Size(0) + + if self.mountpoint == "swap": + ty = "swap" + self.mountpoint = "" + if self.recommended or self.hibernation: + disk_space = getAvailableDiskSpace(storage) + size = autopart.swap_suggestion(hibernation=self.hibernation, disk_space=disk_space) + self.grow = False + else: + if self.fstype != "": + ty = self.fstype + else: + ty = storage.default_fstype + + if size is None and not self.preexist: + if not self.size: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg="Size can not be decided on from kickstart nor obtained from device.")) + try: + size = Size("%d MiB" % self.size) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg="The size \"%s\" is invalid." % self.size)) + + if self.thin_pool: + self.mountpoint = "" + ty = None + + if self.mountpoint.startswith('/') and not self.fsopts: + # enable discard for normal filesystems in dom0 + self.fsopts = "defaults,discard" + + # Sanity check mountpoint + if self.mountpoint != "" and self.mountpoint[0] != '/': + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The mount point \"%s\" is not valid. It must start with a /.") % self.mountpoint)) + + # Check that the VG this LV is a member of has already been specified. + vg = devicetree.get_device_by_name(vgname) + if not vg: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("No volume group exists with the name \"%s\". Specify volume groups before logical volumes.") % self.vgname)) + + # If cache PVs specified, check that they belong to the same VG this LV is a member of + if self.cache_pvs: + pv_devices = (lookupAlias(devicetree, pv) for pv in self.cache_pvs) + if not all(pv in vg.pvs for pv in pv_devices): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Cache PVs must belong to the same VG as the cached LV"))) + + pool = None + if self.thin_volume: + pool = devicetree.get_device_by_name("%s-%s" % (vg.name, self.pool_name)) + if not pool: + err = formatErrorMsg(self.lineno, + msg=_("No thin pool exists with the name \"%s\". Specify thin pools before thin volumes.") % self.pool_name) + raise KickstartParseError(err) + + # If this specifies an existing request that we should not format, + # quit here after setting up enough information to mount it later. + if not self.format: + if not self.name: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("logvol --noformat must also use the --name= option."))) + + dev = devicetree.get_device_by_name("%s-%s" % (vg.name, self.name)) + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Logical volume \"%s\" given in logvol command does not exist.") % self.name)) + + if self.resize: + size = dev.raw_device.align_target_size(size) + if size < dev.currentSize: + # shrink + try: + devicetree.actions.add(ActionResizeFormat(dev, size)) + devicetree.actions.add(ActionResizeDevice(dev, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": dev.name})) + else: + # grow + try: + devicetree.actions.add(ActionResizeDevice(dev, size)) + devicetree.actions.add(ActionResizeFormat(dev, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": dev.name})) + + dev.format.mountpoint = self.mountpoint + dev.format.mountopts = self.fsopts + if ty == "swap": + storage.add_fstab_swap(dev) + return + + # Make sure this LV name is not already used in the requested VG. + if not self.preexist: + tmp = devicetree.get_device_by_name("%s-%s" % (vg.name, self.name)) + if tmp: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Logical volume name \"%(logvol)s\" is already in use in volume group \"%(volgroup)s\".") % + {"logvol": self.name, "volgroup": vg.name})) + + if not self.percent and size and not self.grow and size < vg.pe_size: + raise KickstartParseError(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.pe_size})) + + # Now get a format to hold a lot of these extra values. + fmt = get_format(ty, + mountpoint=self.mountpoint, + label=self.label, + fsprofile=self.fsprofile, + create_options=self.mkfsopts, + mountopts=self.fsopts) + if not fmt.type and not self.thin_pool: + raise KickstartParseError(formatErrorMsg(self.lineno, + 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 + # options on pre-existing LVs. + if self.preexist: + device = devicetree.get_device_by_name("%s-%s" % (vg.name, self.name)) + if not device: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Logical volume \"%s\" given in logvol command does not exist.") % self.name)) + + storage.devicetree.recursive_remove(device, remove_device=False) + + if self.resize: + size = device.raw_device.align_target_size(size) + try: + devicetree.actions.add(ActionResizeDevice(device, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": device.name})) + + devicetree.actions.add(ActionCreateFormat(device, fmt)) + if ty == "swap": + add_fstab_swap = device + else: + # If a previous device has claimed this mount point, delete the + # old one. + try: + if self.mountpoint: + device = storage.mountpoints[self.mountpoint] + storage.destroy_device(device) + except KeyError: + pass + + if self.thin_volume: + parents = [pool] + else: + parents = [vg] + + pool_args = {} + if self.thin_pool: + if self.profile: + matching = (p for p in KNOWN_THPOOL_PROFILES if p.name == self.profile) + profile = next(matching, None) + if profile: + pool_args["profile"] = profile + else: + log.warning("No matching profile for %s found in LVM configuration", self.profile) + if self.metadata_size: + pool_args["metadatasize"] = Size("%d MiB" % self.metadata_size) + if self.chunk_size: + pool_args["chunksize"] = Size("%d KiB" % self.chunk_size) + + if self.maxSizeMB: + try: + maxsize = Size("%d MiB" % self.maxSizeMB) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg="The maximum size \"%s\" is invalid." % self.maxSizeMB)) + else: + maxsize = None + + if self.cache_size and self.cache_pvs: + pv_devices = [lookupAlias(devicetree, pv) for pv in self.cache_pvs] + cache_size = Size("%d MiB" % self.cache_size) + cache_mode = self.cache_mode or None + cache_request = LVMCacheRequest(cache_size, pv_devices, cache_mode) + else: + cache_request = None + + try: + request = storage.new_lv(fmt=fmt, + name=self.name, + parents=parents, + size=size, + thin_pool=self.thin_pool, + thin_volume=self.thin_volume, + grow=self.grow, + maxsize=maxsize, + percent=self.percent, + cache_request=cache_request, + **pool_args) + except (StorageError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + storage.create_device(request) + if ty == "swap": + add_fstab_swap = request + + if self.encrypted: + if self.passphrase and not storage.encryption_passphrase: + storage.encryption_passphrase = 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.encryption_passphrase + self.passphrase = self.passphrase or storage.encryption_passphrase + + cert = getEscrowCertificate(storage.escrow_certificates, self.escrowcert) + if self.preexist: + luksformat = fmt + device.format = get_format("luks", passphrase=self.passphrase, device=device.path, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase) + luksdev = LUKSDevice("luks%d" % storage.next_id, + fmt=luksformat, + parents=device) + else: + luksformat = request.format + request.format = get_format("luks", passphrase=self.passphrase, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase, + min_luks_entropy=MIN_CREATE_ENTROPY) + luksdev = LUKSDevice("luks%d" % storage.next_id, + 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.create_device(luksdev) + + if add_fstab_swap: + storage.add_fstab_swap(add_fstab_swap) + +class Logging(commands.logging.FC6_Logging): + def execute(self, *args): + if logger.loglevel == DEFAULT_LEVEL: + # not set from the command line + level = logLevelMap[self.level] + logger.loglevel = level + setHandlersLevel(log, level) + setHandlersLevel(storage_log, level) + + if logger.remote_syslog == None and len(self.host) > 0: + # not set from the command line, ok to use kickstart + remote_server = self.host + if self.port: + remote_server = "%s:%s" %(self.host, self.port) + logger.updateRemote(remote_server) + +class Network(commands.network.F25_Network): + def __init__(self, *args, **kwargs): + commands.network.F25_Network.__init__(self, *args, **kwargs) + self.packages = [] + + def parse(self, args): + nd = commands.network.F25_Network.parse(self, args) + setting_only_hostname = nd.hostname and len(args) <= 2 + if not setting_only_hostname: + if not nd.device: + ksdevice = flags.cmdline.get('ksdevice') + if ksdevice: + log.info('network: setting %s from ksdevice for missing kickstart --device', ksdevice) + nd.device = ksdevice + else: + log.info('network: setting "link" for missing --device specification in kickstart') + nd.device = "link" + return nd + + def setup(self): + if network.is_using_team_device(): + self.packages = ["teamd"] + + def execute(self, storage, ksdata, instClass): + network.write_network_config(storage, ksdata, instClass, iutil.getSysroot()) + +class Partition(commands.partition.F23_Partition): + def execute(self, storage, ksdata, instClass): + for p in self.partitions: + p.execute(storage, ksdata, instClass) + + if self.partitions: + do_partitioning(storage) + +class PartitionData(commands.partition.F23_PartData): + def execute(self, storage, ksdata, instClass): + devicetree = storage.devicetree + kwargs = {} + + storage.do_autopart = False + + if self.onbiosdisk != "": + # edd_dict is only modified during storage.reset(), so don't do that + # while executing storage. + for (disk, biosdisk) in storage.edd_dict.items(): + if "%x" % biosdisk == self.onbiosdisk: + self.disk = disk + break + + if not self.disk: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("No disk found for specified BIOS disk \"%s\".") % self.onbiosdisk)) + + size = None + + if self.mountpoint == "swap": + ty = "swap" + self.mountpoint = "" + if self.recommended or self.hibernation: + disk_space = getAvailableDiskSpace(storage) + size = autopart.swap_suggestion(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 :( + elif self.mountpoint == "None": + self.mountpoint = "" + if self.fstype: + ty = self.fstype + else: + ty = storage.default_fstype + elif self.mountpoint == 'appleboot': + ty = "appleboot" + self.mountpoint = "" + elif self.mountpoint == 'prepboot': + ty = "prepboot" + self.mountpoint = "" + elif self.mountpoint == 'biosboot': + ty = "biosboot" + self.mountpoint = "" + elif self.mountpoint.startswith("raid."): + ty = "mdmember" + kwargs["name"] = self.mountpoint + self.mountpoint = "" + + if devicetree.get_device_by_name(kwargs["name"]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("RAID partition \"%s\" is defined multiple times.") % kwargs["name"])) + + if self.onPart: + ksdata.onPart[kwargs["name"]] = self.onPart + elif self.mountpoint.startswith("pv."): + ty = "lvmpv" + kwargs["name"] = self.mountpoint + self.mountpoint = "" + + if devicetree.get_device_by_name(kwargs["name"]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("PV partition \"%s\" is defined multiple times.") % kwargs["name"])) + + if self.onPart: + ksdata.onPart[kwargs["name"]] = self.onPart + elif self.mountpoint.startswith("btrfs."): + ty = "btrfs" + kwargs["name"] = self.mountpoint + self.mountpoint = "" + + if devicetree.get_device_by_name(kwargs["name"]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Btrfs partition \"%s\" is defined multiple times.") % kwargs["name"])) + + if self.onPart: + ksdata.onPart[kwargs["name"]] = self.onPart + elif self.mountpoint == "/boot/efi": + if blivet.arch.is_mactel(): + ty = "macefi" + else: + ty = "EFI System Partition" + self.fsopts = "defaults,uid=0,gid=0,umask=077,shortname=winnt" + else: + if self.fstype != "": + ty = self.fstype + elif self.mountpoint == "/boot": + ty = storage.default_boot_fstype + else: + ty = storage.default_fstype + + if self.mountpoint.startswith('/') and not self.fsopts: + # enable discard for normal filesystems in dom0 + self.fsopts = "defaults,discard" + + if not size and self.size: + try: + size = Size("%d MiB" % self.size) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The size \"%s\" is invalid.") % self.size)) + + # If this specified an existing request that we should not format, + # quit here after setting up enough information to mount it later. + if not self.format: + if not self.onPart: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("part --noformat must also use the --onpart option."))) + + dev = devicetree.resolve_device(self.onPart) + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Partition \"%s\" given in part command does not exist.") % self.onPart)) + + if self.resize: + size = dev.raw_device.align_target_size(size) + if size < dev.currentSize: + # shrink + try: + devicetree.actions.add(ActionResizeFormat(dev, size)) + devicetree.actions.add(ActionResizeDevice(dev, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": dev.name})) + else: + # grow + try: + devicetree.actions.add(ActionResizeDevice(dev, size)) + devicetree.actions.add(ActionResizeFormat(dev, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": dev.name})) + + dev.format.mountpoint = self.mountpoint + dev.format.mountopts = self.fsopts + if ty == "swap": + storage.add_fstab_swap(dev) + return + + # Now get a format to hold a lot of these extra values. + kwargs["fmt"] = get_format(ty, + mountpoint=self.mountpoint, + label=self.label, + fsprofile=self.fsprofile, + mountopts=self.fsopts, + create_options=self.mkfsopts, + size=size) + if not kwargs["fmt"].type: + raise KickstartParseError(formatErrorMsg(self.lineno, + 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 + # mapper/ on the front. If that doesn't exist either, it's an error. + if self.disk: + disk = devicetree.resolve_device(self.disk) + # if this is a multipath member promote it to the real mpath + if disk and disk.format.type == "multipath_member": + mpath_device = disk.children[0] + storage_log.info("kickstart: part: promoting %s to %s", + disk.name, mpath_device.name) + disk = mpath_device + if not disk: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" given in part command does not exist.") % self.disk)) + if not disk.partitionable: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Cannot install to unpartitionable device \"%s\".") % self.disk)) + + should_clear = storage.should_clear(disk) + if disk and (disk.partitioned or should_clear): + kwargs["parents"] = [disk] + elif disk: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" in part command is not partitioned.") % self.disk)) + + if not kwargs["parents"]: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Disk \"%s\" given in part command does not exist.") % self.disk)) + + kwargs["grow"] = self.grow + kwargs["size"] = size + if self.maxSizeMB: + try: + maxsize = Size("%d MiB" % self.maxSizeMB) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The maximum size \"%s\" is invalid.") % self.maxSizeMB)) + else: + maxsize = None + + kwargs["maxsize"] = maxsize + + 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 + # on pre-existing partitions. + if self.onPart: + device = devicetree.resolve_device(self.onPart) + if not device: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Partition \"%s\" given in part command does not exist.") % self.onPart)) + + storage.devicetree.recursive_remove(device, remove_device=False) + if self.resize: + size = device.raw_device.align_target_size(size) + try: + devicetree.actions.add(ActionResizeDevice(device, size)) + except ValueError: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Target size \"%(size)s\" for device \"%(device)s\" is invalid.") % + {"size": self.size, "device": device.name})) + + devicetree.actions.add(ActionCreateFormat(device, kwargs["fmt"])) + if ty == "swap": + add_fstab_swap = device + # tmpfs mounts are not disks and don't occupy a disk partition, + # so handle them here + elif self.fstype == "tmpfs": + try: + request = storage.new_tmp_fs(**kwargs) + except (StorageError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + storage.create_device(request) + else: + # If a previous device has claimed this mount point, delete the + # old one. + try: + if self.mountpoint: + device = storage.mountpoints[self.mountpoint] + storage.destroy_device(device) + except KeyError: + pass + + try: + request = storage.new_partition(**kwargs) + except (StorageError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + storage.create_device(request) + if ty == "swap": + add_fstab_swap = request + + if self.encrypted: + if self.passphrase and not storage.encryption_passphrase: + storage.encryption_passphrase = 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.encryption_passphrase + self.passphrase = self.passphrase or storage.encryption_passphrase + + cert = getEscrowCertificate(storage.escrow_certificates, self.escrowcert) + if self.onPart: + luksformat = kwargs["fmt"] + device.format = get_format("luks", passphrase=self.passphrase, device=device.path, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase, + min_luks_entropy=MIN_CREATE_ENTROPY) + luksdev = LUKSDevice("luks%d" % storage.next_id, + fmt=luksformat, + parents=device) + else: + luksformat = request.format + request.format = get_format("luks", passphrase=self.passphrase, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase, + min_luks_entropy=MIN_CREATE_ENTROPY) + luksdev = LUKSDevice("luks%d" % storage.next_id, + 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.create_device(luksdev) + + if add_fstab_swap: + storage.add_fstab_swap(add_fstab_swap) + +class Raid(commands.raid.F25_Raid): + def execute(self, storage, ksdata, instClass): + for r in self.raidList: + r.execute(storage, ksdata, instClass) + +class RaidData(commands.raid.F25_RaidData): + def execute(self, storage, ksdata, instClass): + raidmems = [] + devicetree = storage.devicetree + devicename = self.device + if self.preexist: + device = devicetree.resolve_device(devicename) + if device: + devicename = device.name + + kwargs = {} + + storage.do_autopart = False + + if self.mountpoint == "swap": + ty = "swap" + self.mountpoint = "" + elif self.mountpoint.startswith("pv."): + ty = "lvmpv" + kwargs["name"] = self.mountpoint + ksdata.onPart[kwargs["name"]] = devicename + + if devicetree.get_device_by_name(kwargs["name"]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("PV partition \"%s\" is defined multiple times.") % kwargs["name"])) + + self.mountpoint = "" + elif self.mountpoint.startswith("btrfs."): + ty = "btrfs" + kwargs["name"] = self.mountpoint + ksdata.onPart[kwargs["name"]] = devicename + + if devicetree.get_device_by_name(kwargs["name"]): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Btrfs partition \"%s\" is defined multiple times.") % kwargs["name"])) + + self.mountpoint = "" + else: + if self.fstype != "": + ty = self.fstype + elif self.mountpoint == "/boot" and \ + "mdarray" in storage.bootloader.stage2_device_types: + ty = storage.default_boot_fstype + else: + ty = storage.default_fstype + + if self.mountpoint.startswith('/') and not self.fsopts: + # enable discard for normal filesystems in dom0 + self.fsopts = "defaults,discard" + + # Sanity check mountpoint + if self.mountpoint != "" and self.mountpoint[0] != '/': + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The mount point \"%s\" is not valid. It must start with a /.") % self.mountpoint)) + + # If this specifies an existing request that we should not format, + # quit here after setting up enough information to mount it later. + if not self.format: + if not devicename: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("raid --noformat must also use the --device option."))) + + dev = devicetree.get_device_by_name(devicename) + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("RAID device \"%s\" given in raid command does not exist.") % devicename)) + + dev.format.mountpoint = self.mountpoint + dev.format.mountopts = self.fsopts + if ty == "swap": + storage.add_fstab_swap(dev) + return + + # Get a list of all the RAID members. + for member in self.members: + dev = devicetree.resolve_device(member) + if not dev: + # if member is using --onpart, use original device + mem = ksdata.onPart.get(member, member) + dev = devicetree.resolve_device(mem) or lookupAlias(devicetree, member) + if dev and dev.format.type == "luks": + try: + dev = dev.children[0] + except IndexError: + dev = None + + if dev and dev.format.type != "mdmember": + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("RAID device \"%(device)s\" has a format of \"%(format)s\", but should have a format of \"mdmember\".") % + {"device": member, "format": dev.format.type})) + + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Tried to use undefined partition \"%s\" in RAID specification.") % member)) + + raidmems.append(dev) + + # Now get a format to hold a lot of these extra values. + kwargs["fmt"] = get_format(ty, + label=self.label, + fsprofile=self.fsprofile, + mountpoint=self.mountpoint, + mountopts=self.fsopts, + create_options=self.mkfsopts) + if not kwargs["fmt"].type: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The \"%s\" file system type is not supported.") % ty)) + + kwargs["name"] = devicename + kwargs["level"] = self.level + kwargs["parents"] = raidmems + kwargs["member_devices"] = len(raidmems) - self.spares + kwargs["total_devices"] = len(raidmems) + + if self.chunk_size: + kwargs["chunk_size"] = Size("%d KiB" % self.chunk_size) + + add_fstab_swap = None + + # If we were given a pre-existing RAID 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 on pre-existing RAIDs. + if self.preexist: + device = devicetree.get_device_by_name(devicename) + if not device: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("RAID volume \"%s\" specified with --useexisting does not exist.") % devicename)) + + storage.devicetree.recursive_remove(device, remove_device=False) + devicetree.actions.add(ActionCreateFormat(device, kwargs["fmt"])) + if ty == "swap": + add_fstab_swap = device + else: + if devicename and devicename in (a.name for a in storage.mdarrays): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The RAID volume name \"%s\" is already in use.") % devicename)) + + # If a previous device has claimed this mount point, delete the + # old one. + try: + if self.mountpoint: + device = storage.mountpoints[self.mountpoint] + storage.destroy_device(device) + except KeyError: + pass + + try: + request = storage.new_mdarray(**kwargs) + except (StorageError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + storage.create_device(request) + if ty == "swap": + add_fstab_swap = request + + if self.encrypted: + if self.passphrase and not storage.encryption_passphrase: + storage.encryption_passphrase = self.passphrase + + cert = getEscrowCertificate(storage.escrow_certificates, self.escrowcert) + if self.preexist: + luksformat = kwargs["fmt"] + device.format = get_format("luks", passphrase=self.passphrase, device=device.path, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase) + luksdev = LUKSDevice("luks%d" % storage.next_id, + fmt=luksformat, + parents=device) + else: + luksformat = request.format + request.format = get_format("luks", passphrase=self.passphrase, + cipher=self.cipher, + escrow_cert=cert, + add_backup_passphrase=self.backuppassphrase) + luksdev = LUKSDevice("luks%d" % storage.next_id, + fmt=luksformat, + parents=request) + + if ty == "swap": + # swap is on the LUKS device instead of the parent device, + # override the device here + add_fstab_swap = luksdev + + storage.create_device(luksdev) + + if add_fstab_swap: + storage.add_fstab_swap(add_fstab_swap) + +class RepoData(commands.repo.F21_RepoData): + def __init__(self, *args, **kwargs): + """ Add enabled kwarg + + :param enabled: The repo has been enabled + :type enabled: bool + """ + self.enabled = kwargs.pop("enabled", True) + self.repo_id = kwargs.pop("repo_id", None) + + commands.repo.F21_RepoData.__init__(self, *args, **kwargs) + +class ReqPart(commands.reqpart.F23_ReqPart): + def execute(self, storage, ksdata, instClass): + from blivet.autopart import do_reqpart + + if not self.reqpart: + return + + reqs = platform.set_platform_bootloader_reqs() + if self.addBoot: + bootPartitions = platform.set_platform_boot_partition() + + # blivet doesn't know this - anaconda sets up the default boot fstype + # in various places in this file, as well as in setDefaultPartitioning + # in the install classes. We need to duplicate that here. + for part in bootPartitions: + if part.mountpoint == "/boot": + part.fstype = storage.default_boot_fstype + + reqs += bootPartitions + + do_reqpart(storage, reqs) + +class RootPw(commands.rootpw.F18_RootPw): + def __init__(self, writePriority=100, *args, **kwargs): + if 'lock' not in kwargs: + kwargs['lock'] = True + super(RootPw, self).__init__(writePriority=writePriority, *args, **kwargs) + + def execute(self, storage, ksdata, instClass, users): + if not self.password and not flags.automatedInstall: + self.lock = True + + algo = getPassAlgo(ksdata.authconfig.authconfig) + users.setRootPassword(self.password, self.isCrypted, self.lock, algo, iutil.getSysroot()) + +class SELinux(commands.selinux.FC3_SELinux): + def execute(self, *args): + selinux_states = {SELINUX_DISABLED: "disabled", + SELINUX_ENFORCING: "enforcing", + SELINUX_PERMISSIVE: "permissive"} + + if self.selinux is None: + # Use the defaults set by the installed (or not) selinux package + return + elif self.selinux not in selinux_states: + log.error("unknown selinux state: %s", self.selinux) + return + + try: + selinux_cfg = SimpleConfigFile(iutil.getSysroot()+"/etc/selinux/config") + selinux_cfg.read() + selinux_cfg.set(("SELINUX", selinux_states[self.selinux])) + selinux_cfg.write() + except IOError as msg: + log.error("Error setting selinux mode: %s", msg) + +class Services(commands.services.FC6_Services): + def execute(self, storage, ksdata, instClass): + for svc in self.disabled: + iutil.disable_service(svc) + + for svc in self.enabled: + iutil.enable_service(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.F25_Timezone): + def __init__(self, *args): + commands.timezone.F25_Timezone.__init__(self, *args) + + self._added_chrony = False + self._enabled_chrony = False + self._disabled_chrony = False + + def setup(self, ksdata): + ### Skip the whole NTP setup in Qubes dom0 + return + + # do not install and use NTP package + if self.nontp or NTP_PACKAGE in ksdata.packages.excludedList: + if iutil.service_running(NTP_SERVICE) and \ + can_touch_runtime_system("stop NTP service"): + ret = iutil.stop_service(NTP_SERVICE) + if ret != 0: + log.error("Failed to stop NTP service") + + if self._added_chrony and NTP_PACKAGE in ksdata.packages.packageList: + ksdata.packages.packageList.remove(NTP_PACKAGE) + self._added_chrony = False + + # Both un-enable and disable chrony, because sometimes it's installed + # off by default (packages) and sometimes not (liveimg). + if self._enabled_chrony and NTP_SERVICE in ksdata.services.enabled: + ksdata.services.enabled.remove(NTP_SERVICE) + self._enabled_chrony = False + + if NTP_SERVICE not in ksdata.services.disabled: + ksdata.services.disabled.append(NTP_SERVICE) + self._disabled_chrony = True + # install and use NTP package + else: + if not iutil.service_running(NTP_SERVICE) and \ + can_touch_runtime_system("start NTP service"): + ret = iutil.start_service(NTP_SERVICE) + if ret != 0: + log.error("Failed to start NTP service") + + if not NTP_PACKAGE in ksdata.packages.packageList: + ksdata.packages.packageList.append(NTP_PACKAGE) + self._added_chrony = True + + if self._disabled_chrony and NTP_SERVICE in ksdata.services.disabled: + ksdata.services.disabled.remove(NTP_SERVICE) + self._disabled_chrony = False + + if not NTP_SERVICE in ksdata.services.enabled and \ + not NTP_SERVICE in ksdata.services.disabled: + ksdata.services.enabled.append(NTP_SERVICE) + self._enabled_chrony = True + + def execute(self, *args): + # write out timezone configuration + if not timezone.is_valid_timezone(self.timezone): + # this should never happen, but for pity's sake + log.warning("Timezone %s set in kickstart is not valid, falling "\ + "back to default (America/New_York).", self.timezone) + self.timezone = "America/New_York" + + timezone.write_timezone_config(self, iutil.getSysroot()) + + # write out NTP configuration (if set) and --nontp is not used + 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) + if os.path.exists(chronyd_conf_path): + log.debug("Modifying installed chrony configuration") + try: + 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) + # use chrony conf file from installation environment when + # chrony is not installed (chrony conf file is missing) + else: + log.debug("Creating chrony configuration based on the " + "configuration from installation environment") + try: + ntp.save_servers_to_config(pools, servers, + conf_file_path=ntp.NTP_CONFIG_FILE, + out_file_path=chronyd_conf_path) + except ntp.NTPconfigError as ntperr: + log.warning("Failed to save NTP configuration without chrony package: %s", ntperr) + +class User(commands.user.F19_User): + def execute(self, storage, ksdata, instClass, users): + algo = getPassAlgo(ksdata.authconfig.authconfig) + + for usr in self.userList: + kwargs = usr.__dict__ + kwargs.update({"algo": algo, "root": iutil.getSysroot()}) + + # If the user password came from a kickstart and it is blank we + # need to make sure the account is locked, not created with an + # empty password. + if ksdata.user.seen and kwargs.get("password", "") == "": + kwargs["password"] = None + try: + users.createUser(usr.name, **kwargs) + except ValueError as e: + log.warning(str(e)) + if usr.lukspassword: + try: + users.setLuksPassword(usr.lukspassword) + except ValueError as e: + log.warning(str(e)) + +class VolGroup(commands.volgroup.F21_VolGroup): + def execute(self, storage, ksdata, instClass): + for v in self.vgList: + v.execute(storage, ksdata, instClass) + +class VolGroupData(commands.volgroup.F21_VolGroupData): + def execute(self, storage, ksdata, instClass): + pvs = [] + + devicetree = storage.devicetree + + storage.do_autopart = False + + # Get a list of all the physical volume devices that make up this VG. + for pv in self.physvols: + dev = devicetree.resolve_device(pv) + if not dev: + # if pv is using --onpart, use original device + pv_name = ksdata.onPart.get(pv, pv) + dev = devicetree.resolve_device(pv_name) or lookupAlias(devicetree, pv) + if dev and dev.format.type == "luks": + try: + dev = dev.children[0] + except IndexError: + dev = None + + if dev and dev.format.type != "lvmpv": + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Physical volume \"%(device)s\" has a format of \"%(format)s\", but should have a format of \"lvmpv\".") % + {"device": pv, "format": dev.format.type})) + + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Tried to use undefined partition \"%s\" in Volume Group specification") % pv)) + + pvs.append(dev) + + if len(pvs) == 0 and not self.preexist: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Volume group \"%s\" defined without any physical volumes. Either specify physical volumes or use --useexisting.") % self.vgname)) + + if self.pesize == 0: + # default PE size requested -- we use blivet's default in KiB + self.pesize = LVM_PE_SIZE.convert_to(KiB) + + pesize = Size("%d KiB" % self.pesize) + possible_extents = LVMVolumeGroupDevice.get_supported_pe_sizes() + if pesize not in possible_extents: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Volume group given physical extent size of \"%(extentSize)s\", but must be one of:\n%(validExtentSizes)s.") % + {"extentSize": pesize, "validExtentSizes": ", ".join(str(e) for e in possible_extents)})) + + # If --noformat or --useexisting was given, there's really nothing to do. + if not self.format or self.preexist: + if not self.vgname: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("volgroup --noformat and volgroup --useexisting must also use the --name= option."))) + + dev = devicetree.get_device_by_name(self.vgname) + if not dev: + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("Volume group \"%s\" given in volgroup command does not exist.") % self.vgname)) + elif self.vgname in (vg.name for vg in storage.vgs): + raise KickstartParseError(formatErrorMsg(self.lineno, + msg=_("The volume group name \"%s\" is already in use.") % self.vgname)) + else: + try: + request = storage.new_vg(parents=pvs, + name=self.vgname, + pe_size=pesize) + except (StorageError, ValueError) as e: + raise KickstartParseError(formatErrorMsg(self.lineno, msg=str(e))) + + storage.create_device(request) + if self.reserved_space: + request.reserved_space = self.reserved_space + elif self.reserved_percent: + request.reserved_percent = self.reserved_percent + + # in case we had to truncate or otherwise adjust the specified name + ksdata.onPart[self.vgname] = request.name + +class XConfig(commands.xconfig.F14_XConfig): + def execute(self, *args): + desktop = Desktop() + if self.startX: + desktop.default_target = GRAPHICAL_TARGET + + if self.defaultdesktop: + desktop.desktop = self.defaultdesktop + + # now write it out + desktop.write() + +class SkipX(commands.skipx.FC3_SkipX): + def execute(self, *args): + if self.skipx: + desktop = Desktop() + desktop.default_target = TEXT_ONLY_TARGET + desktop.write() + +class ZFCP(commands.zfcp.F14_ZFCP): + def parse(self, args): + fcp = commands.zfcp.F14_ZFCP.parse(self, args) + try: + blivet.zfcp.zfcp.add_fcp(fcp.devnum, fcp.wwpn, fcp.fcplun) + except ValueError as e: + log.warning(str(e)) + + return fcp + +class Keyboard(commands.keyboard.F18_Keyboard): + def execute(self, *args): + keyboard.write_keyboard_config(self, iutil.getSysroot()) + +class Upgrade(commands.upgrade.F20_Upgrade): + # Upgrade is no longer supported. If an upgrade command was included in + # a kickstart, warn the user and exit. + 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 = "" + # This dictionary should only be modified during __init__, so if it + # changes during iteration something has gone horribly wrong. + 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 +### + +# This is just the latest entry from pykickstart.handlers.control with all the +# classes we're overriding in place of the defaults. +commandMap = { + "auth": Authconfig, + "authconfig": Authconfig, + "autopart": AutoPart, + "btrfs": BTRFS, + "bootloader": Bootloader, + "clearpart": ClearPart, + "eula": Eula, + "fcoe": Fcoe, + "firewall": Firewall, + "firstboot": Firstboot, + "group": Group, + "ignoredisk": IgnoreDisk, + "iscsi": Iscsi, + "iscsiname": IscsiName, + "keyboard": Keyboard, + "lang": Lang, + "logging": Logging, + "logvol": LogVol, + "network": Network, + "part": Partition, + "partition": Partition, + "raid": Raid, + "realm": Realm, + "reqpart": ReqPart, + "rootpw": RootPw, + "selinux": SELinux, + "services": Services, + "sshkey": SshKey, + "skipx": SkipX, + "timezone": Timezone, + "upgrade": Upgrade, + "user": User, + "volgroup": VolGroup, + "xconfig": XConfig, + "zfcp": ZFCP, +} + +dataMap = { + "BTRFSData": BTRFSData, + "LogVolData": LogVolData, + "PartData": PartitionData, + "RaidData": RaidData, + "RepoData": RepoData, + "VolGroupData": VolGroupData, +} + +superclass = returnClassForVersion() + +class AnacondaKSHandler(superclass): + AddonClassType = AddonData + + def __init__(self, addon_paths=None, commandUpdates=None, dataUpdates=None): + if addon_paths is None: + addon_paths = [] + + if commandUpdates is None: + commandUpdates = commandMap + + if dataUpdates is None: + dataUpdates = dataMap + + superclass.__init__(self, commandUpdates=commandUpdates, dataUpdates=dataUpdates) + self.onPart = {} + + # collect all kickstart addons for anaconda to addons dictionary + # which maps addon_id to it's own data structure based on BaseData + # with execute method + addons = {} + + # collect all AddonData subclasses from + # for p in addon_paths:

//ks/*.(py|so) + # and register them under name + for module_name, path in addon_paths: + addon_id = os.path.basename(os.path.dirname(os.path.abspath(path))) + if not os.path.isdir(path): + continue + + classes = collect(module_name, path, lambda cls: issubclass(cls, self.AddonClassType)) + if classes: + addons[addon_id] = classes[0](name=addon_id) + + # 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) + str(self.anaconda) + +class AnacondaPreParser(KickstartParser): + # A subclass of KickstartParser that only looks for %pre scripts and + # sets them up to be run. All other scripts and commands are ignored. + def __init__(self, handler, followIncludes=True, errorsAreFatal=True, + missingIncludeIsFatal=True): + KickstartParser.__init__(self, handler, missingIncludeIsFatal=False) + + def handleCommand(self, lineno, args): + pass + + def setupSections(self): + self.registerSection(PreScriptSection(self.handler, dataObj=AnacondaKSScript)) + self.registerSection(NullSection(self.handler, sectionOpen="%pre-install")) + self.registerSection(NullSection(self.handler, sectionOpen="%post")) + self.registerSection(NullSection(self.handler, sectionOpen="%onerror")) + 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): + def __init__(self, handler, followIncludes=True, errorsAreFatal=True, + missingIncludeIsFatal=True, scriptClass=AnacondaKSScript): + self.scriptClass = scriptClass + KickstartParser.__init__(self, handler) + + def handleCommand(self, lineno, args): + if not self.handler: + return + + return KickstartParser.handleCommand(self, lineno, args) + + def setupSections(self): + self.registerSection(PreScriptSection(self.handler, dataObj=self.scriptClass)) + self.registerSection(PreInstallScriptSection(self.handler, dataObj=self.scriptClass)) + self.registerSection(PostScriptSection(self.handler, dataObj=self.scriptClass)) + self.registerSection(TracebackScriptSection(self.handler, dataObj=self.scriptClass)) + self.registerSection(OnErrorScriptSection(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 + # and run them. This must come in a separate pass in case a script + # generates an included file that has commands for later. + ksparser = AnacondaPreParser(AnacondaKSHandler()) + + try: + ksparser.readKickstart(f) + except KickstartError as e: + # 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 + runPreScripts(ksparser.handler.scripts) + +def parseKickstart(f): + # preprocessing the kickstart file has already been handled in initramfs. + + addon_paths = collect_addon_paths(ADDON_PATHS) + handler = AnacondaKSHandler(addon_paths["ks"]) + ksparser = AnacondaKSParser(handler) + + # We need this so all the /dev/disk/* stuff is set up before parsing. + udev.trigger(subsystem="block", action="change") + + try: + ksparser.readKickstart(f) + except KickstartError as e: + # 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 + +def appendPostScripts(ksdata): + scripts = "" + + # Read in all the post script snippets to a single big string. + for fn in glob.glob("/usr/share/anaconda/post-scripts/*ks"): + f = open(fn, "r") + scripts += f.read() + f.close() + + # Then parse the snippets against the existing ksdata. We can do this + # because pykickstart allows multiple parses to save their data into a + # single data object. Errors parsing the scripts are a bug in anaconda, + # so just raise an exception. + ksparser = AnacondaKSParser(ksdata, scriptClass=AnacondaInternalScript) + ksparser.readKickstartFromString(scripts, reset=False) + +def runPostScripts(scripts): + postScripts = [s for s in scripts if s.type == KS_SCRIPT_POST] + + if len(postScripts) == 0: + return + + log.info("Running kickstart %%post script(s)") + for script in postScripts: + script.run(iutil.getSysroot()) + log.info("All kickstart %%post script(s) have been run") + +def runPreScripts(scripts): + preScripts = [s for s in scripts if s.type == KS_SCRIPT_PRE] + + if len(preScripts) == 0: + return + + log.info("Running kickstart %%pre script(s)") + stdoutLog.info(_("Running pre-installation scripts")) + + for script in preScripts: + script.run("/") + + log.info("All kickstart %%pre script(s) have been run") + +def runPreInstallScripts(scripts): + preInstallScripts = [s for s in scripts if s.type == KS_SCRIPT_PREINSTALL] + + if len(preInstallScripts) == 0: + return + + log.info("Running kickstart %%pre-install script(s)") + + for script in preInstallScripts: + script.run("/") + + log.info("All kickstart %%pre-install script(s) have been run") + +def runTracebackScripts(scripts): + log.info("Running kickstart %%traceback script(s)") + for script in filter(lambda s: s.type == KS_SCRIPT_TRACEBACK, scripts): + script.run("/") + log.info("All kickstart %%traceback script(s) have been run") + +def resetCustomStorageData(ksdata): + for command in ["partition", "raid", "volgroup", "logvol", "btrfs"]: + ksdata.resetCommand(command) + + ksdata.clearpart.type = CLEARPART_TYPE_NONE + +def doKickstartStorage(storage, ksdata, instClass): + """ Setup storage state from the kickstart data """ + ksdata.clearpart.execute(storage, ksdata, instClass) + if not any(d for d in storage.disks + if not d.format.hidden and not d.protected): + return + + # snapshot free space now so that we know how much we had available + storage.create_free_space_snapshot() + + ksdata.bootloader.execute(storage, ksdata, instClass, dry_run=True) + ksdata.autopart.execute(storage, ksdata, instClass) + ksdata.reqpart.execute(storage, ksdata, instClass) + ksdata.partition.execute(storage, ksdata, instClass) + ksdata.raid.execute(storage, ksdata, instClass) + ksdata.volgroup.execute(storage, ksdata, instClass) + ksdata.logvol.execute(storage, ksdata, instClass) + ksdata.btrfs.execute(storage, ksdata, instClass) + # also calls ksdata.bootloader.execute + storage.set_up_bootloader() diff --git a/purism/pyanaconda/ui/gui/spokes/user.py b/purism/pyanaconda/ui/gui/spokes/user.py new file mode 100644 index 00000000..21a3239e --- /dev/null +++ b/purism/pyanaconda/ui/gui/spokes/user.py @@ -0,0 +1,635 @@ +# User creation spoke +# +# 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 +# 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. +# + +import os +import copy +from pyanaconda.flags import flags +from pyanaconda.i18n import _, CN_ +from pyanaconda.users import cryptPassword, validatePassword, guess_username, check_username + +from pyanaconda.ui.gui.spokes import NormalSpoke +from pyanaconda.ui.gui import GUIObject +from pyanaconda.ui.categories.user_settings import UserSettingsCategory +from pyanaconda.ui.helpers import InputCheck +from pyanaconda.ui.gui.helpers import GUISpokeInputCheckHandler, GUIDialogInputCheckHandler +from pyanaconda.ui.gui.utils import blockedHandler, set_password_visibility + +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, PASSWORD_DONE_TWICE,\ + PW_ASCII_CHARS, PASSWORD_ASCII,\ + LUKS_PASSWORD_EMPTY_ERROR, LUKS_PASSWORD_CONFIRM_ERROR_GUI, LUKS_PASSWORD_STRENGTH_DESC,\ + LUKS_PASSWORD_WEAK, LUKS_PASSWORD_WEAK_WITH_ERROR, LUKS_PASSWORD_WEAK_CONFIRM,\ + LUKS_PASSWORD_WEAK_CONFIRM_WITH_ERROR, LUKS_PASSWORD_DONE_TWICE,\ + LUKS_PASSWORD_ASCII +from pyanaconda.regexes import GECOS_VALID, GROUPNAME_VALID, GROUPLIST_FANCY_PARSE + +__all__ = ["UserSpoke"] + +class AdvancedUserDialog(GUIObject, GUIDialogInputCheckHandler): + """ + .. inheritance-diagram:: AdvancedUserDialog + :parts: 3 + """ + builderObjects = ["advancedUserDialog", "uid", "gid"] + mainWidgetName = "advancedUserDialog" + uiFile = "spokes/advanced_user.glade" + + def _validateGroups(self, inputcheck): + groups_string = self.get_input(inputcheck.input_obj) + + # Pass if the string is empty + if not groups_string: + return InputCheck.CHECK_OK + + # Check each group name in the list + for group in groups_string.split(","): + group_name = GROUPLIST_FANCY_PARSE.match(group).group('name') + if not GROUPNAME_VALID.match(group_name): + return _("Invalid group name: %s") % group_name + + return InputCheck.CHECK_OK + + def __init__(self, user, data): + GUIObject.__init__(self, data) + + saveButton = self.builder.get_object("save_button") + GUIDialogInputCheckHandler.__init__(self, saveButton) + + self._user = user + + # Track whether the user has requested a home directory other + # than the default. This way, if the home directory is left as + # the default, the default will change if the username changes. + # Otherwise, once the directory is set it stays that way. + self._origHome = None + + if self._user.homedir: + self._homeSet = True + else: + self._homeSet = False + + def _grabObjects(self): + 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") + + def initialize(self): + GUIObject.initialize(self) + + self._grabObjects() + + # Validate the group input box + self.add_check(self._tGroups, self._validateGroups) + + def refresh(self): + if self._user.homedir: + homedir = self._user.homedir + elif self._user.name: + homedir = "/home/" + self._user.name + + self._tHome.set_text(homedir) + self._origHome = homedir + + self._cUid.set_active(bool(self._user.uid)) + self._cGid.set_active(bool(self._user.gid)) + + self._spinUid.update() + self._spinGid.update() + + self._tGroups.set_text(", ".join(self._user.groups)) + + def apply(self): + # Copy data from the UI back to the kickstart object + homedir = self._tHome.get_text() + + # If the user cleared the home directory, revert back to the + # default + if not homedir: + self._homeSet = False + self._user.homedir = None + # If the user modified the home directory input, save that the + # home directory has been modified and use the value. + elif self._origHome != homedir: + self._homeSet = True + + if not os.path.isabs(homedir): + homedir = "/" + homedir + self._user.homedir = homedir + + # Otherwise leave the home directory alone. If the home + # directory is currently the default value, the next call + # to refresh() will update the input text to reflect + # changes in the username. + + 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 + + # ''.split(',') returns [''] instead of [], which is not what we want + self._user.groups = [g.strip() for g in self._tGroups.get_text().split(",") if g] + + def run(self): + self.window.show() + while True: + rc = self.window.run() + + #OK clicked + if rc == 1: + # Input checks pass + if self.on_ok_clicked(): + self.apply() + break + # Input checks fail, try again + else: + continue + + #Cancel clicked, window destroyed... + else: + break + + self.window.hide() + return rc + + def on_uid_checkbox_toggled(self, togglebutton, data=None): + # Set the UID spinner sensitivity based on the UID checkbox + self._spinUid.set_sensitive(togglebutton.get_active()) + + def on_gid_checkbox_toggled(self, togglebutton, data=None): + # Same as above, for GID + self._spinGid.set_sensitive(togglebutton.get_active()) + + def on_uid_mnemonic_activate(self, widget, group_cycling, user_data=None): + # If this is the only widget with the mnemonic (group_cycling is False), + # and the checkbox is not currently toggled, toggle the checkbox and + # then set the focus to the UID spinner + if not group_cycling and not widget.get_active(): + widget.set_active(True) + self._spinUid.grab_focus() + return True + + # Otherwise just use the default signal handler + return False + + def on_gid_mnemonic_activate(self, widget, group_cycling, user_data=None): + # Same as above, but for GID + if not group_cycling and not widget.get_active(): + widget.set_active(True) + self._spinGid.grab_focus() + return True + + return False + +class UserSpoke(NormalSpoke, GUISpokeInputCheckHandler): + """ + .. inheritance-diagram:: UserSpoke + :parts: 3 + """ + builderObjects = ["userCreationWindow"] + + mainWidgetName = "userCreationWindow" + focusWidgetName = "t_username" + uiFile = "spokes/user.glade" + helpFile = "UserSpoke.xml" + + category = UserSettingsCategory + + icon = "avatar-default-symbolic" + title = CN_("GUI|Spoke", "_USER CREATION") + + @classmethod + def should_run(cls, environment, data): + # 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 len(data.user.userList) == 0: + return True + else: + return False + + def __init__(self, *args): + NormalSpoke.__init__(self, *args) + GUISpokeInputCheckHandler.__init__(self) + + def initialize(self): + NormalSpoke.initialize(self) + + # Create a new UserData object to store this spoke's state + # as well as the state of the advanced user dialog. + if self.data.user.userList: + self._user = copy.copy(self.data.user.userList[0]) + else: + self._user = self.data.UserData() + + self._wheel = self.data.GroupData(name="wheel") + self._qubes = self.data.GroupData(name="qubes") + + self._groupDict = {"wheel": self._wheel, "qubes": self._qubes} + + # placeholders for the text boxes + 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.lukspw = self.builder.get_object("t_lukspassword") + self.luksconfirm = self.builder.get_object("t_verifylukspassword") + + # Counters for checks that ask the user to click Done to confirm + self._waiveStrengthClicks = 0 + self._waiveASCIIClicks = 0 + + self.guesser = True + + self.pw_bar = self.builder.get_object("password_bar") + self.pw_label = self.builder.get_object("password_label") + self.lukspw_bar = self.builder.get_object("lukspassword_bar") + self.lukspw_label = self.builder.get_object("lukspassword_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) + self.pw_bar.add_offset_value("full", 4) + self.lukspw_bar.add_offset_value("low", 2) + self.lukspw_bar.add_offset_value("medium", 3) + self.lukspw_bar.add_offset_value("high", 4) + self.lukspw_bar.add_offset_value("full", 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._password_kickstarted = self.data.user.seen + + # 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 strong password is specified, does it contain non-ASCII data? + # - if a password is required, is there any data in the confirm box? + self.add_check(self.pw, self._checkPasswordEmpty) + self.add_check(self.lukspw, self._checkPasswordEmpty) + + # the password confirmation needs to be checked whenever either of the password + # fields change. attach to the confirm field so that errors focus on confirm, + # and check changes to the password field in password_changed + self._confirm_check = self.add_check(self.confirm, self._checkPasswordConfirm) + self._luksconfirm_check = self.add_check(self.luksconfirm, self._checkPasswordConfirm) + + # Keep a reference to these checks, since they have to be manually run for the + # click Done twice check. + self._pwStrengthCheck = self.add_check(self.pw, self._checkPasswordStrength) + self._pwASCIICheck = self.add_check(self.pw, self._checkPasswordASCII) + self._lukspwStrengthCheck = self.add_check(self.lukspw, self._checkPasswordStrength) + self._lukspwASCIICheck = self.add_check(self.lukspw, self._checkPasswordASCII) + + self.add_check(self.confirm, self._checkPasswordEmpty) + self.add_check(self.luksconfirm, self._checkPasswordEmpty) + + self.add_check(self.username, self._checkUsername) + + # Modify the GUI based on the kickstart and policy information + # This needs to happen after the input checks have been created, since + # the Gtk signal handlers use the input check variables. + if self._password_kickstarted: + self.pw.set_placeholder_text(_("The password was set by kickstart.")) + self.confirm.set_placeholder_text(_("The password was set by kickstart.")) + + # set the visibility of the password entries + set_password_visibility(self.pw, False) + set_password_visibility(self.confirm, False) + set_password_visibility(self.lukspw, False) + set_password_visibility(self.luksconfirm, False) + + def refresh(self): + # Enable the input checks in case they were disabled on the last exit + for check in self.checks: + check.enabled = True + + self.username.set_text(self._user.name) + + self.pw.emit("changed") + self.confirm.emit("changed") + self.lukspw.emit("changed") + self.luksconfirm.emit("changed") + + @property + def status(self): + if len(self.data.user.userList) == 0: + return _("No user will be created") + elif "wheel" 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): + return not flags.automatedInstall + + 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.pw.get_text(): + self._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("") + + self._user.name = self.username.get_text() + self._user.lukspassword = self.lukspw.get_text() + + if "wheel" not in self._user.groups: + self._user.groups.append("wheel") + if "qubes" not in self._user.groups: + self._user.groups.append("qubes") + + # Copy the spoke data back to kickstart + # If the user name is not set, no user will be created. + if self._user.name: + ksuser = copy.copy(self._user) + + if not self.data.user.userList: + self.data.user.userList.append(ksuser) + else: + self.data.user.userList[0] = ksuser + elif self.data.user.userList: + self.data.user.userList.pop(0) + + @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 not self.policy.changesok) + + @property + def completed(self): + return len(self.data.user.userList) > 0 + + def _updatePwQuality(self, empty, strength, luks): + """This method updates the password indicators according + to the password entered by the user. + """ + # If the password is empty, clear the strength bar + if empty: + val = 0 + elif strength < 50: + val = 1 + elif strength < 75: + val = 2 + elif strength < 90: + val = 3 + else: + val = 4 + + if luks: + text = _(PASSWORD_STRENGTH_DESC[val]) + else: + text = _(PASSWORD_STRENGTH_DESC[val]) + + if luks: + self.lukspw_bar.set_value(val) + self.lukspw_label.set_text(text) + else: + self.pw_bar.set_value(val) + self.pw_label.set_text(text) + + def password_changed(self, editable=None, data=None): + """Update the password strength level bar""" + # Reset the counters used for the "press Done twice" logic + self._waiveStrengthClicks = 0 + self._waiveASCIIClicks = 0 + + # Update the password/confirm match check on changes to the main password field + self._confirm_check.update_check_status() + self._luksconfirm_check.update_check_status() + + def on_password_icon_clicked(self, entry, icon_pos, event): + """Called by Gtk callback when the icon of a password entry is clicked.""" + set_password_visibility(entry, not entry.get_visibility()) + + def on_username_set_by_user(self, editable, data=None): + """Called by Gtk on user-driven changes to the username field. + + This handler is blocked during changes from the username guesser. + """ + + # If the user set a user name, turn off the username guesser. + # If the user cleared the username, turn it back on. + if editable.get_text(): + self.guesser = False + else: + self.guesser = True + + def username_changed(self, editable, data=None): + """Called by Gtk on all username changes.""" + + # Re-run the password checks against the new username + self.pw.emit("changed") + self.confirm.emit("changed") + + def _checkPasswordEmpty(self, inputcheck): + """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._password_kickstarted and not self.policy.changesok: + return InputCheck.CHECK_OK + + # Skip the check if no password is required + if self._password_kickstarted: + return InputCheck.CHECK_OK + elif not self.get_input(inputcheck.input_obj): + if inputcheck.input_obj == self.pw: + return _(PASSWORD_EMPTY_ERROR) + if inputcheck.input_obj == self.confirm: + return _(PASSWORD_CONFIRM_ERROR_GUI) + elif inputcheck.input_obj == self.lukspw: + return _(LUKS_PASSWORD_EMPTY_ERROR) + else: + return _(LUKS_PASSWORD_CONFIRM_ERROR_GUI) + else: + return InputCheck.CHECK_OK + + def _checkPasswordConfirm(self, inputcheck): + """If the user has entered confirmation data, check whether it matches the password.""" + + # Skip the check if no password is required + if self._password_kickstarted: + result = InputCheck.CHECK_OK + elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()): + result = _(PASSWORD_CONFIRM_ERROR_GUI) + elif self.luksconfirm.get_text() and (self.lukspw.get_text() != self.luksconfirm.get_text()): + result = _(LUKS_PASSWORD_CONFIRM_ERROR_GUI) + else: + result = InputCheck.CHECK_OK + + return result + + def _checkPasswordStrength(self, inputcheck): + """Update the error message based on password strength. + + The password strength check can be waived by pressing "Done" twice. This + is controlled through the self._waiveStrengthClicks 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 self._password_kickstarted: + return InputCheck.CHECK_OK + + # If the password is empty, clear the strength bar and skip this check + pw = self.get_input(inputcheck.input_obj) + # If input matches luks password, we are checking luks + if inputcheck.input_obj == self.lukspw: + luks = True + else: + luks = False + + if not pw: + self._updatePwQuality(True, 0, luks) + return InputCheck.CHECK_OK + + # determine the password strength + username = self.username.get_text() + valid, pwstrength, error = validatePassword(pw, username, minlen=self.policy.minlen) + + # set the strength bar + self._updatePwQuality(False, pwstrength, luks) + + # If the password failed the validity check, fail this check + if not valid and error: + return error + + if pwstrength < self.policy.minquality: + # If Done has been clicked twice, waive the check + if self._waiveStrengthClicks > 1: + return InputCheck.CHECK_OK + elif self._waiveStrengthClicks == 1: + if error: + if luks: + return _(LUKS_PASSWORD_WEAK_CONFIRM_WITH_ERROR) % error + else: + return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % error + else: + if luks: + return _(LUKS_PASSWORD_WEAK_CONFIRM) + else: + return _(PASSWORD_WEAK_CONFIRM) + else: + # non-strict allows done to be clicked twice + if self.policy.strict: + done_msg = "" + else: + if luks: + done_msg = _(LUKS_PASSWORD_DONE_TWICE) + else: + done_msg = _(PASSWORD_DONE_TWICE) + + if error: + if luks: + return _(LUKS_PASSWORD_WEAK_WITH_ERROR) % error + " " + done_msg + else: + return _(PASSWORD_WEAK_WITH_ERROR) % error + " " + done_msg + else: + if luks: + return _(LUKS_PASSWORD_WEAK) % done_msg + else: + return _(PASSWORD_WEAK) % done_msg + else: + return InputCheck.CHECK_OK + + def _checkPasswordASCII(self, inputcheck): + """Set an error message if the password contains non-ASCII characters. + + Like the password strength check, this check can be bypassed by + pressing Done twice. + """ + + # If Done has been clicked, waive the check + if self._waiveASCIIClicks > 0: + return InputCheck.CHECK_OK + + # If input matches luks password, we are checking luks + if inputcheck.input_obj == self.lukspw: + luks = True + else: + luks = False + + password = self.get_input(inputcheck.input_obj) + if password and any(char not in PW_ASCII_CHARS for char in password): + if luks: + return _(LUKS_PASSWORD_ASCII) + else: + return _(PASSWORD_ASCII) + + return InputCheck.CHECK_OK + + def _checkUsername(self, inputcheck): + name = self.get_input(inputcheck.input_obj) + # Allow empty usernames so the spoke can be exited without creating a user + if name == "": + return InputCheck.CHECK_OK + + valid, msg = check_username(name) + if valid: + return InputCheck.CHECK_OK + else: + return msg or _("Invalid user name") + + def on_back_clicked(self, button): + # 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 not self.policy.strict and failed_check == self._pwStrengthCheck: + self._waiveStrengthClicks += 1 + self._pwStrengthCheck.update_check_status() + elif failed_check == self._pwASCIICheck: + self._waiveASCIIClicks += 1 + self._pwASCIICheck.update_check_status() + + # If there is no user set, skip the checks + if not self.username.get_text(): + for check in self.checks: + check.enabled = False + + if GUISpokeInputCheckHandler.on_back_clicked(self, button): + NormalSpoke.on_back_clicked(self, button) diff --git a/purism/pyanaconda/users.py b/purism/pyanaconda/users.py new file mode 100644 index 00000000..a41b37e1 --- /dev/null +++ b/purism/pyanaconda/users.py @@ -0,0 +1,485 @@ +# +# users.py: Code for creating user accounts and setting the root password +# +# Copyright (C) 2006, 2007, 2008 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 . +# + +# Used for ascii_letters and digits constants +import os +import os.path +import subprocess +from contextlib import contextmanager +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 +from pyanaconda.regexes import GROUPLIST_FANCY_PARSE, USERNAME_VALID, PORTABLE_FS_CHARS +import crypt +from pyanaconda.i18n import _ +import re + +import logging +log = logging.getLogger("anaconda") + +def getPassAlgo(authconfigStr): + """ Reads the auth string and returns a string indicating our desired + password encoding algorithm. + """ + if authconfigStr.find("--enablemd5") != -1 or authconfigStr.find("--passalgo=md5") != -1: + return 'md5' + elif authconfigStr.find("--passalgo=sha256") != -1: + return 'sha256' + elif authconfigStr.find("--passalgo=sha512") != -1: + return 'sha512' + else: + return None + +def cryptPassword(password, algo=None): + salts = {'md5': crypt.METHOD_MD5, + 'sha256': crypt.METHOD_SHA256, + 'sha512': crypt.METHOD_SHA512} + + if algo not in salts: + algo = 'sha512' + + cryptpw = crypt.crypt(password, salts[algo]) + if cryptpw is None: + exn = PasswordCryptError(algo=algo) + if errorHandler.cb(exn) == ERROR_RAISE: + raise exn + + 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 + username, it will tell if this password can be used at all, how + strong the password is on a scale of 1-100, and, if the password is + unusable, why it is unusuable. + + This function uses libpwquality to check the password strength. + pwquality will raise a PWQError on a weak password, which, honestly, + is kind of dumb behavior. A weak password isn't exceptional, it's what + we're asking about! Anyway, this function does not raise PWQError. If + the password fails the PWQSettings conditions, the first member of the + return tuple will be False and the second member of the tuple will be 0. + + :param pw: the password to check + :type pw: string + + :param user: the username for which the password is being set. If no + username is provided, "root" will be used. Use user=None + to disable the username check. + :type user: string + + :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 + """ + + valid = True + message = None + strength = 0 + + if settings is None: + # Generate a default PWQSettings once and save it as a member of this function + if not hasattr(validatePassword, "pwqsettings"): + validatePassword.pwqsettings = pwquality.PWQSettings() + validatePassword.pwqsettings.read_config() + 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) + except pwquality.PWQError as e: + # 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.args[1] + + return (valid, strength, message) + +def check_username(name): + if name in os.listdir("/") + ["root", "home", "daemon", "system", "qubes"]: + return (False, _("User name is reserved for system: %s") % name) + + if name.startswith("-"): + return (False, _("User name cannot start with '-' character")) + + # Final '$' allowed for Samba + if name.endswith("$"): + sname = name[:-1] + else: + sname = name + match = re.search(r'[^' + PORTABLE_FS_CHARS + r']', sname) + if match: + return (False, _("User name cannot contain character: '%s'") % match.group()) + + if len(name) > 32: + return (False, _("User name must be shorter than 33 characters")) + + # Check also with THE regexp to be sure + if not USERNAME_VALID.match(name): + return (False, None) + + return (True, None) + +def guess_username(fullname): + fullname = fullname.split() + + # use last name word (at the end in most of the western countries..) + if len(fullname) > 0: + username = fullname[-1].lower() + else: + username = u"" + + # and prefix it with the first name initial + if len(fullname) > 1: + username = fullname[0][0].lower() + username + + username = strip_accents(username) + return username + +class Users(object): + def _getpwnam(self, user_name, root): + """Like pwd.getpwnam, but is able to use a different root. + + Also just returns the pwd structure as a list, because of laziness. + """ + with open(root + "/etc/passwd", "r") as f: + for line in f: + fields = line.split(":") + if fields[0] == user_name: + return fields + + return None + + def _getgrnam(self, group_name, root): + """Like grp.getgrnam, but able to use a different root. + + Just returns the grp structure as a list, same reason as above. + """ + with open(root + "/etc/group", "r") as f: + for line in f: + fields = line.split(":") + if fields[0] == group_name: + return fields + + return None + + def _getgrgid(self, gid, root): + """Like grp.getgrgid, but able to use a different root. + + Just returns the fields as a list of strings. + """ + # Conver the probably-int GID to a string + gid = str(gid) + + with open(root + "/etc/group", "r") as f: + for line in f: + fields = line.split(":") + if fields[2] == gid: + return fields + + return None + + @contextmanager + def _ensureLoginDefs(self, root): + """Runs a command after creating /etc/login.defs, if necessary. + + groupadd and useradd need login.defs to exist in the chroot, and if + someone is doing a cloud image install or some kind of --nocore thing + it may not. An empty one is ok, though. If it's missing, create it, + run the command, then clean it up. + """ + login_defs_path = root + '/etc/login.defs' + if not os.path.exists(login_defs_path): + open(login_defs_path, "w").close() + login_defs_created = True + else: + login_defs_created = False + + yield + + if login_defs_created: + os.unlink(login_defs_path) + + def createGroup(self, group_name, **kwargs): + """Create a new user on the system with the given name. Optional kwargs: + + :keyword int gid: The GID for the new user. If none is given, the next available one is used. + :keyword str root: The directory of the system to create the new user in. + homedir will be interpreted relative to this. Defaults + to iutil.getSysroot(). + """ + root = kwargs.get("root", iutil.getSysroot()) + + if self._getgrnam(group_name, root): + raise ValueError("Group %s already exists" % group_name) + + args = ["-R", root] + if kwargs.get("gid") is not None: + args.extend(["-g", str(kwargs["gid"])]) + + args.append(group_name) + with self._ensureLoginDefs(root): + status = iutil.execWithRedirect("groupadd", args) + + if status == 4: + raise ValueError("GID %s already exists" % kwargs.get("gid")) + elif status == 9: + raise ValueError("Group %s already exists" % group_name) + elif status != 0: + raise OSError("Unable to create group %s: status=%s" % (group_name, status)) + + def createUser(self, user_name, *args, **kwargs): + """Create a new user on the system with the given name. Optional kwargs: + + :keyword str algo: The password algorithm to use in case isCrypted=True. + If none is given, the cryptPassword default is used. + :keyword str gecos: The GECOS information (full name, office, phone, etc.). + Defaults to "". + :keyword groups: A list of group names the user should be added to. + Each group name can contain an optional GID in parenthesis, + such as "groupName(5000)". Defaults to []. + :type groups: list of str + :keyword str homedir: The home directory for the new user. Defaults to + /home/. + :keyword bool isCrypted: Is the password kwargs already encrypted? Defaults + to False. + :keyword bool lock: Is the new account locked by default? Defaults to + False. + :keyword str password: The password. See isCrypted for how this is interpreted. + If the password is "" then the account is created + with a blank password. If None or False the account will + be left in its initial state (locked) + :keyword str root: The directory of the system to create the new user + in. homedir will be interpreted relative to this. + Defaults to iutil.getSysroot(). + :keyword str shell: The shell for the new user. If none is given, the + login.defs default is used. + :keyword int uid: The UID for the new user. If none is given, the next + available one is used. + :keyword int gid: The GID for the new user. If none is given, the next + available one is used. + """ + + root = kwargs.get("root", iutil.getSysroot()) + + if self.checkUserExists(user_name, root): + raise ValueError("User %s already exists" % user_name) + + args = ["-R", root] + + # Split the groups argument into a list of (username, gid or None) tuples + # the gid, if any, is a string since that makes things simpler + group_gids = [GROUPLIST_FANCY_PARSE.match(group).groups() + for group in kwargs.get("groups", [])] + + # If a specific gid is requested: + # - check if a group already exists with that GID. i.e., the user's + # GID should refer to a system group, such as users. If so, just set + # the GID. + # - check if a new group is requested with that GID. If so, set the GID + # and let the block below create the actual group. + # - if neither of those are true, create a new user group with the requested + # GID + # otherwise use -U to create a new user group with the next available GID. + if kwargs.get("gid", None): + if not self._getgrgid(kwargs['gid'], root) and \ + not any(gid[1] == str(kwargs['gid']) for gid in group_gids): + self.createGroup(user_name, gid=kwargs['gid'], root=root) + + args.extend(['-g', str(kwargs['gid'])]) + else: + args.append('-U') + + # If any requested groups do not exist, create them. + group_list = [] + for group_name, gid in group_gids: + existing_group = self._getgrnam(group_name, root) + + # Check for a bad GID request + if gid and existing_group and gid != existing_group[2]: + raise ValueError("Group %s already exists with GID %s" % (group_name, gid)) + + # Otherwise, create the group if it does not already exist + if not existing_group: + self.createGroup(group_name, gid=gid, root=root) + group_list.append(group_name) + + if group_list: + args.extend(['-G', ",".join(group_list)]) + + if kwargs.get("homedir"): + homedir = kwargs["homedir"] + else: + homedir = "/home/" + user_name + + # useradd expects the parent directory tree to exist. + parent_dir = iutil.parent_dir(root + homedir) + + # If root + homedir came out to "/", such as if we're creating the sshpw user, + # parent_dir will be empty. Don't create that. + if parent_dir: + iutil.mkdirChain(parent_dir) + + args.extend(["-d", homedir]) + + # Check whether the directory exists or if useradd should create it + mk_homedir = not os.path.exists(root + homedir) + if mk_homedir: + args.append("-m") + else: + args.append("-M") + + if kwargs.get("shell"): + args.extend(["-s", kwargs["shell"]]) + + if kwargs.get("uid"): + args.extend(["-u", str(kwargs["uid"])]) + + if kwargs.get("gecos"): + args.extend(["-c", kwargs["gecos"]]) + + args.append(user_name) + with self._ensureLoginDefs(root): + status = iutil.execWithRedirect("useradd", args) + + if status == 4: + raise ValueError("UID %s already exists" % kwargs.get("uid")) + elif status == 6: + raise ValueError("Invalid groups %s" % kwargs.get("groups", [])) + elif status == 9: + raise ValueError("User %s already exists" % user_name) + elif status != 0: + raise OSError("Unable to create user %s: status=%s" % (user_name, status)) + + if not mk_homedir: + try: + stats = os.stat(root + homedir) + orig_uid = stats.st_uid + orig_gid = stats.st_gid + + # Gett the UID and GID of the created user + pwent = self._getpwnam(user_name, root) + + log.info("Home directory for the user %s already existed, " + "fixing the owner and SELinux context.", user_name) + # home directory already existed, change owner of it properly + iutil.chown_dir_tree(root + homedir, + int(pwent[2]), int(pwent[3]), + orig_uid, orig_gid) + iutil.execWithRedirect("restorecon", ["-r", root + homedir]) + except OSError as e: + log.critical("Unable to change owner of existing home directory: %s", e.strerror) + raise + + pw = kwargs.get("password", False) + crypted = kwargs.get("isCrypted", False) + algo = kwargs.get("algo", None) + lock = kwargs.get("lock", False) + + self.setUserPassword(user_name, pw, crypted, lock, algo, root) + + def setLuksPassword(self, lukspassword): + """Set a new lukspassword for hard-coded dom0 device + """ + f = open("/dev/shm/oldpass", "w") + f.write('') + f.close() + f = open("/dev/shm/newpass", "w") + f.write(lukspassword) + f.close() + + try: + iutil.execWithRedirect("cryptsetup", ["luksChangeKey", "--key-file", "/dev/shm/oldpass", "/dev/sda2", "/dev/shm/newpass"]) + iutil.execWithRedirect("shred", ["-u", "/dev/shm/oldpass", "/dev/shm/newpass"]) + except OSError as e: + log.critical("Unable to change luks password: %s", e.strerror) + raise + + def checkUserExists(self, username, root=None): + if self._getpwnam(username, root): + return True + + return False + + def setUserPassword(self, username, password, isCrypted, lock, algo=None, root="/"): + # Only set the password if it is a string, including the empty string. + # Otherwise leave it alone (defaults to locked for new users) and reset sp_lstchg + if password or password == "": + if password == "": + log.info("user account %s setup with no password", username) + elif not isCrypted: + password = cryptPassword(password, algo) + + if lock: + password = "!" + password + log.info("user account %s locked", username) + + proc = iutil.startProgram(["chpasswd", "-R", root, "-e"], stdin=subprocess.PIPE) + proc.communicate(("%s:%s\n" % (username, password)).encode("utf-8")) + if proc.returncode != 0: + raise OSError("Unable to set password for new user: status=%s" % proc.returncode) + + # Reset sp_lstchg to an empty string. On systems with no rtc, this + # field can be set to 0, which has a special meaning that the password + # must be reset on the next login. + iutil.execWithRedirect("chage", ["-R", root, "-d", "", username]) + + def setRootPassword(self, password, isCrypted=False, isLocked=False, algo=None, root="/"): + return self.setUserPassword("root", password, isCrypted, isLocked, algo, root) + + def setUserSshKey(self, username, key, **kwargs): + root = kwargs.get("root", iutil.getSysroot()) + + pwent = self._getpwnam(username, root) + if not pwent: + raise ValueError("setUserSshKey: user %s does not exist" % username) + + homedir = root + pwent[5] + if not os.path.exists(homedir): + log.error("setUserSshKey: home directory for %s does not exist", username) + raise ValueError("setUserSshKey: home directory for %s does not exist" % username) + + uid = pwent[2] + gid = pwent[3] + + sshdir = os.path.join(homedir, ".ssh") + if not os.path.isdir(sshdir): + os.mkdir(sshdir, 0o700) + os.chown(sshdir, int(uid), int(gid)) + + authfile = os.path.join(sshdir, "authorized_keys") + authfile_existed = os.path.exists(authfile) + with iutil.open_with_perm(authfile, "a", 0o600) as f: + f.write(key + "\n") + + # Only change ownership if we created it + if not authfile_existed: + os.chown(authfile, int(uid), int(gid)) + iutil.execWithRedirect("restorecon", ["-r", sshdir])