Skip to content

Commit

Permalink
Thin snapshot support
Browse files Browse the repository at this point in the history
The future is NOW.
  • Loading branch information
mpalmer committed May 22, 2014
1 parent 9e30954 commit cc6ad8b
Show file tree
Hide file tree
Showing 24 changed files with 1,208 additions and 168 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Gemfile.lock
pkg/
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source 'https://rubygems.org/'

gemspec
13 changes: 13 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
guard 'spork' do
watch('Gemfile') { :rspec }
watch('Gemfile.lock') { :rspec }
watch('spec/spec_helper.rb') { :rspec }
end

guard 'rspec',
:cmd => "rspec --drb",
:all_on_start => true do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/}) { "spec" }
end

36 changes: 36 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'rubygems'
require 'bundler'

begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end

require 'git-version-bump/rake-tasks'

Bundler::GemHelper.install_tasks

require 'rdoc/task'

Rake::RDocTask.new do |rd|
rd.main = "README.md"
rd.title = 'lvmsync'
rd.rdoc_files.include("README.md", "lib/**/*.rb")
end

desc "Run guard"
task :guard do
require 'guard'
::Guard.start(:clear => true)
while ::Guard.running do
sleep 0.5
end
end

require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new :test do |t|
t.pattern = "spec/**/*_spec.rb"
end
221 changes: 60 additions & 161 deletions lvmsync → bin/lvmsync
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Usage: Start with lvmsync --help, or read the README for all the gory
# details.
#
# Copyright (C) 2011-2013 Matt Palmer <[email protected]>
# Copyright (C) 2011-2014 Matt Palmer <[email protected]>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
Expand All @@ -18,9 +18,12 @@
# `LICENCE` file for more details.
#
require 'optparse'
require 'lvm'

PROTOCOL_VERSION = "lvmsync PROTO[2]"

include LVM::Helpers

def main()
# Parse me some options
options = {}
Expand Down Expand Up @@ -146,51 +149,35 @@ def run_client(opts)
destdev = opts[:destdev]
outfd = nil

snapshotdm = canonicalise_dm(snapshot)

# First, read the dm table so we can see what we're dealing with
dmtable = read_dm_table
dmlist = read_dm_list

if dmlist[snapshotdm].nil?
$stderr.puts "Could not find dm device '#{snapshot}' (name mangled to '#{snapshotdm}')"
exit 1
end
vg, lv = parse_snapshot_name(snapshot)

if dmtable[snapshotdm][0][:type] != 'snapshot'
$stderr.puts "#{snapshot} does not appear to be a snapshot"
exit 1
end

origindm = dm_from_devnum(dmtable[snapshotdm][0][:args][0], dmlist)
vgconfig = LVM::VGConfig.new(vg)

if origindm.nil?
$stderr.puts "CAN'T HAPPEN: No origin device for #{snapshot} found"
if vgconfig.logical_volumes[lv].nil?
$stderr.puts "#{snapshot}: Could not find logical volume"
exit 1
end

$stderr.puts "Found origin dm device: #{origindm}" if opts[:verbose]

exceptiondm = dm_from_devnum(dmtable[snapshotdm][0][:args][1], dmlist)

if exceptiondm.nil?
$stderr.puts "CAN'T HAPPEN: No exception list device for #{snapshot} found!"
snap = if vgconfig.logical_volumes[lv].snapshot?
if vgconfig.logical_volumes[lv].thin?
LVM::ThinSnapshot.new(vg, lv)
else
LVM::Snapshot.new(vg, lv)
end
else
$stderr.puts "#{snapshot}: Not a snapshot device"
exit 1
end

# Since, in principle, we're not supposed to be reading from the CoW
# device directly, the kernel makes no attempt to make the device's read
$stderr.puts "Origin device: #{vg}/#{snap.origin}" if opts[:verbose]

