PKG User Filler Script

Not applicable

All,

Here's a postinstall script I wrote in an hour or so to fill user home folders for PKG-style packages. It should work as-is, but I have a couple questions. Does anyone know which parameter passed to package scripts tells it where to install? And does anyone know of a way to get id to work properly from a different volume without using chroot?

Thanks.

#!/usr/bin/python

"""
This script clones anything it finds in /private/tmp/fillUsers to all existing
non-hidden users, as well as all user template localizations. It is meant to be
used as a postinstall script for packages that need this functionality,
mimicking the functionality available in JAMF's Casper Suite for DMG packages.
Note that it requires the jamf binary to be installed.
"""

import os, subprocess, sys
from xml.dom import minidom

fsroot = '/'
src = os.path.join(fsroot, '/private/tmp/fillUsers')

userList = subprocess.Popen( [os.path.join(fsroot, '/usr/sbin/jamf'), 'listUsers'], stdout = subprocess.PIPE,
).communicate()[0]
uids = minidom.parseString(userList).getElementsByTagName(u'id')
names = minidom.parseString(userList).getElementsByTagName(u'name')
homes = minidom.parseString(userList).getElementsByTagName(u'home')
uids = [node.firstChild.nodeValue for node in uids]
names = [node.firstChild.nodeValue for node in names]
homes = [node.firstChild.nodeValue for node in homes]
users = list(zip(uids, names, homes))

# kinda nice that uid 0 happens to have the right gid (0) for this
for lang in os.listdir(os.path.join(fsroot, '/System/Library/User Template')): users.append(( '0', 'template (%s)' % (lang, ), os.path.join('/System/Library/User Template/', lang), ))

for uid, name, home in users: gid = subprocess.Popen( ['/usr/bin/id', '-g', uid], stdout = subprocess.PIPE ).communicate()[0] # if fsroot != '/', id won't find the right user print "Filling %s for user %s" % (home, name) subprocess.check_call(['/usr/sbin/chown', '-R', uid + ':' + gid, src]) subprocess.check_call(['/usr/bin/ditto', src, os.path.join(fsroot, home)])
print "Cleaning up %s" % (src, )
subprocess.check_call(['/bin/rm', '-rf', src])

12 REPLIES 12

Not applicable

It needs the GIDs too, to properly fix the ownership. That's what it uses id for.

If you want to learn Python, there are two books I can recommend, depending on your level of experience with programming in general.

http://diveintopython.org/
This book taught me, and is great if you know several other programming languages already. It goes right into the syntax and built-in modules, with the assumption that you have mastered the concepts common to all languages.

http://openbookproject.net//thinkCSpy/
This one is designed for high school students new to programming. You may find it a bit patronizing, but it looks like a good resource if you don't know a language already. I did notice that it's lacking in some of the more advanced aspects of Python (list comprehension comes to mind, which I used in this script to build the user lists). It may be useful to read the other one as well. But do make sure you have the basics first. Also, beware of typos. They are plentiful here (I plan to get in touch with the author about that).

fsjjeff
Contributor II

Hey Benjamin,

I'm looking at your script to Fill users and template folders with lots of interest, as it would totally scratch an itch that I've been struggling with. I don't really have any experience in Python, though, and because of a slightly different usage scenario, I don't think this will quite work for me without modifications. Wondering if anyone can point me in the right direction...

Basically, when I'm creating pkg installers, I often have 2 goals in mind:

(1) Casper deployment for existing image maintenance (2) InstaDMG deployment for building deployable images.

Unfortunately, this has meant that I often have to create 2 separate packages, often because of the User Template or fill user portions. It gets kind of confusing and I'd love to find a method that would let me be lazy and only create 1 installer package.

This script looks awesome for Casper Deployment, but for for 2 reasons I don't think would work for InstaDMG... I suspect that both would be fairly simple to tweak if I knew Python (it's definitely on my list)...

The changes that I love to get some tips or help with:

• During an install from InstaDMG, the script will not have access to the jamf binary, but since you don't actually need to fill existing users in this case, I think it would be safe to have some kind of test that would just skip the "get list of users then copy files to their home folders" step if it's running within an InstaDMG workflow.

• It also looks like the script hard codes the root of the HD, but in an InstaDMG situation you'd need to instead use the target destination - in my bash scripts for InstaDMG I usually reference the $2 (target location) and $3 (target volume) variables.

Anyone feel up to tweaking this or pointing me in the right direction?

Jeff

bentoms
Release Candidate Programs Tester

We have a generic standard admin account in /users for this.

We build all our packages using it & it's part of the base os or can be created from a quickadd.

Works very well.

Regards,

Ben Toms

Not applicable

This version should do the trick:

#!/usr/bin/python

"""
This script clones anything it finds in /private/tmp/fillUsers to all existing
non-hidden users, as well as all user template localizations. It is meant to be
used as a postinstall script for packages that need this functionality,
mimicking the functionality available in JAMF's Casper Suite for DMG packages.
Note that the "fill existing users" functionality requires the jamf binary to be
installed. That feature is skipped if the jamf binary is not found.

The script looks for $3 (InstaDMG uses this) to determine the target fsroot.
If not found, it defaults to '/'. Beware using "fill existing users" when
fsroot is not '/'; it will only see the currently available users database,
which probably won't match the target's database.
"""

