snapsync 0.1.5 → 0.1.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0f6542443b1ff40bd7406fb68c42ce183562ad68
4
- data.tar.gz: d181ac3b9658ce0c309f13fcff710b0c7003bb69
3
+ metadata.gz: 1f9224076ddb7de222c830cb7ad068742631d46b
4
+ data.tar.gz: b3ced85dbdc080938b61ec158e8d803f72ebeed1
5
5
  SHA512:
6
- metadata.gz: 88057a0d14f43499e074f42f3c7cbbd84b09411df4e53e57ca0e4654e43bcc0647102d32164f462fc3cc35560337aab645fa21cd6f5305151dda6936603dc3d1
7
- data.tar.gz: 2b8d3cfbf41498a2ccc0baa55c2cb6651228c68f4ca78e78a93af6edd677082f0084fd5907e94f3b3188c280f0e595052eff37aac2589274d62eb2a694f45534
6
+ metadata.gz: c26502e714514d0d8f686c62a3d237ded6c85f0cd1a665b2d55d340d357c40aa139a36d5040f371fc719d80aa1c7010654199b6de47b3f2534bae1a3871bb56d
7
+ data.tar.gz: 0c67490d86f8ad9fc007e88a7c9054883be9f45ee94410ca18dd55dbc6e422620b90f33406693ff44817a13c70f262253cc9283f568035a50b2b029b2ae56aa8
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /vendor/
11
+ .*.sw?
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in snapsync.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry'
7
+ gem 'pry-byebug'
@@ -19,6 +19,7 @@ require 'snapsync/timeline_sync_policy'
19
19
  require 'snapsync/sync_last_policy'
20
20
 
21
21
  require 'snapsync/partitions_monitor'
22
+ require 'snapsync/sync'
22
23
  require 'snapsync/sync_all'
23
24
  require 'snapsync/auto_sync'
24
25
 
@@ -11,13 +11,21 @@ module Snapsync
11
11
  attr_reader :targets
12
12
  attr_reader :partitions
13
13
 
14
+ DEFAULT_CONFIG_PATH = Pathname.new('/etc/snapsync.conf')
15
+
16
+ def self.load_default
17
+ result = new
18
+ result.load_config
19
+ result
20
+ end
21
+
14
22
  def initialize(config_dir = SnapperConfig.default_config_dir)
15
23
  @config_dir = config_dir
16
24
  @targets = Hash.new
17
25
  @partitions = PartitionsMonitor.new
18
26
  end
19
27
 
20
- def load_config(path)
28
+ def load_config(path = DEFAULT_CONFIG_PATH)
21
29
  conf = YAML.load(path.read) || Array.new
22
30
  parse_config(conf)
23
31
  end
@@ -32,7 +40,7 @@ module Snapsync
32
40
  end
33
41
 
34
42
  def write_config(path)
