lvmsync 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -114,11 +114,11 @@ commands should be run on `vmsrv1`:
114
114
 
115
115
  # Shutdown the VM -- the command you use will probably vary
116
116
  virsh shutdown somevm
117
-
117
+
118
118
  # Once it's shutdown and the block device isn't going to be written to
119
119
  # any more, then you can run lvmsync
120
120
  lvmsync /dev/vmsrv1/somevm-lvmsync vmsrv2:/dev/vmsrv2/somevm
121
-
121
+
122
122
  # You can now start up the VM on vmsrv2, after a fairly small period of
123
123
  # downtime. Once you're done, you can remove the snapshot and,
124
124
  # presumably, the LV itself, from `vmsrv1`
@@ -231,7 +231,7 @@ you'd do it like this:
231
231
 
232
232
  You can also do things like do an lvmsync *from* the destination -- this is
233
233
  useful if (for example) you can SSH from the destination to the source
234
- machine, but not the other way around (fkkn firewalls, how do they work?).
234
+ machine, but not the other way around (fkkn firewalls, how do they work?).
235
235
  You could do this by running something like the following on the destination
236
236
  machine:
237
237
 
@@ -248,7 +248,7 @@ order to work lvmsync.
248
248
  First, a little bit of background about how snapshot LVs work, before I
249
249
  describe how lvmsync makes use of them.
250
250
 
251
- An LVM snapshot "device" is actually not a block device in the usual sense.
251
+ An LVM snapshot "device" is actually not a block device in the usual sense.
252
252
  It isn't just a big area of disk space where you write things. Instead, it
253
253
  is a "meta" device, which points to both an "origin" LV, which is the LV
254
254
  from which the snapshot was made, and a "metadata" LV, which is where the
data/bin/lvmsync CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/ruby
1
+ #!/usr/bin/env ruby
2
2
 
3
3
  # Transfer a set of changes made to the origin of a snapshot LV to another
4
4
  # block device, possibly using SSH to send to a remote system.
@@ -33,7 +33,7 @@ def main()
33
33
  opts.separator " lvmsync [--snapback <file>] <snapshot device> [--stdout | [<desthost>:]<destdevice>]"
34
34
  opts.separator " lvmsync [--snapback <file>] --apply <changes file> <destdevice>"
35
35
  opts.separator ""
36
-
36
+
37
37
  opts.on("--server", "Run in server mode (deprecated; use '--apply -' instead)") do |v|
38
38
  options[:server] = true
39
39
  end
@@ -51,7 +51,7 @@ def main()
51
51
  options[:stdout] = true
52
52
  end
53
53
  end.parse!
54
-
54
+
55
55
  if options[:apply]
56
56
  if ARGV[0].nil?
57
57
  $stderr.puts "No destination device specified."
@@ -98,7 +98,7 @@ def run_apply(opts)
98
98
  snapfile = opts[:snapback] ? File.open(opts[:snapback], 'w') : nil
99
99
  infile = opts[:apply] == '-' ? $stdin : File.open(opts[:apply], 'r')
100
100
  destdev = opts[:device]
101
-
101
+
102
102
  process_dumpdata(infile, destdev, snapfile)
103
103
  ensure
104
104
  snapfile.close unless snapfile.nil?
@@ -116,7 +116,7 @@ def process_dumpdata(instream, destdev, snapback = nil)
116
116
  while header = instream.read(12)
117
117
  offset, chunksize = header.unpack("QN")
118
118
  offset = ntohq(offset)
119
-
119
+
120
120
  begin
121
121
  dest.seek offset * chunksize
122
122
  rescue Errno::EINVAL
@@ -126,13 +126,13 @@ def process_dumpdata(instream, destdev, snapback = nil)
126
126
  # seek past the end of the device. Yes, this may lose data, but
127
127
  # if you didn't notice that your dd shit itself, it's unlikely
128
128
  # you're going to notice now.
129
-
129
+
130
130
  # Skip the chunk of data
