How to write bash script for creating user on multiple Linux hosts?

I wonder whether someone can help me with what I'm trying to achieve

Basically, the objective is one script to create new user on more than 70 linux hosts if required.

Everything works apart from the highlighted part. It gave me an output
passwd: Unknown user name ''. when try to set remote password for new user. It seems like it's trying to set password locally and not remotely.

The idea is all information about username,password,uid,comments is pull out from the file IDENTIFY. That way, if we create new user, people only edit the file than the script.
All fields in the IDENTIFY file separates by white space.

Please help how can I achieve this

#!/bin/bash

HOSTS="/tmp/test/serverlist"
DDID="IDENTIFY"
HOMEPATH="/home/$UNAME"

for i in `cat $HOSTS` ;
do


UNIQUE=`awk -F " " '{print $1 }' $DDID`

RUID=`ssh $i 'grep "$UNIQUE" /etc/passwd'`

if [[ -ne "$RUID" ]]
        then
                echo "User ID is currently available on $i, ready to add new user"
                UNAME=`awk -F " " '{print $2 }' $DDID`
                PASSWORD=`awk -F " " '{print $3 }' $DDID`
                ROLE=`awk -F " " '{print $4 }' $DDID`

               
                `ssh $i useradd -u "$UNIQUE" -d "$HOMEPATH" -s /bin/bash -c "$ROLE" -m -k /etc/skel/ "$UNAME"`
                `ssh $i echo "$PASSWORD" | passwd --stdin "$UNAME"`

else
                echo "User ID exist on $i, check new ID"

fi
done

I would do one ssh per host and ship the scripting to the remote host, for speed and simplicity.

For getting hosts from your config file, just:

while read i
 do
  ...
 done < file

For good error handling, echo each host, time and user as you start each, into a log. you can display the log to the terminal, too, with 'tee -a' or 'tail -f log_file &' (just remember to kill $!). Screens can go poof, and you should keep a report.

A user name can be a substring of any field (like roo or oot !), so put some boundaries in the grep:

grep -c "^${UNIQUE}:" /etc/passwd

Count 0/1 is a nicer test.

1 Like

Thanks DGPickett

I am new in bash, and I didn't totally understand what you're trying to say.

Do you mean, change the for loop to while loop, scp the script to the remote server and run it?

If so, can you shed some light of how while loop fit in my script. I just need some directions that's all

Substring is something that is new to me and will look into it

You can put a whole script on the command line, or just send it into ssh bash. I prefer "echo '...'|ssh ...." but "<<!" is pretty popular, too, just be careful to escape meta like $ from local expansion. No need to leave files lying around or have multiple scripts. Don't forget to set up env on remote shell, like '. ./.profile' or the like.

1 Like

You should take a look at this thread:

In which I propose using the -p parameter of useradd to create the user and assign the password all in one go

1 Like

Passwords should be random and expired already.

1 Like

Your problem is that the right side of the pipe is executed on the calling host.
You need to force it to the remote host, by quoting:

ssh $i echo "$PASSWORD" "|" passwd --stdin "$UNAME"

or simpler

ssh $i echo "$PASSWORD | passwd --stdin $UNAME"

Please omit the backticks! They will run the output as commands. If the output would be "reboot" ... guess what happens!
Further, you can combine two adjacent ssh statements in one ssh statement:

ssh $i "
useradd -u $UNIQUE -d $HOMEPATH -s /bin/bash -c '$ROLE' -m -k /etc/skel/ $UNAME
echo "$PASSWORD | passwd --stdin $UNAME
"

The calling host sees one multi-line "string".
Within the "" quotes it replaces each $VAR by its value.
$ROLE should be quoted on the remote host; you need either \"$ROLE\" or '$ROLE'. The '' ticks do not replace $ROLE by its value, but this is meaningless on the remote host, because it is already replaced by the calling host. And the calling host treats a ' tick as simple characters if it appears within a "string".
There are many more possible optimizations; DGPicket pointed out some...

1 Like

Yes, at least with

echo ' ... ' | ssh .... 

it is easy to predict what goes down the pipe. It is not that hard to type '"'"' when you need a '.

1 Like

An attempt for an optimized script:

#!/bin/bash

HOSTS="/tmp/test/serverlist"
DDID="IDENTIFY"
HOMEPATH="/home/$UNAME"

# read from $DDID
while read UNIQUE UNAME PASSWORD ROLE
do
    # read from $HOST
    while read i
    do

        echo "
# remote script start
# (in quotes, passed to /bin/sh on HOST; don't use quotes here!)
PATH=/bin:/usr/bin:/usr/sbin:/sbin
export PATH
if getent passwd $UNIQUE >/dev/null
then
 echo 'UID $UNIQUE exists on $i, check new ID'
elif getent passwd $UNAME >/dev/null
then
 echo 'User $UNAME exists on $i'
