Un-manage and keep in inventory

cstout
Contributor III
Contributor III

I'm looking to create a script (hopefully automated from beginning to end) that will kick off after successful imaging, kill the JAMF binary, and modify the record in the JSS to be an un-managed inventory-only record.

I've tried several different ways of accomplishing this and none of them work properly and it's likely due to timing. Some of the methods I've tried without success are an at-reboot script and a package that drops a launchdaemon which calls a script to run the removeFramework command.

I'm unsure if I will be able to automate this into my imaging workflow due to the fact that most of my imaging have a workflow that installs several packages at reboot. What I've tried so far attempts to kill the binary before imaging is actually complete. If it turns out I can't have this portion automated, it won't be the end of the world to have a script the technician can run after imaging.

What I'd like to see if I can include is a modification of the script found at:
https://jamfnation.jamfsoftware.com/discussion.html?id=10188

I'm curious to see if anyone out there has successfully used curl to modify this part of the computer inventory:
external image link

We have a department which receives computers imaged and prepared by our technical staff, but they are not on our network and not supported by us. Rather than use a different imaging product for this, I'd like to take advantage of Casper Imaging and have the binary self-destruct at the end; all while keeping the modified computer record in the JSS for inventory only.

Any ideas? Suggestions? Did someone out there already do all of this and today's going to be a really easy day?

2 ACCEPTED SOLUTIONS

mm2270
Legendary Contributor III

Oops. I messed up the xml structure actually. The management section is located under the "General" section which in turn is under "Computer" So it should actually look like this.

#!/bin/sh

apiuser="apiuser"
apipass="apipass"
jssURL="https://my.jss.server:8443/JSSResource/computers/macaddress"

MAC=$( /usr/sbin/networksetup -getmacaddress en0 | awk '{print $3}' | sed 's/:/./g' )

echo "<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<computer>
<general>
<remote_management>
<managed>false</managed>
</remote_management>
</general>
</computer>" > /private/tmp/unmanage_xml.xml

curl -sfku "${apiuser}:${apipass}" "${jssURL}/${MAC}" -T /private/tmp/unmanage_xml.xml -X PUT

Note the general tags added. I just tested it out and it successfully unmanaged a test Mac. Shows up now as "-Not Enrolled-"

View solution in original post

mm2270
Legendary Contributor III

Although it is good practice to properly unload the LaunchDaemon before deleting it, its not strictly necessary.

Can you try flipping the order of the commands under the self destruct part, like this?

#remove JAMF Binary
jamf removeFramework

#delete daemon
/bin/rm -f /Library/LaunchDaemons/unmanage.plist

#delete myself
/bin/rm -f "$0"

That should un-manage the Mac, remove the jamf framework, delete the daemon (without unloading it first), then finally delete the script itself, That will ensure it will never get run again. Only thing is, until the next reboot, launchd will still list that job as being loaded, but will show some error exit status in launchctl.
Again, its not best practice, but I'm unclear why the unload isn't working. I have a similar post install recon package we deploy after a major OS upgrade so it can recon the Mac back to the JSS as soon as possible, then self destructs. This keeps us from needing to have a policy with a "Startup" trigger set to constantly recon machines each time they reboot. We just drop that package in with any policies that are installing something that requires a full reboot and new inventory get submitted right after reboot. My script unloads the LaunchDaemon and I have no issues with that. Maybe its a specific OS issue? Have you seen this happen across different OS versions?

View solution in original post

26 REPLIES 26

mm2270
Legendary Contributor III

@cstout - you could try using the API with something like this script below. I haven't actually tried this, so I'm not sure if simply setting the managed flag in the Mac's record to "false" actually works, but I would assume it does.

#!/bin/sh

apiuser=apiuser"
apipass="apipass"
jssURL="https://my.jss.server:8443/JSSResource/computers/macaddress"

MAC=$( /usr/sbin/networksetup -getmacaddress en0 | awk '{print $3}' | sed 's/:/./g' )

echo "<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<computer>
<remote_management>
<managed>false</managed>
</remote_management>
</computer>" > /private/tmp/unmanage_xml.xml

curl -sfku "${apiuser}:${apipass}" "${jssURL}/${MAC}" -T /private/tmp/unmanage_xml.xml -X PUT

Matt
Valued Contributor

What about an unmanaged smart group based on AD membership?

cstout
Contributor III
Contributor III

