rubysync 0.1.0 → 0.1.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.
data/HISTORY.txt CHANGED
@@ -1,3 +1,9 @@
1
+ == 0.1.1 / 2007-10-29
2
+
3
+ * Some streamlining of the base_pipeline in_handler and out_handler methods
4
+ * Added documentation to the base_connector class so you get a template for building your
5
+ own connector if you run "rubysync connector my_connector -t base"
6
+
1
7
  == 0.1.0 / 2007-09-26
2
8
 
3
9
  * Dropped the map_client_to_vault and map_vault_to_client methods. in_transform and out_transform now handle that
data/README.txt CHANGED
@@ -12,11 +12,17 @@ Alternatively, you can run it in one-shot mode and simply sync A with B.
12
12
  You can configure RubySync to perform transformations on the data as it
13
13
  syncs. RubySync is designed both as a handy utility to pack into your
14
14
  directory management toolkit or as a fully-fledged provisioning system
15
- for your organisation.
15
+ for your organization.
16
16
 
17
17
 
18
18
  == FEATURES/PROBLEMS:
19
-
19
+
20
+ * Event-driven synchronization (if connector supports it) with fall-back to polling
21
+ * Ruby DSL for "configuration" style event processing
22
+ * Clean separation of connector details from data transformation
23
+ * Connectors available for CSV files, XML, LDAP and RDBMS (via ActiveRecord)
24
+ * Easy API for writing your own connectors
25
+
20
26
 
21
27
  == SYNOPSIS:
22
28
 
@@ -40,7 +46,7 @@ for your organisation.
40
46
  You would then edit the file pipelines/my_pipeline.rb to configure the
41
47
  policy for synchronizing between the two connectors.
42
48
 
43
- You may then execute the pipeline in one-shot mode (daemon mode is coming):
49
+ You may then execute the pipeline in one-shot mode:
44
50
 
45
51
  $ rubysync once my
46
52
 
data/bin/rubysync CHANGED
@@ -91,7 +91,8 @@ class Controller < SimpleConsole::Controller
91
91
  :t => :type,
92
92
  :V => :vault,
93
93
  :C => :client},
94
- :int =>{:v => :verbose}
94
+ :int =>{:v => :verbose,
95
+ :d => :delay}
95
96
 
96
97
  def default
97
98
  #RDoc::usage 'Usage'
@@ -115,6 +116,7 @@ class Controller < SimpleConsole::Controller
115
116
  def start
116
117
  pipeline_name = params[:id]
117
118
  pipeline = pipeline_called pipeline_name
119
+ pipeline.delay = params[:delay]
118
120
  if pipeline
119
121
  pipeline.start
120
122
  else
@@ -231,21 +233,20 @@ end
231
233
  def example
232
234
  puts <<"END"
233
235
  This sets up the skeleton of a configuration for importing comma delimeted
234
- text files into a database. Note, if the application happens to be a Rails
235
- app then it can also export changes.
236
+ text files into an XML file.
236
237
 
237
- $ rubysync create db_demo
238
+ $ rubysync create xml_demo
238
239
  $ cd db_demo
239
240
  $ rubysync connector my_csv -t csv_file
240
- $ rubysync connector my_db -t active_record
241
+ $ rubysync connector my_xml -t xml
241
242
 
242
243
  You would then edit the files:
243
244
 
244
245
  connectors/my_csv_connector.rb ;where to get CSV files, field names, etc
245
- connectors/my_db_connector.rb ;how to connect to your DB or Rails app.
246
+ connectors/my_xml_connector.rb ;Set the path to your XML file
246
247
 
247
248
  And enter:
248
- $ rubysync pipeline my -C my_csv -V my_db
249
+ $ rubysync pipeline my -C my_csv -V my_xml
249
250
 
250
251
  You would then edit the file pipelines/my_pipeline.rb to configure the
251
252
  policy for synchronizing between the two connectors.
@@ -348,13 +349,6 @@ class #{name.to_s.camelize}Pipeline < RubySync::Pipelines::BasePipeline
348
349
  # Should evaluate to the path for placing a new record on the client
349
350
  # out_place do
350
351
  # end
351
-
352
-
353
- # These statements control logging. In log level 2 or higher (specify -v 2),
354
- # the event will be dumped to the log at any of the places specified below.
355
- # Uncomment and edit to taste.
356
- # dump_before :in_filter, :in_transform, :in_place, :out_filter, :out_place, :out_transform
357
- # dump_after :in_filter, :in_transform, :in_place, :out_filter, :out_place, :out_transform
358
352
 
359
353
 
360
354
  end
data/bin/rubysync.rb CHANGED
@@ -91,7 +91,8 @@ class Controller < SimpleConsole::Controller
91
91
  :t => :type,
