Skip to content

Commit 2f52660

Browse files
committed
Accurately report when the lvmsync receiver fails
This work was most cleanly achieved by refactoring out a lot of "behind the scenes" logic that should be buried inside a "LogicalVolume" class, so I made one, and used that. Overall, I think it's a definite net win.
1 parent dbb2b56 commit 2f52660

4 files changed

Lines changed: 111 additions & 21 deletions

File tree

bin/lvmsync

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
require 'optparse'
2121
require 'lvm'
2222
require 'git-version-bump'
23+
require 'open3'
2324

2425
PROTOCOL_VERSION = "lvmsync PROTO[3]"
2526

@@ -163,27 +164,19 @@ def run_client(opts)
163164
destdev = opts[:destdev]
164165
outfd = nil
165166

166-
vg, lv = parse_snapshot_name(snapshot)
167-
168-
vgconfig = LVM::VGConfig.new(vg)
169-
170-
if vgconfig.logical_volumes[lv].nil?
171-
$stderr.puts "#{snapshot}: Could not find logical volume"
167+
lv = begin
168+
LVM::LogicalVolume.new(snapshot)
169+
rescue RuntimeError => e
170+
$stderr.puts "#{snapshot}: could not find logical volume (#{e.message})"
172171
exit 1
173172
end
174173

175-
snap = if vgconfig.logical_volumes[lv].snapshot?
176-
if vgconfig.logical_volumes[lv].thin?
177-
LVM::ThinSnapshot.new(vg, lv)
178-
else
179-
LVM::Snapshot.new(vg, lv)
180-
end
181-
else
174+
unless lv.snapshot?
182175
$stderr.puts "#{snapshot}: Not a snapshot device"
183176
exit 1
184177
end
185178

186-
$stderr.puts "Origin device: #{vg}/#{snap.origin}" if opts[:verbose]
179+
$stderr.puts "Origin device: #{lv.origin.path}" if opts[:verbose]
187180

188181
# Since, in principle, we're not supposed to be reading from snapshot
189182
# devices directly, the kernel makes no attempt to make the device's read
@@ -195,27 +188,41 @@ def run_client(opts)
195188
snapback = opts[:snapback] ? "--snapback #{opts[:snapback]}" : ''
196189

197190
if opts[:stdout]
198-
outfd = $stdout
191+
dump_changes(lv, $stdout, opts)
199192
else
200193
server_cmd = if desthost
201194
"ssh #{desthost} lvmsync --apply - #{snapback} #{destdev}"
202195
else
203196
"#{$0} --apply - #{snapback} #{destdev}"
204197
end
205198

206-
outfd = IO.popen(server_cmd, 'w')
199+
exit_status = nil
200+
errors = nil
201+
202+
Open3.popen3(server_cmd) do |stdin_fd, stdout_fd, stderr_fd, wait_thr|
203+
dump_changes(lv, stdin_fd, opts)
204+
stdin_fd.close
205+
errors = stderr_fd.read
206+
exit_status = wait_thr.value if wait_thr
207+
end
208+
209+
if (exit_status or $?).exitstatus != 0
210+
$stderr.puts "APPLY FAILED."
211+
$stderr.puts errors.split("\n").map { |l| "remote: #{l}" }.join("\n")
212+
end
207213
end
214+
end
208215

216+
def dump_changes(lv, outfd, opts)
209217
outfd.puts PROTOCOL_VERSION
210218

211219
start_time = Time.now
212220
xfer_count = 0
213221
xfer_size = 0
214222
total_size = 0
215223

216-
originfile = "/dev/mapper/#{vg.gsub('-', '--')}-#{snap.origin.gsub('-', '--')}"
217-
File.open(originfile, 'r') do |origindev|
218-
snap.differences.each do |r|
224+
File.open(lv.origin.path, 'r') do |origindev|
225+
lv.changes.each do |r|
219226
xfer_count += 1
220227
chunk_size = r.last - r.first + 1
221228
xfer_size += chunk_size
@@ -247,8 +254,6 @@ def run_client(opts)
247254

248255
$stderr.printf "You transferred your changes %.2fx faster than a full dd!\n",
249256
total_size.to_f / xfer_size
250-
ensure
251-
outfd.close unless outfd.nil? or outfd == $stdout
252257
end
253258

