Using launchd to Mount an AFP Share Point Upon Startup

Scenario

Multiple Mac OS X web servers running an application which allows uploading of media assets (photos, zip, pdf, etc.). Uploading will of course store the file on just the single server it was uploaded to, but the file needs to be available to all servers. The cost of a SAN is simply not justifiable.

We could consider at least these two options, and this article discusses the latter:

  • propogate the file to all servers after being uploaded probably using rsync
  • use a single directory on a media server shared via AFP by all servers

Instead of using a SAN to make all files available to the web application on the backend as part of a shared file system, we’ll allow each web server to do local processing of incoming files, then relocate the final files to an AFP share point folder which in turn is served by an HTTP server for content delivery. This last piece is critical to allowing AFP to work for us, but also provides architectural benefits to your web applications.

I’ve used the shared AFP volume technique to upload images and attachments to online articles, to upload photos to be processed for property listings, and other similar things. The web app on each server provides all the post upload processing, but the final storage is moved to a shared location.

Why/How Use a Media Server?

We’re going to create a setup where each web server’s application code can read and write to an AFP share point. That helps each web server have access to the media files which is useful for tasks such as creating thumbnails of uploaded images, renaming files, etc. However, what we cannot do, is use that AFP mount to then serve the files to the HTTP server as though they were a part of the local file system. The operating system’s management of file permissions simply won’t allow Apache (or whatever) access to the files via the AFP mount point. The web code can have access because it will be running as the same user we mount the AFP share point as. However, Apache will not have access to those files (on any server using the AFP mount point). I’ve tried all kinds of approaches, and nothing has worked. If you’ve solved it, let me know! Apache on the server where the master files reside can have access to the master files, so we run a separate web site to deliver those files.

Your app will have to serve all media assets from a dedicated site with fully-qualified URLs from a domain something like media.your_site.com. Instead of using links like <a href=“/assets/ABC123/nifty.pdf”>Get nifty.pdf</a>, you should first create a global variable like $media_server = “media.your_site.com” or whatever the right syntax for the language is. Then, your links would be prefixed with that variable. Again, how exactly you do that depends on the language you’re using. In Ruby it could be <a href=“#{$media_server}/ABC123/nifty.pdf”>Get nifty.pdf</a>.

If you look at the two diagrams, you’ll find a basic setup where the AFP share point (the master source of files), resides on one of two otherwise equal web servers. In this scenario, some configuration details for each server are different because one of the servers has the actual local directory of files, and one has to go through a mounted share point. That is the server that can be setup to be the media server.

In the advanced setup, I have added a separate media server where the AFP shared directory of master files resides. In this setup, all web servers are configured identically even if they have different roles.

In all of these variations, we want the AFP share to mount on server startup, and we want to ensure that it stays mounted. To accomplish both of these goals, we’re going to use Mac OS X’s launchd.

OK, let’s get started with the AFP setup.

Create the Master Share Folder

In Workgroup Manager on the server with the folder to be shared (such as app01 or media01), create a user such as SiteAssets (short name site_assets) or whatever make sense for your application. The reason for creating a new user is that we’ll have to hard code the user name and password in a shell script, and we’d rather that be a limited, non-admin user in case the secrecy of the login credentials are compromised.

Next, create a new folder. Our example will be named /siteAssets. That’s the entry point to all the shared files. If you already have a folder full of files you want to use, read the rest of this section first, then decide your course of action.

Set the owner of /siteAssets to site_assets using either Finder Info or a Terminal chown command.

sudo chown site_assets /PATH_TO/siteAssets/

TIP: you can drag the folder onto the Terminal window, and the path will be entered for you.

Set the group of /siteAssets to staff using either Finder Info or Terminal.

sudo chgrp staff /PATH_TO/siteAssets/

Set the permissions of /siteAssets to allow owner and group to read/write.

sudo chmod u=rw,g=rw /PATH_TO/siteAssets/

If your share folder already exists, and has lots of files in it, you’ll need to add -R to each of the commands above like sudo chown -R site... etc. Don’t do that if you know the enclosed files have specific permissions requirements which are already set. You may have some permissions juggling to do here.

