Script to Mass Remove Home Folders in Users Directory

tomgideon2003
Contributor

We have been needing to find a script that works to delete all the home folders in the Users directory except for four that we need to keep (map, tech, student, and admn). We have a setup of mobile home directories that are cached on each laptop when network users first use them. Here is the script that support helped build. I am getting a syntax error when running it at the first "for".

#!/bin/sh
########################################################################################################
#
# Copyright (c) 2013, JAMF Software, LLC.  All rights reserved.
#
#       Redistribution and use in source and binary forms, with or without
#       modification, are permitted provided that the following conditions are met:
#               * Redistributions of source code must retain the above copyright
#                 notice, this list of conditions and the following disclaimer.
#               * Redistributions in binary form must reproduce the above copyright
#                 notice, this list of conditions and the following disclaimer in the
#                 documentation and/or other materials provided with the distribution.
#               * Neither the name of the JAMF Software, LLC nor the
#                 names of its contributors may be used to endorse or promote products
#                 derived from this software without specific prior written permission.
#
#       THIS SOFTWARE IS PROVIDED BY JAMF SOFTWARE, LLC "AS IS" AND ANY
#       EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
#       WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#       DISCLAIMED. IN NO EVENT SHALL JAMF SOFTWARE, LLC BE LIABLE FOR ANY
#       DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
#       (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
#       LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#       ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#       SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
####################################################################################################
# PURPOSE
# - Remove or Move local user directories from AD bound machines that have "Force Local Homes" checked
# 
# HISTORY
#   Version: 1.0
#   - Created by Bram Cohen October 15. 2013
# 
####################################################################################################

username1="map"
username2="tech"
username3="student"
username4="admn"


