Skip to content

Instantly share code, notes, and snippets.

@movitto
Created March 30, 2016 01:30
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
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

brianjmurrell commented Jan 9, 2022

This is cool. But can this tell me the byte offset of an LV on the physical disk?

I guess it must be able to given the example on the last line, using disk.read() to read from an LV. So I guess the physical disk offset must be calculatable. I will start working through the various objects and see if I can figure it out, but if you cared to save me some time and show me, rather than reading from that "root" disk, how to display the physical volume offset, suitable for giving to dd for example, that would be very useful. Much thanks!

@movitto
Copy link
Author

movitto commented Jan 9, 2022

@brianjmurrell it's been so long since I wrote this that I forgot! Probably with some analysis of the on disk LVM structures.

@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