35
- data = each_target.map do |target|
43
+ data = each_autosync_target.map do |target|
36
44
  Hash['partition_uuid' => target.partition_uuid,
37
45
  'path' => target.path.to_s,
38
46
  'automount' => !!target.automount,
@@ -43,13 +51,86 @@ module Snapsync
43
51
  end
44
52
  end
45
53
 
46
- def each_target
54
+ # Enumerates the declared autosync targets
55
+ #
56
+ # @yieldparam [AutoSync] target
57
+ # @return [void]
58
+ def each_autosync_target
47
59
  return enum_for(__method__) if !block_given?
48
60
  targets.each_value do |targets|
49
61
  targets.each { |t| yield(t) }
50
62
  end
51
63
  end
52
64
 
65
+ # Enumerates the available autosync targets
66
+ #
67
+ # It may mount partitions as needed
68
+ #
69
+ # @yieldparam [Pathname] path the path to the target's base dir
70
+ # (suitable to be processed by e.g. AutoSync)
71
+ # @yieldparam [AutoSyncTarget] target the target located at 'path'
72
+ # @return [void]
73
+ def each_available_autosync_target
74
+ return enum_for(__method__) if !block_given?
75
+ partitions.poll
76
+
77
+ partitions.known_partitions.each do |uuid, fs|
78
+ autosync_targets = targets[uuid]
79
+ next if autosync_targets.empty?
80
+
81
+ mp = fs['MountPoints'].first
82
+ if mp
83
+ mp = Pathname.new(mp[0..-2].pack("U*"))
84
+ end
85
+
86
+ begin
87
+ mounted = false
88
+
89
+ if !mp
90
+ if !autosync_targets.any?(&:automount)
91
+ Snapsync.info "partition #{uuid} is present, but not mounted and automount is false. Ignoring"
92
+ next
93
+ end
94
+
95
+ Snapsync.info "partition #{uuid} is present, but not mounted, automounting"
96
+ begin
97
+ mp = fs.Mount([]).first
98
+ rescue Exception => e
99
+ Snapsync.warn "failed to mount, ignoring this target"
100
+ next
101
+ end
102
+ mp = Pathname.new(mp)
103
+ mounted = true
104
+ end
105
+
106
+ autosync_targets.each do |target|
107
+ yield(mp + target.path, target)
108
+ end
109
+
110
+ ensure
111
+ if mounted
112
+ fs.Unmount([])
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Enumerates the available synchronization targets
119
+ #
120
+ # It may mount partitions as needed
121
+ #
122
+ # @yieldparam [LocalTarget] target the available target
123
+ # @return [void]
124
+ def each_available_target
125
+ return enum_for(__method__) if !block_given?
126
+ each_available_autosync_target do |path, t|
127
+ op = SyncAll.new(path, config_dir: config_dir)
128
+ op.each_target do |target|
129
+ yield(target)
130
+ end
131
+ end
132
+ end
133
+
53
134
  def add(target)
54
135
  targets[target.partition_uuid] ||= Array.new
55
136
  targets[target.partition_uuid] << target
@@ -67,37 +148,10 @@ module Snapsync
67
148
 
68
149
  def run(period: 60)
69
150
  while true
70
- partitions.poll
71
- partitions.known_partitions.each do |uuid, fs|
72
- mp = fs['MountPoints'].first
73
- targets[uuid].each do |t|
74
- if !mp
75
- if t.automount
76
- Snapsync.info "partition #{t.partition_uuid} is present, but not mounted, automounting"
77
- begin
78
- mp = fs.Mount([]).first
79
- rescue Exception => e
80
- Snapsync.warn "failed to mount, ignoring this target"
81
- next
82
- end
83
- mp = Pathname.new(mp)
84
- mounted = true
85
- else
86
- Snapsync.info "partition #{t.partition_uuid} is present, but not mounted and automount is false. Ignoring"
87
- next
88
- end
89
- else
90
- mp = Pathname.new(mp[0..-2].pack("U*"))
91
- end
92
-
93
- full_path = mp + t.path
94
- Snapsync.info "sync-all on #{mp + t.path} (partition #{t.partition_uuid})"
95
- op = SyncAll.new(mp + t.path, config_dir: config_dir)
96
- op.run
97
- if mounted
98
- fs.Unmount([])
99
- end
100
- end
151
+ each_available_autosync_target do |path, t|
152
+ Snapsync.info "sync-all on #{path} (partition #{t.partition_uuid})"
153
+ op = SyncAll.new(path, config_dir: config_dir)
154
+ op.run
101
155
  end
102
156
  Snapsync.info "done all declared autosync partitions, sleeping #{period}s"
103
157
  sleep period
@@ -10,7 +10,7 @@ module Snapsync
10
10
 
11
11
  def cleanup(target, dry_run: false)
12
12
  snapshots = target.each_snapshot.to_a
13
- filtered_snapshots = policy.filter_snapshots_to_sync(target, snapshots).to_set
13
+ filtered_snapshots = policy.filter_snapshots(snapshots).to_set
14
14
 
15
15
  if filtered_snapshots.any? { |s| s.synchronization_point? }
16
16
  raise InvalidPolicy, "#{policy} returned a snapsync synchronization point in its results"
@@ -23,9 +23,6 @@ module Snapsync
23
23
  last_sync_point = snapshots.
24
24
  sort_by(&:num).reverse.
25
25
  find { |s| s.synchronization_point_for?(target) }
26
- if !last_sync_point
27
- binding.pry
28
- end
29
26
  filtered_snapshots << last_sync_point
30
27
  filtered_snapshots = filtered_snapshots.to_set
31
28
 
@@ -34,6 +31,9 @@ module Snapsync
34
31
  target.delete(s, dry_run: dry_run)
35
32
  end
36
33
  end
34
+
35
+ Snapsync.info "Waiting for subvolumes to be deleted"
36
+ IO.popen(["btrfs", "subvolume", "sync", err: '/dev/null']).read
37
37
  end
38
38
  end
39
39
  end
@@ -22,15 +22,48 @@ module Snapsync
22
22
  Snapsync.logger.level = 'DEBUG'
23
23
  end
24
24
  end
25
+
26
+ # Resolves a path (or nil) into a list of snapsync targets and
27
+ # yields them
28
+ #
29
+ # @param [String,nil] dir the path the user gave, or nil if all
30
+ # available auto-sync paths should be processed. If the directory is
31
+ # a target, it is yield as-is. It can also be the root of a sync-all
32
+ # target (with proper snapsync target as subdirectories whose name
33
+ # matches the snapper configurations)
34
+ #
35
+ # @yieldparam [LocalTarget] target
36
+ def each_target(dir = nil)
37
+ return enum_for(__method__) if !block_given?
38
+ if dir
39
+ dir = Pathname.new(dir)
40
+ begin
41
+ return yield(LocalTarget.new(dir, create_if_needed: false))
42
+ rescue LocalTarget::InvalidTargetPath
43
+ end
44
+
45
+ SyncAll.new(dir).each_target do |target|
46
+ yield(target)
47
+ end
48
+ else
49
+ autosync = AutoSync.load_default
50
+ autosync.each_available_target do |target|
51
+ yield(target)
52
+ end
53
+ end
54
+ end
25
55
  end
26
56
 
27
57
  desc 'sync CONFIG DIR', 'synchronizes the snapper configuration CONFIG with the snapsync target DIR'
58
+ option :autoclean, type: :boolean, default: nil,
59
+ desc: 'whether the target should be cleaned of obsolete snapshots',
60
+ long_desc: "The default is to use the value specified in the target's configuration file. This command line option allows to override the default"
28
61
  def sync(config_name, dir)
29
62
  handle_class_options
30
63
 
31
64
  config = config_from_name(config_name)
32
65
  target = LocalTarget.new(Pathname.new(dir))
33
- LocalSync.new(config, target).sync
66
+ Sync.new(config, target, autoclean: options[:autoclean]).run
34
67
  end
35
68
 
36
69
  desc 'sync-all DIR', 'synchronizes all snapper configurations into corresponding subdirectories of DIR'
@@ -120,7 +153,7 @@ policy for more information
120
153
 
121
154
  autosync = AutoSync.new
122
155
  autosync.load_config(conf_path)
123
- exists = autosync.each_target.find do |t|
156
+ exists = autosync.each_autosync_target.find do |t|
124
157
  t.partition_uuid == uuid && t.path.cleanpath == relative.cleanpath
125
158
  end
126
159
  if exists
@@ -163,15 +196,10 @@ for 10 days). snapsync understands the following period names: year month day ho
163
196
  EOD
