]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
98774fe8edcd8de6f666e438606b2d228f60e5fe
[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, 2014 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 from dsa_mq.connection import Connection
29 from dsa_mq.config import Config
30 import email
31 import email.mime.text
32 import glob
33 import logging
34 from optparse import OptionParser
35 import os.path
36 import platform
37 import pwd
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 parser = OptionParser()
47 parser.add_option("-D", "--dryrun",
48                   action="store_true", default=False,
49                   help="Dry run mode")
50
51 parser.add_option("-d", "--debug",
52                   action="store_true", default=False,
53                   help="Enable debug output")
54
55 (options, args) = parser.parse_args()
56 options.section = 'dsa-homedirs'
57 options.config = '/etc/dsa/pubsub.conf'
58 config = Config(options)
59 mq_conf  = {
60   'rabbit_userid': config.username,
61   'rabbit_password': config.password,
62   'rabbit_virtual_host': config.vhost,
63   'rabbit_hosts': ['pubsub02.debian.org', 'pubsub01.debian.org'],
64   'use_ssl': False
65 }
66
67 if options.dryrun:
68   SENDMAIL_COMMAND = ['/bin/cat']
69   RM_COMMAND = ['/bin/echo', 'Would remove']
70 else:
71   SENDMAIL_COMMAND = ['/usr/sbin/sendmail', '-t', '-oi']
72   RM_COMMAND = ['/bin/rm', '-rf']
73
74 CRITERIA = [
75     { 'size': 10240,  'notifyafter':  5, 'deleteafter':  40 },
76     { 'size':  1024,  'notifyafter': 10, 'deleteafter':  50 },
77     { 'size':   100,  'notifyafter': 30, 'deleteafter':  90 },
78     { 'size':    20,  'notifyafter': 90, 'deleteafter': 150 },
79     { 'size':     5,                     'deleteafter': 700 }
80   ]
81 EXCLUDED_USERNAMES = ['lost+found', 'debian']
82 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
83 MAIL_TO = '{username}@{hostname}.debian.org'
84 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
85 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
86 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
87 MAIL_MESSAGE = u"""\
88 Hi {realname}!
89
90 Thanks for your porting effort on {hostname}!
91
92 Please note that, on most porterboxes, /home is quite small, so please
93 remove files that you do not need anymore.
94
95 For your information, you last logged into {hostname} {days_ago} days
96 ago, and your home directory there is {homedir_size} MB in size.
97
98 If you currently do not use {hostname}, please keep ~{username} under
99 10 MB, if possible.
100
101 Please assist us in freeing up space by deleting schroots, also.
102
103 Thanks,
104
105 Debian System Administration Team via Cron
106
107 PS: A reply is not required.
108 """
109
110 class Error(Exception):
111   pass
112
113 class SendmailError(Error):
114   pass
115
116 class LastlogTimes(dict):
117   LASTLOG_STRUCT = '=L32s256s'
118
119   def __init__(self):
120     record_size = struct.calcsize(self.LASTLOG_STRUCT)
121     with open('/var/log/lastlog', 'r') as fp:
122       uid = -1 # there is one record per uid in lastlog
123       for record in iter(lambda: fp.read(record_size), ''):
124         uid += 1 # so keep incrementing uid for each record read
125         lastlog_time, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
126         try:
127           self[pwd.getpwuid(uid).pw_name] = lastlog_time
128         except KeyError:
129           # this is a normal condition
130           continue
131
132 class HomedirSizes(dict):
133   def __init__(self):
134     for direntry in glob.glob('/home/*'):
135       username = os.path.basename(direntry)
136
137       if username in EXCLUDED_USERNAMES:
138         continue
139
140       try:
141         pwinfo = pwd.getpwnam(username)
142       except KeyError:
143         if os.path.isdir(direntry):
144           logging.warning('directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
145         continue
146
147       if pwinfo.pw_dir != direntry:
148         logging.warning('home directory for %s is not %s, but that exists.  confused.', username, direntry)
149         continue
150
151       command = ['/usr/bin/du', '-ms', pwinfo.pw_dir]
152       p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
153       (stdout, stderr) = p.communicate()
154       if p.returncode != 0: # ignore errors from du
155         logging.info('%s failed:', ' '.join(command))
156         logging.info(stderr)
157         continue
158       try:
159         self[username] = int(stdout.split('\t')[0])
160       except ValueError:
161         logging.error('could not convert size output from %s: %s', ' '.join(command), stdout)
162         continue
163
164 class HomedirReminder(object):
165   def __init__(self):
166     self.lastlog_times = LastlogTimes()
167     self.homedir_sizes = HomedirSizes()
168
169   def notify(self, **kwargs):
170     msg = email.mime.text.MIMEText(MAIL_MESSAGE.format(**kwargs), _charset='UTF-8')
171     msg['From'] = MAIL_FROM.format(**kwargs)
172     msg['To'] = MAIL_TO.format(**kwargs)
173     if MAIL_CC != "":
174       msg['Cc'] = MAIL_CC.format(**kwargs)
175     if MAIL_REPLYTO != "":
176       msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
177     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
178     msg['Precedence'] = "bulk"
179     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
180     p = subprocess.Popen(SENDMAIL_COMMAND, stdin=subprocess.PIPE)
181     p.communicate(msg.as_string())
182     logging.debug(msg.as_string())
183     if p.returncode != 0:
184       raise SendmailError
185
186   def remove(self, **kwargs):
187     try:
188       pwinfo = pwd.getpwnam(kwargs.get('username'))
189     except KeyError:
190       return
191
192     command = RM_COMMAND + [pwinfo.pw_dir]
193     p = subprocess.check_call(command)
194
195   def run(self):
196     current_time = time.time()
197     conn = None
198     try:
199       data = {}
200       for user in set(self.homedir_sizes.keys()) | \
201                   set(self.lastlog_times.keys()):
202         data[user] = {
203           'homedir': self.homedir_sizes.get(user, 0),
204           'lastlog': self.lastlog_times.get(user, 0),
205         }
206
207       msg = {
208         'timestamp': current_time,
209         'data': data,
210         'host': platform.node(),
211       }
212       conn = Connection(conf=mq_conf)
213       conn.topic_send(config.topic,
214                       msg,
215                       exchange_name=config.exchange,
216                       timeout=5)
217     except Exception, e:
218       logging.error("Error sending: %s" % e)
219     finally:
220       if conn:
221         conn.close()
222
223     for username, homedir_size in self.homedir_sizes.iteritems():
224       try:
225         realname = pwd.getpwnam(username).pw_gecos.decode('utf-8').split(',', 1)[0]
226       except:
227         realname = username
228       lastlog_time = self.lastlog_times[username]
229       days_ago = int( (current_time - lastlog_time) / 3600 / 24 )
230       kwargs = {
231           'hostname': platform.node(),
232           'username': username,
233           'realname': realname,
234           'homedir_size': homedir_size,
235           'days_ago': days_ago
236         }
237
238       notify = False
239       remove = False
240       for x in CRITERIA:
241         if homedir_size > x['size'] and 'notifyafter' in x and days_ago >= x['notifyafter']:
242           notify = True
243         if homedir_size > x['size'] and 'deleteafter' in x and days_ago >= x['deleteafter']:
244           remove = True
245
246       if remove:
247         self.remove(**kwargs)
248       elif notify:
249         self.notify(**kwargs)
250
251 if __name__ == '__main__':
252   lvl = logging.ERROR
253   if options.debug:
254     lvl = logging.DEBUG
255   logging.basicConfig(level=lvl)
256   HomedirReminder().run()