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