@mm2270][/url, I created an apiuser account and tested out the script. It returns the following:

Script result:
There was an error.
The file /Library/Preferences/com.jamfsoftware.jamf.plist does not exist. Use the createConf verb to create it.

--This wasn't due to the missing quotes at the beginning of the script.

@Matt][/url, these computers are not going to be bound to AD and will be using a non-standard local admin account. Essentially, our technicians are imaging computers to provide this unmanaged department with the same suite of software we use in our supported areas. Once the computers are imaged, they leave campus and are not supported or managed by us.

Matt
Valued Contributor

So you would need the JSS for Inventory purposes only?

You could always put a dummy package hidden in the root and scope of it.

cstout
Contributor III
Contributor III

@mm2270, Sorry, I guess I'm still waking up. I failed to input the correct JSS address. After changing the address, the script passed, but didn't modify the field I'm looking to change.

mm2270
Legendary Contributor III

@cstout - what JSS version and, does the API account you're using have write privileges enabled? it will need that to make any change to the JSS. Read only won't cut it in this case.

I'm about to test this script out on a test Mac. Only thing is, we're still on 8.73 so it may not be a fully valid test if you're on some version of 9. I'll let you know if I can get it to work.

cstout
Contributor III
Contributor III

@mm2270, I'm on 9.32 and the API user has read/write access to JSS objects only.

mm2270
Legendary Contributor III

Oops. I messed up the xml structure actually. The management section is located under the "General" section which in turn is under "Computer" So it should actually look like this.

#!/bin/sh

apiuser="apiuser"
apipass="apipass"
jssURL="https://my.jss.server:8443/JSSResource/computers/macaddress"

MAC=$( /usr/sbin/networksetup -getmacaddress en0 | awk '{print $3}' | sed 's/:/./g' )

echo "<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<computer>
<general>
<remote_management>
<managed>false</managed>
</remote_management>
</general>
</computer>" > /private/tmp/unmanage_xml.xml

curl -sfku "${apiuser}:${apipass}" "${jssURL}/${MAC}" -T /private/tmp/unmanage_xml.xml -X PUT

Note the general tags added. I just tested it out and it successfully unmanaged a test Mac. Shows up now as "-Not Enrolled-"

cstout
Contributor III
Contributor III

@mm2270, that worked perfectly, thank you! I'm going to add 'jamf removeFramework' to the end of that to remove the binary and that should do the trick.

Now, the question is, what would be the best means of having this kick off after imaging? Everything I've tried so far is foiled because Casper Imaging runs inventory after all packages install. If one of my packages kills the binary before it submits inventory, not only do I not get my inventory record, but imaging stalls.

I might just package this script up and drop a pkg on the desktop for technicians to run after imaging completes.

mm2270
Legendary Contributor III

One possible solution- Deploy a LaunchDaemon in a pkg as part of the initial imaging to the Mac that uses either a WatchFolder or QueueDirectories key to look for a file modification or file existence. Have whatever your post imaging is doing as its final step after all is done and recon is run make a change to a file or folder or drop some file into an empty directory being watched,, which should fire off the LaunchDaemon. In the Daemon's ProgramArguments, have it run a script to unmanage it, then remove the JAMF framework.
Also, have the script verify that everything worked successfully, and then remove whatever watch path you're using and self destruct itself, so it won't ever try to run again.

Extra tip - the -f flag in the curl command in my script above can be used to determine a proper exit status of the xml upload. Normally curl will exit with status 0 even if the server refused the upload. This is because http sites are often designed to send back some kind of response, so for example, if your API credentials were wrong or didn't have the correct permissions, or your xml file was improperly formatted, you'd get some error status page back and, as far as curl is concerned, it was successful, because it got a response! The -f flag tells curl to fail silently, so if the command doesn't work it will exit with status 22. You can use that to your advantage in the script, something like

if [ "$?" != "0" ]; then

etc. directly after the curl PUT command.

cstout
Contributor III
Contributor III

@mm2270, Ok, I just made a two-part process based on your recommendation. I've never used a daemon with the QueueDirectories key until now. Would you see if what I've built out is matching what you recommended?

The first part will drop the daemon "/Library/LaunchDaemons/unmanage.plist" before first reboot. It should then load at first reboot and start watching the specified folder: "/private/tmp/unmanage"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>unmanage.job</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/private/tmp/unmanage/unmanage.sh</string>
    </array>
    <key>QueueDirectories</key>
    <array>
        <string>/private/tmp/unmanage/</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

At the end of the imaging process, a pkg will run which places the shell script in the "/private/tmp/unmanage/" folder. Since there are three reboots in my imaging process, I'm hoping it won't try to kick off the script as soon as it sees the file drop because the third and final reboot will likely interrupt the process.

mm2270
Legendary Contributor III

@cstout - In that case, I would suggest changing it up and instead drop both the LaunchDaemon and the script to the Mac contained together in the final package, and skip using any kind of WatchPath or QueueDirectories. Instead, have the LaunchDaemon kick off with a StartInterval of something like 120 seconds. Meaning, it will get deployed along with the script its using in the ProgramArguments in your final package. After it reboots the Mac for the last time, the Daemon will become active, but wait for 2 minutes to run the script, That will hopefully ensure a proper network connection is active at the time, since it will need that to do the XML upload to the JSS.

You could adjust that time out to say, 5 minutes, or, better yet, add some lines in your script up front that first check to see if the JSS is accessible before it continues. If you have a StartInterval set, it will exit, and wait for another x seconds to run and keep trying until it can successfully unmanage the Mac, then remove the jamf framework.

Does that make sense?

cstout
Contributor III
Contributor III

@mm2270][/url, yes, that makes sense. I'm currently trying that right now with a test image.

Side note: I've noticed that once that curl command is issued, the next time you try to image the same computer (after deleting the computer record in the JSS), the computer will be re-created with whatever new name you enter but will disregard the fact that the imaging configuration is telling it to be managed. I've run two test images since the first successful curl modification and each time the computer is set to unmanaged. Any ideas? Do I need to open a ticket on this one?

Edit: Also, I can't seem to get my pkg to drop the shell script in '/private/tmp/' Each time I test the image I see the daemon dropped properly, but the script isn't where I specified in Composer. If I send the pkg through Casper Remote a second time it will place it properly.

mm2270
Legendary Contributor III

Hmm, that's very odd. If the record is deleted from the database, it should be recreating it and setting it as managed once its added back in. If the record was left in the db, then I could understand that since it would pair up with the existing record. But deleting it should force it to create a new record with a new JSS ID.

Actually, can you confirm that the JSS ID is in fact being refreshed in the JSS once its re-imaged?
If so, I'd have to believe this is a bug. Can't imagine how it would be picking up that previous setting.

mm2270
Legendary Contributor III

@cstout - just saw your other comment on the script part. Are you placing the script in /private/tmp/ on a Mac and dragging it into Composer? Not sure that should make any difference, but try it that way, in case you were manually creating the folder path in Composer.
I've deployed plenty of stuff into /tmp/ in packages I've built and never have or had an issue with it.

cstout
Contributor III
Contributor III

Yeah, very strange. Each time I image the ID increases by 1 which is to be expected. What I didn't expect to see is, after deleting the computer from the JSS, imaging with a new name and a new ID, that it would continue to list as unmanaged. I'll do some more digging on this.

As far as the self-destruct pkg goes, I'm having one heck of a time. There's something wrong with my shell script and I'm not familiar enough to see what the problem is. I did place the script in /private/tmp/ and dragged it into Composer. It works properly when I push the pkg through Casper Remote, but when it runs at reboot using Casper Imaging it only places the daemon, not the script.

#!/bin/sh

#modify computer record and change it to an unmanaged, inventory-only record

apiuser="apiuser"
apipass="apiuser"
jssURL="https://casper-jss-name:8443/JSSResource/computers/macaddress"

MAC=$( /usr/sbin/networksetup -getmacaddress en0 | awk '{print $3}' | sed 's/:/./g' )

echo "<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<computer>
<general>
<remote_management>
<managed>false</managed>
</remote_management>
</general>
</computer>" > /private/tmp/unmanage_xml.xml

curl -sfku "${apiuser}:${apipass}" "${jssURL}/${MAC}" -T /private/tmp/unmanage_xml.xml -X PUT

#wait 5 seconds before moving forward
sleep 5

##begin self-destruct##

#unload launchdaemon that kicked off this script
/bin/launchctl unload /Library/LaunchDaemons/unmanage.plist

#delete daemon
/bin/rm -f /Library/LaunchDaemons/unmanage.plist

#remove JAMF Binary
jamf removeFramework

The problem appears to be in the ##begin self-destruct## part of the script since the unmanage.plist file is still on the computer and the binary is still present.

mm2270
Legendary Contributor III

Hmm, its probably because the package is being installed at reboot. /private/tmp/ is cleared upon reboot to my knowledge, and its also possible the folder may be inaccessible at the time the package is run, so ti can't drop anything in it.
Can you set the package with a very high priority and have it installed as the very last package rather than 'at reboot'? Asking honestly because its actually been a few years now since I played with Casper imaging (we use DeployStudio here still) so I'm a little fuzzy on how to do that, or if its even possible.

Not sure about the other part. From here, the commands to self destruct look OK. I'm not sure why it would be failing. Only thing that comes to mind is to make sure the StartInterval is long enough to allow the script to finish before launchd attempts to run the job again. The xml upload can take several seconds to finish out in my experience. I've seen weird occurrences of jobs that take a long time to complete colliding into each other as launchd tries to dutifully run it again at the scheduled Start Interval.

cstout
Contributor III
Contributor III

Ok, just in case the /private/tmp folder is killing my shell script before it can run, I'm going to relocate it to /private/var/tmp. Along with that I'm going to extend the startinterval to 180 seconds instead of 120.

cstout
Contributor III
Contributor III

@mm2270, I just manually loaded the daemon and ran the line in my script to unload it and found that's where the script is breaking. It won't unload and I'm not positive why.

Here's my latest daemon:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>unmanage.plist</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/private/var/tmp/unmanage.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>180</integer>
</dict>
</plist>

The error that I receive when running the unload command is:

launchctl: Error unloading: unmanage.plist

mm2270
Legendary Contributor III

And you did that with sudo in front, yes?

Can you also do this? While the LaunchDaemon is still active, run:

sudo launchctl list | grep "unmanage"

and see what shows up in the results? I'm wondering if the job is showing up under a slightly different name perhaps? OK, I'm taking wild guesses here at this point honestly, since I'm really not clear where the issue would be.

Edit: Fixed to say "unmanage" not "unload"

cstout
Contributor III
Contributor III

@mm2270, ok, here's some more info.

Launchctl is killing the script. After a successful unload, it simply stops the script and doesn't process any further. I just edited the script and tried it without unloading and the script worked great to remove the JAMF framework and change the JSS record to inventory only.

My last test was a script that called another script to see if I could have at least one script finish and delete itself. No luck though. At the same place where it calls and successfully runs the other script, as soon as launchctl's command finishes, the bash script ends even though there's more after that command that needs to process.

With your help I'm 99% there!

mm2270
Legendary Contributor III

Although it is good practice to properly unload the LaunchDaemon before deleting it, its not strictly necessary.

Can you try flipping the order of the commands under the self destruct part, like this?

#remove JAMF Binary
jamf removeFramework

#delete daemon
/bin/rm -f /Library/LaunchDaemons/unmanage.plist

#delete myself
/bin/rm -f "$0"

That should un-manage the Mac, remove the jamf framework, delete the daemon (without unloading it first), then finally delete the script itself, That will ensure it will never get run again. Only thing is, until the next reboot, launchd will still list that job as being loaded, but will show some error exit status in launchctl.
Again, its not best practice, but I'm unclear why the unload isn't working. I have a similar post install recon package we deploy after a major OS upgrade so it can recon the Mac back to the JSS as soon as possible, then self destructs. This keeps us from needing to have a policy with a "Startup" trigger set to constantly recon machines each time they reboot. We just drop that package in with any policies that are installing something that requires a full reboot and new inventory get submitted right after reboot. My script unloads the LaunchDaemon and I have no issues with that. Maybe its a specific OS issue? Have you seen this happen across different OS versions?

cstout
Contributor III
Contributor III

Yeah, I think that's what I'm going to have to go with, at least for now. I haven't tested this on anything other than Mavericks so far and that may be the only OS that we use this on as well. In testing I did have success with simply deleting the daemon instead of unloading then deleting. I'm at a loss as to why running launchctl is stopping my script, but I'm satisfied with the results from simply deleting it.

All that said, I can't thank you enough, Mike. Your help has been instrumental in the success of this script.

themonger13
New Contributor II

@mm2270 The unmanage script works great if I run it locally... If I upload it into the scripts section of the JSS and running it via policy, I keep getting "error 22"

I've tried futzing with it a bit, with no luck. I'd rather not have it run locally, if I can avoid it.

mm2270
Legendary Contributor III

@themonger13 one nice thing about curl is the man page for it has a ton of info on all the possible error codes it can receive. Looking through it I see error 22 states:

22 HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f, --fail is used.

So it has something to do with the JSS URL you're passing to the curl command it would seem. Though that doesn't explain why it would work when run locally and not when run through a policy. You might have to provide a little more detail for us to figure out why its failing.
The other thing is, the script I posted above was written a while ago, tested under a much older version of the JSS, so I don't know if something else changed in the interim that would be causing an issue here.

themonger13
New Contributor II

@mm2270 fun fact- it wasn't working in my test VM, but works fine on a regular computer. Which works for me.