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,54 @@
1
+ require 'goliath'
2
+
3
+ module Visor
4
+ module Image
5
+
6
+ # Get detailed information about all public images.
7
+ #
8
+ class GetImagesDetail < Goliath::API
9
+ include Visor::Common::Exception
10
+ include Visor::Common::Util
11
+ use Goliath::Rack::Render, ['json', 'xml']
12
+
13
+ # Pre-process headers as they arrive and load them into a environment variable.
14
+ #
15
+ # @param [Object] env The Goliath environment variables.
16
+ # @param [Object] headers The incoming request HTTP headers.
17
+ #
18
+ def on_headers(env, headers)
19
+ logger.debug "Received headers: #{headers.inspect}"
20
+ env['headers'] = headers
21
+ end
22
+
23
+ # Query database to retrieve the public images detailed meta and return it in request body.
24
+ #
25
+ # @param [Object] env The Goliath environment variables.
26
+ #
27
+ # @return [Array] The HTTP response containing the images
28
+ # metadata or an error code and its messages if anything was raised.
29
+ #
30
+ def response(env)
31
+ access_key = authorize(env, vas)
32
+ meta = vms.get_images_detail(params, access_key)
33
+ [200, {}, {images: meta}]
34
+ rescue Forbidden => e
35
+ exit_error(403, e.message)
36
+ rescue NotFound => e
37
+ exit_error(404, e.message)
38
+ end
39
+
40
+ # Produce an HTTP response with an error code and message.
41
+ #
42
+ # @param [Fixnum] code The error code.
43
+ # @param [String] message The error message.
44
+ #
45
+ # @return [Array] The HTTP response containing an error code and its message.
46
+ #
47
+ def exit_error(code, message)
48
+ logger.error message
49
+ [code, {}, {code: code, message: message}]
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ require 'goliath'
2
+
3
+ module Visor
4
+ module Image
5
+
6
+ # Head metadata about the image with the given id.
7
+ #
8
+ class HeadImage < Goliath::API
9
+ include Visor::Common::Exception
10
+ include Visor::Common::Util
11
+
12
+ # Pre-process headers as they arrive and load them into a environment variable.
13
+ #
14
+ # @param [Object] env The Goliath environment variables.
15
+ # @param [Object] headers The incoming request HTTP headers.
16
+ #
17
+ def on_headers(env, headers)
18
+ logger.debug "Received headers: #{headers.inspect}"
19
+ env['headers'] = headers
20
+ end
21
+
22
+ # Query database to retrieve the wanted image meta and return it as HTTP headers.
23
+ #
24
+ # @param [Object] env The Goliath environment variables.
25
+ #
26
+ def response(env)
27
+ authorize(env, vas)
28
+ meta = vms.get_image(params[:id])
29
+ header = push_meta_into_headers(meta)
30
+ [200, header, nil]
31
+ rescue Forbidden => e
32
+ exit_error(403, e.message)
33
+ rescue NotFound => e
34
+ exit_error(404, e.message)
35
+ end
36
+
37
+ # Produce an HTTP response with an error code and message.
38
+ #
39
+ # @param [Fixnum] code The error code.
40
+ # @param [String] message The error message.
41
+ #
42
+ # @return [Array] The HTTP response containing an error code and its message.
43
+ #
44
+ def exit_error(code, message)
45
+ logger.error message
46
+ [code, {'x-error-code' => code.to_s, 'x-error-message' => message}, nil]
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,189 @@
1
+ require 'goliath'
2
+ require 'digest/md5'
3
+
4
+ module Visor
5
+ module Image
6
+
7
+ # Post image data and metadata and returns the registered metadata.
8
+ #
9
+ class PostImage < Goliath::API
10
+ include Visor::Common::Exception
11
+ include Visor::Common::Util
12
+ use Goliath::Rack::Render, ['json', 'xml']
13
+
14
+ # Pre-process headers as they arrive and load them into a environment variable.
15
+ #
16
+ # @param [Object] env The Goliath environment variables.
17
+ # @param [Object] headers The incoming request HTTP headers.
18
+ #
19
+ def on_headers(env, headers)
20
+ logger.debug "Received headers: #{headers.inspect}"
21
+ env['headers'] = headers
22
+ end
23
+
24
+ # Pre-process body as it arrives in streaming chunks and load them into a tempfile.
25
+ #
26
+ # @param [Object] env The Goliath environment variables.
27
+ # @param [Object] data The incoming request HTTP body chunks.
28
+ #
29
+ def on_body(env, data)
30
+ (env['body'] ||= Tempfile.open('visor-image', encoding: 'ascii-8bit')) << data
31
+ (env['md5'] ||= Digest::MD5.new) << data
32
+ end
33
+
34
+ # Main response method which processes the received headers and body,
35
+ # managing image metadata and file data.
36
+ #
37
+ # @param [Object] env The Goliath environment variables.
38
+ #
39
+ # @return [Array] The HTTP response containing the already inserted image
40
+ # metadata or an error code and its message if anything was raised.
41
+ #
42
+ def response(env)
43
+ begin
44
+ access_key = authorize(env, vas)
45
+ rescue Forbidden => e
46
+ return exit_error(403, e.message)
47
+ end
48
+
49
+ meta = pull_meta_from_headers(env['headers'])
50
+ meta[:owner] = access_key
51
+ body = env['body']
52
+ location = meta[:location]
53
+
54
+ if location && body
55
+ msg = 'When the location header is present no file content can be provided'
56
+ return exit_error(400, msg)
57
+ end
58
+
59
+ if meta[:store] == 'http' || (location && location.split(':').first == 'http')
60
+ return exit_error(400, 'Cannot post an image file to a HTTP backend') if body
61
+ store = Visor::Image::Store::HTTP.new(location)
62
+
63
+ exist, meta[:size], meta[:checksum] = store.file_exists?(false)
64
+ return exit_error(404, "No image file found at #{location}") unless exist
65
+ end
66
+
67
+ # first registers the image meta or raises on error
68
+ begin
69
+ image = insert_meta(meta)
70
+ rescue ArgumentError => e
71
+ body.close if body
72
+ body.unlink if body
73
+ return exit_error(400, e.message)
74
+ rescue InternalError => e
75
+ body.close if body
76
+ body.unlink if body
77
+ return exit_error(500, e.message)
78
+ end
79
+
80
+ # if has body(image file), upload file and update meta or raise on error
81
+ begin
82
+ image = upload_and_update(env['id'], body)
83
+ rescue UnsupportedStore, ArgumentError => e
84
+ return exit_error(400, e.message, true)
85
+ rescue NotFound => e
86
+ return exit_error(404, e.message, true)
87
+ rescue Duplicated => e
88
+ return exit_error(409, e.message, true)
89
+ ensure
90
+ body.close
91
+ body.unlink
92
+ end unless body.nil?
93
+
94
+ [200, {}, {image: image}]
95
+ end
96
+
97
+ # On connection close log a message.
98
+ #
99
+ # @param [Object] env The Goliath environment variables.
100
+ #
101
+ def on_close(env)
102
+ logger.info 'Connection closed'
103
+ end
104
+
105
+ # Produce an HTTP response with an error code and message.
106
+ #
107
+ # @param [Fixnum] code The error code.
108
+ # @param [String] message The error message.
109
+ # @param [True, False] set_status (false) If true, update the image status to 'error'.
110
+ #
111
+ # @return [Array] The HTTP response containing an error code and its message.
112
+ #
113
+ def exit_error(code, message, set_status=false)
114
+ logger.error message
115
+ begin
116
+ vms.put_image(env['id'], status: 'error') if set_status
117
+ rescue => e
118
+ logger.error "Unable to set image #{env['id']} status to 'error': #{e.message}"
119
+ end
120
+ [code, {}, {code: code, message: message}]
121
+ end
122
+
123
+ # Insert image metadata on database (which set its status to locked).
124
+ #
125
+ # @param [Hash] meta The image metadata.
126
+ #
127
+ # @return [Hash] The already inserted image metadata.
128
+ #
129
+ def insert_meta(meta)
130
+ image = vms.post_image(meta)
131
+ env['id'] = image[:_id]
132
+
133
+ if image[:location]
134
+ logger.debug "Location for image #{env['id']} is #{image[:location]}"
135
+ logger.debug "Setting image #{env['id']} status to 'available'"
136
+ vms.put_image(env['id'], status: 'available')
137
+ else
138
+ image
139
+ end
140
+ end
141
+
142
+ # Update image status and launch upload.
143
+ #
144
+ # @param [Fixnum] id The image _id.
145
+ # @param [FIle] body The image body tempfile descriptor.
146
+ #
147
+ # @return [Hash] The already updated image metadata.
148
+ #
149
+ def upload_and_update(id, body)
150
+ logger.debug "Setting image #{id} status to 'uploading'"
151
+ meta = vms.put_image(id, status: 'uploading')
152
+ checksum = env['md5']
153
+ location, size = do_upload(id, meta, body)
154
+
155
+ logger.debug "Updating image #{id} meta:"
156
+ logger.debug "Setting status to 'available'"
157
+ logger.debug "Setting location to '#{location}'"
158
+ logger.debug "Setting size to '#{size}'"
159
+ logger.debug "Setting checksum to '#{checksum}'"
160
+ vms.put_image(id, status: 'available', uploaded_at: Time.now, location: location, size: size, checksum: checksum)
161
+ end
162
+
163
+ # Upload image file to wanted store.
164
+ #
165
+ # @param [Fixnum] id The image _id.
166
+ # @param [Hash] meta The image metadata.
167
+ # @param [FIle] body The image body tempfile descriptor.
168
+ #
169
+ # @return [Array] Image file location URI and size.
170
+ #
171
+ # @raise [ArgumentError] If request Content-Type isn't 'application/octet-stream'
172
+ #
173
+ def do_upload(id, meta, body)
174
+ content_type = env['headers']['Content-Type'] || ''
175
+ store_name = meta[:store] || configs[:default]
176
+ format = meta[:format] || 'none'
177
+
178
+ unless content_type == 'application/octet-stream'
179
+ raise ArgumentError, 'Request Content-Type must be application/octet-stream'
180
+ end
181
+
182
+ store = Visor::Image::Store.get_backend(store_name, configs)
183
+ logger.debug "Uploading image #{id} data to #{store_name} store"
184
+ store.save(id, body, format)
185
+ end
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,205 @@
1
+ require 'goliath'
2
+ require 'digest/md5'
3
+
4
+ module Visor
5
+ module Image
6
+
7
+ # Put image metadata and/or data for the image with the given id.
8
+ #
9
+ class PutImage < Goliath::API
10
+ include Visor::Common::Exception
11
+ include Visor::Common::Util
12
+ use Goliath::Rack::Render, ['json', 'xml']
13
+
14
+ # Pre-process headers as they arrive and load them into a environment variable.
15
+ #
16
+ # @param [Object] env The Goliath environment variables.
17
+ # @param [Object] headers The incoming request HTTP headers.
18
+ #
19
+ def on_headers(env, headers)
20
+ logger.debug "Received headers: #{headers.inspect}"
21
+ env['headers'] = headers
22
+ end
23
+
24
+ # Pre-process body as it arrives in streaming chunks and load them into a tempfile.
25
+ #
26
+ # @param [Object] env The Goliath environment variables.
27
+ # @param [Object] data The incoming request HTTP body chunks.
28
+ #
29
+ def on_body(env, data)
30
+ (env['body'] ||= Tempfile.open('visor-image', encoding: 'ascii-8bit')) << data
31
+ (env['md5'] ||= Digest::MD5.new) << data
32
+ end
33
+
34
+ # Main response method which processes the received headers and body,
35
+ # managing image metadata and file data.
36
+ #
37
+ # @param [Object] env The Goliath environment variables.
38
+ #
39
+ # @return [Array] The HTTP response containing the already inserted image
40
+ # metadata or an error code and its message if anything was raised.
41
+ #
42
+ def response(env)
43
+ begin
44
+ authorize(env, vas)
45
+ rescue Forbidden => e
46
+ return exit_error(403, e.message)
47
+ end
48
+
49
+ meta = pull_meta_from_headers(env['headers'])
50
+ body = env['body']
51
+ id = params[:id]
52
+ location = meta[:location]
53
+
54
+ # a valid update requires the presence of headers and/or body
55
+ if meta.empty? && body.nil?
56
+ msg = 'No headers or body found for update'
57
+ return exit_error(400, msg)
58
+ end
59
+ # only the x-image-meta-location header or the body content should be provided
60
+ if location && body
61
+ msg = 'When the location header is present no file content can be provided'
62
+ return exit_error(400, msg)
63
+ end
64
+
65
+ if meta[:store] == 'http' || (location && location.split(':').first == 'http')
66
+ return exit_error(400, 'Cannot post an image file to a HTTP backend') if body
67
+ store = Visor::Image::Store::HTTP.new(location)
68
+
69
+ exist, meta[:size], meta[:checksum] = store.file_exists?(false)
70
+ return exit_error(404, "No image file found at #{location}") unless exist
71
+ end
72
+
73
+ # first update the image meta or raises on error
74
+ begin
75
+ image = update_meta(id, meta)
76
+ rescue NotFound => e
77
+ return exit_error(404, e.message)
78
+ rescue ArgumentError => e
79
+ body.close if body
80
+ body.unlink if body
81
+ return exit_error(400, e.message)
82
+ rescue InternalError => e
83
+ body.close if body
84
+ body.unlink if body
85
+ return exit_error(500, e.message)
86
+ end unless meta.empty?
87
+
88
+ # if has body(image file), upload file and update meta or raise on error
89
+ begin
90
+ image = upload_and_update(id, body)
91
+ rescue UnsupportedStore, ArgumentError => e
92
+ return exit_error(400, e.message, true)
93
+ rescue NotFound => e
94
+ return exit_error(404, e.message, true)
95
+ rescue ConflictError => e
96
+ return exit_error(409, e.message)
97
+ rescue Duplicated => e
98
+ return exit_error(409, e.message, true)
99
+ ensure
100
+ body.close
101
+ body.unlink
102
+ end unless body.nil?
103
+
104
+ [200, {}, {image: image}]
105
+ end
106
+
107
+ # On connection close log a message.
108
+ #
109
+ # @param [Object] env The Goliath environment variables.
110
+ #
111
+ def on_close(env)
112
+ logger.info 'Connection closed'
113
+ end
114
+
115
+ # Produce an HTTP response with an error code and message.
116
+ #
117
+ # @param [Fixnum] code The error code.
118
+ # @param [String] message The error message.
119
+ # @param [True, False] set_status (false) If true, update the image status to 'error'.
120
+ #
121
+ # @return [Array] The HTTP response containing an error code and its message.
122
+ #
123
+ def exit_error(code, message, set_status=false)
124
+ logger.error message
125
+ begin
126
+ vms.put_image(params[:id], status: 'error') if set_status
127
+ rescue => e
128
+ logger.error "Unable to set image #{env['id']} status to 'error': #{e.message}"
129
+ end
130
+ [code, {}, {code: code, message: message}]
131
+ end
132
+
133
+ # Update image metadata and set status if needed.
134
+ #
135
+ # @param [Hash] meta The image metadata.
136
+ #
137
+ # @return [Hash] The already inserted image metadata.
138
+ #
139
+ def update_meta(id, meta)
140
+ logger.debug "Updating image #{id} meta:"
141
+ logger.debug "#{id} #{meta}"
142
+ image = vms.put_image(id, meta)
143
+ image.each { |k, v| logger.debug "#{k.to_s.capitalize} setted to '#{v}'" if v }
144
+
145
+ if image[:location]
146
+ logger.debug "Location for image #{env['id']} is #{image[:location]}"
147
+ logger.debug "Setting image #{env['id']} status to 'available'"
148
+ vms.put_image(id, status: 'available')
149
+ else
150
+ image
151
+ end
152
+ end
153
+
154
+ # Update image status and launch upload.
155
+ #
156
+ # @param [Fixnum] id The image _id.
157
+ # @param [FIle] body The image body tempfile descriptor.
158
+ #
159
+ # @return [Hash] The already updated image metadata.
160
+ #
161
+ def upload_and_update(id, body)
162
+ meta = vms.get_image(id)
163
+ checksum = env['md5']
164
+ valid = (meta[:status] == 'locked' || meta[:status] == 'error')
165
+ raise ConflictError, 'Can only assign image file to a locked or error image' unless valid
166
+
167
+ logger.debug "Setting image #{id} status to 'uploading'"
168
+ meta = vms.put_image(id, status: 'uploading')
169
+ location, size = do_upload(id, meta, body)
170
+
171
+ logger.debug "Updating image #{id} meta:"
172
+ logger.debug "Setting status to 'available'"
173
+ logger.debug "Setting location to '#{location}'"
174
+ logger.debug "Setting size to '#{size}'"
175
+ logger.debug "Setting checksum to '#{checksum}'"
176
+ vms.put_image(id, status: 'available', location: location, size: size, checksum: checksum)
177
+ end
178
+
179
+ # Upload image file to wanted store.
180
+ #
181
+ # @param [Fixnum] id The image _id.
182
+ # @param [Hash] meta The image metadata.
183
+ # @param [FIle] body The image body tempfile descriptor.
184
+ #
185
+ # @return [Array] Image file location URI and size.
186
+ #
187
+ # @raise [ArgumentError] If request Content-Type isn't 'application/octet-stream'
188
+ #
189
+ def do_upload(id, meta, body)
190
+ content_type = env['headers']['Content-Type'] || ''
191
+ store_name = meta[:store] || configs[:default]
192
+ format = meta[:format] || 'none'
193
+
194
+ unless content_type == 'application/octet-stream'
195
+ raise ArgumentError, 'Request Content-Type must be application/octet-stream'
196
+ end
197
+
198
+ store = Visor::Image::Store.get_backend(store_name, configs)
199
+ logger.debug "Uploading image #{id} data to #{store_name} store"
200
+ store.save(id, body, format)
201
+ end
202
+ end
203
+
204
+ end
205
+ end