92
92
  :V => :vault,
93
93
  :C => :client},
94
- :int =>{:v => :verbose}
94
+ :int =>{:v => :verbose,
95
+ :d => :delay}
95
96
 
96
97
  def default
97
98
  #RDoc::usage 'Usage'
@@ -115,6 +116,7 @@ class Controller < SimpleConsole::Controller
115
116
  def start
116
117
  pipeline_name = params[:id]
117
118
  pipeline = pipeline_called pipeline_name
119
+ pipeline.delay = params[:delay]
118
120
  if pipeline
119
121
  pipeline.start
120
122
  else
@@ -231,21 +233,20 @@ end
231
233
  def example
232
234
  puts <<"END"
233
235
  This sets up the skeleton of a configuration for importing comma delimeted
234
- text files into a database. Note, if the application happens to be a Rails
235
- app then it can also export changes.
236
+ text files into an XML file.
236
237
 
237
- $ rubysync create db_demo
238
+ $ rubysync create xml_demo
238
239
  $ cd db_demo
239
240
  $ rubysync connector my_csv -t csv_file
240
- $ rubysync connector my_db -t active_record
241
+ $ rubysync connector my_xml -t xml
241
242
 
242
243
  You would then edit the files:
243
244
 
244
245
  connectors/my_csv_connector.rb ;where to get CSV files, field names, etc
245
- connectors/my_db_connector.rb ;how to connect to your DB or Rails app.
246
+ connectors/my_xml_connector.rb ;Set the path to your XML file
246
247
 
247
248
  And enter:
248
- $ rubysync pipeline my -C my_csv -V my_db
249
+ $ rubysync pipeline my -C my_csv -V my_xml
249
250
 
250
251
  You would then edit the file pipelines/my_pipeline.rb to configure the
251
252
  policy for synchronizing between the two connectors.
@@ -348,13 +349,6 @@ class #{name.to_s.camelize}Pipeline < RubySync::Pipelines::BasePipeline
348
349
  # Should evaluate to the path for placing a new record on the client
349
350
  # out_place do
350
351
  # end
351
-
352
-
353
- # These statements control logging. In log level 2 or higher (specify -v 2),
354
- # the event will be dumped to the log at any of the places specified below.
355
- # Uncomment and edit to taste.
356
- # dump_before :in_filter, :in_transform, :in_place, :out_filter, :out_place, :out_transform
357
- # dump_after :in_filter, :in_transform, :in_place, :out_filter, :out_place, :out_transform
358
352
 
359
353
 
360
354
  end
@@ -93,12 +93,18 @@ module RubySync::Connectors
93
93
  raise "Not implemented"
94
94
  end
95
95
 
96
- # Subclasses must override this to interface with the external system
96
+ # Subclasses MAY override this to interface with the external system
97
97
  # and generate an event for every change that affects items within
98
98
  # the scope of this connector.
99
- # todo: Make the default behaviour to build a database of the key of
100
- # each entry with a hash of the contents of the entry. Then to compare
101
- # that against each entry to see if it has changed.
99
+ #
100
+ # The default behaviour is to compare a hash of each entry in the
101
+ # database with a stored hash of its previous value and generate
102
+ # add, modify and delete events appropriately. This is normally a very
103
+ # inefficient way to operate so overriding this method is highly
104
+ # recommended if you can detect changes in a more efficient manner.
105
+ #
106
+ # This method will be called repeatedly until the connector is
107
+ # stopped.
102
108
  def each_change
103
109
  DBM.open(self.mirror_dbm_filename) do |dbm|
104
110
  # scan existing entries to see if any new or modified
@@ -359,8 +365,6 @@ module RubySync::Connectors
359
365
  end
360
366
 
361
367
 
362
-
363
-
364
368
  # Return an array of possible fields for this connector.
365
369
  # Implementations should override this to query the datasource
366
370
  # for possible fields.
@@ -379,6 +383,103 @@ module RubySync::Connectors
379
383
  '::' + "#{connector_name}_connector".camelize
380
384
  end
381
385
 
