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,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