omgf 0.0.0.GIT

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,201 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ $stdout.sync = $stderr.sync = true
4
+ Thread.abort_on_exception = true
5
+ if ENV["COVERAGE"]
6
+ require "coverage"
7
+ Coverage.start
8
+ at_exit do
9
+ # Dirty little text formatter. I tried simplecov but the default
10
+ # HTML+JS is unusable without a GUI (I hate GUIs :P) and it would've
11
+ # taken me longer to search the Internets to find a plain-text
12
+ # formatter I like...
13
+ res = Coverage.result
14
+ relevant = res.keys.grep(%r{/lib/omgf/\w+\.rb})
15
+ relevant.each do |file|
16
+ cov = res[file]
17
+ puts "==> #{file} <=="
18
+ File.readlines(file).each_with_index do |line, i|
19
+ n = cov[i]
20
+ if n == 0 # BAD
21
+ print(" *** 0 #{line}")
22
+ elsif n
23
+ printf("% 7u %s", n, line)
24
+ elsif line =~ /\S/ # probably a line with just "end" in it
25
+ print(" #{line}")
26
+ else # blank line
27
+ print "\n" # don't output trailing whitespace on blank lines
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ require 'test/unit'
34
+ require 'net/http'
35
+ require 'uri'
36
+ require 'tempfile'
37
+ require 'mogilefs'
38
+ require 'stringio'
39
+ require 'logger'
40
+
41
+ module TestMogileFSIntegration
42
+ def x(*cmd)
43
+ out = Tempfile.new("out")
44
+ err = Tempfile.new("err")
45
+ puts cmd.join(' ') if $VERBOSE
46
+ pid = fork do
47
+ $stderr.reopen(err.path, "a")
48
+ $stdout.reopen(out.path, "a")
49
+ out.close
50
+ err.close
51
+ ObjectSpace.each_object(Tempfile) do |tmp|
52
+ next if tmp.closed?
53
+ ObjectSpace.undefine_finalizer(tmp)
54
+ tmp.close_on_exec = true if tmp.respond_to?(:close_on_exec=)
55
+ end
56
+ exec(*cmd)
57
+ end
58
+ _, status = Process.waitpid2(pid)
59
+ out.rewind
60
+ err.rewind
61
+ [ status, out, err ]
62
+ end
63
+
64
+ def x!(*cmd)
65
+ status, out, err = x(*cmd)
66
+ assert status.success?, "#{status.inspect} / #{out.read} / #{err.read}"
67
+ [ status, out, err ]
68
+ end
69
+
70
+ def setup_mogilefs(plugins = nil)
71
+ @test_host = "127.0.0.1"
72
+ setup_mogstored
73
+ @tracker = TCPServer.new(@test_host, 0)
74
+ @tracker_port = @tracker.addr[1]
75
+
76
+ @dbname = Tempfile.new(["mogfresh", ".sqlite3"])
77
+ @mogilefsd_conf = Tempfile.new(["mogilefsd", "conf"])
78
+ @mogilefsd_pid = Tempfile.new(["mogilefsd", "pid"])
79
+
80
+ cmd = %w(mogdbsetup --yes --type=SQLite --dbname) << @dbname.path
81
+ x!(*cmd)
82
+
83
+ @mogilefsd_conf.puts "db_dsn DBI:SQLite:#{@dbname.path}"
84
+ @mogilefsd_conf.write <<EOF
85
+ conf_port #@tracker_port
86
+ listen #@test_host
87
+ pidfile #{@mogilefsd_pid.path}
88
+ replicate_jobs 1
89
+ fsck_jobs 1
90
+ query_jobs 1
91
+ mogstored_stream_port #@mogstored_mgmt_port
92
+ node_timeout 10
93
+ EOF
94
+ @mogilefsd_conf.flush
95
+
96
+ @trackers = @hosts = [ "#@test_host:#@tracker_port" ]
97
+ @tracker.close
98
+ x!("mogilefsd", "--daemon", "--config=#{@mogilefsd_conf.path}")
99
+ wait_for_port @tracker_port
100
+ @admin = MogileFS::Admin.new(:hosts => @hosts)
101
+ 500.times do
102
+ break if File.size(@mogstored_pid.path) > 0
103
+ sleep 0.05
104
+ end
105
+
106
+ args = { :ip => @test_host, :port => @mogstored_http_port }
107
+ args[:status] = "alive"
108
+ @admin.create_host("me", args)
109
+ yield_for_monitor_update { @admin.get_hosts.empty? or break }
110
+
111
+ mogadm!("device", "add", "me", "dev1")
112
+ yield_for_monitor_update { @admin.get_devices.empty? or break }
113
+ wait_for_usage_file "dev1"
114
+ mogadm!("device", "add", "me", "dev2")
115
+ wait_for_usage_file "dev2"
116
+ yield_for_monitor_update { @admin.get_devices.size == 2 and break }
117
+ end
118
+
119
+ def mogadm(*args)
120
+ x("mogadm", "--trackers=#{@trackers.join(',')}", *args)
121
+ end
122
+
123
+ def mogadm!(*args)
124
+ status, out, err = mogadm(*args)
125
+ assert status.success?, "#{status.inspect} / #{out.read} / #{err.read}"
126
+ [ status, out, err ]
127
+ end
128
+
129
+ def yield_for_monitor_update
130
+ 50.times do
131
+ yield
132
+ sleep 0.1
133
+ end
134
+ end
135
+
136
+
137
+ def wait_for_port(port)
138
+ tries = 500
139
+ begin
140
+ TCPSocket.new(@test_host, port).close
141
+ return
142
+ rescue
143
+ sleep 0.05
144
+ end while (tries -= 1) > 0
145
+ raise "#@test_host:#{port} never became ready"
146
+ end
147
+
148
+ def teardown_mogilefs
149
+ if @mogstored_pid
150
+ pid = File.read(@mogstored_pid.path).to_i
151
+ Process.kill(:TERM, pid) if pid > 0
152
+ end
153
+ if @mogilefsd_pid
154
+ s = TCPSocket.new(@test_host, @tracker_port)
155
+ s.write "!shutdown\r\n"
156
+ s.close
157
+ end
158
+ FileUtils.rmtree(@docroot)
159
+ end
160
+
161
+ def wait_for_usage_file(device)
162
+ uri = URI("http://#@test_host:#@mogstored_http_port/#{device}/usage")
163
+ res = nil
164
+ 100.times do
165
+ res = Net::HTTP.get_response(uri)
166
+ if Net::HTTPOK === res
167
+ puts res.body if $DEBUG
168
+ return
169
+ end
170
+ puts res.inspect if $DEBUG
171
+ sleep 0.1
172
+ end
173
+ raise "#{uri} failed to appear: #{res.inspect}"
174
+ end
175
+
176
+ def setup_mogstored
177
+ @docroot = Dir.mktmpdir(["mogfresh", "docroot"])
178
+ Dir.mkdir("#@docroot/dev1")
179
+ Dir.mkdir("#@docroot/dev2")
180
+ @mogstored_mgmt = TCPServer.new(@test_host, 0)
181
+ @mogstored_http = TCPServer.new(@test_host, 0)
182
+ @mogstored_mgmt_port = @mogstored_mgmt.addr[1]
183
+ @mogstored_http_port = @mogstored_http.addr[1]
184
+ @mogstored_conf = Tempfile.new(["mogstored", "conf"])
185
+ @mogstored_pid = Tempfile.new(["mogstored", "pid"])
186
+ @mogstored_conf.write <<EOF
187
+ pidfile = #{@mogstored_pid.path}
188
+ maxconns = 500
189
+ httplisten = #@test_host:#@mogstored_http_port
190
+ mgmtlisten = #@test_host:#@mogstored_mgmt_port
191
+ docroot = #@docroot
192
+ EOF
193
+ @mogstored_conf.flush
194
+ @mogstored_mgmt.close
195
+ @mogstored_http.close
196
+
197
+ x!("mogstored", "--daemon", "--config=#{@mogstored_conf.path}")
198
+ wait_for_port @mogstored_mgmt_port
199
+ wait_for_port @mogstored_http_port
200
+ end
201
+ end
@@ -0,0 +1,78 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ require './test/integration'
4
+ require 'omgf/hysterical_raisins'
5
+ begin
6
+ require 'unicorn'
7
+ rescue LoadError
8
+ end
9
+
10
+ class TestHystScript < Test::Unit::TestCase
11
+ include TestMogileFSIntegration
12
+ def setup
13
+ setup_mogilefs
14
+ @admin.create_domain("testdom")
15
+ srv = TCPServer.new(@test_host, 0)
16
+ @api_port = srv.addr[1]
17
+ ENV["UNICORN_FD"] = srv.fileno.to_s
18
+ api_addr = "#@test_host:#@api_port"
19
+ @app = OMGF::HystericalRaisins.new(:hosts => @hosts)
20
+ @out = Tempfile.new("out")
21
+ @err = Tempfile.new("err")
22
+ @unicorn_pid = fork do
23
+ $stdin.reopen("/dev/null")
24
+ $stdout.reopen(@out.path, "ab")
25
+ $stderr.reopen(@err.path, "ab")
26
+ # hopefully this Unicorn API doesn't change...
27
+ Unicorn::HttpServer.new(@app, :listeners => [api_addr]).start.join
28
+ end
29
+ srv.close
30
+ ENV["HYST_HOST"] = api_addr
31
+ ENV["MOG_DOMAIN"] = 'testdom'
32
+ @hyst = File.dirname(File.dirname(__FILE__)) + "/examples/hyst.bash"
33
+ assert File.executable?(@hyst)
34
+ end
35
+
36
+ def test_hyst
37
+ out = `#@hyst ls`
38
+ assert_equal $?, 0
39
+ assert_equal '', out
40
+
41
+ # tee is a little tricky, unicorn is one of the few Rack servers that
42
+ # handle chunked PUTs
43
+ out = `echo HI | #@hyst tee foo`
44
+ assert_equal 0, $?
45
+ assert_equal "HI\n", out
46
+ assert_equal "HI\n", `#@hyst cat foo`
47
+
48
+ # /dev/null optimization
49
+ out = `echo NULL | #@hyst tee bar >/dev/null`
50
+ assert_equal 0, $?
51
+ assert_equal "", out
52
+ assert_equal "NULL\n", `#@hyst cat bar`
53
+
54
+ # listings
55
+ assert_equal "foo\n", `#@hyst ls fo`
56
+ assert_equal "bar\n", `#@hyst ls b`
57
+ assert_equal "", `#@hyst ls z`
58
+
59
+ # remove a file once
60
+ assert(system("#@hyst rm bar"))
61
+
62
+ # really removed?
63
+ err = Tempfile.new('curl_err')
64
+ a = `#@hyst cat bar 2>#{err.path}`
65
+ assert_equal 22, $?.exitstatus
66
+ assert_equal '', a
67
+ assert_match(/\s+404\b/, err.read)
68
+ err.close!
69
+ end
70
+
71
+ def teardown
72
+ ENV.delete("HYST_HOST")
73
+ Process.kill :QUIT, @unicorn_pid
74
+ _, status = Process.waitpid2(@unicorn_pid)
75
+ assert status.success?, status.inspect
76
+ teardown_mogilefs
77
+ end
78
+ end if defined?(Unicorn)
@@ -0,0 +1,238 @@
1
+ # Copyright (C) 2008-2012, Eric Wong <normalperson@yhbt.net>
2
+ # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3
+ require './test/integration'
4
+ require 'rack/mock'
5
+ require 'open-uri'
6
+ require 'omgf/hysterical_raisins'
7
+
8
+ class TestHystericalRaisins < Test::Unit::TestCase
9
+ include TestMogileFSIntegration
10
+ def setup
11
+ setup_mogilefs
12
+ @app = OMGF::HystericalRaisins.new(:hosts => @hosts)
13
+ @req = Rack::MockRequest.new(@app)
14
+ @err = StringIO.new
15
+ @opts = { "rack.logger" => Logger.new(@err) }
16
+ @admin.create_domain("testdom")
17
+ end
18
+
19
+ def test_all
20
+ resp = @req.get("/")
21
+ assert_equal 200, resp.status, "/ response code is 200"
22
+ assert_equal "", resp.body, "/ returns empty body"
23
+
24
+ # domain listing
25
+ resp = @req.get("/testdom", @opts)
26
+ assert_equal 200, resp.status, "domain listing response code is 200"
27
+ assert_equal "", resp.body, "domain listing is empty"
28
+ assert_equal("text/plain", resp.headers["Content-Type"],
29
+ "content-type is text/plain")
30
+
31
+ json = { "HTTP_ACCEPT" => "application/json" }
32
+
33
+ # json domain listing
34
+ resp = @req.get("/testdom", @opts.merge(json))
35
+ assert_equal 200, resp.status, "domain listing response code is 200"
36
+ assert_equal "[]", resp.body, "domain listing is empty array"
37
+ assert_equal("application/json", resp.headers["Content-Type"],
38
+ "content-type is application/json")
39
+
40
+ # missing domain
41
+ resp = @req.get("/non-existent", @opts)
42
+ assert_equal 0, @err.string.size
43
+ assert_equal 404, resp.status, "non-existent domain listing gives 404"
44
+
45
+ # missing domain (json)
46
+ resp = @req.get("/non-existent", @opts.merge(json))
47
+ assert_equal 0, @err.string.size
48
+ assert_equal 404, resp.status, "non-existent domain listing gives 404"
49
+
50
+ # PUT to domain listing fails
51
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
52
+ resp = @req.put("/non-existent", opts)
53
+ assert_equal 0, @err.string.size
54
+ assert_equal 404, resp.status, "non-existent domain listing PUT gives 404"
55
+
56
+ # DELETEs to bad domains fail
57
+ resp = @req.delete("/non-existent", opts)
58
+ assert_equal 0, @err.string.size
59
+ assert_equal 404, resp.status, "non-existent domain DELETE gives 404"
60
+ resp = @req.delete("/non-existent/key", opts)
61
+ assert_equal 0, @err.string.size
62
+ assert_equal 406, resp.status, "non-existent domain DELETE gives 406"
63
+
64
+ # PUT to bad domain fails
65
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
66
+ resp = @req.put("/non-existent/key", opts)
67
+ assert_equal 0, @err.string.size
68
+ assert_equal 406, resp.status, "bad domain PUT w/key: #{resp.inspect}"
69
+ assert_equal "Invalid domain: non-existent\n", resp.body
70
+
71
+ # PUT with bad key fails
72
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
73
+ resp = @req.put("/testdom/key%20space", opts)
74
+ assert_equal 0, @err.string.size, @err.string
75
+ assert_equal 406, resp.status, "bad key"
76
+
77
+ # PUT with long key fails
78
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
79
+ resp = @req.put("/testdom/key#{'a' * 128}", opts)
80
+ assert_equal 0, @err.string.size, @err.string
81
+ assert_equal 406, resp.status, "bad key"
82
+
83
+ # PUT to good domain succeeds
84
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
85
+ resp = @req.put("/testdom/key", opts)
86
+ assert_equal 0, @err.string.size, @err.string
87
+ assert_equal 200, resp.status
88
+
89
+ # GET succeeds with redirect
90
+ resp = @req.get("/testdom/key", @opts)
91
+ assert_equal 0, @err.string.size, @err.string
92
+ assert_equal 302, resp.status
93
+ assert_equal "HELLO", open(resp["Location"]).read
94
+
95
+ # PUT to good domain and REMOTE_USER succeeds succeeds with 201
96
+ opts = @opts.merge(:input => StringIO.new("HELLO"))
97
+ opts["REMOTE_USER"] = "root"
98
+ resp = @req.put("/testdom/user_key", opts)
99
+ assert_equal 0, @err.string.size, @err.string
100
+ assert_equal 201, resp.status
101
+
102
+ # plain-text key listing succeeds
103
+ resp = @req.get("/testdom", @opts)
104
+ assert_equal 0, @err.string.size, @err.string
105
+ assert_equal 200, resp.status, resp.inspect
106
+ assert_equal 2, resp.body.split(/\n/).size
107
+ assert_equal("text/plain", resp.headers["Content-Type"],
108
+ "content-type is text/plain")
109
+ keys = resp.body.split(/\n/)
110
+ assert_match(/\Akey\|5|[12]\z/ ,keys[0])
111
+ assert_match(/\Auser_key\|5|[12]\z/ ,keys[1])
112
+
113
+ # json key listing succeeds
114
+ resp = @req.get("/testdom", @opts.merge(json))
115
+ assert_equal 0, @err.string.size, @err.string
116
+ assert_equal 200, resp.status, resp.inspect
117
+ keys = JSON.parse(resp.body)
118
+ assert_equal 2, keys.size
119
+ assert_equal("application/json", resp.headers["Content-Type"],
120
+ "content-type is application/json")
121
+ assert_equal "key", keys[0][0]
122
+ assert_equal 5, keys[0][1]
123
+ assert_operator keys[0][2], :>=, 1
124
+ assert_operator keys[0][2], :<=, 2
125
+ assert_equal "user_key", keys[1][0]
126
+ assert_equal 5, keys[1][1]
127
+ assert_operator keys[1][2], :>=, 1
128
+ assert_operator keys[1][2], :<=, 2
129
+
130
+ # HEAD of json listing succeeds
131
+ resp = @req.head("/testdom", @opts.merge(json))
132
+ assert_equal("application/json", resp.headers["Content-Type"],
133
+ "content-type is application/json")
134
+ assert_equal 200, resp.status
135
+ assert_equal "", resp.body
136
+
137
+ # HEAD of text listing succeeds
138
+ resp = @req.head("/testdom", @opts)
139
+ assert_equal("text/plain", resp.headers["Content-Type"],
140
+ "content-type is text/plain")
141
+ assert_equal 200, resp.status
142
+ assert_equal "", resp.body
143
+
144
+ # DELETE succeeds
145
+ resp = @req.delete("/testdom/user_key", @opts)
146
+ assert_equal 0, @err.string.size, @err.string
147
+ assert_equal 204, resp.status, resp.inspect
148
+
149
+ # DELETE again fails
150
+ resp = @req.delete("/testdom/user_key", @opts)
151
+ assert_equal 0, @err.string.size, @err.string
152
+ assert_equal 404, resp.status, resp.inspect
153
+
154
+ # unauthed PUT fails to overwrite
155
+ opts = @opts.merge(:input => StringIO.new("FAIL"))
156
+ resp = @req.put("/testdom/key", opts)
157
+ assert_equal 0, @err.string.size, @err.string
158
+ assert_equal 403, resp.status, resp.inspect
159
+
160
+ # GET returns unchanged file
161
+ resp = @req.get("/testdom/key", @opts)
162
+ assert_equal 0, @err.string.size, @err.string
163
+ assert_equal 302, resp.status
164
+ assert_equal "HELLO", open(resp["Location"]).read
165
+
166
+ # PUT succeeds with overwrite
167
+ opts = @opts.merge(:input => StringIO.new("BLAH"))
168
+ opts["REMOTE_USER"] = "root"
169
+ opts["HTTP_X_OMGF_FORCE"] = "true"
170
+ resp = @req.put("/testdom/key", opts)
171
+ assert_equal 0, @err.string.size, @err.string
172
+ assert_equal 204, resp.status, resp.inspect
173
+
174
+ # GET succeeds with redirect
175
+ resp = @req.get("/testdom/key", @opts)
176
+ assert_equal 0, @err.string.size, @err.string
177
+ assert_equal 302, resp.status
178
+ assert_equal "BLAH", open(resp["Location"]).read
179
+
180
+ # HEAD shows size and metadata
181
+ resp = @req.head("/testdom/key", @opts)
182
+ assert_equal 0, @err.string.size, @err.string
183
+ assert_equal 200, resp.status, resp.inspect
184
+ assert_equal "4", resp["Content-Length"]
185
+ assert_equal "BLAH", open(resp["X-URL-0"]).read
186
+
187
+ # wait for replication
188
+ if ENV["EXPENSIVE"]
189
+ 200.times do
190
+ resp["X-URL-1"] and break
191
+ sleep 0.1
192
+ resp = @req.head("/testdom/key", @opts)
193
+ end
194
+ assert_kind_of String, resp["X-URL-1"]
195
+ assert_equal "BLAH", open(resp["X-URL-1"]).read
196
+ end
197
+
198
+ reproxy_test
199
+ end
200
+
201
+ def reproxy_test
202
+ @app.instance_variable_set(:@reproxy_path, "/reproxy")
203
+ opts = @opts.merge("HTTP_X_OMGF_REPROXY" => "1")
204
+ resp = @req.get("/testdom/key", opts)
205
+ assert_equal 0, @err.string.size, @err.string
206
+ t = Time.parse(resp["X-Redirect-Last-Modified"])
207
+ assert_equal "\"#{t.to_i}\"", resp["ETag"]
208
+ assert_equal "application/octet-stream", resp["X-Redirect-Content-Type"]
209
+ assert_equal "BLAH", open(resp["Location"]).read
210
+ assert_equal "/reproxy", resp["X-Accel-Redirect"]
211
+ assert_nil resp.original_headers["Content-Type"], resp.inspect
212
+ assert_nil resp["Last-Modified"]
213
+
214
+ # no point in redirecting HEAD requests
215
+ resp = @req.head("/testdom/key", opts)
216
+ assert_equal 0, @err.string.size, @err.string
217
+ t = Time.parse(resp["Last-Modified"])
218
+ assert_equal "\"#{t.to_i}\"", resp["ETag"]
219
+ assert_nil resp["X-Accel-Redirect"]
220
+ assert_equal "4", resp["Content-Length"]
221
+ assert_equal "application/octet-stream",
222
+ resp.original_headers["Content-Type"], resp.inspect
223
+
224
+ # explicit filename
225
+ resp = @req.head("/testdom/key?inline=foo.txt", opts)
226
+ assert_equal "inline; filename=foo.txt", resp["Content-Disposition"]
227
+ resp = @req.head("/testdom/key?attachment=foo.txt", opts)
228
+ assert_equal "attachment; filename=foo.txt", resp["Content-Disposition"]
229
+ resp = @req.head("/testdom/key?filename=foo.txt", opts)
230
+ assert_equal "attachment; filename=foo.txt", resp["Content-Disposition"]
231
+
232
+ @app.instance_variable_set(:@reproxy_path, nil)
233
+ end
234
+
235
+ def teardown
236
+ teardown_mogilefs
237
+ end
238
+ end