treequel 1.0.0

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.
Files changed (74) hide show
  1. data/ChangeLog +354 -0
  2. data/LICENSE +27 -0
  3. data/README +66 -0
  4. data/Rakefile +345 -0
  5. data/Rakefile.local +43 -0
  6. data/bin/treeirb +14 -0
  7. data/bin/treequel +229 -0
  8. data/examples/company-directory.rb +112 -0
  9. data/examples/ldap-monitor.rb +143 -0
  10. data/examples/ldap-monitor/public/css/master.css +328 -0
  11. data/examples/ldap-monitor/public/images/card_small.png +0 -0
  12. data/examples/ldap-monitor/public/images/chain_small.png +0 -0
  13. data/examples/ldap-monitor/public/images/globe_small.png +0 -0
  14. data/examples/ldap-monitor/public/images/globe_small_green.png +0 -0
  15. data/examples/ldap-monitor/public/images/plug.png +0 -0
  16. data/examples/ldap-monitor/public/images/shadows/large-30-down.png +0 -0
  17. data/examples/ldap-monitor/public/images/tick.png +0 -0
  18. data/examples/ldap-monitor/public/images/tick_circle.png +0 -0
  19. data/examples/ldap-monitor/public/images/treequel-favicon.png +0 -0
  20. data/examples/ldap-monitor/views/backends.erb +41 -0
  21. data/examples/ldap-monitor/views/connections.erb +74 -0
  22. data/examples/ldap-monitor/views/databases.erb +39 -0
  23. data/examples/ldap-monitor/views/dump_subsystem.erb +14 -0
  24. data/examples/ldap-monitor/views/index.erb +14 -0
  25. data/examples/ldap-monitor/views/layout.erb +35 -0
  26. data/examples/ldap-monitor/views/listeners.erb +30 -0
  27. data/examples/ldap_state.rb +62 -0
  28. data/lib/treequel.rb +145 -0
  29. data/lib/treequel/branch.rb +589 -0
  30. data/lib/treequel/branchcollection.rb +204 -0
  31. data/lib/treequel/branchset.rb +360 -0
  32. data/lib/treequel/constants.rb +604 -0
  33. data/lib/treequel/directory.rb +541 -0
  34. data/lib/treequel/exceptions.rb +32 -0
  35. data/lib/treequel/filter.rb +704 -0
  36. data/lib/treequel/mixins.rb +325 -0
  37. data/lib/treequel/schema.rb +245 -0
  38. data/lib/treequel/schema/attributetype.rb +252 -0
  39. data/lib/treequel/schema/ldapsyntax.rb +96 -0
  40. data/lib/treequel/schema/matchingrule.rb +124 -0
  41. data/lib/treequel/schema/matchingruleuse.rb +124 -0
  42. data/lib/treequel/schema/objectclass.rb +289 -0
  43. data/lib/treequel/sequel_integration.rb +26 -0
  44. data/lib/treequel/utils.rb +169 -0
  45. data/rake/191_compat.rb +26 -0
  46. data/rake/dependencies.rb +76 -0
  47. data/rake/helpers.rb +434 -0
  48. data/rake/hg.rb +261 -0
  49. data/rake/manual.rb +782 -0
  50. data/rake/packaging.rb +135 -0
  51. data/rake/publishing.rb +318 -0
  52. data/rake/rdoc.rb +30 -0
  53. data/rake/style.rb +62 -0
  54. data/rake/svn.rb +668 -0
  55. data/rake/testing.rb +187 -0
  56. data/rake/verifytask.rb +64 -0
  57. data/rake/win32.rb +190 -0
  58. data/spec/lib/constants.rb +93 -0
  59. data/spec/lib/helpers.rb +100 -0
  60. data/spec/treequel/branch_spec.rb +569 -0
  61. data/spec/treequel/branchcollection_spec.rb +213 -0
  62. data/spec/treequel/branchset_spec.rb +376 -0
  63. data/spec/treequel/directory_spec.rb +487 -0
  64. data/spec/treequel/filter_spec.rb +482 -0
  65. data/spec/treequel/mixins_spec.rb +330 -0
  66. data/spec/treequel/schema/attributetype_spec.rb +237 -0
  67. data/spec/treequel/schema/ldapsyntax_spec.rb +83 -0
  68. data/spec/treequel/schema/matchingrule_spec.rb +158 -0
  69. data/spec/treequel/schema/matchingruleuse_spec.rb +137 -0
  70. data/spec/treequel/schema/objectclass_spec.rb +262 -0
  71. data/spec/treequel/schema_spec.rb +118 -0
  72. data/spec/treequel/utils_spec.rb +49 -0
  73. data/spec/treequel_spec.rb +179 -0
  74. metadata +169 -0
