#! /usr/bin/python # Changes the ACLs of sub-directories in a directory and remembers which directories have already # been processed in order to speed things up. # This file needs a changeSmbPerms_XXX.account file with the correspond account information. # ############################### # # Copyright 2008 ETH Zuerich, CISD # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os, re, sys, string, subprocess, time, glob from optparse import OptionParser # Stores all relevant information for a mount point class MountpointConfig: pass MAX_NUMBER_OF_RETRIES = 30 SLEEP_AFTER_FAILURE = 6 # in seconds LOCK = '.changeSmbPerms.LOCK' PROCESSED_FILE_READ = '.processed_read' PROCESSED_FILE_WRITE = '.processed_write' def readMountpointConfigs(): cfgDir = os.path.dirname(sys.argv[0]) mountpointConfigs = {} for fn in glob.glob("%s/changeSmbPerms_*.account" % cfgDir): config = MountpointConfig() f = open(fn) try: config.MOUNTPOINT = f.readline()[:-1] config.USER = f.readline()[:-1] config.PASSWORD = f.readline()[:-1] config.GROUP = f.readline()[:-1] config.SHARE = f.readline()[:-1] config.ADDITIONAL_PATH = f.readline()[:-1] config.FILE_ACL_CHANGE = "ACL:%s:ALLOWED/0/CHANGE,ACL:D\\id-sd-storage-nasadmin:ALLOWED/0/FULL,ACL:%s:ALLOWED/0/FULL" % (config.GROUP, config.USER) config.DIR_ACL_CHANGE = "ACL:%s:ALLOWED/3/CHANGE,ACL:D\\id-sd-storage-nasadmin:ALLOWED/3/FULL,ACL:%s:ALLOWED/3/FULL" % (config.GROUP, config.USER) config.FILE_ACL_READ = "ACL:%s:ALLOWED/0/READ,ACL:D\\id-sd-storage-nasadmin:ALLOWED/0/FULL,ACL:%s:ALLOWED/0/FULL" % (config.GROUP, config.USER) config.DIR_ACL_READ = "ACL:%s:ALLOWED/3/READ,ACL:D\\id-sd-storage-nasadmin:ALLOWED/3/FULL,ACL:%s:ALLOWED/3/FULL" % (config.GROUP, config.USER) config.LIMS_GROUP_ACL_ALLOW_REGEX = re.compile('ACL:%s:ALLOWED/.+' % config.GROUP.replace("\\", "\\\\")) mountpointConfigs[config.MOUNTPOINT] = config finally: f.close() return mountpointConfigs # Reads in all the configured CIFS mount points MOUNTPOINT_CONFIGS = readMountpointConfigs() def getMountpointConfig(path): path = os.path.abspath(path) for mountpoint in MOUNTPOINT_CONFIGS.keys(): if path.startswith(mountpoint): return MOUNTPOINT_CONFIGS[mountpoint] print >>sys.stderr, "ERROR: path '%s' does not start with one of '%s'" % (path, MOUNTPOINT_CONFIGS.keys()) sys.exit(1) def smbcacls(config, path, acls=[]): path = os.path.abspath(path)[len(config.MOUNTPOINT):] command = ["smbcacls", config.SHARE, os.path.join(config.ADDITIONAL_PATH, path), "-U", "%s%%%s" % (config.USER, config.PASSWORD)] + list(acls) for i in range(MAX_NUMBER_OF_RETRIES): # have some retries to cope with temporary NAS failures process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) retval = process.wait() if retval == 0: # it worked fine - get out of here break time.sleep(SLEEP_AFTER_FAILURE) # give the NAS some time to recover output = process.stdout.readlines() if retval != 0: command[4] = "xxx" # avoid sending passwords by email if this script is called by cron print >>sys.stderr, "ERROR when running '%s', return value was '%d'" % (string.join(command, ' '), retval) print >>sys.stderr, "Output was:" for l in output: print >>sys.stderr, " >> %s" % l[:-1] return (retval, output) def getCurrentLimsGroupAclTupleToRevoke(config, path): currentLimsGroupACLS = getACLs(config, path) if currentLimsGroupACLS: return ('-D', string.join(currentLimsGroupACLS, ',')) else: return () def revokeAccess(path): config = getMountpointConfig(path) retval, output = smbcacls(config, path, getCurrentLimsGroupAclTupleToRevoke(config, path)) return retval def chown(path): config = getMountpointConfig(path) retval, output = smbcacls(config, path, ('-C', '%s' % config.USER)) return retval def allowChange(path, isDir, changeOwner): config = getMountpointConfig(path) if changeOwner: retval = chown(path) if retval != 0: return retval if isDir: aclAllowEntry = ('-S', config.DIR_ACL_CHANGE) else: aclAllowEntry = ('-S', config.FILE_ACL_CHANGE) retval, output = smbcacls(config, path, aclAllowEntry) return retval def allowRead(path, isDir, changeOwner): config = getMountpointConfig(path) if changeOwner: retval = chown(path) if retval != 0: return retval if isDir: aclAllowEntry = ('-S', config.DIR_ACL_READ) else: aclAllowEntry = ('-S', config.FILE_ACL_READ) retval, output = smbcacls(config, path, aclAllowEntry) return retval def getACLs(config, path): retval, output = smbcacls(config, path) aclEntries = [] for aclLine in output: aclLine = aclLine[:-1] # Strip trailing newline if config.LIMS_GROUP_ACL_ALLOW_REGEX.match(aclLine): aclEntries.append(aclLine) return aclEntries def readProcessedDirs(isReadOnly): processed = set() # Ensure that we clean up the "wrong" .processed_ file if isReadOnly: RIGHT_PROCESSED_FILE = PROCESSED_FILE_READ WRONG_PROCESSED_FILE = PROCESSED_FILE_WRITE else: RIGHT_PROCESSED_FILE = PROCESSED_FILE_WRITE WRONG_PROCESSED_FILE = PROCESSED_FILE_READ if os.path.exists(WRONG_PROCESSED_FILE): os.remove(WRONG_PROCESSED_FILE) if not os.path.exists(RIGHT_PROCESSED_FILE): return processed for d in open(RIGHT_PROCESSED_FILE): dirname = d[:-1] # strip trailing newline processed.add(dirname) return processed def writeProcessedDirs(processed, isReadOnly): if isReadOnly: PROCESSED_FILE = PROCESSED_FILE_READ else: PROCESSED_FILE = PROCESSED_FILE_WRITE f = open(PROCESSED_FILE, 'w') for d in processed: print >>f, d f.close() def readDirs(): dirs = set(os.listdir('.')) dirs.discard(PROCESSED_FILE_READ) dirs.discard(PROCESSED_FILE_WRITE) dirs.discard(LOCK) return dirs # # Main # parser = OptionParser("usage: %prog [options] ") parser.add_option("-r", "--read-only", action="store_true", dest="readonly", default=False, help="change smb permissions to read-only (default is: read-write)") parser.add_option("-o", "--change-owner", action="store_true", dest="chown", default=False, help="change the owner of each file, too") parser.add_option("-t", "--process-toplevel", action="store_true", dest="process_toplevel", default=False, help="process also the toplevel directory (default: only process children)") parser.add_option("-p", "--ignore-processed", action="store_false", dest="use_processed", default=True, help="ignore the .processed file") (options, args) = parser.parse_args() # What do we need to do? if options.readonly: setACLs = allowRead else: setACLs = allowChange # Go to the right directory if len(args) == 1: pathToCheck = args[0] else: parser.print_help() sys.exit(1) if not os.path.exists(pathToCheck): print >>sys.stderr, "'%s' does not exist." % pathToCheck sys.exit(2) if os.path.isdir(pathToCheck): os.chdir(pathToCheck) elif options.process_toplevel: retval = setACLs(path=pathToCheck, isDir=False, changeOwner=options.chown) if retval != 0: sys.exit(1) else: sys.exit(0) else: print >>sys.stderr, "'%s' is a file (and we are not supposed to process the toplevel)." % pathToCheck sys.exit(2) # Check whether we can get the file lock if os.path.exists(LOCK): sys.exit(100) open(LOCK, 'w').close() try: # Find the sub-directories we need to process if options.use_processed: processed = readProcessedDirs(options.readonly) else: processed = set() current = readDirs() processed &= current # remove directories no longer present current -= processed # remove directories already processed ALL_OK = True if options.process_toplevel: retval = setACLs(path='.', isDir=True, changeOwner=options.chown) if retval != 0: ALL_OK = False for dir in current: OK = True if os.path.isfile(dir): retval = setACLs(path=dir, isDir=False, changeOwner=options.chown) if retval != 0: OK = False else: for root, dirs, files in os.walk(dir): retval = setACLs(path=root, isDir=True, changeOwner=options.chown) if retval != 0: OK = False for f in files: retval = setACLs(path=os.path.join(root, f), isDir=False, changeOwner=options.chown) if retval != 0: OK = False ALL_OK = False if OK: processed.add(dir) if options.use_processed: writeProcessedDirs(processed, options.readonly) if not ALL_OK: sys.exit(1) finally: if os.path.exists(LOCK): os.remove(LOCK)