e2phokz 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/.gitignore +18 -0
  2. data/README.md +24 -0
  3. data/VERSION +1 -0
  4. data/bin/e2phokz +9 -0
  5. data/e2phokz.gemspec +14 -0
  6. data/lib/e2phokz.rb +273 -0
  7. metadata +99 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'e2phokz'
4
+
5
+ require 'zlib'
6
+ require 'stomp'
7
+ require 'yaml'
8
+
9
+ E2Phokz.main(ARGV)
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
+