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