snapsync 0.3.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 [String,nil] dir the path the user gave, or nil if all
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 [LocalTarget] target
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 = Pathname.new(dir)
44
+ dir = Snapsync::path(dir)
40
45
  begin
41
- return yield(nil, LocalTarget.new(dir, create_if_needed: false))
42
- rescue LocalTarget::InvalidTargetPath
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.load_default
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
- partitions = PartitionsMonitor.new
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 = LocalTarget.new(Pathname.new(dir))
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 = Pathname.new(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 CONFIG DIR', 'cleans up the snapsync target DIR based on the policy set by the policy command'
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
- LocalTarget.parse_policy(*policy)
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
- dir = Pathname.new(dir)
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
- LocalTarget.new(path, create_if_needed: false)
193
+ SyncTarget.new(path, create_if_needed: false)
184
194
  Snapsync.info "#{path} was already initialized"
185
- rescue ArgumentError, LocalTarget::NoUUIDError
195
+ rescue ArgumentError, SyncTarget::NoUUIDError
186
196
  path.mkpath
187
- target = LocalTarget.new(path)
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(Pathname.new(dir))
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.path.cleanpath == relative.cleanpath
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 and timeline
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 = Pathname.new(dir)
280
- target = LocalTarget.new(target_dir, create_if_needed: false)
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 |_, target|
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
- def initialize
11
- dbus = DBus::SystemBus.instance
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
- if mount_points.include?(dir.to_s)
48
- return uuid, rel
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
- all[uuid] = dev['org.freedesktop.UDisks2.Filesystem']
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['org.freedesktop.UDisks2.Block'] && dev['org.freedesktop.UDisks2.Filesystem']
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