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