386
+ def self.sample_config
387
+ return <<END
388
+ # The comments in this file should help you to create a custom connector.
389
+ # We're going to assume that you know how to program in Ruby. If you don't then
390
+ # quickly pop-off and learn it: http://ruby-lang.org.
391
+ #
392
+ # Edit the comments as you go to describe the specifics of your connector.
393
+ # If you need more information, consult http://rubysync.org/docs/developer/connectors
394
+
395
+
396
+ # Call the option class method to declare the options used to configure
397
+ # your connector.
398
+ # eg.
399
+ #
400
+ #option :filename, :frequency
401
+ #
402
+ # Would define an option called filename and one called frequency. You could then follow up with:
403
+ #
404
+ # filename 'default.csv'
405
+ # frequency 10
406
+ #
407
+ # And, of course, the same could be done in child classes (aka configuration files)
408
+ # The value set becomes available as a readable method of the same name in instances
409
+ # of the class.
410
+
411
+
412
+ ####### Configuration methods
413
+
414
+ # Return the list of the fields available for this connector. Feel free to print an
415
+ # informative message if you can't determine the available fields for the datastore.
416
+ def self.fields
417
+ puts "The author of #{__FILE__} hasn't got around to implementing the working out the default fields yet :>"
418
+ end
419
+
420
+ # Return the string that will be inserted as the contents of the subclass created
421
+ # when "rubysync connnector blah -t your_connector" is run.
422
+ def self.sample_config
423
+ return <<-END
424
+ # This is the default configuration provided by #{__FILE__}
425
+ #
426
+ # Kind of sparse. Isn't it?
427
+ #
428
+ #
429
+ END
430
+ end
431
+
432
+ ####### Reading methods
433
+
434
+ # If your datasource supports random access (as would, for example, a database) then
435
+ # implement the following:
436
+ #
437
+ #def [](path)
438
+ # #return the entry at location indicated by 'path'
439
+ # #An 'entry' is a hash where the key is the attribute name and the value is an
440
+ # #array containing the value or values for the the attribute
441
+ #end
442
+
443
+ # Subclasses must override this to
444
+ # interface with the external system and generate entries for every
445
+ # entry in the scope passing the entry path (id) and its data (as a hash of arrays).
446
+ def each_entry
447
+ raise "Not implemented"
448
+ end
449
+
450
+ # Subclasses MAY override this to interface with the external system
451
+ # and generate an event for every change that affects items within
452
+ # the scope of this connector.
453
+ #
454
+ # The default behaviour is to compare a hash of each entry in the
455
+ # database with a stored hash of its previous value and generate
456
+ # add, modify and delete events appropriately. This is normally a very
457
+ # inefficient way to operate so overriding this method is highly
458
+ # recommended if you can detect changes in a more efficient manner.
459
+ #
460
+ # This method will be called repeatedly until the connector is
461
+ # stopped.
462
+ #def each_change
463
+ #end
464
+
465
+ ######## Writing methods
466
+
467
+
468
+ # Apply operations to create database a entry at path
469
+ def add(path, operations)
470
+ end
471
+
472
+ # Apply operations to alter database entry at path
473
+ def modify(path, operations)
474
+ end
475
+
476
+
477
+ # Remove database entry at path
478
+ def delete(path)
479
+ end
480
+ END
481
+ end
482
+
382
483
  private
383
484
 
384
485
  def self.options options
@@ -38,7 +38,7 @@ module RubySync
38
38
  log.info "Adding '#{event.target_path}' to '#{name}'"
39
39
  raise Exception.new("#{name}: Entry with path '#{event.target_path}' already exists, add failing.") if self[event.target_path]
40
40
  if is_vault? && event.association && path_for_association(event.association)
41
- raise Exception.new("#{name}: Association already in use. Add failing.")
41
+ raise Exception.new("#{name}: Association (#{event.association.to_s}) already in use. Add failing.")
42
42
  end
43
43
  call_if_exists(:target_transform, event)
44
44
  if add(event.target_path, event.payload)
@@ -87,7 +87,7 @@ module RubySync
87
87
  if entry = self[path]
88
88
  DBM.open(self.mirror_dbm_filename) do |dbm|
89
89
  dbm[path.to_s] = digest(entry)
90
- end
90
+ end
91
91
  end
92
92
  end
93
93
 
@@ -60,7 +60,7 @@ module RubySync::Connectors
60
60
 
61
61
  def each_entry
62
62
  Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
63
- ldap.search :base => search_base, :filter => search_filter do |ldap_entry|
63
+ ldap.search :base => search_base, :filter => search_filter, :return_result => false do |ldap_entry|
64
64
  yield ldap_entry.dn, to_entry(ldap_entry)
65
65
  end
66
66
  end
@@ -118,6 +118,7 @@ END
118
118
  with_ldap do |ldap|
119
119
  result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
120
120
  return nil if !result or result.size == 0
121
+ # todo: See if this can be shortened
121
122
  answer = {}
122
123
  result[0].attribute_names.each do |name|
123
124
  answer[name.to_s] = result[0][name]
@@ -113,8 +113,6 @@ module RubySync::Connectors
113
113
  el
114
114
  end
115
115
 
116
-
117
-
118
116
 
119
117
  def to_entry entry_element
120
118
  entry = {}
@@ -46,7 +46,7 @@ module RubySync
46
46
 
