]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
So giving wrong explanations is better than what we had? Somehow I do not want to...
[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 <phil@philkern.de>
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 SENDMAIL = ['/usr/sbin/sendmail', '-t', '-oi']
47 #SENDMAIL = ['/bin/cat']
48
49 EXPLANATIONS = [
50 u"""\
51 {hostname}'s /home is, unfortunately, not infinite in size.  If you have
52 anything in there that you no longer need, please clean it up."""
53 ,u"""\
54 Can you please look at your $HOME on {hostname} and remove files which
55 you no longer need (such as old sources)."""
56 ,u"""\
57 Thanks for your porting effort on {hostname}!
58
59 Please note that on most porterboxes /home is quite small, so please remove
60 files that you do not need anymore."""
61   ]
62
63 CRITERIA = [
64     { 'days':  5, 'size': 10240 },
65     { 'days': 10, 'size':  1024 },
66     { 'days': 30, 'size':   100 },
67     { 'days': 60, 'size':    60 },
68     { 'days': 90, 'size':    10 }
69   ]
70 EXCLUDED_USERNAMES = ['lost+found']
71 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
72 MAIL_TO = '{username}@{hostname}.debian.org'
73 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
74 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
75 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
76 MAIL_MESSAGE = u"""\
77 Hi {name}!
78
79 {explanation}
80
81 For your information, you last logged into {hostname} {days_ago} days
82 ago, and your home directory there is {homedir_size} MB in size.
83
84 If you currently do not use {hostname}, please keep ~{username} under
85 10 MB, if possible.
86
87 Please assist us in freeing up space by deleting schroots, also.
88
89 Thanks,
90
91 Debian System Administration Team via Cron
92
93 PS: replies not required.
94 """
95
96 class Error(Exception):
97   pass
98
99 class SendmailError(Error):
100   pass
101
102 class LastlogTimes(dict):
103   LASTLOG_STRUCT = '=L32s256s'
104   
105   def __init__(self):
106     record_size = struct.calcsize(self.LASTLOG_STRUCT)
107     with open('/var/log/lastlog', 'r') as fp:
108       uid = -1 # there is one record per uid in lastlog
109       for record in iter(lambda: fp.read(record_size), ''):
110         uid += 1 # so keep incrementing uid for each record read
111         lastlog_time, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
112         if lastlog_time == 0:
113           continue
114         try:
115           self[pwd.getpwuid(uid).pw_name] = lastlog_time
116         except KeyError:
117           logging.error('could not resolve username from uid')
118           continue
119
120 class HomedirSizes(dict):
121   def __init__(self):
122     for direntry in glob.glob('/home/*'):
123       username = os.path.basename(direntry)
124       if username in EXCLUDED_USERNAMES:
125         continue
126       try:
127         pwinfo = pwd.getpwnam(username)
128       except KeyError:
129         if os.path.isdir(direntry):
130           logging.warning('directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
131         continue
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 send_mail(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, 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 run(self):
168     current_time = time.time()
169     for username, homedir_size in self.homedir_sizes.iteritems():
170       try:
171         name = pwd.getpwnam(username).pw_gecos.decode('utf-8').split(',', 1)[0].split(' ', 1)[0]
172       except:
173         name = username
174       lastlog_time = self.lastlog_times[username]
175       days_ago = int( (current_time - lastlog_time) / 3600 / 24 )
176       if [x for x in CRITERIA if days_ago >= x['days'] and homedir_size >= x['size']]:
177         explanation = EXPLANATIONS[random.randint(0,len(EXPLANATIONS)-1)].format(hostname=platform.node())
178         self.send_mail(hostname=platform.node(), username=username, name=name, explanation=explanation, homedir_size=homedir_size, days_ago=days_ago)
179
180 if __name__ == '__main__':
181   logging.basicConfig()
182   # DEBUG for debugging, ERROR for production.
183   logging.getLogger().setLevel(logging.ERROR)
184   HomedirReminder().run()