164
197
  def policy(dir, type, *options)
165
198
  handle_class_options
166
-
167
- dir = Pathname.new(dir)
168
- if !dir.exist?
169
- dir.mkpath
199
+ each_target(dir) do |target|
200
+ target.change_policy(type, options)
201
+ target.write_config
170
202
  end
171
-
172
- target = LocalTarget.new(dir)
173
- target.change_policy(type, options)
174
- target.write_config
175
203
  end
176
204
 
177
205
  desc 'destroy DIR', 'destroys a snapsync target'
@@ -193,10 +221,29 @@ While it can easily be done manually, this command makes sure that the snapshots
193
221
  option :config_file, desc: "path to the config file (defaults to /etc/snapsync.conf)",
194
222
  default: '/etc/snapsync.conf'
195
223
  def auto_sync
224
+ handle_class_options
196
225
  auto = AutoSync.new(SnapperConfig.default_config_dir)
197
226
  auto.load_config(Pathname.new(options[:config_file]))
198
227
  auto.run
199
228
  end
229
+
230
+ desc 'list [DIR]', 'list the snapshots present on DIR. If DIR is omitted, tries to access all targets defined as auto-sync targets'
231
+ def list(dir = nil)
232
+ handle_class_options
233
+ each_target(dir) do |target|
234
+ puts "== #{target.dir}"
235
+ puts "UUID: #{target.uuid}"
236
+ puts "Enabled: #{target.enabled?}"
237
+ puts "Autoclean: #{target.autoclean?}"
238
+ print "Policy: "
239
+ pp target.sync_policy
240
+
241
+ puts "Snapshots:"
242
+ target.each_snapshot do |s|
243
+ puts " #{s.num} #{s.to_time}"
244
+ end
245
+ end
246
+ end
200
247
  end
