diff --git a/salt/modules/groupadd.py b/salt/modules/groupadd.py index e2e1560ab072..15dec6e89837 100644 --- a/salt/modules/groupadd.py +++ b/salt/modules/groupadd.py @@ -12,8 +12,12 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals import logging +import functools +import os from salt.ext import six +import salt.utils.files +import salt.utils.stringutils try: import grp except ImportError: @@ -40,6 +44,18 @@ def add(name, gid=None, system=False, root=None): ''' Add the specified group + name + Name of the new group + + gid + Use GID for the new group + + system + Create a system account + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -51,11 +67,12 @@ def add(name, gid=None, system=False, root=None): cmd.append('-g {0}'.format(gid)) if system and __grains__['kernel'] != 'OpenBSD': cmd.append('-r') - cmd.append(name) if root is not None: cmd.extend(('-R', root)) + cmd.append(name) + ret = __salt__['cmd.run_all'](cmd, python_shell=False) return not ret['retcode'] @@ -65,34 +82,53 @@ def delete(name, root=None): ''' Remove the named group + name + Name group to delete + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.delete foo ''' - cmd = ['groupdel', name] + cmd = ['groupdel'] if root is not None: cmd.extend(('-R', root)) + cmd.append(name) + ret = __salt__['cmd.run_all'](cmd, python_shell=False) return not ret['retcode'] -def info(name): +def info(name, root=None): ''' Return information about a group + name + Name of the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.info foo ''' + if root is not None: + getgrnam = functools.partial(_getgrnam, root=root) + else: + getgrnam = functools.partial(grp.getgrnam) + try: - grinfo = grp.getgrnam(name) + grinfo = getgrnam(name) except KeyError: return {} else: @@ -109,10 +145,16 @@ def _format_info(data): 'members': data.gr_mem} -def getent(refresh=False): +def getent(refresh=False, root=None): ''' Return info on all groups + refresh + Force a refresh of group information + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -123,41 +165,74 @@ def getent(refresh=False): return __context__['group.getent'] ret = [] - for grinfo in grp.getgrall(): + if root is not None: + getgrall = functools.partial(_getgrall, root=root) + else: + getgrall = functools.partial(grp.getgrall) + + for grinfo in getgrall(): ret.append(_format_info(grinfo)) __context__['group.getent'] = ret return ret +def _chattrib(name, key, value, param, root=None): + ''' + Change an attribute for a named user + ''' + pre_info = info(name, root=root) + if not pre_info: + return False + + if value == pre_info[key]: + return True + + cmd = ['groupmod'] + + if root is not None: + cmd.extend(('-R', root)) + + cmd.extend((param, value, name)) + + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root).get(key) == value + + def chgid(name, gid, root=None): ''' Change the gid for a named group + name + Name of the group to modify + + gid + Change the group ID to GID + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' group.chgid foo 4376 ''' - pre_gid = __salt__['file.group_to_gid'](name) - if gid == pre_gid: - return True - cmd = ['groupmod', '-g', gid, name] - - if root is not None: - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - post_gid = __salt__['file.group_to_gid'](name) - if post_gid != pre_gid: - return post_gid == gid - return False + return _chattrib(name, 'gid', gid, '-g', root=root) def adduser(name, username, root=None): ''' Add a user in the group. + name + Name of the group to modify + + username + Username to add to the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -178,7 +253,7 @@ def adduser(name, username, root=None): else: cmd = ['gpasswd', '--add', username, name] if root is not None: - cmd.extend(('-Q', root)) + cmd.extend(('--root', root)) else: cmd = ['usermod', '-G', name, username] if root is not None: @@ -193,6 +268,15 @@ def deluser(name, username, root=None): ''' Remove a user from the group. + name + Name of the group to modify + + username + Username to delete from the group + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -216,7 +300,7 @@ def deluser(name, username, root=None): else: cmd = ['gpasswd', '--del', username, name] if root is not None: - cmd.extend(('-R', root)) + cmd.extend(('--root', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) elif __grains__['kernel'] == 'OpenBSD': out = __salt__['cmd.run_stdout']('id -Gn {0}'.format(username), @@ -239,6 +323,15 @@ def members(name, members_list, root=None): ''' Replaces members of the group with a provided list. + name + Name of the group to modify + + members_list + Username list to set into the group + + root + Directory to chroot into + CLI Example: salt '*' group.members foo 'user1,user2,user3,...' @@ -259,7 +352,7 @@ def members(name, members_list, root=None): else: cmd = ['gpasswd', '--members', members_list, name] if root is not None: - cmd.extend(('-R', root)) + cmd.extend(('--root', root)) retcode = __salt__['cmd.retcode'](cmd, python_shell=False) elif __grains__['kernel'] == 'OpenBSD': retcode = 1 @@ -284,3 +377,43 @@ def members(name, members_list, root=None): return False return not retcode + + +def _getgrnam(name, root=None): + ''' + Alternative implementation for getgrnam, that use only /etc/group + ''' + root = root or '/' + passwd = os.path.join(root, 'etc/group') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if len(comps) < 4: + log.debug('Ignoring group line: %s', line) + continue + if comps[0] == name: + # Generate a getpwnam compatible output + comps[2] = int(comps[2]) + comps[3] = comps[3].split(',') if comps[3] else [] + return grp.struct_group(comps) + raise KeyError('getgrnam(): name not found: {}'.format(name)) + + +def _getgrall(root=None): + ''' + Alternative implemetantion for getgrall, that use only /etc/group + ''' + root = root or '/' + passwd = os.path.join(root, 'etc/group') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if len(comps) < 4: + log.debug('Ignoring group line: %s', line) + continue + # Generate a getgrall compatible output + comps[2] = int(comps[2]) + comps[3] = comps[3].split(',') if comps[3] else [] + yield grp.struct_group(comps) diff --git a/salt/modules/shadow.py b/salt/modules/shadow.py index 9659867f050c..98c7369c5ebf 100644 --- a/salt/modules/shadow.py +++ b/salt/modules/shadow.py @@ -13,6 +13,7 @@ # Import python libs import os import datetime +import functools try: import spwd except ImportError: @@ -24,6 +25,7 @@ import salt.utils.stringutils from salt.exceptions import CommandExecutionError from salt.ext import six +from salt.ext.six.moves import range try: import salt.utils.pycrypto HAS_CRYPT = True @@ -48,21 +50,32 @@ def default_hash(): return '!' -def info(name): +def info(name, root=None): ''' Return information for the specified user + name + User to get the information for + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.info root ''' + if root is not None: + getspnam = functools.partial(_getspnam, root=root) + else: + getspnam = functools.partial(spwd.getspnam) + try: - data = spwd.getspnam(name) + data = getspnam(name) ret = { - 'name': data.sp_nam, - 'passwd': data.sp_pwd, + 'name': data.sp_namp if hasattr(data, 'sp_namp') else data.sp_nam, + 'passwd': data.sp_pwdp if hasattr(data, 'sp_pwdp') else data.sp_pwd, 'lstchg': data.sp_lstchg, 'min': data.sp_min, 'max': data.sp_max, @@ -82,69 +95,99 @@ def info(name): return ret -def set_inactdays(name, inactdays): +def _set_attrib(name, key, value, param, root=None, validate=True): + ''' + Set a parameter in /etc/shadow + ''' + pre_info = info(name, root=root) + + # If the user is not present or the attribute is already present, + # we return early + if not pre_info['name']: + return False + + if value == pre_info[key]: + return True + + cmd = ['chage'] + + if root is not None: + cmd.extend(('-R', root)) + + cmd.extend((param, value, name)) + + ret = not __salt__['cmd.run'](cmd, python_shell=False) + if validate: + ret = info(name, root=root).get(key) == value + return ret + + +def set_inactdays(name, inactdays, root=None): ''' Set the number of days of inactivity after a password has expired before the account is locked. See man chage. + name + User to modify + + inactdays + Set password inactive after this number of days + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_inactdays username 7 ''' - pre_info = info(name) - if inactdays == pre_info['inact']: - return True - cmd = 'chage -I {0} {1}'.format(inactdays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['inact'] != pre_info['inact']: - return post_info['inact'] == inactdays - return False + return _set_attrib(name, 'inact', inactdays, '-I', root=root) -def set_maxdays(name, maxdays): +def set_maxdays(name, maxdays, root=None): ''' Set the maximum number of days during which a password is valid. See man chage. + name + User to modify + + maxdays + Maximum number of days during which a password is valid + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_maxdays username 90 ''' - pre_info = info(name) - if maxdays == pre_info['max']: - return True - cmd = 'chage -M {0} {1}'.format(maxdays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['max'] != pre_info['max']: - return post_info['max'] == maxdays - return False + return _set_attrib(name, 'max', maxdays, '-M', root=root) -def set_mindays(name, mindays): +def set_mindays(name, mindays, root=None): ''' Set the minimum number of days between password changes. See man chage. + name + User to modify + + mindays + Minimum number of days between password changes + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_mindays username 7 ''' - pre_info = info(name) - if mindays == pre_info['min']: - return True - cmd = 'chage -m {0} {1}'.format(mindays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['min'] != pre_info['min']: - return post_info['min'] == mindays - return False + return _set_attrib(name, 'min', mindays, '-m', root=root) def gen_password(password, crypt_salt=None, algorithm='sha512'): @@ -189,77 +232,107 @@ def gen_password(password, crypt_salt=None, algorithm='sha512'): return salt.utils.pycrypto.gen_hash(crypt_salt, password, algorithm) -def del_password(name): +def del_password(name, root=None): ''' .. versionadded:: 2014.7.0 Delete the password from name user + name + User to delete + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.del_password username ''' - cmd = 'passwd -d {0}'.format(name) + cmd = ['passwd'] + if root is not None: + cmd.extend(('-R', root)) + cmd.extend(('-d', name)) + __salt__['cmd.run'](cmd, python_shell=False, output_loglevel='quiet') - uinfo = info(name) + uinfo = info(name, root=root) return not uinfo['passwd'] and uinfo['name'] == name -def lock_password(name): +def lock_password(name, root=None): ''' .. versionadded:: 2016.11.0 Lock the password from specified user + name + User to lock + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.lock_password username ''' - pre_info = info(name) - if pre_info['name'] == '': + pre_info = info(name, root=root) + if not pre_info['name']: return False + if pre_info['passwd'].startswith('!'): return True - cmd = 'passwd -l {0}'.format(name) - __salt__['cmd.run'](cmd, python_shell=False) + cmd = ['passwd'] - post_info = info(name) + if root is not None: + cmd.extend(('-R', root)) - return post_info['passwd'].startswith('!') + cmd.extend(('-l', name)) + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root)['passwd'].startswith('!') -def unlock_password(name): + +def unlock_password(name, root=None): ''' .. versionadded:: 2016.11.0 Unlock the password from name user + name + User to unlock + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.unlock_password username ''' - pre_info = info(name) - if pre_info['name'] == '': + pre_info = info(name, root=root) + if not pre_info['name']: return False - if pre_info['passwd'][0] != '!': + + if not pre_info['passwd'].startswith('!'): return True - cmd = 'passwd -u {0}'.format(name) - __salt__['cmd.run'](cmd, python_shell=False) + cmd = ['passwd'] - post_info = info(name) + if root is not None: + cmd.extend(('-R', root)) - return post_info['passwd'][0] != '!' + cmd.extend(('-u', name)) + + __salt__['cmd.run'](cmd, python_shell=False) + return not info(name, root=root)['passwd'].startswith('!') -def set_password(name, password, use_usermod=False): +def set_password(name, password, use_usermod=False, root=None): ''' Set the password for a named user. The password must be a properly defined hash. The password hash can be generated with this command: @@ -273,6 +346,18 @@ def set_password(name, password, use_usermod=False): Keep in mind that the $6 represents a sha512 hash, if your OS is using a different hashing algorithm this needs to be changed accordingly + name + User to set the password + + password + Password already hashed + + use_usermod + Use usermod command to better compatibility + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -287,6 +372,9 @@ def set_password(name, password, use_usermod=False): s_file = '/etc/tcb/{0}/shadow'.format(name) else: s_file = '/etc/shadow' + if root: + s_file = os.path.join(root, os.path.relpath(s_file, os.path.sep)) + ret = {} if not os.path.isfile(s_file): return ret @@ -306,54 +394,67 @@ def set_password(name, password, use_usermod=False): with salt.utils.files.fopen(s_file, 'w+') as fp_: lines = [salt.utils.stringutils.to_str(_l) for _l in lines] fp_.writelines(lines) - uinfo = info(name) + uinfo = info(name, root=root) return uinfo['passwd'] == password else: # Use usermod -p (less secure, but more feature-complete) - cmd = 'usermod -p {0} {1}'.format(password, name) + cmd = ['usermod'] + if root is not None: + cmd.extend(('-R', root)) + cmd.extend(('-p', password, name)) + __salt__['cmd.run'](cmd, python_shell=False, output_loglevel='quiet') - uinfo = info(name) + uinfo = info(name, root=root) return uinfo['passwd'] == password -def set_warndays(name, warndays): +def set_warndays(name, warndays, root=None): ''' Set the number of days of warning before a password change is required. See man chage. + name + User to modify + + warndays + Number of days of warning before a password change is required + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_warndays username 7 ''' - pre_info = info(name) - if warndays == pre_info['warn']: - return True - cmd = 'chage -W {0} {1}'.format(warndays, name) - __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - if post_info['warn'] != pre_info['warn']: - return post_info['warn'] == warndays - return False + return _set_attrib(name, 'warn', warndays, '-W', root=root) -def set_date(name, date): +def set_date(name, date, root=None): ''' Sets the value for the date the password was last changed to days since the epoch (January 1, 1970). See man chage. + name + User to modify + + date + Date the password was last changed + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_date username 0 ''' - cmd = ['chage', '-d', date, name] - return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 + return _set_attrib(name, 'lstchg', date, '-d', root=root, validate=False) -def set_expire(name, expire): +def set_expire(name, expire, root=None): ''' .. versionchanged:: 2014.7.0 @@ -361,26 +462,77 @@ def set_expire(name, expire): (January 1, 1970). Using a value of -1 will clear expiration. See man chage. + name + User to modify + + date + Date the account expires + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.set_expire username -1 ''' - cmd = ['chage', '-E', expire, name] - return __salt__['cmd.retcode'](cmd, python_shell=False) == 0 + return _set_attrib(name, 'expire', expire, '-E', root=root, validate=False) -def list_users(): +def list_users(root=None): ''' .. versionadded:: 2018.3.0 Return a list of all shadow users + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' shadow.list_users ''' - return sorted([user.sp_nam for user in spwd.getspall()]) + if root is not None: + getspall = functools.partial(_getspall, root=root) + else: + getspall = functools.partial(spwd.getspall) + + return sorted([user.sp_namp if hasattr(user, 'sp_namp') else user.sp_nam + for user in getspall()]) + + +def _getspnam(name, root=None): + ''' + Alternative implementation for getspnam, that use only /etc/shadow + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/shadow') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if comps[0] == name: + # Generate a getspnam compatible output + for i in range(2, 9): + comps[i] = int(comps[i]) if comps[i] else -1 + return spwd.struct_spwd(comps) + raise KeyError + + +def _getspall(root=None): + ''' + Alternative implementation for getspnam, that use only /etc/shadow + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/shadow') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + # Generate a getspall compatible output + for i in range(2, 9): + comps[i] = int(comps[i]) if comps[i] else -1 + yield spwd.struct_spwd(comps) diff --git a/salt/modules/useradd.py b/salt/modules/useradd.py index e370dd4bb31a..e38a094ed28c 100644 --- a/salt/modules/useradd.py +++ b/salt/modules/useradd.py @@ -17,6 +17,8 @@ HAS_PWD = False import logging import copy +import functools +import os # Import salt libs import salt.utils.data @@ -55,12 +57,17 @@ def _quote_username(name): return salt.utils.stringutils.to_str(name) -def _get_gecos(name): +def _get_gecos(name, root=None): ''' Retrieve GECOS field info and return it in dictionary form ''' + if root is not None and __grains__['kernel'] != 'AIX': + getpwnam = functools.partial(_getpwnam, root=root) + else: + getpwnam = functools.partial(pwd.getpwnam) gecos_field = salt.utils.stringutils.to_unicode( - pwd.getpwnam(_quote_username(name)).pw_gecos).split(',', 4) + getpwnam(_quote_username(name)).pw_gecos).split(',', 4) + if not gecos_field: return {} else: @@ -96,7 +103,7 @@ def _update_gecos(name, key, value, root=None): value = six.text_type(value) else: value = salt.utils.stringutils.to_unicode(value) - pre_info = _get_gecos(name) + pre_info = _get_gecos(name, root=root) if not pre_info: return False if value == pre_info[key]: @@ -104,14 +111,13 @@ def _update_gecos(name, key, value, root=None): gecos_data = copy.deepcopy(pre_info) gecos_data[key] = value - cmd = ['usermod', '-c', _build_gecos(gecos_data), name] - + cmd = ['usermod'] if root is not None and __grains__['kernel'] != 'AIX': cmd.extend(('-R', root)) + cmd.extend(('-c', _build_gecos(gecos_data), name)) __salt__['cmd.run'](cmd, python_shell=False) - post_info = info(name) - return _get_gecos(name).get(key) == value + return _get_gecos(name, root=root).get(key) == value def add(name, @@ -129,11 +135,62 @@ def add(name, other='', createhome=True, loginclass=None, - root=None, - nologinit=False): + nologinit=False, + root=None): ''' Add a user to the minion + name + Username LOGIN to add + + uid + User ID of the new account + + gid + Name or ID of the primary group of the new accoun + + groups + List of supplementary groups of the new account + + home + Home directory of the new account + + shell + Login shell of the new account + + unique + Allow to create users with duplicate + + system + Create a system account + + fullname + GECOS field for the full name + + roomnumber + GECOS field for the room number + + workphone + GECOS field for the work phone + + homephone + GECOS field for the home phone + + other + GECOS field for other information + + createhome + Create the user's home directory + + loginclass + Login class for the new account (OpenBSD) + + nologinit + Do not add the user to the lastlog and faillog databases + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -231,17 +288,17 @@ def add(name, # user does exist, and B) running useradd again would result in a # nonzero exit status and be interpreted as a False result. if groups: - chgroups(name, groups) + chgroups(name, groups, root=root) if fullname: - chfullname(name, fullname) + chfullname(name, fullname, root=root) if roomnumber: - chroomnumber(name, roomnumber) + chroomnumber(name, roomnumber, root=root) if workphone: - chworkphone(name, workphone) + chworkphone(name, workphone, root=root) if homephone: - chhomephone(name, homephone) + chhomephone(name, homephone, root=root) if other: - chother(name, other) + chother(name, other, root=root) return True @@ -249,6 +306,18 @@ def delete(name, remove=False, force=False, root=None): ''' Remove a user from the minion + name + Username to delete + + remove + Remove home directory and mail spool + + force + Force some actions that would fail otherwise + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -292,10 +361,16 @@ def delete(name, remove=False, force=False, root=None): return False -def getent(refresh=False): +def getent(refresh=False, root=None): ''' Return the list of all info for all users + refresh + Force a refresh of user information + + root + Directory to chroot into + CLI Example: .. code-block:: bash @@ -306,72 +381,106 @@ def getent(refresh=False): return __context__['user.getent'] ret = [] - for data in pwd.getpwall(): + if root is not None and __grains__['kernel'] != 'AIX': + getpwall = functools.partial(_getpwall, root=root) + else: + getpwall = functools.partial(pwd.getpwall) + + for data in getpwall(): ret.append(_format_info(data)) __context__['user.getent'] = ret return ret -def chuid(name, uid): +def _chattrib(name, key, value, param, persist=False, root=None): + ''' + Change an attribute for a named user + ''' + pre_info = info(name, root=root) + if not pre_info: + raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) + + if value == pre_info[key]: + return True + + cmd = ['usermod'] + + if root is not None and __grains__['kernel'] != 'AIX': + cmd.extend(('-R', root)) + + if persist and __grains__['kernel'] != 'OpenBSD': + cmd.append('-m') + + cmd.extend((param, value, name)) + + __salt__['cmd.run'](cmd, python_shell=False) + return info(name, root=root).get(key) == value + + +def chuid(name, uid, root=None): ''' Change the uid for a named user + name + User to modify + + uid + New UID for the user account + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chuid foo 4376 ''' - pre_info = info(name) - if uid == pre_info['uid']: - return True - cmd = ['usermod', '-u', uid, name] - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('uid') == uid + return _chattrib(name, 'uid', uid, '-u', root=root) def chgid(name, gid, root=None): ''' Change the default group of the user + name + User to modify + + gid + Force use GID as new primary group + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chgid foo 4376 ''' - pre_info = info(name) - if gid == pre_info['gid']: - return True - cmd = ['usermod', '-g', gid, name] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('gid') == gid + return _chattrib(name, 'gid', gid, '-g', root=root) def chshell(name, shell, root=None): ''' Change the default shell of the user + name + User to modify + + shell + New login shell for the user account + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chshell foo /bin/zsh ''' - pre_info = info(name) - if shell == pre_info['shell']: - return True - cmd = ['usermod', '-s', shell, name] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('shell') == shell + return _chattrib(name, 'shell', shell, '-s', root=root) def chhome(name, home, persist=False, root=None): @@ -379,25 +488,25 @@ def chhome(name, home, persist=False, root=None): Change the home directory of the user, pass True for persist to move files to the new home directory if the old home directory exist. + name + User to modify + + home + New home directory for the user account + + presist + Move contents of the home directory to the new location + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chhome foo /home/users/foo True ''' - pre_info = info(name) - if home == pre_info['home']: - return True - cmd = ['usermod', '-d', home] - - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) - - if persist and __grains__['kernel'] != 'OpenBSD': - cmd.append('-m') - cmd.append(name) - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('home') == home + return _chattrib(name, 'home', home, '-d', persist=persist, root=root) def chgroups(name, groups, append=False, root=None): @@ -414,6 +523,9 @@ def chgroups(name, groups, append=False, root=None): If ``True``, append the specified group(s). Otherwise, this function will replace the user's groups with the specified group(s). + root + Directory to chroot into + CLI Examples: .. code-block:: bash @@ -460,20 +572,29 @@ def chgroups(name, groups, append=False, root=None): return result['retcode'] == 0 -def chfullname(name, fullname): +def chfullname(name, fullname, root=None): ''' Change the user's Full Name + name + User to modify + + fullname + GECOS field for the full name + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chfullname foo "Foo Bar" ''' - return _update_gecos(name, 'fullname', fullname) + return _update_gecos(name, 'fullname', fullname, root=root) -def chroomnumber(name, roomnumber): +def chroomnumber(name, roomnumber, root=None): ''' Change the user's Room Number @@ -483,52 +604,88 @@ def chroomnumber(name, roomnumber): salt '*' user.chroomnumber foo 123 ''' - return _update_gecos(name, 'roomnumber', roomnumber) + return _update_gecos(name, 'roomnumber', roomnumber, root=root) -def chworkphone(name, workphone): +def chworkphone(name, workphone, root=None): ''' Change the user's Work Phone + name + User to modify + + workphone + GECOS field for the work phone + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chworkphone foo 7735550123 ''' - return _update_gecos(name, 'workphone', workphone) + return _update_gecos(name, 'workphone', workphone, root=root) -def chhomephone(name, homephone): +def chhomephone(name, homephone, root=None): ''' Change the user's Home Phone + name + User to modify + + homephone + GECOS field for the home phone + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chhomephone foo 7735551234 ''' - return _update_gecos(name, 'homephone', homephone) + return _update_gecos(name, 'homephone', homephone, root=root) -def chother(name, other): +def chother(name, other, root=None): ''' Change the user's other GECOS attribute + name + User to modify + + other + GECOS field for other information + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.chother foobar ''' - return _update_gecos(name, 'other', other) + return _update_gecos(name, 'other', other, root=root) def chloginclass(name, loginclass, root=None): ''' Change the default login class of the user + name + User to modify + + loginclass + Login class for the new account + + root + Directory to chroot into + .. note:: This function only applies to OpenBSD systems. @@ -546,25 +703,43 @@ def chloginclass(name, loginclass, root=None): cmd = ['usermod', '-L', loginclass, name] - if root is not None: + if root is not None and __grains__['kernel'] != 'AIX': cmd.extend(('-R', root)) __salt__['cmd.run'](cmd, python_shell=False) return get_loginclass(name) == loginclass -def info(name): +def info(name, root=None): ''' Return user information + name + User to get the information + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.info root ''' + # If root is provided, we use a less portable solution that + # depends on analyzing /etc/passwd manually. Of course we cannot + # find users from NIS nor LDAP, but in those cases do not makes + # sense to provide a root parameter. + # + # Please, note that if the non-root /etc/passwd file is long the + # iteration can be slow. + if root is not None and __grains__['kernel'] != 'AIX': + getpwnam = functools.partial(_getpwnam, root=root) + else: + getpwnam = functools.partial(pwd.getpwnam) + try: - data = pwd.getpwnam(_quote_username(name)) + data = getpwnam(_quote_username(name)) except KeyError: return {} else: @@ -575,6 +750,9 @@ def get_loginclass(name): ''' Get the login class of the user + name + User to get the information + .. note:: This function only applies to OpenBSD systems. @@ -632,6 +810,9 @@ def primary_group(name): .. versionadded:: 2016.3.0 + name + User to get the information + CLI Example: .. code-block:: bash @@ -645,6 +826,9 @@ def list_groups(name): ''' Return a list of groups the named user belongs to + name + User to get the information + CLI Example: .. code-block:: bash @@ -654,43 +838,79 @@ def list_groups(name): return salt.utils.user.get_group_list(name) -def list_users(): +def list_users(root=None): ''' Return a list of all users + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.list_users ''' - return sorted([user.pw_name for user in pwd.getpwall()]) + if root is not None and __grains__['kernel'] != 'AIX': + getpwall = functools.partial(_getpwall, root=root) + else: + getpwall = functools.partial(pwd.getpwall) + + return sorted([user.pw_name for user in getpwall()]) def rename(name, new_name, root=None): ''' Change the username for a named user + name + User to modify + + new_name + New value of the login name + + root + Directory to chroot into + CLI Example: .. code-block:: bash salt '*' user.rename name new_name ''' - current_info = info(name) - if not current_info: - raise CommandExecutionError('User \'{0}\' does not exist'.format(name)) + if info(new_name, root=root): + raise CommandExecutionError('User \'{0}\' already exists'.format(new_name)) - new_info = info(new_name) - if new_info: - raise CommandExecutionError( - 'User \'{0}\' already exists'.format(new_name) - ) + return _chattrib(name, 'name', new_name, '-l', root=root) - cmd = ['usermod', '-l', new_name, name] - if root is not None and __grains__['kernel'] != 'AIX': - cmd.extend(('-R', root)) +def _getpwnam(name, root=None): + ''' + Alternative implementation for getpwnam, that use only /etc/passwd + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/passwd') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + if comps[0] == name: + # Generate a getpwnam compatible output + comps[2], comps[3] = int(comps[2]), int(comps[3]) + return pwd.struct_passwd(comps) + raise KeyError - __salt__['cmd.run'](cmd, python_shell=False) - return info(name).get('name') == new_name + +def _getpwall(root=None): + ''' + Alternative implemetantion for getpwall, that use only /etc/passwd + ''' + root = '/' if not root else root + passwd = os.path.join(root, 'etc/passwd') + with salt.utils.files.fopen(passwd) as fp_: + for line in fp_: + line = salt.utils.stringutils.to_unicode(line) + comps = line.strip().split(':') + # Generate a getpwall compatible output + comps[2], comps[3] = int(comps[2]), int(comps[3]) + yield pwd.struct_passwd(comps) diff --git a/tests/unit/modules/test_groupadd.py b/tests/unit/modules/test_groupadd.py index 8e0e64749ad4..2ce7897a065a 100644 --- a/tests/unit/modules/test_groupadd.py +++ b/tests/unit/modules/test_groupadd.py @@ -84,21 +84,19 @@ def test_chgid_gid_same(self): ''' Tests if the group id is the same as argument ''' - mock_pre_gid = MagicMock(return_value=10) - with patch.dict(groupadd.__salt__, - {'file.group_to_gid': mock_pre_gid}): + mock = MagicMock(return_value={'gid': 10}) + with patch.object(groupadd, 'info', mock): self.assertTrue(groupadd.chgid('test', 10)) def test_chgid(self): ''' Tests the gid for a named group was changed ''' - mock_pre_gid = MagicMock(return_value=0) - mock_cmdrun = MagicMock(return_value=0) - with patch.dict(groupadd.__salt__, - {'file.group_to_gid': mock_pre_gid}): - with patch.dict(groupadd.__salt__, {'cmd.run': mock_cmdrun}): - self.assertFalse(groupadd.chgid('test', 500)) + mock = MagicMock(return_value=None) + with patch.dict(groupadd.__salt__, {'cmd.run': mock}): + mock = MagicMock(side_effect=[{'gid': 10}, {'gid': 500}]) + with patch.object(groupadd, 'info', mock): + self.assertTrue(groupadd.chgid('test', 500)) # 'delete' function tests: 1 diff --git a/tests/unit/modules/test_useradd.py b/tests/unit/modules/test_useradd.py index 18da8d8ce863..74cafc64409c 100644 --- a/tests/unit/modules/test_useradd.py +++ b/tests/unit/modules/test_useradd.py @@ -415,14 +415,15 @@ def test_rename(self): mock = MagicMock(return_value=None) with patch.dict(useradd.__salt__, {'cmd.run': mock}): - mock = MagicMock(side_effect=[{'name': ''}, False, + mock = MagicMock(side_effect=[False, {'name': ''}, {'name': 'salt'}]) with patch.object(useradd, 'info', mock): self.assertTrue(useradd.rename('name', 'salt')) mock = MagicMock(return_value=None) with patch.dict(useradd.__salt__, {'cmd.run': mock}): - mock = MagicMock(side_effect=[{'name': ''}, False, {'name': ''}]) + mock = MagicMock(side_effect=[False, {'name': ''}, + {'name': ''}]) with patch.object(useradd, 'info', mock): self.assertFalse(useradd.rename('salt', 'salt'))