131
131
  instream.read(chunksize)
132
132
  # Go to the next chunk
133
133
  next
134
134
  end
135
-
135
+
136
136
  if snapback
137
137
  snapback.write(header)
138
138
  snapback.write dest.read(chunksize)
@@ -148,16 +148,16 @@ def run_client(opts)
148
148
  desthost = opts[:desthost]
149
149
  destdev = opts[:destdev]
150
150
  outfd = nil
151
-
151
+
152
152
  vg, lv = parse_snapshot_name(snapshot)
153
153
 
154
154
  vgconfig = LVM::VGConfig.new(vg)
155
-
155
+
156
156
  if vgconfig.logical_volumes[lv].nil?
157
157
  $stderr.puts "#{snapshot}: Could not find logical volume"
158
158
  exit 1
159
159
  end
160
-
160
+
161
161
  snap = if vgconfig.logical_volumes[lv].snapshot?
162
162
  if vgconfig.logical_volumes[lv].thin?
163
163
  LVM::ThinSnapshot.new(vg, lv)
@@ -170,11 +170,11 @@ def run_client(opts)
170
170
  end
171
171
 
172
172
  $stderr.puts "Origin device: #{vg}/#{snap.origin}" if opts[:verbose]
173
-
173
+
174
174
  # Since, in principle, we're not supposed to be reading from snapshot
175
175
  # devices directly, the kernel makes no attempt to make the device's read
176
176
  # cache stay in sync with the actual state of the device. As a result,
177
- # we have to manually drop all caches before the data looks consistent.
177
+ # we have to manually drop all caches before the data looks consistent.
178
178
  # PERFORMANCE WIN!
179
179
  File.open("/proc/sys/vm/drop_caches", 'w') { |fd| fd.print "3" }
180
180
 
@@ -188,12 +188,12 @@ def run_client(opts)
188
188
  else
189
189
  "lvmsync --apply - #{snapback} #{destdev}"
190
190
  end
191
-
191
+
192
192
  outfd = IO.popen(server_cmd, 'w')
193
193
  end
194
-
194
+
195
195
  outfd.puts PROTOCOL_VERSION
196
-
196
+
197
197
  start_time = Time.now
198
198
  xfer_count = 0
199
199
  xfer_size = 0
@@ -205,10 +205,10 @@ def run_client(opts)
205
205
  xfer_count += 1
206
206
  chunk_size = r.last - r.first + 1
207
207
  xfer_size += chunk_size
208
-
208
+
209
209
  $stderr.puts "Sending chunk #{r.to_s}..." if opts[:verbose]
210
210
  $stderr.puts "Seeking to #{r.first} in #{originfile}" if opts[:verbose]
211
-
211
+
212
212
  origindev.seek(r.first, IO::SEEK_SET)
213
213
 
214
214
  outfd.print [htonq(r.first), chunk_size].pack("QN")
@@ -230,7 +230,7 @@ def run_client(opts)
230
230
 
231
231
  $stderr.printf "\rTransferred %i bytes in %.2f seconds\n",
232
232
  xfer_size, Time.now - start_time
233
-
233
+
234
234
  $stderr.printf "You transferred your changes %.2fx faster than a full dd!\n",
235
235
  total_size.to_f / xfer_size
236
236
  ensure
data/lib/lvm/helpers.rb CHANGED
@@ -13,6 +13,6 @@ module LVM::Helpers
13
13
  end
14
14
 
15
15
  def ntohq val
16
- htonq val
16
+ htonq val
17
17
  end
18
18
  end
data/lib/lvm/lv_config.rb CHANGED
@@ -2,17 +2,17 @@ module LVM; end
2
2
 
3
3
  class LVM::LVConfig
4
4
  attr_reader :name
5
-
5
+
6
6
  def initialize(tree, name, vgcfg)
7
7
  @root = tree
8
8
  @name = name
9
9
  @vgcfg = vgcfg
10
10
  end
11
-
11
+
12
12
  def thin?