47
47
  include RubySync::Utilities
48
48
 
49
- attr_accessor :type, # :delete, :add, :modify ...
49
+ attr_accessor :type, # :delete, :add, :modify, :disassociate
50
50
  :source,
51
51
  :target,
52
52
  :payload,
@@ -70,6 +70,12 @@ module RubySync
70
70
  self.new(:modify, source, source_path, association, payload)
71
71
  end
72
72
 
73
+ # Remove the association between the entry on the source and
74
+ # the associated entry (if any) on the target.
75
+ def self.disassociate source, source_path, association=nil, payload=nil
76
+ self.new(:disassociate, source, source_path, association, payload)
77
+ end
78
+
73
79
  def initialize type, source, source_path=nil, association=nil, payload=nil
74
80
  self.type = type.to_sym
75
81
  self.source = source
@@ -125,6 +131,11 @@ module RubySync
125
131
  end
126
132
  end
127
133
 
134
+
135
+ def hint
136
+ "(#{source.name} => #{target.name}) #{source_path}"
137
+ end
138
+
128
139
 
129
140
  def to_yaml_properties
130
141
  %w{ @type @source_path @target_path @association @payload}
@@ -156,7 +167,18 @@ module RubySync
156
167
  subjects = subjects.flatten.collect {|s| s.to_s}
157
168
  @uncommitted_operations = uncommitted_operations.delete_if {|op| !subjects.include?(op.subject.to_s)}
158
169
  end
159
-
170
+
171
+ def delete_when_blank
172
+ @uncommitted_operations = uncommitted_operations.map do |op|
173
+ if op.sets_blank?
174
+ @type == :modify ? op.same_but_as(:delete) : nil
175
+ else
176
+ op
177
+ end
178
+ end.compact
179
+ end
180
+
181
+
160
182
  # Add a value to a given subject unless it already sets a value
161
183
  def add_default field_name, value
162
184
  add_value(field_name.to_s, value) unless sets_value? field_name.to_s
@@ -82,6 +82,7 @@ module RubySync
82
82
  def same_but_as type
83
83
  op = self.dup
84
84
  op.type = type
85
+ op.values = nil if type == :delete
85
86
  op
86
87
  end
87
88
 
@@ -93,6 +94,10 @@ module RubySync
93
94
  op
94
95
  end
95
96
 
97
+ def sets_blank?
98
+ [:add, :replace].include? @type and
99
+ (!@values || as_array(@values).select {|v| v && v != ''}.empty?)
100
+ end
96
101
 
97
102
  end
98
103
  end
@@ -80,10 +80,12 @@ module RubySync
80
80
 
81
81
  def self.in_transform(&blk) event_method :in_transform,&blk; end
82
82
  def self.out_transform(&blk) event_method :out_transform,&blk; end
83
- def self.in_match(&blk) event_method :in_match_if,&blk; end
84
- def self.out_match(&blk) event_method :out_match_if,&blk; end
85
- def self.in_create_if(&blk) event_method :in_create_if,&blk; end
86
- def self.out_create_if(&blk) event_method :out_create_if,&blk; end
83
+ def self.in_match(&blk) event_method :in_match,&blk; end
84
+ def self.out_match(&blk) event_method :out_match,&blk; end
85
+ def self.in_create(&blk) event_method :in_create,&blk; end
86
+ def self.out_create(&blk) event_method :out_create,&blk; end
87
+ def self.in_place(&blk) event_method :in_place,&blk; end
88
+ def self.out_place(&blk) event_method :out_place,&blk; end
87
89
 
88
90
  def self.event_method name,&blk
89
91
  define_method name do |event|
@@ -91,23 +93,17 @@ module RubySync
91
93
  end
92
94
  end
93
95
 
94
- def self.in_place(&blk) place :in, &blk; end
95
- def self.out_place(&blk) place :out, &blk; end
96
-
97
- def self.place direction, &blk
98
- define_method "#{direction}_place" do |event|
99
- event.target_path = event.instance_eval &blk
100
- end
96
+ def in_match(event)
97
+ log.debug "Default matching rule - vault[in_place] exists?"
98
+ path = in_place(event)
99
+ vault.respond_to?('[]') and vault[path] and path
101
100
  end
102
101
 
103
-
104
- # Override to implement some kind of matching
105
- def default_match event
106
- log.debug "Default matching rule - source path exists on client?"
107
- event.target.respond_to?('[]') and event.target[event.source_path]
102
+ def out_match(event)
103
+ log.debug "Default matching rule - client[out_place] exists?"
104
+ path = out_place(event)
105
+ client.respond_to?('[]') and client[path] and path
108
106
  end
109
- alias_method :in_match, :default_match
110
- alias_method :out_match, :default_match
111
107
 
