visor-image 0.0.1

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 (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