visor-image 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/bin/visor +423 -0
  2. data/bin/visor-image +10 -0
  3. data/config/server.rb +14 -0
  4. data/lib/image/auth.rb +147 -0
  5. data/lib/image/cli.rb +397 -0
  6. data/lib/image/client.rb +490 -0
  7. data/lib/image/meta.rb +219 -0
  8. data/lib/image/routes/delete_all_images.rb +40 -0
  9. data/lib/image/routes/delete_image.rb +62 -0
  10. data/lib/image/routes/get_image.rb +78 -0
  11. data/lib/image/routes/get_images.rb +54 -0
  12. data/lib/image/routes/get_images_detail.rb +54 -0
  13. data/lib/image/routes/head_image.rb +51 -0
  14. data/lib/image/routes/post_image.rb +189 -0
  15. data/lib/image/routes/put_image.rb +205 -0
  16. data/lib/image/server.rb +307 -0
  17. data/lib/image/store/cumulus.rb +126 -0
  18. data/lib/image/store/file_system.rb +119 -0
  19. data/lib/image/store/hdfs.rb +149 -0
  20. data/lib/image/store/http.rb +78 -0
  21. data/lib/image/store/lunacloud.rb +126 -0
  22. data/lib/image/store/s3.rb +121 -0
  23. data/lib/image/store/store.rb +39 -0
  24. data/lib/image/store/walrus.rb +130 -0
  25. data/lib/image/version.rb +5 -0
  26. data/lib/visor-image.rb +30 -0
  27. data/spec/lib/client_spec.rb +0 -0
  28. data/spec/lib/meta_spec.rb +230 -0
  29. data/spec/lib/routes/delete_image_spec.rb +98 -0
  30. data/spec/lib/routes/get_image_spec.rb +78 -0
  31. data/spec/lib/routes/get_images_detail_spec.rb +104 -0
  32. data/spec/lib/routes/get_images_spec.rb +104 -0
  33. data/spec/lib/routes/head_image_spec.rb +51 -0
  34. data/spec/lib/routes/post_image_spec.rb +112 -0
  35. data/spec/lib/routes/put_image_spec.rb +109 -0
  36. data/spec/lib/server_spec.rb +62 -0
  37. data/spec/lib/store/cumulus_spec.rb +0 -0
  38. data/spec/lib/store/file_system_spec.rb +32 -0
  39. data/spec/lib/store/http_spec.rb +56 -0
  40. data/spec/lib/store/s3_spec.rb +37 -0
  41. data/spec/lib/store/store_spec.rb +36 -0
  42. data/spec/lib/store/walrus_spec.rb +0 -0
  43. metadata +217 -0
