]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
7710c712968b9d5c49075b469194336a885b2749
[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', 'buildd']
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_32 = '=L32s256s'
118   LASTLOG_STRUCT_64 = '=Q32s256s'
119
120   def __init__(self):
121     record_size_32 = struct.calcsize(self.LASTLOG_STRUCT_32)
122     record_size_64 = struct.calcsize(self.LASTLOG_STRUCT_64)
123     lastlog_size = os.path.getsize('/var/log/lastlog')
124     if 0 == (lastlog_size % record_size_32):
125         self.LASTLOG_STRUCT = self.LASTLOG_STRUCT_32
126         record_size = record_size_32
127     elif 0 == (lastlog_size % record_size_64):
128         self.LASTLOG_STRUCT = self.LASTLOG_STRUCT_64
129         record_size = record_size_64
130     else:
131         raise RuntimeError('Unknown architecture, cannot interpret /var/log/lastlog file size (%d)' % lastlog_size)
132     with open('/var/log/lastlog', 'r') as fp:
133       uid = -1 # there is one record per uid in lastlog
134       for record in iter(lambda: fp.read(record_size), ''):
135         uid += 1 # so keep incrementing uid for each record read
136         lastlog_time, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
137         try:
138           self[pwd.getpwuid(uid).pw_name] = lastlog_time
139         except KeyError:
140           # this is a normal condition
141           continue
142
143 class HomedirSizes(dict):
144   def __init__(self):
145     for direntry in glob.glob('/home/*'):
146       username = os.path.basename(direntry)
147
148       if username in EXCLUDED_USERNAMES:
149         continue
150
151       try:
152         pwinfo = pwd.getpwnam(username)
153       except KeyError:
154         if os.path.isdir(direntry):
155           logging.warning('directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
156         continue
157
158       if pwinfo.pw_dir != direntry:
159         logging.warning('home directory for %s is not %s, but that exists.  confused.', username, direntry)
160         continue
161
162       command = ['/usr/bin/du', '-ms', pwinfo.pw_dir]
163       p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
164       (stdout, stderr) = p.communicate()
165       if p.returncode != 0: # ignore errors from du
166         logging.info('%s failed:', ' '.join(command))
167         logging.info(stderr)
168         continue
169       try:
170         self[username] = int(stdout.split('\t')[0])
171       except ValueError:
172         logging.error('could not convert size output from %s: %s', ' '.join(command), stdout)
173         continue
174
175 class HomedirReminder(object):
176   def __init__(self):
177     self.lastlog_times = LastlogTimes()
178     self.homedir_sizes = HomedirSizes()
179
180   def notify(self, **kwargs):
181     msg = email.mime.text.MIMEText(MAIL_MESSAGE.format(**kwargs), _charset='UTF-8')
182     msg['From'] = MAIL_FROM.format(**kwargs)
183     msg['To'] = MAIL_TO.format(**kwargs)
184     if MAIL_CC != "":
185       msg['Cc'] = MAIL_CC.format(**kwargs)
186     if MAIL_REPLYTO != "":
187       msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
188     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
189     msg['Precedence'] = "bulk"
190     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
191     p = subprocess.Popen(SENDMAIL_COMMAND, stdin=subprocess.PIPE)
192     p.communicate(msg.as_string())
193     logging.debug(msg.as_string())
194     if p.returncode != 0:
195       raise SendmailError
196
197   def remove(self, **kwargs):
198     try:
199       pwinfo = pwd.getpwnam(kwargs.get('username'))
200     except KeyError:
201       return
202
203     command = RM_COMMAND + [pwinfo.pw_dir]
204     p = subprocess.check_call(command)
205
206   def run(self):
207     current_time = time.time()
208     conn = None
209     try:
210       data = {}
211       for user in set(self.homedir_sizes.keys()) | \
212                   set(self.lastlog_times.keys()):
213         data[user] = {
214           'homedir': self.homedir_sizes.get(user, 0),
215           'lastlog': self.lastlog_times.get(user, 0),
216         }
217
218       msg = {
219         'timestamp': current_time,
220         'data': data,
221         'host': platform.node(),
222       }
223       conn = Connection(conf=mq_conf)
224       conn.topic_send(config.topic,
225                       msg,
226                       exchange_name=config.exchange,
227                       timeout=5)
228     except Exception, e:
229       logging.error("Error sending: %s" % e)
230     finally:
231       if conn:
232         conn.close()
233
234     for username, homedir_size in self.homedir_sizes.iteritems():
235       try:
236         realname = pwd.getpwnam(username).pw_gecos.decode('utf-8').split(',', 1)[0]
237       except:
238         realname = username
239       lastlog_time = self.lastlog_times.get(username, 0)
240       days_ago = int( (current_time - lastlog_time) / 3600 / 24 )
241       kwargs = {
242           'hostname': platform.node(),
243           'username': username,
244           'realname': realname,
245           'homedir_size': homedir_size,
246           'days_ago': days_ago
247         }
248
249       notify = False
250       remove = False
251       for x in CRITERIA:
252         if homedir_size > x['size'] and 'notifyafter' in x and days_ago >= x['notifyafter']:
253           notify = True
254         if homedir_size > x['size'] and 'deleteafter' in x and days_ago >= x['deleteafter']:
255           remove = True
256
257       if remove:
258         self.remove(**kwargs)
259       elif notify:
260         self.notify(**kwargs)
261
262 if __name__ == '__main__':
263   lvl = logging.ERROR
264   if options.debug:
265     lvl = logging.DEBUG
266   logging.basicConfig(level=lvl)
267   HomedirReminder().run()