112
108
  # Override to restrict creation on the client
113
109
  def default_create event
@@ -117,15 +113,21 @@ module RubySync
117
113
  alias_method :in_create, :default_create
118
114
  alias_method :out_create, :default_create
119
115
 
120
-
121
116
  # Override to modify the target path for creation on the client
122
117
  def default_place(event)
123
- log.debug "Default placement rule target_path = source_path"
124
- event.target_path = event.source_path
118
+ log.debug "Default placement: same as source_path"
119
+ event.source_path
125
120
  end
126
121
  alias_method :in_place, :default_place
127
122
  alias_method :out_place, :default_place
128
123
 
124
+ def in_place_transform(event)
125
+ event.target_path = in_place(event)
126
+ end
127
+
128
+ def out_place_transform(event)
129
+ event.target_path = out_place(event)
130
+ end
129
131
 
130
132
  def perform_transform name, event, hint=""
131
133
  log.info "Performing #{name}"
@@ -197,43 +199,55 @@ module RubySync
197
199
  def out_event_filter(event); true; end
198
200
 
199
201
  # Called by the 'in' connector in the 'in' thread to process events generated by the client.
202
+ # Note: The client can't really know whether the event is an add or a modify because it doesn't store
203
+ # the association.
200
204
  def in_handler(event)
201
205
  event.target = @vault
202
206
  event.retrieve_association(association_context)
203
207
 
204
- hint = "(#{client.name} => #{vault.name}) #{event.source_path}"
205
- log.info "Processing incoming #{event.type} event "+hint
206
- perform_transform :in_filter, event, hint
207
-
208
- # The client can't really know whether its an add or a modify because it doesn't store
209
- # the association.
210
- if event.type == :modify
211
- unless event.associated? and vault.find_associated(event.association)
212
- log.info "No associated entry in vault for modify event. Converting to add"
213
- event.convert_to_add
214
- end
215
- elsif event.type == :add and event.associated? and vault.find_associated(event.association)
216
- log.info "Associated entry in vault for add event. Converting to modify"
217
- event.convert_to_modify
208
+ log.info "Processing incoming #{event.type} event "+event.hint
209
+ perform_transform :in_filter, event, event.hint
210
+
211
+ associated_entry = nil
212
+ unless event.type == :disassociate
213
+ associated_entry = vault.find_associated(event.association) if event.associated?
214
+ unless associated_entry
215
+ match = in_match(event)
216
+ if match
217
+ log.info("Matching entry found for unassociated event: '#{match}'. Creating association.")
218
+ event.association = Association.new(association_context, event.source_path)
219
+ vault.associate event.association, match
220
+ associated_entry = vault[match]
221
+ end
222
+ end
223
+ end
224
+
225
+ if associated_entry
226
+ if event.type == :add
227
+ log.info "Associated entry in vault for add event. Converting to modify"
228
+ event.convert_to_modify
229
+ end
230
+ elsif event.type == :modify
231
+ log.info "No associated entry in vault for modify event. Converting to add"
232
+ event.convert_to_add
218
233
  end
219
234
 
220
- perform_transform :in_transform, event, hint
235
+ perform_transform :in_transform, event, event.hint
221
236
 
222
-
223
- # todo: Maybe we should merge any add or modify that is associated or matched
224
- if event.type == :add
225
- match = in_match(event)
226
- if match
227
- log.info "Matching record found in vault. Merging."
228
- event.merge(match)
229
- log.info "---\n"; return
230
- end
231
-
237
+ case event.type
238
+ when :add
232
239
  if in_create(event)
233
- perform_transform :in_place, event, hint
234
- log.info "Create on vault allowed. Placing at #{event.target_path}"
240
+ perform_transform :in_place_transform, event, event.hint
241
+ log.info "Create on vault allowed. Placing at #{event.target_path}"
235
242
  else
236
- log.info "Create rule disallowed creation"
243
+ log.info "Create rule disallowed creation"
244
+ log.info "---\n"; return
245
+ end
246
+ when :modify
247
+ event.merge(associated_entry)
248
+ else
249
+ unless event.associated?
250
+ log.info "No associated entry in vault for #{event.type} event. Dropping"
237
251
  log.info "---\n"; return
238
252
  end
239
253
  end
@@ -243,73 +257,133 @@ module RubySync
243
257
 
244
258
  end
245
259
 
