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