visor-image 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
|
6
|
+
module Visor
|
7
|
+
module Image
|
8
|
+
module Store
|
9
|
+
|
10
|
+
# The Apache Hadoop HDFS (HDFS) backend store.
|
11
|
+
#
|
12
|
+
# This class handles the management of image files located in the HDFS storage system,
|
13
|
+
# based on a URI like *hdfs://username@s<host>:<port>/<path>/<bucket>/<image>*.
|
14
|
+
#
|
15
|
+
#http://10.0.3.12:50075/webhdfs/v1/foo/1.iso?op=OPEN&user.name=hadoop&offset=0
|
16
|
+
class HDFS
|
17
|
+
include Visor::Common::Exception
|
18
|
+
|
19
|
+
CONTEXT_ROOT="webhdfs/v1"
|
20
|
+
|
21
|
+
attr_accessor :uri, :config, :username, :bucket, :file, :base, :host, :port
|
22
|
+
|
23
|
+
# Initializes a new Cumulus store client object. Cumulus credentials are loaded from the URI,
|
24
|
+
# on GET and DELETE operations, or from the configuration file for POST and PUT operation.
|
25
|
+
#
|
26
|
+
# @param [String] uri The URI of the file location.
|
27
|
+
# @param config [Hash] A set of configurations for the wanted store, loaded from
|
28
|
+
# VISoR configuration file.
|
29
|
+
#
|
30
|
+
# @return [Object] An instantiated Cumulus store object ready to use.
|
31
|
+
#
|
32
|
+
def initialize(uri, config)
|
33
|
+
@uri = URI(uri)
|
34
|
+
@config = config[:hdfs]
|
35
|
+
|
36
|
+
if @uri.scheme
|
37
|
+
@username = @uri.user
|
38
|
+
@base = @uri.path.split('/')[1..2].join('/')
|
39
|
+
@bucket = @uri.path.split('/')[3]
|
40
|
+
@file = @uri.path.split('/')[4]
|
41
|
+
@host = @uri.host
|
42
|
+
@port = @uri.port
|
43
|
+
else
|
44
|
+
@username = @config[:username]
|
45
|
+
@bucket = @config[:bucket]
|
46
|
+
@base = CONTEXT_ROOT
|
47
|
+
@host = @config[:host]
|
48
|
+
@port = @config[:port]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# Returns the image file to clients, streamed in chunks.
|
54
|
+
#
|
55
|
+
# @return [Object] Yields the file, a chunk at time.
|
56
|
+
#
|
57
|
+
def get
|
58
|
+
uri = generate_uri('op=OPEN')
|
59
|
+
# This raises cant yield from root fiber
|
60
|
+
#res = EventMachine::HttpRequest.new(uri).get
|
61
|
+
#url = URI(res.response_header['LOCATION'])
|
62
|
+
#url.hostname = host
|
63
|
+
#http = EventMachine::HttpRequest.new(url).aget
|
64
|
+
# ...
|
65
|
+
|
66
|
+
# This works, should substitute (uri).get with (url).get in down
|
67
|
+
#require "net/http"
|
68
|
+
#req = Net::HTTP::Get.new(uri.request_uri)
|
69
|
+
#res = Net::HTTP.new(uri.hostname, uri.port).request(req)
|
70
|
+
#url = URI(res['location'])
|
71
|
+
#url.hostname = host
|
72
|
+
#STDERR.puts "URL #{url}"
|
73
|
+
# ...
|
74
|
+
|
75
|
+
# This solves it by manually defining the final location (try to solve the error above)
|
76
|
+
uri.port = 50075
|
77
|
+
uri = uri.to_s + '&offset=0'
|
78
|
+
|
79
|
+
http = EventMachine::HttpRequest.new(uri).aget
|
80
|
+
finish = proc { yield nil }
|
81
|
+
http.stream { |chunk| yield chunk }
|
82
|
+
http.callback &finish
|
83
|
+
http.errback &finish
|
84
|
+
end
|
85
|
+
|
86
|
+
# Saves the image file to the its final destination, based on the temporary file
|
87
|
+
# created by the server at data reception time.
|
88
|
+
#
|
89
|
+
# @param [String] id The image id.
|
90
|
+
# @param [File] tmp_file The temporary file descriptor.
|
91
|
+
# @param [String] format The image file format.
|
92
|
+
#
|
93
|
+
# @return [String, Integer] The generated file location URI and image file size.
|
94
|
+
#
|
95
|
+
# @raise [Duplicated] If the image file already exists.
|
96
|
+
#
|
97
|
+
def save(id, tmp_file, format)
|
98
|
+
@file = "#{id}.#{format}"
|
99
|
+
uri = "hdfs://#{username}@#{host}:#{port}/#{base}/#{bucket}/#{file}"
|
100
|
+
size = tmp_file.size
|
101
|
+
|
102
|
+
path = generate_uri('op=CREATE&overwrite=true')
|
103
|
+
http = EventMachine::HttpRequest.new(path).put
|
104
|
+
location = URI(http.response_header['LOCATION'])
|
105
|
+
|
106
|
+
location.hostname = host
|
107
|
+
raise Duplicated, "The image file #{uri} already exists" if file_exists?(false)
|
108
|
+
STDERR.puts "COPYING!!"
|
109
|
+
|
110
|
+
EventMachine::HttpRequest.new(location).put :file => tmp_file.path
|
111
|
+
[uri, size]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Deletes the image file from its location.
|
115
|
+
#
|
116
|
+
# @raise [NotFound] If the image file was not found.
|
117
|
+
#
|
118
|
+
def delete
|
119
|
+
uri = generate_uri('op=DELETE&recursive=true')
|
120
|
+
EventMachine::HttpRequest.new(uri).delete
|
121
|
+
end
|
122
|
+
|
123
|
+
# Check if the image file exists.
|
124
|
+
#
|
125
|
+
# @param [True, False] raise_exc (true) If it should raise exception or return
|
126
|
+
# true/false whether the file exists or not.
|
127
|
+
#
|
128
|
+
# @return [True, False] If raise_exc is false, return true/false whether the
|
129
|
+
# file exists or not.
|
130
|
+
#
|
131
|
+
# @raise [NotFound] If the image file was not found.
|
132
|
+
#
|
133
|
+
def file_exists?(raise_exc=true)
|
134
|
+
uri = generate_uri('op=GETFILESTATUS')
|
135
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
136
|
+
res = Net::HTTP.new(uri.hostname, uri.port).request(req)
|
137
|
+
exist = res.is_a? Net::HTTPSuccess
|
138
|
+
raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
|
139
|
+
exist
|
140
|
+
end
|
141
|
+
|
142
|
+
def generate_uri(params)
|
143
|
+
URI("http://#{host}:#{port}/#{base}/#{bucket}/#{file}?#{params}&user.name=#{username}")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "em-synchrony"
|
2
|
+
require "em-synchrony/em-http"
|
3
|
+
|
4
|
+
module Visor
|
5
|
+
module Image
|
6
|
+
module Store
|
7
|
+
|
8
|
+
# The HTTP backend store.
|
9
|
+
#
|
10
|
+
# This class handles the management of image files located in a remote HTTP location,
|
11
|
+
# based on a URI like *'http://www.domain.com/path-to-image-file'*.
|
12
|
+
#
|
13
|
+
# Useful for point an image to the last release of some distro, like:
|
14
|
+
# 'http://www.ubuntu.com/start-download?distro=server&bits=64&release=latest'
|
15
|
+
#
|
16
|
+
class HTTP
|
17
|
+
include Visor::Common::Exception
|
18
|
+
|
19
|
+
attr_accessor :uri, :config
|
20
|
+
|
21
|
+
# Initializes a new HTTP store client object.
|
22
|
+
#
|
23
|
+
# @param [String] uri The URI of the file location.
|
24
|
+
# @param [Hash] config (nil) A set of configurations for the wanted store,
|
25
|
+
# loaded from VISoR configuration file.
|
26
|
+
#
|
27
|
+
# @return [Object] An instantiated HTTP store object ready to use.
|
28
|
+
#
|
29
|
+
def initialize(uri, config=nil)
|
30
|
+
@uri = uri
|
31
|
+
@config = config
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the image file to clients, streamed in chunks.
|
35
|
+
#
|
36
|
+
# @return [Object] Yields the file, a chunk at time.
|
37
|
+
#
|
38
|
+
def get
|
39
|
+
http = EventMachine::HttpRequest.new(uri).get
|
40
|
+
finish = proc { yield nil }
|
41
|
+
|
42
|
+
http.stream { |chunk| yield chunk }
|
43
|
+
http.callback &finish
|
44
|
+
http.errback &finish
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if the image file exists. This will follow redirection to a
|
48
|
+
# nested deepness of 5 levels. It will also try to follow the location header
|
49
|
+
# if any.
|
50
|
+
#
|
51
|
+
# Also, after finding the real location of the HTTP file, it will parse the file
|
52
|
+
# metadata, most properly the size and checksum, based on URL headers.
|
53
|
+
#
|
54
|
+
# @return [String] The discovered file checksum and size.
|
55
|
+
#
|
56
|
+
# @raise [NotFound] If the image file was not found.
|
57
|
+
#
|
58
|
+
def file_exists?(raise_exc=true)
|
59
|
+
http = EventMachine::HttpRequest.new(uri, connect_timeout: 2, redirects: 5).head
|
60
|
+
|
61
|
+
if location = http.response_header['LOCATION']
|
62
|
+
http = EventMachine::HttpRequest.new(location, connect_timeout: 2).head
|
63
|
+
end
|
64
|
+
|
65
|
+
exist = (http.response_header.status == 200)
|
66
|
+
length = http.response_header['CONTENT_LENGTH']
|
67
|
+
size = length.nil? ? nil : length.to_i
|
68
|
+
etag = http.response_header['ETAG']
|
69
|
+
checksum = etag.nil? ? '' : etag.gsub('"', '')
|
70
|
+
|
71
|
+
raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
|
72
|
+
[exist, size, checksum]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 's3restful'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
|
6
|
+
module Visor
|
7
|
+
module Image
|
8
|
+
module Store
|
9
|
+
|
10
|
+
# The Lunacloud backend store.
|
11
|
+
#
|
12
|
+
# This class handles the management of image files located in the Lunacloud storage system,
|
13
|
+
# based on a URI like *lunacloud://<access_key>:<secret_key>@<host>:<port>/<bucket>/<image>*.
|
14
|
+
#
|
15
|
+
class Lunacloud
|
16
|
+
include Visor::Common::Exception
|
17
|
+
|
18
|
+
attr_accessor :uri, :config, :access_key, :secret_key, :bucket, :file, :host, :port
|
19
|
+
|
20
|
+
# Initializes a new Lunacloud store client object. Lunacloud credentials are loaded from the URI,
|
21
|
+
# on GET and DELETE operations, or from the configuration file for POST and PUT operation.
|
22
|
+
#
|
23
|
+
# @param [String] uri The URI of the file location.
|
24
|
+
# @param config [Hash] A set of configurations for the wanted store, loaded from
|
25
|
+
# VISoR configuration file.
|
26
|
+
#
|
27
|
+
# @return [Object] An instantiated Lunacloud store object ready to use.
|
28
|
+
#
|
29
|
+
def initialize(uri, config)
|
30
|
+
@uri = URI(uri)
|
31
|
+
@config = config[:lunacloud]
|
32
|
+
|
33
|
+
if @uri.scheme
|
34
|
+
@access_key = @uri.user
|
35
|
+
@secret_key = @uri.password
|
36
|
+
@bucket = @uri.path.split('/')[1]
|
37
|
+
@file = @uri.path.split('/')[2]
|
38
|
+
@host = @uri.host
|
39
|
+
@port = @uri.port
|
40
|
+
else
|
41
|
+
@access_key = @config[:access_key]
|
42
|
+
@secret_key = @config[:secret_key]
|
43
|
+
@bucket = @config[:bucket]
|
44
|
+
@host = @config[:host]
|
45
|
+
@port = @config[:port]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a Happening library S3 Lunacloud compatible connection object.
|
50
|
+
#
|
51
|
+
# @return [Happening::S3::Item] A new Lunacloud connection object.
|
52
|
+
#
|
53
|
+
def connection
|
54
|
+
S3restful::S3::Item.new(bucket, file, server: host, port: port, protocol: 'http',
|
55
|
+
aws_access_key_id: access_key, aws_secret_access_key: secret_key)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the image file to clients, streamed in chunks.
|
59
|
+
#
|
60
|
+
# @return [Object] Yields the file, a chunk at time.
|
61
|
+
#
|
62
|
+
def get
|
63
|
+
s3 = connection.aget
|
64
|
+
finish = proc { yield nil }
|
65
|
+
|
66
|
+
s3.stream { |chunk| yield chunk }
|
67
|
+
s3.callback &finish
|
68
|
+
s3.errback &finish
|
69
|
+
end
|
70
|
+
|
71
|
+
# Saves the image file to the its final destination, based on the temporary file
|
72
|
+
# created by the server at data reception time.
|
73
|
+
#
|
74
|
+
# @param [String] id The image id.
|
75
|
+
# @param [File] tmp_file The temporary file descriptor.
|
76
|
+
# @param [String] format The image file format.
|
77
|
+
#
|
78
|
+
# @return [String, Integer] The generated file location URI and image file size.
|
79
|
+
#
|
80
|
+
# @raise [Duplicated] If the image file already exists.
|
81
|
+
#
|
82
|
+
def save(id, tmp_file, format)
|
83
|
+
@file = "#{id}.#{format}"
|
84
|
+
uri = "lunacloud://#{access_key}:#{secret_key}@#{host}:#{port}/#{bucket}/#{file}"
|
85
|
+
size = tmp_file.size
|
86
|
+
|
87
|
+
raise Duplicated, "The image file #{fp} already exists" if file_exists?(false)
|
88
|
+
STDERR.puts "COPYING!!"
|
89
|
+
|
90
|
+
connection.store tmp_file.path
|
91
|
+
|
92
|
+
[uri, size]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Deletes the image file from its location.
|
96
|
+
#
|
97
|
+
# @raise [NotFound] If the image file was not found.
|
98
|
+
#
|
99
|
+
def delete
|
100
|
+
connection.delete
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if the image file exists.
|
104
|
+
#
|
105
|
+
# @param [True, False] raise_exc (true) If it should raise exception or return
|
106
|
+
# true/false whether the file exists or not.
|
107
|
+
#
|
108
|
+
# @return [True, False] If raise_exc is false, return true/false whether the
|
109
|
+
# file exists or not.
|
110
|
+
#
|
111
|
+
# @raise [NotFound] If the image file was not found.
|
112
|
+
#
|
113
|
+
def file_exists?(raise_exc=true)
|
114
|
+
exist = nil
|
115
|
+
error = proc { exist = false }
|
116
|
+
success = proc { |res| exist = true if res.response_header.status == 200 }
|
117
|
+
|
118
|
+
connection.head(on_error: error, on_success: success)
|
119
|
+
raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
|
120
|
+
exist
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 's3restful'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'em-synchrony/em-http'
|
5
|
+
|
6
|
+
module Visor
|
7
|
+
module Image
|
8
|
+
module Store
|
9
|
+
|
10
|
+
# The Amazon Simple Storage (S3) backend store.
|
11
|
+
#
|
12
|
+
# This class handles the management of image files located in the S3 storage system,
|
13
|
+
# based on a URI like *s3://<access_key>:<secret_key@s3.amazonaws.com/<bucket>/<image>*.
|
14
|
+
#
|
15
|
+
class S3
|
16
|
+
include Visor::Common::Exception
|
17
|
+
|
18
|
+
attr_accessor :uri, :config, :access_key, :secret_key, :bucket, :file
|
19
|
+
|
20
|
+
# Initializes a new S3 store client object. S3 credentials are loaded from the URI,
|
21
|
+
# on GET and DELETE operations, or from the configuration file for POST and PUT operation.
|
22
|
+
#
|
23
|
+
# @param [String] uri The URI of the file location.
|
24
|
+
# @param config [Hash] A set of configurations for the wanted store, loaded from
|
25
|
+
# VISoR configuration file.
|
26
|
+
#
|
27
|
+
# @return [Object] An instantiated S3 store object ready to use.
|
28
|
+
#
|
29
|
+
def initialize(uri, config)
|
30
|
+
@uri = URI(uri)
|
31
|
+
@config = config[:s3]
|
32
|
+
|
33
|
+
if @uri.scheme
|
34
|
+
@access_key = @uri.user
|
35
|
+
@secret_key = @uri.password
|
36
|
+
@bucket = @uri.path.split('/')[1]
|
37
|
+
@file = @uri.path.split('/')[2]
|
38
|
+
else
|
39
|
+
@access_key = @config[:access_key]
|
40
|
+
@secret_key = @config[:secret_key]
|
41
|
+
@bucket = @config[:bucket]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a Happening library S3 connection object.
|
46
|
+
#
|
47
|
+
# @return [Happening::S3::Item] A new S3 connection object.
|
48
|
+
#
|
49
|
+
def connection
|
50
|
+
S3restful::S3::Item.new(bucket, file, aws_access_key_id: access_key, aws_secret_access_key: secret_key)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the image file to clients, streamed in chunks.
|
54
|
+
#
|
55
|
+
# @return [Object] Yields the file, a chunk at time.
|
56
|
+
#
|
57
|
+
def get
|
58
|
+
s3 = connection.aget
|
59
|
+
finish = proc { yield nil }
|
60
|
+
|
61
|
+
s3.stream { |chunk| yield chunk }
|
62
|
+
s3.callback &finish
|
63
|
+
s3.errback &finish
|
64
|
+
end
|
65
|
+
|
66
|
+
# Saves the image file to the its final destination, based on the temporary file
|
67
|
+
# created by the server at data reception time.
|
68
|
+
#
|
69
|
+
# @param [String] id The image id.
|
70
|
+
# @param [File] tmp_file The temporary file descriptor.
|
71
|
+
# @param [String] format The image file format.
|
72
|
+
#
|
73
|
+
# @return [String, Integer] The generated file location URI and image file size.
|
74
|
+
#
|
75
|
+
# @raise [Duplicated] If the image file already exists.
|
76
|
+
#
|
77
|
+
def save(id, tmp_file, format)
|
78
|
+
@file = "#{id}.#{format}"
|
79
|
+
uri = "s3://#{access_key}:#{secret_key}@s3.amazonaws.com/#{bucket}/#{file}"
|
80
|
+
size = tmp_file.size
|
81
|
+
|
82
|
+
raise Duplicated, "The image file #{fp} already exists" if file_exists?(false)
|
83
|
+
STDERR.puts "COPYING!!"
|
84
|
+
|
85
|
+
connection.store tmp_file.path
|
86
|
+
|
87
|
+
[uri, size]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Deletes the image file from its location.
|
91
|
+
#
|
92
|
+
# @raise [NotFound] If the image file was not found.
|
93
|
+
#
|
94
|
+
def delete
|
95
|
+
connection.delete
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check if the image file exists.
|
99
|
+
#
|
100
|
+
# @param [True, False] raise_exc (true) If it should raise exception or return
|
101
|
+
# true/false whether the file exists or not.
|
102
|
+
#
|
103
|
+
# @return [True, False] If raise_exc is false, return true/false whether the
|
104
|
+
# file exists or not.
|
105
|
+
#
|
106
|
+
# @raise [NotFound] If the image file was not found.
|
107
|
+
#
|
108
|
+
def file_exists?(raise_exc=true)
|
109
|
+
exist = nil
|
110
|
+
error = proc { exist = false }
|
111
|
+
success = proc { |res| exist = true if res.response_header.status == 200 }
|
112
|
+
|
113
|
+
connection.head(on_error: error, on_success: success)
|
114
|
+
raise NotFound, "No image file found at #{uri}" if raise_exc && !exist
|
115
|
+
exist
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|