]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
modified text of message
[dsa-puppet.git] / modules / porterbox / files / mail-big-homedirs
1 #!/usr/bin/python
2 ## vim:set et ts=2 sw=2 ai:
3 # homedir_reminder.py - Reminds users about sizable homedirs.
4 ##
5 # Copyright (c) 2013 Philipp Kern <phil@philkern.de>
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 import pwd
42
43 # avoid base64 encoding for utf-8
44 email.charset.add_charset('utf-8', email.charset.SHORTEST, email.charset.QP)
45
46
47 SENDMAIL = ['/usr/sbin/sendmail', '-t', '-oi']
48 #SENDMAIL = ['/bin/cat']
49
50 REPORT_SIZES = [
51     { 'days': 5, 'size': 10240 },
52     { 'days': 10, 'size': 1024 },
53     { 'days': 30, 'size': 100 },
54     { 'days': 75, 'size': 10 }
55   ]
56 USER_EXCLUSION_LIST = ['lost+found']
57 MAIL_FROM = 'debian-admin (via Cron) <bulk@admin.debian.org>'
58 MAIL_TO = '{username}@{hostname}.debian.org'
59 MAIL_CC = 'debian-admin (bulk sink) <bulk@admin.debian.org>'
60 MAIL_REPLYTO = 'debian-admin <dsa@debian.org>'
61 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
62 MAIL_TEXT = u"""\
63 Hi {name},
64
65 you last logged into {hostname} {days_ago} days ago, and your home
66 directory there is {homedir_size}MB in size.
67
68 Disk space on porter boxes is often limited.  Please assist us by freeing up
69 space by deleting schroots and source/build trees in ~{username} as soon as
70 feasible.
71
72 If you currently do not use {hostname}, please keep ~{username} under 10MB.
73
74 Thanks,
75
76 Debian System Administration Team via Cron
77
78 PS: replies not required.
79 """
80
81 class Error(Exception):
82   pass
83
84 class SendmailError(Error):
85   pass
86
87 class LastLog(object):
88   LASTLOG_STRUCT = '=L32s256s'
89
90   def __init__(self, fname='/var/log/lastlog'):
91     record_size = struct.calcsize(self.LASTLOG_STRUCT)
92     self.records = {}
93     with open(fname, 'r') as fp:
94       uid = -1
95       for record in iter(lambda: fp.read(record_size), ''):
96         uid += 1
97         last_login, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
98         if last_login == 0:
99           continue
100         try:
101           self.records[pwd.getpwuid(uid).pw_name] = last_login
102         except KeyError:
103           continue
104
105   def last_login_for_user(self, username):
106     return self.records.get(username, 0)
107
108 class HomedirReminder(object):
109   def __init__(self):
110     self.lastlog = LastLog()
111     self.generate_homedir_list()
112
113   def parse_utmp(self):
114     self.utmp_records = defaultdict(list)
115     for wtmpfile in glob.glob('/var/log/wtmp*'):
116       for entry in utmp.UtmpRecord(wtmpfile):
117         # TODO: Login, does not account for non-idle sessions.
118         self.utmp_records[entry.ut_user].append(entry.ut_tv[0])
119     for username, timestamps in self.utmp_records.iteritems():
120       self.utmp_records[username] = sorted(timestamps)[-1]
121
122   def last_login_for_user(self, username):
123     return self.lastlog.last_login_for_user(username)
124
125   def generate_homedir_list(self):
126     self.homedir_sizes = {}
127     for direntry in glob.glob('/home/*'):
128       username = os.path.basename(direntry)
129       try:
130         pwinfo = pwd.getpwnam(username)
131       except KeyError:
132         if os.path.isdir(direntry):
133           logging.warning('Directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
134         continue
135       homedir = pwinfo.pw_dir
136
137       if username in USER_EXCLUSION_LIST:
138         continue
139       # Ignore errors from du.
140       command = ['/usr/bin/du', '-ms', homedir]
141       p = subprocess.Popen(command,
142                            stdout=subprocess.PIPE,
143                            stderr=subprocess.PIPE)
144       (stdout, stderr) = p.communicate()
145       if p.returncode != 0:
146         logging.info('%s failed:', ' '.join(command))
147         logging.info(stderr)
148       try:
149         size = int(stdout.split('\t')[0])
150       except ValueError:
151         logging.error('Could not convert size output from %s: %s',
152                       ' '.join(command), stdout)
153         continue
154       self.homedir_sizes[username] = size
155
156   def send_mail(self, **kwargs):
157     msg = email.mime.text.MIMEText(MAIL_TEXT.format(**kwargs), _charset='UTF-8')
158     msg['From'] = MAIL_FROM.format(**kwargs)
159     msg['To'] = MAIL_TO.format(**kwargs)
160     if MAIL_CC != "": msg['Cc'] = MAIL_CC.format(**kwargs)
161     if MAIL_REPLYTO != "": msg['Reply-To'] = MAIL_REPLYTO.format(**kwargs)
162     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
163     msg['Precedence'] = "bulk"
164     msg['Auto-Submitted'] = "auto-generated by mail-big-homedirs"
165     p = subprocess.Popen(SENDMAIL, stdin=subprocess.PIPE)
166     p.communicate(msg.as_string())
167     logging.debug(msg.as_string())
168     if p.returncode != 0:
169       raise SendmailError
170
171   def run(self):
172     current_time = time.time()
173     for username, homedir_size in self.homedir_sizes.iteritems():
174       last_login = self.last_login_for_user(username)
175       logging.info('user %s: size %dMB, last login: %d', username, homedir_size, last_login)
176       days_ago = int( (current_time - last_login) / 3600 / 24 )
177
178       reportsize = None
179       for e in REPORT_SIZES:
180         if days_ago > e['days']: reportsize = e['size']
181
182       if reportsize is not None and homedir_size > reportsize:
183         logging.warning('Homedir of user %s is %d and did not login for a while', username, homedir_size)
184         try:
185           name = pwd.getpwnam(username).pw_gecos.decode('utf-8')
186           name = name.split(',', 1)[0]
187         except:
188           name = username
189         self.send_mail(hostname=platform.node(),
190                        username=username,
191                        name=name,
192                        homedir_size=homedir_size,
193                        days_ago=days_ago)
194
195 if __name__ == '__main__':
196   logging.basicConfig()
197   # DEBUG for debugging, ERROR for production.
198   logging.getLogger().setLevel(logging.ERROR)
199   HomedirReminder().run()