Skip to content

Instantly share code, notes, and snippets.

@movitto
Created March 30, 2016 01:30
Show Gist options
  • Save movitto/9beef891b27a39035d51549709cc3899 to your computer and use it in GitHub Desktop.
Save movitto/9beef891b27a39035d51549709cc3899 to your computer and use it in GitHub Desktop.
LVM Parser & Block Reader
# LVM Parser & Block Reader
#
# Copyright (C) 2016 Red Hat Inc
require 'optparse'
require 'ostruct'
require 'binary_struct'
### constants
LVM_PARTITION_TYPE = 142
SECTOR_SIZE = 512
LABEL_SCAN_SECTORS = 4
LVM_ID_LEN = 8
LVM_TYPE_LEN = 8
LVM_ID = "LABELONE"
PV_ID_LEN = 32
MDA_MAGIC_LEN = 16
FMTT_MAGIC = "\040\114\126\115\062\040\170\133\065\101\045\162\060\116\052\076"
# On disk label header.
LABEL_HEADER = BinaryStruct.new([
"A#{LVM_ID_LEN}", 'lvm_id',
'Q', 'sector_xl',
'L', 'crc_xl',
'L', 'offset_xl',
"A#{LVM_TYPE_LEN}", 'lvm_type'
])
# On disk physical volume header.
PV_HEADER = BinaryStruct.new([
"A#{PV_ID_LEN}", 'pv_uuid',
"Q", 'device_size_xl'
])
# On disk disk location structure.
DISK_LOCN = BinaryStruct.new([
"Q", 'offset',
"Q", 'size'
])
# On disk metadata area header.
MDA_HEADER = BinaryStruct.new([
"L", 'checksum_xl',
"A#{MDA_MAGIC_LEN}", 'magic',
"L", 'version',
"Q", 'start',
"Q", 'size'
])
# On disk raw location header, points to metadata.
RAW_LOCN = BinaryStruct.new([
"Q", 'offset',
"Q", 'size',
"L", 'checksum',
"L", 'filler'
])
module MD
HASH_START = '{'
HASH_END = '}'
ARRAY_START = '['
ARRAY_END = ']'
STRING_START = '"'
STRING_END = '"'
end
module Thin
SECTOR_SIZE = 512
THIN_MAGIC = 27022010
SPACE_MAP_ROOT_SIZE = 128
MAX_METADATA_BITMAPS = 255
SUPERBLOCK = BinaryStruct.new([
'L', 'csum',
'L', 'flags_',
'Q', 'block',
'A16', 'uuid',
'Q', 'magic',
'L', 'version',
'L', 'time',
'Q', 'trans_id',
'Q', 'metadata_snap',
"A#{SPACE_MAP_ROOT_SIZE}", 'data_space_map_root',
"A#{SPACE_MAP_ROOT_SIZE}", 'metadata_space_map_root',
'Q', 'data_mapping_root',
'Q', 'device_details_root',
'L', 'data_block_size', # in 512-byte sectors
'L', 'metadata_block_size', # in 512-byte sectors
'Q', 'metadata_nr_blocks',
'L', 'compat_flags',
'L', 'compat_ro_flags',
'L', 'incompat_flags'
])
SPACE_MAP = BinaryStruct.new([
'Q', 'nr_blocks',
'Q', 'nr_allocated',
'Q', 'bitmap_root',
'Q', 'ref_count_root'
])
DISK_NODE = BinaryStruct.new([
'L', 'csum',
'L', 'flags',
'Q', 'blocknr',
'L', 'nr_entries',
'L', 'max_entries',
'L', 'value_size',
'L', 'padding'
#'Q', 'keys'
])
INDEX_ENTRY = BinaryStruct.new([
'Q', 'blocknr',
'L', 'nr_free',
'L', 'none_free_before'
])
METADATA_INDEX = BinaryStruct.new([
'L', 'csum',
'L', 'padding',
'Q', 'blocknr'
])
BITMAP_HEADER = BinaryStruct.new([
'L', 'csum',
'L', 'notused',
'Q', 'blocknr'
])
DEVICE_DETAILS = BinaryStruct.new([
'Q', 'mapped_blocks',
'Q', 'transaction_id',
'L', 'creation_time',
'L', 'snapshotted_time'
])
MAPPING_DETAILS = BinaryStruct.new([
'Q', 'value'
])
end
### structures
class RawDisk
def initialize(path)
@file = File.open(path)
end
def seek(pos)
@file.seek pos
end
def read(len)
@file.read len
end
end
class LVDisk
attr_accessor :lv, :block_size, :extent_size
def initialize(lv)
self.lv = lv
self.block_size = 512
self.extent_size = lv.vg.extent_size * block_size
end
def size
lv.num_blocks
end
def read(pos, len)
return read_thin(pos, len) if lv.thin?
str = ''
end_pos = pos + len - 1
start_seg, end_seg = get_segs(pos, end_pos)
(start_seg..end_seg).each do |si|
seg = lv.segments[si]
seg_start = seg.block_start
srs = seg_start # segment read start
srl = seg.size # segment read length
if si == start_seg
srs = pos
srl = seg.size - (pos - seg_start)
end
if si == end_seg
srl = end_pos - srs + 1
end
str << read_seg(seg, srs, srl)
end
str
end
private
def read_thin(pos, len)
str = ''
device_id = lv.thin_segment.device_id
thin_pool = lv.thin_pool_volume
data_blks = thin_pool.metadata_volume.superblock.device_to_data(device_id, pos, len)
data_blks.each do |offset, len|
str << thin_pool.data_volume.disk.read(offset, len)
end
return str
end
def get_segs(pos, end_pos)
# start / end segment
ss = nil
es = nil
lv.segments.each_with_index do |seg, i|
ss = i if seg.address_range === pos
if seg.address_range === end_pos
raise "Segment sequence error" unless ss
es = i
break
end
end
raise "Segment range error" if !ss || !es
return ss, es
end
def read_seg(seg, start, len)
# TODO: support segment types other than linear (stripeCount = 1)
stripe = seg.stripes[0]
pos = stripe.start_address + start - seg.block_start
disk = stripe.physical_volume.disk
disk.seek pos #, IO::SEEK_SET
disk.read len
end
end
class VolumeGroup
attr_accessor :id, :name,
:extent_size, :seqno, :status,
:physical_volumes, :logical_volumes,
:disk_obj
def initialize(args={})
args.keys.each { |k| send("#{k}=".intern, args[k]) }
physical_volumes.values.each { |pv| pv.vg = self } if physical_volumes
logical_volumes.values.each { |lv| lv.vg = self } if logical_volumes
end
end
class PhysicalVolume
attr_accessor :id, :name,
:device, :device_size,
:pe_start, :pe_count,
:status, :vg
def initialize(args={})
args.keys.each { |k| send("#{k}=".intern, args[k]) }
end
def disk
@disk ||= $volume_groups.find { |vg|
vg.disk_obj.pv_header.pv_uuid == id
}.disk_obj.raw_disk
end
end
class LogicalVolume
attr_accessor :id, :name,
:segment_count, :segments,
:status, :vg
def initialize(args={})
args.keys.each { |k| send("#{k}=".intern, args[k]) }
segments.each { |seg| seg.lv = self } if segments
end
def disk
@disk ||= begin
LVDisk.new self
end
end
def num_blocks
@num_blocks ||= segments.inject(0) { |num, seg| num + seg.num_blocks }
end
# will raise exception if LogicalVolume is not thin metadata volume
def superblock
@superblock ||= Thin::SuperBlock.get self
end
def thin_segments
@thin_segments ||= segments.select { |segment| segment.thin? }
end
def thin_segment
@thin_segment ||= thin_segments.first
end
def thin?
!thin_segments.empty?
end
def thin_pool_volume
return nil unless thin?
return thin_segment.thin_pool_volume
end
def thin_pool_segments
@thin_pool_segments ||= segments.select { |segment| segment.thin_pool? }
end
def thin_pool_segment
thin_pool_segments.first
end
def thin_pool?
!thin_pool_segments.empty?
end
def metadata_volume
thin_pool_segment.metadata_volume
end
def data_volume
thin_pool_segment.data_volume
end
def set_volumes(lvs)
segments.each { |seg| seg.set_volumes lvs }
end
end
class LVSegment
attr_accessor :start_extent, :extent_count, :type,
:stripe_count, :stripes, :device_id, :lv
attr_accessor :thin_pool, :metadata, :pool
attr_accessor :thin_pool_volume, :metadata_volume, :data_volume
def initialize(args={})
args.keys.each { |k| send("#{k}=".intern, args[k]) }
stripes.each { |stripe| stripe.seg = self } if stripes
end
def start_address
@start_address ||= start_extent * lv.disk.extent_size
end
def end_address
@end_address ||= (start_extent + extent_count) * lv.disk.extent_size - 1
end
def address_range
@address_range ||= Range.new start_address, end_address, false
end
def block_start
@block_start ||= address_range.begin
end
def size
@size ||= end_address - start_address + 1
end
def num_blocks
@num_blocks ||= size / lv.disk.block_size
end
def thin?
type == 'thin'
end
def thin_pool?
type == 'thin-pool'
end
def set_metadata_volume(lvs)
@metadata_volume = lvs.find { |lv| lv.name == metadata }
end
def set_data_volume(lvs)
@data_volume = lvs.find { |lv| lv.name == pool }
end
def set_thin_pool_volume(lvs)
@thin_pool_volume = lvs.find { |lv| lv.name == thin_pool }
end
def set_volumes(lvs)
set_metadata_volume lvs
set_data_volume lvs
set_thin_pool_volume lvs
end
end
class LVSegStripe
attr_accessor :pvn, :ext, :seg
def initialize(pvn, ext)
self.pvn = pvn
self.ext = ext
end
def physical_volume
@physical_volume ||= begin
pv = seg.lv.vg.physical_volumes[pvn]
raise "Physical volume object (#{pvn}) not found in volume group" if pv.nil?
pv
end
end
def start_address
@start_address ||= (physical_volume.pe_start * seg.lv.disk.block_size) +
(ext * seg.lv.disk.extent_size)
end
end
### thin structures
module Thin
class BTree
FLAGS = { :internal => 1, :leaf => 2}
attr_accessor :root_address
def initialize(superblock, root_address, value_type)
@superblock = superblock
@root_address = root_address
@value_type = value_type
end
def root
@root ||= begin
@superblock.seek root_address
@superblock.read_struct DISK_NODE
end
end
def internal?
(root['flags'] & FLAGS[:internal]) != 0
end
def leaf?
(root['flags'] & FLAGS[:leaf]) != 0
end
def num_entries
@num_entries ||= root['nr_entries']
end
def max_entries
@max_entries ||= root['max_entries']
end
def key_base
root_address + DISK_NODE.size
end
def key_address(i)
key_base + i * 8
end
def value_base
key_address(max_entries)
end
def value_address(i)
value_base + @value_type.size * i
end
def keys
@keys ||= begin
@superblock.seek key_base
@superblock.read(num_entries * 8).unpack("Q#{num_entries}")
end
end
def entries
@entries ||= begin
@superblock.seek value_base
@superblock.read_structs @value_type, num_entries
end
end
def entry_for(key)
entries[keys.index(key)]
end
def to_h
@h ||=
Hash[0.upto(num_entries-1).collect do |i|
k = keys[i]
e = entries[i].kind_of?(BTree) ? entries[i].to_h : entries[i]
[k, e]
end]
end
def [](key)
return to_h[key]
end
end # class BTree
class DataMap < BTree
TIME_MASK = (1 << 24) - 1
def initialize(superblock, root_address)
super superblock, root_address, MAPPING_DETAILS
end
alias :device_blocks :keys
def entries
@dmentries ||= begin
super.collect do |entry|
value = entry['value']
internal? ? DataMap.new(@superblock, @superblock.md_block_address(value)) :
[extract_data_block(value), extract_time(value)]
end
end
end
def data_block(device_block)
device_blocks.reverse.each do |map_device_block|
if map_device_block <= device_block
entry = entry_for(map_device_block)
return entry.data_block(device_block) if entry.is_a?(DataMap)
raise RuntimeError, "LVM2Thin cannot find device block: #{device_block} (closest: #{map_device_block})" unless map_device_block == device_block
return entry.first
end
end
raise RuntimeError, "LVM2Thin could not find data block for #{device_block}"
end
private
def extract_data_block(value)
value >> 24
end
def extract_time(value)
value & TIME_MASK
end
end # class DataMap
class MappingTree < BTree
def initialize(superblock, root_address)
super superblock, root_address, MAPPING_DETAILS
end
def entries
@mtentries ||= begin
super.collect do |entry|
DataMap.new @superblock, @superblock.md_block_address(entry['value'])
end
end
end
def map_for(device_id)
entry_for(device_id)
end
end # class MappingTree
class DataSpaceMap
attr_accessor :struct
def initialize(superblock, struct)
@superblock = superblock
@struct = struct
end
def btree_root_address
@btree_root_address ||= @superblock.md_block_address(struct['bitmap_root'])
end
def btree
@btree ||= BTree.new @superblock, btree_root_address, INDEX_ENTRY
end
end
class MetadataSpaceMap
def metadata_root_address
@metadata_root_address ||= @superblock.md_block_address(struct['bitmap_root'])
end
def root
@metadata_root ||= begin
@superblock.seek metadata_root_address
@superblock.read_struct METADATA_INDEX
end
end
def indices
@metadata_indices ||= (struct['nr_blocks'].to_f / @superblock.entries_per_block).ceil
end
def index_entries
@index_entries ||=
0.upto(indices-1).collect do |i|
address = metadata_root_address + METADATA_INDEX.size + i * INDEX_ENTRY.size
@superblock.seek address
@superblock.read_struct INDEX_ENTRY
end
end
def bitmaps
@bitmaps ||= index_entries.collect do |index_entry|
@superblock.seek @superblock.md_block_address(index_entry['blocknr'])
@superblock.read_struct BITMAP_HEADER
end
end
attr_accessor :struct
def initialize(superblock, struct)
@superblock = superblock
@struct = struct
end
end # class DataSpaceMap
class SuperBlock
attr_accessor :metadata_volume
attr_accessor :struct
def self.get(metadata_volume)
@superblock ||= begin
superblock = SuperBlock.new
superblock.metadata_volume = metadata_volume
superblock.seek 0
superblock.struct = superblock.read_struct SUPERBLOCK
raise "unknown lvm2 thin metadata magic number" if superblock.struct.magic != THIN_MAGIC
superblock
end
end
### superblock properties:
def md_block_size
@md_block_size ||= struct['metadata_block_size'] * 512 # = 4096
end
def md_block_address(blk_addr)
blk_addr * md_block_size
end
def entries_per_block
@entries_per_block ||= (md_block_size - BITMAP_HEADER.size) * 4
end
def data_block_size
@data_block_size ||= struct['data_block_size'] * 512
end
def data_block_address(blk_addr)
blk_addr * data_block_size
end
### lvm thin structures:
def data_space_map
@data_space_map ||= begin
seek SUPERBLOCK.offset('data_space_map_root')
DataSpaceMap.new self, read_struct(SPACE_MAP)
end
end
def metadata_space_map
@metadata_space_map ||= begin
seek SUPERBLOCK.offset('metadata_space_map_root')
MetadataSpaceMap.new self, read_struct(SPACE_MAP)
end
end
def data_mapping_address
@data_mapping_address ||= md_block_address(struct['data_mapping_root'])
end
def data_mapping
@data_mapping ||= MappingTree.new self, data_mapping_address
end
def device_details_address
@device_details_address ||= md_block_address(struct['device_details_root'])
end
def device_details
@device_details ||= BTree.new self, device_details_address, DEVICE_DETAILS
end
### address resolution / mapping:
def device_block(device_address)
(device_address / data_block_size).to_i
end
def device_block_offset(device_address)
device_address % data_block_size
end
# return array of tuples containing data volume addresses and lengths to
# read from them to read the specified device offset & length
def device_to_data(device_id, pos, len)
dev_blk = device_block(pos)
dev_off = device_block_offset(pos)
total_len = 0
data_blks = []
num_data_blks = (len / data_block_size).to_i + 1
0.upto(num_data_blks - 1) do |i|
data_blk = data_mapping.map_for(device_id).data_block(dev_blk + i)
blk_start = data_blk * data_block_size
blk_len = 0
if i == 0
blk_start += dev_off
blk_len = data_block_size - dev_off - 1
elsif i == num_data_blks - 1
blk_len = len - total_len
else
blk_len = data_block_size
end
total_len += blk_len
data_blks << [blk_start, blk_len]
end
data_blks
end
### metadata volume disk helpers:
def seek(pos)
@seek_pos = pos
end
def read(n)
@metadata_volume.disk.read @seek_pos, n
end
def read_struct(struct)
OpenStruct.new(struct.decode(@metadata_volume.disk.read(@seek_pos, struct.size)))
end
def read_structs(struct, num)
Array.new(num) do
read_struct struct
end
end
end # class SuperBlock
end # module Thin
### processing
def disks
@disks ||= []
end
optparse = OptionParser.new do |opts|
opts.on('-h', '--help', 'Display this help screen') do
puts opts
exit
end
opts.on('-d', '--disk |path|') do |disk|
disks << disk
end
end
optparse.parse!
if disks.empty?
puts "need at least one disk"
exit 1
end
$volume_groups = []
disks.each do |disk|
$disk_obj = OpenStruct.new
$disk_obj.raw_disk = RawDisk.new disk
def disk_obj
$disk_obj
end
def seek(pos)
disk_obj.raw_disk.seek pos
end
def read(len)
disk_obj.raw_disk.read len
end
def read_struct(struct)
OpenStruct.new(struct.decode(read(struct.size)))
end
### label scanner / metadata extractor
def label_sectors
(0...LABEL_SCAN_SECTORS).collect do |s|
s * SECTOR_SIZE
end
end
def is_label?(struct)
struct.lvm_id == LVM_ID
end
def labels
disk_obj.labels ||=
label_sectors.collect do |s|
seek s
struct = read_struct LABEL_HEADER
is_label?(struct) ? struct : nil
end.compact
end
def label
disk_obj.label ||= labels.first
end
def pv_header_address
disk_obj.pv_header_address ||=
(label.sector_xl * SECTOR_SIZE) + label.offset_xl
end
def _pv_header
disk_obj._pv_header ||= begin
seek pv_header_address
read_struct PV_HEADER
end
end
def _disk_locations
_pv_header.disk_locations ||= begin
locations = []
loop do
location = read_struct DISK_LOCN
break if location.offset == 0
locations << location
end
locations
end
end
def _metadata_disk_locations
_pv_header.metadata_disk_locations ||= begin
locations = []
loop do
location = read_struct DISK_LOCN
break if location.offset == 0
locations << location
end
locations
end
end
def pv_header
disk_obj.pv_header ||= begin
ph = _pv_header
_disk_locations
_metadata_disk_locations
ph
end
end
def _metadata_locations(md)
locations = []
loop do
location = read_struct RAW_LOCN
break if location.offset == 0
location.base = md.start
locations << location
end
locations
end
def metadata_headers
disk_obj.metadata_headers ||= begin
pv_header.metadata_disk_locations.collect do |loc|
seek loc.offset
md = read_struct MDA_HEADER
raise "Unknown LVM2 Magic" if md.magic != FMTT_MAGIC
md.raw_locations = _metadata_locations(md)
md
end
end
end
def _sanitize_raw_metadata(md)
md.gsub(/#.*$/, "")
.gsub("[", "[ ")
.gsub("]", " ]")
.gsub('"', ' " ')
.delete("=,")
.gsub(/\s+/, " ")
.split(' ')
end
def raw_metadata
disk_obj.raw_metadata ||= begin
metadata_headers.collect { |hdr| hdr.raw_locations }
.flatten.collect do |location|
seek location.base + location.offset
_sanitize_raw_metadata read(location.size)
end
end
end
def num_vgs
@num_vgs ||= raw_metadata.size
end
### metadata parser
$raw_metadata = nil
$raw_metadata_i = nil
def _raw_metadata
$raw_metadata ||= Array.new(raw_metadata[$raw_metadata_i])
end
def _raw_vg_name
$raw_metadata = nil
_raw_metadata.shift
end
def _parse_metadata_hash
hash = {}
name = _raw_metadata.shift
while name && name != MD::HASH_END
hash[name] = _parse_metadata_obj
name = _raw_metadata.shift
end
hash
end
def _parse_metadata_array
array = []
val = _raw_metadata.shift
while val && val != MD::ARRAY_END
array << _parse_metadata_val(val)
val = _raw_metadata.shift
end
array
end
def _parse_metadata_val(val)
if val == MD::STRING_START
return _parse_metadata_string
else
return val
end
end
def _parse_metadata_string
str = ''
word = _raw_metadata.shift
while word && word != MD::STRING_END
str << word + " "
word = _raw_metadata.shift
end
str.chomp(" ")
end
def _parse_metadata_obj
val = _raw_metadata.shift
case val
when MD::HASH_START
_parse_metadata_hash
when MD::ARRAY_START
_parse_metadata_array
else
_parse_metadata_val(val)
end
end
def metadata
disk_obj.metadata ||= begin
md = {}
0.upto(num_vgs-1) do |i|
$raw_metadata_i = i
md[_raw_vg_name] = _parse_metadata_obj
end
md
end
end
def vg_objs
disk_obj.vg_objs ||= begin
metadata.collect do |vg_name, vgobj|
VolumeGroup.new :id => vgobj['id'],
:name => vg_name,
:extent_size => vgobj['extent_size'].to_i,
:seqno => vgobj['seqno'],
:status => vgobj["status"],
:physical_volumes => _pv_objs(vgobj["physical_volumes"]),
:logical_volumes => _lv_objs(vgobj["logical_volumes"]),
:disk_obj => disk_obj
end
end
end
def _pv_objs(pv_metadata)
pvobjs = {}
pv_metadata.each do |name, pvobj|
pvobjs[name] = _pv_obj(name, pvobj)
end
pvobjs
end
def _pv_obj(name, pvobj)
PhysicalVolume.new :id => pvobj['id'].delete('-'),
:name => name,
:device => pvobj['device'],
:device_size => pvobj['dev_size'],
:pe_start => pvobj['pe_start'].to_i,
:pe_count => pvobj['pe_count'].to_i,
:status => pvobj["status"]
end
def _lv_objs(lv_metadata)
lvobjs = {}
lv_metadata.each do |name, lvobj|
lvobjs[name] = _lv_obj(name, lvobj)
end
lvobjs
end
def _lv_obj(name, lvobj)
LogicalVolume.new :id => lvobj['id'],
:name => name,
:segment_count => lvobj['segment_count'].to_i,
:status => lvobj['status'],
:segments => _lv_segments(lvobj)
end
def _lv_segments(lvobj)
(1..lvobj['segment_count'].to_i).collect { |s| _lv_segment(lvobj["segment#{s}"]) }
end
def _lv_segment(segobj)
LVSegment.new :start_extent => segobj['start_extent'].to_i,
:extent_count => segobj['extent_count'].to_i,
:type => segobj['type'],
:stripe_count => segobj['stripe_count'].to_i,
:device_id => _lv_seg_device_id(segobj),
:thin_pool => segobj['thin_pool'],
:metadata => segobj['metadata'],
:pool => segobj['pool'],
:stripes => _lv_seg_stripes(segobj)
end
def _lv_seg_device_id(segobj)
segobj['device_id'] ? segobj['device_id'].to_i : nil
end
def _lv_seg_stripes(segobj)
segobj['stripes'] ? (segobj['stripes'].each_slice(2).collect { |pv, o| LVSegStripe.new(pv, o.to_i) }) : nil
end
###
$volume_groups += vg_objs
end
logical_volumes = $volume_groups.collect { |vg| vg.logical_volumes.values }.flatten
logical_volumes.each { |lv| lv.set_volumes logical_volumes }
#puts $volume_groups.first.logical_volumes["root"].disk.read 7929824, 512
@brianjmurrell
Copy link

@movitto Heh. Yeah. I understand doing things so long ago that you forget the details. I've come up with:

  def offset()
    start_seg, end_seg = get_segs(0, 50000000000)
    puts start_seg, end_seg

    (start_seg..end_seg).each do |si|
      seg       = lv.segments[si]
      seg_start = seg.block_start
      srs       = seg_start # segment read start
      srl       = seg.size  # segment read length
      stripe    = seg.stripes[0]
      pos       = stripe.start_address + srs - seg.block_start
      puts pos, seg.size
    end
  end

as a member of LVDisk, however the 50000000000 is just some value greater than the size of the LV to ensure all segments are returned. That would need fleshing out a bit.

Maybe a better class member would be 'dd_cmd() which would give you the dd command(s) to copy the LV somewhere. :-)

Anyway, the above seems to be working for my purpose of recovering some LVs on a broken VG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment