visor-meta 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/visor-meta ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # VISoR Meta command line interface script.
4
+ # Run <visor-meta -h> to get more usage help.
5
+
6
+ require File.expand_path('../../lib/visor-meta', __FILE__)
7
+
8
+ Visor::Meta::CLI.new(Visor::Meta::Server, 'visor-meta').run!
@@ -0,0 +1,302 @@
1
+ module Visor::Meta
2
+ module Backends
3
+
4
+ # This is the Base super class for all Backends. Each new backend inherits from Base,
5
+ # which contains the model and all validations for the images metadata.
6
+ #
7
+ # Implementing a new backend is as simple as create a new backend class which inherits
8
+ # from Base and them implement the specific methods for querying the underlying database.
9
+ #
10
+ class Base
11
+ # TODO validate owner user
12
+
13
+ # Keys validation
14
+ #
15
+ # Mandatory attributes
16
+ MANDATORY = [:name, :architecture]
17
+ # Read-only attributes
18
+ READONLY = [:_id, :uri, :created_at, :updated_at, :accessed_at, :access_count]
19
+ # Optional attributes
20
+ OPTIONAL = [:owner, :status, :size, :checksum, :access, :type, :format,
21
+ :uploaded_at, :store, :location, :kernel, :ramdisk]
22
+ # All attributes
23
+ ALL = MANDATORY + OPTIONAL + READONLY
24
+
25
+ # Values validation
26
+ #
27
+ # Architecture options
28
+ ARCHITECTURE = %w[i386 x86_64]
29
+ # Access options
30
+ ACCESS = %w[public private]
31
+ # Possible disk formats
32
+ FORMAT = %w[iso vhd vdi vmdk ovf ami aki ari]
33
+ # Possible types
34
+ TYPE = %w[kernel ramdisk machine]
35
+ # Possible status
36
+ STATUS = %w[locked uploading error available]
37
+ # Possible storage
38
+ STORE = %w[s3 cumulus walrus hdfs lcs http file]
39
+
40
+ # Presentation options
41
+ #
42
+ # Brief attributes used to return only brief information about images.
43
+ BRIEF = [:_id, :name, :architecture, :type, :format, :store, :size]
44
+ # Attributes to exclude from get public images requests, allowing to show other custom attributes.
45
+ DETAIL_EXC = [:accessed_at, :access_count]
46
+ # Valid parameters to filter results from requests query, add sort parameter and sort direction.
47
+ FILTERS = ALL + [:sort, :dir]
48
+
49
+ attr_reader :db, :host, :port, :user, :password, :conn
50
+
51
+ # Initializes a Backend instance.
52
+ #
53
+ # @option [Hash] opts Any of the available options can be passed.
54
+ #
55
+ # @option opts [String] :host The host address.
56
+ # @option opts [Integer] :port The port to be used.
57
+ # @option opts [String] :db The wanted database.
58
+ # @option opts [String] :user The username to be authenticate db access.
59
+ # @option opts [String] :password The password to be authenticate db access.
60
+ # @option opts [Object] :conn The connection pool to access database.
61
+ #
62
+ def initialize(opts)
63
+ @host = opts[:host]
64
+ @port = opts[:port]
65
+ @db = opts[:db]
66
+ @user = opts[:user]
67
+ @password = opts[:password]
68
+ @conn = opts[:conn]
69
+ end
70
+
71
+ # Validates the image metadata for a post operation, based on possible keys and values.
72
+ #
73
+ # @param [Hash] meta The image metadata.
74
+ #
75
+ # @raise[ArgumentError] If some of the metadata fields do not respect the
76
+ # possible values, contains any read-only or misses any mandatory field.
77
+ #
78
+ def validate_data_post(meta)
79
+ meta.assert_exclusion_keys(READONLY)
80
+ meta.assert_inclusion_keys(MANDATORY)
81
+
82
+ meta.assert_valid_values_for(:architecture, ARCHITECTURE)
83
+ meta.assert_valid_values_for(:access, ACCESS)
84
+ meta.assert_valid_values_for(:format, FORMAT)
85
+ meta.assert_valid_values_for(:type, TYPE)
86
+ meta.assert_valid_values_for(:store, STORE)
87
+
88
+ assert_ramdisk_and_kernel(meta)
89
+ end
90
+
91
+ # Validates the image metadata for a put operation, based on possible keys and values.
92
+ #
93
+ # @param [Hash] meta The image metadata.
94
+ #
95
+ # @raise[ArgumentError] If some of the metadata fields do not respect the
96
+ # possible values, contains any read-only or misses any mandatory field.
97
+ #
98
+ def validate_data_put(meta)
99
+ meta.assert_exclusion_keys(READONLY)
100
+
101
+ meta.assert_valid_values_for(:architecture, ARCHITECTURE)
102
+ meta.assert_valid_values_for(:access, ACCESS)
103
+ meta.assert_valid_values_for(:format, FORMAT)
104
+ meta.assert_valid_values_for(:type, TYPE)
105
+ meta.assert_valid_values_for(:store, STORE)
106
+
107
+ assert_ramdisk_and_kernel(meta)
108
+ end
109
+
110
+ # Validates that incoming query filters fields are valid.
111
+ #
112
+ # @param [Hash] filters The image metadata filters comming from a GET request.
113
+ #
114
+ # @raise[ArgumentError] If some of the query filter fields do not respect the
115
+ # possible values.
116
+ #
117
+ def validate_query_filters(filters)
118
+ filters.symbolize_keys!
119
+ filters.assert_valid_keys(FILTERS)
120
+
121
+ end
122
+
123
+ # Set protected fields value from a post operation.
124
+ # Being them the _id, uri, owner, size, access, status and created_at.
125
+ #
126
+ # @param [Hash] meta The image metadata.
127
+ #
128
+ # @option [Hash] opts Any of the available options can be passed.
129
+ #
130
+ # @option opts [String] :owner (Nil) The image owner.
131
+ # @option opts [String] :size (Nil) The image file size.
132
+ #
133
+ # @return [Hash] The image metadata filled with protected fields values.
134
+ #
135
+ def set_protected_post(meta, opts = {})
136
+ owner, size = opts[:owner], opts[:size]
137
+ meta.merge!(_id: SecureRandom.uuid)
138
+ meta.merge!(access: 'public') unless meta[:access]
139
+ meta.merge!(owner: owner) if owner
140
+ meta.merge!(size: size) if size
141
+ meta.merge!(created_at: Time.now, uri: build_uri(meta[:_id]), status: 'locked')
142
+ end
143
+
144
+ # Set protected fields value from a get operation.
145
+ # Being them the accessed_at and access_count.
146
+ #
147
+ # @param [Hash] meta The image metadata update.
148
+ #
149
+ # @return [Hash] The image metadata update with protected fields setted.
150
+ #
151
+ def set_protected_put(meta)
152
+ meta.merge!(updated_at: Time.now)
153
+ end
154
+
155
+ # Build an URI for the given image _id based on VISoR Image Server configuration.
156
+ #
157
+ # @param [String] id The _id of the image.
158
+ #
159
+ # @return [String] The generated URI.
160
+ #
161
+ def build_uri(id)
162
+ conf = Visor::Common::Config.load_config :visor_image
163
+ host = conf[:bind_host] || Visor::Meta::Server::DEFAULT_HOST
164
+ port = conf[:bind_port] || Visor::Meta::Server::DEFAULT_PORT
165
+ "http://#{host}:#{port}/images/#{id}"
166
+ end
167
+
168
+ # Serializes with JSON and encapsulate additional (not on the table schema) image attributes
169
+ # on the others schema field.
170
+ #
171
+ # This is used for SQL Backends, as they are not schema free.
172
+ #
173
+ # @example Instantiate a client with default values:
174
+ # # So this:
175
+ # {name: 'example', access: 'public', extra_key: 'value', another: 'value'}
176
+ # # becomes this:
177
+ # {name: "example", access: "public", others: "{\"extra_key\":\"value\",\"another\":\"value\"}"}"}
178
+ #
179
+ # @param [Hash] meta The image metadata.
180
+ #
181
+ def serialize_others(meta)
182
+ other_keys = meta.keys - ALL
183
+ unless other_keys.empty?
184
+ others = {}
185
+ other_keys.each { |key| others[key] = meta.delete(key) }
186
+ meta.merge!(others: others.to_json)
187
+ end
188
+ end
189
+
190
+ # Deserializes with JSON and decapsulate additional (not on the table schema) image attributes
191
+ # from the others schema field.
192
+ #
193
+ # This is used for SQL Backends, as they are not schema free.
194
+ #
195
+ # @example Instantiate a client with default values:
196
+ # # So this:
197
+ # {name: "example", access: "public", others: "{\"extra_key\":\"value\",\"another\":\"value\"}"}"}
198
+ # # becomes this:
199
+ # {name: 'example', access: 'public', extra_key: 'value', another: 'value'}
200
+ #
201
+ # @param [Hash] meta The image metadata.
202
+ #
203
+ def deserialize_others(meta)
204
+ if meta[:others]
205
+ others = meta.delete :others
206
+ meta.merge! JSON.parse(others, symbolize_names: true)
207
+ end
208
+ end
209
+
210
+ # Verifies if a given object is a String, a Time or a Hash.
211
+ #
212
+ # @param [Object] v The input value.
213
+ #
214
+ # @return [true, false] If the provided value is or not a String, a Time or a Hash.
215
+ #
216
+ def string_time_or_hash?(v)
217
+ v.is_a?(String) or v.is_a?(Time) or v.is_a?(Hash)
218
+ end
219
+
220
+ # Generates a compatible SQL WHERE string from a hash.
221
+ #
222
+ # @param [Hash] h The input hash.
223
+ #
224
+ # @return [String] A string as "k='v' AND k1='v1'",
225
+ # only Strings Times or Hashes values are surrounded with '<value>'.
226
+ #
227
+ def to_sql_where(h)
228
+ h.map { |k, v| string_time_or_hash?(v) ? "#{k}='#{v}'" : "#{k}=#{v}" }.join(' AND ')
229
+ end
230
+
231
+ # Generates a compatible SQL UPDATE string from a hash.
232
+ #
233
+ # @param [Hash] h The input hash.
234
+ #
235
+ # @return [String] A string as "k='v', k1='v1'",
236
+ # only Strings Times or Hashes values are surrounded with '<value>'.
237
+ #
238
+ def to_sql_update(h)
239
+ h.map { |k, v| string_time_or_hash?(v) ? "#{k}='#{v}'" : "#{k}=#{v}" }.join(', ')
240
+ end
241
+
242
+ # Generates a compatible SQL INSERT string from a hash.
243
+ #
244
+ # @param [Hash] h The input hash.
245
+ #
246
+ # @return [String] A string as "(k, k1) VALUES ('v', 'v1')",
247
+ # only Strings Times or Hashes values are surrounded with '<value>'.
248
+ #
249
+ def to_sql_insert(h)
250
+ surround = h.values.map { |v| string_time_or_hash?(v) ? "'#{v}'" : v }
251
+ %W{(#{h.keys.join(', ')}) (#{surround.join(', ')})}
252
+ end
253
+
254
+ private
255
+
256
+ # Assert that an image referenced as the corresponding kernel or ramdisk image
257
+ # is present and is a kernel or ramdisk image.
258
+ #
259
+ # A valid kernel image is an image that has its type setted to 'kernel'
260
+ # and/or its format setted to 'aki' (Amazon Kernel Image).
261
+ #
262
+ # A valid ramdisk image is an image that has its type setted to 'ramdisk'
263
+ # and/or its format setted to 'ari' (Amazon Ramdisk Image).
264
+ #
265
+ # As all backends implement the same external API, we can call here the #get_image
266
+ # without self and it will pickup the self.get_image method of the backend in use.
267
+ #
268
+ # @param [Hash] meta The image metadata.
269
+ #
270
+ # @raise[NotFound] If the referenced image is not found.
271
+ # @raise[ArgumentError] If the referenced image is not a kernel or ramdisk image.
272
+ #
273
+ def assert_ramdisk_and_kernel(meta)
274
+ if meta[:kernel]
275
+ id = meta[:kernel]
276
+ type = get_image(id).symbolize_keys[:type]
277
+ if type != 'kernel' && meta[:format] != 'aki'
278
+ raise ArgumentError, "The image with id #{id} is not a kernel image."
279
+ end
280
+ end
281
+ if meta[:ramdisk]
282
+ id = meta[:ramdisk]
283
+ type = get_image(id).symbolize_keys[:type]
284
+ if type != 'ramdisk' && meta[:format] != 'ari'
285
+ raise ArgumentError, "The image with id #{id} is not a ramdisk image."
286
+ end
287
+ end
288
+ end
289
+
290
+ # Generate a message with the possible values for a given attribute.
291
+ #
292
+ # @param [Symbol] attr The attribute.
293
+ # @param [String] value The current invalid attribute value.
294
+ #
295
+ #def invalid_options_for attr, value
296
+ # options = self.class.const_get(attr.to_s.upcase).join(', ')
297
+ # "Invalid image #{attr.to_s} '#{value}', available options:\n #{options}"
298
+ #end
299
+
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,195 @@
1
+ require 'mongo'
2
+ require 'uri'
3
+
4
+ module Visor::Meta
5
+ module Backends
6
+
7
+ # The MongoDB Backend for the VISoR Meta.
8
+ #
9
+ class MongoDB < Base
10
+
11
+ include Visor::Common::Exception
12
+
13
+ # Connection constants
14
+ #
15
+ # Default MongoDB database
16
+ DEFAULT_DB = 'visor'
17
+ # Default MongoDB host address
18
+ DEFAULT_HOST = '127.0.0.1'
19
+ # Default MongoDB host port
20
+ DEFAULT_PORT = 27017
21
+ # Default MongoDB user
22
+ DEFAULT_USER = nil
23
+ # Default MongoDB password
24
+ DEFAULT_PASSWORD = nil
25
+
26
+ # Initializes a MongoDB Backend instance.
27
+ #
28
+ # @option [Hash] opts Any of the available options can be passed.
29
+ #
30
+ # @option opts [String] :uri The connection uri, if provided, no other option needs to be setted.
31
+ # @option opts [String] :db (DEFAULT_DB) The wanted database.
32
+ # @option opts [String] :host (DEFAULT_HOST) The host address.
33
+ # @option opts [Integer] :port (DEFAULT_PORT) The port to be used.
34
+ # @option opts [String] :user (DEFAULT_USER) The user to be used.
35
+ # @option opts [String] :password (DEFAULT_PASSWORD) The password to be used.
36
+ # @option opts [Object] :conn The connection pool to access database.
37
+ #
38
+ def self.connect(opts = {})
39
+ opts[:uri] ||= ''
40
+ uri = URI.parse(opts[:uri])
41
+ opts[:db] = uri.path ? uri.path.gsub('/', '') : DEFAULT_DB
42
+ opts[:host] = uri.host || DEFAULT_HOST
43
+ opts[:port] = uri.port || DEFAULT_PORT
44
+ opts[:user] = uri.user || DEFAULT_USER
45
+ opts[:password] = uri.password || DEFAULT_PASSWORD
46
+
47
+ self.new opts
48
+ end
49
+
50
+ def initialize(opts)
51
+ super opts
52
+ @conn = connection
53
+ end
54
+
55
+ # Establishes and returns a MongoDB database connection.
56
+ #
57
+ # @return [Mongo::Collection] A MongoDB collection object.
58
+ #
59
+ def connection
60
+ db = Mongo::Connection.new(@host, @port, :pool_size => 10, :pool_timeout => 5).db(@db)
61
+ db.authenticate(@user, @password) unless @user.empty? && @password.empty?
62
+ db.collection('images')
63
+ end
64
+
65
+ # Returns the requested image metadata.
66
+ #
67
+ # @param [Integer] id The requested image's _id.
68
+ # @param [true, false] pass_timestamps If we want to pass timestamps setting step.
69
+ #
70
+ # @return [BSON::OrderedHash] The requested image metadata.
71
+ #
72
+ # @raise [NotFound] If image not found.
73
+ #
74
+ def get_image(id, pass_timestamps = false)
75
+ meta = @conn.find_one({_id: id}, fields: exclude)
76
+ raise NotFound, "No image found with id '#{id}'." if meta.nil?
77
+ set_protected_get id unless pass_timestamps
78
+ meta
79
+ end
80
+
81
+ # Returns an array with the public images metadata.
82
+ #
83
+ # @param [true, false] brief (false) If true, the returned images will
84
+ # only contain BRIEF attributes.
85
+ #
86
+ # @option [Hash] filters Image attributes for filtering the returned results.
87
+ # Besides common attributes filters, the following options can be passed to.
88
+ #
89
+ # @option opts [String] :sort (_id) The image attribute to sort returned results.
90
+ #
91
+ # @return [Array] The public images metadata.
92
+ #
93
+ # @raise [NotFound] If there is no public images.
94
+ #
95
+ def get_public_images(brief = false, filters = {})
96
+ validate_query_filters filters unless filters.empty?
97
+
98
+ sort = [(filters.delete(:sort) || '_id'), (filters.delete(:dir) || 'asc')]
99
+ filters.merge!({access: 'public'}) unless filters[:owner]
100
+ fields = brief ? BRIEF : exclude
101
+
102
+ pub = @conn.find(filters, fields: fields, sort: sort).to_a
103
+
104
+ raise NotFound, "No public images found." if pub.empty? && filters.empty?
105
+ raise NotFound, "No public images found with given parameters." if pub.empty?
106
+
107
+ pub
108
+ end
109
+
110
+ # Delete an image record.
111
+ #
112
+ # @param [Integer] id The image's _id to remove.
113
+ #
114
+ # @return [BSON::OrderedHash] The deleted image metadata.
115
+ #
116
+ # @raise [NotFound] If image not found.
117
+ #
118
+ def delete_image(id)
119
+ img = @conn.find_one({_id: id})
120
+ raise NotFound, "No image found with id '#{id}'." unless img
121
+
122
+ @conn.remove({_id: id})
123
+ img
124
+ end
125
+
126
+ # Delete all images records.
127
+ #
128
+ def delete_all!
129
+ @conn.remove
130
+ end
131
+
132
+ # Create a new image record for the given metadata.
133
+ #
134
+ # @param [Hash] meta The image metadata.
135
+ # @option [Hash] opts Any of the available options can be passed.
136
+ #
137
+ # @option opts [String] :owner (Nil) The owner of the image.
138
+ # @option opts [Integer] :size (Nil) The image file size.
139
+ #
140
+ # @return [BSON::OrderedHash] The already added image metadata.
141
+ # @raise [Invalid] If image meta validation fails.
142
+ #
143
+ def post_image(meta, opts = {})
144
+ validate_data_post meta
145
+ set_protected_post meta, opts
146
+ id = @conn.insert(meta)
147
+ self.get_image(id, true)
148
+ end
149
+
150
+ # Update an image's metadata.
151
+ #
152
+ # @param [Integer] id The image _id to update.
153
+ # @param [Hash] update The image metadata to update.
154
+ #
155
+ # @return [BSON::OrderedHash] The updated image metadata.
156
+ # @raise [Invalid] If update metadata validation fails.
157
+ # @raise [NotFound] If image not found.
158
+ #
159
+ def put_image(id, update)
160
+ validate_data_put update
161
+
162
+ img = @conn.find_one({_id: id})
163
+ raise NotFound, "No image found with id '#{id}'." unless img
164
+
165
+ set_protected_put update
166
+ @conn.update({_id: id}, :$set => update)
167
+ self.get_image(id, true)
168
+ end
169
+
170
+
171
+ private
172
+
173
+ # Retrieve a hash with all fields that should not be retrieved from database.
174
+ # This sets each key to 0, so Mongo ignores each one of the keys present in it.
175
+ #
176
+ # @return [Hash] The image parameters that should not be retrieved from database.
177
+ #
178
+ def exclude
179
+ DETAIL_EXC.inject({}) { |h, v| h[v] = 0; h }
180
+ end
181
+
182
+ # Atomically set protected fields value from a get operation.
183
+ # Being them the accessed_at and access_count.
184
+ #
185
+ # @param [String] id The _id of the image being retrieved.
186
+ #
187
+ def set_protected_get(id)
188
+ @conn.update({_id: id}, :$set => {accessed_at: Time.now}, :$inc => {access_count: 1})
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+
195
+