Finally, in Server Admin, activate /siteAssets as a share point. I’ll assume you know how to do that, can refer to Apple documentation, or another blog. Be sure to go through all protocols, and allow only AFP to share this folder unless you know for sure you need another protocol.

Write a shell script to mount the share point

Now, let’s move to one of the servers that has to mount that shared folder (apps02 in my diagrams).

Below is the minimum shell script needed to mount an AFP share point. Pretty much every other article you’ll find on this, shows something like the first line and last line only. That is, mkdir followed by mount_afp (or mount -t afp). That works great for mounting an AFP volume but not a share point. At least, for me it never has. The secret that worked for me was setting permissions on the mount directory—those two steps in the middle.

In the script below, the ADMIN_PSWD is the password for an admin user named ADMIN_USER. I suppose any user is possible, but I’ve always used the main admin user I log in as so that it’s easier to work on the share files if needed. Maybe that’s poor Unix joo-joo. You can adjust accordingly.

#!/bin/bash
echo "ADMIN_PSWD" | sudo -S -u ADMIN_USER mkdir /Volumes/siteAssets/
echo "ADMIN_PSWD" | sudo -S -u ADMIN_USER chown ADMIN_USER /Volumes/siteAssets/
echo "ADMIN_PSWD" | sudo -S -u ADMIN_USER chmod +rwx /Volumes/siteAssets/
echo "ADMIN_PSWD" | sudo -S -u ADMIN_USER mount_afp afp://site_assets:SITE_ASSETS_PSWD@your_domain.tld/siteAssets/ /Volumes/siteAssets

If you’ve been researching, you’ve probably seen the mount_afp comand a dozen times, but just in case, a more readable rendition is:

sudo mount_afp afp://user:pswd@host/siteAssets/ /Volumes/siteAssets

Where, user is the user name of the owner of the share point, pswd is that user’s password, and host is the domain or IP of the server hosting the AFP share point.

Put the bash script in a filed named mount_site_assets.sh inside a folder perhaps named /Scripts at the root of your startup drive or together with other utilities for your application. Make sure the executable flag is set using Terminal if necessary.

sudo chmod +x /PATH_TO/mount_site_assets.sh
Note: to unmount use these two commands:
sudo umount /Volumes/siteAssets
sudo rmdir /Volumes/siteAssets

Write a LaunchDaemon to run the shell script at startup

Still on a server such as apps02, let’s now get launchd involved. This is the minimum plist you’ll need. It will run the script once at startup.

<?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>tld.your_domain.mount-assets</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Scripts/mount_site_assets.sh</string>
    </array>
    <key>UserName</key>
    <string>root</string>
    <key>UserGroup</key>
    <string>wheel</string>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

You’ll want to change Label value to reflect your own domain name. And, you’ll want to change ProgramArguments to point to the actual path of your script file.

I’ve found the best way to edit/install the plist is to have a copy in the same place as the shell script file. Edit it there, and when done, use the following Terminal command to put it where it needs to go with the correct permissions:

sudo cp /Scripts/mount_site_assets.sh /Library/LaunchDaemons/

Remember, that you can drag a file or folder into Terminal as a shortcut way of entering that path.

Try It

At this point, you should be able to install the plist, reboot, and see your share point mounted automatically. Now, the real purpose of doing it this way is to have the share point mounted even if you don’t log into the computer. So, in my web server scenario, the servers start up, but they don’t automatically login a user. If you have the same working scenario, once the server starts up, you can ssh into it from another computer, then run

ls -l /volumes

and you should see the share point in that list. You should be able to further list the contents of the share point. If the share point is empty (and isn’t supposed to be), the mkdir command worked, but mount_afp command did not. Go back and check all the details.

Making the scripts more robust

What happens if the share point becomes unavailable? If it becomes unmounted, we have to restart the web servers to get it remounted. Not a great plan. So, let’s make the two scripts smarter about handling that. While we’re at it, let’s make the shell script easier to reuse by adding in some variables.

#!/bin/bash

