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