restic-service 0.1.0 → 0.2.0

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: 5eb6ed2de697e518e95589565d8104e15aff50c4
4
- data.tar.gz: afc4fd0c966455bb46920ce2ca5cd42902eb1e1c
3
+ metadata.gz: 4f4ae91f2cc1e092886efb337eaf4fcc6b31f1e8
4
+ data.tar.gz: c1aed6a2ed9345f1209c36b4cfb19e537607a1d4
5
5
  SHA512:
6
- metadata.gz: 6c9e35b848ca8d1cb6627aefcc3c3073a3499b2770b995bf9d22ae673ff085f84193dbb52fa54a329ec6dd1dedf802dbe069ebcd8bd8f9ac932bd8806235ef9b
7
- data.tar.gz: 31d8e7db6d33568552a33a90cef56c3c86996d6e1021a90f56b43224fda7a80e28a910465583a8c8186564784fb041abbb5c9302da66a2fbdda2c81d1ff380db
6
+ metadata.gz: 968968cee27de96246836215811f1d50ce80f6039b33f5c9b66539c60843c555c6e2601b09d4b95a161e47d43029835263c2cf5a01f77ceaa110bb7e7bcfb465
7
+ data.tar.gz: abc0903185556bf615d1f4ec1c5c1632f716b74757647e857189d8637505ae397fdeb04528e61dee2a86ac31185db78f56afb4f73044b099f9c2b73d41710579
@@ -4,6 +4,10 @@ PROGRAM=restic-service
4
4
 
5
5
  dev_path=$PWD
6
6
 
7
+ if test "x$1" = "x--systemd"; then
8
+ systemd=1
9
+ fi
10
+
7
11
  target=`mktemp -d`
8
12
  cd $target
9
13
  cat > Gemfile <<GEMFILE
@@ -11,16 +15,22 @@ source "https://rubygems.org"
11
15
  gem '${PROGRAM}', path: "$dev_path"
12
16
  GEMFILE
13
17
 
