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
data/lib/image/cli.rb ADDED
@@ -0,0 +1,397 @@
1
+ require 'open-uri'
2
+ require 'logger'
3
+ require 'optparse'
4
+ require 'fileutils'
5
+
6
+ require 'goliath/api'
7
+ require 'goliath/runner'
8
+
9
+ module Visor
10
+ module Image
11
+ class CLI
12
+
13
+ attr_reader :argv, :conf_file, :options, :new_opts, :command, :parser
14
+
15
+ # Available commands
16
+ COMMANDS = %w[start stop restart status clean]
17
+ # Commands that wont load options from the config file
18
+ NO_CONF_LOAD = %w[stop status clean]
19
+ # Default files directory
20
+ DEFAULT_DIR = File.expand_path('~/.visor')
21
+
22
+ # Initialize a new CLI
23
+ def initialize(argv=ARGV)
24
+ @argv = argv
25
+ @options = {}
26
+ @conf_file = load_conf_file
27
+ @options = defaults
28
+ @new_opts = []
29
+ @parser = parser
30
+ @command = parse!
31
+ end
32
+
33
+ # Generate the default options
34
+ def defaults
35
+ {:config => ENV['GOLIATH_CONF'],
36
+ :address => conf_file[:bind_host],
37
+ :port => conf_file[:bind_port],
38
+ :log_file => File.join(File.expand_path(conf_file[:log_path]), conf_file[:log_file]),
39
+ :pid_file => File.join(DEFAULT_DIR, 'visor_api.pid'),
40
+ :env => :production,
41
+ :daemonize => true,
42
+ :log_stdout => false}
43
+ end
44
+
45
+ # OptionParser parser
46
+ def parser
47
+ OptionParser.new do |opts|
48
+ opts.banner = "Usage: visor-image [OPTIONS] COMMAND"
49
+
50
+ opts.separator ""
51
+ opts.separator "Commands:"
52
+ opts.separator " start start the server"
53
+ opts.separator " stop stop the server"
54
+ opts.separator " restart restart the server"
55
+ opts.separator " status current server status"
56
+
57
+ opts.separator ""
58
+ opts.separator "Options:"
59
+
60
+ opts.on("-c", "--config FILE", "Load a custom configuration file") do |file|
61
+ options[:conf_file] = File.expand_path(file)
62
+ new_opts << :config_file
63
+ end
64
+ opts.on("-a", "--address HOST", "Bind to HOST address (default: #{options[:address]})") do |addr|
65
+ options[:address] = addr
66
+ new_opts << :address
67
+ end
68
+ opts.on("-p", "--port PORT", "Use PORT (default: #{options[:port]})") do |port|
69
+ options[:port] = port.to_i
70
+ new_opts << :port
71
+ end
72
+ opts.on("-e", "--env NAME", "Set the execution environment (default: #{options[:env]})") do |env|
73
+ options[:env] = env.to_sym
74
+ new_opts << :env
75
+ end
76
+
77
+ opts.separator ""
78
+ opts.on('-l', '--log FILE', "Log to file (default: #{@options[:log_file]})") do |file|
79
+ @options[:log_file] = file
80
+ new_opts << :log_file
81
+ end
82
+ opts.on('-u', '--user USER', "Run as specified user") do |v|
83
+ @options[:user] = v
84
+ new_opts << :user
85
+ end
86
+ opts.on("-f", "--foreground", "Do not daemonize") do
87
+ options[:daemonize] = false
88
+ options[:log_stdout] = true
89
+ new_opts << :daemonize
90
+ end
91
+
92
+ #opts.separator ""
93
+ #opts.separator "SSL options:"
94
+ #opts.on('--ssl', 'Enables SSL (default: off)') {|v| @options[:ssl] = v }
95
+ #opts.on('--ssl-key FILE', 'Path to private key') {|v| @options[:ssl_key] = v }
96
+ #opts.on('--ssl-cert FILE', 'Path to certificate') {|v| @options[:ssl_cert] = v }
97
+ #opts.on('--ssl-verify', 'Enables SSL certificate verification') {|v| @options[:ssl_verify] = v }
98
+
99
+ opts.separator ""
100
+ opts.separator "Common options:"
101
+
102
+ opts.on_tail("-d", "--debug", "Set debugging on") do
103
+ options[:debug] = true
104
+ new_opts << :debug
105
+ end
106
+ opts.on_tail('-v', '--verbose', "Enable verbose logging") do
107
+ options[:verbose] = true
108
+ new_opts << :verbose
109
+ end
110
+ opts.on_tail("-h", "--help", "Show this message") { show_options(opts) }
111
+ opts.on_tail('-V', '--version', "Show version") { show_version }
112
+ end
113
+ end
114
+
115
+ # Parse the current shell arguments and run the command.
116
+ # Exits on error.
117
+ def run!
118
+ if command.nil?
119
+ abort @parser.to_s
120
+
121
+ elsif COMMANDS.include?(command)
122
+ unless NO_CONF_LOAD.include?(command)
123
+ @options.merge!({address: options[:address], port: options[:port]})
124
+ end
125
+
126
+ case command
127
+ when 'start' then
128
+ start
129
+ when 'stop' then
130
+ stop
131
+ when 'restart' then
132
+ restart
133
+ when 'status' then
134
+ status
135
+ else
136
+ clean
137
+ end
138
+ exit 0
139
+ else
140
+ abort "Unknown command: #{command}. Available commands: #{COMMANDS.join(', ')}"
141
+ end
142
+ end
143
+
144
+ # Remove all files created by the daemon.
145
+ def clean
146
+ begin
147
+ FileUtils.rm(pid_file) rescue Errno::ENOENT
148
+ FileUtils.rm(url_file) rescue Errno::ENOENT
149
+ end
150
+ put_and_log :warn, "Removed all tracking files created at server start"
151
+ end
152
+
153
+ # Restart server
154
+ def restart
155
+ @restart = true
156
+ stop
157
+ sleep 0.1 while running?
158
+ start
159
+ end
160
+
161
+ # Display current server status
162
+ def status
163
+ if running?
164
+ STDERR.puts "VISoR Image Server is running PID: #{fetch_pid} URL: #{fetch_url}"
165
+ else
166
+ STDERR.puts "VISoR Image Server is not running."
167
+ end
168
+ end
169
+
170
+ # Stop the server
171
+ def stop
172
+ begin
173
+ pid = File.read(pid_file)
174
+ put_and_log :warn, "Stopping VISoR Image Server with PID: #{pid.to_i} Signal: INT"
175
+ Process.kill(:INT, pid.to_i)
176
+ File.delete(url_file)
177
+ rescue
178
+ put_and_log :warn, "Cannot stop VISoR Image Server, is it running?"
179
+ exit! 1
180
+ end
181
+ end
182
+
183
+ # Start the server
184
+ def start
185
+ FileUtils.mkpath(DEFAULT_DIR) unless Dir.exists?(DEFAULT_DIR)
186
+ begin
187
+ is_it_running?
188
+ can_use_port?
189
+ write_url
190
+ launch!
191
+ rescue => e
192
+ put_and_log :warn, "Error starting VISoR Image Server: #{e.message}\n#{e.backtrace.to_s}"
193
+ exit! 1
194
+ end
195
+ end
196
+
197
+ # Launch the server
198
+ def launch!
199
+ put_and_log :info, "Starting VISoR Image Server at #{options[:address]}:#{options[:port]}"
200
+ debug_settings
201
+
202
+ runner = Goliath::Runner.new(opts_to_goliath, Visor::Image::Server.new)
203
+ runner.app = Goliath::Rack::Builder.build(Visor::Image::Server, runner.api)
204
+ runner.run
205
+ end
206
+
207
+
208
+ protected
209
+
210
+ # Convert options hash to a compatible Goliath ARGV array
211
+ def opts_to_goliath
212
+ argv_like = []
213
+ @options.each do |k, v|
214
+ case k
215
+ when :config then
216
+ argv_like << '-c' << v.to_s
217
+ when :log_file then
218
+ argv_like << '-l' << v.to_s
219
+ when :pid_file then
220
+ argv_like << '-P' << v.to_s
221
+ when :env then
222
+ argv_like << '-e' << v.to_s
223
+ when :address then
224
+ argv_like << '-a' << v.to_s
225
+ when :port then
226
+ argv_like << '-p' << v.to_s
227
+ when :user then
228
+ argv_like << '-u' << v.to_s
229
+ when :daemonize then
230
+ argv_like << '-d' << v.to_s if v
231
+ when :verbose then
232
+ argv_like << '-v' << v.to_s if v
233
+ when :log_stdout then
234
+ argv_like << '-s' << v.to_s if v
235
+ end
236
+ end
237
+ argv_like
238
+ end
239
+
240
+ # Display options
241
+ def show_options(opts)
242
+ puts opts
243
+ exit
244
+ end
245
+
246
+ # Show VISoR Image Server version
247
+ def show_version
248
+ puts "VISoR Image Server v#{Visor::Image::VERSION}"
249
+ exit
250
+ end
251
+
252
+ # Check if the server is already running
253
+ def is_it_running?
254
+ if files_exist?(pid_file, url_file)
255
+ if running?
256
+ put_and_log :warn, "VISoR Image Server is already running at #{fetch_url}"
257
+ exit! 1
258
+ else
259
+ clean
260
+ end
261
+ end
262
+ end
263
+
264
+ # Test process pid to access the server status
265
+ def running?
266
+ begin
267
+ Process.kill 0, fetch_pid
268
+ true
269
+ rescue Errno::ESRCH
270
+ false
271
+ rescue Errno::EPERM
272
+ true
273
+ rescue
274
+ false
275
+ end
276
+ end
277
+
278
+ # Test if port is open
279
+ def can_use_port?
280
+ unless port_open?
281
+ put_and_log :warn, "Port #{options[:port]} already in use. Please try other."
282
+ exit! 1
283
+ end
284
+ end
285
+
286
+ # Access that port is open
287
+ def port_open?
288
+ begin
289
+ STDERR.puts url
290
+ options[:no_proxy] ? open(url, proxy: nil) : open(url)
291
+ false
292
+ rescue OpenURI::HTTPError #TODO: quick-fix, try solve this
293
+ false
294
+ rescue Errno::ECONNREFUSED
295
+ true
296
+ end
297
+ end
298
+
299
+ # Retrieve a logger instance
300
+ def logger
301
+ @logger ||=
302
+ begin
303
+ log = options[:daemonize] ? Logger.new(log_file) : Logger.new(STDERR)
304
+ conf_level = conf_file[:log_level] == 'INFO' ? 1 : 0
305
+ log.level = options[:debug] ? 0 : conf_level
306
+ log.formatter = proc do |s, t, n, msg|
307
+ #"[#{t.strftime(conf_file[:log_datetime_format])}] #{s} - #{msg}\n"
308
+ "[#{Process.pid}:#{s}] #{t.strftime(conf_file[:log_datetime_format])} :: #{msg}\n"
309
+ end
310
+ log
311
+ end
312
+ end
313
+
314
+ # Print to stderr and log message
315
+ def put_and_log(level, msg)
316
+ STDERR.puts msg
317
+ logger.send level, msg
318
+ end
319
+
320
+ # Parse argv arguments
321
+ def parse!
322
+ parser.parse! argv
323
+ argv.shift
324
+ end
325
+
326
+ # Log debug settings
327
+ def debug_settings
328
+ logger.debug "Configurations loaded from #{conf_file[:file]}:"
329
+ logger.debug "**************************************************************"
330
+ conf_file.each { |k, v| logger.debug "#{k}: #{v}" unless k == :file }
331
+ logger.debug "**************************************************************"
332
+
333
+ logger.debug "Configurations passed from VISoR Image Server CLI:"
334
+ logger.debug "**************************************************************"
335
+ if new_opts.empty?
336
+ logger.debug "none"
337
+ else
338
+ new_opts.each { |k| logger.debug "#{k}: #{options[k]}" }
339
+ end
340
+ logger.debug "**************************************************************"
341
+ end
342
+
343
+ # Check if a set of files exist
344
+ def files_exist?(*files)
345
+ files.each { |file| return false unless File.exists?(File.expand_path(file)) }
346
+ true
347
+ end
348
+
349
+ # Generate the listening url
350
+ def url
351
+ "http://#{options[:address]}:#{options[:port]}"
352
+ end
353
+
354
+ # Write url to file
355
+ def write_url
356
+ File.open(url_file, 'w') { |f| f << url }
357
+ end
358
+
359
+ # Load url from file
360
+ def fetch_url
361
+ IO.read(url_file).split('//').last
362
+ rescue
363
+ nil
364
+ end
365
+
366
+ # Load configuration file options
367
+ def load_conf_file
368
+ Visor::Common::Config.load_config(:visor_image, options[:conf_file])
369
+ rescue => e
370
+ raise "There was an error loading the configuration file: #{e.message}"
371
+ end
372
+
373
+ # Fetch pid from file
374
+ def fetch_pid
375
+ IO.read(pid_file).to_i
376
+ rescue
377
+ nil
378
+ end
379
+
380
+ # Current pid file option
381
+ def pid_file
382
+ options[:pid_file]
383
+ end
384
+
385
+ # Current log file option
386
+ def log_file
387
+ options[:log_file]
388
+ end
389
+
390
+ # Current url file location
391
+ def url_file
392
+ File.join(DEFAULT_DIR, 'visor_api.url')
393
+ end
394
+
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,490 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'json'
5
+
6
+ module Visor
7
+ module Image
8
+
9
+ # The Client API for the VISoR Image Server. This class supports all image metadata and
10
+ # files operations through a programmatically interface.
11
+ #
12
+ # After Instantiate a Client object its possible to directly interact with the
13
+ # image server and its store backends.
14
+ #
15
+ class Client
16
+ include Visor::Common::Exception
17
+ include Visor::Common::Util
18
+
19
+ attr_reader :host, :port, :ssl, :access_key, :secret_key
20
+
21
+ # Initializes a new new VISoR Image Client.
22
+ #
23
+ # @option opts [String] :host (DEFAULT_HOST) The host address where VISoR image server resides.
24
+ # @option opts [String] :port (DEFAULT_PORT) The host port where VISoR image server resides.
25
+ # @option opts [String] :ssl (false) If the connection should be made through HTTPS (SSL).
26
+ #
27
+ # @example Instantiate a client with default values:
28
+ # client = Visor::Image::Client.new
29
+ #
30
+ # @example Instantiate a client with custom host and port:
31
+ # client = Visor::Image::Client.new(host: '127.0.0.1', port: 3000)
32
+ #
33
+ def initialize(opts = {})
34
+ configs = Common::Config.load_config :visor_image
35
+ @host = opts[:host] || configs[:bind_host] || '0.0.0.0'
36
+ @port = opts[:port] || configs[:bind_port] || 4568
37
+ @ssl = opts[:ssl] || false
38
+ @access_key = configs[:access_key]
39
+ @secret_key = configs[:secret_key]
40
+ end
41
+
42
+ # Retrieves detailed image metadata of the image with the given id.
43
+ #
44
+ # @param id [String] The wanted image's _id.
45
+ #
46
+ # @example Retrieve the image metadata with _id value:
47
+ # # wanted image _id
48
+ # id = "5e47a41e-7b94-4f65-824e-28f94e15bc6a"
49
+ # # ask for that image metadata
50
+ # client.head_image(id)
51
+ #
52
+ # # return example:
53
+ # {
54
+ # :_id => "2cceffc6-ebc5-4741-9653-745524e7ac30",
55
+ # :name => "Ubuntu 10.10",
56
+ # :architecture => "x86_64",
57
+ # :access => "public",
58
+ # :uri => "http://0.0.0.0:4567/images/2cceffc6-ebc5-4741-9653-745524e7ac30",
59
+ # :format => "iso",
60
+ # :status => "available",
61
+ # :store => "file"
62
+ # }
63
+ #
64
+ # @return [Hash] The requested image metadata.
65
+ #
66
+ # @raise [NotFound] If image not found.
67
+ # @raise [InternalError] On internal server error.
68
+ #
69
+ def head_image(id)
70
+ path = "/images/#{id}"
71
+ req = Net::HTTP::Head.new(path)
72
+ res = do_request(req, false)
73
+ pull_meta_from_headers(res)
74
+ end
75
+
76
+ # Retrieves brief metadata of all public images.
77
+ # Options for filtering the returned results can be passed in.
78
+ #
79
+ # @option query [String] :attribute The image attribute value to filter returned results.
80
+ # @option query [String] :sort ("_id") The image attribute to sort returned results.
81
+ # @option query [String] :dir ("asc") The direction to sort results ("asc"/"desc").
82
+ #
83
+ # @example Retrieve all public images brief metadata:
84
+ # client.get_images
85
+ #
86
+ # # returns:
87
+ # [<all images brief metadata>]
88
+ #
89
+ # @example Retrieve all public 32bit images brief metadata:
90
+ # client.get_images(architecture: 'i386')
91
+ #
92
+ # # returns something like:
93
+ # [
94
+ # {:_id => "28f94e15...", :architecture => "i386", :name => "Fedora 16"},
95
+ # {:_id => "8cb55bb6...", :architecture => "i386", :name => "Ubuntu 11.10 Desktop"}
96
+ # ]
97
+ #
98
+ # @example Retrieve all public 64bit images brief metadata, descending sorted by their name:
99
+ # client.get_images(architecture: 'x86_64', sort: 'name', dir: 'desc')
100
+ #
101
+ # # returns something like:
102
+ # [
103
+ # {:_id => "5e47a41e...", :architecture => "x86_64", :name => "Ubuntu 10.04 Server"},
104
+ # {:_id => "069320f0...", :architecture => "x86_64", :name => "CentOS 6"}
105
+ # ]
106
+ #
107
+ # @return [Array] All public images brief metadata.
108
+ # Just {Visor::Meta::Backends::Base::BRIEF BRIEF} fields are returned.
109
+ #
110
+ # @raise [NotFound] If there are no public images registered on the server.
111
+ # @raise [InternalError] On internal server error.
112
+ #
113
+ def get_images(query = {})
114
+ str = build_query(query)
115
+ req = Net::HTTP::Get.new("/images#{str}")
116
+ do_request(req)
117
+ end
118
+
119
+ # Retrieves detailed metadata of all public images.
120
+ #
121
+ # @note Filtering and querying works the same as with {#get_images}. The only difference is the number
122
+ # of disclosed attributes.
123
+ #
124
+ # @option query [String] :attribute_name The image attribute value to filter returned results.
125
+ # @option query [String] :sort ("_id") The image attribute to sort returned results.
126
+ # @option query [String] :dir ("asc") The direction to sort results ("asc"/"desc").
127
+ #
128
+ # @example Retrieve all public images detailed metadata:
129
+ # # request for it
130
+ # client.get_images_detail
131
+ # # returns an array of hashes with all public images metadata.
132
+ #
133
+ # @return [Array] All public images detailed metadata.
134
+ # The {Visor::Meta::Backends::Base::DETAIL_EXC DETAIL_EXC} fields are excluded from results.
135
+ #
136
+ # @raise [NotFound] If there are no public images registered on the server.
137
+ # @raise [InternalError] On internal server error.
138
+ #
139
+ def get_images_detail(query = {})
140
+ str = build_query(query)
141
+ req = Net::HTTP::Get.new("/images/detail#{str}")
142
+ do_request(req)
143
+ end
144
+
145
+ # Retrieves the file of the image with the given id.
146
+ #
147
+ # The file is yielded in streaming chunks, so its possible to receive a big file
148
+ # without buffering it all in memory.
149
+ #
150
+ # @param id [String] The wanted image's _id.
151
+ #
152
+ # @example Retrieve the image file with _id value:
153
+ # # wanted image _id
154
+ # id = "5e47a41e-7b94-4f65-824e-28f94e15bc6a"
155
+ # # ask for that image file
156
+ # client.get_image(id) do |chunk|
157
+ # # do something with chunks as they arrive here (e.g. write to file, etc)
158
+ # end
159
+ #
160
+ # @return [Binary] The requested image file binary data.
161
+ #
162
+ # @raise [NotFound] If image not found.
163
+ # @raise [InternalError] On internal server error.
164
+ #
165
+ def get_image(id)
166
+ req = Net::HTTP::Get.new("/images/#{id}")
167
+ prepare_headers(req)
168
+
169
+ Net::HTTP.start(host, port) do |http|
170
+ http.request(req) do |res|
171
+ assert_response(res)
172
+ res.read_body { |chunk| yield chunk }
173
+ end
174
+ end
175
+ end
176
+
177
+ # Register a new image on the server with the given metadata and optionally
178
+ # upload its file, or provide a :location parameter containing the full path to
179
+ # the already existing image file, stored somewhere.
180
+ #
181
+ # The image file is streamed to the server in chunks, which in turn also buffers chunks
182
+ # as they arrive, avoiding buffering large files in memory in both clients and server.
183
+ #
184
+ # @note If the :location parameter is passed, you can not pass an image file
185
+ # and the other way around too.
186
+ #
187
+ # @param meta [Hash] The image metadata.
188
+ # @param file [String] The path to the image file.
189
+ #
190
+ # @example Insert a sample image metadata:
191
+ # # sample image metadata
192
+ # meta = {:name => 'example', :architecture => 'i386'}
193
+ # # insert the new image metadata
194
+ # client.post_image(meta)
195
+ #
196
+ # # returns:
197
+ # {
198
+ # :_id => "d8b36b3f-e044-4a57-88fc-27b57338be10",
199
+ # :uri => "http://0.0.0.0:4568/images/d8b36b3f-e044-4a57-88fc-27b57338be10",
200
+ # :name => "Ubuntu 10.04 Server",
201
+ # :architecture => "x86_64",
202
+ # :access => "public",
203
+ # :status => "locked",
204
+ # :created_at => "2012-02-04 16:33:27 +0000"
205
+ # }
206
+ #
207
+ # @example Insert a sample image metadata and provide the location of its file:
208
+ # # sample image poiting to the latest release of Ubuntu Server distro
209
+ # meta = {:name => 'Ubuntu Server (Latest)', :architecture => 'x86_64', :format => 'iso',
210
+ # :store => 'http', :location => 'http://www.ubuntu.com/start-download?distro=server&bits=64&release=latest'}
211
+ # # insert the new image metadata
212
+ # client.post_image(meta)
213
+ #
214
+ # # returns:
215
+ # {
216
+ # :_id => "0733827b-836d-469e-8860-b900d4dabc46",
217
+ # :uri => "http://0.0.0.0:4568/images/0733827b-836d-469e-8860-b900d4dabc46",
218
+ # :name => "Ubuntu Server (Latest)",
219
+ # :architecture => "x86_64",
220
+ # :access => "public",
221
+ # :format => "iso",
222
+ # :store => "http",
223
+ # :location => "http://www.ubuntu.com/start-download?distro=server&bits=64&release=latest",
224
+ # :status => "available",
225
+ # :size => 715436032, # it will fetch the correct remote file size
226
+ # :created_at => "2012-02-04 16:40:04 +0000",
227
+ # :updated_at => "2012-02-04 16:40:04 +0000",
228
+ # :checksum => "76264-2aa4b000-4af0618f1b180" # it will also fetch the remote file checksum or etag
229
+ # }
230
+ #
231
+ # @example Insert a sample image metadata and upload its file:
232
+ # # sample image metadata
233
+ # meta = {:name => 'Ubuntu 10.04 Server', :architecture => 'x86_64', :store => 's3', :format => 'iso'}
234
+ # # sample image file path
235
+ # file = '~/ubuntu-10.04.3-server-amd64.iso'
236
+ # # insert the new image metadata and upload file
237
+ # client.post_image(meta, file)
238
+ #
239
+ # # returns:
240
+ # {
241
+ # :_id => "8074d23e-a9c0-454d-b935-cda5f6eb1bc8",
242
+ # :uri => "http://0.0.0.0:4568/images/8074d23e-a9c0-454d-b935-cda5f6eb1bc8",
243
+ # :name => "Ubuntu 10.04 Server",
244
+ # :architecture => "x86_64",
245
+ # :access => "public",
246
+ # :format => "iso",
247
+ # :store => "file",
248
+ # :location => "s3://<access_key>:<secret_key>@s3.amazonaws.com/<bucket>/8074d23e-a9c0-454d-b935-cda5f6eb1bc8.iso",
249
+ # :status => "available",
250
+ # :size => 713529344,
251
+ # :created_at => "2012-02-04 16:29:04 +0000",
252
+ # :updated_at => "2012-02-04 16:29:04 +0000",
253
+ # :checksum => "fbd9044604120a1f6cc708048a21e066"
254
+ # }
255
+ #
256
+ # @return [Hash] The already inserted image metadata.
257
+ #
258
+ # @raise [Invalid] If image metadata validation fails.
259
+ # @raise [Invalid] If the location header is present no file content can be provided.
260
+ # @raise [Invalid] If trying to post an image file to a HTTP backend.
261
+ # @raise [Invalid] If provided store is an unsupported store backend.
262
+ # @raise [NotFound] If no image data is found at the provided location.
263
+ # @raise [ConflictError] If the provided image file already exists in the backend store.
264
+ # @raise [InternalError] On internal server error.
265
+ #
266
+ def post_image(meta, file = nil)
267
+ req = Net::HTTP::Post.new('/images')
268
+ push_meta_into_headers(meta, req)
269
+ if file
270
+ req['Content-Type'] = 'application/octet-stream'
271
+ req['Transfer-Encoding'] = 'chunked'
272
+ req.body_stream = File.open(File.expand_path file)
273
+ end
274
+ do_request(req)
275
+ end
276
+
277
+ # Updates an image record with the given metadata and optionally
278
+ # upload its file, or provide a :location parameter containing the full path to
279
+ # the already existing image file, stored somewhere.
280
+ #
281
+ # The image file is streamed to the server in chunks, which in turn also buffers chunks
282
+ # as they arrive, avoiding buffering large files in memory in both clients and server.
283
+ #
284
+ # @note Only images with status set to 'locked' or 'error' can be updated
285
+ # with an image data file.
286
+ #
287
+ # @param id [String] The image's _id which will be updated.
288
+ # @param meta [Hash] The image metadata.
289
+ # @param file [String] The path to the image file.
290
+ #
291
+ # @example Update a sample image metadata:
292
+ # # wanted image _id
293
+ # id = "2373c3e5-b302-4529-8e23-c4ffc85e7613"
294
+ # # metadata to update
295
+ # update = {:name => 'Debian 6.0', :architecture => "x86_64"}
296
+ # # update the image metadata with some new values
297
+ # client.put_image(id, update)
298
+ #
299
+ # # returns:
300
+ # {
301
+ # :_id => "2373c3e5-b302-4529-8e23-c4ffc85e7613",
302
+ # :uri => "http://0.0.0.0:4568/images/2373c3e5-b302-4529-8e23-c4ffc85e7613",
303
+ # :name => "Debian 6.0",
304
+ # :architecture => "x86_64",
305
+ # :access => "public",
306
+ # :status => "locked",
307
+ # :created_at => "2012-02-03 12:40:30 +0000",
308
+ # :updated_at => "2012-02-04 16:35:10 +0000"
309
+ # }
310
+ #
311
+ # @example Update a sample image metadata and provide the location of its file:
312
+ # # wanted image _id
313
+ # id = "2373c3e5-b302-4529-8e23-c4ffc85e7613"
314
+ # # metadata update
315
+ # update = {:format => 'iso', :store => 'file', :location => 'file:///Users/server/debian-6.0.4-amd64.iso'}
316
+ # # update the image metadata with file values
317
+ # client.put_image(id, update)
318
+ #
319
+ # # returns:
320
+ # {
321
+ # :_id => "2373c3e5-b302-4529-8e23-c4ffc85e7613",
322
+ # :uri => "http://0.0.0.0:4568/images/2373c3e5-b302-4529-8e23-c4ffc85e7613",
323
+ # :name => "Debian 6.0",
324
+ # :architecture => "x86_64",
325
+ # :access => "public",
326
+ # :status => "locked",
327
+ # :format => "iso",
328
+ # :store => "file",
329
+ # :location => "file:///Users/server/debian-6.0.4-amd64.iso",
330
+ # :status => "available",
331
+ # :size => 764529654,
332
+ # :created_at => "2012-02-03 12:40:30 +0000",
333
+ # :updated_at => "2012-02-04 16:38:55 +0000"
334
+ # }
335
+ #
336
+ # @example Update image metadata and upload its file:
337
+ # # wanted image _id
338
+ # id = "d5bebdc8-66eb-4450-b8d1-d8127f50779d"
339
+ # # metadata update
340
+ # update = {:format => 'iso', :store => 's3'}
341
+ # # sample image file path
342
+ # file = '~/CentOS-6.2-x86_64-LiveCD.iso'
343
+ # # insert the new image metadata and upload file
344
+ # client.put_image(id, meta, file)
345
+ #
346
+ # # returns:
347
+ # {
348
+ # :_id => "d5bebdc8-66eb-4450-b8d1-d8127f50779d",
349
+ # :uri => "http://0.0.0.0:4568/images/d5bebdc8-66eb-4450-b8d1-d8127f50779d",
350
+ # :name => "CentOS 6.2",
351
+ # :architecture => "x86_64",
352
+ # :access => "public",
353
+ # :format => "iso",
354
+ # :store => "s3",
355
+ # :location => "s3://<access_key>:<secret_key>@s3.amazonaws.com/<bucket>/d5bebdc8-66eb-4450-b8d1-d8127f50779d.iso",
356
+ # :status => "available",
357
+ # :size => 731906048,
358
+ # :created_at => "2012-01-20 16:29:01 +0000",
359
+ # :updated_at => "2012-02-04 16:50:12 +0000",
360
+ # :checksum => "610c0b9684dba804467514847e8a012f"
361
+ # }
362
+ #
363
+ # @return [Hash] The already inserted image metadata.
364
+ #
365
+ # @raise [Invalid] If the image metadata validation fails.
366
+ # @raise [Invalid] If no headers neither body found for update.
367
+ # @raise [Invalid] If the location header is present no file content can be provided.
368
+ # @raise [Invalid] If trying to post an image file to a HTTP backend.
369
+ # @raise [Invalid] If provided store is an unsupported store backend.
370
+ # @raise [NotFound] If no image data is found at the provided location.
371
+ # @raise [ConflictError] If trying to assign image file to a locked or uploading image.
372
+ # @raise [ConflictError] If the provided image file already exists in the backend store.
373
+ # @raise [InternalError] On internal server error.
374
+ #
375
+ def put_image(id, meta, file = nil)
376
+ req = Net::HTTP::Put.new("/images/#{id}")
377
+ push_meta_into_headers(meta, req) if meta
378
+ if file
379
+ req['Content-Type'] = 'application/octet-stream'
380
+ req['Transfer-Encoding'] = 'chunked'
381
+ req.body_stream = File.open(File.expand_path file)
382
+ end
383
+ do_request(req)
384
+ end
385
+
386
+ # Removes an image record based on its _id and returns its metadata. If the image
387
+ # have some registered image file, that file is also deleted on its source store.
388
+ #
389
+ # @param id [String] The image's _id which will be deleted.
390
+ #
391
+ # @example Delete an image metadata:
392
+ # # wanted image _id
393
+ # id = "66414330-bbb5-42be-8a0e-b336cf6665f4"
394
+ # # delete the image metadata and file
395
+ # client.delete_image(id)
396
+ #
397
+ # # returns:
398
+ # {
399
+ # :_id => "66414330-bbb5-42be-8a0e-b336cf6665f4",
400
+ # :uri => "http://0.0.0.0:4568/images/66414330-bbb5-42be-8a0e-b336cf6665f4",
401
+ # :name => "Ubuntu 11.04 Server",
402
+ # :architecture => "x86_64",
403
+ # :access => "public",
404
+ # :format => "iso",
405
+ # :store => "file",
406
+ # :location => "file:///Users/server/VMs/66414330-bbb5-42be-8a0e-b336cf6665f4.iso",
407
+ # :status => "available",
408
+ # :size => 722549344,
409
+ # :created_at => "2012-02-04 15:23:48 +0000",
410
+ # :updated_at => "2012-02-04 15:54:52 +0000",
411
+ # :accessed_at => "2012-02-04 16:02:44 +0000",
412
+ # :access_count => 26,
413
+ # :checksum => "fbd9044604120a1f6cc708048a21e066"
414
+ # }
415
+ #
416
+ # @return [Hash] The already deleted image metadata. Useful for recover on accidental delete.
417
+ #
418
+ # @raise [NotFound] If image meta or data not found.
419
+ # @raise [Forbidden] If user does not have permission to manipulate the image file.
420
+ # @raise [InternalError] On internal server error.
421
+ #
422
+ def delete_image(id)
423
+ req = Net::HTTP::Delete.new("/images/#{id}")
424
+ do_request(req)
425
+ end
426
+
427
+ def delete_by_query(query)
428
+ result = []
429
+ images = get_images(query)
430
+ images.each do |image|
431
+ req = Net::HTTP::Delete.new("/images/#{image[:_id]}")
432
+ result << do_request(req)
433
+ end
434
+ result
435
+ end
436
+
437
+
438
+ private
439
+
440
+ # Prepare headers for request
441
+ def prepare_headers(req)
442
+ sign_request(access_key, secret_key, req.method, req.path, req)
443
+ req['User-Agent'] = 'VISoR Image Server client'
444
+ req['Accept'] = "application/json"
445
+ end
446
+
447
+ # Parses the response, which is either a JSON string inside body
448
+ # or a error message passed on headers
449
+ def parse_response(res)
450
+ if res.body
451
+ result = JSON.parse(res.body, symbolize_names: true)
452
+ result[:image] || result[:images] || result[:message]
453
+ else
454
+ res['x-error-message']
455
+ end
456
+ end
457
+
458
+ # Build query string from hash
459
+ def build_query(h)
460
+ (h.nil? or h.empty?) ? '' : '?' + URI.encode_www_form(h)
461
+ end
462
+
463
+ # Assert response code and raise if necessary
464
+ def assert_response(res)
465
+ case res
466
+ when Net::HTTPNotFound then
467
+ raise NotFound, parse_response(res)
468
+ when Net::HTTPBadRequest then
469
+ raise Invalid, parse_response(res)
470
+ when Net::HTTPConflict then
471
+ raise ConflictError, parse_response(res)
472
+ when Net::HTTPForbidden then
473
+ raise Forbidden, parse_response(res)
474
+ when Net::HTTPInternalServerError then
475
+ raise InternalError, parse_response(res)
476
+ end
477
+ end
478
+
479
+ # Process requests
480
+ def do_request(req, parse=true)
481
+ prepare_headers(req)
482
+ http = Net::HTTP.new(host, port)
483
+ http.read_timeout = 600
484
+ res = http.request(req)
485
+ assert_response(res)
486
+ parse ? parse_response(res) : res
487
+ end
488
+ end
489
+ end
490
+ end