246
-
247
- # Called by the identity-vault connector in the 'out' thread to process events generated
248
- # by the identity vault.
249
- def out_handler(event)
250
- event.target = @client
251
- event.retrieve_association(association_context)
252
- event.convert_to_modify if event.associated? and event.type == :add
253
-
254
- hint = "(path=#{event.source_path} #{vault.name} => #{client.name})"
255
- log.info "Processing out-going #{event.type} event #{hint}"
256
- #log.info YAML.dump(event)
257
- unless out_event_filter event
258
- log.info "Disallowed by out_event_filter"
259
- log.info "---\n"; return
260
- end
261
-
262
- # Remove unwanted attributes
263
- perform_transform :out_filter, event
260
+ # Called by the 'in' connector in the 'in' thread to process events generated by the client.
261
+ # Note: The client can't really know whether the event is an add or a modify because it doesn't store
262
+ # the association.
263
+ def out_handler(event)
264
+ event.target = @client
265
+ event.retrieve_association(association_context)
264
266
 
265
- unless event.associated?
266
- log.info "no association"
267
- if [:delete, :remove_association].include? event.type
268
- log.info "#{name}: No action for #{event.type} of unassociated entry"
269
- log.info "---\n"; return
270
- end
271
- end
267
+ log.info "Processing outgoing #{event.type} event "+ event.hint
268
+ perform_transform :out_filter, event, event.hint
272
269
 
273
- if event.type == :modify
274
- unless event.associated? and client.has_entry_for_key?(event.association.key)
275
- log.info "Can't find associated client record so converting modify to add"
276
- event.convert_to_add
277
- end
270
+ associated_entry = nil
271
+ unless event.type == :disassociate
272
+ associated_entry = client.entry_for_own_association_key(event.association.key) if event.associated?
273
+ unless associated_entry
274
+ match = out_match(event)
275
+ if match
276
+ log.info("Matching entry found for unassociated event: '#{match}'. Creating association.")
277
+ event.association = Association.new(association_context, match)
278
+ vault.associate event.association, event.source_path
279
+ associated_entry = client[match]
278
280
  end
281
+ end
282
+ end
283
+
284
+ if associated_entry
285
+ if event.type == :add
286
+ log.info "Associated entry in client for add event. Converting to modify"
287
+ event.convert_to_modify
288
+ end
289
+ elsif event.type == :modify
290
+ log.info "No associated entry in client for modify event. Converting to add"
291
+ event.convert_to_add
292
+ end
279
293
 
280
- if event.type == :add
281
- match = out_match(event)
282
- log.info "Attempting to match"
283
- if match # exactly one event record on the client matched
284
- log.info "Match found, merging"
285
- event.merge(match)
286
- association = Association.new(self.association_context, match.source_path)
287
- vault.associate asssociation, event.source_path
288
- log.info "---\n"; return
289
- end
290
- log.info "No match found, creating"
291
- unless out_create(event)
292
- log.info "Creation denied by create rule"
293
- log.info "---\n"; return
294
- end
295
- perform_transform :out_place, event
296
- log.info "Placing new entry at #{event.target_path}"
297
- end
294
+ perform_transform :out_transform, event, event.hint
298
295
 
299
- perform_transform :out_transform, event
300
- association_key = nil
301
- with_rescue("#{client.name}: Processing command") do
302
- association_key = client.process(event)
303
- end
304
- if association_key
305
- association = Association.new(association_context, association_key)
306
- with_rescue("#{client.name}: Storing association #{association} in vault") do
307
- vault.associate(association, event.source_path)
308
- end
309
- else
310
- log.info "Client didn't return an association key"
311
- end
296
+ case event.type
297
+ when :add
298
+ if out_create(event)
299
+ perform_transform :out_place_transform, event, event.hint
300
+ log.info "Create on client allowed. Placing at #{event.target_path}"
301
+ else
302
+ log.info "Create rule disallowed creation"
303
+ log.info "---\n"; return
312
304
  end
