rubysync 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/HISTORY.txt +11 -0
- data/Manifest.txt +4 -1
- data/Rakefile +3 -4
- data/bin/rubysync +242 -152
- data/bin/rubysync.rb +242 -152
- data/lib/ruby_sync.rb +1 -1
- data/lib/ruby_sync/connectors/base_connector.rb +286 -378
- data/lib/ruby_sync/connectors/connector_event_processing.rb +24 -20
- data/lib/ruby_sync/connectors/csv_file_connector.rb +3 -5
- data/lib/ruby_sync/connectors/dbm_association_tracking.rb +88 -0
- data/lib/ruby_sync/connectors/dbm_change_tracking.rb +110 -0
- data/lib/ruby_sync/connectors/ldap_changelog_connector.rb +79 -78
- data/lib/ruby_sync/connectors/ldap_connector.rb +84 -57
- data/lib/ruby_sync/connectors/memory_association_tracking.rb +60 -0
- data/lib/ruby_sync/connectors/memory_change_tracking.rb +84 -0
- data/lib/ruby_sync/connectors/memory_connector.rb +3 -0
- data/lib/ruby_sync/connectors/xml_connector.rb +6 -2
- data/lib/ruby_sync/event.rb +73 -50
- data/lib/ruby_sync/operation.rb +3 -3
- data/lib/ruby_sync/pipelines/base_pipeline.rb +76 -48
- data/lib/ruby_sync/util/utilities.rb +72 -29
- data/test/tc_csv_file_connector.rb +2 -2
- data/test/tc_ldap_connector.rb +2 -2
- data/test/tc_memory_connectors.rb +0 -2
- data/test/tc_transformation.rb +14 -13
- data/test/tc_xml_connectors.rb +4 -2
- data/test/ts_rubysync.rb +1 -1
- metadata +102 -84
- metadata.gz.sig +0 -0
- data/lib/ruby_sync/connectors/ldap_associations.rb +0 -126
@@ -22,12 +22,13 @@ module RubySync
|
|
22
22
|
module ConnectorEventProcessing
|
23
23
|
|
24
24
|
def process(event)
|
25
|
+
perform_transform(:target_transform, event, name)
|
25
26
|
case event.type
|
26
|
-
when :add
|
27
|
-
when :delete
|
28
|
-
when :modify
|
27
|
+
when :add then return perform_add(event)
|
28
|
+
when :delete then return perform_delete(event)
|
29
|
+
when :modify then return perform_modify(event)
|
29
30
|
else
|
30
|
-
|
31
|
+
raise Exception.new("#{name}: Unknown event type '#{event.type}' received")
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
@@ -38,9 +39,10 @@ module RubySync
|
|
38
39
|
log.info "Adding '#{event.target_path}' to '#{name}'"
|
39
40
|
raise Exception.new("#{name}: Entry with path '#{event.target_path}' already exists, add failing.") if self[event.target_path]
|
40
41
|
if is_vault? && event.association && path_for_association(event.association)
|
41
|
-
|
42
|
+
log.warn("#{name}: Removing obsolete association (#{event.association.to_s}) found for non-existent #{event.target_path}.")
|
43
|
+
self.remove_association(event.association)
|
42
44
|
end
|
43
|
-
call_if_exists(:target_transform, event)
|
45
|
+
# call_if_exists(:target_transform, event)
|
44
46
|
if add(event.target_path, event.payload)
|
45
47
|
log.info "Add succeeded"
|
46
48
|
update_mirror event.target_path
|
@@ -61,36 +63,38 @@ module RubySync
|
|
61
63
|
|
62
64
|
def perform_delete event
|
63
65
|
raise Exception.new("#{name}: Delete of unassociated object. No action taken.") unless event.association
|
64
|
-
path =
|
66
|
+
path = associated_path event
|
65
67
|
log.info "Deleting '#{path}' from '#{name}'"
|
66
68
|
delete(path) or log.warn("#{name}: Attempted to delete non-existent entry '#{path}'\nMay be an echo of a delete from this connector, ignoring.")
|
67
69
|
delete_from_mirror path
|
68
70
|
return nil # don't want to create any new associations
|
69
71
|
end
|
70
72
|
|
71
|
-
def delete_from_mirror path
|
72
|
-
DBM.open(self.mirror_dbm_filename) do |dbm|
|
73
|
-
dbm.delete(path)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
73
|
def perform_modify event
|
78
|
-
path =
|
74
|
+
path = associated_path event
|
79
75
|
raise Exception.new("#{name}: Attempted to modify non-existent entry '#{path}'") unless self[path]
|
80
|
-
call_if_exists(:target_transform, event)
|
76
|
+
#call_if_exists(:target_transform, event)
|
81
77
|
modify path, event.payload
|
82
78
|
update_mirror path
|
83
79
|
return (is_vault?)? nil : own_association_key_for(event.target_path)
|
84
80
|
end
|
85
81
|
|
86
82
|
def update_mirror path
|
87
|
-
if entry = self[path]
|
88
|
-
DBM.open(self.mirror_dbm_filename) do |dbm|
|
89
|
-
dbm[path.to_s] = digest(entry)
|
90
|
-
end
|
91
|
-
end
|
92
83
|
end
|
93
84
|
|
85
|
+
def delete_from_mirror path
|
86
|
+
end
|
87
|
+
|
88
|
+
def clean
|
89
|
+
remove_associations if respond_to? :remove_associations
|
90
|
+
remove_mirror if respond_to? :remove_mirror
|
91
|
+
end
|
92
|
+
|
93
|
+
def associated_path event
|
94
|
+
(is_vault?)? path_for_association(event.association) : path_for_own_association_key(event.association.key)
|
95
|
+
end
|
96
|
+
|
97
|
+
|
94
98
|
end
|
95
99
|
end
|
96
100
|
end
|
@@ -6,9 +6,8 @@
|
|
6
6
|
require "csv"
|
7
7
|
require "ruby_sync/connectors/file_connector"
|
8
8
|
|
9
|
-
module RubySync
|
9
|
+
module RubySync::Connectors
|
10
10
|
|
11
|
-
module Connectors
|
12
11
|
|
13
12
|
# Reads files containing Comma Separated Values from the in_path directory and
|
14
13
|
# treats each line as an incoming event.
|
@@ -27,7 +26,7 @@ module RubySync
|
|
27
26
|
|
28
27
|
|
29
28
|
# Called for each filename matching in_glob in in_path
|
30
|
-
# Yields
|
29
|
+
# Yields an add event for each row found in the file.
|
31
30
|
def each_file_change(filename)
|
32
31
|
header = header_line
|
33
32
|
CSV.open(filename, 'r') do |row|
|
@@ -46,7 +45,7 @@ module RubySync
|
|
46
45
|
row[i] and data[field_name] = row[i].data
|
47
46
|
end
|
48
47
|
association_key = source_path = path_for(data)
|
49
|
-
yield RubySync::Event.
|
48
|
+
yield RubySync::Event.add(self, source_path, association_key, create_operations_for(data))
|
50
49
|
end
|
51
50
|
end
|
52
51
|
|
@@ -102,4 +101,3 @@ END
|
|
102
101
|
|
103
102
|
end
|
104
103
|
end
|
105
|
-
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
require 'yaml'
|
17
|
+
require 'yaml/dbm'
|
18
|
+
|
19
|
+
|
20
|
+
module RubySync
|
21
|
+
module Connectors
|
22
|
+
module DbmAssociationTracking
|
23
|
+
|
24
|
+
# Store association for the given path
|
25
|
+
def associate association, path
|
26
|
+
YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
|
27
|
+
assocs = dbm[path.to_s] || {}
|
28
|
+
assocs[association.context.to_s] = association.key.to_s
|
29
|
+
dbm[path.to_s] = assocs
|
30
|
+
end
|
31
|
+
DBM.open(association_to_path_dbm_filename) do |dbm|
|
32
|
+
dbm[association.to_s] = path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def path_for_association association
|
37
|
+
is_vault? or return path_for_own_association_key(association.key)
|
38
|
+
DBM.open(association_to_path_dbm_filename) do |dbm|
|
39
|
+
dbm[association.to_s]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def associations_for path
|
44
|
+
YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
|
45
|
+
assocs = dbm[path.to_s]
|
46
|
+
assocs.values
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def remove_association association
|
52
|
+
path = nil
|
53
|
+
DBM.open(association_to_path_dbm_filename) do |dbm|
|
54
|
+
return unless path =dbm.delete(association.to_s)
|
55
|
+
end
|
56
|
+
YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
|
57
|
+
assocs = dbm[path.to_s]
|
58
|
+
assocs.delete(association.context) and dbm[path.to_s] = assocs
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def association_key_for context, path
|
63
|
+
YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
|
64
|
+
assocs = dbm[path.to_s] || {}
|
65
|
+
assocs[context.to_s]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def remove_associations
|
70
|
+
File.delete_if_exists(["#{association_to_path_dbm_filename}.db","#{path_to_association_dbm_filename}.db"])
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Stores association keys indexed by path:association_context
|
75
|
+
def path_to_association_dbm_filename
|
76
|
+
dbm_path + "_path_to_assoc"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Stores paths indexed by association_context:association_key
|
80
|
+
def association_to_path_dbm_filename
|
81
|
+
dbm_path + "_assoc_to_path"
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
require 'yaml'
|
18
|
+
require 'yaml/dbm'
|
19
|
+
require 'digest/md5'
|
20
|
+
|
21
|
+
# When included by a connector, tracks changes to the connector using
|
22
|
+
# a dbm database of the form path:digest. Digest is a MD5 hash of the
|
23
|
+
# record so we can tell if the record has changed. We can't, however,
|
24
|
+
# tell how the record has changed so change events generated will be
|
25
|
+
# for the entire record.
|
26
|
+
module RubySync::Connectors::DbmChangeTracking
|
27
|
+
|
28
|
+
option :dbm_path
|
29
|
+
|
30
|
+
# set a default dbm path
|
31
|
+
def dbm_path()
|
32
|
+
p = "#{base_path}/db"
|
33
|
+
::FileUtils.mkdir_p p
|
34
|
+
::File.join(p,name)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Stores a hash for each entry so we can tell when
|
38
|
+
# entries are added, deleted or modified
|
39
|
+
def mirror_dbm_filename
|
40
|
+
dbm_path + "_mirror"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Subclasses MAY override this to interface with the external system
|
44
|
+
# and generate an event for every change that affects items within
|
45
|
+
# the scope of this connector.
|
46
|
+
#
|
47
|
+
# The default behaviour is to compare a hash of each entry in the
|
48
|
+
# database with a stored hash of its previous value and generate
|
49
|
+
# add, modify and delete events appropriately. This is normally a very
|
50
|
+
# inefficient way to operate so overriding this method is highly
|
51
|
+
# recommended if you can detect changes in a more efficient manner.
|
52
|
+
#
|
53
|
+
# This method will be called repeatedly until the connector is
|
54
|
+
# stopped.
|
55
|
+
def each_change
|
56
|
+
DBM.open(self.mirror_dbm_filename) do |dbm|
|
57
|
+
# scan existing entries to see if any new or modified
|
58
|
+
each_entry do |path, entry|
|
59
|
+
digest = digest(entry)
|
60
|
+
unless stored_digest = dbm[path.to_s] and digest == stored_digest
|
61
|
+
operations = create_operations_for(entry)
|
62
|
+
yield RubySync::Event.add(self, path, nil, operations)
|
63
|
+
dbm[path.to_s] = digest
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# scan dbm to find deleted
|
68
|
+
dbm.each do |key, stored_hash|
|
69
|
+
unless self[key]
|
70
|
+
yield RubySync::Event.delete(self, key)
|
71
|
+
dbm.delete key
|
72
|
+
if is_vault? and @pipeline
|
73
|
+
association = association_for @pipeline.association_context, key
|
74
|
+
remove_association association
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def digest(o)
|
82
|
+
Digest::MD5.hexdigest(o.to_yaml)
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
def remove_mirror
|
88
|
+
File.delete_if_exists(["#{mirror_dbm_filename}.db"])
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_from_mirror path
|
92
|
+
DBM.open(self.mirror_dbm_filename) do |dbm|
|
93
|
+
dbm.delete(path)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_mirror path
|
98
|
+
if entry = self[path]
|
99
|
+
DBM.open(self.mirror_dbm_filename) do |dbm|
|
100
|
+
dbm[path.to_s] = digest(entry)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def mirror_dbm_filename
|
106
|
+
dbm_path + "_mirror"
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
end
|
@@ -27,100 +27,101 @@ require 'net/ldap'
|
|
27
27
|
class LdapChangelogConnector < RubySync::Connectors::LdapConnector
|
28
28
|
|
29
29
|
option :changelog_dn
|
30
|
-
|
30
|
+
changelog_dn "cn=changelog"
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
32
|
+
def initialize options={}
|
33
|
+
super options
|
34
|
+
@last_change_number = 1
|
35
|
+
# TODO: Persist the current CSN, for now we'll just skip to the end of the changelog
|
36
|
+
skip_existing_changelog_entries
|
37
|
+
end
|
38
|
+
# Look for changelog entries. This is not supported by all LDAP servers
|
39
|
+
# Changelog entries have these attributes
|
40
|
+
# targetdn
|
41
|
+
# changenumber
|
42
|
+
# objectclass
|
43
|
+
# changes
|
44
|
+
# changetime
|
45
|
+
# changetype
|
46
|
+
# dn
|
47
|
+
#
|
48
|
+
# TODO: Detect presence/location of changelog from root DSE
|
49
|
+
def each_change
|
50
|
+
with_ldap do |ldap|
|
51
|
+
log.debug "@last_change_number = #{@last_change_number}"
|
52
|
+
filter = "(changenumber>=#{@last_change_number})"
|
53
|
+
first = true
|
54
|
+
@full_refresh_required = false
|
55
|
+
ldap.search :base => changelog_dn, :filter =>filter do |change|
|
56
|
+
change_number = change.changenumber[0].to_i
|
57
|
+
if first
|
58
|
+
first = false
|
59
|
+
# TODO: Persist the change_number so that we don't do a full resync everytime rubysync starts
|
60
|
+
if change_number != @last_change_number
|
61
|
+
log.warn "Earliest change number (#{change_number}) differs from that recorded (#{@last_change_number})."
|
62
|
+
log.warn "A full refresh is required."
|
63
|
+
@full_refresh_required = true
|
64
|
+
break
|
65
|
+
end
|
66
|
+
else
|
67
|
+
@last_change_number = change_number if change_number > @last_change_number
|
68
|
+
# TODO: A proper DN object would be nice instead of string manipulation
|
69
|
+
target_dn = change.targetdn[0].gsub(/\s*,\s*/,',')
|
70
|
+
if target_dn =~ /#{search_base}$/oi
|
71
|
+
change_type = change.changetype[0]
|
72
|
+
event = event_for_changelog_entry(change)
|
73
|
+
yield event
|
74
|
+
end
|
75
|
+
end
|
77
76
|
end
|
78
77
|
end
|
78
|
+
each_entry if @full_refresh_required
|
79
|
+
end
|
79
80
|
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
end
|
82
|
+
def skip_existing_changelog_entries
|
83
|
+
with_ldap do |ldap|
|
84
|
+
filter = "(changenumber>=#{@last_change_number})"
|
85
|
+
@full_refresh_required = false
|
86
|
+
ldap.search :base => changelog_dn, :filter =>filter do |change|
|
87
|
+
change_number = change.changenumber[0].to_i
|
88
|
+
@last_change_number = change_number if change_number > @last_change_number
|
89
89
|
end
|
90
90
|
end
|
91
|
+
end
|
91
92
|
|
92
93
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
# Called by unit tests to inject data
|
95
|
+
def test_add id, details
|
96
|
+
details << RubySync::Operation.new(:add, "objectclass", ['inetOrgPerson', 'organizationalPerson', 'person', 'top', 'RubySyncSynchable'])
|
97
|
+
add id, details
|
98
|
+
end
|
98
99
|
|
99
100
|
|
100
101
|
private
|
101
102
|
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
end
|
104
|
+
def event_for_changelog_entry cle
|
105
|
+
payload = nil
|
106
|
+
dn = cle.targetdn[0]
|
107
|
+
changetype = cle.changetype[0]
|
108
|
+
if cle.attribute_names.include? :changes
|
109
|
+
payload = []
|
110
|
+
cr = Net::LDIF.parse("dn: #{dn}\nchangetype: #{changetype}\n#{cle.changes[0]}")[0]
|
111
|
+
if changetype.to_sym == :add
|
112
|
+
# cr.data will be a hash of arrays or strings (attr-name=>[value1, value2, ...])
|
113
|
+
cr.data.each do |name, values|
|
114
|
+
payload << RubySync::Operation.add(name, values)
|
115
|
+
end
|
116
|
+
else
|
117
|
+
# cr.data will be an array of arrays of form [:action, :subject, [values]]
|
118
|
+
cr.data.each do |record|
|
119
|
+
payload << RubySync::Operation.new(record[0], record[1], record[2])
|
120
|
+
end
|
121
121
|
end
|
122
|
-
RubySync::Event.new(changetype, self, dn, nil, payload)
|
123
122
|
end
|
123
|
+
RubySync::Event.new(changetype, self, dn, nil, payload)
|
124
|
+
end
|
124
125
|
|
125
126
|
end
|
126
127
|
|