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