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 +6 -0
- data/README.txt +9 -3
- data/bin/rubysync +8 -14
- data/bin/rubysync.rb +8 -14
- data/lib/ruby_sync/connectors/base_connector.rb +107 -6
- data/lib/ruby_sync/connectors/connector_event_processing.rb +2 -2
- data/lib/ruby_sync/connectors/ldap_connector.rb +2 -1
- data/lib/ruby_sync/connectors/xml_connector.rb +0 -2
- data/lib/ruby_sync/event.rb +24 -2
- data/lib/ruby_sync/operation.rb +5 -0
- data/lib/ruby_sync/pipelines/base_pipeline.rb +193 -110
- data/lib/ruby_sync.rb +1 -1
- data/test/tc_transformation.rb +13 -0
- data.tar.gz.sig +0 -0
- metadata +26 -4
- metadata.gz.sig +0 -0
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
|
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
|
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
|
235
|
-
app then it can also export changes.
|
236
|
+
text files into an XML file.
|
236
237
|
|
237
|
-
$ rubysync create
|
238
|
+
$ rubysync create xml_demo
|
238
239
|
$ cd db_demo
|
239
240
|
$ rubysync connector my_csv -t csv_file
|
240
|
-
$ rubysync connector
|
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/
|
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
|
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
|
235
|
-
app then it can also export changes.
|
236
|
+
text files into an XML file.
|
236
237
|
|
237
|
-
$ rubysync create
|
238
|
+
$ rubysync create xml_demo
|
238
239
|
$ cd db_demo
|
239
240
|
$ rubysync connector my_csv -t csv_file
|
240
|
-
$ rubysync connector
|
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/
|
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
|
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
|
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
|
-
#
|
100
|
-
#
|
101
|
-
#
|
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
|
-
|
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]
|
data/lib/ruby_sync/event.rb
CHANGED
@@ -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
|
data/lib/ruby_sync/operation.rb
CHANGED
@@ -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 :
|
84
|
-
def self.out_match(&blk) event_method :
|
85
|
-
def self.
|
86
|
-
def self.
|
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
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
124
|
-
event.
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
|
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 :
|
234
|
-
|
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
|
-
|
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
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
266
|
-
|
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
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
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
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
data/test/tc_transformation.rb
CHANGED
@@ -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.
|
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.
|
7
|
-
date: 2007-
|
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
|
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
|