lvmsync 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENCE +674 -0
- data/README.md +310 -0
- data/bin/lvmsync +256 -0
- data/lib/lvm.rb +5 -0
- data/lib/lvm/helpers.rb +18 -0
- data/lib/lvm/lv_config.rb +39 -0
- data/lib/lvm/pv_config.rb +7 -0
- data/lib/lvm/snapshot.rb +113 -0
- data/lib/lvm/thin_snapshot.rb +186 -0
- data/lib/lvm/vg_config.rb +72 -0
- data/lib/vgcfgbackup.rb +67 -0
- data/lib/vgcfgbackup.treetop +37 -0
- metadata +240 -0
data/README.md
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
# lvmsync
|
2
|
+
|
3
|
+
Have you ever wanted to do a partial sync on a block device, possibly over a
|
4
|
+
network, but were stymied by the fact that rsync just didn't work?
|
5
|
+
|
6
|
+
Well, fret no longer. As long as you use LVM for your block devices, you
|
7
|
+
too can have efficient delta-transfer of changed blocks.
|
8
|
+
|
9
|
+
|
10
|
+
## What is it good for?
|
11
|
+
|
12
|
+
Mostly, transferring entire block devices from one machine to another, with
|
13
|
+
minimal downtime. Until now, you had to shutdown your service/VM/whatever,
|
14
|
+
do a big cross-network dd (using netcat or something), and wait while all
|
15
|
+
that transferred.
|
16
|
+
|
17
|
+
`lvmsync` allows you to use the following workflow to transfer a block
|
18
|
+
device "mostly live" to another machine:
|
19
|
+
|
20
|
+
1. Take a snapshot of an existing LV.
|
21
|
+
1. Transfer the entire snapshot over the network, while whatever uses the
|
22
|
+
block device itself keeps running.
|
23
|
+
1. When the initial transfer is finished, you shutdown/unmount/whatever the
|
24
|
+
initial block device.
|
25
|
+
1. Run lvmsync on the snapshot to transfer the changed blocks
|
26
|
+
* The only thing transferred over the network is the blocks that have
|
27
|
+
changed (which, hopefully, will be minimal)
|
28
|
+
1. If you're paranoid, you can md5sum the content of the source and
|
29
|
+
destination block devices, to make sure everything's OK (although this will
|
30
|
+
destroy any performance benefit you got by running lvmsync in the first
|
31
|
+
lace)
|
32
|
+
1. Bring the service/VM/whatever back up in it's new home in a *much*
|
33
|
+
shorter (as in, "orders of magnitude") time than was previously possible.
|
34
|
+
|
35
|
+
`lvmsync` also has a basic "snapshot-and-rollback" feature, where it can
|
36
|
+
save a copy of the data in the LV that you're overwriting to a file for
|
37
|
+
later application if you need to rollback. See "Snapback support" under
|
38
|
+
"How do I use it?" for more details.
|
39
|
+
|
40
|
+
|
41
|
+
## How does it work?
|
42
|
+
|
43
|
+
By the magic of LVM snapshots. `lvmsync` is able to read the metadata that
|
44
|
+
device-mapper uses to keep track of what parts of the block device have
|
45
|
+
changed, and use that information to only send those modified blocks over
|
46
|
+
the network.
|
47
|
+
|
48
|
+
If you're really interested in the gory details, there's a brief "Theory of
|
49
|
+
Operation" section at the bottom of this README, or else you can just head
|
50
|
+
straight for the source code.
|
51
|
+
|
52
|
+
|
53
|
+
## Installation
|
54
|
+
|
55
|
+
On the machine you're transferring from, you'll need have `dmsetup` and
|
56
|
+
`ssh` installed and available on the PATH, and an installation of Ruby 1.8
|
57
|
+
(or later). Then just copy the `lvmsync` script to somewhere in root's
|
58
|
+
PATH.
|
59
|
+
|
60
|
+
On the machine you're transferring *to*, you'll need `sshd` installed and
|
61
|
+
available for connection, and an installation of Ruby 1.8 (or later). Then
|
62
|
+
just copy the `lvmsync` script to somewhere in root's PATH.
|
63
|
+
|
64
|
+
|
65
|
+
## How do I use it?
|
66
|
+
|
67
|
+
For an overview of all available options, run `lvmsync -h`.
|
68
|
+
|
69
|
+
|
70
|
+
### Efficient block device transfer
|
71
|
+
|
72
|
+
At present, the only part of the block device syncing process that is
|
73
|
+
automated is the actual transfer of the snapshot changes -- the rest (making
|
74
|
+
the snapshot, doing the initial transfer, and stopping all writes to the LV)
|
75
|
+
you'll have to do yourself. Those other steps aren't difficult, though, and
|
76
|
+
are trivial to script to suit your local environment (see the example,
|
77
|
+
below).
|
78
|
+
|
79
|
+
Once you've got the snapshot installed, done the initial sync, and stopped
|
80
|
+
I/O, you just call `lvmsync` like this:
|
81
|
+
|
82
|
+
lvmsync <snapshot LV device> <destserver>:<destblock>
|
83
|
+
|
84
|
+
This requires that `lvmsync` is installed on `<destserver>`, and that you
|
85
|
+
have the ability to SSH into `<destserver>` as root. All data transfer
|
86
|
+
takes place over SSH, because we don't trust any network, and it simplifies
|
87
|
+
so many things (such as link-level compression, if you want it). If CPU is
|
88
|
+
an issue, you shouldn't be running LVM on your phone to begin with.
|
89
|
+
|
90
|
+
The reason why `lvmsync` needs you to specify the snapshot you want to sync,
|
91
|
+
and not the base LV, is that you might have more than one snapshot of a
|
92
|
+
given LV, and while we can determine the base LV given a snapshot, you can't
|
93
|
+
work out which snapshot to sync given a base LV. Remember to always specify
|
94
|
+
the full device path, not just the LV name.
|
95
|
+
|
96
|
+
|
97
|
+
#### Example
|
98
|
+
|
99
|
+
Let's say you've got an LV, named `vmsrv1/somevm`, and you'd like to
|
100
|
+
synchronise it to a new VM server, named `vmsrv2`. Assuming that `lvmsync` is
|
101
|
+
installed on `vmsrv2` and `vmsrv2` has an LV named `vmsrv2/somevm` large
|
102
|
+
enough to take the data, the following will do the trick rather nicely (all
|
103
|
+
commands should be run on `vmsrv1`:
|
104
|
+
|
105
|
+
# Take a snapshot before we do anything, so LVM will record all changes
|
106
|
+
# made while we're doing the initial sync
|
107
|
+
lvcreate --snapshot -L10G -n somevm-lvmsync vmsrv1/somevm
|
108
|
+
|
109
|
+
# Pre-sync all data across -- this will take some time, but while it's
|
110
|
+
# happening the VM is still serving traffic. pv is a great tool for
|
111
|
+
# showing you how fast your data's moving, but you can leave it out of
|
112
|
+
# the pipeline if you don't have it installed.
|
113
|
+
dd if=/dev/vmsrv1/somevm-lvmsync bs=1M | pv -ptrb | ssh root@vmsrv2 dd of=/dev/vmsrv2/somevm bs=1M
|
114
|
+
|
115
|
+
# Shutdown the VM -- the command you use will probably vary
|
116
|
+
virsh shutdown somevm
|
117
|
+
|
118
|
+
# Once it's shutdown and the block device isn't going to be written to
|
119
|
+
# any more, then you can run lvmsync
|
120
|
+
lvmsync /dev/vmsrv1/somevm-lvmsync vmsrv2:/dev/vmsrv2/somevm
|
121
|
+
|
122
|
+
# You can now start up the VM on vmsrv2, after a fairly small period of
|
123
|
+
# downtime. Once you're done, you can remove the snapshot and,
|
124
|
+
# presumably, the LV itself, from `vmsrv1`
|
125
|
+
|
126
|
+
|
127
|
+
### Snapback support
|
128
|
+
|
129
|
+
In addition to being able to efficiently transfer the changes to an LV
|
130
|
+
across a network, `lvmsync` now supports a simple form of point-in-time
|
131
|
+
recovery, which I've called 'snapback'.
|
132
|
+
|
133
|
+
The way this works is startlingly simple: as `lvmsync` writes the changed
|
134
|
+
blocks out to the destination block device, it reads the data that is being
|
135
|
+
overwritten, and stores it to a file (specified with the `--snapback`
|
136
|
+
option). The format of this file is the same as the wire protocol that
|
137
|
+
`lvmsync` uses to transfer changed blocks over the network. This means
|
138
|
+
that, in the event that you need to rollback a block device to an earlier
|
139
|
+
state, you can do so by simply applying the saved snapback files created
|
140
|
+
previously, until you get to the desired state.
|
141
|
+
|
142
|
+
|
143
|
+
#### Example
|
144
|
+
|
145
|
+
To setup a snapback process, you need to have a local LV, with a snapshot,
|
146
|
+
whose contents have been sent to a remote server, perhaps something like
|
147
|
+
this:
|
148
|
+
|
149
|
+
lvcreate --snapshot -L10G -n somevm-snapback vmsrv1/somevm
|
150
|
+
dd if=/dev/vmsrv1/somevm-snapback bs=1M | pv -ptrb | \
|
151
|
+
ssh root@vmsrv2 dd of=/dev/vmsrv2/somevm
|
152
|
+
|
153
|
+
Now, you can run something like the following periodically (say, out of cron
|
154
|
+
each hour):
|
155
|
+
|
156
|
+
lvcreate --snapshot -L10G -n somevm-snapback-new vmsrv1/somevm
|
157
|
+
lvmsync /dev/vmsrv1/somevm-snapback vmsrv2:/dev/vmsrv2/somevm --snapback \
|
158
|
+
/var/snapbacks/somevm.$(date +%Y%m%d-%H%M)
|
159
|
+
lvremove -f vmsrv1/somevm-snapback
|
160
|
+
lvrename vmsrv1/somevm-snapback-new somevm-snapback
|
161
|
+
|
162
|
+
This will produce files in /var/snapbacks named `somevm.<date-time>`. You
|
163
|
+
need to create the `somevm-snapback-new` snapshot before you start
|
164
|
+
`lvmsync`, so that you can guarantee no changes won't get noticed.
|
165
|
+
|
166
|
+
There are some fairly large caveats to this method -- the LV will still be
|
167
|
+
collecting writes while you're transferring the snapshots, so you won't get
|
168
|
+
a consistent snapshot (in the event you have to rollback, it's almost
|
169
|
+
certain you'll need to fsck). You'll almost certainly want to incorporate
|
170
|
+
some sort of I/O freezing into the process, but the exact execution of that
|
171
|
+
is system-specific, and left as an exercise for the reader.
|
172
|
+
|
173
|
+
Restoring data from a snapback setup is straightforward -- just take each
|
174
|
+
snapback **in reverse order** and run it through `lvmsync --apply` on the
|
175
|
+
destination machine (`vmsrv2` in our example). Say at 1145 `vmsrv1`
|
176
|
+
crashed, and it was determined that you needed to rollback to the state of
|
177
|
+
the system at 8am. You could do this:
|
178
|
+
|
179
|
+
lvmsync --apply /var/snapbacks/somevm.20120119-1100 /dev/vmsrv2/somevm
|
180
|
+
lvmsync --apply /var/snapbacks/somevm.20120119-1000 /dev/vmsrv2/somevm
|
181
|
+
lvmsync --apply /var/snapbacks/somevm.20120119-0900 /dev/vmsrv2/somevm
|
182
|
+
|
183
|
+
And you're done -- `/dev/vmsrv2/somevm` is now at the state it was at at
|
184
|
+
8am. A whole pile of fsck will no doubt be required, but hopefully you'll
|
185
|
+
still be able to salvage *something*.
|
186
|
+
|
187
|
+
If you're wondering why I only restored the 0900 snapback, and not the 0800
|
188
|
+
one, it's because the snapback made at 0900 copied the changes that were sent
|
189
|
+
at 0800 (and about to be overwritten at 0900) and wrote them to the 0900
|
190
|
+
snapback file. Confused much? Good.
|
191
|
+
|
192
|
+
|
193
|
+
### Transferring snapshots on the same machine
|
194
|
+
|
195
|
+
If you need to transfer an LV between different VGs on the same machine,
|
196
|
+
then running everything through SSH is just an unnecessary overhead. If you
|
197
|
+
instead just run `lvmsync` without the `<destserver>:` in the destination
|
198
|
+
specification, everything runs locally, like this:
|
199
|
+
|
200
|
+
lvmsync /dev/vg0/srclv-snapshot /dev/vg1/destlv
|
201
|
+
|
202
|
+
All other parts of the process (creating the snapshot, doing the initial
|
203
|
+
data move with `dd`, and so on) are unchanged.
|
204
|
+
|
205
|
+
As an aside, if you're trying to move LVs between PVs in the same VG, then
|
206
|
+
you don't need `lvmsync`, you need `pvmove`.
|
207
|
+
|
208
|
+
|
209
|
+
### Taking a space- and IO-efficient snapshot of an LV
|
210
|
+
|
211
|
+
But wait, there's more! `lvmsync` also has the ability to dump out the
|
212
|
+
snapshot data to disk, rather than immediately applying it to another block
|
213
|
+
device.
|
214
|
+
|
215
|
+
To do this, use the `--stdout` option when you're running `lvmsync`, and
|
216
|
+
instead of writing the changes to another block device, it'll instead dump
|
217
|
+
the "change stream" to stdout (so redirect somewhere useful). This allows
|
218
|
+
you to dump the changes to a file, or do some sort of fancy footwork to
|
219
|
+
transfer the data to another lvmsync process to apply the changes to a block
|
220
|
+
device.
|
221
|
+
|
222
|
+
For example, if you just wanted to take a copy of the contents of a
|
223
|
+
snapshot, you could do something like this:
|
224
|
+
|
225
|
+
lvmsync --stdout /dev/somevg/somelv-snapshot >~/somechanges
|
226
|
+
|
227
|
+
At a later date, if you wanted to apply those writes to a block device,
|
228
|
+
you'd do it like this:
|
229
|
+
|
230
|
+
lvmsync --apply ~/somechanges /dev/somevg/someotherlv
|
231
|
+
|
232
|
+
You can also do things like do an lvmsync *from* the destination -- this is
|
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?).
|
235
|
+
You could do this by running something like the following on the destination
|
236
|
+
machine:
|
237
|
+
|
238
|
+
ssh srcmachine lvmsync --stdout /dev/srcvg/srclv-snap | lvmsync --apply - /dev/destvg/destlv
|
239
|
+
|
240
|
+
|
241
|
+
## Theory of Operation
|
242
|
+
|
243
|
+
This section is for those people who can't sleep well at night without
|
244
|
+
knowing the magic behind the curtain (and to remind myself occasionally how
|
245
|
+
this stuff works). It is completely unnecessary to read this section in
|
246
|
+
order to work lvmsync.
|
247
|
+
|
248
|
+
First, a little bit of background about how snapshot LVs work, before I
|
249
|
+
describe how lvmsync makes use of them.
|
250
|
+
|
251
|
+
An LVM snapshot "device" is actually not a block device in the usual sense.
|
252
|
+
It isn't just a big area of disk space where you write things. Instead, it
|
253
|
+
is a "meta" device, which points to both an "origin" LV, which is the LV
|
254
|
+
from which the snapshot was made, and a "metadata" LV, which is where the
|
255
|
+
magic happens.
|
256
|
+
|
257
|
+
The "metadata" LV is a list of "chunks" of the origin LV which have been
|
258
|
+
modified, along with the original contents of those chunks. In a way, you
|
259
|
+
can think of it as a sort of "binary diff", which says "these are the ways
|
260
|
+
in which this snapshot LV differs from the origin LV". When a write happens
|
261
|
+
to the origin LV, this "diff" is potentially modified to maintain the
|
262
|
+
original "view" from the time the snapshot was taken.
|
263
|
+
|
264
|
+
(Sidenote: this is why you can write to snapshots -- if you write to a
|
265
|
+
snapshot, the "diff" is written to some more, to say "here are some more
|
266
|
+
differences between the origin and the snapshot").
|
267
|
+
|
268
|
+
From here, it shouldn't be hard to work out how LVM uses the combination of
|
269
|
+
the origin and metadata LVs to give you a consistent snapshot view -- when
|
270
|
+
you ask to read a chunk, LVM looks in the metadata LV to see if it has the
|
271
|
+
chunk in there, and if not it can be sure that the chunk hasn't changed, so
|
272
|
+
it just reads it from the origin LV. Miiiiighty clever!
|
273
|
+
|
274
|
+
In lvmsync, we only make use of a tiny fraction of the data stored in the
|
275
|
+
metadata LV for the snapshot. We don't care what the original contents were
|
276
|
+
(they're what we're trying to get *away* from). What we want is the list of
|
277
|
+
which chunks have been modified, because that's what we use to work out
|
278
|
+
which blocks on the original LV we need to copy across. lvmsync never
|
279
|
+
*actually* reads any disk data from the snapshot block device itself -- all
|
280
|
+
it reads is the list of changed blocks, then it reads the changed data from
|
281
|
+
the original LV (which is where the modified blocks are stored).
|
282
|
+
|
283
|
+
By specifying a snapshot to lvmsync, you're telling it "this is the list of
|
284
|
+
changes I want you to copy" -- it already knows which original LV it needs
|
285
|
+
to copy from (the snapshot metadata has that info available).
|
286
|
+
|
287
|
+
|
288
|
+
## See Also
|
289
|
+
|
290
|
+
Whilst I think `lvmsync` is awesome (and I hope you will too), here are some
|
291
|
+
other tools that might be of use to you if `lvmsync` doesn't float your
|
292
|
+
mustard:
|
293
|
+
|
294
|
+
* [`blocksync.py`](http://www.bouncybouncy.net/programs/blocksync.py) --
|
295
|
+
Implements the "hash the chunks and send the ones that don't match"
|
296
|
+
strategy of block device syncing. It needs to read the entire block
|
297
|
+
device at each end to work out what to send, so it's not as efficient,
|
298
|
+
but on the other hand it doesn't require LVM.
|
299
|
+
|
300
|
+
* [`bdsync`](http://bdsync.rolf-fokkens.nl/) -- Another "hash the chunks"
|
301
|
+
implementation, with the same limitations and advantages as
|
302
|
+
`blocksync.py`.
|
303
|
+
|
304
|
+
* [`ddsnap`](http://zumastor.org/man/ddsnap.8.html) -- Part of the
|
305
|
+
"Zumastor" project, appears to provide some sort of network-aware block
|
306
|
+
device snapshotting (I'm not sure, the Zumastor homepage includes the word
|
307
|
+
"Enterprise", so I fell asleep before finishing reading). Seems to
|
308
|
+
require kernel patches, so there's a non-trivial barrier to entry, but
|
309
|
+
probably not such a big deal if you're after network-aware snapshots as
|
310
|
+
part of your core infrastructure.
|
data/bin/lvmsync
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Transfer a set of changes made to the origin of a snapshot LV to another
|
4
|
+
# block device, possibly using SSH to send to a remote system.
|
5
|
+
#
|
6
|
+
# Usage: Start with lvmsync --help, or read the README for all the gory
|
7
|
+
# details.
|
8
|
+
#
|
9
|
+
# Copyright (C) 2011-2014 Matt Palmer <matt@hezmatt.org>
|
10
|
+
#
|
11
|
+
# This program is free software: you can redistribute it and/or modify it
|
12
|
+
# under the terms of the GNU General Public License version 3, as published
|
13
|
+
# by the Free Software Foundation.
|
14
|
+
#
|
15
|
+
# This program is distributed in the hope that it will be useful,
|
16
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
+
# `LICENCE` file for more details.
|
19
|
+
#
|
20
|
+
require 'optparse'
|
21
|
+
require 'lvm'
|
22
|
+
|
23
|
+
PROTOCOL_VERSION = "lvmsync PROTO[2]"
|
24
|
+
|
25
|
+
include LVM::Helpers
|
26
|
+
|
27
|
+
def main()
|
28
|
+
# Parse me some options
|
29
|
+
options = {}
|
30
|
+
OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: lvmsync [options]"
|
32
|
+
opts.separator ""
|
33
|
+
opts.separator " lvmsync [--snapback <file>] <snapshot device> [--stdout | [<desthost>:]<destdevice>]"
|
34
|
+
opts.separator " lvmsync [--snapback <file>] --apply <changes file> <destdevice>"
|
35
|
+
opts.separator ""
|
36
|
+
|
37
|
+
opts.on("--server", "Run in server mode (deprecated; use '--apply -' instead)") do |v|
|
38
|
+
options[:server] = true
|
39
|
+
end
|
40
|
+
opts.on("-v", "--[no-]verbose",
|
41
|
+
"Run verbosely") { |v| options[:verbose] = true }
|
42
|
+
opts.on("-b <file>", "--snapback <file>",
|
43
|
+
"Make a backup snapshot file on the destination") do |v|
|
44
|
+
options[:snapback] = v
|
45
|
+
end
|
46
|
+
opts.on("-a", "--apply <file>",
|
47
|
+
"Apply mode: write the contents of a snapback file to a device") do |v|
|
48
|
+
options[:apply] = v
|
49
|
+
end
|
50
|
+
opts.on("-s", "--stdout", "Write output data to stdout rather than another lvmsync process") do |v|
|
51
|
+
options[:stdout] = true
|
52
|
+
end
|
53
|
+
end.parse!
|
54
|
+
|
55
|
+
if options[:apply]
|
56
|
+
if ARGV[0].nil?
|
57
|
+
$stderr.puts "No destination device specified."
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
options[:device] = ARGV[0]
|
61
|
+
run_apply(options)
|
62
|
+
elsif options[:server]
|
63
|
+
$stderr.puts "--server is deprecated; please use '--apply -' instead"
|
64
|
+
if (ARGV[0].nil?)
|
65
|
+
$stderr.puts "No destination block device specified. WTF?"
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
options[:apply] = '-'
|
69
|
+
options[:device] = ARGV[0]
|
70
|
+
run_apply(options)
|
71
|
+
else
|
72
|
+
if ARGV[0].nil?
|
73
|
+
$stderr.puts "ERROR: No snapshot specified. Exiting."
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
options[:snapdev] = ARGV[0]
|
77
|
+
|
78
|
+
if options[:stdout] and options[:snapback]
|
79
|
+
$stderr.puts "--snapback cannot be used with --stdout"
|
80
|
+
exit 1
|
81
|
+
end
|
82
|
+
|
83
|
+
if (options[:stdout].nil? and ARGV[1].nil?)
|
84
|
+
$stderr.puts "No destination specified."
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
if options[:stdout].nil?
|
88
|
+
dev, host = ARGV[1].split(':', 2).reverse
|
89
|
+
options[:desthost] = host
|
90
|
+
options[:destdev] = dev
|
91
|
+
end
|
92
|
+
|
93
|
+
run_client(options)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def run_apply(opts)
|
98
|
+
snapfile = opts[:snapback] ? File.open(opts[:snapback], 'w') : nil
|
99
|
+
infile = opts[:apply] == '-' ? $stdin : File.open(opts[:apply], 'r')
|
100
|
+
destdev = opts[:device]
|
101
|
+
|
102
|
+
process_dumpdata(infile, destdev, snapfile)
|
103
|
+
ensure
|
104
|
+
snapfile.close unless snapfile.nil?
|
105
|
+
infile.close unless infile.nil? or infile == $stdin
|
106
|
+
end
|
107
|
+
|
108
|
+
def process_dumpdata(instream, destdev, snapback = nil)
|
109
|
+
handshake = instream.readline.chomp
|
110
|
+
unless handshake == PROTOCOL_VERSION
|
111
|
+
$stderr.puts "Handshake failed; protocol mismatch? (saw '#{handshake}' expected '#{PROTOCOL_VERSION}'"
|
112
|
+
exit 1
|
113
|
+
end
|
114
|
+
|
115
|
+
File.open(destdev, 'w+') do |dest|
|
116
|
+
while header = instream.read(12)
|
117
|
+
offset, chunksize = header.unpack("QN")
|
118
|
+
offset = ntohq(offset)
|
119
|
+
|
120
|
+
begin
|
121
|
+
dest.seek offset * chunksize
|
122
|
+
rescue Errno::EINVAL
|
123
|
+
# In certain rare circumstances, we want to transfer a block
|
124
|
+
# device where the destination is smaller than the source (DRBD
|
125
|
+
# volumes is the canonical use case). So, we ignore attempts to
|
126
|
+
# seek past the end of the device. Yes, this may lose data, but
|
127
|
+
# if you didn't notice that your dd shit itself, it's unlikely
|
128
|
+
# you're going to notice now.
|
129
|
+
|
130
|
+
# Skip the chunk of data
|
131
|
+
instream.read(chunksize)
|
132
|
+
# Go to the next chunk
|
133
|
+
next
|
134
|
+
end
|
135
|
+
|
136
|
+
if snapback
|
137
|
+
snapback.write(header)
|
138
|
+
snapback.write dest.read(chunksize)
|
139
|
+
dest.seek offset * chunksize
|
140
|
+
end
|
141
|
+
dest.write instream.read(chunksize)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def run_client(opts)
|
147
|
+
snapshot = opts[:snapdev]
|
148
|
+
desthost = opts[:desthost]
|
149
|
+
destdev = opts[:destdev]
|
150
|
+
outfd = nil
|
151
|
+
|
152
|
+
vg, lv = parse_snapshot_name(snapshot)
|
153
|
+
|
154
|
+
vgconfig = LVM::VGConfig.new(vg)
|
155
|
+
|
156
|
+
if vgconfig.logical_volumes[lv].nil?
|
157
|
+
$stderr.puts "#{snapshot}: Could not find logical volume"
|
158
|
+
exit 1
|
159
|
+
end
|
160
|
+
|
161
|
+
snap = if vgconfig.logical_volumes[lv].snapshot?
|
162
|
+
if vgconfig.logical_volumes[lv].thin?
|
163
|
+
LVM::ThinSnapshot.new(vg, lv)
|
164
|
+
else
|
165
|
+
LVM::Snapshot.new(vg, lv)
|
166
|
+
end
|
167
|
+
else
|
168
|
+
$stderr.puts "#{snapshot}: Not a snapshot device"
|
169
|
+
exit 1
|
170
|
+
end
|
171
|
+
|
172
|
+
$stderr.puts "Origin device: #{vg}/#{snap.origin}" if opts[:verbose]
|
173
|
+
|
174
|
+
# Since, in principle, we're not supposed to be reading from snapshot
|
175
|
+
# devices directly, the kernel makes no attempt to make the device's read
|
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.
|
178
|
+
# PERFORMANCE WIN!
|
179
|
+
File.open("/proc/sys/vm/drop_caches", 'w') { |fd| fd.print "3" }
|
180
|
+
|
181
|
+
snapback = opts[:snapback] ? "--snapback #{opts[:snapback]}" : ''
|
182
|
+
|
183
|
+
if opts[:stdout]
|
184
|
+
outfd = $stdout
|
185
|
+
else
|
186
|
+
server_cmd = if desthost
|
187
|
+
"ssh #{desthost} lvmsync --apply - #{snapback} #{destdev}"
|
188
|
+
else
|
189
|
+
"lvmsync --apply - #{snapback} #{destdev}"
|
190
|
+
end
|
191
|
+
|
192
|
+
outfd = IO.popen(server_cmd, 'w')
|
193
|
+
end
|
194
|
+
|
195
|
+
outfd.puts PROTOCOL_VERSION
|
196
|
+
|
197
|
+
start_time = Time.now
|
198
|
+
xfer_count = 0
|
199
|
+
xfer_size = 0
|
200
|
+
total_size = 0
|
201
|
+
|
202
|
+
originfile = "/dev/mapper/#{vg.gsub('-', '--')}-#{snap.origin.gsub('-', '--')}"
|
203
|
+
File.open(originfile, 'r') do |origindev|
|
204
|
+
snap.differences.each do |r|
|
205
|
+
xfer_count += 1
|
206
|
+
chunk_size = r.last - r.first + 1
|
207
|
+
xfer_size += chunk_size
|
208
|
+
|
209
|
+
$stderr.puts "Sending chunk #{r.to_s}..." if opts[:verbose]
|
210
|
+
$stderr.puts "Seeking to #{r.first} in #{originfile}" if opts[:verbose]
|
211
|
+
|
212
|
+
origindev.seek(r.first, IO::SEEK_SET)
|
213
|
+
|
214
|
+
outfd.print [htonq(r.first), chunk_size].pack("QN")
|
215
|
+
outfd.print origindev.read(chunk_size)
|
216
|
+
|
217
|
+
# Progress bar!
|
218
|
+
if xfer_count % 100 == 50
|
219
|
+
$stderr.printf "\e[2K\rSending chunk %i of %i, %.2fMB/s",
|
220
|
+
xfer_count,
|
221
|
+
snap.differences.length,
|
222
|
+
xfer_size / (Time.now - start_time) / 1048576
|
223
|
+
$stderr.flush
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
origindev.seek(0, IO::SEEK_END)
|
228
|
+
total_size = origindev.tell
|
229
|
+
end
|
230
|
+
|
231
|
+
$stderr.printf "\rTransferred %i bytes in %.2f seconds\n",
|
232
|
+
xfer_size, Time.now - start_time
|
233
|
+
|
234
|
+
$stderr.printf "You transferred your changes %.2fx faster than a full dd!\n",
|
235
|
+
total_size.to_f / xfer_size
|
236
|
+
ensure
|
237
|
+
outfd.close unless outfd.nil? or outfd == $stdout
|
238
|
+
end
|
239
|
+
|
240
|
+
# Take a device name in any number of different formats and return a [VG, LV] pair.
|
241
|
+
# Raises ArgumentError if the name couldn't be parsed.
|
242
|
+
def parse_snapshot_name(origname)
|
243
|
+
case origname
|
244
|
+
when %r{^/dev/mapper/(.*[^-])-([^-].*)$} then
|
245
|
+
[$1, $2]
|
246
|
+
when %r{^/dev/([^/]+)/(.+)$} then
|
247
|
+
[$1, $2]
|
248
|
+
when %r{^([^/]+)/(.*)$} then
|
249
|
+
[$1, $2]
|
250
|
+
else
|
251
|
+
raise ArgumentError,
|
252
|
+
"Could not determine snapshot name and VG from #{origname.inspect}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
main
|