snapsync 0.3.8 → 0.4.0
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 +5 -5
- data/README.md +15 -2
- data/lib/snapsync/auto_sync.rb +75 -39
- data/lib/snapsync/btrfs.rb +123 -35
- data/lib/snapsync/btrfs_subvolume.rb +103 -0
- data/lib/snapsync/cleanup.rb +1 -0
- data/lib/snapsync/cli.rb +73 -42
- data/lib/snapsync/partitions_monitor.rb +74 -7
- data/lib/snapsync/remote_pathname.rb +330 -0
- data/lib/snapsync/snapper_config.rb +6 -0
- data/lib/snapsync/snapshot.rb +32 -3
- data/lib/snapsync/{local_sync.rb → snapshot_transfer.rb} +27 -28
- data/lib/snapsync/ssh_popen.rb +107 -0
- data/lib/snapsync/sync.rb +8 -4
- data/lib/snapsync/sync_all.rb +1 -1
- data/lib/snapsync/{local_target.rb → sync_target.rb} +14 -4
- data/lib/snapsync/test.rb +2 -9
- data/lib/snapsync/util.rb +18 -0
- data/lib/snapsync/version.rb +1 -1
- data/lib/snapsync.rb +31 -2
- data/snapsync.gemspec +13 -6
- data/snapsync.service +4 -1
- data/snapsync.timer +10 -0
- metadata +117 -33
data/lib/snapsync/cli.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'thor'
|
2
2
|
require 'snapsync'
|
3
|
+
require 'set'
|
3
4
|
|
4
5
|
module Snapsync
|
5
6
|
class CLI < Thor
|
6
7
|
class_option :debug, type: :boolean, default: false
|
8
|
+
class_option :ssh_debug, type: :boolean, default: false
|
7
9
|
|
8
10
|
no_commands do
|
9
11
|
def config_from_name(name)
|
@@ -21,74 +23,78 @@ module Snapsync
|
|
21
23
|
if options[:debug]
|
22
24
|
Snapsync.logger.level = 'DEBUG'
|
23
25
|
end
|
26
|
+
|
27
|
+
Snapsync.SSH_DEBUG = options[:ssh_debug]
|
24
28
|
end
|
25
29
|
|
26
30
|
# Resolves a path (or nil) into a list of snapsync targets and
|
27
31
|
# yields them
|
28
32
|
#
|
29
|
-
# @param [
|
33
|
+
# @param [AgnosticPath,nil] dir the path the user gave, or nil if all
|
30
34
|
# available auto-sync paths should be processed. If the directory is
|
31
35
|
# a target, it is yield as-is. It can also be the root of a sync-all
|
32
36
|
# target (with proper snapsync target as subdirectories whose name
|
33
37
|
# matches the snapper configurations)
|
34
38
|
#
|
35
|
-
# @yieldparam [
|
39
|
+
# @yieldparam [SnapperConfig] config
|
40
|
+
# @yieldparam [SyncTarget] target
|
36
41
|
def each_target(dir = nil)
|
37
42
|
return enum_for(__method__) if !block_given?
|
38
43
|
if dir
|
39
|
-
dir =
|
44
|
+
dir = Snapsync::path(dir)
|
40
45
|
begin
|
41
|
-
return yield(nil,
|
42
|
-
rescue
|
46
|
+
return yield(nil, SyncTarget.new(dir, create_if_needed: false))
|
47
|
+
rescue SyncTarget::InvalidTargetPath
|
43
48
|
end
|
44
49
|
|
45
50
|
SyncAll.new(dir).each_target do |config, target|
|
46
51
|
yield(config, target)
|
47
52
|
end
|
48
53
|
else
|
49
|
-
autosync = AutoSync.
|
54
|
+
autosync = AutoSync.new
|
50
55
|
autosync.each_available_target do |config, target|
|
51
56
|
yield(config, target)
|
52
57
|
end
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
61
|
+
# @return [String, Snapsync::Path, Pathname] uuid, mountpoint, relative
|
56
62
|
def partition_of(dir)
|
57
|
-
|
58
|
-
PartitionsMonitor.new.partition_of(dir)
|
63
|
+
PartitionsMonitor.new(dir).partition_of(dir)
|
59
64
|
end
|
60
65
|
end
|
61
66
|
|
62
|
-
desc 'sync CONFIG DIR', 'synchronizes the snapper configuration CONFIG with the snapsync target DIR'
|
67
|
+
desc 'sync <CONFIG> <DIR>', 'synchronizes the snapper configuration CONFIG with the snapsync target DIR'
|
63
68
|
option :autoclean, type: :boolean, default: nil,
|
64
69
|
desc: 'whether the target should be cleaned of obsolete snapshots',
|
65
70
|
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"
|
66
71
|
def sync(config_name, dir)
|
67
72
|
handle_class_options
|
73
|
+
dir = Snapsync::path(dir)
|
68
74
|
|
69
75
|
config = config_from_name(config_name)
|
70
|
-
target =
|
76
|
+
target = SyncTarget.new(dir)
|
71
77
|
Sync.new(config, target, autoclean: options[:autoclean]).run
|
72
78
|
end
|
73
79
|
|
74
|
-
desc 'sync-all DIR', 'synchronizes all snapper configurations into corresponding subdirectories of DIR'
|
80
|
+
desc 'sync-all <DIR>', 'synchronizes all snapper configurations into corresponding subdirectories of DIR'
|
75
81
|
option :autoclean, type: :boolean, default: nil,
|
76
82
|
desc: 'whether the target should be cleaned of obsolete snapshots',
|
77
83
|
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"
|
78
84
|
def sync_all(dir)
|
79
85
|
handle_class_options
|
80
86
|
|
81
|
-
dir =
|
87
|
+
dir = Snapsync::path(dir)
|
82
88
|
op = SyncAll.new(dir, config_dir: SnapperConfig.default_config_dir, autoclean: options[:autoclean])
|
83
89
|
op.run
|
84
90
|
end
|
85
91
|
|
86
|
-
desc 'cleanup
|
92
|
+
desc 'cleanup [--debug] [--dry-run] <CONFIG_DIR>', 'cleans up the snapsync target DIR based on the policy set by the policy command'
|
87
93
|
option :dry_run, type: :boolean, default: false
|
88
94
|
def cleanup(dir)
|
89
95
|
handle_class_options
|
96
|
+
target = SyncTarget.new(Snapsync::path(dir))
|
90
97
|
|
91
|
-
target = LocalTarget.new(Pathname.new(dir))
|
92
98
|
if target.cleanup
|
93
99
|
target.cleanup.cleanup(target, dry_run: options[:dry_run])
|
94
100
|
else
|
@@ -107,16 +113,18 @@ module Snapsync
|
|
107
113
|
[args.shift, args]
|
108
114
|
end
|
109
115
|
|
110
|
-
|
116
|
+
SyncTarget.parse_policy(*policy)
|
111
117
|
return *policy
|
112
118
|
end
|
113
119
|
end
|
114
120
|
|
115
|
-
desc 'init [NAME] DIR [POLICY]', 'creates a synchronization target, optionally adding it to the auto-sync targets and specifying the synchronization and cleanup policies'
|
121
|
+
desc 'init [NAME] <DIR> [POLICY]', 'creates a synchronization target, optionally adding it to the auto-sync targets and specifying the synchronization and cleanup policies'
|
116
122
|
long_desc <<-EOD
|
117
123
|
NAME must be provided if DIR is to be added to the auto-sync targets (which
|
118
124
|
is the default).
|
119
125
|
|
126
|
+
DIR can be a remote filesystem path in scp-like format ( [user[:password]@]host:/path/to/drive/snapsync )
|
127
|
+
|
120
128
|
By default, the default policy is used. To change this, provide additional
|
121
129
|
arguments as would be expected by the policy subcommand. Run snapsync help
|
122
130
|
policy for more information
|
@@ -145,7 +153,9 @@ policy for more information
|
|
145
153
|
end
|
146
154
|
dir, *policy = *args
|
147
155
|
end
|
148
|
-
|
156
|
+
|
157
|
+
dir = Snapsync::path(dir)
|
158
|
+
remote = dir.instance_of? RemotePathname
|
149
159
|
|
150
160
|
# Parse the policy option early to avoid breaking later
|
151
161
|
begin
|
@@ -180,11 +190,11 @@ policy for more information
|
|
180
190
|
|
181
191
|
dirs.each do |path|
|
182
192
|
begin
|
183
|
-
|
193
|
+
SyncTarget.new(path, create_if_needed: false)
|
184
194
|
Snapsync.info "#{path} was already initialized"
|
185
|
-
rescue ArgumentError,
|
195
|
+
rescue ArgumentError, SyncTarget::NoUUIDError
|
186
196
|
path.mkpath
|
187
|
-
target =
|
197
|
+
target = SyncTarget.new(path)
|
188
198
|
target.change_policy(*policy)
|
189
199
|
target.write_config
|
190
200
|
Snapsync.info "initialized #{path} as a snapsync target"
|
@@ -198,21 +208,18 @@ policy for more information
|
|
198
208
|
end
|
199
209
|
end
|
200
210
|
|
201
|
-
desc 'auto-add NAME DIR', "add DIR to the set of targets for auto-sync"
|
211
|
+
desc 'auto-add [NAME] <DIR>', "add DIR to the set of targets for auto-sync"
|
202
212
|
option :automount, type: :boolean, default: true,
|
203
213
|
desc: 'whether the supporting partition should be auto-mounted by snapsync when needed or not (the default is yes)'
|
204
214
|
option :config_file, default: '/etc/snapsync.conf',
|
205
215
|
desc: 'the configuration file that should be updated'
|
206
216
|
def auto_add(name, dir)
|
207
|
-
uuid, relative = partition_of(
|
217
|
+
uuid, mountpoint, relative = partition_of(Snapsync::path(dir))
|
208
218
|
conf_path = Pathname.new(options[:config_file])
|
209
219
|
|
210
|
-
autosync = AutoSync.new
|
211
|
-
if conf_path.exist?
|
212
|
-
autosync.load_config(conf_path)
|
213
|
-
end
|
220
|
+
autosync = AutoSync.new snapsync_config_file: conf_path
|
214
221
|
exists = autosync.each_autosync_target.find do |t|
|
215
|
-
t.partition_uuid == uuid && t.
|
222
|
+
t.partition_uuid == uuid && t.mountpoint.cleanpath == mountpoint.cleanpath && t.relative.cleanpath == relative.cleanpath
|
216
223
|
end
|
217
224
|
if exists
|
218
225
|
if !exists.name
|
@@ -231,7 +238,7 @@ policy for more information
|
|
231
238
|
end
|
232
239
|
exists.name ||= name
|
233
240
|
else
|
234
|
-
autosync.add AutoSync::AutoSyncTarget.new(uuid, relative, options[:automount], name)
|
241
|
+
autosync.add AutoSync::AutoSyncTarget.new(uuid, mountpoint, relative, options[:automount], name)
|
235
242
|
end
|
236
243
|
autosync.write_config(conf_path)
|
237
244
|
end
|
@@ -239,29 +246,32 @@ policy for more information
|
|
239
246
|
desc 'auto-remove NAME', "remove a target from auto-sync by name"
|
240
247
|
def auto_remove(name)
|
241
248
|
conf_path = Pathname.new('/etc/snapsync.conf')
|
242
|
-
autosync = AutoSync.new
|
243
|
-
autosync.load_config(conf_path)
|
249
|
+
autosync = AutoSync.new snapsync_config_file: conf_path
|
244
250
|
autosync.remove(name: name)
|
245
251
|
autosync.write_config(conf_path)
|
246
252
|
end
|
247
253
|
|
248
|
-
desc 'policy DIR TYPE [OPTIONS]', 'sets the synchronization and cleanup policy for the given target or targets'
|
254
|
+
desc 'policy <DIR> <TYPE> [OPTIONS]', 'sets the synchronization and cleanup policy for the given target or targets'
|
249
255
|
long_desc <<-EOD
|
250
256
|
This command sets the policy used to decide which snapshots to synchronize to
|
251
257
|
the target, and which to not synchronize.
|
252
258
|
|
253
|
-
Three policy types can be used: default, last
|
259
|
+
TYPE: Three policy types can be used: default, last, or timeline
|
254
260
|
|
255
|
-
The default policy takes no argument. It will synchronize all snapshots present in the source, and do no cleanup
|
261
|
+
The 'default' policy takes no argument. It will synchronize all snapshots present in the source, and do no cleanup
|
256
262
|
|
257
|
-
The last policy takes no argument. It will synchronize (and keep) only the last snapshot
|
263
|
+
The 'last' policy takes no argument. It will synchronize (and keep) only the last snapshot
|
258
264
|
|
259
|
-
The timeline policy takes periods of time as argument (as e.g. day 10 or month 20). It will keep at least
|
265
|
+
The 'timeline' policy takes periods of time as argument (as e.g. day 10 or month 20). It will keep at least
|
260
266
|
one snapshot for each period, and for the duration specified (day 10 tells to keep one snapshot per day
|
261
|
-
for 10 days). snapsync understands the following period names: year month day hour.
|
267
|
+
for 10 days). snapsync understands the following period names: year, month, week, day, hour.
|
268
|
+
|
269
|
+
OPTIONS := { year {int} | month {int} | week {int} | day {int} | hour {int} }
|
262
270
|
EOD
|
263
271
|
def policy(dir, type, *options)
|
264
272
|
handle_class_options
|
273
|
+
dir = Snapsync::path(dir)
|
274
|
+
|
265
275
|
# Parse the policy early to avoid breaking later
|
266
276
|
policy = normalize_policy([type, *options])
|
267
277
|
each_target(dir) do |_, target|
|
@@ -270,14 +280,15 @@ for 10 days). snapsync understands the following period names: year month day ho
|
|
270
280
|
end
|
271
281
|
end
|
272
282
|
|
273
|
-
desc 'destroy DIR', 'destroys a snapsync target'
|
283
|
+
desc 'destroy <DIR>', 'destroys a snapsync target'
|
274
284
|
long_desc <<-EOD
|
275
285
|
While it can easily be done manually, this command makes sure that the snapshots are properly deleted
|
276
286
|
EOD
|
277
287
|
def destroy(dir)
|
278
288
|
handle_class_options
|
279
|
-
target_dir =
|
280
|
-
|
289
|
+
target_dir = Snapsync::path(dir)
|
290
|
+
|
291
|
+
target = SyncTarget.new(target_dir, create_if_needed: false)
|
281
292
|
snapshots = target.each_snapshot.to_a
|
282
293
|
snapshots.sort_by(&:num).each do |s|
|
283
294
|
target.delete(s)
|
@@ -292,8 +303,7 @@ While it can easily be done manually, this command makes sure that the snapshots
|
|
292
303
|
default: '/etc/snapsync.conf'
|
293
304
|
def auto_sync
|
294
305
|
handle_class_options
|
295
|
-
auto = AutoSync.new(SnapperConfig.default_config_dir)
|
296
|
-
auto.load_config(Pathname.new(options[:config_file]))
|
306
|
+
auto = AutoSync.new(SnapperConfig.default_config_dir, snapsync_config_file: Pathname.new(options[:config_file]))
|
297
307
|
if options[:one_shot]
|
298
308
|
auto.sync
|
299
309
|
else
|
@@ -304,18 +314,39 @@ While it can easily be done manually, this command makes sure that the snapshots
|
|
304
314
|
desc 'list [DIR]', 'list the snapshots present on DIR. If DIR is omitted, tries to access all targets defined as auto-sync targets'
|
305
315
|
def list(dir = nil)
|
306
316
|
handle_class_options
|
307
|
-
each_target(dir) do |
|
317
|
+
each_target(dir) do |config, target|
|
308
318
|
puts "== #{target.dir}"
|
309
319
|
puts "UUID: #{target.uuid}"
|
310
320
|
puts "Enabled: #{target.enabled?}"
|
311
321
|
puts "Autoclean: #{target.autoclean?}"
|
322
|
+
puts "Snapper config: #{config.name}"
|
312
323
|
print "Policy: "
|
313
324
|
pp target.sync_policy
|
314
325
|
|
326
|
+
snapshots_seen = Set.new
|
327
|
+
|
328
|
+
# @type [Snapshot]
|
329
|
+
last_snapshot = nil
|
315
330
|
puts "Snapshots:"
|
316
331
|
target.each_snapshot do |s|
|
332
|
+
snapshots_seen.add(s.num)
|
333
|
+
last_snapshot = s
|
317
334
|
puts " #{s.num} #{s.to_time}"
|
318
335
|
end
|
336
|
+
|
337
|
+
puts " [transferrable:]"
|
338
|
+
config.each_snapshot do |s|
|
339
|
+
if not snapshots_seen.include? s.num
|
340
|
+
delta = s.size_diff_from(last_snapshot)
|
341
|
+
puts " #{s.num} #{s.to_time} => from: #{last_snapshot.num} delta: " \
|
342
|
+
+"#{Snapsync.human_readable_size(delta)}"
|
343
|
+
|
344
|
+
# If the delta was 0, then the data already exists on remote.
|
345
|
+
if delta > 0
|
346
|
+
last_snapshot = s
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
319
350
|
end
|
320
351
|
end
|
321
352
|
end
|
@@ -4,11 +4,46 @@ module Snapsync
|
|
4
4
|
|
5
5
|
attr_reader :dirty
|
6
6
|
|
7
|
+
# @return [Set<String>]
|
7
8
|
attr_reader :monitored_partitions
|
9
|
+
# @return [Hash<String, Dev>]
|
8
10
|
attr_reader :known_partitions
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
# @return [Hash<String, DBus::ProxyObjectInterface>]
|
13
|
+
attr_reader :partition_table
|
14
|
+
|
15
|
+
# @param [RemotePathname] machine Remote machine to connect to
|
16
|
+
def initialize(machine = nil)
|
17
|
+
if machine.nil?
|
18
|
+
dbus = DBus::SystemBus.instance
|
19
|
+
else
|
20
|
+
sock_path = '/tmp/snapsync_%04d_remote.sock' % rand(10000)
|
21
|
+
|
22
|
+
|
23
|
+
ready = Concurrent::AtomicBoolean.new(false)
|
24
|
+
@ssh_thr = Thread.new do
|
25
|
+
machine.dup_ssh do |ssh|
|
26
|
+
@ssh = ssh
|
27
|
+
if Snapsync.SSH_DEBUG
|
28
|
+
log = Logger.new(STDOUT)
|
29
|
+
log.level = Logger::DEBUG
|
30
|
+
ssh.logger = log
|
31
|
+
ssh.logger.sev_threshold=Logger::Severity::DEBUG
|
32
|
+
end
|
33
|
+
ssh.forward.local_socket(sock_path, '/var/run/dbus/system_bus_socket')
|
34
|
+
ObjectSpace.define_finalizer(@ssh, proc {
|
35
|
+
File.delete sock_path
|
36
|
+
})
|
37
|
+
ready.make_true
|
38
|
+
ssh.loop { true }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
while ready.false?
|
42
|
+
sleep 0.001
|
43
|
+
end
|
44
|
+
|
45
|
+
dbus = DBus::RemoteBus.new "unix:path=#{sock_path}"
|
46
|
+
end
|
12
47
|
@udisk = dbus.service('org.freedesktop.UDisks2')
|
13
48
|
udisk.introspect
|
14
49
|
|
@@ -22,12 +57,30 @@ module Snapsync
|
|
22
57
|
|
23
58
|
@monitored_partitions = Set.new
|
24
59
|
@known_partitions = Hash.new
|
60
|
+
@partition_table = Hash.new
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param [DBus::ProxyObjectInterface] fs
|
64
|
+
# @return [Array<String>]
|
65
|
+
def mountpoints(fs)
|
66
|
+
raise "Not mounted?" if fs.nil?
|
67
|
+
mount_points = fs['MountPoints'].map do |str|
|
68
|
+
str[0..-2].pack("U*")
|
69
|
+
end
|
70
|
+
return mount_points
|
71
|
+
end
|
72
|
+
|
73
|
+
def mountpoint_of_uuid(partition_uuid)
|
74
|
+
mounts = mountpoints(known_partitions[partition_uuid])
|
75
|
+
raise "Ambiguous mountpoints: #{mounts}" if mounts.length > 1
|
76
|
+
mounts[0]
|
25
77
|
end
|
26
78
|
|
27
79
|
def monitor_for(partition_uuid)
|
28
80
|
monitored_partitions << partition_uuid.to_str
|
29
81
|
end
|
30
82
|
|
83
|
+
# @return [String, Snapsync::Path, Pathname] uuid dir rel
|
31
84
|
def partition_of(dir)
|
32
85
|
rel = Pathname.new("")
|
33
86
|
dir = dir.expand_path
|
@@ -36,16 +89,22 @@ module Snapsync
|
|
36
89
|
dir = dir.dirname
|
37
90
|
end
|
38
91
|
|
92
|
+
# Collect partitions list from udisk
|
93
|
+
parts = []
|
39
94
|
each_partition_with_filesystem do |name, dev|
|
40
95
|
partition = dev['org.freedesktop.UDisks2.Block']
|
41
96
|
uuid = partition['IdUUID']
|
42
|
-
|
43
97
|
fs = dev['org.freedesktop.UDisks2.Filesystem']
|
44
98
|
mount_points = fs['MountPoints'].map do |str|
|
45
99
|
str[0..-2].pack("U*")
|
46
100
|
end
|
47
|
-
|
48
|
-
|
101
|
+
parts.push([name, uuid, mount_points])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Find any partition that is a parent of the folder we are looking at
|
105
|
+
parts.each do |name, uuid, mount_points|
|
106
|
+
if mount_points.include?(dir.path_part)
|
107
|
+
return uuid, dir, rel
|
49
108
|
end
|
50
109
|
end
|
51
110
|
raise ArgumentError, "cannot guess the partition UUID of the mountpoint #{dir} for #{dir + rel}"
|
@@ -81,11 +140,19 @@ module Snapsync
|
|
81
140
|
|
82
141
|
all = Hash.new
|
83
142
|
each_partition_with_filesystem do |name, dev|
|
143
|
+
# @type [DBus::ProxyObjectInterface]
|
84
144
|
partition = dev['org.freedesktop.UDisks2.Block']
|
145
|
+
# @type [String]
|
85
146
|
uuid = partition['IdUUID']
|
86
147
|
|
87
148
|
if monitored_partitions.include?(uuid)
|
88
|
-
|
149
|
+
fs = dev['org.freedesktop.UDisks2.Filesystem']
|
150
|
+
|
151
|
+
# If it is a btrfs raid, it will have multiple partitions with the same uuid, but only one will be
|
152
|
+
# mounted.
|
153
|
+
next if all.has_key?(uuid) and all[uuid]['MountPoints'].size > 0
|
154
|
+
|
155
|
+
all[uuid] = fs
|
89
156
|
end
|
90
157
|
end
|
91
158
|
|
@@ -116,7 +183,7 @@ module Snapsync
|
|
116
183
|
return enum_for(__method__) if !block_given?
|
117
184
|
udisk.root['org']['freedesktop']['UDisks2']['block_devices'].each do |device_name, _|
|
118
185
|
dev = udisk.object("/org/freedesktop/UDisks2/block_devices/#{device_name}")
|
119
|
-
if dev
|
186
|
+
if dev.has_iface?('org.freedesktop.UDisks2.Block') && dev.has_iface?('org.freedesktop.UDisks2.Filesystem')
|
120
187
|
yield(device_name, dev)
|
121
188
|
end
|
122
189
|
end
|