14
- cat Gemfile
18
+ bundler install --binstubs --without development --path vendor
19
+ for i in vendor/ruby/*; do
20
+ gem_home_relative=$i
21
+ done
22
+ GEM_HOME=$PWD/$gem_home_relative gem install bundler --no-document
23
+ for stub in bin/*; do
24
+ sed -i "/usr.bin.env/a ENV['GEM_HOME']='/opt/restic-service/$gem_home_relative'" $stub
25
+ done
15
26
 
16
- bundler install --standalone --binstubs
17
27
  if test -d /opt/${PROGRAM}; then
18
28
  sudo rm -rf /opt/${PROGRAM}
19
29
  fi
20
30
  sudo cp -r . /opt/${PROGRAM}
21
31
  sudo chmod go+rX /opt/${PROGRAM}
22
32
 
23
- if test -d /lib/systemd/system; then
33
+ if test "x$systemd" = "x1" && test -d /lib/systemd/system; then
24
34
  target_gem=`bundler show ${PROGRAM}`
25
35
  sudo cp $target_gem/${PROGRAM}.service /lib/systemd/system
26
36
  ( sudo systemctl stop ${PROGRAM}.service
data/install.sh CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  PROGRAM=restic-service
4
4
 
5
+ if test "x$1" = "x--systemd"; then
6
+ systemd=1
7
+ fi
8
+
5
9
  target=`mktemp -d`
6
10
  cd $target
7
11
  cat > Gemfile <<GEMFILE
@@ -9,14 +13,22 @@ source "https://rubygems.org"
9
13
  gem '${PROGRAM}'
10
14
  GEMFILE
11
15
 
12
- bundler install --standalone --binstubs
16
+ bundler install --binstubs --without development --path vendor
17
+ for i in vendor/ruby/*; do
18
+ gem_home_relative=$i
19
+ done
20
+ GEM_HOME=$PWD/$gem_home_relative gem install bundler --no-document
21
+ for stub in bin/*; do
22
+ sed -i "/usr.bin.env/a ENV['GEM_HOME']='/opt/restic-service/$gem_home_relative'" $stub
23
+ done
24
+
13
25
  if test -d /opt/${PROGRAM}; then
14
26
  sudo rm -rf /opt/${PROGRAM}
15
27
  fi
16
28
  sudo cp -r . /opt/${PROGRAM}
17
29
  sudo chmod go+rX /opt/${PROGRAM}
18
30
 
19
- if test -d /lib/systemd/system; then
31
+ if test "x$systemd" = "x1" && test -d /lib/systemd/system; then
20
32
  target_gem=`bundler show ${PROGRAM}`
21
33
  sudo cp $target_gem/${PROGRAM}.service /lib/systemd/system
22
34
  ( sudo systemctl stop ${PROGRAM}.service
@@ -3,6 +3,7 @@ require "pathname"
3
3
  require 'yaml'
4
4
  require 'tempfile'
5
5
  require "restic/service/version"
6
+ require "restic/service/auto_update"
6
7
  require "restic/service/targets/base"
7
8
  require "restic/service/targets/restic"
8
9
  require "restic/service/targets/b2"
@@ -0,0 +1,106 @@
1
+ require 'net/https'
2
+
3
+ module Restic
4
+ module Service
5
+ class AutoUpdate
6
+ class FailedUpdate < RuntimeError
7
+ end
8
+
9
+ RESTIC_RELEASE_VERSION = "0.8.3"
10
+
11
+ def self.restic_release_url(platform)
12
+ "https://github.com/restic/restic/releases/download/v#{RESTIC_RELEASE_VERSION}/restic_#{RESTIC_RELEASE_VERSION}_#{platform}.bz2"
13
+ end
14
+
15
+
16
+ def initialize(binary_path)
17
+ @root = File.dirname(File.dirname(binary_path))
18
+ unless File.file?(File.join(@root, "Gemfile"))
19
+ raise FailedUpdate, "cannot guess installation path (tried #{@root})"
20
+ end
21
+
22
+ @gem_home = ENV['GEM_HOME']
23
+ end
24
+
25
+ def patch_binstubs
26
+ bindir = File.join(@root, 'bin')
27
+ Dir.new(bindir).each do |entry|
28
+ entry = File.join(bindir, entry)
29
+ if File.file?(entry)
30
+ patched_contents = File.readlines(entry)
31
+ patched_contents.insert(1, "ENV['GEM_HOME'] = '#{@gem_home}'\n")
32
+ File.open(entry, 'w') do |io|
33
+ io.write patched_contents.join("")
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def current_gem_version(gem_name)
40
+ gemfile_lock = File.join(@root, "Gemfile.lock")
41
+ File.readlines(gemfile_lock).each do |line|
42
+ match = /^\s+#{gem_name} \((.*)\)$/.match(line)
43
+ return match[1] if match
44
+ end
45
+ raise FailedUpdate, "cannot find the version line for #{gem_name} in #{gemfile_lock}"
46
+ end
47
+
48
+ def update_restic_service
49
+ current_version = current_gem_version "restic-service"
50
+ reader, writer = IO.pipe
51
+ if !system("bundle", "update", out: writer, err: writer)
52
+ writer.close
53
+ puts reader.read(1024)
54
+ raise FailedUpdate, "failed to run bundle update"
55
+ end
56
+ patch_binstubs
57
+ new_version = current_gem_version "restic-service"
58
+ [current_version, new_version]
59
+ ensure
60
+ writer.close if writer && !writer.closed?
61
+ reader.close if reader && !reader.closed?
62
+ end
63
+
64
+ def update_restic(platform, target_path)
65
+ release_url = self.class.restic_release_url(platform)
66
+
67
+ release_binary = nil
68
+ while !release_binary
69
+ response = Net::HTTP.get_response(URI(release_url))
70
+ case response
71
+ when Net::HTTPSuccess
72
+ release_binary = response.body
73
+ when Net::HTTPRedirection
74
+ release_url = response['location']
75
+ else
76
+ raise FailedUpdate, "failed to fetch restic at #{release_url}: #{response}"
77
+ end
78
+ end
79
+
80
+ tmpdir = Dir.mktmpdir
81
+ restic_path = File.join(tmpdir, "restic")
82
+ File.open("#{restic_path}.bz2", 'w') do |io|
83
+ io.write release_binary
84
+ end
85
+
86
+ if !system("bzip2", "-d", "#{restic_path}.bz2")
87
+ raise FailedUpdate, "failed to uncompress the restic release file"
88
+ end
89
+
90
+ if File.file?(target_path)
91
+ current = File.read(target_path)
92
+ new = File.read(restic_path)
93
+ return if current == new
94
+ end
95
+
96
+ FileUtils.mv restic_path, target_path
97
+ FileUtils.chmod 0755, target_path
98
+ true
99
+
100
+ ensure
101
+ FileUtils.rm_rf tmpdir if tmpdir
102
+ end
103
+ end
104
+ end
105
+ end
106
+
@@ -26,31 +26,50 @@ module Restic
26
26
  Conf.load(conf_file_path)
27
27
  end
28
28
 
29
- def run_sync(conf, *targets)
29
+ def each_selected_and_available_target(conf, *targets)
30
30
  has_target = false
31
31
  conf.each_target do |target|
32
32
  has_target = true
33
33
  if !targets.empty? && !targets.include?(target.name)
34
34
  next
35
- end
36
-
37
- if !target.available?
35
+ elsif !target.available?
38
36
  puts "#{target.name} is not available"
39
37
  next
40
38
  end
41
39
 
40
+ yield(target)
41
+ end
42
+
43
+ if !has_target
44
+ STDERR.puts "WARNING: no targets in #{options[:conf]}"
45
+ end
46
+ end
47
+
48
+ def run_sync(conf, *targets)
49
+ each_selected_and_available_target(conf, *targets) do |target|
42
50
  puts
43
51
  puts "-----"
44
52
  puts "#{Time.now} - Synchronizing #{target.name}"
45
53
  target.run
46
54
  end
47
- if !has_target
48
- STDERR.puts "WARNING: no targets in #{options[:conf]}"
55
+ end
56
+
57
+ def run_forget(conf, *targets)
58
+ each_selected_and_available_target(conf, *targets) do |target|
59
+ unless target.respond_to?(:forget)
60
+ puts "#{target.name} does not supports forget"
61
+ next
62
+ end
63
+
64
+ puts
65
+ puts "-----"
66
+ puts "#{Time.now} - Running forget pass on #{target.name}"
67
+ target.forget
49
68
  end
50
69
  end
51
70
  end
52
71
 
53
- desc 'available-targets', 'finds the available backup targets'
72
+ desc 'whereami', 'finds the available backup targets'
54
73
  def whereami
55
74
  STDOUT.sync = true
56
75
  conf = load_conf
@@ -60,6 +79,53 @@ module Restic
60
79
  end
61
80
  end
62
81
 
82
+ desc 'install-restic PATH PLATFORM', 'install restic'
83
+ def install_restic(path, platform)
84
+ updater = AutoUpdate.new($0)
85
+ updater.update_restic(platform, path)
86
+ end
87
+
88
+ desc 'auto-update', 'perform auto-updating as configured in the configuration file'
89
+ def auto_update
90
+ conf = load_conf
91
+ STDOUT.sync = true
92
+
93
+ updater = AutoUpdate.new($0)
94
+ if conf.auto_update_restic_service?
95
+ puts "attempting to auto-update restic-service"
96
+ old_version, new_version = updater.update_restic_service
97
+ if old_version != new_version
98
+ puts "updated restic-service from #{old_version} to #{new_version}, restarting"
99
+ exec "bundle", "exec", Gem.ruby, $0, "auto-update"
100
+ else
101
+ puts "restic-service was already up-to-date: #{new_version}"
102
+ end
103
+ else
104
+ puts "updating restic-service disabled in configuration"
105
+ end
106
+
107
+ update_restic = conf.auto_update_restic?
108
+ if update_restic
109
+ begin
110
+ restic_path = conf.tool_path('restic')
111
+ rescue ArgumentError
112
+ puts "cannot auto-update restic, provide an explicit path in the 'tools' section of the configuration first"
113
+ update_restic = false
114
+ end
115
+ else
116
+ puts "updating restic disabled in configuration"
117
+ end
118
+
119
+ if update_restic
120
+ puts "attempting to auto-update restic"
121
+ if updater.update_restic(conf.restic_platform, restic_path)
122
+ puts "updated restic to version #{AutoUpdate::RESTIC_RELEASE_VERSION}"
123
+ else
124
+ puts "restic was already up-to-date"
125
+ end
126
+ end
127
+ end
128
+
63
129
  desc 'sync', 'synchronize all (some) targets'
64
130
  def sync(*targets)
65
131
  STDOUT.sync = true
@@ -67,6 +133,13 @@ module Restic
67
133
  run_sync(conf, *targets)
68
134
  end
69
135
 
136
+ desc 'forget', 'delete historical data'
137
+ def forget(*targets)
138
+ STDOUT.sync = true
139
+ conf = load_conf
140
+ run_forget(conf, *targets)
141
+ end
142
+
70
143
  desc 'auto', 'periodically runs the backups, pass target names to restrict to these'
71
144
  def auto(*targets)
72
145
  STDOUT.sync = true
@@ -76,6 +149,7 @@ module Restic
76
149
  puts ""
77
150
 
78
151
  run_sync(conf, *targets)
152
+ run_forget(conf, *targets)
79
153
 
80
154
  puts ""
81
155
  puts "#{Time.now} Finished automatic synchronization pass"
@@ -61,6 +61,8 @@ module Restic
61
61
  yaml['tools'][tool_name] ||= tool_name
62
62
  end
63
63
 
64
+ yaml['auto_update'] ||= Array.new
65
+
64
66
  target_names = Array.new
65
67
  yaml['targets'] = yaml['targets'].map do |target|
66
68
  if !target['name']
@@ -215,6 +217,18 @@ module Restic
215
217
  end
216
218
  end
217
219
 
220
+ def auto_update_restic_service?
221
+ @auto_update_restic_service
222
+ end
223
+
224
+ def auto_update_restic?
225
+ @auto_update_restic
226
+ end
227
+
228
+ def restic_platform
229
+ @auto_update_restic
230
+ end
231
+
218
232
  # Add the information stored in a YAML-like hash into this
219
233
  # configuration
220
234
  #
@@ -228,6 +242,14 @@ module Restic
228
242
  Conf.parse_bandwidth_limit(limit_yaml)
229
243
  end
230
244
 
245
+ yaml['auto_update'].each do |update_target, do_update|
246
+ if update_target == 'restic-service'
247
+ @auto_update_restic_service = do_update
248
+ elsif update_target == 'restic'
249
+ @auto_update_restic = do_update
250
+ end
251
+ end
252
+
231
253
  yaml['targets'].each do |yaml_target|
232
254
  type = yaml_target['type']
233
255
  target_class = Conf.target_class_from_type(type)
@@ -48,40 +48,81 @@ module Restic
48
48
  @io_class = Integer(yaml['io_class'])
49
49
  @io_priority = Integer(yaml['io_priority'])
50
50
  @cpu_priority = Integer(yaml['cpu_priority'])
51
+ @forget = parse_forget_setup(yaml['forget'] || Hash.new)
51
52
  end
52
53
 
53
- def run(*args)
54
- old_home = ENV['HOME']
55
- ENV['HOME'] = old_home || '/root'
54
+ Forget = Struct.new :prune, :tags, :hourly, :daily, :weekly, :monthly, :yearly do
55
+ def prune?
56
+ prune
57
+ end
58
+ end
59
+ FORGET_DURATION_KEYS = %w{tags hourly daily weekly monthly yearly}
60
+ FORGET_KEYS = ['prune', *FORGET_DURATION_KEYS].freeze
61
+ def parse_forget_setup(setup)
62
+ parsed = Forget.new true, []
63
+ if (invalid_key = setup.find { |k, _| !FORGET_KEYS.include?(k) })
64
+ raise ArgumentError, "#{invalid_key} is not a valid key within "\
65
+ "'forget', valid keys are: #{FORGET_KEYS.join(", ")}"
66
+ end
56
67
 
68
+ FORGET_KEYS.each do |key|
69
+ parsed[key] = setup.fetch(key, key == "prune")
70
+ end
71
+ parsed
72
+ end
73
+
74
+ def run_backup(*args, **options)
75
+ extra_args = []
76
+ if one_filesystem?
77
+ extra_args << '--one-file-system'
78
+ end
79
+
80
+ run_restic(*args, *extra_args,
81
+ *@excludes.flat_map { |e| ['--exclude', e] },
82
+ *@includes)
83
+ end
84
+
85
+ def run_restic(*args, **options)
86
+ home = ENV['HOME'] || '/root'
57
87
  env = if args.first.kind_of?(Hash)
58
88
  env = args.shift
59
89
  else
60
90
  env = Hash.new
61
91
  end
62
92
 
63
- ionice_args = []
64
- if @io_class != 3
65
- ionice_args << '-n' << @io_priority.to_s
66
- end
67
-
68
93
  extra_args = []
69
- if one_filesystem?
70
- extra_args << '--one-file-system'
71
- end
72
94
  if @bandwidth_limit
73
95
  limit_KiB = @bandwidth_limit / 1000
74
96
  extra_args << '--limit-download' << limit_KiB.to_s << '--limit-upload' << limit_KiB.to_s
75
97
  end
76
98
 
77
- system(Hash['RESTIC_PASSWORD' => @password].merge(env),
99
+ ionice_args = []
100
+ if @io_class != 3
101
+ ionice_args << '-n' << @io_priority.to_s
102
+ end
103
+
104
+ system(Hash['HOME' => home, 'RESTIC_PASSWORD' => @password].merge(env),
78
105
  'ionice', '-c', @io_class.to_s, *ionice_args,
79
106
  'nice', "-#{@cpu_priority}",
80
- @restic_path.to_path, *args, *extra_args,
81
- *@excludes.flat_map { |e| ['--exclude', e] },
82
- *@includes, in: :close)
83
- ensure
84
- ENV['HOME'] = old_home
107
+ @restic_path.to_path, "--cleanup-cache", *args, *extra_args, in: :close, **options)
108
+ end
109
+
110
+ def run_forget(*args)
111
+ extra_args = []
112
+ FORGET_DURATION_KEYS.each do |key|
113
+ arg_key =
114
+ if key == "tags" then "tag"
115
+ else key
116
+ end
117
+ if value = @forget[key]
118
+ extra_args << "--keep-#{arg_key}" << value.to_s
119
+ end
120
+ end
121
+ if @forget.prune?
122
+ extra_args << "--prune"
123
+ puts "PRUNE"
124
+ end
125
+ run_restic(*args, *extra_args)
85
126
  end
86
127
  end
87
128
  end
@@ -13,7 +13,13 @@ module Restic
13
13
  end
14
14
 
15
15
  def run
16
- super(Hash['B2_ACCOUNT_ID' => @id, 'B2_ACCOUNT_KEY' => @key], '-r', "b2:#{@bucket}:#{@path}", 'backup')
16
+ run_backup(Hash['B2_ACCOUNT_ID' => @id, 'B2_ACCOUNT_KEY' => @key],
17
+ '-r', "b2:#{@bucket}:#{@path}", 'backup')
18
+ end
19
+
20
+ def forget
21
+ run_forget(Hash['B2_ACCOUNT_ID' => @id, 'B2_ACCOUNT_KEY' => @key],
22
+ '-r', "b2:#{@bucket}:#{@path}", 'forget')
17
23
  end
18
24
  end
19
25
  end
@@ -44,16 +44,21 @@ module Restic
44
44
  end
45
45
 
46
46
  def run
47
- current_home = ENV['HOME']
48
- ENV['HOME'] = current_home || '/root'
47
+ ssh = SSHKeys.new
48
+ ssh_config_name = ssh.ssh_setup_config(@target_name, @username, @host, @key_path)
49
+
50
+ run_backup('-r', "sftp:#{ssh_config_name}:#{@path}", 'backup')
51
+ ensure
52
+ ssh.ssh_cleanup_config
53
+ end
49
54
 
55
+ def forget
50
56
  ssh = SSHKeys.new
51
57
  ssh_config_name = ssh.ssh_setup_config(@target_name, @username, @host, @key_path)
52
58
 
53
- super('-r', "sftp:#{ssh_config_name}:#{@path}", 'backup')
59
+ run_forget('-r', "sftp:#{ssh_config_name}:#{@path}", 'forget')
54
60
  ensure
55
61
  ssh.ssh_cleanup_config
56
- ENV['HOME'] = current_home
57
62
  end
58
63
  end
59
64
  end
@@ -1,5 +1,5 @@
1
1
  module Restic
2
2
  module Service
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restic-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sylvain Joyeux
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-26 00:00:00.000000000 Z
11
+ date: 2018-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -100,6 +100,7 @@ files:
100
100
  - install-dev.sh
101
101
  - install.sh
102
102
  - lib/restic/service.rb
103
+ - lib/restic/service/auto_update.rb
103
104
  - lib/restic/service/cli.rb
104
105
  - lib/restic/service/conf.rb
105
106
  - lib/restic/service/ssh_keys.rb