# Since, in principle, we're not supposed to be reading from snapshot
# devices directly, the kernel makes no attempt to make the device's read
# cache stay in sync with the actual state of the device. As a result,
# we have to manually drop all caches before the data looks consistent.
# PERFORMANCE WIN!
File.open("/proc/sys/vm/drop_caches", 'w') { |fd| fd.print "3" }

$stderr.puts "Reading snapshot metadata from /dev/mapper/#{exceptiondm}" if opts[:verbose]
$stderr.puts "Reading changed chunks from /dev/mapper/#{origindm}" if opts[:verbose]

xfer_count = 0
total_size = 0
chunksize = nil
snapback = opts[:snapback] ? "--snapback #{opts[:snapback]}" : ''

if opts[:stdout]
Expand All @@ -208,150 +195,62 @@ def run_client(opts)
outfd.puts PROTOCOL_VERSION

start_time = Time.now
File.open("/dev/mapper/#{origindm}", 'r') do |origindev|
File.open("/dev/mapper/#{exceptiondm}", 'r') do |snapdev|
chunksize = read_header(snapdev)
origin_offset = nil
xfer_count = 0
xfer_size = 0
total_size = 0

originfile = "/dev/mapper/#{vg.gsub('-', '--')}-#{snap.origin.gsub('-', '--')}"
File.open(originfile, 'r') do |origindev|
snap.differences.each do |r|
xfer_count += 1
chunk_size = r.last - r.first + 1
xfer_size += chunk_size

$stderr.puts "Sending chunk #{r.to_s}..." if opts[:verbose]
$stderr.puts "Seeking to #{r.first} in #{originfile}" if opts[:verbose]

snapdev.seek chunksize
in_progress = true
t = Time.now
while in_progress
(chunksize / 16).times do
origin_offset, snap_offset = snapdev.read(16).unpack("QQ")
origin_offset = ntohq(origin_offset)
snap_offset = ntohq(snap_offset)
if snap_offset == 0
in_progress = false
break
end
xfer_count += 1
$stderr.puts "Sending chunk #{origin_offset}" if opts[:verbose]
origindev.seek origin_offset * chunksize
outfd.print [htonq(origin_offset), chunksize].pack("QN")
outfd.print origindev.read(chunksize)
end
snapdev.seek chunksize * chunksize / 16, IO::SEEK_CUR if in_progress
$stderr.printf "\e[2K\rSending chunk %i (origin device offset %i), %.2fMB/s",
xfer_count,
origin_offset * chunksize,
chunksize * 16 / (Time.now - t) / 1048576
origindev.seek(r.first, IO::SEEK_SET)

outfd.print [htonq(r.first), chunk_size].pack("QN")
outfd.print origindev.read(chunk_size)

# Progress bar!
if xfer_count % 100 == 50
$stderr.printf "\e[2K\rSending chunk %i of %i, %.2fMB/s",
xfer_count,
snap.differences.length,
xfer_size / (Time.now - start_time) / 1048576
$stderr.flush
t = Time.now
end
end

origindev.seek(0, IO::SEEK_END)
total_size = origindev.tell / 4096
total_size = origindev.tell
end
$stderr.printf "\rTransferred %i of %i chunks (%i bytes per chunk) in %.2f seconds\n",
xfer_count, total_size, chunksize, Time.now - start_time

$stderr.printf "\rTransferred %i bytes in %.2f seconds\n",
xfer_size, Time.now - start_time

$stderr.printf "You transferred your changes %.2fx faster than a full dd!\n",
total_size.to_f / xfer_count
total_size.to_f / xfer_size
ensure
outfd.close unless outfd.nil? or outfd == $stdout

end

# Call dmsetup ls and turn that into a hash of dm_name => [maj, min] data.
# Note that maj, min will be integers, and dm_name a string.
def read_dm_list
dmlist = {}
`dmsetup ls`.split("\n").each do |l|
next unless l =~ /^(\S+)\s+\((\d+)(, |:)(\d+)\)$/
dmlist[$1] = [$2.to_i, $4.to_i]
end
dmlist
end