####################################################################################################
# SCRIPT OPERATIONS -  - REALLY!!! - DO NOT MODIFY BELOW THIS LINE
####################################################################################################
RESULT=""
for U in /Users/*; do 
    if [ -d "$U" ]; then
        if [ "$U" == "/Users/Shared" ] || [ "$U" == "/Users/Guest" ]; then
            /bin/echo "Found $U, ignored"
            USERNAME=`/bin/echo $U | tr '/' ' ' | awk '{print $NF}'`
            RESULT=`echo "$RESULT$USERNAME-IGNORED "`
        else
            /bin/echo "Found $U, continuing..."
            USERNAME=`/bin/echo $U | tr '/' ' ' | awk '{print $NF}'`
            /bin/echo "Parsed username as: $USERNAME"
            ADMINCHECK=`/usr/bin/dsmemberutil checkmembership -U $USERNAME -G admin | awk '{print $3}'`
            if [ "$ADMINCHECK" == "not" ]; then
                if [ "$USERNAME" == "$username1" ] || [ "$USERNAME" == "$username2" ] || [ "$USERNAME" == "$username3" ] || [ "$USERNAME" == "$username4" ]; then
                    /bin/echo "$USERNAME is on the exempt list, ignoring..."
                    RESULT=`echo "$RESULT$USERNAME-EXEMPT "`
                else    
                    /bin/echo "$USERNAME not an Admin nor exempt, taking action..."
                    RESULT=`echo "$RESULT$USERNAME-Action Taken "`
                    #/bin/echo "Forcing the removal of $U" && /bin/rm -rf $U
                fi
            else
                /bin/echo "$USERNAME was found to be a Local Admin, ignoring"
                RESULT=`echo "$RESULT$USERNAME-ADMIN "`
            fi
        fi
    fi
done
echo ""
echo "============================================================"
echo "Summary: $RESULT"
echo "============================================================"
1 ACCEPTED SOLUTION

denmoff
Contributor III

Here's a python script that i've created to do basically what you are looking for. I've added in your four accounts as the directories to ignore. I've defanged it by commenting out the shutil.rmtree command. That will delete the directories.

UPDATED Code The following code has been updated to more correctly get the logged in username and to solve an issue that was causing the shutil.rmtree() to fail.

If you are new to python, spacing matters! There are exactly 4 spaces for every indent(don't use tab).

#!/usr/bin/python

import os, shutil, getpass, subprocess, sys
from SystemConfiguration import SCDynamicStoreCopyConsoleUser

# Get the logged in user's username
username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]
username = [username,""][username in [u"loginwindow", None, u""]]
logged_in_user = username

# directory to work in
dir_to_check = '/Users/'

do_not_touch = []
do_not_touch.append(os.path.join(dir_to_check,'Shared'))
# directories to ignore. Add any directory in /Users that you want to protect below. 
# For example:
# do_not_touch.append(os.path.join(dir_to_check,'admin'))

# If you do not want the script to remove the logged in user's directory, uncomment the next line
# do_not_touch.append(os.path.join(dir_to_check, logged_in_user))


dirs = os.listdir(dir_to_check)

# run thru each file in the directory and delete all directories that are not specifically excluded.
for d in dirs:
    full_path = os.path.join(dir_to_check, d)
    if os.path.isfile(full_path) or full_path in do_not_touch:
        continue
    else:
        # clear all flags that might be set. 
        subprocess.call(['/usr/bin/chflags','-R','0',full_path])
        try:
            # You can test with a print statement before uncommenting the shutil.rmtree line
            print full_path
            # When you are ready, uncomment the next line. It will remove the user's directory and everything in it. So, be careful! 
            # shutil.rmtree(full_path)
        except Exception, e:
            print e

View solution in original post

34 REPLIES 34

cdenesha
Valued Contributor II

I did not have any trouble with the script, exactly as written (thank you for commenting out the rm). What is the error you are getting?

tomgideon2003
Contributor

Actually, I got it to work now. I am nowhere good at scripting. The problem I am having now is that it is seeing the home directories as Admin accounts and it is not removing them. I tried removing the Admincheck statement but it still did not work. I am just wanting to keep those 4 local accounts on the computers. Any ideas? Thanks!

mm2270
Legendary Contributor III

Try using dseditgroup istead of dsmemberutil.

ADMINCHECK=`/usr/sbin/dseditgroup -o checkmember -u $USERNAME admin 1 > /dev/null; echo $?`
if [ "$ADMINCHECK" != "0" ]; then
     etc, etc, etc,

tomgideon2003
Contributor

I tried that and I am getting this on each home directory:

This is an example of one (names different):
Found /Users/zima micah, continuing...
Parsed username as: micah
Unable to find the user record
micah not an Admin not exempt, taking action...

I am not sure about the parsing but I am guessing maybe because our naming convention has a space that is messing it up? It would be nice if there was any way to delete all the home directories that include a space. But I don't know if that is possible.

mm2270
Legendary Contributor III

@tomgideon2003][/url][/url][/url

Give this a try. I deleted your additional comments and stuff while I was working on it, so you'd need to add that all back in, but this uses a process substitution loop. It also builds a list of accounts based on dscl instead of looking in the Users directory. This has both advantages and disadvantages. The advantages are that it will find any accounts with UIDs of 501 and up no matter where their home directories are located. It also avoids needing to exclude items like Shared and other possible folders in the /Users/ directory.
Lastly, thug hnot really necessary it builds an array with the results and echoes that back out in the end.

This could be useful if you decided to modify this to be an Extension Attribute since you can only have one echo "<result></result>" line in an EA. Putting it all into an array lets you echo out the entire array later at the end.

#!/bin/bash

user1="map"
user2="tech"
user3="student"
user4="admn"

RESULT=()

while read user; do
    userHome=$( dscl . read /Users/$user NFSHomeDirectory | awk '{print $NF}' )
    if [ "$user" == "$user1" ] || [ "$user" == "$user2" ] || [ "$user" == "$user3" ] || [ "$user" == "$user4" ]; then
        RESULT+=( $(echo "Found user $user in exempt list, ignoring...
") )
    else
        RESULT+=( $(echo "User $user not found in exempt list, checking admin status...
") )
        if [[ $( dseditgroup -o checkmember -u $user admin 1 > /dev/null; echo $? ) == "0" ]]; then
            RESULT+=( $(echo "User $user was found to be a Local Admin, ignoring...
" ) )
        else
            RESULT+=( $(echo "User $user is not a Local Admin and is not in the exempt list, taking action...
" ) )
            RESULT+=( $(echo "$userHome will be deleted...") )
            # rm -rf "$userHome"
        fi
    fi
done < <(dscl . list /Users UniqueID | awk '$2 > 500 {print $1}')

echo -e "${RESULT[@]}"

Note also that I changed the shell to bash. The bourne shell doesn't support process substitution.

In testing this out on my Mac with 3 accounts (one AD based (mobile account) and two local. Of the local ones, one that is an admin and the other not), I added my account name into the exempt list and it correctly reported mine as being in the exempt list, the second account as being an admin and skipped, and the 3rd needing action to be taken on. So in short, seemed to work for me., See if it works for you.

tomgideon2003
Contributor

Thank you for your work, I tried it out but the home directory folders all stayed there. It did successfully exclude the four accounts that I need.

mm2270
Legendary Contributor III

Totally silly question, but did you uncomment the rm line? I had it commented out when I was testing the script and I forgot to remove the # and posted it that way.

Edit: Also, try using rm -Rfd for the delete line, which should also get rid of the top level folder.

tomgideon2003
Contributor

I tried both of those and the home directory folders still stayed there. It is like it doesn't even see those folders since they are mobile accounts.

mm2270
Legendary Contributor III

Curious. Are they true mobile accounts as in cached for offline use, or do they only work when connected to your domain? Its possible the way they are set up has something to do with it. Though a cached mobile account should be getting picked up correctly with the dscl line. They would have UIDs in the 1000 range (usually much higher) My AD account is a mobile account and the script sees it and checks my name against the exclude list, so its seeing it.
What output do you get in the echo? Is it listing that it found those accounts at all?

mm2270
Legendary Contributor III

While I haven;t tested this against mobile accounts, I did just run a test (on a test Mac) with a few accounts on it, one being a non admin local account. It correctly deleted the home folder from the /Users/ directory. But I realized after we forgot to add one crucial line. After the rm -Rfd.. add in:

dscl . delete /Users/$user

That makes sure it removes it from directory services. Although the home folder was gone in my case, it still appeared in Users & Groups. The above line will make it disappear from there as well.

If this all doesn't work for your mobile accounts, you may need to pick apart the script line by line and see where its getting tripped up, For example. run this in Terminal to see what the output is:

dscl . list /Users UniqueID | awk '$2 > 500 {print $1}'

If you see the mobile accounts listed there, copy one of them and run this-

dscl . read /Users/<copied name> NFSHomeDirectory

See what the home directory path is being listed as.

And lastly, you are running this as root, or running it from a Casper policy, correct?

denmoff
Contributor III

Here's a python script that i've created to do basically what you are looking for. I've added in your four accounts as the directories to ignore. I've defanged it by commenting out the shutil.rmtree command. That will delete the directories.

UPDATED Code The following code has been updated to more correctly get the logged in username and to solve an issue that was causing the shutil.rmtree() to fail.

If you are new to python, spacing matters! There are exactly 4 spaces for every indent(don't use tab).

#!/usr/bin/python

import os, shutil, getpass, subprocess, sys
from SystemConfiguration import SCDynamicStoreCopyConsoleUser

# Get the logged in user's username
username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]
username = [username,""][username in [u"loginwindow", None, u""]]
logged_in_user = username

# directory to work in
dir_to_check = '/Users/'

do_not_touch = []
do_not_touch.append(os.path.join(dir_to_check,'Shared'))
# directories to ignore. Add any directory in /Users that you want to protect below. 
# For example:
# do_not_touch.append(os.path.join(dir_to_check,'admin'))

# If you do not want the script to remove the logged in user's directory, uncomment the next line
# do_not_touch.append(os.path.join(dir_to_check, logged_in_user))


dirs = os.listdir(dir_to_check)

# run thru each file in the directory and delete all directories that are not specifically excluded.
for d in dirs:
    full_path = os.path.join(dir_to_check, d)
    if os.path.isfile(full_path) or full_path in do_not_touch:
        continue
    else:
        # clear all flags that might be set. 
        subprocess.call(['/usr/bin/chflags','-R','0',full_path])
        try:
            # You can test with a print statement before uncommenting the shutil.rmtree line
            print full_path
            # When you are ready, uncomment the next line. It will remove the user's directory and everything in it. So, be careful! 
            # shutil.rmtree(full_path)
        except Exception, e:
            print e

tomgideon2003
Contributor

@mm2270][/url

I tried running it with the local root account. It hung up and is saying "Group not found". I did the command to list user ID's over 500. I got just these 4 accounts that I want to keep to display. The mobile user accounts are not available outside our domain so it is probably a much different setup.

mm2270
Legendary Contributor III

@tomgideon2003][/url - yeah,. must be. I'm not familiar with the kind of setup you have, so I don't know how much I can help. It sounds like to do what you really want, you may need to consider moving those AD accounts over to mobile accounts with cached local home directories.
There are obviously reasons why some orgs decide not to do this, but there are also some real benefits of using them that way.

tomgideon2003
Contributor

@denmoff

Hi! I tried running your script as the root user. I got this error:

line 24 full_path = os.path.join(dir_to_check, d) ^ (at the end of "full_path")
IndentationError: expected an indented block

PeterClarke
Contributor II

Here is one that I use:

It's designed to ONLY delete AD-Mobile-Accounts:

#!/bin/sh

# Program: REMOVE_ALL_AD_User_Accounts_On_LAB_Machines.sh
# By: Peter Clarke
echo "Program: REMOVE_ALL_AD_User_Accounts.sh"
echo

count=0
for TheUser in $(dscl . -list /Users AuthenticationAuthority | grep LocalCachedUser | awk '{print $1}' | tr ' ' ' '); do

echo "--------------------------------------------------------------------------" echo "User: " ${TheUser} echo sudo dscl . -delete /Users/${TheUser} sudo rm -rdfv /Users/${TheUser}

count=$((count + 1))
done

sudo -k
echo
echo "Completed."
echo "Number of accounts removed: " ${count}
echo

denmoff
Contributor III

@tomgideon2003 There should be exactly four spaces in each indent. Python is very particular about this.

tomgideon2003
Contributor

@denmoff

Thank you, this has now worked excellent for my situation!

Zvordauk
New Contributor III
New Contributor III

@denmoff Thank you, very elegant solution!
One query, I have a Mac (only one so far - testing the script) that will not remove the content from a user folder. It spits out the following error:

OSError: [Errno 1] Operation not permitted: '/Users/2549/.Trash/Portable Apps/PhotoshopPortable/Data/PhotoshopCS5/AppDataAll/Acrobat/9.0/Replicate/Security/directories.acrodata'

Any advice on how to log the error and carry on to the next file/folder?

denmoff
Contributor III

@Zvordauk Looks like i wrote this before learning about handling exceptions. I'll have to look at this later when i have some time.

Are you running this as root? Looks like a permissions issue. If you run as root, you shouldn't run into this issue.

Cranappras
Release Candidate Programs Tester

@denmoff I'm getting the same error as @Zvordauk when trying to run this, but it doesn't happen all of the time. I haven't been able to test this yet, but I'm wondering if it only happens when there are items sitting in the ~/.Trash/ folder.

OSError: [Errno 1] Operation not permitted: '/Users/anadams/.Trash/CONTENTS/AUDIO/0001IY00.MXF'

Zvordauk
New Contributor III
New Contributor III

Just an update from my side: I ran this across the network and of 125 Macs in the group I had around 10 fails. These were not confined to .trash
(This was run as a policy - it would have run as root).

Some examples:
OSError: [Errno 1] Operation not permitted: '/Users/3549/Desktop/Aarons Scipt.fdx.txt’

OSError: [Errno 1] Operation not permitted: '/Users/2284/Desktop/documents/Adobe/Acrobat 7.0/Help/ENU/Reader.pdf’

OSError: [Errno 1] Operation not permitted: '/Users/1939/Desktop/Web Design/lesson2/Exccx82axccx82exccx80nxccx83xc3x9faxccx80Exccx80exccx80.Nxccx83Ixccx82Uxccx81’

OSError: [Errno 66] Directory not empty: '/Users/fomahony/Library’

Hope this helps

denmoff
Contributor III

@Zvordauk @jsmith-ppu Thanks for the updates. This is a weird issue. I looked thru my logs and i found one computer having this problem with one of the user directories. It was an [errno 1], which is a permissions issue. It was a .DS_Store file in the .Trash folder. I logged onto that computer and ran the shutil.rmtree() from a python shell as root and it spit out the same error. I copied the .Trash directory to another directory and ran the command again on that directory and it worked without error. I ran an rm -Rf on the original .Trash directory and that worked without error(expected it to fail) and then reran the shutil.rmtree() on the user directory and it worked without error. I have no idea why the python shutil.rmtree() would error like that. Still looking into it, but now i can't really test it because that one computer was my only one with a problem.

denmoff
Contributor III

@Zvordauk @jsmith-ppu Would either of you be able to login to an affected machine and run an 'ls -le' on the file that's causing trouble? I wonder if it has some strange extended attributes that are causing this issue.

Cranappras
Release Candidate Programs Tester

@denmoff Here is the output from both the script and the 'ls -le' command. The script worked on some of the home directories before finally getting an [errno 1].

/Users/blbogol
/Users/bmdean
/Users/bmtabic
/Users/brweinz
/Users/bzalak
/Users/cbcolli
Traceback (most recent call last):
File "/Library/Application Support/JAMF/tmp/MassClearHomeDirectories", line 30, in
shutil.rmtree(full_path)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 247, in rmtree
rmtree(fullname, ignore_errors, onerror)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 247, in rmtree
rmtree(fullname, ignore_errors, onerror)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 247, in rmtree
rmtree(fullname, ignore_errors, onerror)
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 252, in rmtree
onerror(os.remove, fullname, sys.exc_info())
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/shutil.py", line 250, in rmtree
os.remove(fullname)
OSError: [Errno 1] Operation not permitted: '/Users/cbcolli/.Trash/CONTENTS/AUDIO/0001PL00.MXF'
Submitting log
Finished.

Running command ls -le '/Users/cbcolli/.Trash/CONTENTS/AUDIO/'...
Result of command:
total 798752
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:03 0001PL00.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:03 0001PL01.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:03 0001PL02.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:03 0001PL03.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:08 0002CJ00.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:08 0002CJ01.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:08 0002CJ02.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 28381312 Apr 1 13:08 0002CJ03.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 19988928 Apr 1 13:13 0003T400.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 19988928 Apr 1 13:13 0003T401.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 19988928 Apr 1 13:13 0003T402.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 19988928 Apr 1 13:13 0003T403.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 10830976 Apr 1 13:18 0004IE00.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 10830976 Apr 1 13:18 0004IE01.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 10830976 Apr 1 13:18 0004IE02.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 10830976 Apr 1 13:18 0004IE03.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 14639584 Apr 1 13:21 0005BC00.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 14639584 Apr 1 13:21 0005BC01.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 14639584 Apr 1 13:21 0005BC02.MXF
-rwxrwxrwx 1 cbcolli POINTPARKStudents 14639584 Apr 1 13:21 0005BC03.MXF
Submitting log
Finished.

denmoff
Contributor III

Okay. That didn't help. But i think i may know what's going on. The append only flag might be set on those files. Try running this command on that 0001PL00.MXF file:

ls -lO /Users/cbcolli/.Trash/CONTENTS/AUDIO/0001PL00.MXF

Cranappras
Release Candidate Programs Tester

Here you go:

Running command ls -lO /Users/cbcolli/.Trash/CONTENTS/AUDIO/0001PL00.MXF...
Result of command:
-rwxrwxrwx 1 cbcolli POINTPARKStudents uchg 28381312 Apr 1 13:03 /Users/cbcolli/.Trash/CONTENTS/AUDIO/0001PL00.MXF

denmoff
Contributor III

Wow! that was fast. That appears to be the user immutable flag. The solution to this is going to be to run 'chflags -R nouchg' on the user's directory prior to running the shutil.rmtree operation. I'll edit the script above so that i incorporates the chflags command into it.

Cranappras
Release Candidate Programs Tester

Awesome, thanks for taking the time to diagnose this and update the script. It's a huge help!

denmoff
Contributor III

Glad to help! I've just finished making the changes. I made a few other alterations to clean up a few things. Let me know if you have any questions about what i did.

Cranappras
Release Candidate Programs Tester

I just tried out the new script and it worked great on the systems that were previously experiencing problems. Thanks again!

Zvordauk
New Contributor III
New Contributor III

Sorry @denmoff been bogged down in deployment! Can you send on a link to the updated script and I'll run it through tomorrow.
Thanks for the support!

denmoff
Contributor III

@Zvordauk I've made the changes in my original script up above.

Zvordauk
New Contributor III
New Contributor III

D'oh! That's the problem with skimming stuff after an 18 hour shift :(

I'm probably doing something very stupid but I'm getting the following error:

Script result: File "/Library/Application Support/JAMF/tmp/RemoveExtraUsers.py", line 41
except Exception, e:
^
SyntaxError: invalid syntax

denmoff
Contributor III

@Zvordauk Probably because everything in the "try" statement is commented out. I've changed the script again so that has the print statement uncommented after the try statement. But don't forget that i've commented out the shutil.rmtree statement. You'll need to uncomment that line before it will actually do anything irrevocable.