201
248
  end
202
249
 
@@ -8,7 +8,7 @@ module Snapsync
8
8
  #
9
9
  # Synchronization policy objects are used by the synchronization passes to
10
10
  # decide which snapshots to copy and which to not copy. They have to provide
11
- # {#filter_snapshots_to_sync}.
11
+ # {#filter_snapshots}.
12
12
  #
13
13
  # This default policy is to copy everything but the snapsync-created
14
14
  # synchronization points that are not involving the current target
@@ -27,7 +27,7 @@ module Snapsync
27
27
  # @param [#uuid] target the target object
28
28
  # @param [Array<Snapshot>] the snapshot candidates
29
29
  # @return [Array<Snapshot>] the snapshots that should be copied
30
- def filter_snapshots_to_sync(target, snapshots)
30
+ def filter_snapshots(snapshots)
31
31
  # Filter out any snapsync-generated snapshot
32
32
  snapshots.find_all { |s| !s.synchronization_point? }
33
33
  end
@@ -179,17 +179,28 @@ module Snapsync
179
179
  sync_snapshot ||= create_synchronization_point
180
180
 
181
181
  target_snapshots = target.each_snapshot.sort_by(&:num)
182
+ nums_on_target = target_snapshots.map(&:num).to_set
182
183
 
183
184
  last_common_snapshot = source_snapshots.find do |s|
184
- target_snapshots.find { |src| src.num == s.num }
185
+ nums_on_target.include?(s.num)
185
186
  end
186
187
  if !last_common_snapshot
187
188
  Snapsync.warn "no common snapshot found, will have to synchronize the first snapshot fully"
188
189
  end
189
190
 
190
- snapshots_to_sync = target.sync_policy.filter_snapshots_to_sync(target, source_snapshots)
191
- snapshots_to_sync.each do |src|
192
- if synchronize_snapshot(target.dir + src.num.to_s, src, parent: last_common_snapshot)
191
+ # Merge source and target snapshots to find out which are needed on
192
+ # the target, and then remove the ones that are already present.
193
+ all_snapshots = source_snapshots.find_all { |s| !nums_on_target.include?(s.num) } +
194
+ target_snapshots
195
+ nums_required = target.sync_policy.filter_snapshots(all_snapshots).
196
+ map(&:num).to_set
197
+ source_snapshots.each do |src|
198
+ if !nums_required.include?(src.num)
199
+ if nums_on_target.include?(src.num)
200
+ last_common_snapshot = src
201
+ end
202
+ next
203
+ elsif synchronize_snapshot(target.dir + src.num.to_s, src, parent: last_common_snapshot)
193
204
  last_common_snapshot = src