13
13
  @root.groups['segment1'].variable_value('type') == 'thin'
14
14
  end
15
-
15
+
16
16
  def snapshot?
17
17
  thin? ? !origin.nil? : !@vgcfg.logical_volumes.values.find { |lv| lv.cow_store == name }.nil?
18
18
  end
@@ -32,7 +32,7 @@ class LVM::LVConfig
32
32
  def cow_store
33
33
  @root.groups['segment1'].variable_value('cow_store')
34
34
  end
35
-
35
+
36
36
  def chunk_size
37
37
  @root.groups['segment1'].variable_value('chunk_size') * 512
38
38
  end
data/lib/lvm/snapshot.rb CHANGED
@@ -5,7 +5,7 @@ module LVM; end
5
5
 
6
6
  class LVM::Snapshot
7
7
  include LVM::Helpers
8
-
8
+
9
9
  def initialize(vg, lv)
10
10
  @vg = vg
11
11
  @lv = lv
@@ -18,16 +18,16 @@ class LVM::Snapshot
18
18
  # For a regular, old-skool snapshot, getting the differences is
19
19
  # pretty trivial -- just read through the snapshot metadata, and
20
20
  # the list of changed blocks is right there.
21
- #
21
+ #
22
22
  diff_block_list = []
23
23
 
24
24
  File.open(metadata_device, 'r') do |metafd|
25
25
  in_progress = true
26
-
26
+
27
27
  # The first chunk of the metadata LV is the header, which we
28
28
  # don't care for at all
29
29
  metafd.seek chunk_size, IO::SEEK_SET
30
-
30
+
31
31
  while in_progress
32
32
  # The snapshot on-disk format is a stream of <blocklist>, <blockdata>
33
33
  # sets; within each <blocklist>, it's network-byte-order 64-bit block
@@ -39,7 +39,7 @@ class LVM::Snapshot
39
39
  origin_offset, snap_offset = metafd.read(16).unpack("QQ")
40
40
  origin_offset = ntohq(origin_offset)
41
41
  snap_offset = ntohq(snap_offset)
42
-
42
+
43
43
  # A snapshot offset of 0 would point back to the metadata
44
44
  # device header, so that's clearly invalid -- hence it's the
45
45
  # "no more blocks" indicator.
@@ -47,21 +47,21 @@ class LVM::Snapshot
47
47
  in_progress = false
48
48
  break
49
49
  end
50
-
50
+
51
51
  diff_block_list << origin_offset
52
52
  end
53
-
53
+
54
54
  # We've read through a set of origin => data mappings; now we need
55
55
  # to take a giant leap over the data blocks that follow it.
56
56
  metafd.seek chunk_size * chunk_size / 16, IO::SEEK_CUR
57
57
  end
58
58
  end
59
-
59
+
60
60
  # Block-to-byte-range is pretty trivial, and we're done!
61
61
  diff_block_list.map do |b|
62
62
  ((b*chunk_size)..(((b+1)*chunk_size)-1))
63
63
  end
64
-
64
+
65
65
  # There is one optimisation we could make here that we haven't --
66
66
  # coalescing adjacent byte ranges into single larger ranges. I haven't
67
67
  # done it for two reasons: Firstly, I don't have any idea how much of a
@@ -69,44 +69,44 @@ class LVM::Snapshot
69
69
  # to do it elegantly. So I punted.
70
70
  end
71
71
  end
72
-
72
+
73
73
  def origin
74
74
  # Man old-skool snapshots are weird
75
75
  vgcfg.logical_volumes.values.find { |lv| lv.cow_store == @lv }.origin
76
76
  end
77
-
77
+
78
78
  private
79
79
  def vgcfg
80
80
  @vgcfg ||= LVM::VGConfig.new(@vg)
81
81
  end
82
-
82
+
83
83
  def chunk_size
84
84
  @chunk_size ||= metadata_header[:chunk_size]
85
85
  end
86
-
86
+
87
87
  def metadata_header
