rubysync 0.1.1 → 0.2.1

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