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