else
 echo 'UID $UNIQUE is currently available on $i, ready to add new user'
 useradd -u $UNIQUE -d '$HOMEPATH' -s /bin/bash -c '$ROLE' -m -k /etc/skel/ $UNAME
 echo '$PASSWORD' | passwd --stdin $UNAME
fi
# remote script end
" | ssh -x $i /bin/sh

    done < $HOSTS

done < $DDID

This script allows more than one line in the IDENTITY file.

1 Like

Thanks madeingermany

That is another neat way of doing it, unfortunately

when I run it twice, it doesn't give out the output that the user or uid exist

[root@GPGLNX02 test]# ./useradd-1.sh
[root@GPGLNX02 test]# ./useradd-1.sh

Also, when I ssh to remote server, the home directory hasn't been created by script

[root@GPGLNX02 test]# ssh user1@gpglnx05
user1@gpglnx05's password:
-bash-3.2$ pwd
/home/

---------- Post updated at 05:02 PM ---------- Previous update was at 04:58 PM ----------

Thanks Guys for the -p option in useradd.

The password side of my script is now working

However, it doesn't create home directory using the script and also the if arguments kept re-create user instead of saying "User ID does exist....." if I ran the script twice

Can someone have another second look please?

#!/bin/bash

HOSTS="/tmp/test/serverlist"
DDID="IDENTIFY"
HOMEPATH="/home/$UNAME"

for i in `cat $HOSTS` ;
do

UNIQUE=`awk -F " " '{print $1}' $DDID`

RUID=`ssh $i 'grep "$UNIQUE" /etc/passwd'`

if [[ -ne "$RUID" ]]
        then
                echo "=====User ID is currently available on $i, ready to add new user====="
                UNAME=`awk -F " " '{print $2 }' $DDID`
                PASSWORD=`awk -F " " '{print $3 }' $DDID`
                ROLE=`awk -F " " '{print $4 }' $DDID`

                        #`ssh $i useradd -u "$UNIQUE" -d "$HOMEPATH" -s /bin/bash -c "$ROLE" -m -k /etc/skel/ "$UNAME"`
                        ssh $i useradd -u "$UNIQUE" -d "$HOMEPATH" -c "$ROLE"  -p $(openssl passwd "$PASSWORD") "$UNAME"
                 echo "==========User $UNAME created=========="
        else
                echo "*****User ID does exist on $i, check new ID*****"
fi

done

-m will only create the home directory if CREATE_HOME in /etc/login.defs is set to yes.

Don't think this test is correct, UID may appear in other places in password file (eg phone extension, etc), -ne incorrect for testing for blank strings:

RUID=`ssh $i 'grep "$UNIQUE" /etc/passwd'`

if [[ -ne "$RUID" ]]

Try something like this

RUID=`ssh $i  awk -F: '\$3=="'$UNIQUE'"' /etc/passwd`

if [[ -z "$RUID" ]]
1 Like

The userid was $1 last I looked! OK, maybe user name, but isn't that what admins should be keying on? If you want the uid to match on all hosts, it's yp/nis time.

1 Like

You're right
DGPickett

$1 is the uid value

Chubler_XL
Can you please clarification why you have both double and single quotes on
"'$UNIQUE'"

RUID=`ssh $i  awk -F: '\$3=="'$UNIQUE'"' /etc/passwd`

It's horrible isn't it. The reason is that we need $UNIQUE to be expanded client side (before it's passwd over ssh) the double quotes are needed in the awk program so awk ends up seeing something like $3=="2213" .

These double and triple levels of quoting is one of the main reasons I nearly always try and build my scripts client side, then scp them to the server and execute there. Anything longer than one or two lines turns into a quoting storm that's very difficult to read or debug.

One way to avoid grief is to put everything in ', except ' itself and $varaibles and file globs. For $variable, you nip out of ' land and into " land for the variable and then go back to ' land. ' itself is '"'"' or '\'' file globs must be barefoot.

Yes, that technique helps somewhat but I still end up reverting to trial and error more often than not.

Especially with awk which inexplicably seems to need odd numbers of backslash characters (3,5 or 7!) to get some complex REs working (and worse still it varies depending the expression being within // or "").

Chubler

What is the reason of the escape(\) on $3 on the following option you suggested

RUID=`ssh $i  awk -F: '\$3=="'$UNIQUE'"' /etc/passwd`

That stops the remote shell from expanding $3

Command starts as

ssh $i awk -F: '\$3=="'$UNIQUE'"' /etc/passwd`

The remote shell gets:

ssh $i awk -F: \$3=="2341" /etc/passwd

Awk sees:

$3==2341

So you can see the double quotes aren't really doing much and should probably also have backslash to protect them from the remote shell and pass them thru to awk (my intention was for awk to see $3="2341" but as UIDs are numeric this will still work)

That's why my optimized script above has

getent passwd $UNIQUE

Funny I always associate getent with NIS, is it installed by default on most systems now?