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