254259
# Take a device name in any number of different formats and return a [VG, LV] pair.

lib/lvm.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'lvm/helpers'
22
require 'lvm/thin_snapshot'
33
require 'lvm/snapshot'
4+
require 'lvm/logical_volume'
45
require 'lvm/lv_config'
56
require 'lvm/vg_config'

lib/lvm/logical_volume.rb

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module LVM; end
2+
3+
# This class represents an LVM logical volume, in all its glory. You can
4+
# perform various operations on it.
5+
class LVM::LogicalVolume
6+
# Create a new instance of LVM::LogicalVolume.
7+
#
8+
# New instances can be created in one of two ways:
9+
#
10+
# * Pass a single argument, containing any path which LVM can resolve to
11+
# a logical volume. Typically, this will either be `/dev/<vg>/<lv>`
12+
# or `/dev/mapper/<vg>-<lv>`, but we don't try to parse it ourselves,
13+
# relying instead on `lvs` to do the heavy lifting.
14+
#
15+
# * Pass two arguments, which are the volume group name and logical
16+
# volume name, respectively.
17+
#
18+
# This method will raise `RuntimeError` if the path specified can't be
19+
# resolved to an LV, or if the specified VG name or LV name don't resolve
20+
# to an active logical volume.
21+
#
22+
def initialize(path_or_vg_name, lv_name=nil)
23+
if lv_name.nil?
24+
path = path_or_vg_name
25+
@vg_name, @lv_name = `lvs --noheadings -o vg_name,lv_name #{path} 2>/dev/null`.strip.split(/\s+/, 2)
26+
if $?.exitstatus != 0
27+
raise RuntimeError,
28+
"Failed to interrogate LVM about '#{path}'. Perhaps you misspelt it?"
29+
end
30+
else
31+
@vg_name = path_or_vg_name
32+
@lv_name = lv_name
33+
end
34+
35+
@vgcfg = LVM::VGConfig.new(@vg_name)
36+
@lvcfg = @vgcfg.logical_volumes[@lv_name]
37+
38+
if @lvcfg.nil?
39+
raise RuntimeError,
40+
"Logical volume #{@lv_name} does not exist in volume group #{@vg_name}"
41+
end
42+
end
43+
44+
# Return a string containing a canonical path to the block device
45+
# representing this LV.
46+
def path
47+
"/dev/mapper/#{@vg_name.gsub('-', '--')}-#{@lv_name.gsub('-', '--')}"
48+
end
49+
50+
# Is this LV a snapshot?
51+
def snapshot?
52+
@lvcfg.snapshot?
53+
end
54+
55+
# Return an LVM::LogicalVolume object which is the origin volume of
56+
# this one (if this LV is a snapshot), or `nil` otherwise.
57+
def origin
58+
return nil unless snapshot?
59+
60+
if @lvcfg.origin
61+
LVM::LogicalVolume.new(@vg_name, @lvcfg.origin)
62+
else
63+
origin_lv_name = @vgcfg.logical_volumes.values.find { |lv| lv.cow_store == @lv_name }.origin
64+
LVM::LogicalVolume.new(@vg_name, origin_lv_name)
65+
end
66+
end
67+
68+
# Return an array of ranges, each of which represents an inclusive range
69+
# of bytes which are different between this logical volume and its
70+
# origin.
71+
#
72+
# If this LV is not a snapshot, this method returns an empty array.
73+
#
74+
def changes
75+
return [] unless snapshot?
76+
77+
if @lvcfg.thin?
78+
LVM::ThinSnapshot.new(@vg_name, @lv_name)
79+
else
80+
LVM::Snapshot.new(@vg_name, @lv_name)
81+
end.differences
82+
end
83+
end

lvmsync.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
1919
bin/lvmsync
2020
lib/lvm.rb
2121
lib/lvm/lv_config.rb
22+
lib/lvm/logical_volume.rb
2223
lib/lvm/thin_snapshot.rb
2324
lib/lvm/snapshot.rb
2425
lib/lvm/vg_config.rb

0 commit comments

Comments
 (0)