visor-meta 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.
@@ -0,0 +1,313 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'json'
5
+
6
+ module Visor
7
+ module Meta
8
+
9
+ # The Client API for the VISoR Meta. This class supports all image metadata manipulation
10
+ # operations through a programmatically interface.
11
+ #
12
+ # After Instantiate a Client object its possible to directly interact with the meta server and its
13
+ # database backend.
14
+ #
15
+ class Client
16
+
17
+ include Visor::Common::Exception
18
+
19
+ configs = Common::Config.load_config :visor_meta
20
+
21
+ DEFAULT_HOST = configs[:bind_host] || '0.0.0.0'
22
+ DEFAULT_PORT = configs[:bind_port] || 4567
23
+
24
+ attr_reader :host, :port, :ssl
25
+
26
+ # Initializes a new new VISoR Meta Client.
27
+ #
28
+ # @option opts [String] :host (DEFAULT_HOST) The host address where VISoR meta server resides.
29
+ # @option opts [String] :port (DEFAULT_PORT) The host port where VISoR meta server resides.
30
+ # @option opts [String] :ssl (false) If the connection should be made through HTTPS (SSL).
31
+ #
32
+ # @example Instantiate a client with default values:
33
+ # client = Visor::Meta::Client.new
34
+ #
35
+ # @example Instantiate a client with default values and SSL enabled:
36
+ # client = Visor::Meta::Client.new(ssl: true)
37
+ #
38
+ # @example Instantiate a client with custom host and port:
39
+ # client = Visor::Meta::Client.new(host: '127.0.0.1', port: 3000)
40
+ #
41
+ def initialize(opts = {})
42
+ @host = opts[:host] || DEFAULT_HOST
43
+ @port = opts[:port] || DEFAULT_PORT
44
+ @ssl = opts[:ssl] || false
45
+ end
46
+
47
+ # Retrieves brief metadata of all public images.
48
+ # Options for filtering the returned results can be passed in.
49
+ #
50
+ # @option query [String] :attribute The image attribute value to filter returned results.
51
+ # @option query [String] :sort ("_id") The image attribute to sort returned results.
52
+ # @option query [String] :dir ("asc") The direction to sort results ("asc"/"desc").
53
+ #
54
+ # @example Retrieve all public images brief metadata:
55
+ # client.get_images
56
+ #
57
+ # # returns:
58
+ # [<all images brief metadata>]
59
+ #
60
+ # @example Retrieve all public 32bit images brief metadata:
61
+ # client.get_images(architecture: 'i386')
62
+ #
63
+ # # returns something like:
64
+ # [
65
+ # {:_id => "28f94e15...", :architecture => "i386", :name => "Fedora 16"},
66
+ # {:_id => "8cb55bb6...", :architecture => "i386", :name => "Ubuntu 11.10 Desktop"}
67
+ # ]
68
+ #
69
+ # @example Retrieve all public 64bit images brief metadata, descending sorted by their name:
70
+ # client.get_images(architecture: 'x86_64', sort: 'name', dir: 'desc')
71
+ #
72
+ # # returns something like:
73
+ # [
74
+ # {:_id => "5e47a41e...", :architecture => "x86_64", :name => "Ubuntu 10.04 Server"},
75
+ # {:_id => "069320f0...", :architecture => "x86_64", :name => "CentOS 6"}
76
+ # ]
77
+ #
78
+ # @return [Array] All public images brief metadata.
79
+ # Just {Visor::Meta::Backends::Base::BRIEF BRIEF} fields are returned.
80
+ #
81
+ # @raise [NotFound] If there are no public images registered on the server.
82
+ #
83
+ def get_images(query = {})
84
+ str = build_query(query)
85
+ request = Net::HTTP::Get.new("/images#{str}")
86
+ do_request(request)
87
+ end
88
+
89
+ # Retrieves detailed metadata of all public images.
90
+ #
91
+ # Filtering and querying works the same as with {#get_images}. The only difference is the number
92
+ # of disclosed attributes.
93
+ #
94
+ # @option query [String] :attribute The image attribute value to filter returned results.
95
+ # @option query [String] :sort ("_id") The image attribute to sort returned results.
96
+ # @option query [String] :dir ("asc") The direction to sort results ("asc"/"desc").
97
+ #
98
+ # @example Retrieve all public images detailed metadata:
99
+ # # request for it
100
+ # client.get_images_detail
101
+ # # returns an array of hashes with all public images metadata.
102
+ #
103
+ # @return [Array] All public images detailed metadata.
104
+ # The {Visor::Meta::Backends::Base::DETAIL_EXC DETAIL_EXC} fields are excluded from results.
105
+ #
106
+ # @raise [NotFound] If there are no public images registered on the server.
107
+ #
108
+ def get_images_detail(query = {})
109
+ str = build_query(query)
110
+ request = Net::HTTP::Get.new("/images/detail#{str}")
111
+ do_request(request)
112
+ end
113
+
114
+ # Retrieves detailed image metadata of the image with the given id.
115
+ #
116
+ # @param id [String] The wanted image's _id.
117
+ #
118
+ # @example Retrieve the image metadata with _id value:
119
+ # # wanted image _id
120
+ # id = "5e47a41e-7b94-4f65-824e-28f94e15bc6a"
121
+ # # ask for that image metadata
122
+ # client.get_image(id)
123
+ #
124
+ # # return example:
125
+ # {
126
+ # :_id => "2cceffc6-ebc5-4741-9653-745524e7ac30",
127
+ # :name => "Ubuntu 10.10",
128
+ # :architecture => "x86_64",
129
+ # :access => "public",
130
+ # :uri => "http://0.0.0.0:4567/images/2cceffc6-ebc5-4741-9653-745524e7ac30",
131
+ # :format => "iso",
132
+ # :status => "available",
133
+ # :store => "file"
134
+ # }
135
+ #
136
+ # @return [Hash] The requested image metadata.
137
+ #
138
+ # @raise [NotFound] If image not found.
139
+ #
140
+ def get_image(id)
141
+ request = Net::HTTP::Get.new("/images/#{id}")
142
+ do_request(request)
143
+ end
144
+
145
+ # Register a new image on the server with the given metadata and returns its metadata.
146
+ #
147
+ # @param meta [Hash] The image metadata.
148
+ #
149
+ # @example Insert a sample image metadata:
150
+ # # sample image metadata
151
+ # meta = {name: 'example', architecture: 'i386', access: 'public'}
152
+ # # insert the new image metadata
153
+ # client.post_image(meta)
154
+ #
155
+ # # returns:
156
+ # { :_id=>"2373c3e5-b302-4529-8e23-c4ffc85e7613",
157
+ # :name=>"example",
158
+ # :architecture=>"i386",
159
+ # :access=>"public",
160
+ # :uri=>"http://0.0.0.0:4567/images/2373c3e5-b302-4529-8e23-c4ffc85e7613",
161
+ # :status=>"locked",
162
+ # :created_at=>"2011-12-13 19:19:26 UTC" }
163
+ #
164
+ # @return [Hash] The already inserted image metadata.
165
+ #
166
+ # @raise [Invalid] If image meta validation fails.
167
+ #
168
+ def post_image(meta)
169
+ request = Net::HTTP::Post.new('/images')
170
+ request.body = prepare_body(meta)
171
+ do_request(request)
172
+ end
173
+
174
+ # Updates an image record with the given metadata and returns its metadata.
175
+ #
176
+ # @param id [String] The image's _id which will be updated.
177
+ # @param meta [Hash] The image metadata.
178
+ #
179
+ # @example Update a sample image metadata:
180
+ # # wanted image _id
181
+ # id = "2373c3e5-b302-4529-8e23-c4ffc85e7613"
182
+ # # update the image metadata with some new values
183
+ # client.put_image(id, name: 'update example')
184
+ #
185
+ # # returns:
186
+ # { :_id=>"2373c3e5-b302-4529-8e23-c4ffc85e7613",
187
+ # :name=>"update example",
188
+ # :architecture=>"i386",
189
+ # :access=>"public",
190
+ # :uri=>"http://0.0.0.0:4567/images/2373c3e5-b302-4529-8e23-c4ffc85e7613",
191
+ # :status=>"locked",
192
+ # :created_at=>"2011-12-13 19:19:26 UTC",
193
+ # :updated_at=>"2011-12-13 19:24:37 +0000" }
194
+ #
195
+ # @return [Hash] The already updated image metadata.
196
+ #
197
+ # @raise [Invalid] If image meta validation fails.
198
+ # @raise [NotFound] If required image was not found.
199
+ #
200
+ def put_image(id, meta)
201
+ request = Net::HTTP::Put.new("/images/#{id}")
202
+ request.body = prepare_body(meta)
203
+ do_request(request)
204
+ end
205
+
206
+ # Removes an image record based on its _id and returns its metadata.
207
+ #
208
+ # @param id [String] The image's _id which will be deleted.
209
+ #
210
+ # @example Delete an image metadata:
211
+ # # wanted image _id
212
+ # id = "2373c3e5-b302-4529-8e23-c4ffc85e7613"
213
+ # # delete the image metadata, which returns it as it was before deletion
214
+ # client.delete_image(id)
215
+ #
216
+ # @return [Hash] The already deleted image metadata. This is useful for recover on accidental delete.
217
+ #
218
+ # @raise [NotFound] If required image was not found.
219
+ #
220
+ def delete_image(id)
221
+ request = Net::HTTP::Delete.new("/images/#{id}")
222
+ do_request(request)
223
+ end
224
+
225
+ private
226
+
227
+ # Parses a response body with the JSON parser and extracts and returns a single
228
+ # key value from it if defined, otherwise returns all the body.
229
+ #
230
+ # @param key (nil) [Symbol] The hash key to extract the wanted value.
231
+ # @param response [Net::HTTPResponse] The response which contains the body to parse.
232
+ #
233
+ # @return [String, Hash] If key is provided and exists on the response body, them return
234
+ # its value, otherwise return all the body hash.
235
+ #
236
+ def parse(key=nil, response)
237
+ parsed = JSON.parse(response.body, symbolize_names: true)
238
+ key ? parsed[key] : parsed
239
+ end
240
+
241
+ # Generate a valid URI query string from key/value pairs of the given hash.
242
+ #
243
+ # @param opts [Hash] The hash with the key/value pairs to generate query from.
244
+ #
245
+ # @return [String] The generated query in the form of "?k=v&k1=v1".
246
+ #
247
+ def build_query(opts)
248
+ opts.empty? ? '' : '?' + URI.encode_www_form(opts)
249
+ end
250
+
251
+ # Fill common header keys before each request. This sets the 'User-Agent' and 'Accept'
252
+ # headers for every request and additionally sets the 'content-type' header
253
+ # for POST and PUT requests.
254
+ #
255
+ # @param request [Net::HTTPResponse] The request which will be modified in its headers.
256
+ #
257
+ def prepare_headers(request)
258
+ request['User-Agent'] = 'VISoR meta server'
259
+ request['Accept'] = 'application/json'
260
+ request['content-type'] = 'application/json' if ['POST', 'PUT'].include?(request.method)
261
+ end
262
+
263
+ # Generate a valid JSON request body for POST and PUT requests.
264
+ # It generates a JSON object encapsulated inside a :image key and then returns it.
265
+ #
266
+ # @param hash [Hash] The hash with the key/value pairs to generate a JSON object from.
267
+ #
268
+ # @return [Hash] If an :image key is already present in the hash, it just returns the plain
269
+ # JSON object, otherwise, encapsulate the hash inside a :image key and returns it.
270
+ #
271
+ def prepare_body(hash)
272
+ hash.has_key?(:image) ? meta.to_json : {image: hash}.to_json
273
+ end
274
+
275
+ # Process requests by preparing its headers, launch them and assert or raise their response.
276
+ #
277
+ # @param request [Net::HTTPResponse] The request which will be launched.
278
+ #
279
+ # @return [String, Hash] If an error is raised, then it parses and returns its message,
280
+ # otherwise it properly parse and return the response body.
281
+ #
282
+ # @raise [NotFound] If required image was not found (on a GET, PUT or DELETE request).
283
+ # @raise [Invalid] If image meta validation fails (on a POST or PUT request).
284
+ #
285
+ def do_request(request)
286
+ prepare_headers(request)
287
+ response = http_or_https.request(request)
288
+ case response
289
+ when Net::HTTPNotFound then
290
+ raise NotFound, parse(:message, response)
291
+ when Net::HTTPBadRequest then
292
+ raise Invalid, parse(:message, response)
293
+ else
294
+ parse(:image, response) or parse(:images, response)
295
+ end
296
+ end
297
+
298
+ # Generate a new HTTP or HTTPS connection based on initialization parameters.
299
+ #
300
+ # @return [Net::HTTP] A HTTP or HTTPS (not done yet) connection ready to use.
301
+ #
302
+ def http_or_https
303
+ if @ssl
304
+ #TODO: ssl connection
305
+ #https://github.com/augustl/net-http-cheat-sheet/blob/master/ssl_and_https.rb
306
+ else
307
+ Net::HTTP.new(@host, @port)
308
+ end
309
+ end
310
+
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,295 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module Visor
6
+ module Meta
7
+
8
+ # The VISoR Meta Server class. This class supports all image metadata manipulation
9
+ # operations through the VISoR REST API implemented along the following routes.
10
+ #
11
+ # After initialize the Server its possible to directly interact with the meta backend.
12
+ #
13
+ class Server < Sinatra::Base
14
+ include Visor::Common::Exception
15
+ include Visor::Common::Config
16
+
17
+ # Configuration
18
+ #
19
+ configure do
20
+ backend_map = {'mongodb' => Visor::Meta::Backends::MongoDB,
21
+ 'mysql' => Visor::Meta::Backends::MySQL}
22
+
23
+ conf = Visor::Common::Config.load_config(:visor_meta)
24
+ log = Visor::Common::Config.build_logger(:visor_meta)
25
+
26
+ DB = backend_map[conf[:backend].split(':').first].connect uri: conf[:backend]
27
+
28
+ #enable :threaded
29
+ disable :show_exceptions, :logging #, :protection
30
+
31
+ use Rack::CommonLogger, log
32
+ end
33
+
34
+ #configure :development do
35
+ #require 'sinatra/reloader'
36
+ #register Sinatra::Reloader
37
+ #end
38
+
39
+ # Helpers
40
+ #
41
+ helpers do
42
+ def json_error(code, message)
43
+ error code, {code: code, message: message}.to_json
44
+ end
45
+ end
46
+
47
+ # Filters
48
+ #
49
+ before do
50
+ @parse_opts = {symbolize_names: true}
51
+ content_type :json
52
+ end
53
+
54
+ # Routes
55
+ #
56
+
57
+ # @method get_all_brief
58
+ # @overload get '/images'
59
+ #
60
+ # Get brief information about all public images.
61
+ #
62
+ # { "images": [{
63
+ # "_id":<_id>,
64
+ # "uri":<uri>,
65
+ # "name":<name>,
66
+ # "architecture":<architecture>,
67
+ # "type":<type>,
68
+ # "format":<format>,
69
+ # "store":<type>,
70
+ # "size":<size>,
71
+ # "created_at":<creation timestamp>
72
+ # }, ...]}
73
+ #
74
+ # The following options can be passed as query parameters, plus any other additional
75
+ # image attribute not defined in the schema.
76
+ #
77
+ # @param [String] name The image name.
78
+ # @param [String] architecture The image architecture.
79
+ # @param [String] type The image type.
80
+ # @param [String] format The image format.
81
+ # @param [String] store The image store.
82
+ # @param [Fixnum] size The image size.
83
+ # @param [Date] created_at The image creation timestamp.
84
+ # @param [String] sort ('_id') The image attribute to sort results.
85
+ # @param [String] dir ('asc') The sorting order ('asc'/'desc').
86
+ #
87
+ # @return [JSON] The public images brief metadata.
88
+ #
89
+ # @raise [HTTP Error 404] If there is no public images.
90
+ #
91
+ get '/images' do
92
+ begin
93
+ images = DB.get_public_images(true, params)
94
+ {images: images}.to_json
95
+ rescue NotFound => e
96
+ json_error 404, e.message
97
+ rescue => e
98
+ json_error 500, e.message
99
+ end
100
+ end
101
+
102
+ # @method get_all_detail
103
+ # @overload get '/images/detail'
104
+ #
105
+ # Get detailed information about all public images.
106
+ #
107
+ # {"images": [{
108
+ # "_id":<_id>,
109
+ # "uri":<uri>,
110
+ # "name":<name>,
111
+ # "architecture":<architecture>,
112
+ # "access":<access>,
113
+ # "status":<status>,
114
+ # "size":<size>,
115
+ # "type":<type>,
116
+ # "format":<format>,
117
+ # "store":<store>,
118
+ # "created_at":<timestamp>
119
+ # "updated_at":<timestamp>,
120
+ # "kernel":<associated kernel>,
121
+ # "ramdisk":<associated ramdisk>,
122
+ # ...
123
+ # }, ...]}
124
+ #
125
+ # The following options can be passed as query parameters, plus any other additional
126
+ # image attribute not defined in the schema.
127
+ #
128
+ # @param [String] name The image name.
129
+ # @param [String] architecture The image architecture.
130
+ # @param [String] access The image access permission.
131
+ # @param [String] type The image type.
132
+ # @param [String] format The image format.
133
+ # @param [String] store The image store.
134
+ # @param [Fixnum] size The image size.
135
+ # @param [Date] created_at The image creation timestamp.
136
+ # @param [Date] updated_at The image update timestamp.
137
+ # @param [String] kernel The image associated kernel image's _id.
138
+ # @param [String] ramdisk The image associated kernel image's _id.
139
+ # @param [String] sort (_id) The image attribute to sort results.
140
+ # @param [String] dir ('asc') The sorting order ('asc'/'desc').
141
+ #
142
+ # @return [JSON] The public images detailed metadata.
143
+ #
144
+ # @raise [HTTP Error 404] If there is no public images.
145
+ #
146
+ get '/images/detail' do
147
+ begin
148
+ images = DB.get_public_images(false, params)
149
+ {images: images}.to_json
150
+ rescue NotFound => e
151
+ json_error 404, e.message
152
+ rescue => e
153
+ json_error 500, e.message
154
+ end
155
+ end
156
+
157
+ # @method get_detail
158
+ # @overload get '/images/:id'
159
+ #
160
+ # Get detailed information about a specific image.
161
+ #
162
+ # {"image": {
163
+ # "_id":<_id>,
164
+ # "uri":<uri>,
165
+ # "name":<name>,
166
+ # "architecture":<architecture>,
167
+ # "access":<access>,
168
+ # "status":<status>,
169
+ # "size":<size>,
170
+ # "type":<type>,
171
+ # "format":<format>,
172
+ # "store":<type>,
173
+ # "created_at":<creation timestamp>
174
+ # "updated_at":<update timestamp>,
175
+ # "kernel":<associated kernel>,
176
+ # "ramdisk":<associated ramdisk>,
177
+ # ...
178
+ # }}
179
+ #
180
+ # @param [String] id The wanted image _id.
181
+ #
182
+ # @return [JSON] The image detailed metadata.
183
+ #
184
+ # @raise [HTTP Error 404] If image not found.
185
+ #
186
+ get '/images/:id' do |id|
187
+ begin
188
+ image = DB.get_image(id)
189
+ {image: image}.to_json
190
+ rescue NotFound => e
191
+ json_error 404, e.message
192
+ rescue => e
193
+ json_error 500, e.message
194
+ end
195
+ end
196
+
197
+ # @method post
198
+ # @overload post '/images'
199
+ #
200
+ # Create a new image metadata and returns it.
201
+ #
202
+ # @param [JSON] http-body The image metadata.
203
+ #
204
+ # @return [JSON] The already created image detailed metadata.
205
+ #
206
+ # @raise [HTTP Error 400] Image metadata validation errors.
207
+ #
208
+ post '/images' do
209
+ begin
210
+ meta = JSON.parse(request.body.read, @parse_opts)
211
+ image = DB.post_image(meta[:image])
212
+ {image: image}.to_json
213
+ rescue NotFound => e
214
+ json_error 404, e.message
215
+ rescue ArgumentError => e
216
+ json_error 400, e.message
217
+ rescue => e
218
+ json_error 500, e.message
219
+ end
220
+ end
221
+
222
+ # @method put
223
+ # @overload put '/images/:id'
224
+ #
225
+ # Update an existing image metadata and return it.
226
+ #
227
+ # @param [String] id The wanted image _id.
228
+ # @param [JSON] http-body The image metadata.
229
+ #
230
+ # @return [JSON] The already updated image detailed metadata.
231
+ #
232
+ # @raise [HTTP Error 400] Image metadata update validation errors.
233
+ #
234
+ put '/images/:id' do |id|
235
+ begin
236
+ meta = JSON.parse(request.body.read, @parse_opts)
237
+ image = DB.put_image(id, meta[:image])
238
+ {image: image}.to_json
239
+ rescue NotFound => e
240
+ json_error 404, e.message
241
+ rescue ArgumentError => e
242
+ json_error 400, e.message
243
+ rescue => e
244
+ json_error 500, e.message
245
+ end
246
+ end
247
+
248
+ # @method delete
249
+ # @overload delete '/images/:id'
250
+ #
251
+ # Delete an image metadata and returns it.
252
+ #
253
+ # @param [String] id The image _id to delete.
254
+ #
255
+ # @return [JSON] The already deleted image detailed metadata.
256
+ #
257
+ # @raise [HTTP Error 404] If image not found.
258
+ #
259
+ delete '/images/:id' do
260
+ begin
261
+ image = DB.delete_image(params[:id])
262
+ {image: image}.to_json
263
+ rescue NotFound => e
264
+ json_error 404, e.message
265
+ rescue => e
266
+ json_error 500, e.message
267
+ end
268
+ end
269
+
270
+ # misc handlers: error, not_found, etc.
271
+ get "*" do
272
+ json_error 404, 'Invalid operation or path.'
273
+ end
274
+
275
+ put "*" do
276
+ json_error 404, 'Invalid operation or path.'
277
+ end
278
+
279
+ post "*" do
280
+ json_error 404, 'Invalid operation or path.'
281
+ end
282
+
283
+ delete "*" do
284
+ json_error 404, 'Invalid operation or path.'
285
+ end
286
+
287
+ error do
288
+ json_error 500, env['sinatra.error'].message
289
+ end
290
+
291
+ end
292
+ end
293
+ end
294
+
295
+
@@ -0,0 +1,5 @@
1
+ module Visor
2
+ module Meta
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
data/lib/visor-meta.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'visor-common'
2
+
3
+ $:.unshift File.expand_path('../../lib', __FILE__)
4
+ require 'meta/version'
5
+ require 'meta/backends/base'
6
+ require 'meta/backends/mongo_db'
7
+ require 'meta/backends/mysql_db'
8
+ require 'meta/server'
9
+ require 'meta/client'
10
+ require 'meta/cli'
11
+
12
+