305
+ when :modify
306
+ event.merge(associated_entry)
307
+ else
308
+ unless event.associated?
309
+ log.info "No associated entry in client for #{event.type} event. Dropping"
310
+ log.info "---\n"; return
311
+ end
312
+ end
313
+
314
+ with_rescue("#{client.name}: Processing command") {client.process(event)}
315
+ log.info "---\n"
316
+
317
+ end
318
+
319
+
320
+ # Called by the identity-vault connector in the 'out' thread to process events generated
321
+ # by the identity vault.
322
+ # def out_handler(event)
323
+ # event.target = @client
324
+ # event.retrieve_association(association_context)
325
+ # event.convert_to_modify if event.associated? and event.type == :add
326
+ #
327
+ # hint = "(path=#{event.source_path} #{vault.name} => #{client.name})"
328
+ # log.info "Processing out-going #{event.type} event #{hint}"
329
+ # #log.info YAML.dump(event)
330
+ # unless out_event_filter event
331
+ # log.info "Disallowed by out_event_filter"
332
+ # log.info "---\n"; return
333
+ # end
334
+ #
335
+ # # Remove unwanted attributes
336
+ # perform_transform :out_filter, event
337
+ #
338
+ # unless event.associated?
339
+ # log.info "no association"
340
+ # if [:delete, :disassociate].include? event.type
341
+ # log.info "#{name}: No action for #{event.type} of unassociated entry"
342
+ # log.info "---\n"; return
343
+ # end
344
+ # end
345
+ #
346
+ # if event.type == :modify
347
+ # unless event.associated? and client.has_entry_for_key?(event.association.key)
348
+ # log.info "Can't find associated client record so converting modify to add"
349
+ # event.convert_to_add
350
+ # end
351
+ # end
352
+ #
353
+ # if event.type == :add
354
+ # match = out_match(event)
355
+ # log.info "Attempting to match"
356
+ # if match # exactly one event record on the client matched
357
+ # log.info "Match found, merging"
358
+ # perform_transform :out_place, event
359
+ # event.merge(match)
360
+ # association = Association.new(self.association_context, match.source_path)
361
+ # vault.associate asssociation, event.source_path
362
+ # log.info "---\n"; return
363
+ # end
364
+ # log.info "No match found, creating"
365
+ # unless out_create(event)
366
+ # log.info "Creation denied by create rule"
367
+ # log.info "---\n"; return
368
+ # end
369
+ # perform_transform :out_place_transform, event
370
+ # log.info "Placing new entry at #{event.target_path}"
371
+ # end
372
+ #
373
+ # perform_transform :out_transform, event
374
+ # association_key = nil
375
+ # with_rescue("#{client.name}: Processing command") do
376
+ # association_key = client.process(event)
377
+ # end
378
+ # if association_key
379
+ # association = Association.new(association_context, association_key)
380
+ # with_rescue("#{client.name}: Storing association #{association} in vault") do
381
+ # vault.associate(association, event.source_path)
382
+ # end
383
+ # else
384
+ # log.info "Client didn't return an association key"
385
+ # end
386
+ # end
313
387
 
314
388
 
315
389
 
@@ -337,6 +411,15 @@ module RubySync
337
411
  op.subject = map[op.subject] || op.subject if op.subject
338
412
  end
339
413
  end
414
+
415
+
416
+
417
+
418
+ # Override to perform whatever transformation on the event is required
419
+ #def in_transform(event); event; end
420
+
421
+ # Convert fields in the incoming event to those used by the identity vault
422
+ #def in_map_schema(event); end
340
423
 
341
424
  # Specify which fields will be allowed through the incoming filter
342
425
  # If nil (the default), all fields are allowed.
data/lib/ruby_sync.rb CHANGED
@@ -26,7 +26,7 @@ require 'ruby_sync/event'
26
26
 
27
27
 
28
28
  module RubySync
29
- VERSION = '0.1.0'
29
+ VERSION = '0.1.1'
30
30
  module Connectors
31
31
  end
32
32
  module Pipelines
@@ -47,10 +47,14 @@ class TransformationTestPipeline < RubySync::Pipelines::BasePipeline
47
47
  # Constant string
48
48
  map(:note) {"Created by RubySync"}
49
49
  map(:shopping) {%w/fish milk bread/}
50
+ # Conditional mapping
51
+ map(:password) {value_for(:givenName)} if type == :add
50
52
  end
51
53
 
52
54
  in_place { "#{self.source_path}/path/in/vault"}
53
55
  out_place { "#{self.source_path}".split('/')[0] }
56
+
57
+ dump_after :in_transform
54
58
  end
55
59
 
56
60
  class TcTransformation < Test::Unit::TestCase
@@ -67,13 +71,22 @@ class TcTransformation < Test::Unit::TestCase
67
71
 
68
72
  def test_transform
69
73
  @client[client_path] = @bob_details
74
+ assert_nil @vault.path_for_association(RubySync::Association.new(@pipeline.association_context, client_path)), "Association appears on vault before sync. Strange."
70
75
  @pipeline.run_once
76
+ assert_not_nil @vault.path_for_association(RubySync::Association.new(@pipeline.association_context, client_path)), "Association doesn't appear to have been created on vault"
71
77
  assert_not_nil @vault[vault_path],"Bob wasn't created on the vault"
72
78
  assert_equal @bob_details['givenName'], @vault[vault_path]['first_name']
73
79
  assert_equal @bob_details['sn'], @vault[vault_path]['last_name']
74
80
  assert_equal "Created by RubySync", @vault[vault_path]['note'][0]
75
81
  assert_equal "music:makeup", @vault[vault_path]['hobbies'][0]
76
82
  assert_equal %w/fish milk bread/, @vault[vault_path]['shopping']