@@ -0,0 +1,43 @@
1
+ #!rake
2
+ # coding: utf-8
3
+
4
+ require 'pathname'
5
+ require 'English'
6
+ require 'pp'
7
+
8
+ # Project-specific tasks
9
+
10
+ #####################################################################
11
+ ### T A S K S
12
+ #####################################################################
13
+
14
+ MANUAL_PAGES = Pathname.glob( MANUALDIR + '**/*.page' )
15
+
16
+ task :local => :check_manual
17
+
18
+ # Check the manual for laika references before committing
19
+ Rake::Task[ 'checkin' ].prerequisites.unshift( 'check_manual' )
20
+
21
+ desc "Check the manual for various problems and/or inconsistencies"
22
+ task :check_manual => MANUAL_PAGES do
23
+ MANUAL_PAGES.each do |page|
24
+ lines = page.readlines
25
+ lines.each_with_index do |line, i|
26
+ if line =~ /\blaika\b/i
27
+ msg = [
28
+ ' ',
29
+ i.zero? ? "" : lines[ i - 1 ],
30
+ ' ',
31
+ $PREMATCH,
32
+ colorize( :bold, :yellow ) { $MATCH },
33
+ $POSTMATCH,
34
+ ' ',
35
+ lines[ i + 1 ]
36
+ ].join
37
+ abort "Page #{page} has a LAIKA reference on line #{i + 1}:\n#{msg}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'irb'
4
+ require 'irb/extend-command'
5
+ require 'irb/cmd/nop'
6
+ require 'treequel'
7
+
8
+
9
+ uri = ARGV.shift or raise "usage: #$0 [LDAPURI]"
10
+ $dir = Treequel.directory( uri )
11
+ $stderr.puts "Directory is in $dir:", ' ' + $dir.inspect
12
+
13
+ IRB.start( $0 )
14
+
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'readline'
5
+ require 'logger'
6
+ require 'shellwords'
7
+ require 'tempfile'
8
+ require 'digest/sha1'
9
+ require 'abbrev'
10
+ require 'treequel'
11
+ require 'treequel/mixins'
12
+ require 'treequel/constants'
13
+
14
+
15
+ class Shell
16
+ include Treequel::Loggable,
17
+ Treequel::Constants::Patterns
18
+
19
+
20
+ ### Create a new shell that will traverse the directory at the specified +uri+.
21
+ def initialize( uri )
22
+ Treequel.logger.level = Logger::WARN
23
+
24
+ @uri = uri
25
+ @quit = false
26
+ @dir = Treequel.directory( @uri )
27
+ @currbranch = @dir
28
+
29
+ @commands = self.find_commands
30
+ @completions = @commands.abbrev
31
+ @command_table = make_command_table( @commands )
32
+ end
33
+
34
+
35
+ ### The command loop: run the shell until the user wants to quit
36
+ def run
37
+ $stderr.puts "Connected to %s" % [ @uri ]
38
+
39
+ self.setup_completion
40
+
41
+ until @quit
42
+ input = Readline.readline( @currbranch.dn + '> ', true )
43
+ self.log.debug "Input is: %p" % [ input ]
44
+
45
+ # EOL makes the shell quit
46
+ if input.nil?
47
+ @quit = true
48
+
49
+ elsif input == ''
50
+ self.log.debug "No command. Re-displaying the prompt."
51
+
52
+ # Parse everything else into command + everything else
53
+ else
54
+ command, *args = Shellwords.shellwords( input )
55
+
56
+ begin
57
+ if meth = @command_table[ command ]
58
+ meth.call( *args )
59
+ else
60
+ self.handle_missing_command( command )
61
+ end
62
+ rescue => err
63
+ $stderr.puts "Error: %s" % [ err.message ]
64
+ err.backtrace.each do |frame|
65
+ self.log.debug " " + frame
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ $stderr.puts "done."
72
+ end
73
+
74
+
75
+ #########
76
+ protected
77
+ #########
78
+
79
+ ### Set up Readline completion
80
+ def setup_completion
81
+ Readline.completion_proc = self.method( :completion_callback ).to_proc
82
+ Readline.completer_word_break_characters = ''
83
+ end
84
+
85
+
86
+ ### Handle completion requests from Readline.
87
+ def completion_callback( input )
88
+ if command = @completions[ input ]
89
+ return []
90
+ end
91
+ end
92
+
93
+
94
+ ### Quit the shell.
95
+ def quit_command( *args )
96
+ $stderr.puts "Okay, exiting."
97
+ @quit = true
98
+ end
99
+
100
+
101
+ LOG_LEVELS = {
102
+ 'debug' => Logger::DEBUG,
103
+ 'info' => Logger::INFO,
104
+ 'warn' => Logger::WARN,
105
+ 'error' => Logger::ERROR,
106
+ 'fatal' => Logger::FATAL,
107
+ }.freeze
108
+ LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
109
+
110
+ ### Set the logging level (if invoked with an argument) or display the current
111
+ ### level (with no argument).
112
+ def log_command( *args )
113
+ newlevel = args.shift
114
+ if newlevel
115
+ if LOG_LEVELS.key?( newlevel )
116
+ Treequel.logger.level = LOG_LEVELS[ newlevel ]
117
+ $stderr.puts "Set log level to: %s" % [ newlevel ]
118
+ else
119
+ levelnames = LOG_LEVEL_NAMES.keys.sort.join(', ')
120
+ raise "Invalid log level %p: valid values are:\n %s" % [ newlevel, levelnames ]
121
+ end
122
+ else
123
+ $stderr.puts "Log level is currently: %s" %
124
+ [ LOG_LEVEL_NAMES[Treequel.logger.level] ]
125
+ end
126
+ end
127
+
128
+
129
+ ### Show the completions hash
130
+ def show_completions_command
131
+ $stderr.puts "Completions:",
132
+ @completions.inspect
133
+ end
134
+
135
+
136
+ ### Display LDIF for the specified RDNs.
137
+ def cat_command( *args )
138
+ args.each do |rdn|
139
+ branch = rdn.split( /\s*,\s*/ ).inject( @currbranch ) do |branch, dnpair|
140
+ attribute, value = dnpair.split( /\s*=\s*/, 2 )
141
+ branch.send( attribute, value )
142
+ end
143
+
144
+ $stdout.puts( branch.to_ldif )
145
+ end
146
+ end
147
+
148
+
149
+ ### List the children of the current branch.
150
+ def ls_command( *args )
151
+ $stdout.puts *@currbranch.children.collect {|b| b.rdn }.sort
152
+ end
153
+
154
+
155
+ ### Change the current working DN to +rdn+.
156
+ def cdn_command( rdn, *args )
157
+ raise "invalid RDN %p" % [ rdn ] unless RELATIVE_DISTINGUISHED_NAME.match( rdn )
158
+
159
+ pairs = rdn.split( /\s*,\s*/ )
160
+ pairs.each do |dnpair|
161
+ self.log.debug " cd to %p" % [ dnpair ]
162
+ attribute, value = dnpair.split( /=/, 2 )
163
+ self.log.debug " changing to %s( %p )" % [ attribute, value ]
164
+ @currbranch = @currbranch.send( attribute, value )
165
+ end
166
+ end
167
+
168
+
169
+ ### Change the current working DN to the current entry's parent.
170
+ def parent_command( *args )
171
+ parent = @currbranch.parent or raise "%s is the root DN" % [ @currbranch.dn ]
172
+
173
+ self.log.debug " changing to %s" % [ parent.dn ]
174
+ @currbranch = parent
175
+ end
176
+
177
+
178
+ ### Edit the entry specified by +rdn+.
179
+ def edit_command( rdn, *args )
180
+ branch = @currbranch.get_child( rdn )
181
+
182
+ fn = Digest::SHA1.hexdigest( rdn )
183
+ tf = Tempfile.new( fn )
184
+ if branch.exists?
185
+ tf.print( )
186
+ end
187
+
188
+
189
+ ### Handle a command from the user that doesn't exist.
190
+ def handle_missing_command( *args )
191
+ command = args.shift || '(testing?)'
192
+ $stderr.puts "Unknown command %p" % [ command ]
193
+ $stderr.puts "Known commands: ", ' ' + @commands.join(', ')
194
+ end
195
+
196
+
197
+ ### Find methods that implement commands and return them in a sorted Array.
198
+ def find_commands
199
+ return self.methods.
200
+ grep( /^(\w+)_command$/ ).
201
+ collect {|mname| mname[/^(\w+)_command$/, 1] }.
202
+ sort
203
+ end
204
+
205
+
206
+ #######
207
+ private
208
+ #######
209
+
210
+ ### Create a command table that maps command abbreviations to the Method object that
211
+ ### implements it.
212
+ def make_command_table( commands )
213
+ table = commands.abbrev
214
+ table.keys.each do |abbrev|
215
+ mname = table.delete( abbrev )
216
+ table[ abbrev ] = self.method( mname + '_command' )
217
+ end
218
+
219
+ return table
220
+ end
221
+
222
+ end
223
+
224
+
225
+ if __FILE__ == $0
226
+ ldapuri = URI( ARGV.shift || 'ldap://localhost' )
227
+ Shell.new( ldapuri ).run
228
+ end
229
+
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'sinatra'
5
+ require 'treequel'
6
+ require 'pathname'
7
+
8
+ # A barebones web-based company directory
9
+
10
+ LDAP_URL = "ldap://ldap.yourcompany.com/dc=yourcompany,dc=com"
11
+
12
+ configure do
13
+ # Borrow the CSS and images from the 'ldap-monitor' example
14
+ set :root, Pathname( __FILE__ ).dirname + 'ldap-monitor'
15
+ end
16
+
17
+ before do
18
+ $stderr.puts "Connecting to #{LDAP_URL}"
19
+ @ldap ||= Treequel.directory( LDAP_URL )
20
+ end
21
+
22
+
23
+ ### GET /
24
+ get '/' do
25
+
26
+ # Get every entry under ou=people that has an email address and sort them
27
+ # by last name, first name, and UID.
28
+ people = @ldap.ou( :people ).filter( :mail ).sort_by do |person|
29
+ [ person[:sn], person[:givenName], person[:uid] ]
30
+ end
31
+
32
+ erb :index,
33
+ :locals => {
34
+ :people => people
35
+ }
36
+ end
37
+
38
+
39
+ ### GET /uid
40
+ get '/:uid' do
41
+
42
+ # Look up the person associated with the given UID, returning NOT FOUND if
43
+ # there isn't any such entry
44
+ uid = params[:uid]
45
+ person = @ldap.ou( :people ).uid( uid )
46
+ halt 404, "No such person" unless person.exists?
47
+
48
+ erb :details,
49
+ :locals => {
50
+ :person => person
51
+ }
52
+ end
53
+
54
+
55
+
56
+ __END__
57
+
58
+ @@layout
59
+ <?xml version="1.0" encoding="UTF-8"?>
60
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
61
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
62
+
63
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
64
+ <head>
65
+ <title>Company Directory</title>
66
+ <link rel="stylesheet" href="/css/master.css" type="text/css" media="screen" title="no title" charset="utf-8" />
67
+ </head>
68
+
69
+ <body>
70
+
71
+ <div id="content">
72
+ <h1>Company Directory</h1>
73
+
74
+ <%= yield %>
75
+
76
+ </div>
77
+
78
+ <div id="footer">Treequel Company Directory Example</div>
79
+ </body>
80
+ </html>
81
+
82
+
83
+ @@index
84
+ <table>
85
+ <thead>
86
+ <tr>
87
+ <th class="odd">Name</th>
88
+ <th class="even">Email</th>
89
+ <th class="odd">Badge #</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody>
93
+ <% people.each_with_index do |person, i| %>
94
+ <% rowclass = i.divmod(2).last.zero? ? "even" : "odd" %>
95
+ <tr class="<%= rowclass %>">
96
+ <td class="odd"><a href="/<%= person[:uid] %>"><%= person[:cn] %></p></td>
97
+ <td class="even"><a href="/<%= person[:uid] %>"><%= person[:mail] %></a></td>
98
+ <td class="odd"><%= person[:employeeNumber] %></td>
99
+ </tr>
100
+ <% end %>
101
+ </tbody>
102
+ </table>
103
+
104
+ @@details
105
+
106
+ <h2>Details for <%= person[:cn] %> <%= person[:sn] %>
107
+ &lt;<%= person[:mail] %>&gt;</h2>
108
+
109
+ <pre>
110
+ <%= person.to_ldif %>
111
+ </pre>
112
+
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ BEGIN {
4
+ require 'pathname'
5
+ basedir = Pathname( __FILE__ ).dirname.parent
6
+ libdir = basedir + 'lib'
7
+
8
+ $LOAD_PATH.unshift( libdir.to_s )
9
+ }
10
+
11
+ require 'rubygems'
12
+ require 'sinatra'
13
+ require 'treequel'
14
+ require 'erb'
15
+
16
+ include ERB::Util
17
+
18
+ # The real data is all in operational attributes, so fetch them by default
19
+ Treequel::Branch.include_operational_attrs = true
20
+
21
+ configure do
22
+ set :root, Pathname( __FILE__ ).dirname + 'ldap-monitor'
23
+ end
24
+
25
+ before do
26
+ @monitor ||= Treequel.directory( 'ldap://localhost/cn=Monitor',
27
+ :bind_dn => 'cn=admin,cn=Monitor', :pass => 'monitor' )
28
+ end
29
+
30
+ helpers do
31
+
32
+ ### Return a string describing the amount of time in the given number of
33
+ ### seconds in terms a human can understand easily.
34
+ def time_delta_string( start_time )
35
+ start = Time.parse( start_time ) or return "some time"
36
+ seconds = Time.now - start
37
+
38
+ return 'less than a minute' if seconds < 60
39
+
40
+ if seconds < 50 * 60
41
+ return "%d minute%s" % [seconds / 60, seconds/60 == 1 ? '' : 's']
42
+ end
43
+
44
+ return 'about an hour' if seconds < 90 * MINUTES
45
+ return "%d hours" % [seconds / HOURS] if seconds < 18 * HOURS
46
+ return 'one day' if seconds < 1 * DAYS
47
+ return 'about a day' if seconds < 2 * DAYS
48
+ return "%d days" % [seconds / DAYS] if seconds < 1 * WEEKS
49
+ return 'about a week' if seconds < 2 * WEEKS
50
+ return "%d weeks" % [seconds / WEEKS] if seconds < 3 * MONTHS
51
+ return "%d months" % [seconds / MONTHS] if seconds < 2 * YEARS
52
+ return "%d years" % [seconds / YEARS]
53
+ end
54
+
55
+ end
56
+
57
+ ### GET /
58
+ get '/' do
59
+ erb :index,
60
+ :locals => {
61
+ :server_info => @monitor.base['monitoredInfo'],
62
+ :datapoints => @monitor.children,
63
+ }
64
+ end
65
+
66
+
67
+ #
68
+ # Subsystems
69
+ #
70
+
71
+ get '/backends' do
72
+ subsystem = @monitor.cn( :backends )
73
+ backends = subsystem.
74
+ filter( :objectClass => :monitoredObject ).
75
+ select( :+, :* )
76
+
77
+ erb :backends,
78
+ :locals => {
79
+ :subsystem => subsystem,
80
+ :backends => backends,
81
+ }
82
+ end
83
+
84
+
85
+ get '/connections' do
86
+ subsystem = @monitor.cn( :connections )
87
+ connections = subsystem.
88
+ filter( :objectClass => :monitorConnection ).
89
+ select( :+, :* )
90
+
91
+ erb :connections,
92
+ :locals => {
93
+ :subsystem => subsystem,
94
+ :total => subsystem.cn( :total ),
95
+ :current => subsystem.cn( :current ),
96
+ :connections => connections.all,
97
+ }
98
+ end
99
+
100
+
101
+ get '/databases' do
102
+ subsystem = @monitor.cn( :databases )
103
+ databases = subsystem.
104
+ filter( :objectClass => :monitoredObject ).
105
+ select( :+, :* )
106
+
107
+ erb :databases,
108
+ :locals => {
109
+ :subsystem => subsystem,
110
+ :databases => databases,
111
+ }
112
+ end
113
+
114
+
115
+ get '/listeners' do
116
+ subsystem = @monitor.cn( :listeners )
117
+ listeners = subsystem.
118
+ filter( :objectClass => :monitoredObject ).
119
+ select( :+, :* )
120
+
121
+ erb :listeners,
122
+ :locals => {
123
+ :subsystem => subsystem,
124
+ :listeners => listeners,
125
+ }
126
+ end
127
+
128
+
129
+ ### Fallback handler for subsystems
130
+ get '/:subsystem' do
131
+ subsystem = @monitor.cn( params[:subsystem] )
132
+ contents = subsystem.
133
+ filter( :objectClass => :monitoredObject ).
134
+ select( :+, :* )
135
+
136
+ erb :dump_subsystem,
137
+ :locals => {
138
+ :subsystem => subsystem,
139
+ :contents => contents,
140
+ }
141
+ end
142
+
143
+