48a70fecef7333a45f25990d5f41c4b9aa1fe020
[myplc.git] / support-scripts / gen_aliases.py
1 #!/usr/bin/python
2 #
3 # Generates:
4 #
5 # 1. /etc/mail/aliasesPL, /etc/mail/virtusertable
6 #    <slicename>@slices.planet-lab.org: all users and PIs of a slice
7 #    pi-<loginbase>@sites.planet-lab.org: all PIs at a site
8 #    tech-<loginbase>@sites.planet-lab.org: all techs at a site
9 # 2. /etc/mail/local-host-names
10 #    Additional local e-mail FQDNs
11 #
12 # Updates:
13 #
14 # 1. announce@lists.planet-lab.org membership from the database and from
15 #    the membership of announce-additions@lists.planet-lab.org
16 # 2. {pis,techs}@lists.planet-lab.org membership from the database.
17 # 3. cvs@lists.planet-lab.org accept_these_nonmembers from
18 #    /usr/share/doc/plc/accounts.txt
19 #
20 # N.B.:
21 #
22 # 1. See plc/mail/sendmail-mail.mc for the ALIAS_FILE definition that
23 #    includes /etc/mail/aliasesPL.
24 #
25 # Mark Huang <mlhuang@cs.princeton.edu>
26 # Copyright (C) 2005 The Trustees of Princeton University
27 #
28 # $Id$
29 #
30
31 import os, sys
32 import plcapilib
33 import tempfile
34 from sets import Set
35
36 # Procmail command for aliases. See plc/mail/procmailrc for how these
37 # two scripts interact with each other.
38 procmail = "|/usr/bin/procmail -m /etc/mail/procmailrc"
39
40 # Parse additional options
41 shortopts = "n"
42 longopts = ["dryrun"]
43 moreusage = """
44 usage: %s [OPTION]... [slice|pi|tech] [slicename|sitename]
45
46 gen_aliases.py options:
47         -n              Dry run, do not sync memberships or write files
48 """ % sys.argv[0]
49
50 # Debug
51 dryrun = False
52
53 (plcapi, moreopts, argv) = plcapilib.plcapi(globals(), sys.argv, shortopts, longopts, moreusage)
54 for opt, optval in moreopts.iteritems():
55     if opt == "-n" or opt == "--dryrun":
56         dryrun = True
57
58 # Parse /usr/share/doc/plc/accounts.txt (or /etc/passwd)
59 def passwd(path = '/etc/passwd'):
60     entries = []
61
62     required = ['account', 'password', 'uid', 'gid', 'gecos', 'directory', 'shell']
63     optional = ['email', 'servers']
64
65     for line in file(path):
66         # Comment
67         if line.strip() == '' or line[0] in '#':
68             continue
69         # princeton_mlh:x:...
70         fields = line.strip().split(':')
71         if len(fields) < len(required):
72             continue
73         # {'account': 'princeton_mlh', 'password': 'x', ...}
74         entries.append(dict(zip(required + optional, fields)))
75
76     return entries
77
78 def GetPIs(site_id_or_login_base):
79     pis = []
80     for site in GetSites([site_id_or_login_base], ['person_ids']):
81         persons = GetPersons(site['person_ids'], ['email', 'roles', 'enabled'])
82         pis += filter(lambda person: 'pi' in person['roles'] and person['enabled'], persons)
83     if pis:
84         return [pi['email'] for pi in pis]
85     return ["NO-pi"]
86
87 def GetTechs(login_base):
88     techs = []
89     for site in GetSites([login_base], ['person_ids']):
90         persons = GetPersons(site['person_ids'], ['email', 'roles', 'enabled'])
91         techs += filter(lambda person: 'tech' in person['roles'] and person['enabled'], persons)
92     if techs:
93         return [tech['email'] for tech in techs]
94     return ["NO-tech"]
95
96 def GetSliceUsers(name):
97     # Get the users of the slice
98     enabledpersons = []
99     users = []
100     for slice in GetSlices([name], ['site_id', 'person_ids']):
101         persons = GetPersons(slice['person_ids'], ['email', 'enabled'])
102         enabledpersons += filter(lambda person: person['enabled'], persons)
103         users += [person['email'] for person in enabledpersons]
104         # Add all the PIs for the site
105         users += GetPIs(slice['site_id'])
106     # Remove duplicates and sort
107     users = list(Set(users))
108     users.sort()
109     return users
110
111 # Called every 10 minutes without arguments to sync
112 # {announce,pis,techs,cvs}@lists.planet-lab.org memberships and to
113 # regenerate /etc/mail/{aliasesPL,virtusertable,local-host-names}.
114 if len(argv) == 0:
115     flags = ""
116     if dryrun:
117         flags += "-n"
118         local_host_names = virtusertable = aliases = cvs_config = sys.stdout
119     else:
120         local_host_names = file("local-host-names", 'w')
121         virtusertable = file("virtusertable", 'w')
122         aliases = file("aliasesPL", 'w')
123         cvs_config = tempfile.NamedTemporaryFile()
124
125     # Parse /usr/share/doc/plc/accounts.txt. XXX Should probably make
126     # CVS access a DB property.
127     cvs_nonmembers = []
128     for pw in passwd('/usr/share/doc/plc/accounts.txt'):
129         # Only allow posts from those with implicit or explicit access to
130         # all servers or explicit access to the CVS server
131         if pw.has_key('servers') and pw['servers'] not in ['*', 'cvs']:
132             continue
133
134         # System users are those with UIDs greater than 2000 and less than 3000
135         if int(pw['uid']) >= 2000 and int(pw['uid']) < 3000:
136             continue
137
138         cvs_nonmembers.append("'" + pw['account'] + "@planet-lab.org" + "'")
139
140     # Update accept_these_nonmembers property of the CVS mailing
141     # list. This ensures that those with access to the CVS server are
142     # able to post loginfo messages when they check in files. The CVS
143     # server's sendmail setup is configured to masquerade as
144     # @planet-lab.org.
145     cvs_config.write("accept_these_nonmembers = [" + ", ".join(cvs_nonmembers) + "]\n")
146     cvs_config.flush()
147     if not dryrun:
148         config_list = os.popen("/var/mailman/bin/config_list -i %s cvs" % cvs_config.name)
149         if config_list.close() is not None:
150             raise Exception, "/var/mailman/bin/config_list cvs failed"
151
152     # Get all emails
153     announce = []
154     pis = []
155     techs = []
156     for person in GetPersons({'enabled': True}, ['person_id', 'email', 'roles']):
157         announce.append(person['email'])
158         if 'pi' in person['roles']:
159             pis.append(person['email'])
160         if 'tech' in person['roles']:
161             techs.append(person['email'])
162
163     # Generate announce@lists.planet-lab.org membership
164
165     # Merge in membership of announce-additions
166     list_members = os.popen("/var/mailman/bin/list_members announce-additions", 'r')
167     announce += map(lambda line: line.strip(), list_members.readlines())
168     list_members.close()
169     # Remove duplicates and sort
170     announce = list(Set(announce))
171     announce.sort()
172     # Update membership
173     sync_members = os.popen("/var/mailman/bin/sync_members %s -w=yes -g=yes -f - announce" % flags, 'w')
174     sync_members.write("\n".join(announce))
175     if sync_members.close() is not None:
176         raise Exception, "/var/mailman/bin/sync_members announce failed"
177
178     # Generate {pis,techs}@lists.planet-lab.org membership
179
180     # Remove duplicates and sort
181     pis = list(Set(pis))
182     pis.sort()
183     techs = list(Set(techs))
184     techs.sort()
185     # Update membership
186     sync_members = os.popen("/var/mailman/bin/sync_members %s -w=no -g=no -f - pis" % flags, 'w')
187     sync_members.write("\n".join(pis))
188     if sync_members.close() is not None:
189         raise Exception, "/var/mailman/bin/sync_members pis failed"
190     sync_members = os.popen("/var/mailman/bin/sync_members %s -w=no -g=no -f - techs" % flags, 'w')
191     sync_members.write("\n".join(techs))
192     if sync_members.close() is not None:
193         raise Exception, "/var/mailman/bin/sync_members techs failed"
194
195     # Generate local-host-names file
196     local_host_names.write("planet-lab.org\n")
197     local_host_names.write("slices.planet-lab.org\n")
198     local_host_names.write("sites.planet-lab.org\n")
199
200     # Generate SLICENAME@slices.planet-lab.org mapping and
201     # slice-SLICENAME alias
202     for slice in GetSlices(None, ['name']):
203         name = slice['name']
204         virtusertable.write("%s@slices.planet-lab.org\tslice-%s\n" % \
205                             (name, name))
206         aliases.write("slice-%s: \"%s slice %s\"\n" % \
207                       (name, procmail, name))
208
209     # Generate {pi,tech}-LOGINBASE@sites.planet-lab.org and
210     # {pi,tech}-LOGINBASE alias
211     for site in GetSites(None, ['login_base']):
212         for prefix in ['pi', 'tech']:
213             # This is probably unnecessary since the mapping is 1-1
214             virtusertable.write("%s-%s@sites.planet-lab.org\t%s-%s\n" % \
215                                 (prefix, site['login_base'], prefix, site['login_base']))
216             aliases.write("%s-%s: \"%s %s %s\"\n" % \
217                           (prefix, site['login_base'], procmail, prefix, site['login_base']))
218
219     # Generate special cases. all-pi and all-tech used to be aliases
220     # for all PIs and all techs. They are now mailing lists like
221     # announce. NO-pi and NO-tech notify support that a site is
222     # missing one or the other and are used by some scripts.
223     aliases.write("all-pi: pis\n")
224     aliases.write("all-tech: techs\n")
225     aliases.write("NO-pi: support\n")
226     aliases.write("NO-tech: support\n")
227
228     if not dryrun:
229         local_host_names.close()
230         virtusertable.close()
231         aliases.close()
232         cvs_config.close()
233
234 # Otherwise, print space-separated list of aliases
235 elif len(argv) == 2:
236     if argv[0] == "slice":
237         print " ".join(GetSliceUsers(argv[1]))
238     elif argv[0] == "pi":
239         print " ".join(GetPIs(argv[1]))
240     elif argv[0] == "tech":
241         print " ".join(GetTechs(argv[1]))
242
243 else:
244     plcapi.usage(moreusage)
245     sys.exit(1)