]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
8eb9264f41e7ef722458d732817c784ccbca87a8
[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': 75, 'size': 10 }
54   ]
55 USER_EXCLUSION_LIST = ['lost+found']
56 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
57 MAIL_TO = '{username}@{hostname}.debian.org'
58 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
59 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
60 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
61 MAIL_TEXT = u"""\
62 Hi {name},
63
64 you last logged into {hostname} {days_ago} days ago, and your home
65 directory there is {homedir_size}MB in size.
66
67 Disk space on porter boxes is often limited.  Please respect your fellow
68 porters by cleaning up after yourself and deleting schroots and source/build
69 trees in your home directory as soon as feasible.
70
71 If you currently do not use {hostname}, please keep ~{username} under 10MB.
72
73 Thanks,
74 Cron, on behalf of your catherders/admins
75
76 PS: replies not required.
77 """
78
79 class Error(Exception):
80   pass
81
82 class SendmailError(Error):
83   pass
84
85 class LastLog(object):
86   LASTLOG_STRUCT = '=L32s256s'
87
88   def __init__(self, fname='/var/log/lastlog'):
89     record_size = struct.calcsize(self.LASTLOG_STRUCT)
90     self.records = {}
91     with open(fname, 'r') as fp:
92       uid = -1
93       for record in iter(lambda: fp.read(record_size), ''):
94         uid += 1
95         last_login, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
96         if last_login == 0:
97           continue
98         try:
99           self.records[pwd.getpwuid(uid).pw_name] = last_login
100         except KeyError:
101           continue
102
103   def last_login_for_user(self, username):
104     return self.records.get(username, 0)
105
106 class HomedirReminder(object):
107   def __init__(self):
108     self.lastlog = LastLog()
109     self.generate_homedir_list()
110
111   def parse_utmp(self):
112     self.utmp_records = defaultdict(list)
113     for wtmpfile in glob.glob('/var/log/wtmp*'):
114       for entry in utmp.UtmpRecord(wtmpfile):
115         # TODO: Login, does not account for non-idle sessions.
116         self.utmp_records[entry.ut_user].append(entry.ut_tv[0])
117     for username, timestamps in self.utmp_records.iteritems():
118       self.utmp_records[username] = sorted(timestamps)[-1]
119
120   def last_login_for_user(self, username):
121     return self.lastlog.last_login_for_user(username)
122
123   def generate_homedir_list(self):
124     self.homedir_sizes = {}
125     for direntry in glob.glob('/home/*'):
126       username = os.path.basename(direntry)
127       try:
128         pwinfo = pwd.getpwnam(username)
129       except KeyError:
130         if os.path.isdir(direntry):
131           logging.warning('Directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
132         continue
133       homedir = pwinfo.pw_dir
134
135       if username in USER_EXCLUSION_LIST:
136         continue
137       # Ignore errors from du.
138       command = ['/usr/bin/du', '-ms', homedir]
139       p = subprocess.Popen(command,
140                            stdout=subprocess.PIPE,
141                            stderr=subprocess.PIPE)
142       (stdout, stderr) = p.communicate()
143       if p.returncode != 0:
144         logging.info('%s failed:', ' '.join(command))
145         logging.info(stderr)
146       try:
147         size = int(stdout.split('\t')[0])
148       except ValueError:
149         logging.error('Could not convert size output from %s: %s',
150                       ' '.join(command), stdout)
151         continue
152       self.homedir_sizes[username] = size
153
154   def send_mail(self, **kwargs):
155     msg = email.mime.text.MIMEText(MAIL_TEXT.format(**kwargs), _charset='UTF-8')
156     msg['From'] = MAIL_FROM.format(**kwargs)
157     msg['To'] = MAIL_TO.format(**kwargs)
158     if MAIL_CC != "": msg['Cc'] = MAIL_CC.format(**kwargs)
159     if MAIL_REPLYTO != "": msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
160     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
161     msg['Precedence'] = "bulk"
162     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
163     p = subprocess.Popen(SENDMAIL, stdin=subprocess.PIPE)
164     p.communicate(msg.as_string())
165     logging.debug(msg.as_string())
166     if p.returncode != 0:
167       raise SendmailError
168
169   def run(self):
170     current_time = time.time()
171     for username, homedir_size in self.homedir_sizes.iteritems():
172       last_login = self.last_login_for_user(username)
173       logging.info('user %s: size %dMB, last login: %d', username, homedir_size, last_login)
174       days_ago = int( (current_time - last_login) / 3600 / 24 )
175
176       reportsize = None
177       for e in REPORT_SIZES:
178         if days_ago > e['days']: reportsize = e['size']
179
180       if reportsize is not None and homedir_size > reportsize:
181         logging.warning('Homedir of user %s is %d and did not login for a while', username, homedir_size)
182         try:
183           name = pwd.getpwnam(username).pw_gecos.decode('utf-8')
184           name = name.split(',', 1)[0]
185         except:
186           name = username
187         self.send_mail(hostname=platform.node(),
188                        username=username,
189                        name=name,
190                        homedir_size=homedir_size,
191                        days_ago=days_ago)
192
193 if __name__ == '__main__':
194   logging.basicConfig()
195   # DEBUG for debugging, ERROR for production.
196   logging.getLogger().setLevel(logging.ERROR)
197   HomedirReminder().run()