88
88
  @metadata_header ||= begin
89
89
  magic, valid, version, chunk_size = File.read(metadata_device, 16).unpack("VVVV")
90
-
90
+
91
91
  unless magic == 0x70416e53
92
92
  raise RuntimeError,
93
93
  "#{@vg}/#{@lv}: Invalid snapshot magic number"
94
94
  end
95
-
95
+
96
96
  unless valid == 1
97
97
  raise RuntimeError,
98
98
  "#{@vg}/#{@lv}: Snapshot is marked as invalid"
99
99
  end
100
-
100
+
101
101
  unless version == 1
102
102
  raise RuntimeError,
103
103
  "#{@vg}/#{@lv}: Incompatible snapshot metadata version"
104
104
  end
105
-
105
+
106
106
  { :chunk_size => chunk_size * 512 }
107
107
  end
108
108
  end
109
-
109
+
110
110
  def metadata_device
111
111
  "/dev/mapper/#{@vg}-#{@lv}-cow"
112
112
  end
@@ -69,19 +69,19 @@ class LVM::ThinSnapshot
69
69
  diff_maps = ((flat_origin_blocklist - flat_snapshot_blocklist) +
70
70
  (flat_snapshot_blocklist - flat_origin_blocklist)
71
71
  ).uniq
72
-
72
+
73
73
  # At this point, we're off to a good start -- we've got the mappings
74
74
  # that are different. But we're not actually interested in the
75
75
  # mappings themselves -- all we want is "the list of LV blocks which
76
76
  # are different" (we'll translate LV blocks into byte ranges next).
77
77
  #
78
78
  changed_blocks = diff_maps.map { |m| m[0] }.uniq
79
-
79
+
80
80
  # Block-to-byte-range is pretty trivial, and we're done!
81
81
  changed_blocks.map do |b|
82
82
  ((b*chunk_size)..(((b+1)*chunk_size)-1))
83
83
  end
84
-
84
+
85
85
  # There is one optimisation we could make here that we haven't --
86
86
  # coalescing adjacent byte ranges into single larger ranges. I haven't
87
87
  # done it for two reasons: Firstly, I don't have any idea how much of a
@@ -89,44 +89,44 @@ class LVM::ThinSnapshot
89
89
  # to do it elegantly. So I punted.
90
90
  end
91
91
  end
92
-
92
+
93
93
  def origin
94
94
  @origin ||= vgcfg.logical_volumes[@lv].origin
95
95
  end
96
-
96
+
97
97
  private
98
98
  def vgcfg
99
99
  @vgcfg ||= LVM::VGConfig.new(@vg)
100
100
  end
101
-
101
+
102
102
  def flat_origin_blocklist
103
103
  @flat_origin_blocklist ||= flatten_blocklist(origin_blocklist)
104
104
  end
105
-
105
+
106
106
  def flat_snapshot_blocklist
107
107
  @flat_snapshot_blocklist ||= flatten_blocklist(snapshot_blocklist)
108
108
  end
109
-
109
+
110
110
  def origin_blocklist
111
111
  @origin_blocklist ||= vg_block_dump[@vgcfg.logical_volumes[origin].device_id]
112
112
  end
113
-
113
+
114
114
  def snapshot_blocklist
115
115
  @snapshot_blocklist ||= vg_block_dump[@vgcfg.logical_volumes[@lv].device_id]
116
116
  end
117
-
117
+
118
118
  def thin_pool_name
119
119
  @thin_pool_name ||= vgcfg.logical_volumes[@lv].thin_pool
120
120
  end
121
-
121
+
122
122
  def thin_pool
123
123
  @thin_pool ||= vgcfg.logical_volumes[thin_pool_name]
124
124
  end
125
-
125
+
126
126
  def chunk_size
127
127
  @chunk_size ||= thin_pool.chunk_size
128
128
  end
129
-
129
+
130
130
  # Take a hash of <block-or-range> => <block-or-range> elements and turn
