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