]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
Do not always annoy people with more than 5mb
[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 <pkern@debian.org>
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 collections import defaultdict
28 import email
29 import email.mime.text
30 import glob
31 import logging
32 import os.path
33 import platform
34 import pwd
35 import subprocess
36 import struct
37 import time
38 import StringIO
39
40 # avoid base64 encoding for utf-8
41 email.charset.add_charset('utf-8', email.charset.SHORTEST, email.charset.QP)
42
43 DRYRUN = False
44
45 if DRYRUN:
46   SENDMAIL_COMMAND = ['/bin/cat']
47 else:
48   SENDMAIL_COMMAND = ['/usr/sbin/sendmail', '-t', '-oi']
49
50 if DRYRUN:
51   RM_COMMAND = ['/bin/echo', 'Would remove']
52 else:
53   RM_COMMAND = ['/bin/rm', '-rf']
54
55 CRITERIA = [
56     { 'size': 10240,  'notifyafter':  5}, #, 'deleteafter':  40 },
57     { 'size':  1024,  'notifyafter': 10}, #, 'deleteafter':  50 },
58     { 'size':   100,  'notifyafter': 30}, #, 'deleteafter':  90 },
59     { 'size':    20,  'notifyafter': 90}, #, 'deleteafter': 150 },
60     { 'size':     5,                        'deleteafter': 450 }
61   ]
62 EXCLUDED_USERNAMES = ['lost+found']
63 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
64 MAIL_TO = '{username}@{hostname}.debian.org'
65 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
66 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
67 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
68 MAIL_MESSAGE = u"""\
69 Hi {realname}!
70
71 Thanks for your porting effort on {hostname}!
72
73 Please note that, on most porterboxes, /home is quite small, so please
74 remove files that you do not need anymore.
75
76 For your information, you last logged into {hostname} {days_ago} days
77 ago, and your home directory there is {homedir_size} MB in size.
78
79 If you currently do not use {hostname}, please keep ~{username} under
80 10 MB, if possible.
81
82 Please assist us in freeing up space by deleting schroots, also.
83
84 Thanks,
85
86 Debian System Administration Team via Cron
87
88 PS: A reply is not required.
89 """
90
91 class Error(Exception):
92   pass
93
94 class SendmailError(Error):
95   pass
96
97 class LastlogTimes(dict):
98   LASTLOG_STRUCT = '=L32s256s'
99
100   def __init__(self):
101     record_size = struct.calcsize(self.LASTLOG_STRUCT)
102     with open('/var/log/lastlog', 'r') as fp:
103       uid = -1 # there is one record per uid in lastlog
104       for record in iter(lambda: fp.read(record_size), ''):
105         uid += 1 # so keep incrementing uid for each record read
106         lastlog_time, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
107         try:
108           self[pwd.getpwuid(uid).pw_name] = lastlog_time
109         except KeyError:
110           # this is a normal condition
111           continue
112
113 class HomedirSizes(dict):
114   def __init__(self):
115     for direntry in glob.glob('/home/*'):
116       username = os.path.basename(direntry)
117
118       if username in EXCLUDED_USERNAMES:
119         continue
120
121       try:
122         pwinfo = pwd.getpwnam(username)
123       except KeyError:
124         if os.path.isdir(direntry):
125           logging.warning('directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
126         continue
127
128       if pwinfo.pw_dir != direntry:
129         logging.warning('home directory for %s is not %s, but that exists.  confused.', username, direntry)
130         continue
131
132       command = ['/usr/bin/du', '-ms', pwinfo.pw_dir]
133       p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
134       (stdout, stderr) = p.communicate()
135       if p.returncode != 0: # ignore errors from du
136         logging.info('%s failed:', ' '.join(command))
137         logging.info(stderr)
138         continue
139       try:
140         self[username] = int(stdout.split('\t')[0])
141       except ValueError:
142         logging.error('could not convert size output from %s: %s', ' '.join(command), stdout)
143         continue
144
145 class HomedirReminder(object):
146   def __init__(self):
147     self.lastlog_times = LastlogTimes()
148     self.homedir_sizes = HomedirSizes()
149
150   def notify(self, **kwargs):
151     msg = email.mime.text.MIMEText(MAIL_MESSAGE.format(**kwargs), _charset='UTF-8')
152     msg['From'] = MAIL_FROM.format(**kwargs)
153     msg['To'] = MAIL_TO.format(**kwargs)
154     if MAIL_CC != "":
155       msg['Cc'] = MAIL_CC.format(**kwargs)
156     if MAIL_REPLYTO != "":
157       msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
158     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
159     msg['Precedence'] = "bulk"
160     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
161     p = subprocess.Popen(SENDMAIL_COMMAND, stdin=subprocess.PIPE)
162     p.communicate(msg.as_string())
163     logging.debug(msg.as_string())
164     if p.returncode != 0:
165       raise SendmailError
166
167   def remove(self, **kwargs):
168     try:
169       pwinfo = pwd.getpwnam(kwargs.get('username'))
170       return
171     except KeyError:
172       return
173
174     command = RM_COMMAND + [pwinfo.pw_dir]
175     p = subprocess.check_call(command)
176
177   def run(self):
178     current_time = time.time()
179
180     for username, homedir_size in self.homedir_sizes.iteritems():
181       try:
182         realname = pwd.getpwnam(username).pw_gecos.decode('utf-8').split(',', 1)[0]
183       except:
184         realname = username
185       lastlog_time = self.lastlog_times[username]
186       days_ago = int( (current_time - lastlog_time) / 3600 / 24 )
187       kwargs = {
188           'hostname': platform.node(),
189           'username': username,
190           'realname': realname,
191           'homedir_size': homedir_size,
192           'days_ago': days_ago
193         }
194
195       notify = False
196       remove = False
197       for x in CRITERIA:
198         if homedir_size > x['size'] and 'notifyafter' in x and days_ago >= x['notifyafter']:
199           notify = True
200         if homedir_size > x['size'] and 'deleteafter' in x and days_ago >= x['deleteafter']:
201           remove = True
202
203       if remove:
204         self.remove(**kwargs)
205       elif notify:
206         self.notify(**kwargs)
207
208 if __name__ == '__main__':
209   logging.basicConfig()
210   # DEBUG for debugging, ERROR for production.
211   logging.getLogger().setLevel(logging.ERROR)
212   HomedirReminder().run()