snapsync 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9d6c814ce6fcbbbeb80bfc50d4f980f041bc3dc7
4
+ data.tar.gz: 895b0008fcbc01edb78c9455d0eaf580e5f762ae
5
+ SHA512:
6
+ metadata.gz: b3ce31df068bbd6d05afd5a29d0175c7dfcdb8cf21489ffb36a0cf6eed68cce46c7618fa79b85f4c46669eb06afc62a6e7d16048eb10ebf88a71121e1d59260b
7
+ data.tar.gz: 92c04cdf3c5c4feb7efbc3595a0c439ad2fcfbc05dbafafc0d678df5f18668d126f126f160bd615e7c8dfb17d9d1a946e5d7dcc1a206545e25d36c05a77f144d
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install: gem install bundler -v 1.10.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snapsync.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Sylvain Joyeux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ # Snapsync
2
+
3
+ A synchronization tool for snapper
4
+
5
+ This gem implements snapper-based backup, by allowing you to synchronize a
6
+ snapper snapshot directory to a different location. It uses btrfs send and
7
+ receive to achieve it
8
+
9
+ ## Installation
10
+
11
+ Run
12
+
13
+ $ gem install snapsync
14
+
15
+ ## Usage
16
+
17
+ To synchronize the snapshots of the 'home' snapper configuration to an existing
18
+ directory, run
19
+
20
+ $ snapsync home /media/backup
21
+
22
+ Snapsync uses sudo to get root access. If you wish to not run it as root, you
23
+ will need to change the snapper permissions to give read access to all the
24
+ snapper shapshots, e.g.
25
+
26
+ $ chmod go+rx /.snapshots
27
+ $ chmod go+r /.snapshots/*/info.xml
28
+
29
+ In addition, sudo will ask for your root password when applicable. If you wish
30
+ to fully automate, you will need to allow snapsync to run the btrfs tool without
31
+ password in the sudoers file.
32
+
33
+ ## Development
34
+
35
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
36
+
37
+ ## Contributing
38
+
39
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/snapsync.
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
44
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,3 @@
1
+ #! /usr/bin/env ruby
2
+ require 'snapsync/cli'
3
+ Snapsync::CLI.start(ARGV)
@@ -0,0 +1,92 @@
1
+ require 'pathname'
2
+ require 'logging'
3
+ require 'pp'
4
+ require 'securerandom'
5
+ require 'rexml/document'
6
+ require 'dbus'
7
+ require 'concurrent'
8
+
9
+ require "snapsync/version"
10
+ require "snapsync/exceptions"
11
+ require "snapsync/snapper_config"
12
+ require "snapsync/snapshot"
13
+ require "snapsync/local_target"
14
+ require "snapsync/local_sync"
15
+ require 'snapsync/cleanup'
16
+
17
+ require 'snapsync/default_sync_policy'
18
+ require 'snapsync/timeline_sync_policy'
19
+ require 'snapsync/sync_last_policy'
20
+
21
+ require 'snapsync/partitions_monitor'
22
+ require 'snapsync/sync_all'
23
+ require 'snapsync/auto_sync'
24
+
25
+ module Logging
26
+ module Installer
27
+ def logger
28
+ @logger ||= Logging.logger[self]
29
+ end
30
+ end
31
+
32
+ module Forwarder
33
+ ::Logging::LEVELS.each do |name, _|
34
+ puts name
35
+ define_method name do |*args, &block|
36
+ logger.send(name, *args, &block)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ class Module
43
+ def install_root_logging(level: 'INFO', forward: true, &block)
44
+ extend Logging::Installer
45
+ if block_given?
46
+ yield(logger)
47
+ else
48
+ logger.add_appenders Logging.appenders.stdout
49
+ end
50
+
51
+ if forward
52
+ singleton_class.class_eval do
53
+ install_logging_forwarder
54
+ end
55
+ end
56
+ logger.level = level
57
+ end
58
+
59
+ def install_logging_forwarder
60
+ ::Logging::LEVELS.each do |name, _|
61
+ define_method name do |*args, &block|
62
+ logger.send(name, *args, &block)
63
+ end
64
+ end
65
+ end
66
+
67
+ def install_logging(forward: true, &block)
68
+ extend Logging::Installer
69
+ if forward
70
+ singleton_class.class_eval do
71
+ install_logging_forwarder
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ class Class
78
+ def install_logging(forward: true, on_instance: false)
79
+ super(forward: forward)
80
+ if on_instance
81
+ include Logging::Installer
82
+ if forward
83
+ install_logging_forwarder
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ module Snapsync
90
+ install_root_logging(forward: true)
91
+ end
92
+
@@ -0,0 +1,74 @@
1
+ module Snapsync
2
+ # Implementation of the auto-sync feature
3
+ #
4
+ # This class implements the 'snapsync auto' functionality. It monitors for
5
+ # partition availability, and will run sync-all on each (declared) targets
6
+ # when they are available, optionally auto-mounting them
7
+ class AutoSync
8
+ AutoSyncTarget = Struct.new :partition_uuid, :path, :automount
9
+
10
+ attr_reader :config_dir
11
+ attr_reader :targets
12
+ attr_reader :partitions
13
+
14
+ def initialize(config_dir = SnapperConfig.default_config_dir)
15
+ @config_dir = config_dir
16
+ @targets = Hash.new
17
+ @partitions = PartitionsMonitor.new
18
+ end
19
+
20
+ def load_config(path)
21
+ conf = YAML.load(path.read) || Array.new
22
+ parse_config(conf)
23
+ end
24
+
25
+ def parse_config(conf)
26
+ conf.each do |hash|
27
+ target = AutoSyncTarget.new
28
+ hash.each { |k, v| target[k] = v }
29
+ add(target)
30
+ end
31
+ end
32
+
33
+ def add(target)
34
+ targets[target.partition_uuid] ||= Array.new
35
+ targets[target.partition_uuid] << target
36
+ partitions.monitor_for(target.partition_uuid)
37
+ end
38
+
39
+ def run(period: 60)
40
+ while true
41
+ partitions.poll
42
+ partitions.known_partitions.each do |uuid, fs|
43
+ mp = fs['MountPoints'].first
44
+ targets[uuid].each do |t|
45
+ if !mp
46
+ if t.automount
47
+ Snapsync.info "partition #{t.partition_uuid} is present, but not mounted, automounting"
48
+ mp = fs.Mount([]).first
49
+ mp = Pathname.new(mp)
50
+ mounted = true
51
+ else
52
+ Snapsync.info "partition #{t.partition_uuid} is present, but not mounted and automount is false. Ignoring"
53
+ next
54
+ end
55
+ else
56
+ mp = Pathname.new(mp[0..-2].pack("U*"))
57
+ end
58
+
59
+ full_path = mp + t.path
60
+ Snapsync.info "sync-all on #{mp + t.path} (partition #{t.partition_uuid})"
61
+ op = SyncAll.new(mp + t.path, config_dir: config_dir)
62
+ op.run
63
+ if mounted
64
+ fs.Unmount([])
65
+ end
66
+ end
67
+ end
68
+ Snapsync.info "done all declared autosync partitions, sleeping #{period}s"
69
+ sleep period
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,40 @@
1
+ module Snapsync
2
+ class Cleanup
3
+ # The underlying timeline policy object that we use to compute which
4
+ # snapshots to delete and which to keep
5
+ attr_reader :policy
6
+
7
+ def initialize(policy)
8
+ @policy = policy
9
+ end
10
+
11
+ def cleanup(target, dry_run: false)
12
+ snapshots = target.each_snapshot.to_a
13
+ filtered_snapshots = policy.filter_snapshots_to_sync(target, snapshots).to_set
14
+
15
+ if filtered_snapshots.any? { |s| s.synchronization_point? }
16
+ raise InvalidPolicy, "#{policy} returned a snapsync synchronization point in its results"
17
+ end
18
+
19
+ if filtered_snapshots.empty?
20
+ raise InvalidPolicy, "#{policy} returned no snapshots"
21
+ end
22
+
23
+ last_sync_point = snapshots.
24
+ sort_by(&:num).reverse.
25
+ find { |s| s.synchronization_point_for?(target) }
26
+ if !last_sync_point
27
+ binding.pry
28
+ end
29
+ filtered_snapshots << last_sync_point
30
+ filtered_snapshots = filtered_snapshots.to_set
31
+
32
+ snapshots.sort_by(&:num).each do |s|
33
+ if !filtered_snapshots.include?(s)
34
+ target.delete(s, dry_run: dry_run)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,125 @@
1
+ require 'thor'
2
+ require 'snapsync'
3
+
4
+ module Snapsync
5
+ class CLI < Thor
6
+ class_option :debug, type: :boolean, default: false
7
+
8
+ no_commands do
9
+ def config_from_name(name)
10
+ path = Pathname.new(name)
11
+ if !path.exist?
12
+ path = SnapperConfig.default_config_dir + path
13
+ if !path.exist?
14
+ raise ArgumentError, "cannot find any snapper configuration called #{name}"
15
+ end
16
+ end
17
+ SnapperConfig.load(path)
18
+ end
19
+
20
+ def handle_class_options
21
+ if options[:debug]
22
+ Snapsync.logger.level = 'DEBUG'
23
+ end
24
+ end
25
+ end
26
+
27
+ desc 'sync CONFIG DIR', 'synchronizes the snapper configuration CONFIG with the snapsync target DIR'
28
+ def sync(config_name, dir)
29
+ handle_class_options
30
+
31
+ config = config_from_name(config_name)
32
+ target = LocalTarget.new(Pathname.new(dir))
33
+ LocalSync.new(config, target).sync
34
+ end
35
+
36
+ desc 'sync-all DIR', 'synchronizes all snapper configurations into corresponding subdirectories of DIR'
37
+ option :autoclean, type: :boolean, default: nil,
38
+ desc: 'whether the target should be cleaned of obsolete snapshots',
39
+ 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"
40
+ def sync_all(dir)
41
+ handle_class_options
42
+
43
+ op = SyncAll.new(dir, config_dir: SnapperConfig.default_config_dir, autoclean: options[:autoclean])
44
+ op.run
45
+ end
46
+
47
+ desc 'cleanup CONFIG DIR', 'cleans up the snapsync target DIR based on the policy set by the policy command'
48
+ option :dry_run, type: :boolean, default: false
49
+ def cleanup(dir)
50
+ handle_class_options
51
+
52
+ target = LocalTarget.new(Pathname.new(dir))
53
+ if target.cleanup
54
+ target.cleanup.cleanup(target, dry_run: options[:dry_run])
55
+ else
56
+ Snapsync.info "#{target.sync_policy.class.name} policy set, nothing to do"
57
+ end
58
+ end
59
+
60
+ desc 'init DIR', 'creates a synchronization target with a default policy'
61
+ def init(dir)
62
+ dir = Pathname.new(dir)
63
+ if !dir.exist?
64
+ dir.mkpath
65
+ end
66
+
67
+ target = LocalTarget.new(dir)
68
+ target.change_policy('default', Hash.new)
69
+ target.write_config
70
+ end
71
+
72
+ desc 'policy DIR TYPE [OPTIONS]', 'sets the synchronization and cleanup policy for the given target'
73
+ long_desc <<-EOD
74
+ This command sets the policy used to decide which snapshots to synchronize to
75
+ the target, and which to not synchronize.
76
+
77
+ Three policy types can be used: default, last and timeline
78
+
79
+ The default policy takes no argument. It will synchronize all snapshots present in the source, and do no cleanup
80
+
81
+ The last policy takes no argument. It will synchronize (and keep) only the last snapshot
82
+
83
+ The timeline policy takes periods of time as argument (as e.g. day 10 or month 20). It will keep at least
84
+ one snapshot for each period, and for the duration specified (day 10 tells to keep one snapshot per day
85
+ for 10 days). snapsync understands the following period names: year month day hour.
86
+ EOD
87
+ def policy(dir, type, *options)
88
+ handle_class_options
89
+
90
+ dir = Pathname.new(dir)
91
+ if !dir.exist?
92
+ dir.mkpath
93
+ end
94
+
95
+ target = LocalTarget.new(dir)
96
+ target.change_policy(type, options)
97
+ target.write_config
98
+ end
99
+
100
+ desc 'destroy DIR', 'destroys a snapsync target'
101
+ long_desc <<-EOD
102
+ While it can easily be done manually, this command makes sure that the snapshots are properly deleted
103
+ EOD
104
+ def destroy(dir)
105
+ handle_class_options
106
+ target_dir = Pathname.new(dir)
107
+ target = LocalTarget.new(target_dir, create_if_needed: false)
108
+ snapshots = target.each_snapshot.to_a
109
+ snapshots.sort_by(&:num).each do |s|
110
+ target.delete(s)
111
+ end
112
+ target_dir.rmtree
113
+ end
114
+
115
+ desc "auto-sync", "automatic synchronization"
116
+ option :config_file, desc: "path to the config file (defaults to /etc/snapsync.conf)",
117
+ default: '/etc/snapsync.conf'
118
+ def auto_sync
119
+ auto = AutoSync.new(SnapperConfig.default_config_dir)
120
+ auto.load_config(Pathname.new(options[:config_file]))
121
+ auto.run
122
+ end
123
+ end
124
+ end
125
+