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.
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
+