@@ -0,0 +1,39 @@
1
+ require 'uri'
2
+
3
+ module Visor
4
+ module Image
5
+
6
+ # Visor Image Store module encapsulates all store backend classes, plus a set of
7
+ # utility methods common to all stores.
8
+ #
9
+ module Store
10
+ extend self
11
+ include Visor::Common::Exception
12
+
13
+ # Base API mapping for the multiple storage backend classes
14
+ BACKENDS = {:s3 => Visor::Image::Store::S3,
15
+ :cumulus => Visor::Image::Store::Cumulus,
16
+ :walrus => Visor::Image::Store::Walrus,
17
+ :lunacloud => Visor::Image::Store::Lunacloud,
18
+ :hdfs => Visor::Image::Store::HDFS,
19
+ :file => Visor::Image::Store::FileSystem,
20
+ :http => Visor::Image::Store::HTTP}
21
+
22
+ # Get a store backend class object ready to use, based on a file URI or store name.
23
+ #
24
+ # @param string [String] The file location URI or backend name.
25
+ # @param config [Hash] A set of configurations for the wanted store, loaded from
26
+ # VISoR configuration file.
27
+ #
28
+ # @return [Object] An instantiated store object ready to use.
29
+ #
30
+ def get_backend(string, config)
31
+ name = URI(string).scheme || string
32
+ store = BACKENDS[name.to_sym]
33
+ raise UnsupportedStore, "The store '#{name}' is not supported" unless store
34
+ store.new(string, config)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,130 @@
1
+ require 'uri'
2
+ require 's3restful'
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/em-http'
5
+
6
+ module Visor
7
+ module Image
8
+ module Store
9
+
10
+ # The Eucalyptus Walrus (Walrus) backend store.
11
+ #
12
+ # This class handles the management of image files located in the Walrus storage system,
13
+ # based on a URI like *walrus://<access_key>:<secret_key>@<host>:<port>/<bucket>/<image>*.
14
+ #
15
+ class Walrus
16
+ include Visor::Common::Exception
17
+
18
+ attr_accessor :uri, :config, :access_key, :secret_key, :bucket, :file, :host, :port
19
+
20
+ # Initializes a new Walrus store client object. Walrus credentials are loaded from the URI,
21
+ # on GET and DELETE operations, or from the configuration file for POST and PUT operation.
22
+ #
23
+ # @param [String] uri The URI of the file location.
24
+ # @param config [Hash] A set of configurations for the wanted store, loaded from
25
+ # VISoR configuration file.
26
+ #
27
+ # @return [Object] An instantiated Walrus store object ready to use.
28
+ #
29
+ def initialize(uri, config)
30
+ @uri = URI(uri)
31
+ @config = config[:walrus]
32
+
33
+ if @uri.scheme
34
+ @access_key = @uri.user
35
+ @secret_key = @uri.password
36
+ # Trick:
37
+ # To build a successful Walrus connection we need happening to sign the
38
+ # '/services/Walrus' in path too. If we append it to server's address it wont get
39
+ # signed in the authorization string, we need to prepend it to the bucket name
40
+ @bucket = File.join('services/Walrus', @uri.path.split('/')[3])
41
+ @file = @uri.path.split('/').last
42
+ @host = @uri.host
43
+ @port = @uri.port
44
+ else
45
+ @access_key = @config[:access_key]
46
+ @secret_key = @config[:secret_key]
47
+ @bucket = File.join('services/Walrus', @config[:bucket])
48
+ @host = @config[:host]
49
+ @port = @config[:port]
50
+ end
51
+ end
52
+
53
+ # Returns a Happening library S3 Walrus compatible connection object.
54
+ #
55
+ # @return [Happening::S3::Item] A new Walrus connection object.
56
+ #
57
+ def connection
58
+ S3restful::S3::Item.new(bucket, file, server: host, port: port, protocol: 'http',
59
+ aws_access_key_id: access_key, aws_secret_access_key: secret_key)
60
+ end
61
+
62
+ # Returns the image file to clients, streamed in chunks.
63
+ #
64
+ # @return [Object] Yields the file, a chunk at time.
65
+ #
66
+ def get
67
+ s3 = connection.aget
68
+ finish = proc { yield nil }
69
+
70
+ s3.stream { |chunk| yield chunk }
71
+ s3.callback &finish
72
+ s3.errback &finish
73
+ end
74
+
75
+ # Saves the image file to the its final destination, based on the temporary file
76
+ # created by the server at data reception time.
77
+ #
78
+ # @param [String] id The image id.
79
+ # @param [File] tmp_file The temporary file descriptor.
80
+ # @param [String] format The image file format.
81
+ #
82
+ # @return [String, Integer] The generated file location URI and image file size.
83
+ #
84
+ # @raise [Duplicated] If the image file already exists.
85
+ #
86
+ def save(id, tmp_file, format)
87
+ @file = "#{id}.#{format}"
88
+ uri = "walrus://#{access_key}:#{secret_key}@#{host}:#{port}/#{bucket}/#{file}"
89
+ size = tmp_file.size
90
+
91
+ raise Duplicated, "The image file #{fp} already exists" if file_exists?(false)
92
+ STDERR.puts "COPYING!!"
93
+
94
+ connection.store tmp_file.path
95
+
96
+ [uri, size]
97
+ end
98
+
99
+ # Deletes the image file from its location.
100
+ #
101
+ # @raise [NotFound] If the image file was not found.
102
+ #
103
+ def delete
104
+ connection.delete
105
+ end
106
+
107
+ # Check if the image file exists.
108
+ #
109
+ # @param [True, False] raise_exc (true) If it should raise exception or return
110
+ # true/false whether the file exists or not.
111
+ #
112
+ # @return [True, False] If raise_exc is false, return true/false whether the
113
+ # file exists or not.
114
+ #
115
+ # @raise [NotFound] If the image file was not found.
116
+ #
117
+ def file_exists?(raise_exc=true)
118
+ exist = nil
119
+ error = proc { exist = false }
120
+ success = proc { |res| exist = true if res.response_header.status == 200 }
121
+
122
+ connection.head(on_error: error, on_success: success)
123
+ raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
124
+ exist
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,5 @@
1
+ module Visor
2
+ module Image
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,30 @@
1
+ require 'goliath'
2
+ require 'visor-common'
3
+
4
+ $:.unshift File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'image/version'
7
+ require 'image/meta'
8
+ require 'image/auth'
9
+ require 'image/cli'
10
+ require 'image/store/s3'
11
+ require 'image/store/cumulus'
12
+ require 'image/store/walrus'
13
+ require 'image/store/lunacloud'
14
+ require 'image/store/hdfs'
15
+ require 'image/store/http'
16
+ require 'image/store/file_system'
17
+ require 'image/store/store'
18
+ require 'image/routes/head_image'
19
+ require 'image/routes/get_images'
20
+ require 'image/routes/get_images_detail'
21
+ require 'image/routes/get_image'
22
+ require 'image/routes/post_image'
23
+ require 'image/routes/put_image'
24
+ require 'image/routes/delete_image'
25
+ require 'image/routes/delete_all_images'
26
+ require 'image/server'
27
+ require 'image/client'
28
+
29
+
30
+
File without changes
@@ -0,0 +1,230 @@
1
+ require "spec_helper"
2
+
3
+ describe Visor::Image::Meta do
4
+
5
+ let(:meta) { Visor::Image::Meta.new }
6
+ let(:not_found) { Visor::Common::Exception::NotFound }
7
+ let(:invalid) { Visor::Common::Exception::Invalid }
8
+
9
+ let(:valid_post) { {name: 'client_spec', architecture: 'i386', access: 'public'} }
10
+ let(:invalid_post) { {name: 'client_spec', architecture: 'i386', access: 'invalid'} }
11
+
12
+ let(:valid_update) { {architecture: 'x86_64'} }
13
+ let(:invalid_update) { {architecture: 'invalid'} }
14
+
15
+ inserted = []
16
+
17
+ before(:all) do
18
+ EM.synchrony do
19
+ inserted << meta.post_image(valid_post)[:_id]
20
+ inserted << meta.post_image(valid_post.merge(architecture: 'x86_64'))[:_id]
21
+ EM.stop
22
+ end
23
+ end
24
+
25
+ after(:all) do
26
+ EM.synchrony do
27
+ inserted.each { |id| meta.delete_image(id) }
28
+ EM.stop
29
+ end
30
+ end
31
+
32
+ describe "#initialize" do
33
+ it "should create a new meta api object with default parameters" do
34
+ meta.host.should == Visor::Image::Meta::DEFAULT_HOST
35
+ meta.port.should == Visor::Image::Meta::DEFAULT_PORT
36
+ end
37
+
38
+ it "should create a new meta api object with custom parameters" do
39
+ custom = Visor::Image::Meta.new(host: '0.0.0.0', port: 1111)
40
+ custom.host.should == '0.0.0.0'
41
+ custom.port.should == 1111
42
+ end
43
+ end
44
+
45
+ describe "#get_images" do
46
+ it "should return an array of hashes" do
47
+ EM.synchrony do
48
+ res = meta.get_images
49
+ res.should be_a Array
50
+ res.each { |h| h.should be_a Hash }
51
+ EM.stop
52
+ end
53
+ end
54
+
55
+ it "should filter results by parameter" do
56
+ EM.synchrony do
57
+ res = meta.get_images(architecture: 'i386')
58
+ res.each { |h| h[:architecture].should == 'i386' }
59
+ EM.stop
60
+ end
61
+ end
62
+
63
+ it "should sort results by parameter and direction" do
64
+ EM.synchrony do
65
+ pub = meta.get_images(sort: 'architecture', dir: 'desc')
66
+ pub.first[:architecture].should == 'x86_64'
67
+ pub = meta.get_images(sort: 'architecture', dir: 'asc')
68
+ pub.first[:architecture].should == 'i386'
69
+ EM.stop
70
+ end
71
+ end
72
+
73
+ it "should raise if no matches found" do
74
+ EM.synchrony do
75
+ lambda { meta.get_images(:name => 'fake') }.should raise_error not_found
76
+ EM.stop
77
+ end
78
+ end
79
+ end
80
+
81
+ describe "#get_images_detail" do
82
+ it "should return an array of hashes" do
83
+ EM.synchrony do
84
+ res = meta.get_images
85
+ res.should be_a Array
86
+ res.each { |h| h.should be_a Hash }
87
+ EM.stop
88
+ end
89
+ end
90
+
91
+ it "should filter results by parameter" do
92
+ EM.synchrony do
93
+ res = meta.get_images(architecture: 'x86_64')
94
+ res.each { |h| h[:architecture].should == 'x86_64' }
95
+ EM.stop
96
+ end
97
+ end
98
+
99
+ it "should sort results by parameter and direction" do
100
+ EM.synchrony do
101
+ pub = meta.get_images(sort: 'architecture', dir: 'desc')
102
+ pub.first[:architecture].should == 'x86_64'
103
+ pub = meta.get_images(sort: 'architecture', dir: 'asc')
104
+ pub.first[:architecture].should == 'i386'
105
+ EM.stop
106
+ end
107
+ end
108
+
109
+ it "should raise if no matches found" do
110
+ EM.synchrony do
111
+ lambda { meta.get_images(:name => 'fake') }.should raise_error not_found
112
+ EM.stop
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "#get_image" do
118
+ before(:each) do
119
+ EM.synchrony do
120
+ @id = meta.post_image(valid_post)[:_id]
121
+ inserted << @id
122
+ @image = meta.get_image(@id)
123
+ EM.stop
124
+ end
125
+ end
126
+
127
+ it "should return a hash" do
128
+ @image.should be_a Hash
129
+ end
130
+
131
+ it "should return the asked image metadata" do
132
+ @image[:_id].should == @id
133
+ end
134
+
135
+ it "should raise an exception if image not found" do
136
+ fake_id = 0
137
+ EM.synchrony do
138
+ lambda { meta.get_image(fake_id) }.should raise_error not_found
139
+ EM.stop
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "#delete_image" do
145
+ before(:each) do
146
+ EM.synchrony do
147
+ @id = meta.post_image(valid_post)[:_id]
148
+ @image = meta.delete_image(@id)
149
+ EM.stop
150
+ end
151
+ end
152
+
153
+ it "should return a hash" do
154
+ @image.should be_a Hash
155
+ end
156
+
157
+ it "should return the deleted image metadata" do
158
+ @image[:_id].should == @id
159
+ end
160
+
161
+ it "should trully delete that image from database" do
162
+ EM.synchrony do
163
+ lambda { meta.get_image(@id) }.should raise_error not_found
164
+ EM.stop
165
+ end
166
+ end
167
+
168
+ it "should raise an exception if image not found" do
169
+ fake_id = 0
170
+ EM.synchrony do
171
+ lambda { meta.delete_image(fake_id) }.should raise_error not_found
172
+ EM.stop
173
+ end
174
+ end
175
+ end
176
+
177
+ describe "#post_image" do
178
+ before(:each) do
179
+ EM.synchrony do
180
+ @image = meta.post_image(valid_post)
181
+ inserted << @image[:_id]
182
+ EM.stop
183
+ end
184
+ end
185
+
186
+ it "should return a hash" do
187
+ @image.should be_a Hash
188
+ end
189
+
190
+ it "should return posted image metadata" do
191
+ @image[:_id].should be_a(String)
192
+ @image[:access].should == valid_post[:access]
193
+ end
194
+
195
+ it "should raise an exception if meta validation fails" do
196
+ EM.synchrony do
197
+ lambda { meta.post_image(invalid_post) }.should raise_error invalid
198
+ EM.stop
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "#put_image" do
204
+ before :each do
205
+ EM.synchrony do
206
+ @id = meta.post_image(valid_post)[:_id]
207
+ inserted << @id
208
+ @image = meta.put_image(@id, valid_update)
209
+ EM.stop
210
+ end
211
+ end
212
+
213
+ it "should return a hash" do
214
+ @image.should be_a Hash
215
+ end
216
+
217
+ it "should return update image metadata" do
218
+ @image[:_id].should == @id
219
+ @image[:architecture].should == valid_update[:architecture]
220
+ end
221
+
222
+ it "should raise an exception if meta validation fails" do
223
+ EM.synchrony do
224
+ lambda { meta.put_image(@id, invalid_update) }.should raise_error invalid
225
+ EM.stop
226
+ end
227
+ end
228
+ end
229
+
230
+ end
@@ -0,0 +1,98 @@
1
+ require "spec_helper"
2
+
3
+ describe Visor::Image::Server do
4
+
5
+ let(:test_api) { Visor::Image::Server }
6
+ let(:err) { Proc.new { fail "API request failed" } }
7
+
8
+ let(:accept_json) { {'Accept' => 'application/json'} }
9
+ let(:accept_xml) { {'Accept' => 'application/xml'} }
10
+
11
+ let(:parse_opts) { {symbolize_names: true} }
12
+ let(:valid_post) { {name: 'server_spec', architecture: 'i386', access: 'public'} }
13
+ let(:api_options) { {config: File.expand_path(File.join(File.dirname(__FILE__), '../../../', 'config/server.rb'))} }
14
+
15
+ inserted = []
16
+
17
+ before(:each) do
18
+ EM.synchrony do
19
+ inserted << DB.post_image(valid_post)[:_id]
20
+ EM.stop
21
+ end
22
+ end
23
+
24
+ after(:all) do
25
+ EM.synchrony do
26
+ inserted.each { |id| DB.delete_image(id) }
27
+ EM.stop
28
+ end
29
+ end
30
+
31
+ #
32
+ # DELETE /images/<id>
33
+ #
34
+ describe "DELETE /images/:id" do
35
+
36
+ it "should delete an image metadata" do
37
+ id = inserted.delete_at(0)
38
+
39
+ with_api(test_api, api_options) do
40
+ delete_request({:path => "/images/#{id}"}, err) do |c|
41
+ assert_200 c
42
+ c.response.should be_a String
43
+ end
44
+ end
45
+
46
+ with_api(test_api, api_options) do
47
+ delete_request({:path => "/images/#{id}"}, err) do |c|
48
+ assert_404 c
49
+ c.response.should match /id/
50
+ end
51
+ end
52
+ end
53
+
54
+ it "should return a JSON string if no accept header provided" do
55
+ with_api(test_api, api_options) do
56
+ id = inserted.delete_at(0)
57
+ delete_request({:path => "/images/#{id}"}, err) do |c|
58
+ assert_200 c
59
+ c.response.should match /\{/
60
+ end
61
+ end
62
+ end
63
+
64
+ it "should return a JSON string if accepted" do
65
+ with_api(test_api, api_options) do
66
+ id = inserted.delete_at(0)
67
+ delete_request({:path => "/images/#{id}", head: accept_json}, err) do |c|
68
+ assert_200 c
69
+ c.response.should match /\{/
70
+ end
71
+ end
72
+ end
73
+
74
+ it "should return a XML document if accepted" do
75
+ with_api(test_api, api_options) do
76
+ id = inserted.delete_at(0)
77
+ delete_request({:path => "/images/#{id}", head: accept_xml}, err) do |c|
78
+ assert_200 c
79
+ c.response.should match /\<?xml/
80
+ end
81
+ end
82
+ end
83
+
84
+ it "should raise a HTTPNotFound 404 error if no images found" do
85
+ with_api(test_api, api_options) do
86
+ delete_request({:path => "/images/fakeid"}, err) do |c|
87
+ assert_404 c
88
+ c.response.should match /fakeid/
89
+ end
90
+ end
91
+ end
92
+
93
+ it "should raise a HTTPInternalServer 500 error if no server error" do
94
+
95
+ end
96
+
97
+ end
98
+ end