import os, subprocess, sys
from xml.dom import minidom

fsroot = (sys.argv[3] if len(sys.argv) > 3 else '/')
src = os.path.join(fsroot, '/private/tmp/fillUsers')

if os.path.exists('/usr/sbin/jamf'):
userList = subprocess.Popen(
[os.path.join(fsroot, '/usr/sbin/jamf'), 'listUsers'],
stdout = subprocess.PIPE,
).communicate()[0]
uids = minidom.parseString(userList).getElementsByTagName(u'id')
names = minidom.parseString(userList).getElementsByTagName(u'name')
homes = minidom.parseString(userList).getElementsByTagName(u'home')
uids = [node.firstChild.nodeValue for node in uids]
names = [node.firstChild.nodeValue for node in names]
homes = [node.firstChild.nodeValue for node in homes]
users = list(zip(uids, names, homes))
else:
users = []

# kinda nice that uid 0 happens to have the right gid (0) for this
for lang in os.listdir(os.path.join(fsroot, '/System/Library/User Template')):
users.append((
'0',
'template (%s)' % (lang, ),
os.path.join('/System/Library/User Template/', lang),
))

for uid, name, home in users:
# Warning: if fsroot != '/', id won't necessarily find the right user, but
# for uid 0 (root) it doesn't matter; all Mac OS X systems should have
# the same gid for root.
gid = subprocess.Popen(
['/usr/bin/id', '-g', uid],
stdout = subprocess.PIPE
).communicate()[0]
print "Filling %s for user %s" % (home, name)
subprocess.check_call(['/usr/sbin/chown', '-R', uid + ':' + gid, src])
subprocess.check_call(['/usr/bin/ditto', src, os.path.join(fsroot, home)])
print "Cleaning up %s" % (src, )
subprocess.check_call(['/bin/rm', '-rf', src])

fsjjeff
Contributor II

I'm not quite sure I'm following you - how are you using a generic admin account to auto-populate files into user accounts and the User Template folders?

jeff

fsjjeff
Contributor II

Has anyone told you lately that you're awesome! If not, please allow me. I *really* need to get on to the Python or Ruby learning thing, that looks so much more elegant than what it would have taken to do something similar in Bash - in particular the parsing the Users list thing.

Jeff

tlarkin
Honored Contributor

Before my Lynda.com account expired I grabbed all the python/ruby training videos I could. Still need to watch them though. If you can get your work to spring for an account, it isn't all that expensive for a single user license and you get access to tons and tons of training videos. I think that a multiple license account for an enterprise is also dirt cheap. Though I know that in this current financial crisis the first thing to go for us was our training budget.

Not applicable

Thanks! Actually, people tell me that all the time. :P
I wish I could take all the credit, but Python makes a lot of this very easy. You should see the script I wrote that converts VBS to Python...

Hmm, I hadn't noticed that the email mangles the tabs. You'll need to add the tabs back in, or else Python will give you a SyntaxError.

Not applicable

I still say your best resource for learning Python is either Dive Into Python<http://diveintopython.org/> (for seasoned developers) or How To Think Like a Computer Scientist (Python version)<http://greenteapress.com/thinkpython/thinkCSpy/> (for those new to development). Can't beat free!

On Mar 2, 2011, at 10:16 AM, Thomas Larkin wrote:

Before my Lynda.com<http://Lynda.com> account expired I grabbed all the python/ruby training videos I could. Still need to watch them though. If you can get your work to spring for an account, it isn't all that expensive for a single user license and you get access to tons and tons of training videos. I think that a multiple license account for an enterprise is also dirt cheap. Though I know that in this current financial crisis the first thing to go for us was our training budget.

tlarkin
Honored Contributor

Yeah my lynda.com account was free, paid for by employer, but then budget crunch time and no more training budget....thanks for those links I am tagging them right now!

fsjjeff
Contributor II

Thanks again for this script, had to experiment a bit with the missing tabs. I also think I found one small glitch, but a quick Google search found a solution...

When you grab the gid of the user, it returns a response with /n at the end, which chokes the the chown command that comes immediately after. To fix, I added a line right under the gid command:

gid = gid.rstrip(' ')

So far working awesome in initial testing, just building an InstaDMG image now, should be able to verify that it works there as well.

Might make you very popular if you post this on the InstaDMG forums - if you don't have time, and don't mind, I could do that as well, giving you attribution of course.

Cheers

Jeff

Not applicable

I have time (for now), but I don't really feel like joining yet another forum...

Ah, I keep forgetting about that pesky ' '. The way I would usually get rid of it is by adding .strip(), like so:

gid = subprocess.Popen( ['/usr/bin/id', '-g', uid], stdout = subprocess.PIPE,
).communicate()[0].strip()

But the way you found works too.