rubysync 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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