83
+ assert_equal @bob_details['givenName'], @vault[vault_path]['password']
84
+ @vault[vault_path]['password'] = new_password = 'myNewPassword'
85
+ assert_equal new_password, @vault[vault_path]['password']
86
+ @client[client_path]['givenName'] = new_given_name = 'Mary'
87
+ @pipeline.run_once
88
+ assert_equal [new_given_name], @vault[vault_path]['first_name']
89
+ assert_equal new_password, @vault[vault_path]['password']
77
90
  end
78
91
 
79
92
  end
data.tar.gz.sig ADDED
Binary file
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.0
2
+ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: rubysync
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.0
7
- date: 2007-09-26 00:00:00 +08:00
6
+ version: 0.1.1
7
+ date: 2007-10-29 00:00:00 +09:00
8
8
  summary: Event driven identity synchronization engine
9
9
  require_paths:
10
10
  - lib
11
11
  email: ritchiey@gmail.com
12
12
  homepage: " by Ritchie Young"
13
13
  rubyforge_project: rubysync
14
- description: "You can configure RubySync to perform transformations on the data as it syncs. RubySync is designed both as a handy utility to pack into your directory management toolkit or as a fully-fledged provisioning system for your organisation. == FEATURES/PROBLEMS: == SYNOPSIS: This sets up the skeleton of a configuration for importing comma delimited text files into a database. Note, if the application happens to be a Rails app then it can also export changes."
14
+ description: "You can configure RubySync to perform transformations on the data as it syncs. RubySync is designed both as a handy utility to pack into your directory management toolkit or as a fully-fledged provisioning system for your organization. == FEATURES/PROBLEMS: * Event-driven synchronization (if connector supports it) with fall-back to polling * Ruby DSL for \"configuration\" style event processing * Clean separation of connector details from data transformation * Connectors available for CSV files, XML, LDAP and RDBMS (via ActiveRecord) * Easy API for writing your own connectors == SYNOPSIS:"
15
15
  autorequire:
16
16
  default_executable:
17
17
  bindir: bin
@@ -25,6 +25,28 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
25
25
  platform: ruby
26
26
  signing_key:
27
27
  cert_chain:
28
+ - |
29
+ -----BEGIN CERTIFICATE-----
30
+ MIIDMjCCAhqgAwIBAgIBADANBgkqhkiG9w0BAQUFADA/MREwDwYDVQQDDAhyaXRj
31
+ aGlleTEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYDY29t
32
+ MB4XDTA3MTAyOTA0MTI0MloXDTA4MTAyODA0MTI0MlowPzERMA8GA1UEAwwIcml0
33
+ Y2hpZXkxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
34
+ bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK7xOC2DjOT9aUE/I0MM
35
+ 71CXMM2vMldkLLT0gZo/85I1sTSEBFua4up/ad9j+kr0ymtdhWC/ulKnnub/GtEO
36
+ OFRxTDKUGKeoYDQgOZ+UqcWuyjW2DUn6mOxQrQClUa/s7REg4cQyCBusQ6rMxFn8
37
+ zhOq7IR1ZPkbPB4HcSrXh+3/S2iy2LkfQndt3051cEDX5AilcLPy6KcG60VS84N5
38
+ E5JBDZIj/y56Ve2WdRwvbL8Od5uGYUftW17K6yjz3stXG/Li4I93t7WU4Bp34Rkd
39
+ Mfx90RY+smkRp34zPf7rtlY77dnQP/3HRxghft5dnfWpHjJpMkEe2r6x0wAAF4L2
40
+ MvUCAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFK42
41
+ S/x8c9AqJK9Ak8nb9C7fsh9EMA0GCSqGSIb3DQEBBQUAA4IBAQApFEzful/Wtetr
42
+ pfFP5+VENNzNpXMP0NDP/FRYvEkf10kF3g3FRbT9wnrRAqwupBRNP+LRMywE6pXb
43
+ CGP1uhA2ElYCPjKl/HgmHqRk7+dGpcXg/wlC/RpFGf1k9AB//akBBrAwGcvMeOv7
44
+ CDBOYma/WXx6kL/nyYQAWqJlHB3aBUjD9OXbKbSuHV6v7G23YugwtubMJbOCCIr9
45
+ 5wkZH0Ih6BfsOnuGCd6Gfada2L8nxV3c4XMiOjg53EdpauP5RD/bxbSlr0XDN7Bh
46
+ moz1RHd78JYoT3J7s8pd1KdyhAhtgRJRuMPT5/Lk2edSgLIP/XOhFqQjs8GTKSG0
47
+ +lEskbtv
48
+ -----END CERTIFICATE-----
49
+
28
50
  post_install_message:
29
51
  authors:
30
52
  - Ritchie Young
metadata.gz.sig ADDED
Binary file