131
131
  # it into an array of [block, block] pairs -- any <range> => <range>
132
132
  # elements get expanded out into their constituent <block> => <block>
@@ -138,9 +138,9 @@ class LVM::ThinSnapshot
138
138
  if elem[0].is_a? Range
139
139
  lv_blocks = elem[0].to_a
140
140
  data_blocks = elem[1].to_a
141
-
141
+
142
142
  # This will now produce an array of two-element arrays, which
143
- # will itself be inside the top-level array that we're mapping.
143
+ # will itself be inside the top-level array that we're mapping.
144
144
  # A flatten(1) at the end will take care of that problem,
145
145
  # though.
146
146
  lv_blocks.inject([]) { |a, v| a << [v, data_blocks[a.length]] }
@@ -155,14 +155,14 @@ class LVM::ThinSnapshot
155
155
  end
156
156
  end.flatten(1)
157
157
  end
158
-
158
+
159
159
  def vg_block_dump
160
160
  @vg_block_dump ||= begin
161
161
  doc = REXML::Document.new(`thin_dump /dev/mapper/#{@vg.gsub('-', '--')}-#{thin_pool_name.gsub('-','--')}_tmeta`)
162
-
162
+
163
163
  doc.elements['superblock'].inject({}) do |h, dev|
164
164
  next h unless dev.node_type == :element
165
-
165
+
166
166
  maps = dev.elements[''].inject({}) do |h2, r|
167
167
  next h2 unless r.node_type == :element
168
168
 
data/lib/lvm/vg_config.rb CHANGED
@@ -23,50 +23,55 @@ class LVM::VGConfig
23
23
  "Cannot parse vgcfgbackup output: #{@parser.failure_reason}"
24
24
  end
25
25
  end
26
-
26
+
27
27
  def version
28
28
  @version ||= @root.variable_value('version')
29
29
  end
30
-
30
+
31
31
  def description
32
32
  @description ||= @root.variable_value('description')
33
33
  end
34
-
34
+
35
35
  def uuid
36
36
  @uuid ||= volume_group.variable_value('id')
37
37
  end
38
-
38
+
39
39
  def volume_group
40
40
  @volume_group ||= @root.groups[@vg_name]
41
41
  end
42
-
42
+
43
43
  def physical_volumes
44
44
  @physical_volumes ||= volume_group.groups['physical_volumes'].groups.to_a.inject({}) { |h,v| h[v[0]] = LVM::PVConfig.new(v[1]); h }
45
45
  end
46
-
46
+
47
47
  def logical_volumes
48
48
  @logical_volumes ||= volume_group.groups['logical_volumes'].groups.to_a.inject({}) { |h,v| h[v[0]] = LVM::LVConfig.new(v[1], v[0], self); h }
49
49
  end
50
-
50
+
51
51
  private
52
52
  def vgcfgbackup_output
53
53
  @vgcfgbackup_output ||= begin
54
+ out = nil
55
+
54
56
  Tempfile.open('vg_config') do |tmpf|
55
57
  cmd = "#{@vgcfgbackup_cmd} -f #{tmpf.path} #{@vg_name}"
56
- Open3.popen3(cmd) do |stdin_fd, stdout_fd, stderr_fd, thr|
58
+ stdout = nil
59
+ stderr = nil
60
+ Open3.popen3(cmd) do |stdin_fd, stdout_fd, stderr_fd|
57
61
  stdin_fd.close
58
62
  stdout = stdout_fd.read
59
63
  stderr = stderr_fd.read
60
- exit_status = thr.value
61
-
62
- if exit_status != 0
63
- raise RuntimeError,
64
- "Failed to run vgcfgbackup: #{stdout}\n#{stderr}"
65
- end
66
64
  end
67
-
68
- File.read(tmpf.path)
65
+
66
+ if $?.exitstatus != 0
67
+ raise RuntimeError,
68
+ "Failed to run vgcfgbackup: #{stdout}\n#{stderr}"
69
+ end
70
+
71
+ out = File.read(tmpf.path)
69
72
  end
