e2phokz 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/README.md +24 -0
- data/VERSION +1 -0
- data/bin/e2phokz +9 -0
- data/e2phokz.gemspec +14 -0
- data/lib/e2phokz.rb +273 -0
- metadata +99 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
e2phokz
|
2
|
+
=======
|
3
|
+
|
4
|
+
dump ext2/ext3 filesystem with skipping unused blocks (output them as zeroes).
|
5
|
+
|
6
|
+
|
7
|
+
This is what is used to create minified and compressed virtualmaster images.
|
8
|
+
Prior to this approach, we used following two scenarios:
|
9
|
+
|
10
|
+
a) make a new blockdevice or image file of same size and copy relevant data with tar or rsync
|
11
|
+
Drawback was changing disk uuid.
|
12
|
+
|
13
|
+
|
14
|
+
b) mount image, fill the free space with big file full of zeroes, sync,
|
15
|
+
remove file and unmount.
|
16
|
+
|
17
|
+
This way was too slow
|
18
|
+
|
19
|
+
I know there exists probably a more suitable tool called partimage,
|
20
|
+
but we wanted two simple features: output to pipe and progress bar to
|
21
|
+
other channel (stomp in this case).
|
22
|
+
|
23
|
+
|
24
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.3
|
data/bin/e2phokz
ADDED
data/e2phokz.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = 'e2phokz'
|
3
|
+
gem.summary = "dump ext2/ext3 filesystem with skipping unused blocks (output them as zeroes)"
|
4
|
+
gem.description = gem.summary
|
5
|
+
gem.authors = ["Josef Liska"]
|
6
|
+
gem.email = 'josef.liska@virtualmaster.com'
|
7
|
+
gem.homepage = "https://github.com/phokz/e2phokz"
|
8
|
+
|
9
|
+
gem.add_development_dependency "rspec"
|
10
|
+
gem.files = `git ls-files`.split("\n")
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.version = File.open('VERSION').read.strip
|
13
|
+
gem.add_dependency "stomp", "~> 1.1.6"
|
14
|
+
end
|
data/lib/e2phokz.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# e2phokz: script which copies and optionally compresses ext2/ext3 filesystem
|
2
|
+
# it only copies used block, instead of free blocks it writes zeros
|
3
|
+
#
|
4
|
+
# usage:
|
5
|
+
# e2phokz block_device_or_image_file output_file [stomp_channel_name]
|
6
|
+
#
|
7
|
+
# if output_file is suffixed with .gz, output will be compressed
|
8
|
+
# if output_file is -, output will be sent to stdout
|
9
|
+
# if optional stomp channel name is supplied, progress info will be sent
|
10
|
+
# there. Script reads /etc/e2phokz.yml to get server info and credential.
|
11
|
+
|
12
|
+
module E2Phokz
|
13
|
+
CONFIG_FILENAME="/etc/e2phokz.yml"
|
14
|
+
|
15
|
+
class App
|
16
|
+
|
17
|
+
BUFFER_SIZE=16 #megabytes
|
18
|
+
|
19
|
+
def initialize(options)
|
20
|
+
@options = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def buffercopy(infile,outfile,cls,s,e)
|
24
|
+
|
25
|
+
bs = @fsinfo['block_size'].to_i
|
26
|
+
num = (e-s+1)*bs
|
27
|
+
warn "#{cls} from block #{s} to #{e} number of bytes #{num}" unless @debug.nil?
|
28
|
+
|
29
|
+
maxbuf = BUFFER_SIZE*1024*1024 #16M
|
30
|
+
rest = num
|
31
|
+
count = 0
|
32
|
+
|
33
|
+
infile.seek(s*bs,IO::SEEK_SET)
|
34
|
+
|
35
|
+
while rest > 0
|
36
|
+
count = rest > maxbuf ? maxbuf : rest
|
37
|
+
rest = rest - count
|
38
|
+
|
39
|
+
a = Time.now.to_f
|
40
|
+
outfile.write(infile.read(count))
|
41
|
+
b = Time.now.to_f
|
42
|
+
warn "#{count} bytes read in #{b-a} sec, #{count/(b-a)} bytes/s" unless @debug.nil?
|
43
|
+
eta_continuous count
|
44
|
+
GC.start
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def progress_bar progress
|
49
|
+
t = progress.last
|
50
|
+
i = progress.first.split('%').first.to_f
|
51
|
+
|
52
|
+
h=(t/3600).to_i
|
53
|
+
m=((t%3600)/60).to_i
|
54
|
+
s=((t%60)*10).to_i/10.0
|
55
|
+
bars=(40*i/100).ceil
|
56
|
+
bbars=(1..bars).collect {'#'}.join
|
57
|
+
spaces=40-bars
|
58
|
+
bspaces=(1..spaces).collect {' '}.join
|
59
|
+
sep = @debug.nil? ? "\r" : "\n"
|
60
|
+
$stderr.write "ETA #{sprintf('%02d:%02d:%02.1f',h,m,s)} #{sprintf('%3d',i)}% [#{bbars}#{bspaces}] #{sep}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def eta_continuous count
|
64
|
+
elapsed=Time.now.to_f-@time_start
|
65
|
+
@written = @written + count
|
66
|
+
to_be_written = @fsinfo['block_count'].to_i * @fsinfo['block_size'].to_i
|
67
|
+
progress = (1000.0*@written/to_be_written)/10
|
68
|
+
# weighted average
|
69
|
+
eta = elapsed/progress*100 - elapsed
|
70
|
+
new_eta = (progress*eta + (100-progress)*@initial_eta)/100
|
71
|
+
#TODO support for non-gzipped ETA calculation
|
72
|
+
progress_stomp [progress.to_s+'%',new_eta]
|
73
|
+
progress_bar [progress.to_s+'%',new_eta]
|
74
|
+
end
|
75
|
+
|
76
|
+
def eta_initial
|
77
|
+
#TODO support for non-gzipped ETA calculation
|
78
|
+
|
79
|
+
#constants
|
80
|
+
gzip_free=0.02
|
81
|
+
gzip_data=0.09
|
82
|
+
|
83
|
+
bs=@fsinfo['block_size'].to_i
|
84
|
+
@megs_used=(@fsinfo['block_count'].to_i-@fsinfo['free_blocks'].to_i)*bs/1024/1024
|
85
|
+
@megs_free=(@fsinfo['free_blocks'].to_i*bs/1024/1024)
|
86
|
+
eta = @megs_used*gzip_data+@megs_free*gzip_free
|
87
|
+
@written = 0
|
88
|
+
@initial_eta = eta
|
89
|
+
progress_stomp ['0%',eta]
|
90
|
+
progress_bar ['0%',eta]
|
91
|
+
end
|
92
|
+
|
93
|
+
def progress_stomp progress
|
94
|
+
unless @stomp_client.nil?
|
95
|
+
@stomp_client.send(@channel_name,progress.join(';'))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def connect_stomp
|
100
|
+
begin
|
101
|
+
config=YAML::load(File.open(CONFIG_FILENAME))['stomp']
|
102
|
+
rescue
|
103
|
+
warn "Error: cannot load or parse stomp config #{CONFIG_FILENAME}" && exit
|
104
|
+
end
|
105
|
+
|
106
|
+
begin
|
107
|
+
@stomp_client = Stomp::Client.new(config['user'], config['password'], config['server'], config['port'])
|
108
|
+
rescue
|
109
|
+
warn "Error: cannot connect to stomp server #{config['server']}" && exit
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_args
|
114
|
+
@time_start=Time.now.to_f
|
115
|
+
|
116
|
+
if @options.length < 2 or @options.length > 3
|
117
|
+
puts File.open(__FILE__).read.split("\n").collect{|l| l.gsub('# ','') unless l.match(/^# /).nil?}.compact.join("\n")
|
118
|
+
exit
|
119
|
+
end
|
120
|
+
if @options.length == 3
|
121
|
+
@channel_name = @options.last
|
122
|
+
connect_stomp
|
123
|
+
end
|
124
|
+
|
125
|
+
#test for presence of in file || device
|
126
|
+
begin
|
127
|
+
warn "Warning: input file #{@options.first} is not a block device" unless File::Stat.new(@options.first).blockdev?
|
128
|
+
@file=File.open(@options.first,'rb')
|
129
|
+
rescue
|
130
|
+
warn "Error: infile #{@options.first} cannot be open." && exit
|
131
|
+
end
|
132
|
+
|
133
|
+
#test for presence of out file
|
134
|
+
begin
|
135
|
+
if File::Stat.new(@options[1]).blockdev?
|
136
|
+
warn "Error: I will not overwrite block device #{@options[1]}"
|
137
|
+
else
|
138
|
+
warn "Error: I will not overwrite output file #{@options[1]}"
|
139
|
+
end
|
140
|
+
exit
|
141
|
+
rescue
|
142
|
+
warn "OK"
|
143
|
+
end
|
144
|
+
|
145
|
+
if @options[1]=='-'
|
146
|
+
# writing to stdout, no compression
|
147
|
+
@gzip=IO.new(1,"w")
|
148
|
+
else
|
149
|
+
begin
|
150
|
+
if @options[1].split('.').last == 'gz'
|
151
|
+
#compression required
|
152
|
+
@gzip=Zlib::GzipWriter.open(@options[1])
|
153
|
+
else
|
154
|
+
@gzip=File.open(@options[1],'wb')
|
155
|
+
end
|
156
|
+
rescue
|
157
|
+
warn "Error: cannot open output file #{@options[1]} for writing."
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
@devzero=File.open("/dev/zero",'rb')
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_dumpe2fs
|
166
|
+
begin
|
167
|
+
dump = File.popen("dumpe2fs #{@options.first}",'r').read.split("\n")
|
168
|
+
rescue
|
169
|
+
warn "Error: cannot execute dump2efs" && exit
|
170
|
+
end
|
171
|
+
|
172
|
+
header = 1
|
173
|
+
@fsinfo={}
|
174
|
+
group_number=0
|
175
|
+
from=0
|
176
|
+
to=0
|
177
|
+
dump.each do |row|
|
178
|
+
if row=='' # header ends with empty row
|
179
|
+
header = 0
|
180
|
+
#we have processed heade, so we can count ETA now
|
181
|
+
eta_initial
|
182
|
+
next
|
183
|
+
end
|
184
|
+
|
185
|
+
if header == 1
|
186
|
+
#we are processing header to @fsinfo hash
|
187
|
+
fields = row.split(':')
|
188
|
+
varname = fields.shift.split(' ').join('_').downcase
|
189
|
+
value = fields.join(':').strip
|
190
|
+
@fsinfo.merge!({varname => value})
|
191
|
+
else
|
192
|
+
#we are processing group info
|
193
|
+
fields = row.split(' ')
|
194
|
+
if fields.first=='Group'
|
195
|
+
group_number = fields[1].to_i
|
196
|
+
from = fields[3].split('-').first.to_i
|
197
|
+
to = fields[3].split('-').last.to_i
|
198
|
+
warn "Processing group #{group_number} #{from} #{to}" unless @debug.nil?
|
199
|
+
else
|
200
|
+
# we are only interested in free blocks info as of now
|
201
|
+
fields = row.split(':')
|
202
|
+
name = fields.first.strip
|
203
|
+
if name == 'Free blocks' then
|
204
|
+
ranges = fields.last.strip.split(', ')
|
205
|
+
if ranges.empty?
|
206
|
+
#copy whole group
|
207
|
+
warn "Full copy of group #{group_number}" unless @debug.nil?
|
208
|
+
buffercopy(@file,@gzip,'copy', from,to)
|
209
|
+
else
|
210
|
+
#copy only non-free blocks
|
211
|
+
warn "Partial copy of group #{group_number}" unless @debug.nil?
|
212
|
+
pointer = from
|
213
|
+
ranges.each do |range|
|
214
|
+
range_start=range.split('-').first.to_i
|
215
|
+
range_end=range.split('-').last.to_i
|
216
|
+
buffercopy(@file,@gzip,'copy',pointer,range_start-1)
|
217
|
+
buffercopy(@devzero,@gzip,'zero',range_start,range_end)
|
218
|
+
pointer=range_end+1
|
219
|
+
end
|
220
|
+
if to > pointer
|
221
|
+
warn "Remaining blocks from last free to end of group #{group_number}" unless @debug.nil?
|
222
|
+
buffercopy(@file,@gzip,'copy',pointer,to)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
def close_files
|
234
|
+
@file.close
|
235
|
+
@gzip.close
|
236
|
+
@devzero.close
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
def run!
|
241
|
+
parse_args
|
242
|
+
parse_dumpe2fs
|
243
|
+
close_files
|
244
|
+
warn "\n"
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
def E2Phokz.config_file
|
250
|
+
return if File.exists?(CONFIG_FILENAME)
|
251
|
+
begin
|
252
|
+
File.open(CONFIG_FILENAME,'w') do |f|
|
253
|
+
f.puts("# sample config - you can just ignore this if you do not inted to use stomp")
|
254
|
+
f.puts(E2Phokz.sample_config.to_yaml)
|
255
|
+
end
|
256
|
+
system("editor #{CONFIG_FILENAME}")
|
257
|
+
rescue
|
258
|
+
puts "I was not able to place sample config to #{CONFIG_FILENAME}."
|
259
|
+
puts "Please do it yourself. Config is only needed for stomp."
|
260
|
+
puts "If you do not intend to use stomp, you can just ignore this."
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def E2Phokz.sample_config
|
265
|
+
{"stomp"=>{"server"=>"stompserver.domain.tld", "user"=>"username", "port"=>61613, "password"=>"secure_enough_password"}}
|
266
|
+
end
|
267
|
+
|
268
|
+
def E2Phokz.main(options)
|
269
|
+
E2Phokz.config_file
|
270
|
+
E2Phokz::App.new(options).run!
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: e2phokz
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 3
|
10
|
+
version: 0.1.3
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Josef Liska
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-02-11 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: stomp
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 31
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 1
|
46
|
+
- 6
|
47
|
+
version: 1.1.6
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: dump ext2/ext3 filesystem with skipping unused blocks (output them as zeroes)
|
51
|
+
email: josef.liska@virtualmaster.com
|
52
|
+
executables:
|
53
|
+
- e2phokz
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- README.md
|
61
|
+
- VERSION
|
62
|
+
- bin/e2phokz
|
63
|
+
- e2phokz.gemspec
|
64
|
+
- lib/e2phokz.rb
|
65
|
+
homepage: https://github.com/phokz/e2phokz
|
66
|
+
licenses: []
|
67
|
+
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 3
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
hash: 3
|
88
|
+
segments:
|
89
|
+
- 0
|
90
|
+
version: "0"
|
91
|
+
requirements: []
|
92
|
+
|
93
|
+
rubyforge_project:
|
94
|
+
rubygems_version: 1.8.15
|
95
|
+
signing_key:
|
96
|
+
specification_version: 3
|
97
|
+
summary: dump ext2/ext3 filesystem with skipping unused blocks (output them as zeroes)
|
98
|
+
test_files: []
|
99
|
+
|