Skip to content

Instantly share code, notes, and snippets.

@pkern
Last active December 30, 2015 22:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pkern/7895342 to your computer and use it in GitHub Desktop.
Save pkern/7895342 to your computer and use it in GitHub Desktop.
#!/usr/bin/python
## vim:set et ts=2 sw=2 ai:
# homedir_reminder.py - Reminds users about sizable homedirs.
##
# Copyright (c) 2013 Philipp Kern <phil@philkern.de>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import print_function
from collections import defaultdict
import email.mime.text
import glob
import logging
import os.path
import platform
import pwd
import subprocess
import struct
import time
import utmp
import StringIO
REPORT_DELAY = 3600 * 24 * 10 # 10 days
REPORT_SIZE = 1024 * 1024 * 10 # 10 MiB
USER_EXCLUSION_LIST = ['lost+found']
MAIL_FROM = 'noreply@{hostname}'
MAIL_TO = 'pkern@{hostname}.0x539.de'
MAIL_SUBJECT = 'Please clean up your home on {hostname}'
MAIL_TEXT = """\
Hi {username},
you used resources on {hostname} and the size of your home directory is
{homedir_size} bytes. This exceeds the threshold of {report_size}.
Furthermore your last login was on {last_login}s since epoch. This is
more than {report_delay}s from now. Hence we think that your home directory
content might be a leftover from a previous session and can be deleted.
Please clean it up yourself.
Sincerely,
Cron
"""
class Error(Exception):
pass
class SendmailError(Error):
pass
class LastLog(object):
LASTLOG_STRUCT = '=L32s256s'
def __init__(self, fname='/var/log/lastlog'):
record_size = struct.calcsize(self.LASTLOG_STRUCT)
self.records = {}
with open(fname, 'r') as fp:
uid = -1
for record in iter(lambda: fp.read(record_size), ''):
uid += 1
last_login, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
if last_login == 0:
continue
try:
self.records[pwd.getpwuid(uid).pw_name] = last_login
except KeyError:
continue
def last_login_for_user(self, username):
return self.records.get(username, 0)
class HomedirReminder(object):
def __init__(self):
self.lastlog = LastLog()
self.generate_homedir_list()
def parse_utmp(self):
self.utmp_records = defaultdict(list)
for wtmpfile in glob.glob('/var/log/wtmp*'):
for entry in utmp.UtmpRecord(wtmpfile):
# TODO: Login, does not account for non-idle sessions.
self.utmp_records[entry.ut_user].append(entry.ut_tv[0])
for username, timestamps in self.utmp_records.iteritems():
self.utmp_records[username] = sorted(timestamps)[-1]
def last_login_for_user(self, username):
return self.lastlog.last_login_for_user(username)
def generate_homedir_list(self):
self.homedir_sizes = {}
for homedir in glob.glob('/home/*'):
username = os.path.basename(homedir)
if username in USER_EXCLUSION_LIST:
continue
# Ignore errors from du.
command = ['/usr/bin/du', '-bs', homedir]
p = subprocess.Popen(command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
if p.returncode != 0:
logging.info('%s failed:', ' '.join(command))
logging.info(stderr)
try:
size = int(stdout.split('\t')[0])
except ValueError:
logging.error('Could not convert size output from %s: %s',
' '.join(command), stdout)
continue
self.homedir_sizes[username] = size
def send_mail(self, **kwargs):
msg = email.mime.text.MIMEText(MAIL_TEXT.format(**kwargs))
msg['From'] = MAIL_FROM.format(**kwargs)
msg['To'] = MAIL_TO.format(**kwargs)
msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
p = subprocess.Popen(['/usr/sbin/sendmail', '-t', '-oi'],
stdin=subprocess.PIPE)
p.communicate(msg.as_string())
logging.debug(msg.as_string())
if p.returncode != 0:
raise SendmailError
def run(self):
current_time = time.time()
for username, homedir_size in self.homedir_sizes.iteritems():
try:
pwd.getpwnam(username)
except KeyError:
# User does not exist, report to admin.
logging.warning('user %s still has an homedir on %s (size=%d)',
username, platform.node(), homedir_size)
last_login = self.last_login_for_user(username)
logging.info('user %s: size %d, last login: %d', username,
homedir_size, last_login)
login_exceeded = (current_time - self.last_login_for_user(username)
> REPORT_DELAY)
homedir_size_exceeded = homedir_size > REPORT_SIZE
if login_exceeded and homedir_size_exceeded:
logging.warning('Homedir of user %s is %d and '
'did not login for a while',
username, homedir_size)
self.send_mail(hostname=platform.node(),
username=username,
homedir_size=homedir_size,
last_login=last_login,
report_size=REPORT_SIZE,
report_delay=REPORT_DELAY)
if __name__ == '__main__':
logging.basicConfig()
# DEBUG for debugging, ERROR for production.
logging.getLogger().setLevel(logging.ERROR)
HomedirReminder().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment