[master 1/2] Get rid of libuser. (#1255066)
dashea
installerbot-noreply at redhat.com
Thu Aug 20 16:07:52 UTC 2015
From: David Shea <dshea at redhat.com>
The changes made to libuser for python3 support have made it difficult
to work with since it must now be imported from a UTF-8 locale, working
with it in the first place already involved a bunch of gross hacks, and
there's no reason why adding users and groups should need this
complicated of a library. Dump it and use the moderately less quirky
command-line tools.
---
anaconda | 6 -
anaconda.spec.in | 1 -
pyanaconda/install.py | 3 +-
pyanaconda/kickstart.py | 6 +-
pyanaconda/users.py | 444 +++++++++++++++++++++---------------------------
utils/handle-sshpw | 4 -
6 files changed, 200 insertions(+), 264 deletions(-)
diff --git a/anaconda b/anaconda
index 1ccad07..b57e7e9 100755
--- a/anaconda
+++ b/anaconda
@@ -386,8 +386,6 @@ def setupPythonPath():
sys.path.extend(ADDON_PATHS)
def setupEnvironment():
- from pyanaconda.users import createLuserConf
-
# This method is run before any threads are started, so this is the one
# point where it's ok to modify the environment.
# pylint: disable=environment-modify
@@ -416,10 +414,6 @@ def setupEnvironment():
else:
os.environ["DISPLAY"] = ":%s" % constants.X_DISPLAY_NUMBER
- # Call createLuserConf now to setup $LIBUSER_CONF
- # the config file can change later but the environment variable cannot
- createLuserConf(iutil.getSysroot())
-
def setupLoggingFromOpts(options):
if (options.debug or options.updateSrc) and not options.loglevel:
# debugging means debug logging if an explicit level hasn't been st
diff --git a/anaconda.spec.in b/anaconda.spec.in
index 10baf31..13fb99e 100644
--- a/anaconda.spec.in
+++ b/anaconda.spec.in
@@ -94,7 +94,6 @@ Requires: python3-requests-ftp
Requires: python3-kickstart >= %{pykickstartver}
Requires: langtable-data >= %{langtablever}
Requires: langtable-python3 >= %{langtablever}
-Requires: libuser-python3
Requires: authconfig
Requires: firewalld >= %{firewalldver}
Requires: util-linux >= %{utillinuxver}
diff --git a/pyanaconda/install.py b/pyanaconda/install.py
index 43655c9..e0618fc 100644
--- a/pyanaconda/install.py
+++ b/pyanaconda/install.py
@@ -25,7 +25,7 @@
from blivet.devices import BTRFSDevice
from pyanaconda.bootloader import writeBootLoader
from pyanaconda.progress import progress_report, progress_message, progress_step, progress_complete, progress_init
-from pyanaconda.users import createLuserConf, getPassAlgo, Users
+from pyanaconda.users import Users
from pyanaconda import flags
from pyanaconda import iutil
from pyanaconda import timezone
@@ -90,7 +90,6 @@ def doConfiguration(storage, payload, ksdata, instClass):
# Creating users and groups requires some pre-configuration.
with progress_report(_("Creating users")):
- createLuserConf(iutil.getSysroot(), algoname=getPassAlgo(ksdata.authconfig.authconfig))
u = Users()
ksdata.rootpw.execute(storage, ksdata, instClass, u)
ksdata.group.execute(storage, ksdata, instClass, u)
diff --git a/pyanaconda/kickstart.py b/pyanaconda/kickstart.py
index a2b5899..0c72750 100644
--- a/pyanaconda/kickstart.py
+++ b/pyanaconda/kickstart.py
@@ -99,9 +99,6 @@ def run(self, chroot):
else:
scriptRoot = "/"
- # Environment variables that cause problems for %post scripts
- env_prune = ["LIBUSER_CONF"]
-
(fd, path) = tempfile.mkstemp("", "ks-script-", scriptRoot + "/tmp")
iutil.eintr_retry_call(os.write, fd, self.script.encode("utf-8"))
@@ -128,8 +125,7 @@ def run(self, chroot):
with open(messages, "w") as fp:
rc = iutil.execWithRedirect(self.interp, ["/tmp/%s" % os.path.basename(path)],
stdout=fp,
- root=scriptRoot,
- env_prune=env_prune)
+ root=scriptRoot)
if rc != 0:
log.error("Error code %s running the kickstart script at line %s", rc, self.lineno)
diff --git a/pyanaconda/users.py b/pyanaconda/users.py
index 5eee4fc..9e8f03f 100644
--- a/pyanaconda/users.py
+++ b/pyanaconda/users.py
@@ -19,7 +19,6 @@
# Author(s): Chris Lumens <clumens at redhat.com>
#
-import libuser
# Used for ascii_letters and digits constants
import string # pylint: disable=deprecated-module
import crypt
@@ -27,6 +26,9 @@
import tempfile
import os
import os.path
+import subprocess
+import shutil
+from contextlib import contextmanager
from pyanaconda import iutil
import pwquality
from pyanaconda.iutil import strip_accents
@@ -37,51 +39,6 @@
import logging
log = logging.getLogger("anaconda")
-def createLuserConf(instPath, algoname='sha512'):
- """ Writes a libuser.conf for instPath.
-
- This must be called before User() is instantiated the first time
- so that libuser.admin will use the temporary config file.
- """
-
- # If LIBUSER_CONF is not set, create a new temporary file
- if "LIBUSER_CONF" not in os.environ:
- (fp, fn) = tempfile.mkstemp(prefix="libuser.")
- log.info("created new libuser.conf at %s with instPath=\"%s\"", fn, instPath)
- fd = os.fdopen(fp, "w")
- # This is only ok if createLuserConf is first called before threads are started
- os.environ["LIBUSER_CONF"] = fn # pylint: disable=environment-modify
- else:
- fn = os.environ["LIBUSER_CONF"]
- log.info("Clearing libuser.conf at %s", fn)
- fd = open(fn, "w")
-
- buf = """
-[defaults]
-skeleton = %(instPath)s/etc/skel
-mailspooldir = %(instPath)s/var/mail
-crypt_style = %(algo)s
-modules = files shadow
-create_modules = files shadow
-[files]
-directory = %(instPath)s/etc
-[shadow]
-directory = %(instPath)s/etc
-""" % {"instPath": instPath, "algo": algoname}
-
- # Import login.defs if installed
- if os.path.exists(os.path.normpath(instPath + "/etc/login.defs")):
- buf += """
-[import]
-login_defs = %(instPath)s/etc/login.defs
-""" % {"instPath": instPath}
-
-
- fd.write(buf)
- fd.close()
-
- return fn
-
def getPassAlgo(authconfigStr):
""" Reads the auth string and returns a string indicating our desired
password encoding algorithm.
@@ -198,39 +155,49 @@ def guess_username(fullname):
username = strip_accents(username)
return username
-class Users:
- def __init__(self):
- self.admin = libuser.admin()
+class Users(object):
+ def _getpwnam(self, user_name, root):
+ """Like pwd.getpwnam, but is able to use a different root.
- def _prepareChroot(self, root):
- # Unfortunately libuser doesn't have an API to operate on a
- # chroot, so we hack it here by forking a child and calling
- # chroot() in that child's context.
+ 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
- childpid = os.fork()
- if not childpid:
- if not root in ["", "/"]:
- os.chroot(root)
- os.chdir("/")
- # This is ok because it's after a fork
- del(os.environ["LIBUSER_CONF"]) # pylint: disable=environment-modify
+ return None
- self.admin = libuser.admin()
+ def _groupExists(self, group_name, root):
+ """Returns whether a group with the given name already exists."""
+ with open(root + "/etc/group", "r") as f:
+ for line in f:
+ if line.split(":")[0] == group_name:
+ return True
- return childpid
+ return False
- def _finishChroot(self, childpid):
- assert childpid > 0
- try:
- status = iutil.eintr_retry_call(os.waitpid, childpid, 0)[1]
- except OSError as e:
- log.critical("exception from waitpid: %s %s", e.errno, e.strerror)
- return False
+ @contextmanager
+ def _ensureLoginDefs(self, root):
+ """Runs a command after creating /etc/login.defs, if necessary.
- if os.WIFEXITED(status) and (os.WEXITSTATUS(status) == 0):
- return True
+ 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:
- return False
+ 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:
@@ -241,27 +208,25 @@ def createGroup(self, group_name, **kwargs):
in. homedir will be interpreted relative to this.
Defaults to /mnt/sysimage.
"""
+ root = kwargs.get("root", iutil.getSysroot())
- childpid = self._prepareChroot(kwargs.get("root", iutil.getSysroot()))
- if childpid == 0:
- if self.admin.lookupGroupByName(group_name):
- log.error("Group %s already exists, not creating.", group_name)
- os._exit(1)
-
- groupEnt = self.admin.initGroup(group_name)
+ if self._groupExists(group_name, root):
+ raise ValueError("Group %s already exists" % group_name)
- if kwargs.get("gid", -1) >= 0:
- groupEnt.set(libuser.GIDNUMBER, kwargs["gid"])
+ args = ["-R", root]
+ if "gid" in kwargs:
+ args.extend(["-g", str(kwargs["gid"])])
- try:
- self.admin.addGroup(groupEnt)
- except RuntimeError as e:
- log.critical("Error when creating new group: %s", e)
- os._exit(1)
+ args.append(group_name)
+ with self._ensureLoginDefs(root):
+ status = iutil.execWithRedirect("groupadd", args)
- os._exit(0)
- else:
- return self._finishChroot(childpid)
+ if status == 4:
+ raise ValueError("GID %s already exists" % kwargs.get("gid", -1))
+ 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:
@@ -286,179 +251,166 @@ def createUser(self, user_name, *args, **kwargs):
in. homedir will be interpreted relative to this.
Defaults to /mnt/sysimage.
shell -- The shell for the new user. If none is given, the
- libuser default is used.
+ login.defs default is used.
uid -- The UID for the new user. If none is given, the next
available one is used.
gid -- The GID for the new user. If none is given, the next
available one is used.
+ mkmailspool -- Whether to create a /var/spool/mail file for the user.
+ Defaults to True.
"""
- childpid = self._prepareChroot(kwargs.get("root", iutil.getSysroot()))
- if childpid == 0:
- if self.admin.lookupUserByName(user_name):
- log.error("User %s already exists, not creating.", user_name)
- os._exit(1)
-
- userEnt = self.admin.initUser(user_name)
- groupEnt = self.admin.initGroup(user_name)
-
- gid = kwargs.get("gid") or -1
- if gid >= 0:
- groupEnt.set(libuser.GIDNUMBER, kwargs["gid"])
-
- grpLst = [grp for grp in map(self.admin.lookupGroupByName, kwargs.get("groups", [])) if grp]
- userEnt.set(libuser.GIDNUMBER, [groupEnt.get(libuser.GIDNUMBER)[0]] +
- [grp.get(libuser.GIDNUMBER)[0] for grp in grpLst])
-
- homedir = kwargs.get("homedir", None)
- if not homedir:
- homedir = "/home/" + user_name
- # libuser expects the parent directory tree to exist.
- parent_dir = iutil.parent_dir(homedir)
- if parent_dir:
- iutil.mkdirChain(parent_dir)
- userEnt.set(libuser.HOMEDIRECTORY, homedir)
-
- if kwargs.get("shell", False):
- userEnt.set(libuser.LOGINSHELL, kwargs["shell"])
-
- uid = kwargs.get("uid") or -1
- if uid >= 0:
- userEnt.set(libuser.UIDNUMBER, kwargs["uid"])
-
- if kwargs.get("gecos", False):
- userEnt.set(libuser.GECOS, kwargs["gecos"])
-
- # need to create home directory for the user or does it already exist?
- # userEnt.get returns lists (usually with a single item)
- mk_homedir = not os.path.exists(userEnt.get(libuser.HOMEDIRECTORY)[0])
- try:
- self.admin.addUser(userEnt, mkmailspool=kwargs.get("mkmailspool", True),
- mkhomedir=mk_homedir)
- except RuntimeError as e:
- log.critical("Error when creating new user: %s", e)
- os._exit(1)
+ root = kwargs.get("root", iutil.getSysroot())
- try:
- self.admin.addGroup(groupEnt)
- except RuntimeError as e:
- log.critical("Error when creating new group: %s", e)
- os._exit(1)
-
- if not mk_homedir:
- try:
- stats = os.stat(userEnt.get(libuser.HOMEDIRECTORY)[0])
- orig_uid = stats.st_uid
- orig_gid = stats.st_gid
-
- 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(userEnt.get(libuser.HOMEDIRECTORY)[0],
- userEnt.get(libuser.UIDNUMBER)[0],
- groupEnt.get(libuser.GIDNUMBER)[0],
- orig_uid, orig_gid)
- iutil.execWithRedirect("restorecon",
- ["-r", userEnt.get(libuser.HOMEDIRECTORY)[0]])
- except OSError as e:
- log.critical("Unable to change owner of existing home directory: %s",
- os.strerror)
- os._exit(1)
-
- pw = kwargs.get("password", False)
- try:
- if pw:
- if kwargs.get("isCrypted", False):
- password = kwargs["password"]
- else:
- password = cryptPassword(kwargs["password"], algo=kwargs.get("algo", None))
- self.admin.setpassUser(userEnt, password, True)
- userEnt.set(libuser.SHADOWLASTCHANGE, "")
- self.admin.modifyUser(userEnt)
- elif pw == "":
- # Setup the account with *NO* password
- self.admin.unlockUser(userEnt)
- log.info("user account %s setup with no password", user_name)
-
- if kwargs.get("lock", False):
- self.admin.lockUser(userEnt)
- log.info("user account %s locked", user_name)
- # setpassUser raises SystemError on failure, while unlockUser and lockUser
- # raise RuntimeError
- except (RuntimeError, SystemError) as e:
- log.critical("Unable to set password for new user: %s", e)
- os._exit(1)
-
- # Add the user to all the groups they should be part of.
- grpLst.append(self.admin.lookupGroupByName(user_name))
- try:
- for grp in grpLst:
- grp.add(libuser.MEMBERNAME, user_name)
- self.admin.modifyGroup(grp)
- except RuntimeError as e:
- log.critical("Unable to add user to groups: %s", e)
- os._exit(1)
-
- os._exit(0)
- else:
- return self._finishChroot(childpid)
+ if self.checkUserExists(user_name, root):
+ raise ValueError("User %s already exists" % user_name)
- def checkUserExists(self, username, root=None):
- childpid = self._prepareChroot(root)
+ args = ["-R", root]
- if childpid == 0:
- if self.admin.lookupUserByName(username):
- os._exit(0)
- else:
- os._exit(1)
+ # If a specific gid is requested, create the user group with that GID.
+ # Otherwise let useradd do it automatically.
+ if "gid" in kwargs:
+ self.createGroup(user_name, gid=kwargs['gid'], root=root)
+ args.extend(['-g', str(kwargs['gid'])])
+ else:
+ args.append('-U')
+
+ if "groups" in kwargs:
+ args.extend(['-G', ",".join(kwargs["groups"])])
+
+ homedir = kwargs.get("homedir", "/home/" + user_name)
+ # useradd expects the parent directory tree to exist.
+ parent_dir = iutil.parent_dir(root + homedir)
+ 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:
- return self._finishChroot(childpid)
+ args.append("-M")
- def setUserPassword(self, username, password, isCrypted, lock, algo=None):
- user = self.admin.lookupUserByName(username)
+ if "shell" in kwargs:
+ args.extend(["-s", kwargs["shell"]])
- if isCrypted:
- self.admin.setpassUser(user, password, True)
+ if "uid" in kwargs:
+ args.extend(["-u", str(kwargs["uid"])])
+
+ if "gecos" in kwargs:
+ args.extend(["-c", kwargs["gecos"]])
+
+ # The mail spool is annoying. We can control where it is created, but
+ # not whether it is created. So, have useradd create it, maybe, in a
+ # temp directory, remove that directory, and then if we actually wanted
+ # a spool file just make an empty file in /var/spool/mail.
+ spool_dir = tempfile.mkdtemp()
+ args.extend(["-K", "MAIL_DIR=%s" % spool_dir])
+
+ args.append(user_name)
+ try:
+ with self._ensureLoginDefs(root):
+ status = iutil.execWithRedirect("useradd", args)
+
+ if status == 4:
+ raise ValueError("UID %s already exists" % kwargs.get("uid", -1))
+ 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))
+ finally:
+ shutil.rmtree(spool_dir)
+
+ if kwargs.get("mkmailspool", True):
+ iutil.mkdirChain(root + "/var/spool/mail")
+ open(root + "/var/spool/mail/" + user_name, "w").close()
+
+ 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,
+ pwent[2], 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", "")
+ 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 checkUserExists(self, username, root=None):
+ if self._getpwnam(username, root):
+ return True
+
+ return False
+
+ def setUserPassword(self, username, password, isCrypted, lock, algo=None, root="/"):
+ if password:
+ if not isCrypted:
+ password = cryptPassword(password, algo)
else:
- self.admin.setpassUser(user, cryptPassword(password, algo=algo), True)
+ log.info("user account %s setup with no password", username)
if lock:
- self.admin.lockUser(user)
+ password = "!" + password
+ log.info("user account %s locked", username)
- user.set(libuser.SHADOWLASTCHANGE, "")
- return self.admin.modifyUser(user)
+ 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)
- def setRootPassword(self, password, isCrypted=False, isLocked=False, algo=None):
- return self.setUserPassword("root", password, isCrypted, isLocked, algo)
+ # 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):
- childpid = self._prepareChroot(kwargs.get("root", iutil.getSysroot()))
-
- if childpid == 0:
- user = self.admin.lookupUserByName(username)
- if not user:
- log.error("setUserSshKey: user %s does not exist", username)
- os._exit(1)
-
- homedir = user.get(libuser.HOMEDIRECTORY)[0]
- if not os.path.exists(homedir):
- log.error("setUserSshKey: home directory for %s does not exist", username)
- os._exit(1)
-
- sshdir = os.path.join(homedir, ".ssh")
- if not os.path.isdir(sshdir):
- os.mkdir(sshdir, 0o700)
- iutil.eintr_retry_call(os.chown, sshdir, user.get(libuser.UIDNUMBER)[0], user.get(libuser.GIDNUMBER)[0])
-
- authfile = os.path.join(sshdir, "authorized_keys")
- authfile_existed = os.path.exists(authfile)
- with iutil.open_with_perm(authfile, "a", 0o600) as f:
- f.write(key + "\n")
-
- # Only change ownership if we created it
- if not authfile_existed:
- iutil.eintr_retry_call(os.chown, authfile, user.get(libuser.UIDNUMBER)[0], user.get(libuser.GIDNUMBER)[0])
- iutil.execWithRedirect("restorecon", ["-r", sshdir])
- os._exit(0)
- else:
- return self._finishChroot(childpid)
+ 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)
+ os._exit(1)
+
+ uid = pwent[2]
+ gid = pwent[3]
+
+ sshdir = os.path.join(homedir, ".ssh")
+ if not os.path.isdir(sshdir):
+ os.mkdir(sshdir, 0o700)
+ iutil.eintr_retry_call(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:
+ iutil.eintr_retry_call(os.chown, authfile, int(uid), int(gid))
+ iutil.execWithRedirect("restorecon", ["-r", sshdir])
diff --git a/utils/handle-sshpw b/utils/handle-sshpw
index f6d0688..7a6fd71 100755
--- a/utils/handle-sshpw
+++ b/utils/handle-sshpw
@@ -41,10 +41,6 @@ ksparser.registerSection(NullSection(handler, sectionOpen="%addon"))
ksparser.registerSection(NullSection(handler, sectionOpen="%anaconda"))
ksparser.readKickstart(ksfile)
-# we need to have a libuser.conf that points to the installer root for
-# sshpw, but after that we start sshd, we need one that points to the
-# install target.
-luserConf = users.createLuserConf(instPath="")
u = users.Users()
userdata = ksparser.handler.sshpw.dataList()
--
To view this commit on github, visit https://github.com/rhinstaller/anaconda/commit/1199fe25e02937fe8cef346f6950587a65d1619c
More information about the anaconda-patches
mailing list