49f3ab6757
This adds user_password() to abstract how the user's password is looked up. If spwd is not available, this will read the shadow file for the user's shadow entry. This will then facilitate idempotent password changes on hosts without spwd.
332 lines
11 KiB
Python
Executable file
332 lines
11 KiB
Python
Executable file
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible 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 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import os
|
|
import pwd
|
|
import grp
|
|
try:
|
|
import spwd
|
|
HAVE_SPWD=True
|
|
except:
|
|
HAVE_SPWD=False
|
|
|
|
SHADOWFILE = '/etc/shadow'
|
|
if os.path.exists('/etc/master.passwd'):
|
|
SHADOWFILE = '/etc/master.passwd' # FreeBSD passwd
|
|
# Note: while the above has the correct location for where
|
|
# encrypted passwords are stored on FreeBSD, the code below doesn't
|
|
# invoke adduser in lieu of useradd, nor pw in lieu of usermod.
|
|
# That is, this won't work on FreeBSD.
|
|
|
|
def get_bin_path(module, arg):
|
|
if os.path.exists('/usr/sbin/%s' % arg):
|
|
return '/usr/sbin/%s' % arg
|
|
elif os.path.exists('/sbin/%s' % arg):
|
|
return '/sbin/%s' % arg
|
|
else:
|
|
module.fail_json(msg="Cannot find %s" % arg)
|
|
|
|
def user_del(module, user, **kwargs):
|
|
cmd = [get_bin_path(module, 'userdel')]
|
|
for key in kwargs:
|
|
if key == 'force' and kwargs[key] == 'yes':
|
|
cmd.append('-f')
|
|
elif key == 'remove' and kwargs[key] == 'yes':
|
|
cmd.append('-r')
|
|
cmd.append(user)
|
|
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate()
|
|
rc = p.returncode
|
|
return (rc, out, err)
|
|
|
|
def user_add(module, user, **kwargs):
|
|
cmd = [get_bin_path(module, 'useradd')]
|
|
for key in kwargs:
|
|
if key == 'uid' and kwargs[key] is not None:
|
|
cmd.append('-u')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'group' and kwargs[key] is not None:
|
|
if not group_exists(kwargs[key]):
|
|
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
|
cmd.append('-g')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'groups' and kwargs[key] is not None:
|
|
for g in kwargs[key].split(','):
|
|
if not group_exists(g):
|
|
module.fail_json(msg="Group %s does not exist" % (g))
|
|
cmd.append('-G')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'comment' and kwargs[key] is not None:
|
|
cmd.append('-c')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'home' and kwargs[key] is not None:
|
|
cmd.append('-d')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'shell' and kwargs[key] is not None:
|
|
cmd.append('-s')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'password' and kwargs[key] is not None:
|
|
cmd.append('-p')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'createhome':
|
|
if kwargs[key] is not None:
|
|
if kwargs[key] == 'yes':
|
|
cmd.append('-m')
|
|
else:
|
|
cmd.append('-M')
|
|
elif key == 'system' and kwargs[key] == 'yes':
|
|
cmd.append('-r')
|
|
cmd.append(user)
|
|
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate()
|
|
rc = p.returncode
|
|
return (rc, out, err)
|
|
|
|
"""
|
|
Without spwd, we would have to resort to reading /etc/shadow
|
|
to get the encrypted string. For now, punt on idempotent password changes.
|
|
"""
|
|
def user_mod(module, user, **kwargs):
|
|
cmd = [get_bin_path(module, 'usermod')]
|
|
info = user_info(user)
|
|
for key in kwargs:
|
|
if key == 'uid':
|
|
if kwargs[key] is not None and info[2] != int(kwargs[key]):
|
|
cmd.append('-u')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'group' and kwargs[key] is not None:
|
|
if not group_exists(kwargs[key]):
|
|
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
|
ginfo = group_info(kwargs[key])
|
|
if info[3] != ginfo[2]:
|
|
cmd.append('-g')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'groups' and kwargs[key] is not None:
|
|
current_groups = user_group_membership(user)
|
|
groups = kwargs[key].split(',')
|
|
for g in groups:
|
|
if not group_exists(g):
|
|
module.fail_json(msg="Group %s does not exist" % (g))
|
|
group_diff = set(sorted(current_groups)).symmetric_difference(set(sorted(groups)))
|
|
groups_need_mod = False
|
|
|
|
if group_diff:
|
|
if kwargs['append'] is not None and kwargs['append'] == 'yes':
|
|
for g in groups:
|
|
if g in group_diff:
|
|
cmd.append('-a')
|
|
groups_need_mod = True
|
|
else:
|
|
groups_need_mod = True
|
|
|
|
if groups_need_mod:
|
|
cmd.append('-G')
|
|
cmd.append(','.join(groups))
|
|
|
|
elif key == 'comment':
|
|
if kwargs[key] is not None and info[4] != kwargs[key]:
|
|
cmd.append('-c')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'home':
|
|
if kwargs[key] is not None and info[5] != kwargs[key]:
|
|
cmd.append('-d')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'shell':
|
|
if kwargs[key] is not None and info[6] != kwargs[key]:
|
|
cmd.append('-s')
|
|
cmd.append(kwargs[key])
|
|
elif key == 'password':
|
|
if kwargs[key] is not None and info[1] != kwargs[key]:
|
|
cmd.append('-p')
|
|
cmd.append(kwargs[key])
|
|
# skip if no changes to be made
|
|
if len(cmd) == 1:
|
|
return (None, '', '')
|
|
cmd.append(user)
|
|
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate()
|
|
rc = p.returncode
|
|
return (rc, out, err)
|
|
|
|
def group_exists(group):
|
|
try:
|
|
if group.isdigit():
|
|
if grp.getgrgid(group):
|
|
return True
|
|
else:
|
|
if grp.getgrnam(group):
|
|
return True
|
|
except KeyError:
|
|
return False
|
|
|
|
def group_info(group):
|
|
if not group_exists(group):
|
|
return False
|
|
if group.isdigit():
|
|
return list(grp.getgrgid(group))
|
|
else:
|
|
return list(grp.getgrnam(group))
|
|
|
|
def user_group_membership(user):
|
|
groups = []
|
|
info = get_pwd_info(user)
|
|
for group in grp.getgrall():
|
|
if user in group[3] and info[3] != group[2]:
|
|
groups.append(group[0])
|
|
return groups
|
|
|
|
def user_exists(user):
|
|
try:
|
|
if pwd.getpwnam(user):
|
|
return True
|
|
except KeyError:
|
|
return False
|
|
|
|
def get_pwd_info(user):
|
|
if not user_exists(user):
|
|
return False
|
|
return list(pwd.getpwnam(user))
|
|
|
|
def user_info(user):
|
|
if not user_exists(user):
|
|
return False
|
|
info = get_pwd_info(user)
|
|
if len(info[1]) == 1 or len(info[1]) == 0:
|
|
info[1] = user_password(user)
|
|
return info
|
|
|
|
def user_password(user):
|
|
passwd = ''
|
|
if not user_exists(user):
|
|
return passwd
|
|
if HAVE_SPWD:
|
|
try:
|
|
passwd = spwd.getspnam(user)[1]
|
|
except KeyError:
|
|
return passwd
|
|
else:
|
|
# Read shadow file for user's encrypted password string
|
|
if os.path.exists(SHADOWFILE) and os.access(SHADOWFILE, os.R_OK):
|
|
for line in open(SHADOWFILE).readlines():
|
|
if line.startswith('%s:' % user):
|
|
passwd = line.split(':')[1]
|
|
return passwd
|
|
|
|
# ===========================================
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec = dict(
|
|
state=dict(default='present', choices=['present', 'absent']),
|
|
name=dict(required=True),
|
|
uid=dict(default=None),
|
|
group=dict(default=None),
|
|
groups=dict(default=None),
|
|
comment=dict(default=None),
|
|
home=dict(default=None),
|
|
shell=dict(default=None),
|
|
password=dict(default=None),
|
|
# following options are specific to userdel
|
|
force=dict(default='no', choices=['yes', 'no']),
|
|
remove=dict(default='no', choices=['yes', 'no']),
|
|
# following options are specific to useradd
|
|
createhome=dict(default='yes', choices=['yes', 'no']),
|
|
system=dict(default='no', choices=['yes', 'no']),
|
|
# following options are specific to usermod
|
|
append=dict(default='no', choices=['yes', 'no']),
|
|
)
|
|
)
|
|
|
|
state = module.params['state']
|
|
name = module.params['name']
|
|
uid = module.params['uid']
|
|
group = module.params['group']
|
|
groups = module.params['groups']
|
|
comment = module.params['comment']
|
|
home = module.params['home']
|
|
shell = module.params['shell']
|
|
password = module.params['password']
|
|
force = module.params['force']
|
|
remove = module.params['remove']
|
|
createhome = module.params['createhome']
|
|
system = module.params['system']
|
|
append = module.params['append']
|
|
|
|
rc = None
|
|
out = ''
|
|
err = ''
|
|
result = {}
|
|
result['name'] = name
|
|
result['state'] = state
|
|
if state == 'absent':
|
|
if user_exists(name):
|
|
(rc, out, err) = user_del(module, name, force=force, remove=remove)
|
|
if rc != 0:
|
|
module.fail_json(name=name, msg=err, rc=rc)
|
|
result['force'] = force
|
|
result['remove'] = remove
|
|
elif state == 'present':
|
|
if not user_exists(name):
|
|
(rc, out, err) = user_add(module,
|
|
name, uid=uid, group=group, groups=groups,
|
|
comment=comment, home=home, shell=shell,
|
|
password=password, createhome=createhome,
|
|
system=system)
|
|
result['system'] = system
|
|
result['createhome'] = createhome
|
|
else:
|
|
(rc, out, err) = user_mod(module,
|
|
name, uid=uid, group=group, groups=groups,
|
|
comment=comment, home=home, shell=shell,
|
|
password=password, append=append)
|
|
result['append'] = append
|
|
if rc is not None and rc != 0:
|
|
module.fail_json(name=name, msg=err, rc=rc)
|
|
if password is not None:
|
|
result['password'] = 'NOT_LOGGING_PASSWORD'
|
|
|
|
if rc is None:
|
|
result['changed'] = False
|
|
else:
|
|
result['changed'] = True
|
|
if out:
|
|
result['stdout'] = out
|
|
if err:
|
|
result['stderr'] = err
|
|
if user_exists(name):
|
|
info = user_info(name)
|
|
if info == False:
|
|
result['msg'] = "failed to look up user name: %s" % name
|
|
result['failed'] = True
|
|
result['uid'] = info[2]
|
|
result['group'] = info[3]
|
|
result['comment'] = info[4]
|
|
result['home'] = info[5]
|
|
result['shell'] = info[6]
|
|
groups = user_group_membership(name)
|
|
result['uid'] = info[2]
|
|
if len(groups) > 0:
|
|
result['groups'] = groups
|
|
|
|
module.exit_json(**result)
|
|
|
|
# include magic from lib/ansible/module_common.py
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
main()
|