194
205
  end
195
206
  end
@@ -30,7 +30,8 @@ module Snapsync
30
30
  # Defaults to true
31
31
  def autoclean?; !!@autoclean end
32
32
 
33
- class InvalidUUIDError < RuntimeError; end
33
+ class InvalidTargetPath < RuntimeError; end
34
+ class InvalidUUIDError < InvalidTargetPath; end
34
35
  class NoUUIDError < InvalidUUIDError; end
35
36
 
36
37
  def initialize(dir, create_if_needed: true)
@@ -0,0 +1,50 @@
1
+ module Snapsync
2
+ # Single-target synchronization
3
+ class Sync
4
+ attr_reader :config
5
+
6
+ attr_reader :target
7
+
8
+ def initialize(config, target, autoclean: nil)
9
+ @config = config
10
+ @target = target
11
+ @autoclean =
12
+ if autoclean.nil? then target.autoclean?
13
+ else autoclean
14
+ end
15
+ end
16
+
17
+ # Whether the target should be cleaned after synchronization.
18
+ #
19
+ # This is determined either by {#autoclean?} if {.new} was called with
20
+ # true or false, or by the target's own autoclean flag if {.new} was
21
+ # called with nil
22
+ def autoclean?
23
+ @autoclean
24
+ end
25
+
26
+ # The method that performs synchronization
27
+ #
28
+ # One usually wants to call {#run}, which also takes care of running
29
+ # cleanup if {#autoclean?} is true
30
+ def sync
31
+ LocalSync.new(config, target).sync
32
+ end
33
+
34
+ def run
35
+ sync
36
+
37
+ if autoclean?
38
+ if target.cleanup
39
+ Snapsync.info "running cleanup for #{target.dir}"
40
+ target.cleanup.cleanup(target)
41
+ else
42
+ Snapsync.info "#{target.sync_policy.class.name} policy set, no cleanup to do for #{target.dir}"
43
+ end
44
+ else
45
+ Snapsync.info "autoclean not set on #{target.dir}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -23,43 +23,35 @@ module Snapsync
23
23
  @autoclean = autoclean
24
24
  end
25
25
 
26
- # Whether the given target should be cleaned after synchronization.
27
- #
28
- # This is determined either by {#autoclean?} if {.new} was called with
29
- # true or false, or by the target's own autoclean flag if {.new} was
30
- # called with nil
31
- def should_autoclean_target?(target)
32
- if @autoclean.nil?
33
- target.autoclean?
34
- else
35
- @autoclean
36
- end
26
+ # Whether the target should be forced to autoclean(true), force to not
27
+ # run cleanup (false) or use their own config file to decide (nil)
28
+ #
29
+ # The default is nil
30
+ #
31
+ # @return [Boolean,nil]
32
+ def autoclean?
33
+ @autoclean
37
34
  end
38
35
 
39
- def run
36
+ # Enumerate the targets available under {#target_dir}
37
+ def each_target
40
38
  SnapperConfig.each_in_dir(config_dir) do |config|
41
39
  dir = target_dir + config.name
42
40
  if !dir.exist?
43
- Snapsync.warn "not synchronizing #{config.name}, there are no corresponding directory in #{target_dir}. Call snapsync init to create a proper target directory"
41
+ Snapsync.warn "no directory for configuration #{config.name} in #{target_dir}"
44
42
  else
45
- target = LocalTarget.new(dir)
46
- if !target.enabled?
47
- Snapsync.warn "not synchronizing #{config.name}, it is disabled"
48
- next
49
- end
43
+ yield(LocalTarget.new(dir))
44
+ end
45
+ end
46
+ end
50
47
 