73
+
74
+ out
70
75
  end
71
76
  end
72
77
  end
data/lib/vgcfgbackup.rb CHANGED
@@ -1,30 +1,30 @@
1
1
  module VgCfgBackup
2
2
  class Node < Treetop::Runtime::SyntaxNode
3
3
  end
4
-
4
+
5
5
  class Group < Node
6
6
  def name
7
7
  self.elements[0].text_value
8
8
  end
9
-
9
+
10
10
  def variables
11
11
  self.elements[3].elements.select { |e| e.is_a? Variable }
12
12
  end
13
-
13
+
14
14
  def variable_value(name)
15
15
  self.variables.find { |v| v.name == name }.value rescue nil
16
16
  end
17
-
17
+
18
18
  def groups
19
19
  self.elements[3].elements.select { |e| e.is_a? Group }.inject({}) { |h,v| h[v.name] = v; h }
20
20
  end
21
21
  end
22
-
22
+
23
23
  class Config < Group
24
24
  def name
25
25
  nil
26
26
  end
27
-
27
+
28
28
  def variables
29
29
  self.elements.select { |e| e.is_a? Variable }
30
30
  end
@@ -33,32 +33,32 @@ module VgCfgBackup
33
33
  self.elements.select { |e| e.is_a? Group }.inject({}) { |h,v| h[v.name] = v; h }
34
34
  end
35
35
  end
36
-
36
+
37
37
  class Variable < Node
38
38
  def name
39
39
  self.elements[0].text_value
40
40
  end
41
-
41
+
42
42
  def value
43
43
  self.elements[2].value
44
44
  end
45
45
  end
46
-
46
+
47
47
  class VariableName < Node
48
48
  end
49
-
49
+
50
50
  class Integer < Node
51
51
  def value
52
52
  self.text_value.to_i
53
53
  end
54
54
  end
55
-
55
+
56
56
  class String < Node
57
57
  def value
58
58
  self.elements[1].text_value
59
59
  end
60
60
  end
61
-
61
+
62
62
  class List < Node
63
63
  def value
64
64
  self.elements.find { |e| e.is_a?(String) }.map { |e| e.value }
@@ -6,31 +6,31 @@ grammar VgCfgBackup
6
6
  rule variable
7
7
  variable_name " = " (integer / string / list) <Variable>
8
8
  end
9
-
9
+
10
10
  rule variable_name
11
11
  [a-z0-9_]+ <VariableName>
12
12
  end
13
-
13
+
14
14
  rule group
15
15
  variable_name space "{" (variable / group / space / comment)+ "}" <Group>
16
16
  end
17
-
17
+
18
18
  rule integer
19
19
  [1-9] [0-9]* <Integer> / "0" <Integer>
20
20
  end
21
-
21
+
22
22
  rule string
23
23
  '"' [^"]* '"' <String>
24
24
  end
25
-
25
+
26
26
  rule list
27
27
  "[" space? ((string / integer) ", " / (string / integer))* space? "]" <List>
28
28
  end
29
-
29
+
30
30
  rule space
31
31
  [\s]+
32
32
  end
33
-
33
+
34
34
  rule comment
35
35
  "#" [^\n]*
36
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lvmsync
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-05-22 00:00:00.000000000 Z
12
+ date: 2014-05-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: git-version-bump
@@ -221,7 +221,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
221
  version: '0'
222
222
  segments:
223
223
  - 0
224
- hash: -3927931017616602686
224
+ hash: -3361242777286472333
225
225
  required_rubygems_version: !ruby/object:Gem::Requirement
226
226
  none: false
227
227
  requirements:
@@ -230,7 +230,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
230
230
  version: '0'
231
231
  segments:
232
232
  - 0
233
- hash: -3927931017616602686
233
+ hash: -3361242777286472333
234
234
  requirements: []
235
235
  rubyforge_project:
236
236
  rubygems_version: 1.8.23