restic-service 0.1.0 → 0.2.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 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