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.
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
+