]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
Start cleaning out homedirs automatically, as announced
[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     except KeyError:
168       return
169
170     command = RM_COMMAND + [pwinfo.pw_dir]
171     p = subprocess.check_call(command)
172
173   def run(self):
174     current_time = time.time()
175
176     for username, homedir_size in self.homedir_sizes.iteritems():
177       try:
178         realname = pwd.getpwnam(username).pw_gecos.decode('utf-8').split(',', 1)[0]
179       except:
180         realname = username
181       lastlog_time = self.lastlog_times[username]
182       days_ago = int( (current_time - lastlog_time) / 3600 / 24 )
183       kwargs = {
184           'hostname': platform.node(),
185           'username': username,
186           'realname': realname,
187           'homedir_size': homedir_size,
188           'days_ago': days_ago
189         }
190
191       notify = False
192       remove = False
193       for x in CRITERIA:
194         if homedir_size > x['size'] and 'notifyafter' in x and days_ago >= x['notifyafter']:
195           notify = True
196         if homedir_size > x['size'] and 'deleteafter' in x and days_ago >= x['deleteafter']:
197           remove = True
198
199       if remove:
200         self.remove(**kwargs)
201       elif notify:
202         self.notify(**kwargs)
203
204 if __name__ == '__main__':
205   logging.basicConfig()
206   # DEBUG for debugging, ERROR for production.
207   logging.getLogger().setLevel(logging.ERROR)
208   HomedirReminder().run()