local_admin_user="xxxxx"            # an admin user name on the computer running this script
local_admin_pswd="xxxxx"            # the password for the above user
mount_point="/Volumes/siteAssets"   # the name the mounted share point will be known as on this computer

host_name="xxxxx"                   # the domain name or IP of the system hosting the share point
share_point="SiteAssets"            # the name of the remote share point
share_owner="siteassets"            # the user name of the owner of the remote share point
share_pswd="abc123"                 # the password for the above user

# IMPORTANT:
# If the share_pswd contains any symbol characters, they need to be escaped.
# So, instead of: share_pswd="abc&123#"
# Do this: share_pswd="abc\&123\#"

#--------
# is the share already mounted?
# the result will be 0 if the share point is not mounted
share_is_mounted='mount | grep -c SiteAssets'

if [ $share_is_mounted == 0 ]; then

    # let’s clean up just in case the mount was dropped 
    echo $local_admin_pswd | sudo -S -u $local_admin_user umount $mount_point
    echo $local_admin_pswd | sudo -S -u $local_admin_user rmdir $mount_point
    
    # basic steps to mount the share point
    echo $local_admin_pswd | sudo -S -u $local_admin_user mkdir $mount_point
    echo $local_admin_pswd | sudo -S -u $local_admin_user chown $local_admin_user $mount_point
    echo $local_admin_pswd | sudo -S -u $local_admin_user chmod +rwx $mount_point
    echo $local_admin_pswd | sudo -S -u $local_admin_user mount_afp afp://$share_owner:$share_pswd@$host_name/$share_point/ $mount_point
fi

What can be done to make the launchd plist better? First, launchd should run our script every minute so to check up on our share point. Adding StartInterval does that. It accepts an integer number of seconds. So, in this case our shell script will be launched every 90 seconds. Next, we may as well log any errors that our script might generate. By adding StandardErrorPath, we’ll have a log file to check out. I’ve also added StandardOutPath mostly for an example of how to have a regular log. The shell script currently has no statemennts that would print to the display because we wouldn’t see them anyway. However, perhaps you want to log every time the script runs just to be sure it is running, at least during setup testing. In that case, you can add an echo statement to the shell script to print a time stamp and a short message. That will show up in the log.

To find the definitions of the keys, use man launchd.plist in Terminal.

<?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>com.araelium.mountSiteAssets</string>
    <key>StandardOutPath</key>
    <string>/var/log/araelium/mount_site_assets.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/araelium/mount_site_assets_error.log</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Scripts/automount_site_assets.sh</string>
    </array>
    <key>UserName</key>
    <string>root</string>
    <key>UserGroup</key>
    <string>wheel</string>
    <key>RunAtLoad</key>
    <true/>
    <key>StartInterval</key>
    <integer>90</integer>
</dict>
</plist>

The updated versions are now much more “production” worthy. So far, I’ve found this system to be reliable for making a shared volume available to multiple servers.

Abstracting the Location of the Shared Files

One more detail—you can’t mount an AFP share point on the same computer that it is hosted from. That means in our simple two-server setup, apps01 has to refer to the folder using the local folder path, and apps02 has to use the path starting with /Volumes/siteAssets. That’s not a good plan since the app code should be the same on both servers. Something has to make up for this difference though.

A symlink (not a Mac OS alias) is ideal for abstracting the share poing on each machine. Have your application refer to an imaginary location which can be the same on all servers. Then, one server symlinks the mounted AFP share, and the other server symlinks the local share folder. To the web application code, the path to the symlink is all that matters and is the same on all servers. Since I don’t know which web app language/framework you’ll be using, I can’t suggest exactly where to put that link. In a Rails app, it would be inside the /public folder so you’d maybe use /public/assets/.... as the pathname for doing local file manipulations such as creating thumbnails, etc.

Wrapup

All articles have missing details to help you solve your specific problem, but I hope this article was complete enough to get you going.

 
 

A basic setup with two servers where one server has the master media files and runs the media server, and the second server connects to the master files via an AFP share point. (large image of basic setup)

A more advanced setup with a dedicated server for master media files and the media server domain, and multiple app servers each connected to the master files through an AFP share point. (large image of advanced setup)