whimsy-asf 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ module ASF
2
+
3
+ class Authorization
4
+ include Enumerable
5
+
6
+ def self.find_by_id(value)
7
+ new.select {|auth, ids| ids.include? value}.map(&:first)
8
+ end
9
+
10
+ def initialize(file='asf')
11
+ @file = file
12
+ end
13
+
14
+ def each
15
+ auth = ASF::SVN['infra/infrastructure/trunk/subversion/authorization']
16
+ File.read("#{auth}/#{@file}-authorization-template").
17
+ scan(/^([-\w]+)=(\w.*)$/).each do |pmc, ids|
18
+ yield pmc, ids.split(',')
19
+ end
20
+ end
21
+ end
22
+
23
+ class Person
24
+ def auth
25
+ @auths ||= ASF::Authorization.find_by_id(name)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,124 @@
1
+ module ASF
2
+
3
+ class Base
4
+ end
5
+
6
+ class Committee < Base
7
+ attr_accessor :info, :names, :emeritus, :report
8
+ def initialize(*args)
9
+ @info = []
10
+ @emeritus = []
11
+ super
12
+ end
13
+
14
+ # mapping of committee names to canonical names (generally from ldap)
15
+ # See also www/roster/committee.cgi
16
+ @@aliases = Hash.new {|hash, name| name}
17
+ @@aliases.merge! \
18
+ 'community development' => 'comdev',
19
+ 'conference planning' => 'concom',
20
+ 'conferences' => 'concom',
21
+ 'http server' => 'httpd',
22
+ 'httpserver' => 'httpd',
23
+ 'java community process' => 'jcp',
24
+ 'lucene.net' => 'lucenenet',
25
+ 'quetzalcoatl' => 'quetz',
26
+ 'security team' => 'security',
27
+ 'open climate workbench' => 'climate',
28
+ 'c++ standard library' => 'stdcxx',
29
+ 'travel assistance' => 'tac',
30
+ 'traffic server' => 'trafficserver',
31
+ 'web services' => 'ws',
32
+ 'xml graphics' => 'xmlgraphics'
33
+
34
+ @@namemap = Proc.new do |name|
35
+ cname = @@aliases[name.downcase]
36
+ cname
37
+ end
38
+
39
+ def self.load_committee_info
40
+ board = ASF::SVN['private/committers/board']
41
+ file = "#{board}/committee-info.txt"
42
+ if @committee_info and File.mtime(file) == @committee_mtime
43
+ return @committee_info
44
+ end
45
+ @committee_mtime = File.mtime(file)
46
+
47
+ info = File.read(file).split(/^\* /)
48
+ head, report = info.shift.split(/^\d\./)[1..2]
49
+ head.scan(/^\s+(\w.*?)\s\s+.*<(\w+)@apache\.org>/).each do |name, id|
50
+ find(name).chair = ASF::Person.find(id)
51
+ end
52
+ @nonpmcs = head.sub(/.*?also has/m,'').
53
+ scan(/^\s+(\w.*?)\s\s+.*<\w+@apache\.org>/).flatten.uniq.
54
+ map {|name| find(name)}
55
+
56
+ info.each do |roster|
57
+ committee = find(@@namemap.call(roster[/(\w.*?)\s+\(/,1]))
58
+ roster.gsub! /^.*\(\s*emeritus\s*\).*/i do |line|
59
+ committee.emeritus += line.scan(/<(.*?)@apache\.org>/).flatten
60
+ ''
61
+ end
62
+ committee.info = roster.scan(/<(.*?)@apache\.org>/).flatten
63
+ committee.names = Hash[roster.gsub(/\(\w+\)/, '').
64
+ scan(/^\s*(.*?)\s*<(.*?)@apache\.org>/).map {|list| list.reverse}]
65
+ end
66
+
67
+ report.scan(/^([^\n]+)\n---+\n(.*?)\n\n/m).each do |period, committees|
68
+ committees.scan(/^ \s*(.*)/).each do |committee|
69
+ committee, comment = committee.first.split(/\s+#\s+/,2)
70
+ committee = find(committee)
71
+ if comment
72
+ committee.report = "#{period}: #{comment}"
73
+ elsif period == 'Next month'
74
+ committee.report = 'Every month'
75
+ else
76
+ committee.report = period
77
+ end
78
+ end
79
+ end
80
+
81
+ @committee_info = ASF::Committee.collection.values
82
+ end
83
+
84
+ def self.nonpmcs
85
+ @nonpmcs
86
+ end
87
+
88
+ def self.find(name)
89
+ result = super(@@aliases[name.downcase])
90
+ result.display_name = name if name =~ /[A-Z]/
91
+ result
92
+ end
93
+
94
+ def chair
95
+ Committee.load_committee_info
96
+ @chair
97
+ end
98
+
99
+ def display_name
100
+ Committee.load_committee_info
101
+ @display_name || name
102
+ end
103
+
104
+ def display_name=(name)
105
+ @display_name ||= name
106
+ end
107
+
108
+ def chair=(person)
109
+ @chair = person
110
+ end
111
+
112
+ def info=(list)
113
+ @info = list
114
+ end
115
+
116
+ def names=(hash)
117
+ @names = hash
118
+ end
119
+
120
+ def nonpmc?
121
+ Committee.nonpmcs.include? self
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,67 @@
1
+ module ASF
2
+
3
+ class ICLA
4
+ include Enumerable
5
+
6
+ def self.find_by_id(value)
7
+ return if value == 'notinavail'
8
+ new.each do |id, name, email|
9
+ if id == value
10
+ return Struct.new(:id, :name, :email).new(id, name, email)
11
+ end
12
+ end
13
+ nil
14
+ end
15
+
16
+ def self.find_by_email(value)
17
+ value = value.downcase
18
+ ICLA.new.each do |id, name, email|
19
+ if email.downcase == value
20
+ return Struct.new(:id, :name, :email).new(id, name, email)
21
+ end
22
+ end
23
+ nil
24
+ end
25
+
26
+ def self.availids
27
+ return @availids if @availids
28
+ availids = []
29
+ ICLA.new.each {|id, name, email| availids << id unless id == 'notinavail'}
30
+ @availids = availids
31
+ end
32
+
33
+ def each(&block)
34
+ officers = ASF::SVN['private/foundation/officers']
35
+ iclas = File.read("#{officers}/iclas.txt")
36
+ iclas.scan(/^([-\w]+):.*?:(.*?):(.*?):/).each(&block)
37
+ end
38
+ end
39
+
40
+ class Person
41
+ def icla
42
+ @icla ||= ASF::ICLA.find_by_id(name)
43
+ end
44
+
45
+ def icla?
46
+ ICLA.availids.include? name
47
+ end
48
+ end
49
+
50
+ # Search archive for historical records of people who were committers
51
+ # but never submitted an ICLA (some of which are still ASF members or
52
+ # members of a PMC).
53
+ def self.search_archive_by_id(value)
54
+ require 'net/http'
55
+ require 'nokogiri'
56
+ historical_committers = 'http://people.apache.org/~rubys/committers.html'
57
+ doc = Nokogiri::HTML(Net::HTTP.get(URI.parse(historical_committers)))
58
+ doc.search('tr').each do |tr|
59
+ tds = tr.search('td')
60
+ next unless tds.length == 3
61
+ return tds[1].text if tds[0].text == value
62
+ end
63
+ nil
64
+ rescue
65
+ nil
66
+ end
67
+ end
@@ -0,0 +1,232 @@
1
+ require 'wunderbar'
2
+ require 'rubygems'
3
+ require 'ldap'
4
+
5
+ module ASF
6
+
7
+ # determine whether or not the LDAP API can be used
8
+ def self.init_ldap
9
+ @ldap = nil
10
+ begin
11
+ conf = '/etc/ldap/ldap.conf'
12
+ host = File.read(conf).scan(/^uri\s+ldaps:\/\/(\S+?):(\d+)/i).first
13
+ if host
14
+ Wunderbar.info "Connecting to LDAP server: [ldaps://#{host[0]}:#{host[1]}]"
15
+ end
16
+ rescue Errno::ENOENT
17
+ host = nil
18
+ Wunderbar.error "No LDAP server defined, there must be a LDAP ldaps:// URI in /etc/ldap/ldap.conf"
19
+ end
20
+ begin
21
+ @ldap = LDAP::SSLConn.new(host.first, host.last.to_i) if host
22
+ rescue LDAP::ResultError=>re
23
+ Wunderbar.error "Error binding to LDAP server: message: ["+ re.message + "]"
24
+ end
25
+ end
26
+
27
+ # search with a scope of one
28
+ def self.search_one(base, filter, attrs=nil)
29
+ init_ldap unless defined? @ldap
30
+ return [] unless @ldap
31
+
32
+ Wunderbar.info "ldapsearch -x -LLL -b #{base} -s one #{filter} " +
33
+ "#{[attrs].flatten.join(' ')}"
34
+
35
+ begin
36
+ result = @ldap.search2(base, LDAP::LDAP_SCOPE_ONELEVEL, filter, attrs)
37
+ rescue
38
+ result = []
39
+ end
40
+
41
+ result.map! {|hash| hash[attrs]} if String === attrs
42
+
43
+ result
44
+ end
45
+
46
+ def self.pmc_chairs
47
+ @pmc_chairs ||= Service.find('pmc-chairs').members
48
+ end
49
+
50
+ def self.committers
51
+ @committers ||= Group.find('committers').members
52
+ end
53
+
54
+ def self.members
55
+ @members ||= Group.find('member').members
56
+ end
57
+
58
+ class Base
59
+ attr_reader :name
60
+
61
+ def self.base
62
+ @base
63
+ end
64
+
65
+ def base
66
+ self.class.base
67
+ end
68
+
69
+ def self.collection
70
+ @collection ||= Hash.new
71
+ end
72
+
73
+ def self.[] name
74
+ collection[name] || new(name)
75
+ end
76
+
77
+ def self.find name
78
+ collection[name] || new(name)
79
+ end
80
+
81
+ def self.new name
82
+ collection[name] || super
83
+ end
84
+
85
+ def initialize name
86
+ self.class.collection[name] = self
87
+ @name = name
88
+ end
89
+
90
+ unless Object.respond_to? :id
91
+ def id
92
+ @name
93
+ end
94
+ end
95
+ end
96
+
97
+ class LazyHash < Hash
98
+ def initialize(&initializer)
99
+ @initializer = initializer
100
+ end
101
+
102
+ def load
103
+ return unless @initializer
104
+ merge! @initializer.call || {}
105
+ @initializer = super
106
+ end
107
+
108
+ def [](key)
109
+ result = super
110
+ if not result and not keys.include? key and @initializer
111
+ merge! @initializer.call || {}
112
+ @initializer = nil
113
+ result = super
114
+ end
115
+ result
116
+ end
117
+ end
118
+
119
+ class Person < Base
120
+ @base = 'ou=people,dc=apache,dc=org'
121
+
122
+ def self.list(filter='uid=*')
123
+ ASF.search_one(base, filter, 'uid').flatten.map {|uid| find(uid)}
124
+ end
125
+
126
+ # pre-fetch a given attribute, for a given list of people
127
+ def self.preload(attributes, people={})
128
+ attributes = [attributes].flatten
129
+
130
+ if people.empty?
131
+ filter = "(|#{attributes.map {|attribute| "(#{attribute}=*)"}.join})"
132
+ else
133
+ filter = "(|#{people.map {|person| "(uid=#{person.name})"}.join})"
134
+ end
135
+
136
+ zero = Hash[attributes.map {|attribute| [attribute,nil]}]
137
+
138
+ data = ASF.search_one(base, filter, attributes + ['uid'])
139
+ data = Hash[data.map! {|hash| [find(hash['uid'].first), hash]}]
140
+ data.each {|person, hash| person.attrs.merge!(zero.merge(hash))}
141
+
142
+ if people.empty?
143
+ (collection.values - data.keys).each do |person|
144
+ person.attrs.merge! zero
145
+ end
146
+ end
147
+ end
148
+
149
+ def attrs
150
+ @attrs ||= LazyHash.new {ASF.search_one(base, "uid=#{name}").first}
151
+ end
152
+
153
+ def public_name
154
+ cn = [attrs['cn']].flatten.first
155
+ cn.force_encoding('utf-8') if cn.respond_to? :force_encoding
156
+ return cn if cn
157
+ return icla.name if icla
158
+ ASF.search_archive_by_id(name)
159
+ end
160
+
161
+ def asf_member?
162
+ ASF::Member.status[name] or ASF.members.include? self
163
+ end
164
+
165
+ def banned?
166
+ not attrs['loginShell'] or attrs['loginShell'].include? "/usr/bin/false"
167
+ end
168
+
169
+ def mail
170
+ attrs['mail'] || []
171
+ end
172
+
173
+ def alt_email
174
+ attrs['asf-altEmail'] || []
175
+ end
176
+
177
+ def pgp_key_fingerprints
178
+ attrs['asf-pgpKeyFingerprint']
179
+ end
180
+
181
+ def urls
182
+ attrs['asf-personalURL'] || []
183
+ end
184
+
185
+ def committees
186
+ Committee.list("member=uid=#{name},#{base}")
187
+ end
188
+
189
+ def groups
190
+ Group.list("memberUid=#{name}")
191
+ end
192
+ end
193
+
194
+ class Group < Base
195
+ @base = 'ou=groups,dc=apache,dc=org'
196
+
197
+ def self.list(filter='cn=*')
198
+ ASF.search_one(base, filter, 'cn').flatten.map {|cn| find(cn)}
199
+ end
200
+
201
+ def members
202
+ ASF.search_one(base, "cn=#{name}", 'memberUid').flatten.
203
+ map {|uid| Person.find(uid)}
204
+ end
205
+ end
206
+
207
+ class Committee < Base
208
+ @base = 'ou=pmc,ou=committees,ou=groups,dc=apache,dc=org'
209
+
210
+ def self.list(filter='cn=*')
211
+ ASF.search_one(base, filter, 'cn').flatten.map {|cn| Committee.find(cn)}
212
+ end
213
+
214
+ def members
215
+ ASF.search_one(base, "cn=#{name}", 'member').flatten.
216
+ map {|uid| Person.find uid[/uid=(.*?),/,1]}
217
+ end
218
+ end
219
+
220
+ class Service < Base
221
+ @base = 'ou=groups,ou=services,dc=apache,dc=org'
222
+
223
+ def self.list(filter='cn=*')
224
+ ASF.search_one(base, filter, 'cn').flatten
225
+ end
226
+
227
+ def members
228
+ ASF.search_one(base, "cn=#{name}", 'member').flatten.
229
+ map {|uid| Person.find uid[/uid=(.*?),/,1]}
230
+ end
231
+ end
232
+ end