]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
5dbe9859f64d1fa1f70fa86a86fc2d098de3014c
[dsa-puppet.git] / modules / porterbox / files / mail-big-homedirs
1 #!/usr/bin/python
2 ## vim:set et ts=2 sw=2 ai:
3 # Send email reminders to users having sizable homedirs.
4 ##
5 # Copyright (c) 2013 Philipp Kern <phil@philkern.de>
6 # Copyright (c) 2013 Peter Palfrader <peter@palfrader.org>
7 # Copyright (c) 2013 Luca Filipozzi <lfilipoz@debian.org>
8 #
9 # Permission is hereby granted, free of charge, to any person obtaining a copy
10 # of this software and associated documentation files (the "Software"), to deal
11 # in the Software without restriction, including without limitation the rights
12 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 # copies of the Software, and to permit persons to whom the Software is
14 # furnished to do so, subject to the following conditions:
15 #
16 # The above copyright notice and this permission notice shall be included in
17 # all copies or substantial portions of the Software.
18 #
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 # THE SOFTWARE.
26
27 from __future__ import print_function
28
29 from collections import defaultdict
30 import email
31 import email.mime.text
32 import glob
33 import logging
34 import os.path
35 import platform
36 import pwd
37 import random
38 import subprocess
39 import struct
40 import time
41 import StringIO
42
43 # avoid base64 encoding for utf-8
44 email.charset.add_charset('utf-8', email.charset.SHORTEST, email.charset.QP)
45
46 SENDMAIL = ['/usr/sbin/sendmail', '-t', '-oi']
47 #SENDMAIL = ['/bin/cat']
48
49 EXPLANATIONS = [
50 u"""\
51 {hostname} /home is growing close to full.  If you have anything in there that
52 you no longer need, please clean it up.""" # By Martin Zobel-Helas
53 ,u"""\
54 Can you please look at your $HOME on {hostname} and remove files which you
55 no longer need (such as old sources).""" # By Martin Michlmayr
56 ,u"""\
57 Thanks for your porting effort on {hostname}!
58
59 Please note that /home is running short of diskspace, so please remove files
60 you do not need anymore.""" # By Bill Allombert
61   # Please add more from your archive!
62   ]
63
64 REPORT_SIZES = [
65     { 'days':  5, 'size': 10240 },
66     { 'days': 10, 'size':  1024 },
67     { 'days': 30, 'size':   100 },
68     { 'days': 60, 'size':    60 },
69     { 'days': 90, 'size':    30 }
70   ]
71 USER_EXCLUSION_LIST = ['lost+found']
72 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
73 MAIL_TO = '{username}@{hostname}.debian.org'
74 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
75 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
76 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
77 MAIL_TEXT = u"""\
78 Hi {name}!
79
80 {explanation}
81
82 For your information, you last logged into {hostname} {days_ago} days ago, and
83 your home directory there is {homedir_size} MB in size.
84
85 If you currently do not use {hostname}, please keep ~{username} under 30 MB,
86 if possible.  Please assist us in freeing up space by deleting schroots, also.
87
88 Thanks,
89
90 Debian System Administration Team via Cron
91
92 PS: replies not required.
93 """
94
95 class Error(Exception):
96   pass
97
98 class SendmailError(Error):
99   pass
100
101 class LastLog(object):
102   LASTLOG_STRUCT = '=L32s256s'
103
104   def __init__(self, fname='/var/log/lastlog'):
105     record_size = struct.calcsize(self.LASTLOG_STRUCT)
106     self.records = {}
107     with open(fname, 'r') as fp:
108       uid = -1
109       for record in iter(lambda: fp.read(record_size), ''):
110         uid += 1
111         last_login, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
112         if last_login == 0:
113           continue
114         try:
115           self.records[pwd.getpwuid(uid).pw_name] = last_login
116         except KeyError:
117           continue
118
119   def last_login_for_user(self, username):
120     return self.records.get(username, 0)
121
122 class HomedirReminder(object):
123   def __init__(self):
124     self.lastlog = LastLog()
125     self.generate_homedir_list()
126
127   def parse_utmp(self):
128     self.utmp_records = defaultdict(list)
129     for wtmpfile in glob.glob('/var/log/wtmp*'):
130       for entry in utmp.UtmpRecord(wtmpfile):
131         # TODO: Login, does not account for non-idle sessions.
132         self.utmp_records[entry.ut_user].append(entry.ut_tv[0])
133     for username, timestamps in self.utmp_records.iteritems():
134       self.utmp_records[username] = sorted(timestamps)[-1]
135
136   def last_login_for_user(self, username):
137     return self.lastlog.last_login_for_user(username)
138
139   def generate_homedir_list(self):
140     self.homedir_sizes = {}
141     for direntry in glob.glob('/home/*'):
142       username = os.path.basename(direntry)
143       try:
144         pwinfo = pwd.getpwnam(username)
145       except KeyError:
146         if os.path.isdir(direntry):
147           logging.warning('Directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
148         continue
149       homedir = pwinfo.pw_dir
150
151       if username in USER_EXCLUSION_LIST:
152         continue
153       # Ignore errors from du.
154       command = ['/usr/bin/du', '-ms', homedir]
155       p = subprocess.Popen(command,
156                            stdout=subprocess.PIPE,
157                            stderr=subprocess.PIPE)
158       (stdout, stderr) = p.communicate()
159       if p.returncode != 0:
160         logging.info('%s failed:', ' '.join(command))
161         logging.info(stderr)
162       try:
163         size = int(stdout.split('\t')[0])
164       except ValueError:
165         logging.error('Could not convert size output from %s: %s',
166                       ' '.join(command), stdout)
167         continue
168       self.homedir_sizes[username] = size
169
170   def send_mail(self, **kwargs):
171     msg = email.mime.text.MIMEText(MAIL_TEXT.format(**kwargs), _charset='UTF-8')
172     msg['From'] = MAIL_FROM.format(**kwargs)
173     msg['To'] = MAIL_TO.format(**kwargs)
174     if MAIL_CC != "": msg['Cc'] = MAIL_CC.format(**kwargs)
175     if MAIL_REPLYTO != "": msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
176     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
177     msg['Precedence'] = "bulk"
178     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
179     p = subprocess.Popen(SENDMAIL, stdin=subprocess.PIPE)
180     p.communicate(msg.as_string())
181     logging.debug(msg.as_string())
182     if p.returncode != 0:
183       raise SendmailError
184
185   def run(self):
186     current_time = time.time()
187     for username, homedir_size in self.homedir_sizes.iteritems():
188       last_login = self.last_login_for_user(username)
189       logging.info('user %s: size %dMB, last login: %d', username, homedir_size, last_login)
190       days_ago = int( (current_time - last_login) / 3600 / 24 )
191
192       reportsize = None
193       for e in REPORT_SIZES:
194         if days_ago >= e['days']: reportsize = e['size']
195
196       if reportsize is not None and homedir_size > reportsize:
197         logging.warning('Homedir of user %s is %d and did not login for a while', username, homedir_size)
198         try:
199           name = pwd.getpwnam(username).pw_gecos.decode('utf-8')
200           name = name.split(',', 1)[0]
201           name = name.split(' ', 1)[0]
202         except:
203           name = username
204         explanation = EXPLANATIONS[random.randint(0,len(EXPLANATIONS)-1)]
205         explanation = explanation.format(hostname=platform.node())
206         self.send_mail(hostname=platform.node(),
207                        username=username,
208                        name=name,
209                        explanation=explanation,
210                        homedir_size=homedir_size,
211                        days_ago=days_ago)
212
213 if __name__ == '__main__':
214   logging.basicConfig()
215   # DEBUG for debugging, ERROR for production.
216   logging.getLogger().setLevel(logging.ERROR)
217   HomedirReminder().run()