snapsync 0.1.5 → 0.1.6

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