rubysync 0.1.1 → 0.2.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.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
|
|