Rework of user module to support portability

Split module into a main calling function, and a generic
(Linux useradd/usermod/userdel) User class.
Added a __new__ function that selects most appropriate superclass
Added a FreeBSD User class
Tested against FreeBSD 9.0
reviewable/pr18780/r1
Nigel Metheringham 12 years ago
parent 2c4d559523
commit 346f8ffe0e

739
user

@ -155,111 +155,174 @@ examples:
import os
import pwd
import grp
import syslog
import platform
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 user_del(module, user, **kwargs):
cmd = [module.get_bin_path('userdel', True)]
for key in kwargs:
if key == 'force' and module.boolean(kwargs[key]):
class User(object):
platform = 'Generic'
distribution = None
SHADOWFILE = '/etc/shadow'
def __new__(cls, *arguments, **keyword):
subclass = None
this_platform = platform.system()
if this_platform == 'Linux':
try:
distribution = platform.linux_distribution()[0].capitalize
except:
distribution = platform.dist()[0].capitalize
else:
distribution = None
# get the most specific superclass for this platform
if distribution is not None:
for sc in User.__subclasses__():
if sc.distribution is not None and sc.distribution == distribution and sc.platform == this_platform:
subclass = sc
if subclass is None:
for sc in User.__subclasses__():
if sc.platform == this_platform and sc.distribution is None:
subclass = sc
if subclass is None:
subclass = cls
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def __init__(self, module):
self.module = module
self.state = module.params['state']
self.name = module.params['name']
self.uid = module.params['uid']
self.group = module.params['group']
self.groups = module.params['groups']
self.comment = module.params['comment']
self.home = module.params['home']
self.shell = module.params['shell']
self.password = module.params['password']
self.force = module.boolean(module.params['force'])
self.remove = module.boolean(module.params['remove'])
self.createhome = module.boolean(module.params['createhome'])
self.system = module.boolean(module.params['system'])
self.append = module.boolean(module.params['append'])
self.sshkeygen = module.boolean(module.params['ssh_key'])
self.ssh_bits = module.params['ssh_key_bits']
self.ssh_type = module.params['ssh_key_type']
self.ssh_comment = module.params['ssh_key_comment']
self.ssh_passphrase = module.params['ssh_key_passphrase']
if module.params['ssh_key_file'] is not None:
self.ssh_file = module.params['ssh_key_file']
else:
self.ssh_file = os.path.join('.ssh', 'id_%s' % self.ssh_type)
self.syslogging = True
def execute_command(self,cmd):
if self.syslogging:
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Command %s' % '|'.join(cmd))
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
def remove_user_userdel(self):
cmd = [self.module.get_bin_path('userdel', True)]
if self.force:
cmd.append('-f')
elif key == 'remove' and module.boolean(kwargs[key]):
elif self.remove:
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 = [module.get_bin_path('useradd', True)]
for key in kwargs:
if key == 'uid' and kwargs[key] is not None:
cmd.append(self.name)
return self.execute_command(cmd)
def create_user_useradd(self, command_name='useradd'):
cmd = [self.module.get_bin_path(command_name, True)]
if self.uid 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(self.uid)
if self.group is not None:
if not user.group_exists(self.group):
self.module.fail_json(msg="Group %s does not exist" % self.group)
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(self.group)
if self.groups is not None:
for g in self.groups.split(','):
if not self.group_exists(g):
self.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(self.groups)
if self.comment is not None:
cmd.append('-c')
cmd.append(kwargs[key])
elif key == 'home' and kwargs[key] is not None:
cmd.append(self.comment)
if self.home is not None:
cmd.append('-d')
cmd.append(kwargs[key])
elif key == 'shell' and kwargs[key] is not None:
cmd.append(self.home)
if self.shell is not None:
cmd.append('-s')
cmd.append(kwargs[key])
elif key == 'password' and kwargs[key] is not None:
cmd.append(self.shell)
if self.password is not None:
cmd.append('-p')
cmd.append(kwargs[key])
elif key == 'createhome':
if kwargs[key] is not None:
value = module.boolean(kwargs[key])
if value:
cmd.append('-m')
else:
cmd.append('-M')
elif key == 'system' and module.boolean(kwargs[key]):
cmd.append(self.password)
if self.createhome:
cmd.append('-m')
else:
cmd.append('-M')
if self.system:
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 = [module.get_bin_path('usermod', True)]
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])
cmd.append(self.name)
return self.execute_command(cmd)
def modify_user_usermod(self):
cmd = [self.module.get_bin_path('usermod', True)]
info = self.user_info()
if self.uid is not None and info[2] != int(self.uid):
cmd.append('-u')
cmd.append(self.uid)
if self.group is not None:
if not self.group_exists(self.group):
module.fail_json(msg="Group %s does not exist" % self.group)
ginfo = self.group_info(self.group)
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(',')
cmd.append(self.group)
if self.groups is not None:
current_groups = self.user_group_membership()
groups = self.groups.split(',')
for g in groups:
if not group_exists(g):
if not self.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 module.boolean(kwargs['append']):
if self.append:
for g in groups:
if g in group_diff:
cmd.append('-a')
groups_need_mod = True
break
else:
groups_need_mod = True
@ -267,156 +330,298 @@ def user_mod(module, user, **kwargs):
cmd.append('-G')
cmd.append(','.join(groups))
elif key == 'comment':
if kwargs[key] is not None and info[4] != kwargs[key]:
if self.comment is not None and info[4] != self.comment:
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:
cmd.append(self.comment)
if self.home is not None and info[5] != self.home:
cmd.append('-d')
cmd.append(self.home)
if self.shell is not None and info[6] != self.shell:
cmd.append('-s')
cmd.append(self.shell)
if self.password is not None and info[1] != self.password:
cmd.append('-p')
cmd.append(self.password)
# skip if no changes to be made
if len(cmd) == 1:
return (None, '', '')
cmd.append(self.name)
return self.execute_command(cmd)
def group_exists(self,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(self,group):
if not self.group_exists(group):
return False
if group.isdigit():
if grp.getgrgid(group):
return True
return list(grp.getgrgid(group))
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]:
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:
return list(grp.getgrnam(group))
def user_group_membership(self):
groups = []
info = self.get_pwd_info()
for group in grp.getgrall():
if self.name in group[3] and info[3] != group[2]:
groups.append(group[0])
return groups
def user_exists(self):
try:
passwd = spwd.getspnam(user)[1]
if pwd.getpwnam(self.name):
return True
except KeyError:
return False
def get_pwd_info(self):
if not self.user_exists():
return False
return list(pwd.getpwnam(self.name))
def user_info(self):
if not self.user_exists():
return False
info = self.get_pwd_info()
if len(info[1]) == 1 or len(info[1]) == 0:
info[1] = self.user_password()
return info
def user_password(self):
passwd = ''
if not self.user_exists():
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 get_ssh_key_path(user, ssh_file):
info = user_info(user)
if os.path.isabs(ssh_file):
ssh_key_file = ssh_file
else:
ssh_key_file = os.path.join(info[5], ssh_file)
return ssh_key_file
def ssh_key_gen(module, user, ssh):
info = user_info(user)
if not os.path.exists(info[5]):
return (1, '', 'User %s home directory does not exist' % user)
ssh_key_file = get_ssh_key_path(user, ssh['file'])
ssh_dir = os.path.dirname(ssh_key_file)
if not os.path.exists(ssh_dir):
try:
os.mkdir(ssh_dir, 0700)
except OSError, e:
return (1, '', 'Failed to create %s: %s' % (ssh_dir, str(e)))
if os.path.exists(ssh_key_file):
return (None, 'Key already exists', '')
cmd = [module.get_bin_path('ssh-keygen', True)]
for key in ssh:
if key == 'type' and ssh[key] is not None:
cmd.append('-t')
cmd.append(ssh[key])
elif key == 'bits' and ssh[key] is not None:
cmd.append('-b')
cmd.append(ssh[key])
elif key == 'comment' and ssh[key] is not None:
cmd.append('-C')
cmd.append(ssh[key])
elif key == 'file' and ssh[key] is not None:
cmd.append('-f')
cmd.append(ssh_key_file)
elif key == 'passphrase':
cmd.append('-N')
if ssh[key] is not None:
cmd.append(ssh['passphrase'])
else:
cmd.append('')
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
def ssh_key_fingerprint(module, user, ssh):
ssh_key_file = get_ssh_key_path(user, ssh['file'])
if not os.path.exists(ssh_key_file):
return (1, 'SSH Key file %s does not exist' % ssh_key_file, '')
cmd = [module.get_bin_path('ssh-keygen', True)]
cmd.append('-l')
cmd.append('-f')
cmd.append(ssh_key_file)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
if HAVE_SPWD:
try:
passwd = spwd.getspnam(self.name)[1]
except KeyError:
return passwd
else:
# Read shadow file for user's encrypted password string
if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK):
for line in open(self.SHADOWFILE).readlines():
if line.startswith('%s:' % self.name):
passwd = line.split(':')[1]
return passwd
def get_ssh_key_path(self):
info = self.user_info
if os.path.isabs(self.ssh_file):
ssh_key_file = self.ssh_file
else:
ssh_key_file = os.path.join(info[5], self.ssh_file)
return ssh_key_file
def ssh_key_gen(self):
info = self.user_info
if not os.path.exists(info[5]):
return (1, '', 'User %s home directory does not exist' % self.name)
ssh_key_file = self.get_ssh_key_path
ssh_dir = os.path.dirname(ssh_key_file)
if not os.path.exists(ssh_dir):
try:
os.mkdir(ssh_dir, 0700)
except OSError, e:
return (1, '', 'Failed to create %s: %s' % (ssh_dir, str(e)))
if os.path.exists(ssh_key_file):
return (None, 'Key already exists', '')
cmd = [self.module.get_bin_path('ssh-keygen', True)]
cmd.append('-t')
cmd.append(self.ssh_type)
cmd.append('-b')
cmd.append(self.ssh_bits)
cmd.append('-C')
cmd.append(self.ssh_comment)
cmd.append('-f')
cmd.append(ssh_key_file)
cmd.append('-N')
if self.ssh_passphrase is not None:
cmd.append(self.ssh_passphrase)
else:
cmd.append('')
return self.execute_command(cmd)
def ssh_key_fingerprint(self):
ssh_key_file = self.get_ssh_key_path
if not os.path.exists(ssh_key_file):
return (1, 'SSH Key file %s does not exist' % ssh_key_file, '')
cmd = [module.get_bin_path('ssh-keygen', True)]
cmd.append('-l')
cmd.append('-f')
cmd.append(ssh_key_file)
return self.execute_command(cmd)
def create_user(self):
# by default we use the create_user_useradd method
return self.create_user_useradd()
def remove_user(self):
# by default we use the remove_user_userdel method
return self.remove_user_userdel()
def modify_user(self):
# by default we use the modify_user_usermod method
return self.modify_user_usermod()
# ===========================================
class FreeBSD_User(User):
platform = 'FreeBSD'
distribution = None
SHADOWFILE = '/etc/master.passwd'
def remove_user(self):
cmd = [self.module.get_bin_path('pw', True),
'userdel',
'-n',
self.name ]
if self.remove:
cmd.append('-r')
return self.execute_command(cmd)
def create_user(self):
cmd = [self.module.get_bin_path('pw', True),
'useradd',
'-n',
self.name ]
if self.uid is not None:
cmd.append('-u')
cmd.append(self.uid)
if self.comment is not None:
cmd.append('-c')
cmd.append(self.comment)
if self.home is not None:
cmd.append('-d')
cmd.append(self.home)
if self.group is not None:
if not user.group_exists(self.group):
self.module.fail_json(msg="Group %s does not exist" % self.group)
cmd.append('-g')
cmd.append(self.group)
if self.groups is not None:
for g in self.groups.split(','):
if not self.group_exists(g):
self.module.fail_json(msg="Group %s does not exist" % (g))
cmd.append('-G')
cmd.append(self.groups)
if self.createhome:
cmd.append('-m')
if self.shell is not None:
cmd.append('-s')
cmd.append(self.shell)
# system cannot be handled currently - should we error if its requested?
# create the user
(rc, out, err) = self.execute_command(cmd)
if rc is not None and rc != 0:
module.fail_json(name=self.name, msg=err, rc=rc)
# we have to set the password in a second command
if self.password is not None:
cmd = [self.module.get_bin_path('chpass', True),
'-p',
self.password,
self.name ]
return self.execute_command(cmd)
return (rc, out, err)
def modify_user(self):
cmd = [self.module.get_bin_path('pw', True),
'usermod',
'-n',
self.name ]
info = self.user_info()
if self.uid is not None and info[2] != int(self.uid):
cmd.append('-u')
cmd.append(self.uid)
if self.comment is not None and info[4] != self.comment:
cmd.append('-c')
cmd.append(self.comment)
if self.home is not None and info[5] != self.home:
cmd.append('-d')
cmd.append(self.home)
if self.group is not None:
if not user.group_exists(self.group):
self.module.fail_json(msg="Group %s does not exist" % self.group)
ginfo = self.group_info(self.group)
if info[3] != ginfo[2]:
cmd.append('-g')
cmd.append(self.group)
if self.shell is not None and info[6] != self.shell:
cmd.append('-s')
cmd.append(self.shell)
if self.groups is not None:
current_groups = self.user_group_membership()
groups = self.groups.split(',')
for g in groups:
if not self.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 self.append:
for g in groups:
if g in group_diff:
groups_need_mod = True
break
else:
groups_need_mod = True
if groups_need_mod:
new_groups = groups
if self.append:
new_groups.append(current_groups)
cmd.append(','.join(new_groups))
# modify the user
(rc, out, err) = self.execute_command(cmd)
if rc is not None and rc != 0:
module.fail_json(name=self.name, msg=err, rc=rc)
# we have to set the password in a second command
if self.password is not None and info[1] != self.password:
cmd = [self.module.get_bin_path('chpass', True),
'-p',
self.password,
self.name ]
return self.execute_command(cmd)
return (rc, out, err)
# ===========================================
def main():
@ -426,8 +631,6 @@ def main():
'passphrase': None,
'comment': 'ansible-generated'
}
ssh_defaults['file'] = os.path.join('.ssh', 'id_%s' % ssh_defaults['type'])
ssh = dict(ssh_defaults)
module = AnsibleModule(
argument_spec = dict(
state=dict(default='present', choices=['present', 'absent']),
@ -451,69 +654,44 @@ def main():
ssh_key=dict(choices=['generate']),
ssh_key_bits=dict(default=ssh_defaults['bits']),
ssh_key_type=dict(default=ssh_defaults['type']),
ssh_key_file=dict(default=ssh_defaults['file']),
ssh_key_file=dict(default=None),
ssh_key_comment=dict(default=ssh_defaults['comment']),
ssh_key_passphrase=dict(default=None)
)
)
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']
sshkeygen = module.params['ssh_key']
ssh['bits'] = module.params['ssh_key_bits']
ssh['type'] = module.params['ssh_key_type']
ssh['file'] = module.params['ssh_key_file']
ssh['comment'] = module.params['ssh_key_comment']
ssh['passphrase'] = module.params['ssh_key_passphrase']
# If using default filename, make sure it is named appropriately
if ssh['file'] == ssh_defaults['file']:
ssh['file'] = os.path.join('.ssh', 'id_%s' % ssh_defaults['type'])
user = User(module)
if user.syslogging:
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'User instantiated - platform %s' % user.platform)
if user.distribution:
syslog.syslog(syslog.LOG_NOTICE, 'User instantiated - distribution %s' % user.distribution)
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)
result['name'] = user.name
result['state'] = user.state
if user.state == 'absent':
if user.user_exists():
(rc, out, err) = user.remove_user()
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
result['force'] = user.force
result['remove'] = user.remove
elif user.state == 'present':
if not user.user_exists():
(rc, out, err) = user.create_user()
result['system'] = user.system
result['createhome'] = user.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
(rc, out, err) = user.modify_user()
result['append'] = user.append
if rc is not None and rc != 0:
module.fail_json(name=name, msg=err, rc=rc)
if password is not None:
module.fail_json(name=user.name, msg=err, rc=rc)
if user.password is not None:
result['password'] = 'NOT_LOGGING_PASSWORD'
if rc is None:
@ -524,32 +702,35 @@ def main():
result['stdout'] = out
if err:
result['stderr'] = err
if user_exists(name):
info = user_info(name)
if user.user_exists():
info = user.user_info()
if info == False:
result['msg'] = "failed to look up user name: %s" % name
result['msg'] = "failed to look up user name: %s" % user.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)
groups = user.user_group_membership()
result['uid'] = info[2]
if len(groups) > 0:
result['groups'] = groups
if sshkeygen:
(rc, out, err) = ssh_key_gen(module, name, ssh)
if user.groups is not None:
result['groups'] = user.groups
# deal with ssh key
if user.sshkeygen:
(rc, out, err) = user.ssh_key_gen()
if rc is not None and rc != 0:
module.fail_json(name=name, msg=err, rc=rc)
module.fail_json(name=user.name, msg=err, rc=rc)
if rc == 0:
result['changed'] = True
(rc, out, err) = ssh_key_fingerprint(module, name, ssh)
(rc, out, err) = user.ssh_key_fingerprint()
if rc == 0:
result['ssh_fingerprint'] = out.strip()
else:
result['ssh_fingerprint'] = err.strip()
result['ssh_key_file'] = get_ssh_key_path(name, ssh['file'])
result['ssh_key_file'] = user.get_ssh_key_path()
module.exit_json(**result)

Loading…
Cancel
Save