summaryrefslogtreecommitdiff
path: root/bin/byteback-prune
blob: d005289fe0fa6f8837cc8ef3cdc9e185960899cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/ruby
#
# Program to free up space on the backup-storage volume, by removing
# backups (whether by age, or importance).
#

$LOAD_PATH.unshift('/usr/lib/byteback')

require 'trollop'
require 'byteback'
require 'sys/filesystem'
include Byteback
include Byteback::Log
include Byteback::Util

opts = Trollop.options do
  banner "Prune old backup directories to ensure there's enough space"

  opt :minpercent, 'Start prune when disk has less than this %age free',
      type: :integer,
      default: 5

  opt :maxpercent, 'Stop prune when disk has more than this %age free',
      type: :integer,
      default: 10

  opt :list, 'List backups in pruning order, no other action'

  opt :prune, 'Prune the next backup if necessary'

  opt :prune_force, 'Prune the next backup regardless'

  opt :order, "Order backups by 'age' or 'importance'",
      type: :string,
      default: 'importance'

  opt :verbose, 'Show debugging messages'
end

@order = opts[:order]
@verbose = opts[:verbose]
@do_list = opts[:list]
@do_prune = opts[:prune]
@do_prune_force = opts[:prune_force]
@minpercent = opts[:minpercent]
@maxpercent = opts[:maxpercent]

@do_prune = true if @do_prune_force

fatal('Must specify one of --prune or --list') unless
  (@do_prune || @do_list) &&
  !(@do_prune && @do_list)

fatal("Must specify --order as 'age' or 'importance'") unless
  @order == 'age' || @order == 'importance'

if BackupDirectory.all.empty?
  fatal('No backup directories found, need to run byteback-snapshot')
end

lock_out_other_processes('byteback-prune')

@df_history = DiskFreeHistory.new(ENV['HOME'])
begin
  @df_history.new_reading!
rescue Errno::ENOSPC
  if @do_list
    warn("Couldn't write disk history file due to lack of space, ignoring")
  else
    warn("Couldn't write disk history file due to lack of space, going to --prune-force")
    @do_prune = @do_prune_force = true
  end
rescue => anything_else
  error("Couldn't record disk history of #{@df_history.mountpoint} in #{@df_history.history_file}, installation problem?")
  raise
end

gradient_30m = @df_history.gradient(1800)

# Check whether we should still be pruning
#
@free = @df_history.list.last.percent_free
PRUNING_FLAG = "#{ENV['HOME']}/.byteback.pruning"

if @do_prune_force
  info('Forcing prune')
elsif @free <= @minpercent && !File.exist?(PRUNING_FLAG)
  info("Starting prune #{@free}% -> #{@maxpercent} free")
  File.write(PRUNING_FLAG, '')
elsif @free >= @maxpercent && File.exist?(PRUNING_FLAG)
  info("Stopping prune, reached #{@free}% free")
  File.unlink(PRUNING_FLAG)
elsif File.exist?(PRUNING_FLAG)
  info("Continuing prune #{@free}% -> #{@maxpercent}, gradient = #{gradient_30m}")
end

debug("Disc free #{@free}%, 30m gradient = #{gradient_30m}")

def snapshots_in_order
  list = BackupDirectory.all_snapshots
  if @order == 'importance'
    Snapshot.sort_by_importance(list)
  elsif @order == 'age'
    list.sort.reverse
  else
    fail ArgumentError.new("Unknown snapshot sort method #{method}")
  end
end

snapshots = snapshots_in_order

if @do_list
  print "Backups by #{@order}:\n"
  snapshots.each_with_index do |snapshot, index|
    print "#{sprintf('% 3d', index)}: #{snapshot.path}\n"
  end
end

# Don't do anything if we've not got two hours of readings
#
unless @do_prune_force
  if @df_history.list.last.time - @df_history.list.first.time < 1800
    warn('Not enough disc space history to make a decision')
    exit 0
  end
end

exit 0 unless
  (@do_prune && File.exist?(PRUNING_FLAG)) ||
  @do_prune_force

exit 0 unless @do_prune_force || gradient_30m == 0

if snapshots.empty?
  error('No snapshots to delete, is there enough disc space?')
  exit 1
end

info("Deleting #{snapshots.last.path}")
log_system("/sbin/btrfs subvolume delete #{snapshots.last.path}")