]> git.donarmstrong.com Git - dsa-puppet.git/blob - modules/porterbox/files/mail-big-homedirs
No need to Cc dsa@
[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 #
8 # Permission is hereby granted, free of charge, to any person obtaining a copy
9 # of this software and associated documentation files (the "Software"), to deal
10 # in the Software without restriction, including without limitation the rights
11 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the Software is
13 # furnished to do so, subject to the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be included in
16 # all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24 # THE SOFTWARE.
25
26 from __future__ import print_function
27
28 from collections import defaultdict
29 import email
30 import email.mime.text
31 import glob
32 import logging
33 import os.path
34 import platform
35 import pwd
36 import subprocess
37 import struct
38 import time
39 import StringIO
40 import pwd
41
42 # avoid base64 encoding for utf-8
43 email.charset.add_charset('utf-8', email.charset.SHORTEST, email.charset.QP)
44
45
46 SENDMAIL = ['/usr/sbin/sendmail', '-t', '-oi']
47 #SENDMAIL = ['/bin/cat']
48
49 REPORT_SIZES = [
50     { 'days': 5, 'size': 10240 },
51     { 'days': 10, 'size': 1024 },
52     { 'days': 30, 'size': 100 },
53     { 'days': 60, 'size': 20 },
54     { 'days': 150, 'size': 10 }
55   ]
56 USER_EXCLUSION_LIST = ['lost+found']
57 MAIL_FROM = 'dsa@debian.org'
58 MAIL_TO = '{username}@{hostname}.debian.org'
59 MAIL_CC = ''
60 MAIL_SUBJECT = 'Please clean up ~{username} on {hostname}.debian.org'
61 MAIL_TEXT = u"""\
62 Hi {name},
63
64 you last logged into {hostname} {days_ago} days ago, and your home
65 directory there is {homedir_size}MB in size.
66
67 Disk space on porter boxes is often limited.  Please respect your fellow
68 porters by cleaning up after yourself and deleting schroots and source/build
69 trees in your home directory as soon as feasible.
70
71 If you currently do not use {hostname}, please keep ~{username} under 10MB.
72
73 Thanks,
74 Cron, on behalf of your catherders/admins
75 """
76
77 class Error(Exception):
78   pass
79
80 class SendmailError(Error):
81   pass
82
83 class LastLog(object):
84   LASTLOG_STRUCT = '=L32s256s'
85
86   def __init__(self, fname='/var/log/lastlog'):
87     record_size = struct.calcsize(self.LASTLOG_STRUCT)
88     self.records = {}
89     with open(fname, 'r') as fp:
90       uid = -1
91       for record in iter(lambda: fp.read(record_size), ''):
92         uid += 1
93         last_login, _, _ = list(struct.unpack(self.LASTLOG_STRUCT, record))
94         if last_login == 0:
95           continue
96         try:
97           self.records[pwd.getpwuid(uid).pw_name] = last_login
98         except KeyError:
99           continue
100
101   def last_login_for_user(self, username):
102     return self.records.get(username, 0)
103
104 class HomedirReminder(object):
105   def __init__(self):
106     self.lastlog = LastLog()
107     self.generate_homedir_list()
108
109   def parse_utmp(self):
110     self.utmp_records = defaultdict(list)
111     for wtmpfile in glob.glob('/var/log/wtmp*'):
112       for entry in utmp.UtmpRecord(wtmpfile):
113         # TODO: Login, does not account for non-idle sessions.
114         self.utmp_records[entry.ut_user].append(entry.ut_tv[0])
115     for username, timestamps in self.utmp_records.iteritems():
116       self.utmp_records[username] = sorted(timestamps)[-1]
117
118   def last_login_for_user(self, username):
119     return self.lastlog.last_login_for_user(username)
120
121   def generate_homedir_list(self):
122     self.homedir_sizes = {}
123     for direntry in glob.glob('/home/*'):
124       username = os.path.basename(direntry)
125       try:
126         pwinfo = pwd.getpwnam(username)
127       except KeyError:
128         if os.path.isdir(direntry):
129           logging.warning('Directory %s exists on %s but there is no %s user', direntry, platform.node(), username)
130         continue
131       homedir = pwinfo.pw_dir
132
133       if username in USER_EXCLUSION_LIST:
134         continue
135       # Ignore errors from du.
136       command = ['/usr/bin/du', '-ms', homedir]
137       p = subprocess.Popen(command,
138                            stdout=subprocess.PIPE,
139                            stderr=subprocess.PIPE)
140       (stdout, stderr) = p.communicate()
141       if p.returncode != 0:
142         logging.info('%s failed:', ' '.join(command))
143         logging.info(stderr)
144       try:
145         size = int(stdout.split('\t')[0])
146       except ValueError:
147         logging.error('Could not convert size output from %s: %s',
148                       ' '.join(command), stdout)
149         continue
150       self.homedir_sizes[username] = size
151
152   def send_mail(self, **kwargs):
153     msg = email.mime.text.MIMEText(MAIL_TEXT.format(**kwargs), _charset='UTF-8')
154     msg['From'] = MAIL_FROM.format(**kwargs)
155     msg['To'] = MAIL_TO.format(**kwargs)
156     msg['Cc'] = MAIL_CC.format(**kwargs)
157     msg['Subject'] = MAIL_SUBJECT.format(**kwargs)
158     p = subprocess.Popen(SENDMAIL, stdin=subprocess.PIPE)
159     p.communicate(msg.as_string())
160     logging.debug(msg.as_string())
161     if p.returncode != 0:
162       raise SendmailError
163
164   def run(self):
165     current_time = time.time()
166     for username, homedir_size in self.homedir_sizes.iteritems():
167       last_login = self.last_login_for_user(username)
168       logging.info('user %s: size %dMB, last login: %d', username, homedir_size, last_login)
169       days_ago = int( (current_time - last_login) / 3600 / 24 )
170
171       reportsize = None
172       for e in REPORT_SIZES:
173         if days_ago > e['days']: reportsize = e['size']
174
175       if reportsize is not None and homedir_size > reportsize:
176         logging.warning('Homedir of user %s is %d and did not login for a while', username, homedir_size)
177         try:
178           name = pwd.getpwnam(username).pw_gecos.decode('utf-8')
179           name = name.split(',', 1)[0]
180         except:
181           name = username
182         self.send_mail(hostname=platform.node(),
183                        username=username,
184                        name=name,
185                        homedir_size=homedir_size,
186                        days_ago=days_ago)
187
188 if __name__ == '__main__':
189   logging.basicConfig()
190   # DEBUG for debugging, ERROR for production.
191   logging.getLogger().setLevel(logging.ERROR)
192   HomedirReminder().run()