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.
- data/bin/visor +423 -0
- data/bin/visor-image +10 -0
- data/config/server.rb +14 -0
- data/lib/image/auth.rb +147 -0
- data/lib/image/cli.rb +397 -0
- data/lib/image/client.rb +490 -0
- data/lib/image/meta.rb +219 -0
- data/lib/image/routes/delete_all_images.rb +40 -0
- data/lib/image/routes/delete_image.rb +62 -0
- data/lib/image/routes/get_image.rb +78 -0
- data/lib/image/routes/get_images.rb +54 -0
- data/lib/image/routes/get_images_detail.rb +54 -0
- data/lib/image/routes/head_image.rb +51 -0
- data/lib/image/routes/post_image.rb +189 -0
- data/lib/image/routes/put_image.rb +205 -0
- data/lib/image/server.rb +307 -0
- data/lib/image/store/cumulus.rb +126 -0
- data/lib/image/store/file_system.rb +119 -0
- data/lib/image/store/hdfs.rb +149 -0
- data/lib/image/store/http.rb +78 -0
- data/lib/image/store/lunacloud.rb +126 -0
- data/lib/image/store/s3.rb +121 -0
- data/lib/image/store/store.rb +39 -0
- data/lib/image/store/walrus.rb +130 -0
- data/lib/image/version.rb +5 -0
- data/lib/visor-image.rb +30 -0
- data/spec/lib/client_spec.rb +0 -0
- data/spec/lib/meta_spec.rb +230 -0
- data/spec/lib/routes/delete_image_spec.rb +98 -0
- data/spec/lib/routes/get_image_spec.rb +78 -0
- data/spec/lib/routes/get_images_detail_spec.rb +104 -0
- data/spec/lib/routes/get_images_spec.rb +104 -0
- data/spec/lib/routes/head_image_spec.rb +51 -0
- data/spec/lib/routes/post_image_spec.rb +112 -0
- data/spec/lib/routes/put_image_spec.rb +109 -0
- data/spec/lib/server_spec.rb +62 -0
- data/spec/lib/store/cumulus_spec.rb +0 -0
- data/spec/lib/store/file_system_spec.rb +32 -0
- data/spec/lib/store/http_spec.rb +56 -0
- data/spec/lib/store/s3_spec.rb +37 -0
- data/spec/lib/store/store_spec.rb +36 -0
- data/spec/lib/store/walrus_spec.rb +0 -0
- 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
|
data/lib/image/client.rb
ADDED
@@ -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
|