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,235 @@
1
+ require 'mysql2'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module Visor::Meta
6
+ module Backends
7
+
8
+ # The MySQL Backend for the VISoR Meta.
9
+ #
10
+ class MySQL < Base
11
+ include Visor::Common::Exception
12
+
13
+ # Connection constants
14
+ #
15
+ # Default MySQL database
16
+ DEFAULT_DB = 'visor'
17
+ # Default MySQL host address
18
+ DEFAULT_HOST = '127.0.0.1'
19
+ # Default MySQL host port
20
+ DEFAULT_PORT = 3306
21
+ # Default MySQL user
22
+ DEFAULT_USER = 'visor'
23
+ # Default MySQL password
24
+ DEFAULT_PASSWORD = 'passwd'
25
+
26
+ #CREATE DATABASE visor;
27
+ #CREATE USER 'visor'@'localhost' IDENTIFIED BY 'visor';
28
+ #SET PASSWORD FOR 'visor'@'localhost' = PASSWORD('passwd');
29
+ #GRANT ALL PRIVILEGES ON visor.* TO 'visor'@'localhost';
30
+
31
+ # Initializes a MongoDB Backend instance.
32
+ #
33
+ # @option [Hash] opts Any of the available options can be passed.
34
+ #
35
+ # @option opts [String] :uri The connection uri, if provided, no other option needs to be setted.
36
+ # @option opts [String] :db (DEFAULT_DB) The wanted database.
37
+ # @option opts [String] :host (DEFAULT_HOST) The host address.
38
+ # @option opts [Integer] :port (DEFAULT_PORT) The port to be used.
39
+ # @option opts [String] :user (DEFAULT_USER) The user to be used.
40
+ # @option opts [String] :password (DEFAULT_PASSWORD) The password to be used.
41
+ # @option opts [Object] :conn The connection pool to access database.
42
+ #
43
+ def self.connect(opts = {})
44
+ opts[:uri] ||= ''
45
+ uri = URI.parse(opts[:uri])
46
+ opts[:db] = uri.path ? uri.path.gsub('/', '') : DEFAULT_DB
47
+ opts[:host] = uri.host || DEFAULT_HOST
48
+ opts[:port] = uri.port || DEFAULT_PORT
49
+ opts[:user] = uri.user || DEFAULT_USER
50
+ opts[:password] = uri.password || DEFAULT_PASSWORD
51
+
52
+ self.new opts
53
+ end
54
+
55
+ def initialize(opts)
56
+ super opts
57
+ @conn = connection
58
+ @conn.query %[
59
+ CREATE TABLE IF NOT EXISTS `#{opts[:db]}`.`images` (
60
+ `_id` VARCHAR(45) NOT NULL ,
61
+ `uri` VARCHAR(255) NULL ,
62
+ `name` VARCHAR(45) NOT NULL ,
63
+ `architecture` VARCHAR(45) NOT NULL ,
64
+ `access` VARCHAR(45) NOT NULL ,
65
+ `type` VARCHAR(45) NULL ,
66
+ `format` VARCHAR(45) NULL ,
67
+ `store` VARCHAR(45) NULL ,
68
+ `location` VARCHAR(255) NULL ,
69
+ `kernel` VARCHAR(45) NULL ,
70
+ `ramdisk` VARCHAR(45) NULL ,
71
+ `owner` VARCHAR(45) NULL ,
72
+ `status` VARCHAR(45) NULL ,
73
+ `size` INT NULL ,
74
+ `created_at` DATETIME NULL ,
75
+ `uploaded_at` DATETIME NULL ,
76
+ `updated_at` DATETIME NULL ,
77
+ `accessed_at` DATETIME NULL ,
78
+ `access_count` INT NULL DEFAULT 0 ,
79
+ `checksum` VARCHAR(255) NULL ,
80
+ `others` VARCHAR(255) NULL,
81
+ PRIMARY KEY (`_id`) )
82
+ ENGINE = InnoDB;
83
+ ]
84
+ end
85
+
86
+ # Establishes and returns a MySQL database connection and
87
+ # creates Images table if it does not exists.
88
+ #
89
+ # @return [Mysql2::Client] It returns a database client object.
90
+ #
91
+ def connection
92
+ Mysql2::Client.new(host: @host, port: @port, database: @db,
93
+ username: @user, password: @password)
94
+ end
95
+
96
+ # Returns the requested image metadata.
97
+ #
98
+ # @param [String] id The requested image's _id.
99
+ #
100
+ # @return [Hash] The requested image metadata.
101
+ #
102
+ # @raise [NotFound] If image not found.
103
+ #
104
+ def get_image(id, pass_timestamps = false)
105
+ meta = @conn.query("SELECT * FROM images WHERE _id='#{id}'", symbolize_keys: true).first
106
+ raise NotFound, "No image found with id '#{id}'." if meta.nil?
107
+
108
+ set_protected_get(id) unless pass_timestamps
109
+
110
+ exclude(meta)
111
+ meta
112
+ end
113
+
114
+ # Returns an array with the public images metadata.
115
+ #
116
+ # @param [true, false] brief (false) If true, the returned images will
117
+ # only contain BRIEF attributes.
118
+ #
119
+ # @option [Hash] filters Image attributes for filtering the returned results.
120
+ # Besides common attributes filters, the following options can be passed to.
121
+ #
122
+ # @option opts [String] :sort (_id) The image attribute to sort returned results.
123
+ #
124
+ # @return [Array] The public images metadata.
125
+ #
126
+ # @raise [NotFound] If there is no public images.
127
+ #
128
+ def get_public_images(brief = false, filters = {})
129
+ validate_query_filters filters unless filters.empty?
130
+
131
+ sort = [(filters.delete(:sort) || '_id'), (filters.delete(:dir) || 'asc')]
132
+ filter = {access: 'public'}.merge(filters)
133
+ fields = brief ? BRIEF.join(', ') : '*'
134
+
135
+ pub = @conn.query("SELECT #{fields} FROM images WHERE #{to_sql_where(filter)}
136
+ ORDER BY #{sort[0]} #{sort[1]}", symbolize_keys: true).to_a
137
+
138
+ raise NotFound, "No public images found." if pub.empty? && filters.empty?
139
+ raise NotFound, "No public images found with given parameters." if pub.empty?
140
+ pub.each { |meta| exclude(meta) } if fields == '*'
141
+ pub
142
+ end
143
+
144
+ # Delete an image record.
145
+ #
146
+ # @param [String] id The image's _id to remove.
147
+ #
148
+ # @return [Hash] The deleted image metadata.
149
+ #
150
+ # @raise [NotFound] If image not found.
151
+ #
152
+ def delete_image(id)
153
+ meta = @conn.query("SELECT * FROM images WHERE _id='#{id}'", symbolize_keys: true).first
154
+ raise NotFound, "No image found with id '#{id}'." if meta.nil?
155
+
156
+ @conn.query "DELETE FROM images WHERE _id='#{id}'"
157
+ meta
158
+ end
159
+
160
+ # Delete all images records.
161
+ #
162
+ def delete_all!
163
+ @conn.query "DELETE FROM images"
164
+ end
165
+
166
+ # Create a new image record for the given metadata.
167
+ #
168
+ # @param [Hash] meta The metadata.
169
+ # @option [Hash] opts Any of the available options can be passed.
170
+ #
171
+ # @option opts [String] :owner (Nil) The owner of the image.
172
+ # @option opts [Integer] :size (Nil) The image file size.
173
+ #
174
+ # @return [Hash] The already inserted image metadata.
175
+ # @raise [Invalid] If image meta validation fails.
176
+ #
177
+ def post_image(meta, opts = {})
178
+ validate_data_post meta
179
+
180
+ set_protected_post meta, opts
181
+ serialize_others(meta)
182
+
183
+ keys_values = to_sql_insert(meta)
184
+ @conn.query "INSERT INTO images #{keys_values[0]} VALUES #{keys_values[1]}"
185
+ self.get_image(meta[:_id], true)
186
+ end
187
+
188
+ # Update an image's metadata.
189
+ #
190
+ # @param [String] id The image _id to update.
191
+ # @param [Hash] update The image metadata to update.
192
+ #
193
+ # @return [Hash] The updated image metadata.
194
+ # @raise [Invalid] If update metadata validation fails.
195
+ # @raise [NotFound] If image not found.
196
+ #
197
+ def put_image(id, update)
198
+ validate_data_put update
199
+ img = @conn.query("SELECT * FROM images WHERE _id='#{id}'", symbolize_keys: true).first
200
+ raise NotFound, "No image found with id '#{id}'." if img.nil?
201
+
202
+ set_protected_put update
203
+ serialize_others(update)
204
+
205
+ @conn.query "UPDATE images SET #{to_sql_update(update)} WHERE _id='#{id}'"
206
+ self.get_image(id, true)
207
+ end
208
+
209
+
210
+ private
211
+
212
+ # Excludes details that should not be disclosed on get detailed image meta.
213
+ # Also deserialize others attributes from the others column.
214
+ #
215
+ # @return [Hash] The image parameters that should not be retrieved from database.
216
+ #
217
+ def exclude(meta)
218
+ deserialize_others(meta)
219
+ DETAIL_EXC.each { |key| meta.delete(key) }
220
+ end
221
+
222
+ # Atomically set protected fields value from a get operation.
223
+ # Being them the accessed_at and access_count.
224
+ #
225
+ # @param [String] id The _id of the image being retrieved.
226
+ # @param [Mysql2::Client] conn The connection to the database.
227
+ #
228
+ def set_protected_get(id)
229
+ @conn.query "UPDATE images SET accessed_at='#{Time.now}', access_count=access_count+1 WHERE _id='#{id}'"
230
+ end
231
+
232
+ end
233
+ end
234
+ end
235
+
data/lib/meta/cli.rb ADDED
@@ -0,0 +1,333 @@
1
+ require 'open-uri'
2
+ require 'logger'
3
+ require 'optparse'
4
+ require 'fileutils'
5
+ require 'rack'
6
+
7
+ module Visor
8
+ module Meta
9
+ class CLI
10
+
11
+ attr_reader :app, :cli_name, :argv, :options,
12
+ :port, :host, :env, :command, :parser
13
+
14
+ # Available commands
15
+ COMMANDS = %w[start stop restart status clean]
16
+ # Commands that wont load options from the config file
17
+ NO_CONF_COMMANDS = %w[stop status]
18
+ # Default config files directories to look at
19
+ DEFAULT_DIR = File.expand_path('~/.visor')
20
+ # Default host address
21
+ DEFAULT_HOST = '0.0.0.0'
22
+ # Default port
23
+ DEFAULT_PORT = 4567
24
+ # Default application environment
25
+ DEFAULT_ENV = :production
26
+
27
+ # Initialize a CLI
28
+ #
29
+ def initialize(app, cli_name, argv=ARGV)
30
+ @app = app
31
+ @cli_name = cli_name
32
+ @argv = argv
33
+ @options = default_opts
34
+ @parser = parser
35
+ @command = parse!
36
+ end
37
+
38
+ def default_opts
39
+ {debug: false,
40
+ foreground: false,
41
+ no_proxy: false,
42
+ environment: DEFAULT_ENV}
43
+ end
44
+
45
+ # OptionParser parser
46
+ #
47
+ def parser
48
+ OptionParser.new do |opts|
49
+ opts.banner = "Usage: #{cli_name} [OPTIONS] COMMAND"
50
+
51
+ opts.separator ""
52
+ opts.separator "Commands:"
53
+ opts.separator " start start the server"
54
+ opts.separator " stop stop the server"
55
+ opts.separator " restart restart the server"
56
+ opts.separator " status current server status"
57
+
58
+ opts.separator ""
59
+ opts.separator "Options:"
60
+
61
+ opts.on("-c", "--config FILE", "Load a custom configuration file") do |file|
62
+ options[:config] = File.expand_path(file)
63
+ end
64
+ opts.on("-o", "--host HOST", "listen on HOST (default: #{DEFAULT_HOST})") do |host|
65
+ options[:host] = host.to_s
66
+ end
67
+ opts.on("-p", "--port PORT", "use PORT (default: #{DEFAULT_PORT})") do |port|
68
+ options[:port] = port.to_i
69
+ end
70
+ opts.on("-x", "--no-proxy", "ignore proxy settings if any") do
71
+ options[:no_proxy] = true
72
+ end
73
+ opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{DEFAULT_ENV})") do |env|
74
+ options[:environment] = env.to_sym
75
+ end
76
+ opts.on("-F", "--foreground", "don't daemonize, run in the foreground") do
77
+ options[:foreground] = true
78
+ end
79
+
80
+ opts.separator ""
81
+ opts.separator "Common options:"
82
+
83
+ opts.on_tail("-d", "--debug", "Set debugging on (with foreground only)") do
84
+ options[:debug] = true
85
+ end
86
+ opts.on_tail("-h", "--help", "Show this message") do
87
+ puts opts
88
+ exit
89
+ end
90
+ opts.on_tail('-v', '--version', "Show version") do
91
+ puts "VISoR Meta Server v#{Visor::Meta::VERSION}"
92
+ exit
93
+ end
94
+ end
95
+ end
96
+
97
+ # Parse the current shell arguments and run the command.
98
+ # Exits on error.
99
+ #
100
+ def run!
101
+ if command.nil?
102
+ abort @parser.to_s
103
+ elsif COMMANDS.include?(command)
104
+ run_command
105
+ else
106
+ abort "Unknown command: #{command}. Available commands: #{COMMANDS.join(', ')}"
107
+ end
108
+ end
109
+
110
+ # Execute the command
111
+ #
112
+ def run_command
113
+ unless NO_CONF_COMMANDS.include?(command)
114
+ @conf = load_conf_file
115
+ @host = options[:host] || @conf[:bind_host] || DEFAULT_HOST
116
+ @port = options[:port] || @conf[:bind_port] || DEFAULT_PORT
117
+ @env = options[:environment]
118
+ end
119
+
120
+ case command
121
+ when 'start' then start
122
+ when 'stop' then stop
123
+ when 'restart' then restart
124
+ when 'status' then status
125
+ else clean
126
+ end
127
+ exit 0
128
+ end
129
+
130
+ # Remove all files created by the daemon.
131
+ #
132
+ def clean
133
+ begin
134
+ FileUtils.rm(pid_file) rescue Errno::ENOENT
135
+ end
136
+ begin
137
+ FileUtils.rm(url_file) rescue Errno::ENOENT
138
+ end
139
+ put_and_log :warn, "Removed all files created by server start"
140
+ end
141
+
142
+ # Restart server
143
+ #
144
+ def restart
145
+ @restart = true
146
+ stop
147
+ sleep 0.1 while running?
148
+ start
149
+ end
150
+
151
+ # Display current server status
152
+ #
153
+ def status
154
+ if running?
155
+ STDERR.puts "#{cli_name} is running PID: #{fetch_pid} URL: #{fetch_url}"
156
+ else
157
+ STDERR.puts "#{cli_name} is not running."
158
+ end
159
+ end
160
+
161
+ # Stop the server
162
+ #
163
+ def stop
164
+ begin
165
+ pid = File.read(pid_file)
166
+ put_and_log :warn, "Stopping #{cli_name} with PID: #{pid.to_i} Signal: INT"
167
+ Process.kill(:INT, pid.to_i)
168
+ File.delete(url_file)
169
+ rescue
170
+ put_and_log :warn, "Cannot stop #{cli_name}, is it running?"
171
+ exit! 1
172
+ end
173
+ end
174
+
175
+ # Start the server
176
+ #
177
+ def start
178
+ FileUtils.mkpath(DEFAULT_DIR)
179
+ begin
180
+ is_it_running?
181
+ can_use_port?
182
+ write_url
183
+ launch!
184
+ rescue => e
185
+ put_and_log :warn, "ERROR starting #{cli_name}: #{e}"
186
+ exit! 1
187
+ end
188
+ end
189
+
190
+ # Launch the server
191
+ #
192
+ def launch!
193
+ put_and_log :info, "Starting #{cli_name} at #{host}:#{port}"
194
+ debug_settings
195
+
196
+ Rack::Server.start(app: app,
197
+ Host: host,
198
+ Port: port,
199
+ environment: get_env,
200
+ daemonize: daemonize?,
201
+ pid: pid_file)
202
+ end
203
+
204
+ protected
205
+
206
+ def is_it_running?
207
+ if files_exist?(pid_file, url_file)
208
+ if running?
209
+ put_and_log :warn, "'#{cli_name}' is already running at #{fetch_url}"
210
+ exit! 1
211
+ else
212
+ clean
213
+ end
214
+ end
215
+ end
216
+
217
+ def running?
218
+ begin
219
+ Process.kill 0, fetch_pid
220
+ true
221
+ rescue Errno::ESRCH
222
+ false
223
+ rescue Errno::EPERM
224
+ true
225
+ rescue
226
+ false
227
+ end
228
+ end
229
+
230
+ def can_use_port?
231
+ unless port_open?
232
+ put_and_log :warn, "Port #{port} already in use. Please try other."
233
+ exit! 1
234
+ end
235
+ end
236
+
237
+ def port_open?
238
+ begin
239
+ options[:no_proxy] ? open(url, proxy: nil) : open(url)
240
+ false
241
+ rescue OpenURI::HTTPError #TODO: quick-fix, try solve this
242
+ false
243
+ rescue Errno::ECONNREFUSED
244
+ true
245
+ end
246
+ end
247
+
248
+ def daemonize?
249
+ !options[:foreground]
250
+ end
251
+
252
+ def get_env
253
+ env == 'development' ? env : 'deployment'
254
+ end
255
+
256
+ def logger
257
+ @conf ||= load_conf_file
258
+ @logger ||=
259
+ begin
260
+ log = options[:foreground] ? Logger.new(STDERR) : Visor::Common::Config.build_logger(:visor_meta)
261
+ conf_level = @conf[:log_level] == 'INFO' ? 1 : 0
262
+ log.level = options[:debug] ? 0 : conf_level
263
+ log.formatter = Proc.new {|s, t, n, msg| "[#{t.strftime("%Y-%m-%d %H:%M:%S")}] #{s} - #{msg}\n"}
264
+ log
265
+ end
266
+ end
267
+
268
+ def put_and_log(level, msg)
269
+ STDERR.puts msg
270
+ logger.send level, msg
271
+ end
272
+
273
+ def parse!
274
+ parser.parse! argv
275
+ argv.shift
276
+ end
277
+
278
+ def debug_settings
279
+ logger.debug "Configurations loaded from #{@conf[:file]}:"
280
+ logger.debug "***************************************************"
281
+ @conf.each { |k, v| logger.debug "#{k}: #{v}" }
282
+ logger.debug "***************************************************"
283
+
284
+ logger.debug "Configurations passed from #{cli_name} CLI:"
285
+ logger.debug "***************************************************"
286
+ options.each { |k, v| logger.debug "#{k}: #{v}" if options[k] != default_opts[k] }
287
+ logger.debug "***************************************************"
288
+ end
289
+
290
+ def files_exist?(*files)
291
+ files.each { |file| return false unless File.exists?(File.expand_path(file)) }
292
+ true
293
+ end
294
+
295
+ def write_url
296
+ File.open(url_file, 'w') { |f| f << url }
297
+ end
298
+
299
+ def load_conf_file
300
+ Visor::Common::Config.load_config(:visor_meta, options[:config])
301
+ end
302
+
303
+ def safe_cli_name
304
+ cli_name.gsub('-', '_')
305
+ end
306
+
307
+ def fetch_pid
308
+ IO.read(pid_file).to_i
309
+ rescue
310
+ nil
311
+ end
312
+
313
+ def fetch_url
314
+ IO.read(url_file).split('//').last
315
+ rescue
316
+ nil
317
+ end
318
+
319
+ def pid_file
320
+ File.join(DEFAULT_DIR, "#{safe_cli_name}.pid")
321
+ end
322
+
323
+ def url_file
324
+ File.join(DEFAULT_DIR, "#{safe_cli_name}.url")
325
+ end
326
+
327
+ def url
328
+ "http://#{host}:#{port}"
329
+ end
330
+
331
+ end
332
+ end
333
+ end