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.
@@ -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: return perform_add(event)
27
- when :delete: return perform_delete(event)
28
- when :modify: return perform_modify(event)
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
- raise Exception.new("#{name}: Unknown event type '#{event.type}' received")
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
- raise Exception.new("#{name}: Association (#{event.association.to_s}) already in use. Add failing.")
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 = (is_vault?)? path_for_association(event.association) : path_for_own_association_key(event.association.key)
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 = (is_vault?)? path_for_association(event.association) : path_for_own_association_key(event.association.key)
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 a modify event for each row found in the file.
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.modify(self, source_path, association_key, create_operations_for(data))
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
- changelog_dn "cn=changelog"
30
+ changelog_dn "cn=changelog"
31
31
 
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
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
- def skip_existing_changelog_entries
82
- with_ldap do |ldap|
83
- filter = "(changenumber>=#{@last_change_number})"
84
- @full_refresh_required = false
85
- ldap.search :base => changelog_dn, :filter =>filter do |change|
86
- change_number = change.changenumber[0].to_i
87
- @last_change_number = change_number if change_number > @last_change_number
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
- # Called by unit tests to inject data
94
- def test_add id, details
95
- details << RubySync::Operation.new(:add, "objectclass", ['inetOrgPerson', 'organizationalPerson', 'person', 'top', 'RubySyncSynchable'])
96
- add id, details
97
- end
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
- def event_for_changelog_entry cle
104
- payload = nil
105
- dn = cle.targetdn[0]
106
- changetype = cle.changetype[0]
107
- if cle.attribute_names.include? :changes
108
- payload = []
109
- cr = Net::LDIF.parse("dn: #{dn}\nchangetype: #{changetype}\n#{cle.changes[0]}")[0]
110
- if changetype.to_sym == :add
111
- # cr.data will be a hash of arrays or strings (attr-name=>[value1, value2, ...])
112
- cr.data.each do |name, values|
113
- payload << RubySync::Operation.add(name, values)
114
- end
115
- else
116
- # cr.data will be an array of arrays of form [:action, :subject, [values]]
117
- cr.data.each do |record|
118
- payload << RubySync::Operation.new(record[0], record[1], record[2])
119
- end
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