# Call dmsetup table and turn that into a complicated hash of dm table data.
#
# Structure is:
#
# dm_name => [
# { :offset => int,
# :length => int,
# :type => (linear|snapshot|...),
# :args => [str, str, str]
# },
# { ... }
# ],
# dm_name => [ ... ]
#
# The arguments are kept as a list of strings (split on whitespace), and
# you'll need to interpret them yourself. Turning this whole shebang into a
# class hierarchy is a task for another time.
#
def read_dm_table
dmtable = {}
`dmsetup table`.split("\n").each do |l|
next unless l =~ /^(\S+): (\d+) (\d+) (\S+) (.*)$/
dmtable[$1] ||= []
dmtable[$1] << { :offset => $2,
:length => $3,
:type => $4,
:args => $5.split(/\s+/)
}
end
dmtable
end

# Take a device name in any number of different formats and turn it into a
# "canonical" devicemapper name, equivalent to what you'll find in the likes
# of dmsetup ls
def canonicalise_dm(origname)
# Take a device name in any number of different formats and return a [VG, LV] pair.
# Raises ArgumentError if the name couldn't be parsed.
def parse_snapshot_name(origname)
case origname
when %r{^/dev/mapper/(.+)$} then
$1
when %r{^/dev/mapper/(.*[^-])-([^-].*)$} then
[$1, $2]
when %r{^/dev/([^/]+)/(.+)$} then
vg = $1
lv = $2
vg.gsub('-', '--') +
'-' +
lv.gsub('-', '--')
[$1, $2]
when %r{^([^/]+)/(.*)$} then
vg = $1
lv = $2
vg.gsub('-', '--') +
'-' +
lv.gsub('-', '--')
[$1, $2]
else
# Let's *assume* that the user just gave us vg-lv...
origname
raise ArgumentError,
"Could not determine snapshot name and VG from #{origname.inspect}"
end
end

# Find the name of a dm device that corresponds to the given <maj>:<min>
# string provided.
def dm_from_devnum(devnum, dmlist)
maj, min = devnum.split(':', 2)
dmlist.invert[[maj.to_i, min.to_i]]
end

# Read the header off our snapshot device, validate all is well, and return
# the chunksize used by the snapshot, in bytes
def read_header(snapdev)
magic, valid, metadata_version, chunksize = snapdev.read(16).unpack("VVVV")
raise RuntimeError.new("Invalid snapshot magic number") unless magic == 0x70416e53
raise RuntimeError.new("Snapshot marked as invalid") unless valid == 1
raise RuntimeError.new("Incompatible metadata version") unless metadata_version == 1

# With all that out of the way, we can get down to business
chunksize * 512
end

# Are we on a big-endian system? Needed for our htonq/ntohq methods
def big_endian?
@bigendian ||= [1].pack("s") == [1].pack("n")
end

def htonq val
big_endian? ? ([val].pack("Q").reverse.unpack("Q").first) : val
end

def ntohq val
htonq val
end

main if __FILE__ == $0
main
1 change: 0 additions & 1 deletion files/lvmsync

This file was deleted.

5 changes: 5 additions & 0 deletions lib/lvm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'lvm/helpers'
require 'lvm/thin_snapshot'
require 'lvm/snapshot'
require 'lvm/lv_config'
require 'lvm/vg_config'
18 changes: 18 additions & 0 deletions lib/lvm/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module LVM; end

module LVM::Helpers
# Are we on a big-endian system? Needed for our htonq/ntohq methods
def big_endian?
@bigendian ||= [1].pack("s") == [1].pack("n")
end

def htonq val
# This won't work on a nUxi byte-order machine, but if you have one of
# those, I'm guessing you've got bigger problems
big_endian? ? ([val].pack("Q").reverse.unpack("Q").first) : val
end

def ntohq val
htonq val
end
end
Loading

0 comments on commit cc6ad8b

Please sign in to comment.