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