├── .gitignore ├── sample └── example.conf ├── sh ├── getpwd.sh ├── setpwd.sh └── getsetpwd.sh ├── launch └── com.irouble.macmounter.plist ├── TODO ├── uninstall.sh ├── install.sh ├── README.md └── scripts └── macmounter.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .*.sw? 3 | *~ 4 | -------------------------------------------------------------------------------- /sample/example.conf: -------------------------------------------------------------------------------- 1 | [example] 2 | PING_CMD= 3 | WAKE_CMD= 4 | WAKE_ATTEMPTS=5 5 | MOUNT_TEST_CMD= 6 | PRE_MOUNT_CMD= 7 | MOUNT_CMD= 8 | POST_MOUNT_CMD= 9 | MOUNT_SUCCESS_CMD= 10 | -------------------------------------------------------------------------------- /sh/getpwd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PWD=`security find-generic-password -wa $1` 4 | 5 | # for mount_smbfs, spaces in passwords need to be escaped as %20 6 | printf "%s" `echo ${PWD// /%20}` 7 | -------------------------------------------------------------------------------- /sh/setpwd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo -n Account: 4 | read account 5 | 6 | echo -n Service: 7 | read service 8 | 9 | echo -n Password: 10 | read -s password 11 | echo "$password" 12 | 13 | security add-generic-password -a "${account}" -s "${service}" -w "${password}" 14 | -------------------------------------------------------------------------------- /sh/getsetpwd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PASSWD=$(security find-generic-password -wa $1) 4 | if [[ -z $PASSWD ]] 5 | then 6 | echo -n Account: 7 | read account 8 | 9 | echo -n Service: 10 | read service 11 | 12 | echo -n Password: 13 | read -s password 14 | 15 | security add-generic-password -a "${account}" -s "${service}" -w "${password}" 16 | fi 17 | -------------------------------------------------------------------------------- /launch/com.irouble.macmounter.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.irouble.macmounter 7 | ProgramArguments 8 | 9 | /usr/local/bin/macmounter.py 10 | -m 11 | -v 12 | debug 13 | 14 | RunAtLoad 15 | 16 | KeepAlive 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | *1. Kill threads immediately. 2 | *2. PING_SUCCESS_CMD, PING_FAIL_CMD, MOUNT_FAIL_CMD, MOUNT_SUCCESS_CMD 3 | *3. Password management 4 | *4. startup scripting 5 | 5. Support python 3 (yeah right...) 6 | 6. refactor executeCommand() 7 | 7. test with no files in configs 8 | *8. test with just mount_cmd specified 9 | *9. test with no mount test specified 10 | *10. test with no ping specified 11 | *11. LOST_MOUNT feature 12 | *12. Add state 13 | *13. Add retest interval when mounted, vs retest interval when not mounted 14 | 14. Detect network changes to remount! 15 | 15. Write uninstall script 16 | 16. Add wiki on Github 17 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USER=$(logname) 4 | HOME=$(eval echo "~$USER") 5 | echo "USER=$USER" 6 | echo "HOME=$HOME" 7 | 8 | if [ -f "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" ]; then 9 | launchctl unload "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" 10 | fi 11 | rm -f "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" 12 | rm -f /usr/local/bin/macmounter.py 13 | rm -rf "$HOME/Library/Application Support/macmounter" 14 | 15 | echo -n "Do you want delete user configs [y|n] (default:no)? " 16 | read yesno 17 | if [ "$yesno" == "y" ]; then 18 | rm -rf "$HOME/.macmounter" 19 | else 20 | echo "Leaving configs in $HOME/.macmounter" 21 | fi 22 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USER=$(logname) 4 | HOME=$(eval echo "~$USER") 5 | echo "USER=$USER" 6 | echo "HOME=$HOME" 7 | 8 | echo "Creating $HOME/.macmounter" 9 | mkdir -p "$HOME/.macmounter" 10 | if [ ! -d "$HOME/.macmounter" ]; then 11 | echo "Error creating .macmounter folder." 12 | else 13 | chown $USER "$HOME/.macmounter" 14 | fi 15 | cp ./sample/example.conf "$HOME/.macmounter" 16 | chown $USER "$HOME/.macmounter/example.conf" 17 | 18 | echo "Creating $HOME/Library/Application Support/macmounter" 19 | mkdir -p "$HOME/Library/Application Support/macmounter" 20 | if [ ! -d "$HOME/Library/Application Support/macmounter" ]; then 21 | echo "Error creating application folder." 22 | else 23 | chown $USER "$HOME/Library/Application Support/macmounter" 24 | fi 25 | 26 | if [ ! -d /usr/local/bin/ ]; then 27 | echo "Creating /usr/local/bin ..." 28 | mkdir -p /usr/local/bin/ 29 | fi 30 | 31 | echo "Installing scripts in /usr/local/bin" 32 | cp ./scripts/macmounter.py /usr/local/bin 33 | if [ ! -f /usr/local/bin/macmounter.py ]; then 34 | echo "Error installing script." 35 | fi 36 | 37 | echo "Installing launcher in $HOME/Library/LaunchAgents" 38 | cp ./launch/com.irouble.macmounter.plist "$HOME/Library/LaunchAgents" 39 | if [ ! -f "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" ]; then 40 | echo "Error installing launcher." 41 | else 42 | sudo -u $USER launchctl list | grep macmounter 43 | if [ $? -eq 0 ]; then 44 | echo "Stopping service" 45 | sudo -u $USER launchctl unload "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" 46 | fi 47 | sleep 2 48 | echo "Starting service" 49 | sudo -u $USER launchctl load -w "$HOME/Library/LaunchAgents/com.irouble.macmounter.plist" 50 | fi 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # macmounter 2 | 3 | A light weight, scalable, python daemon that **automatically mounts** remote servers on login, and **keeps them mounted**. macmounter can be used for *any* protocol, sshfs, samba, afp, ftp, ntfs, webdav etc. 4 | 5 | In its current state it is written to run as a service on OS X (10.9+) out of the box, but it, _should_ be portable to any system that runs python 2.7.X. I have no plans to port it anywhere else at this time. 6 | 7 | This tool is completely command line driven and is **not** moron friendly. For g33ks, it is a two step install, and a one step configuration per server. The logs are very verbose and should indicate any issues. 8 | 9 | ## Install 10 | 11 | ### Step 1 - Get code 12 | ``` 13 | git clone https://github.com/roubles/macmounter.git 14 | ``` 15 | 16 | ### Step 2 - Install 17 | ``` 18 | cd macmounter 19 | sudo ./install.sh 20 | ``` 21 | 22 | ## Configure 23 | Add config files in the directory ~/.macmounter/ 24 | 25 | A simple config file looks like: 26 | ``` 27 | $ cat example.conf 28 | [example.com] 29 | MOUNT_TEST_CMD=ls -l /Users/roubles/somelocalfolder/ 30 | PING_CMD=/sbin/ping -q -c3 -o example.com 31 | PRE_MOUNT_CMD=/bin/mkdir -p /Users/roubles/somelocalfolder/ 32 | MOUNT_CMD=/usr/local/bin/sshfs roubles@example.com:/someremotefolder /Users/roubles/somelocalfolder/ -oauto_cache,reconnect,volname=example 33 | FOUND_MOUNT_CMD=/bin/echo "" | /usr/bin/mail -s "mounted example.com!" roubles@github.com 34 | 35 | [anotherexample.com] 36 | MOUNT_TEST_CMD=ls -l /Volumes/someotherfolder && /sbin/mount | grep -q someotherfolder 37 | PING_CMD=/sbin/ping -q -c3 -o anotherexample.com 38 | PRE_MOUNT_CMD=/sbin/umount -f /Volumes/someotherfolder; /bin/mkdir -p /Volumes/someotherfolder 39 | MOUNT_CMD=/sbin/mount -t smbfs "//roubles:whatmeworry@anotherexample.com/someotherremotefolder" /Volumes/someotherfolder 40 | FOUND_MOUNT_CMD=/bin/echo "" | /usr/bin/mail -s "mounted anotherexample.com!" roubles@github.com 41 | ``` 42 | 43 | But, it can be simpler. The simplest config only needs to specify MOUNT_CMD, though this may be inefficient. 44 | ``` 45 | [example.com] 46 | MOUNT_CMD=/usr/local/bin/sshfs roubles@example.com:/someremotefolder /Users/prmehta/somelocalfolder/ -oauto_cache,reconnect,volname=auto 47 | 48 | [anotherexample.com] 49 | MOUNT_CMD=/sbin/mount -t smbfs //roubles:whatmeworry@anotherexample.com/someotherremotefolder /Volumes/someotherfolder 50 | ``` 51 | 52 | ## Starting/Stopping 53 | 54 | The macmounter service should start when it is installed. 55 | 56 | The macmounter service starts everytime you login, and dies everytime you log off. 57 | 58 | You can manually startup macmounter using the commandline: 59 | ``` 60 | launchctl load -w ~/Library/LaunchAgents/com.irouble.macmounter.plist 61 | ``` 62 | or, logout and log back in. 63 | 64 | ## Quick reload configs 65 | There may be instances where you don't want to wait for macmounters timers to kick in to perform mounts. For this,you can force it to re-attempt all mounts instantly by running: 66 | ``` 67 | $ macmounter.py --reload 68 | ``` 69 | 70 | ## Logs 71 | 72 | Detailed logs can be found here: ~/Library/Application Support/macmounter/macmounter.log 73 | 74 | ## Troubleshooting 75 | 76 | Tail ~/Library/Application Support/macmounter/macmounter.log, it is very informative. 77 | 78 | I find these bash aliases handy: 79 | ``` 80 | alias tailmmlogs='tail -f ~/Library/Application\ Support/macmounter/macmounter.log' 81 | alias vimmlogs='vi ~/Library/Application\ Support/macmounter/macmounter.log' 82 | ``` 83 | 84 | ## Basic example 85 | 86 | A basic configuration example that should get you started can be found [here](https://github.com/roubles/macmounter/wiki/basic-example). 87 | 88 | ## More examples 89 | 90 | ### Testing mounts before remounting 91 | It is prudent to test if the mount is active and functioning before blindly remounting. [These examples](https://github.com/roubles/macmounter/wiki/testing-mounts) show the various options for testing mounts. 92 | 93 | ### Multiple mounts from the same server 94 | This is pretty straight forward, just add a section for each mount. [These examples] (https://github.com/roubles/macmounter/wiki/Mounting-multiple-folders-from-the-same-server) show examples. 95 | 96 | ### Waking up servers before mounting 97 | Sometimes NAS boxes go to sleep when idle. [These examples](https://github.com/roubles/macmounter/wiki/wakeup-server-before-mounting) show the various options for waking up remote drives on the LAN. 98 | 99 | ### Unmounting before mounting 100 | If your mount test fails, it is very likely that the mount is in a weird state. It is recommended that you force an unmount before trying to remount. [These examples](https://github.com/roubles/macmounter/wiki/unmount-before-mount) show the various options for unmounting before remounting. 101 | 102 | ### Success/Failure commands 103 | Sometimes it is desirable to run commands on success or failure. One good reason is to notify someone of the success or failure. [These examples](https://github.com/roubles/macmounter/wiki/status-notification-commands) show the various options of running commands on success or failure. 104 | 105 | ### Custom retry timers 106 | macmounter by default uses a five minute timer to retry for every case. However, it is written to be very configurable, and you can fine tune the retry time for pretty much every state. [These examples](https://github.com/roubles/macmounter/wiki/configure-polling-intervals) show the various options for fine tuning the retry timers per state. 107 | 108 | ### Miscellaneous examples 109 | [This](https://github.com/roubles/macmounter/wiki/Example-Configs) is a full list of example configs. 110 | 111 | ## Password management 112 | 113 | It is not recommended to store your password in cleartext in the config files. macmounter provides simple shell scripts to use OSX's keychain to store passwords. More information is [here](https://github.com/roubles/macmounter/wiki/password-management). 114 | 115 | ## Architecture/Wiki 116 | 117 | More detailed documentation can be found on the wiki [here](https://github.com/roubles/macmounter/wiki). 118 | 119 | ## Uninstall 120 | ``` 121 | cd macmounter 122 | sudo ./uninstall.sh 123 | ``` 124 | -------------------------------------------------------------------------------- /scripts/macmounter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Author: Rouble Matta (@roubles) 3 | 4 | import os 5 | import sys 6 | import ConfigParser 7 | import argparse 8 | import logging 9 | import logging.handlers 10 | import itertools 11 | import subprocess 12 | from subprocess import CalledProcessError 13 | import time 14 | import threading 15 | import signal 16 | import traceback 17 | import shlex 18 | 19 | def ctrlc_handler (signal, frame): 20 | global running 21 | logger.info('You pressed Ctrl+C!') 22 | running = False 23 | killMounters() 24 | 25 | def hup_handler (signal, frame): 26 | logger.info('Caught signal HUP. macmounter restarted!') 27 | launchMounters(updateConfig()) 28 | for mounter in mounterMap.values(): 29 | mounter.reload = True # This triggers the mounter to restart 30 | 31 | # Global variables are evil. Except these. 32 | logger = logging.getLogger("macmounter") 33 | 34 | # Constants 35 | DEFAULT_RECHECK_INTERVAL_SECONDS = 300 36 | DEFAULT_WAKE_ATTEMPTS = 2 37 | DEFAULT_MOUNT_TEST_CMD = None 38 | DEFAULT_PING_CMD = None 39 | DEFAULT_PING_SUCCESS_CMD = None 40 | DEFAULT_PING_FAILURE_CMD = None 41 | DEFAULT_WAKE_CMD = None 42 | DEFAULT_PRE_MOUNT_CMD = None 43 | DEFAULT_MOUNT_CMD = None 44 | DEFAULT_POST_MOUNT_CMD = None 45 | DEFAULT_LOST_MOUNT_CMD = None 46 | DEFAULT_FOUND_MOUNT_CMD = None 47 | DEFAULT_MOUNT_SUCCESS_CMD = None 48 | DEFAULT_MOUNT_FAILURE_CMD = None 49 | homeConfigFolder = os.path.join(os.path.expanduser("~"), ".macmounter") 50 | homeConfigFile = os.path.join(os.path.expanduser("~"), ".macmounter.conf") 51 | 52 | # Actual global variables 53 | mounterMap = dict() 54 | running = True 55 | 56 | # global configs 57 | conffile = None 58 | confdir = None 59 | confFileMtime = None 60 | confDirMtime = None 61 | dotMacMounterFileConfMtime = None 62 | dotMacMounterDirConfMtime = None 63 | 64 | def setupParser (): 65 | #Setup argparse 66 | loglevel = ['critical', 'error', 'warning', 'info', 'debug'] 67 | parser = argparse.ArgumentParser(description='Mac Mounter. Keeps external drives mounted.') 68 | parser.add_argument('-c', '--conffile', help='Full path to conf file') 69 | parser.add_argument('-d', '--confdir', help='Full path to conf file directory') 70 | parser.add_argument('-l', '--logfile', help="Logging file", action="store", default=None) 71 | parser.add_argument("-v", "--loglevel", help="Set log level", choices=loglevel, default='info') 72 | parser.add_argument("-m", "--macdefaults", help="Use mac defaults for logs", action="store_true", default=False) 73 | parser.add_argument("-o", "--nostdout", help="Do not display logs to stdout", action="store_true", default=False) 74 | parser.add_argument("-r", "--reload", help="Reload currently running daemon (or mount all configured mounts now!)", action="store_true", default=False) 75 | return parser 76 | 77 | def setupLogger (logger, loglevel, logfile, stdout = False, rollover = False, rotate = False, backupCount = 3, maxBytes = 2097152): 78 | logger.setLevel(logging.DEBUG) 79 | 80 | #create a steam handler 81 | stdouthandler = logging.StreamHandler(sys.stdout) 82 | if stdout: 83 | stdouthandler.setLevel(logging.getLevelName(loglevel.upper())) 84 | else: 85 | # We want to output errors to stdout no matter what 86 | stdouthandler.setLevel(logging.WARNING) 87 | 88 | # create a logging format for stdout 89 | # stdoutformatter = logging.Formatter('%(message)s') 90 | stdoutformatter = logging.Formatter('%(asctime)s - %(levelname)s - %(thread)d - %(message)s') 91 | stdouthandler.setFormatter(stdoutformatter) 92 | 93 | # add the stdout handler to the logger 94 | logger.addHandler(stdouthandler) 95 | 96 | if logfile is not None: 97 | # create a file handler 98 | # We rotate the log file when it hits 2MB, and we save at most 3 log 99 | # files, so 6MB of total log data. 100 | if rotate: 101 | filehandler = logging.handlers.RotatingFileHandler(logfile, maxBytes=maxBytes, backupCount=backupCount) 102 | # On startup rollover the last file 103 | if rollover: 104 | if os.path.isfile(logfile): 105 | filehandler.doRollover() 106 | else: 107 | filehandler = logging.FileHandler(logfile) 108 | 109 | filehandler.setLevel(logging.getLevelName(loglevel.upper())) 110 | # create a logging format for the log file 111 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(thread)d - %(message)s') 112 | filehandler.setFormatter(formatter) 113 | 114 | # add the file handler to the logger 115 | logger.addHandler(filehandler) 116 | 117 | return logger 118 | 119 | def getConfFilesFromFolder (foldername, newerThanTime=0): 120 | logger.info("Looking for config files in: " + foldername) 121 | configFiles = [] 122 | 123 | if (os.path.isdir(foldername)): 124 | for dirname,subdirs,files in os.walk(foldername): 125 | for fname in files: 126 | if fname.endswith(".conf"): 127 | full_path = os.path.join(dirname, fname) 128 | mtime = os.path.getmtime(full_path) 129 | if mtime > newerThanTime: 130 | configFiles.append(full_path) 131 | 132 | return configFiles 133 | 134 | def operateOnSection(section, filename): 135 | mounterthread = mounter(section, filename) 136 | logger.info("Created thread: " + str(mounterthread)) 137 | mounterthread.start() 138 | return mounterthread 139 | 140 | def operateOnFile(filename): 141 | config = ConfigParser.ConfigParser() 142 | config.read(filename) 143 | threads = [] 144 | for section in config.sections(): 145 | logger.info("Found Section: " + section + ", filename: " + str(filename)) 146 | if section + filename not in mounterMap: 147 | threads.append(operateOnSection(section, filename)) 148 | else: 149 | logger.info("Not recreating thread for section: " + section + " in file: " + filename) 150 | return threads 151 | 152 | def get_absolute_path (path): 153 | if path is None: 154 | return path 155 | return os.path.abspath(os.path.expandvars(os.path.expanduser(path))) 156 | 157 | def getConfFileMTime (): 158 | if os.path.isfile(conffile): 159 | return getFileMTime(conffile) 160 | else: 161 | return None 162 | 163 | def getFileMTime (filename): 164 | return os.path.getmtime(filename) 165 | 166 | def getDirMTime (directory): 167 | max_mtime = 0 168 | dirmtime = os.path.getmtime(directory) 169 | for dirname,subdirs,files in os.walk(directory): 170 | for fname in files: 171 | full_path = os.path.join(dirname, fname) 172 | mtime = os.path.getmtime(full_path) 173 | if mtime > max_mtime: 174 | max_mtime = mtime 175 | max_dir = dirname 176 | max_file = fname 177 | if (dirmtime > max_mtime): 178 | return dirmtime 179 | else: 180 | return max_mtime 181 | 182 | def getConfDirMTime (): 183 | if os.path.isdir(confdir): 184 | return getDirMTime(confdir) 185 | else: 186 | return None 187 | 188 | def getDotMacMounterFileConfMtime (): 189 | if os.path.isfile(homeConfigFile): 190 | return getFileMTime(homeConfigFile) 191 | else: 192 | return None 193 | 194 | def getDotMacMounterDirConfMtime (): 195 | if os.path.isdir(homeConfigFolder): 196 | return getDirMTime(homeConfigFolder) 197 | else: 198 | return None 199 | 200 | def updateConfig (): 201 | global dotMacMounterDirConfMtime 202 | global dotMacMounterFileConfMtime 203 | global confFileMtime 204 | global confDirMtime 205 | 206 | configFiles = [] 207 | if conffile or confdir: 208 | if conffile: 209 | logger.info("Got conf file: " + conffile) 210 | configFiles.append(conffile) 211 | confFileMtime = getConfFileMTime() 212 | logger.info("Config file modification time: " + time.ctime(confFileMtime)) 213 | if confdir: 214 | logger.info("Got conf dir: " + confdir) 215 | configFiles.extend(getConfFilesFromFolder(confdir)) 216 | confDirMtime = getConfDirMTime() 217 | logger.info("Config directory modification time: " + time.ctime(confDirMtime)) 218 | else: 219 | if os.path.isfile(homeConfigFile): 220 | logger.info("Looking for configs in " + homeConfigFile) 221 | configFiles.append(homeConfigFile) 222 | dotMacMounterFileConfMtime = getDotMacMounterFileConfMtime() 223 | logger.info("~/.macmounter.conf modification time: " + time.ctime(dotMacMounterFileConfMtime)) 224 | 225 | if os.path.isdir(homeConfigFolder): 226 | logger.info("Looking for configs in " + homeConfigFolder) 227 | configFiles.extend(getConfFilesFromFolder(homeConfigFolder)) 228 | dotMacMounterDirConfMtime = getDotMacMounterDirConfMtime() 229 | logger.info("~/.macmounter/ modification time: " + time.ctime(dotMacMounterDirConfMtime)) 230 | 231 | 232 | return configFiles 233 | 234 | def killMounters (): 235 | for mounter in mounterMap.values(): 236 | mounter.stop() 237 | 238 | def monitorConfigs (): 239 | global dotMacMounterDirConfMtime 240 | global dotMacMounterFileConfMtime 241 | global confFileMtime 242 | global confDirMtime 243 | 244 | logger.info("Monitoring configs.") 245 | while running: 246 | configFiles = [] 247 | if conffile or confdir: 248 | if conffile: 249 | newConfFileMtime = getConfFileMTime() 250 | if newConfFileMtime > confFileMtime: 251 | logger.info("config file changed!") 252 | logger.info("new time: " + time.ctime(newConfFileMtime)) 253 | logger.info("old time: " + time.ctime(confFileMtime)) 254 | configFiles.append(conffile) 255 | confFileMtime = newConfFileMtime 256 | if confdir: 257 | newConfDirMtime = getConfDirMTime() 258 | if newConfDirMtime > confDirMtime: 259 | logger.info("config directory changed!") 260 | logger.info("new time: " + time.ctime(newConfDirMtime)) 261 | logger.info("old time: " + time.ctime(confDirMtime)) 262 | configFiles.extend(getConfFilesFromFolder(confdir, confDirMtime)) 263 | confDirMtime = newConfDirMtime 264 | else: 265 | if os.path.isfile(homeConfigFile): 266 | newDotMacMounterFileConfMtime = getDotMacMounterFileConfMtime() 267 | if newDotMacMounterFileConfMtime > dotMacMounterFileConfMtime: 268 | logger.info("~/.macmounter.conf file changed!") 269 | logger.info("new time: " + time.ctime(newDotMacMounterFileConfMtime)) 270 | logger.info("old time: " + time.ctime(dotMacMounterFileConfMtime)) 271 | configFiles.append(homeConfigFile) 272 | dotMacMounterFileConfMtime = newDotMacMounterFileConfMtime 273 | if os.path.isdir(homeConfigFolder): 274 | newDotMacMounterDirConfMtime = getDotMacMounterDirConfMtime() 275 | if newDotMacMounterDirConfMtime > dotMacMounterDirConfMtime: 276 | logger.info("~/.macmounter directory changed!") 277 | logger.info("new time: " + time.ctime(newDotMacMounterDirConfMtime)) 278 | logger.info("old time: " + time.ctime(dotMacMounterDirConfMtime)) 279 | configFiles.extend(getConfFilesFromFolder(homeConfigFolder, dotMacMounterDirConfMtime)) 280 | dotMacMounterDirConfMtime = newDotMacMounterDirConfMtime 281 | if configFiles: 282 | logger.info("Configs have changed!") 283 | launchMounters(configFiles) 284 | #logger.info("Sleeping...") 285 | time.sleep(float(1)) 286 | #logger.info("Done sleeping...") 287 | logger.info("Tango down. Config monitor thread dead.") 288 | 289 | def waitOnMounters (): 290 | logger.info("Waiting on mounters: " + str(mounterMap.keys())) 291 | mounterMapCount = len(mounterMap) 292 | if mounterMapCount > 0: 293 | logger.info("Waiting on " + str(mounterMapCount) + " mounters!") 294 | time.sleep(float(1)) 295 | logger.info("Mounters eliminated.") 296 | logger.info("Dave, this conversation can serve no purpose anymore. Goodbye.") 297 | 298 | def launchMounters (configFiles): 299 | logger.info("Launching mounters... for configFiles: " + str(configFiles)) 300 | for filename in configFiles: 301 | logger.info("Found file: " + str(filename)) 302 | operateOnFile(filename) 303 | 304 | def getConfig(config, section, option, default=None, ctype=str, logPrefix=""): 305 | ret = None 306 | try: 307 | ret = config.get(section, option) 308 | except: 309 | pass 310 | if isBlank(ret): 311 | ret = default 312 | logger.info(logPrefix + "For config: " + option + "=>" + str(ret)) 313 | if ret: 314 | return ctype(ret) 315 | else: 316 | return None 317 | 318 | # int main(int argc, char *argv[]); 319 | def crux (): 320 | global conffile 321 | global confdir 322 | 323 | parser = setupParser() 324 | args = parser.parse_args() 325 | 326 | conffile = get_absolute_path(args.conffile) 327 | if conffile and not os.path.isfile(conffile): 328 | logger.error(conffile + " is not a valid file. Ignoring") 329 | conffile = None 330 | 331 | confdir = get_absolute_path(args.confdir) 332 | if confdir and not os.path.isdir(confdir): 333 | logger.error(confdir + " is not a valid dir. Ignoring") 334 | confdir = None 335 | 336 | # Apply mac defaults 337 | if args.macdefaults and not args.logfile: 338 | args.logfile = os.path.join(os.path.expanduser("~"), "Library/Application Support/macmounter/macmounter.log") 339 | 340 | args.logfile = get_absolute_path(args.logfile) 341 | setupLogger (logger, args.loglevel, args.logfile, not args.nostdout, False, True, 10) 342 | logger.info("===> Starting macmounter on " + time.strftime("%Y-%m-%dT%H.%M.%S") + "with pid " + str(os.getpid()) + "<===") 343 | 344 | if args.reload: 345 | # BSD Specific? 346 | cmd = "launchctl list | grep com.irouble.macmounter | cut -f1" 347 | pid = executeCommand(cmd, "", True) 348 | if isNotBlank(pid): 349 | logger.info("Restarting PID="+pid) 350 | os.kill(int(pid), signal.SIGHUP) 351 | return 352 | 353 | launchMounters(updateConfig()) 354 | monitorConfigs() 355 | waitOnMounters() 356 | 357 | def executeCommand(cmd, logPrefix="", returnstdout=False): 358 | rc = None 359 | #args = shlex.split(cmd) 360 | args = cmd 361 | logger.info(logPrefix + "Running cmd: " + str(args)) 362 | try: 363 | child = subprocess.Popen(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True) 364 | streamdata = child.communicate()[0] 365 | child.wait() 366 | rc = child.returncode 367 | #print "SD: " + streamdata 368 | #print "here RC" 369 | except CalledProcessError as e: 370 | #print "here2 RC" 371 | rc = e.returncode 372 | except OSError as ose: 373 | #print "here3 RC" 374 | rc = ose.errno 375 | except: 376 | #print "here4 RC" 377 | print traceback.format_exc() 378 | print sys.exc_info()[0] 379 | finally: 380 | #print "here5 RC" 381 | logger.info(logPrefix + "RC=" + str(rc)) 382 | if returnstdout: 383 | return streamdata 384 | else: 385 | return rc 386 | 387 | def isBlank (myString): 388 | if myString and myString.strip(): 389 | #myString is not None AND myString is not empty or blank 390 | return False 391 | #myString is None OR myString is empty or blank 392 | return True 393 | 394 | def isNotBlank (myString): 395 | if myString and myString.strip(): 396 | #myString is not None AND myString is not empty or blank 397 | return True 398 | #myString is None OR myString is empty or blank 399 | return False 400 | 401 | def runCmd (cmd, logPrefix=""): 402 | if cmd and (executeCommand(cmd, logPrefix) == 0): 403 | return True 404 | else: 405 | return False 406 | 407 | class mounter (threading.Thread): 408 | def __init__ (self, section, filename): 409 | threading.Thread.__init__( self ) 410 | logger.info("Setting up thread for resource " + section) 411 | logger.info("Added thread to mounter map: " + section + filename + "=>" + str(self)) 412 | mounterMap[section + filename] = self 413 | self.states = ['INIT', 'PING_SUCCESS', 'PING_FAILURE', 'MOUNT_SUCCESS', 'MOUNT_FAILURE'] 414 | self.state = 'INIT' 415 | self.modifyTime = os.path.getmtime(filename) 416 | self.filename = filename 417 | self.section = section 418 | self.logprefix = "[" + self.section + "] " 419 | self.config = ConfigParser.ConfigParser() 420 | self.mounted = False 421 | self.reload = False 422 | self.updateConfigs() 423 | 424 | # Should be called *after* changing state 425 | def updateCurrentInterval (self): 426 | logger.info(self.logprefix + "Updating current interval in state [" + self.state + "]") 427 | if self.state is 'PING_SUCCESS': 428 | self.setCurrentInterval(self.intervalpingsuccess) 429 | elif self.state is 'PING_FAILURE': 430 | self.setCurrentInterval(self.intervalpingfailure) 431 | elif self.state is 'MOUNT_SUCCESS': 432 | self.setCurrentInterval(self.intervalmountsuccess) 433 | elif self.state is 'MOUNT_FAILURE': 434 | self.setCurrentInterval(self.intervalmountfailure) 435 | else: 436 | self.setCurrentInterval(self.interval) 437 | 438 | def setCurrentInterval (self, currentinterval): 439 | logger.info(self.logprefix + "Setting current interval to [" + str(currentinterval) + "]") 440 | self.currentinterval = currentinterval 441 | 442 | def changeState (self, toState): 443 | if self.state not in self.states: 444 | logger.error(self.logprefix + "Unknown state [" + toState + "]") 445 | return 446 | logger.info(self.logprefix + "Changing state from [" + self.state + "] to [" + toState + "]") 447 | self.state = toState 448 | 449 | def updateConfigs (self): 450 | logger.info(self.logprefix + "Updating configs from file: " + self.filename + " and section: " + self.section) 451 | self.config = ConfigParser.ConfigParser() 452 | self.config.read(self.filename) 453 | if not self.config.has_section(self.section): 454 | logger.info(self.logprefix + "Section has been removed from config file.") 455 | self.stop() 456 | return 457 | self.interval = getConfig(self.config, self.section, 'RECHECK_INTERVAL_SECONDS', DEFAULT_RECHECK_INTERVAL_SECONDS, int, logPrefix=self.logprefix) 458 | self.intervalpingsuccess = getConfig(self.config, self.section, 'RECHECK_INTERVAL_SECONDS_PING_SUCCESS', self.interval, int, logPrefix=self.logprefix) 459 | self.intervalpingfailure = getConfig(self.config, self.section, 'RECHECK_INTERVAL_SECONDS_PING_FAILURE', self.interval, int, logPrefix=self.logprefix) 460 | self.intervalmountsuccess = getConfig(self.config, self.section, 'RECHECK_INTERVAL_SECONDS_MOUNT_SUCCESS', self.interval, int, logPrefix=self.logprefix) 461 | self.intervalmountfailure = getConfig(self.config, self.section, 'RECHECK_INTERVAL_SECONDS_MOUNT_FAILURE', self.interval, int, logPrefix=self.logprefix) 462 | self.mounttestcmd = getConfig(self.config, self.section, 'MOUNT_TEST_CMD', DEFAULT_MOUNT_TEST_CMD, logPrefix=self.logprefix) 463 | self.pingcmd = getConfig(self.config, self.section, 'PING_CMD', DEFAULT_PING_CMD, logPrefix=self.logprefix) 464 | self.premountcmd = getConfig(self.config, self.section, 'PRE_MOUNT_CMD', DEFAULT_PRE_MOUNT_CMD, logPrefix=self.logprefix) 465 | self.wakecmd = getConfig(self.config, self.section, 'WAKE_CMD', DEFAULT_WAKE_CMD, logPrefix=self.logprefix) 466 | self.wakeattempts = getConfig(self.config, self.section, 'WAKE_ATTEMPTS', DEFAULT_WAKE_ATTEMPTS, int, logPrefix=self.logprefix) 467 | self.mountcmd = getConfig(self.config, self.section, 'MOUNT_CMD', DEFAULT_MOUNT_CMD, logPrefix=self.logprefix) 468 | self.mountsuccesscmd = getConfig(self.config, self.section, 'MOUNT_SUCCESS_CMD', DEFAULT_MOUNT_SUCCESS_CMD, logPrefix=self.logprefix) 469 | self.mountfailurecmd = getConfig(self.config, self.section, 'MOUNT_FAILURE_CMD', DEFAULT_MOUNT_FAILURE_CMD, logPrefix=self.logprefix) 470 | self.postmountcmd = getConfig(self.config, self.section, 'POST_MOUNT_CMD', DEFAULT_POST_MOUNT_CMD, logPrefix=self.logprefix) 471 | self.lostmountcmd = getConfig(self.config, self.section, 'LOST_MOUNT_CMD', DEFAULT_LOST_MOUNT_CMD, logPrefix=self.logprefix) 472 | self.foundmountcmd = getConfig(self.config, self.section, 'FOUND_MOUNT_CMD', DEFAULT_FOUND_MOUNT_CMD, logPrefix=self.logprefix) 473 | 474 | def stop (self): 475 | logger.info(self.logprefix + "Stopping thread: " + str(threading.current_thread())) 476 | self.running = False 477 | 478 | def mountFailure (self, reason=""): 479 | logger.info(self.logprefix + "Mount failure!") 480 | if isNotBlank(self.mountfailurecmd): 481 | logger.info(self.logprefix + "Mount failure command specified. Running.") 482 | runCmd("export REASON=\"" + reason + "\"; " + self.mountfailurecmd, self.logprefix) 483 | if self.mounted: 484 | if isNotBlank(self.lostmountcmd): 485 | logger.info(self.logprefix + "Lost mount command specified. Running.") 486 | runCmd("export REASON=\"" + reason + "\"; " + self.lostmountcmd, self.logprefix) 487 | self.mounted = False 488 | 489 | def mountSuccess (self): 490 | logger.info(self.logprefix + "Mount success!") 491 | if isNotBlank(self.mountsuccesscmd): 492 | logger.info(self.logprefix + "Mount success command specified. Running.") 493 | runCmd(self.mountsuccesscmd, self.logprefix) 494 | if not self.mounted: 495 | if isNotBlank(self.foundmountcmd): 496 | logger.info(self.logprefix + "Found mount command specified. Running.") 497 | runCmd(self.foundmountcmd, self.logprefix) 498 | self.mounted = True 499 | 500 | def run (self): 501 | seconds = 0 502 | self.updateCurrentInterval() 503 | self.running = True 504 | while self.running: 505 | try: 506 | modifyTime = os.path.getmtime(self.filename) 507 | if modifyTime > self.modifyTime: 508 | logger.info(self.logprefix + "Configs have changed!") 509 | logger.info(self.logprefix + "new config time: " + time.ctime(modifyTime)) 510 | logger.info(self.logprefix + "old config time: " + time.ctime(self.modifyTime)) 511 | self.updateConfigs() 512 | self.modifyTime = modifyTime 513 | self.configsmodified = True 514 | else: 515 | self.configsmodified = False 516 | except Exception as e: 517 | logger.error(e) 518 | logger.info("File " + self.filename + " is gone!") 519 | break 520 | if (seconds % self.currentinterval == 0) or self.configsmodified or self.reload: 521 | try: 522 | self.reload = False 523 | logger.info(self.logprefix + "Working on section [" + self.section + "] from file [" + self.filename + "]") 524 | if isBlank(self.mountcmd): 525 | #First make sure we have a mount command. 526 | logger.info(self.logprefix + "No mount command specified. Nothing to do for") 527 | else: 528 | if isBlank(self.mounttestcmd): 529 | logger.info(self.logprefix + "No mount test command specified. Can't test mount. Assume not mounted.") 530 | # Assume not mounted! 531 | else: 532 | logger.info(self.logprefix + "Mount test command specified.") 533 | if runCmd(self.mounttestcmd, self.logprefix): 534 | # Resource is already mounted. Do nothing. 535 | logger.info(self.logprefix + "Resource is already mounted. Nothing to do.") 536 | self.mountSuccess() 537 | self.changeState('MOUNT_SUCCESS') 538 | else: 539 | logger.info(self.logprefix + "Resource is no longer mounted.") 540 | self.mountFailure() 541 | self.changeState('MOUNT_FAILURE') 542 | if not self.mounted: 543 | # Resource is not mounted. 544 | logger.info(self.logprefix + "Resource is NOT mounted. Lets get to work.") 545 | pingSuccess = False 546 | if isNotBlank(self.pingcmd): 547 | logger.info(self.logprefix + "Ping command specified.") 548 | if isNotBlank(self.wakecmd): 549 | logger.info(self.logprefix + "Wake command specified.") 550 | wakeAttempts = self.wakeattempts 551 | while (True): 552 | logger.info(self.logprefix + "PING! Houston do you copy?") 553 | if not runCmd(self.pingcmd, self.logprefix): 554 | logger.info(self.logprefix + "Ping failed! Wake attempts left: " + str(wakeAttempts)) 555 | if (wakeAttempts == 0): 556 | self.mountFailure("Ping failed.") 557 | self.changeState('PING_FAILURE') 558 | break 559 | logger.info(self.logprefix + "Now try to wake the resource up.") 560 | runCmd(self.wakecmd, self.logprefix) 561 | wakeAttempts -= 1 562 | else: 563 | logger.info(self.logprefix + "Ping successful.") 564 | self.changeState('PING_SUCCESS') 565 | pingSuccess = True 566 | break 567 | else: 568 | logger.info(self.logprefix + "PING! Houston do you copy?") 569 | if runCmd(self.pingcmd, self.logprefix): 570 | logger.info(self.logprefix + "Ping successful.") 571 | self.changeState('PING_SUCCESS') 572 | pingSuccess = True 573 | else: 574 | self.mountFailure("Ping failed.") 575 | self.changeState('PING_FAILURE') 576 | logger.info(self.logprefix + "Resource is down. Will not attempt mount.") 577 | else: 578 | # No ping command, we assume ping suceeds, and 579 | # we force mounting process 580 | logger.info(self.logprefix + "Ping command not specified. Assuming success.") 581 | pingSuccess = True 582 | 583 | if pingSuccess: 584 | if isNotBlank(self.premountcmd): 585 | logger.info(self.logprefix + "Pre mount command specified. Running.") 586 | if not runCmd(self.premountcmd, self.logprefix): 587 | logger.info(self.logprefix + "Pre mount command failed!") 588 | 589 | logger.info(self.logprefix + "Mounting...") 590 | if not runCmd(self.mountcmd, self.logprefix): 591 | self.mountFailure("Mount failed.") 592 | self.changeState('MOUNT_FAILURE') 593 | else: 594 | self.mountSuccess() 595 | self.changeState('MOUNT_SUCCESS') 596 | 597 | if isNotBlank(self.postmountcmd): 598 | logger.info(self.logprefix + "Post mount command specified. Running.") 599 | if not runCmd(self.postmountcmd, self.logprefix): 600 | logger.info(self.logprefix + "Post Mount command failed!") 601 | self.updateCurrentInterval() 602 | logger.info(self.logprefix + "Next test after " + str(self.currentinterval) + " seconds") 603 | except Exception as e: 604 | logger.error("Caught exception! Logging and continuing...") 605 | logger.error(e) 606 | logger.exception(e) 607 | time.sleep(float(1)) 608 | seconds += 1 609 | logger.info(self.logprefix + "Hasta La Vista. Baby.") 610 | mounterMap.pop(self.section + self.filename, None) 611 | logger.info("Removed thread from mounter map: " + self.section + self.filename) 612 | 613 | # Register signal handler 614 | signal.signal(signal.SIGINT, ctrlc_handler) 615 | signal.signal(signal.SIGHUP, hup_handler) 616 | 617 | if __name__ == "__main__": crux() 618 | --------------------------------------------------------------------------------