whimsy-asf 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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