treequel 1.9.1 → 1.10.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.
data/bin/treewhat DELETED
@@ -1,394 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'yaml'
4
- require 'abbrev'
5
- require 'trollop'
6
- require 'highline'
7
- require 'shellwords'
8
-
9
- # Work around MacOS X's vendored 'sysexits' that does the same thing,
10
- # but with a different API
11
- gem 'sysexits'
12
- require 'sysexits'
13
-
14
- require 'treequel'
15
- require 'treequel/mixins'
16
- require 'treequel/constants'
17
-
18
-
19
- # A tool for displaying information about a directory's records and schema artifacts.
20
- class Treequel::What
21
- extend Sysexits
22
- include Sysexits,
23
- Treequel::Loggable,
24
- Treequel::ANSIColorUtilities,
25
- Treequel::Constants::Patterns,
26
- Treequel::HashUtilities
27
-
28
- COLOR_SCHEME = HighLine::ColorScheme.new do |scheme|
29
- scheme[:header] = [ :bold, :yellow ]
30
- scheme[:subheader] = [ :bold, :white ]
31
- scheme[:key] = [ :white ]
32
- scheme[:value] = [ :bold, :white ]
33
- scheme[:error] = [ :red ]
34
- scheme[:warning] = [ :yellow ]
35
- scheme[:message] = [ :reset ]
36
- end
37
-
38
-
39
- ### Run the utility with the given +args+.
40
- def self::run( args )
41
- HighLine.color_scheme = COLOR_SCHEME
42
-
43
- oparser = self.make_option_parser
44
- opts = Trollop.with_standard_exception_handling( oparser ) do
45
- oparser.parse( args )
46
- end
47
-
48
- pattern = oparser.leftovers.join( ' ' ) if oparser.leftovers
49
-
50
- self.new( opts ).run( pattern )
51
- exit :ok
52
-
53
- rescue => err
54
- Treequel.logger.fatal "Oops: %s: %s" % [ err.class.name, err.message ]
55
- Treequel.logger.debug { ' ' + err.backtrace.join("\n ") }
56
-
57
- exit :software_error
58
- end
59
-
60
-
61
- ### Create and configure a command-line option parser (a Trollop::Parser) for the command.
62
- def self::make_option_parser
63
- progname = File.basename( $0 )
64
- default_directory = Treequel.directory_from_config
65
- loglevels = Treequel::LOG_LEVELS.
66
- sort_by {|name,lvl| lvl }.
67
- collect {|name,lvl| name.to_s }.
68
- join( ', ' )
69
-
70
- return Trollop::Parser.new do
71
- banner "Usage: #{progname} [OPTIONS] [PATTERN]"
72
-
73
- text ''
74
- text %{Search for an object in an LDAP directory that matches PATTERN and } +
75
- %{display some information about it.}
76
- text ''
77
- text %{The PATTERN can be the DN (or RDN relative to the base) of an entry, } +
78
- %{a search filter, or the name of an artifact in the directory's schema, } +
79
- %{such as an objectClass, matching rule, syntax, etc.}
80
- text ''
81
- text %{If no PATTERN is specified, general information about the directory is } +
82
- %{output instead.}
83
- text ''
84
-
85
- text 'Connection Options:'
86
- opt :ldapurl, "Specify the directory to connect to.",
87
- :default => default_directory.uri.to_s
88
- text ''
89
-
90
- text 'Display Options:'
91
- opt :attrtypes, "Show attribute types for objects that have them."
92
- opt :objectclasses, "Show objectclasses for objects that have them."
93
- opt :syntaxes, "Show syntaxes for objects that have them."
94
- opt :matching_rules, "Show matching rules for objects that have them."
95
- opt :matching_rule_uses, "Show matching rule uses for objects that have them."
96
- opt :all, "Show any of the above that are applicable."
97
- text ''
98
-
99
- text 'Other Options:'
100
- opt :debug, "Turn debugging on. Also sets the --loglevel to 'debug'."
101
- opt :loglevel, "Set the logging level. Must be one of: #{loglevels}",
102
- :default => Treequel::LOG_LEVEL_NAMES[ Treequel.logger.level ]
103
- opt :binddn, "The DN of the user to bind as. Defaults to anonymous binding.",
104
- :type => :string
105
- end
106
- end
107
-
108
-
109
- #################################################################
110
- ### I N S T A N C E M E T H O D S
111
- #################################################################
112
-
113
- ### Create a new instance of the command and set it up with the given
114
- ### +options+.
115
- def initialize( options )
116
- Treequel.logger.formatter = Treequel::ColorLogFormatter.new( Treequel.logger )
117
-
118
- if options.debug
119
- $DEBUG = true
120
- $VERBOSE = true
121
- Treequel.logger.level = Logger::DEBUG
122
- elsif options.loglevel
123
- Treequel.logger.level = Treequel::LOG_LEVELS[ options.loglevel ]
124
- end
125
-
126
- @options = options
127
- if @options.all?
128
- @options[:attrtypes] =
129
- @options[:objectclasses] =
130
- @options[:syntaxes] =
131
- @options[:matching_rules] =
132
- @options[:matching_rule_uses] =
133
- true
134
- end
135
-
136
- @directory = Treequel.directory( options.ldapurl )
137
- @prompt = HighLine.new
138
-
139
- @prompt.wrap_at = @prompt.output_cols - 10
140
-
141
- self.log.debug "Created new treewhat command object for %s" % [ @directory ]
142
- end
143
-
144
-
145
- ######
146
- public
147
- ######
148
-
149
- # The LDAP directory the command will connect to
150
- attr_reader :directory
151
-
152
- # The Trollop options hash the command will read its configuration from
153
- attr_reader :options
154
-
155
- # The HighLine object to use for prompting and displaying stuff
156
- attr_reader :prompt
157
-
158
-
159
- ### Display an +object+ highlighted as a header.
160
- def print_header( object )
161
- self.prompt.say( self.prompt.color(object.to_s, :header) )
162
- end
163
-
164
-
165
- ### Run the command with the specified +pattern+.
166
- def run( pattern=nil )
167
- self.log.debug "Running with pattern = %p" % [ pattern ]
168
-
169
- self.bind_to_directory if self.options.binddn
170
-
171
- case pattern
172
-
173
- # No argument
174
- when NilClass, ''
175
- self.show_directory_overview
176
-
177
- # DN/RDN or filter if it contains a '='
178
- when /=/
179
- self.show_entry( pattern )
180
-
181
- # Otherwise, try to find a schema item that matches
182
- else
183
- self.show_schema_artifact( pattern )
184
- end
185
-
186
- end
187
-
188
-
189
- ### Prompt for a password and then bind to the command's directory using the binddn in
190
- ### the options.
191
- def bind_to_directory
192
- binddn = self.options.binddn or
193
- raise ArgumentError, "no binddn in the options hash?!"
194
- self.log.debug "Attempting to bind to the directory as %s" % [ binddn ]
195
-
196
- pass = self.prompt.ask( "password: " ) {|q| q.echo = '*' }
197
- user = Treequel::Branch.new( self.directory, binddn )
198
-
199
- self.directory.bind_as( user, pass )
200
- self.log.debug " bound as %s" % [ user ]
201
-
202
- return true
203
- end
204
-
205
-
206
- ### Show general information about the directory if the user doesn't give a pattern on
207
- ### the command line.
208
- def show_directory_overview
209
- pr = self.prompt
210
- dir = self.directory
211
-
212
- self.print_header( dir.uri.to_s )
213
- pr.say( "\n" )
214
- pr.say( dir.schema.to_s )
215
-
216
- self.show_column_list( dir.schema.attribute_types.values, 'Attribute Types' ) if
217
- self.options.attrtypes
218
- self.show_column_list( dir.schema.object_classes.values, "Object Classes" ) if
219
- self.options.objectclasses
220
- self.show_column_list( dir.schema.ldap_syntaxes.values, "Syntaxes" ) if
221
- self.options.syntaxes
222
- self.show_column_list( dir.schema.matching_rules.values, "Matching Rules" ) if
223
- self.options.matching_rules
224
- self.show_column_list( dir.schema.matching_rule_uses.values, "Matching Rule Uses" ) if
225
- self.options.matching_rule_uses
226
- end
227
-
228
-
229
- ### Show the items from the given +enum+ under the specified +subheading+ in a columnized list.
230
- def show_column_list( enum, subheading )
231
- pr = self.prompt
232
- items = nil
233
-
234
- if enum.first.respond_to?( :name )
235
- items = enum.map( &:name ).map( &:to_s ).uniq
236
- else
237
- items = enum.map( &:oid ).uniq
238
- end
239
-
240
- pr.say( "\n" )
241
- pr.say( pr.color(subheading, :subheader) )
242
- pr.say( pr.list(items.sort_by(&:downcase), :columns_down) )
243
- end
244
-
245
-
246
- #
247
- # 'Show entry' mode
248
- #
249
-
250
- ### Fetch an entry from the directory and display it like Treequel's editing mode.
251
- def show_entry( pattern )
252
- dir = self.directory
253
- branch = Treequel::Branch.new( dir, pattern )
254
-
255
- if !branch.exists?
256
- branch = Treequel::Branch.new( dir, pattern + ',' + dir.base_dn )
257
- end
258
-
259
- if !branch.exists?
260
- branch = dir.filter( pattern ).first
261
- end
262
-
263
- if !branch
264
- self.prompt.say( self.prompt.color("No match.", :error) )
265
- end
266
-
267
- yaml = self.branch_as_yaml( branch )
268
- self.prompt.say( yaml )
269
- end
270
-
271
-
272
- ### Return the specified Treequel::Branch object as YAML. If +include_operational+ is true,
273
- ### include the entry's operational attributes. If +extra_objectclasses+ contains
274
- ### one or more objectClass OIDs, include their MUST and MAY attributes when building the
275
- ### YAML representation of the branch.
276
- def branch_as_yaml( object, include_operational=false )
277
- object.include_operational_attrs = include_operational
278
-
279
- # Make sure the displayed entry has the MUST attributes
280
- entryhash = stringify_keys( object.must_attributes_hash )
281
- entryhash.merge!( object.entry || {} )
282
- entryhash['objectClass'] ||= []
283
-
284
- entryhash.delete( 'dn' ) # Special attribute, can't be edited
285
-
286
- yaml = entryhash.to_yaml
287
- yaml[ 5, 0 ] = self.prompt.color( "# #{object.dn}\n", :header )
288
-
289
- # Make comments out of MAY attributes that are unset
290
- mayhash = stringify_keys( object.may_attributes_hash )
291
- self.log.debug "MAY hash is: %p" % [ mayhash ]
292
- mayhash.delete_if {|attrname,val| entryhash.key?(attrname) }
293
- yaml << mayhash.to_yaml[5..-1].gsub( /\n\n/, "\n" ).gsub( /^/, '# ' )
294
-
295
- return yaml
296
- end
297
-
298
-
299
-
300
-
301
- #
302
- # 'Show schema artifact' mode
303
- #
304
-
305
- SCHEMA_ARTIFACT_TYPES = [
306
- :object_classes,
307
- :attribute_types,
308
- :ldap_syntaxes,
309
- :matching_rules,
310
- :matching_rule_uses,
311
- ]
312
-
313
- ### Find an artifact in the directory's schema that matches +pattern+, and display it
314
- ### if it exists.
315
- def show_schema_artifact( pattern )
316
- pr = self.prompt
317
- schema = self.directory.schema
318
- artifacts = SCHEMA_ARTIFACT_TYPES.
319
- collect {|type| schema.send( type ).values.uniq }.flatten
320
-
321
- if match = find_exact_matching_artifact( artifacts, pattern )
322
- self.display_schema_artifact( match )
323
- elsif match = find_substring_matching_artifact( artifacts, pattern )
324
- pr.say( "No exact match. Falling back to substring match:" )
325
- self.display_schema_artifact( match )
326
- else
327
- pr.say( pr.color("No match.", :error) )
328
- end
329
- end
330
-
331
-
332
- ### Display a schema artifact in a readable way.
333
- def display_schema_artifact( artifact )
334
- self.prompt.say( self.prompt.color(artifact.class.name.sub(/.*::/, ''), :header) + ' ' )
335
- self.prompt.say( self.prompt.color(artifact.to_s, :subheader) )
336
-
337
- # Display some other stuff depending on what kind of thing it is
338
- case artifact
339
- when Treequel::Schema::AttributeType
340
- self.display_attrtype_details( artifact )
341
- end
342
- end
343
-
344
-
345
- ### Display additional details for the specified +attrtype+ (a Treequel::Schema::AttributeType).
346
- def display_attrtype_details( attrtype )
347
- ocs = self.directory.schema.object_classes.values.find_all do |oc|
348
- ( oc.must_oids | oc.may_oids ).include?( attrtype.name.to_sym )
349
- end
350
-
351
- if ocs.empty?
352
- self.prompt.say "No objectClasses with the '%s' attribute are in the current schema." %
353
- [ attrtype.name ]
354
- else
355
- ocnames = ocs.uniq.map( &:name ).map( &:to_s ).sort
356
-
357
- self.prompt.say "objectClasses with the '%s' attribute in the current schema:" %
358
- [ attrtype.name ]
359
- self.prompt.say( self.prompt.list(ocnames, :columns_across) )
360
- end
361
- end
362
-
363
-
364
- ### Try to find an artifact in +artifacts+ whose name or oid matches +pattern+ exactly.
365
- ### Returns the first matching artifact.
366
- def find_exact_matching_artifact( artifacts, pattern )
367
- self.log.debug "Trying to find an exact match for %p in %d artifacts." %
368
- [ pattern, artifacts.length ]
369
- return artifacts.find do |obj|
370
- (obj.respond_to?( :names ) && obj.names.map(&:to_s).include?(pattern) ) ||
371
- (obj.respond_to?( :name ) && obj.name.to_s == pattern ) ||
372
- (obj.respond_to?( :oid ) && obj.oid == pattern )
373
- end
374
- end
375
-
376
-
377
- ### Try to find an artifact in +artifacts+ whose name or oid contains +pattern+.
378
- ### Returns the first matching artifact.
379
- def find_substring_matching_artifact( artifacts, pattern )
380
- pattern = Regexp.new( Regexp.escape(pattern), Regexp::IGNORECASE )
381
-
382
- return artifacts.find do |obj|
383
- (obj.respond_to?( :names ) && obj.names.find {|name| name.to_s =~ pattern} ) ||
384
- (obj.respond_to?( :name ) && obj.name.to_s =~ pattern ) ||
385
- (obj.respond_to?( :oid ) && obj.oid =~ pattern )
386
- end
387
- end
388
-
389
-
390
- end # class Treequel::What
391
-
392
-
393
- Treequel::What.run( ARGV.dup )
394
-