#!/usr/bin/python # (c) 2012, Stephen Fromm # # 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 . try: import json except ImportError: import simplejson as json import os import re import pwd import grp import shlex import subprocess import sys import syslog try: import spwd HAVE_SPWD=True except: HAVE_SPWD=False USERADD = "/usr/sbin/useradd" USERMOD = "/usr/sbin/usermod" USERDEL = "/usr/sbin/userdel" def exit_json(rc=0, **kwargs): if 'name' in kwargs: add_user_info(kwargs) print json.dumps(kwargs) sys.exit(rc) def fail_json(**kwargs): kwargs['failed'] = True exit_json(rc=1, **kwargs) def add_user_info(kwargs): name = kwargs['name'] if user_exists(name): kwargs['state'] = 'present' info = user_info(name) if info == False: if 'failed' in kwargs: kwargs['notice'] = "failed to look up user name: %s" % name else: kwargs['msg'] = "failed to look up user name: %s" % name kwargs['failed'] = True return kwargs kwargs['uid'] = info[2] kwargs['group'] = info[3] kwargs['comment'] = info[4] kwargs['home'] = info[5] kwargs['shell'] = info[6] kwargs['createhome'] = os.path.exists(info[5]) groups = user_group_membership(name) if len(groups) > 0: kwargs['groups'] = groups else: kwargs['state'] = 'absent' return kwargs def user_del(user, **kwargs): cmd = [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(user, **kwargs): cmd = [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]): 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): 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(user, **kwargs): cmd = [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]): fail_json(msg="Group %s does not exist" % (kwargs[key])) ginfo = group_info(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(',') for g in groups: if not group_exists(g): 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 try: info = get_pwd_info(user) if HAVE_SPWD: sinfo = spwd.getspnam(user) except KeyError: return False if HAVE_SPWD: info[1] = sinfo[1] return info # =========================================== if not os.path.exists(USERADD): if os.path.exists("/sbin/useradd"): USERADD = "/sbin/useradd" else: fail_json(msg="Cannot find useradd") if not os.path.exists(USERMOD): if os.path.exists("/sbin/usermod"): USERMOD = "/sbin/usermod" else: fail_json(msg="Cannot find usermod") if not os.path.exists(USERDEL): if os.path.exists("/sbin/userdel"): USERDEL = "/sbin/userdel" else: fail_json(msg="Cannot find userdel") argfile = sys.argv[1] args = open(argfile, 'r').read() items = shlex.split(args) syslog.openlog('ansible-%s' % os.path.basename(__file__)) log_args = re.sub(r'password=.+ (.*)', r"password=NOT_LOGGING_PASSWORD \1", args) syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % log_args) if not len(items): fail_json(msg='the module requires arguments -a') sys.exit(1) params = {} for x in items: (k, v) = x.split("=") params[k] = v state = params.get('state','present') name = params.get('name', None) uid = params.get('uid', None) group = params.get('group', None) groups = params.get('groups', None) comment = params.get('comment', None) home = params.get('home', None) shell = params.get('shell', None) password = params.get('password', None) # =========================================== # following options are specific to userdel force = params.get('force', 'no') remove = params.get('remove', 'no') # =========================================== # following options are specific to useradd createhome = params.get('createhome', 'yes') system = params.get('system', 'no') # =========================================== # following options are specific to usermod append = params.get('append', 'no') if state not in [ 'present', 'absent' ]: fail_json(msg='invalid state') if createhome not in [ 'yes', 'no' ]: fail_json(msg='invalid createhome') if system not in ['yes', 'no']: fail_json(msg='invalid system') if append not in [ 'yes', 'no' ]: fail_json(msg='invalid append') if force not in ['yes', 'no']: fail_json(msg="invalid option for force, requires yes or no (defaults to no)") if remove not in ['yes', 'no']: fail_json(msg="invalid option for remove, requires yes or no (defaults to no)") if name is None: fail_json(msg='name is required') rc = None out = '' err = '' result = {} result['name'] = name if state == 'absent': if user_exists(name): (rc, out, err) = user_del(name, force=force, remove=remove) if rc != 0: fail_json(name=name, msg=err) result['force'] = force result['remove'] = remove elif state == 'present': if not user_exists(name): (rc, out, err) = user_add(name, uid=uid, group=group, groups=groups, comment=comment, home=home, shell=shell, password=password, createhome=createhome, system=system) else: (rc, out, err) = user_mod(name, uid=uid, group=group, groups=groups, comment=comment, home=home, shell=shell, password=password, append=append) if rc is not None and rc != 0: fail_json(name=name, msg=err) if password is not None: result['password'] = 'NOTLOGGINGPASSWORD' if rc is None: result['changed'] = False else: result['changed'] = True if out: result['stdout'] = out if err: result['stderr'] = err exit_json(**result) sys.exit(0)