visor-meta 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+