51
- LocalSync.new(config, target).sync
52
- if should_autoclean_target?(target)
53
- if target.cleanup
54
- Snapsync.info "running cleanup for #{config.name}"
55
- target.cleanup.cleanup(target)
56
- else
57
- Snapsync.info "#{target.sync_policy.class.name} policy set, no cleanup to do for #{config.name}"
58
- end
59
- else
60
- Snapsync.info "autoclean not set on #{config.name}"
61
- end
48
+ def run
49
+ each_target do |target|
50
+ if !target.enabled?
51
+ Snapsync.warn "not synchronizing to #{target.dir}, it is disabled"
52
+ next
62
53
  end
54
+ Sync.new(config, target, autoclean: autoclean?).run
63
55
  end
64
56
  end
65
57
  end
@@ -14,8 +14,8 @@ module Snapsync
14
14
  pp.text "will keep only the latest snapshot"
15
15
  end
16
16
 
17
- # (see DefaultSyncPolicy#filter_snapshots_to_sync)
18
- def filter_snapshots_to_sync(target, snapshots)
17
+ # (see DefaultSyncPolicy#filter_snapshots)
18
+ def filter_snapshots(snapshots)
19
19
  last = snapshots.sort_by(&:num).reverse.
20
20
  find { |s| !s.synchronization_point? }
21
21
  [last]
@@ -91,6 +91,8 @@ module Snapsync
91
91
  def compute_required_snapshots(target_snapshots)
92
92
  keep_flags = Hash.new { |h,k| h[k] = [false, []] }
93
93
 
94
+ target_snapshots = target_snapshots.sort_by(&:num)
95
+
94
96
  # Mark all important snapshots as kept
95
97
  target_snapshots.each do |s|
96
98
  if s.user_data['important'] == 'yes'
@@ -122,7 +124,7 @@ module Snapsync
122
124
 
123
125
  # Finally, guard against race conditions. Always keep all snapshots
124
126
  # between the last-to-keep and the last
125
- target_snapshots.sort_by(&:num).reverse.each do |s|
127
+ target_snapshots.reverse.each do |s|
126
128
  break if keep_flags[s.num][0]
127
129
  keep_flags[s.num][0] = true
128
130
  keep_flags[s.num][1] << "last snapshot"
@@ -130,9 +132,10 @@ module Snapsync
130
132
  keep_flags
131
133
  end
132
134
 
133
- def filter_snapshots_to_sync(target, source_snapshots)
135
+ def filter_snapshots(snapshots)
134
136
  Snapsync.debug do
135
137
  Snapsync.debug "Filtering snapshots according to timeline"
138
+ Snapsync.debug "Snapshots: #{snapshots.map(&:num).sort.join(", ")}"
136
139
  timeline.each do |t|
137
140
  Snapsync.debug " #{t}"
138
141
  end
@@ -140,10 +143,10 @@ module Snapsync
140
143
  end
141
144
 
142
145
  default_policy = DefaultSyncPolicy.new
143
- source_snapshots = default_policy.filter_snapshots_to_sync(target, source_snapshots)
146
+ snapshots = default_policy.filter_snapshots(snapshots)
144
147
 
145
- keep_flags = compute_required_snapshots(source_snapshots)
146
- source_snapshots.sort_by(&:num).find_all do |s|
148
+ keep_flags = compute_required_snapshots(snapshots)
149
+ snapshots.sort_by(&:num).find_all do |s|
147
150
  keep, reason = keep_flags.fetch(s.num, nil)
148
151
  if keep
149
152
  Snapsync.debug "Timeline: selected snapshot #{s.num} #{s.date.to_time}"
@@ -1,3 +1,3 @@
1
1
  module Snapsync
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: snapsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvain Joyeux
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-04 00:00:00.000000000 Z
11
+ date: 2015-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logging
@@ -186,6 +186,7 @@ files:
186
186
  - lib/snapsync/partitions_monitor.rb
187
187
  - lib/snapsync/snapper_config.rb
188
188
  - lib/snapsync/snapshot.rb
189
+ - lib/snapsync/sync.rb
189
190
  - lib/snapsync/sync_all.rb
190
191
  - lib/snapsync/sync_last_policy.rb
191
192
  - lib/snapsync/test.rb