rise-mirror_manager 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module Rise
2
+ module MirrorManager
3
+ class << self
4
+ attr_accessor :configuration
5
+ end
6
+
7
+ def self.configure
8
+ self.configuration ||= Configuration.new
9
+ yield(configuration)
10
+ end
11
+
12
+ # Class containing application configuration
13
+ class Configuration
14
+ attr_accessor :log_dir, :admin_email, :mirror_data_file, :database, :slack_webhook
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ require 'sqlite3'
2
+
3
+ module Rise
4
+ module MirrorManager
5
+ # Module for database interaction
6
+ module Database
7
+ DB = SQLite3::Database.open(Rise::MirrorManager.configuration.database, results_as_hash: true)
8
+
9
+ def get_sync_status(mirror_name)
10
+ columns = %w[sync_status total_bandwidth error]
11
+ row = DB.execute("SELECT #{columns.join(',')} FROM sync_info WHERE mirror_id = (SELECT mirror_id FROM mirrors WHERE name = ?)", [mirror_name])[0] #Retrieve first row of the Array
12
+ if row.nil? || row.empty?
13
+ nil
14
+ else
15
+ return row.select { |key, _| columns.include?(key.to_s) }
16
+ end
17
+ end
18
+
19
+ def update_sync_status(mirror_name:, status:, total_bandwidth: nil, error: nil)
20
+ mirror_id = retrieve_mirror_id(mirror_name)
21
+ if sync_info_exists?(mirror_id)
22
+ DB.execute 'UPDATE sync_info SET sync_status = ?, total_bandwidth = ?, error = ? WHERE mirror_id = ?', [status, total_bandwidth, error, mirror_id]
23
+ else
24
+ DB.execute 'INSERT INTO sync_info (mirror_id, sync_status, total_bandwidth, error) VALUES (?, ?, ?, ?)', [mirror_id, status, total_bandwidth, error]
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def retrieve_mirror_id(mirror_name)
31
+ row = (DB.execute 'SELECT mirror_id FROM mirrors WHERE name = ?', [mirror_name])[0]
32
+ if row.nil? || row.empty?
33
+ raise "#{mirror_name} not in database."
34
+ else
35
+ row["mirror_id"]
36
+ end
37
+ end
38
+
39
+ def sync_info_exists?(mirror_id)
40
+ sync_info = DB.execute 'SELECT sync_info_id FROM sync_info WHERE mirror_id = ?', [mirror_id]
41
+ if sync_info.nil? || sync_info.empty?
42
+ false
43
+ else
44
+ true
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'custom_error'
2
+
3
+ module Rise
4
+ module MirrorManager
5
+ module Error
6
+ # Custom exception class for cmd failures
7
+ class CmdError < CustomError
8
+ def initialize(msg, original = $ERROR_INFO)
9
+ super(:cmd_error, msg)
10
+ @original = original
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Rise
2
+ module MirrorManager
3
+ module Error
4
+ # Class for RISE custom error exception
5
+ class CustomError < StandardError
6
+ attr_reader :status, :error, :message
7
+
8
+ def initialize(error = nil, msg = nil)
9
+ @error = error || :generic_error
10
+ @message = msg || 'Something went wrong'
11
+ end
12
+
13
+ def log_error
14
+ # log error
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module Rise
2
+ module MirrorManager
3
+ module Error
4
+ module ErrorHandler
5
+
6
+ def self.included(clazz)
7
+ clazz.class_eval do
8
+ rescue_from CmdError do |e|
9
+ #log error
10
+ end
11
+ rescue_from StandardError do |e|
12
+ #log error
13
+ end
14
+
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+
@@ -0,0 +1,14 @@
1
+ require_relative 'custom_error'
2
+
3
+ module Rise
4
+ module MirrorManager
5
+ module Error
6
+ class LockError < CustomError
7
+ def initialize()
8
+ super(:lock_error,
9
+
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,60 @@
1
+ require_relative 'configuration'
2
+ require 'logger'
3
+
4
+ module Rise
5
+ module MirrorManager
6
+ # Module containing logging functionalities
7
+ module Logging
8
+ class << self
9
+ attr_reader :app_logger
10
+ end
11
+
12
+ def self.setup_logger
13
+ log_file = File.new(File.join(Rise::MirrorManager.configuration.log_dir, 'mirror_manager.log'), File::CREAT | File::WRONLY | File::APPEND)
14
+ log_file.sync = true
15
+ app_logger = Logger.new(log_file, 'weekly',
16
+ formatter: proc { |severity, datetime, _progname, msg|
17
+ "[#{Process.pid}][#{datetime}] #{severity} -- #{msg}\n"
18
+ })
19
+ end
20
+
21
+ # class IOToLog < IO
22
+ # def initialize(logger)
23
+ # @logger = logger
24
+ # end
25
+
26
+ # def write(message)
27
+ # @logger.error message
28
+ # end
29
+ # end
30
+
31
+ # Logger object for individual sync jobs
32
+ class SyncLog
33
+ def initialize(filename)
34
+ log_file = File.open(File.join(Rise::MirrorManager.configuration.log_dir,
35
+ "#{filename}.log"), File::CREAT | File::WRONLY | File::APPEND)
36
+ log_file.sync = true # flush logs in realtime
37
+ @logger = Logger.new(log_file, 'weekly', formatter: proc { |severity, datetime, _progname, msg|
38
+ "[#{Process.pid}:#{Thread.current.object_id}][#{datetime}] #{severity} -- #{msg}\n"
39
+ })
40
+ end
41
+
42
+ def log_info(msg)
43
+ logger.info msg
44
+ end
45
+
46
+ def log_warn(msg)
47
+ logger.warn msg
48
+ end
49
+
50
+ def log_error(msg)
51
+ logger.error msg
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :logger
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'configuration'
2
+ require_relative 'logging'
3
+ require 'ostruct'
4
+ require 'inifile'
5
+
6
+ module Rise
7
+ module MirrorManager
8
+ # Module for Mirror data
9
+ module Mirror
10
+ def self.retrieve_mirrors(interval)
11
+ mirrors = extract_mirrors(Rise::MirrorManager.configuration.mirror_data_file, interval)
12
+ if mirrors.empty?
13
+ raise "No mirror was retrieved for interval: #{interval}."
14
+ end
15
+ mirrors
16
+ end
17
+
18
+ def self.extract_mirrors(mirror_data_file, interval)
19
+ data_file = IniFile.load(mirror_data_file)
20
+ if data_file.nil?
21
+ raise "#{mirror_data_file} not found."
22
+ end
23
+ mirrors = []
24
+ data_file.each_section do |section|
25
+ next if data_file[section]['INTERVAL'] != interval
26
+ mirror = OpenStruct.new(name: data_file[section]['NAME'])
27
+ if data_file[section]['CUSTOM_SCRIPT']
28
+ mirror[:custom_script] = data_file[section]['CUSTOM_SCRIPT']
29
+ else
30
+ mirror[:remote_source] = data_file[section]['REMOTE_SOURCE']
31
+ mirror[:local_dir] = data_file[section]['LOCAL_DIR']
32
+ mirror[:rsync_opts] = data_file[section]['RSYNC_OPTS']
33
+ mirror[:rsync_del_opts] = data_file[section]['RSYNC_DEL_OPTS']
34
+ mirror[:exclude] = data_file[section]['EXCLUDE']
35
+ mirror[:include] = data_file[section]['INCLUDE']
36
+ mirror[:filter] = data_file[section]['FILTER']
37
+ mirror[:precmd] = data_file[section]['PRECMD']
38
+ mirror[:postcmd] = data_file[section]['POSTCMD']
39
+ mirror[:depth] = data_file[section]['DEPTH']
40
+ end
41
+ mirrors << mirror
42
+ end
43
+ mirrors
44
+ end end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'configuration'
2
+ require 'pony'
3
+ require 'slack/incoming/webhooks'
4
+
5
+ module Rise
6
+ module MirrorManager
7
+ # Module containing notification functionalities
8
+ module Notification
9
+ SLACK = Slack::Incoming::Webhooks.new Rise::MirrorManager.configuration.slack_webhook
10
+
11
+ def send_email(msg)
12
+ Pony.mail({
13
+ :to => Rise::MirrorManager.configuration.admin_email,
14
+ :from => "MirrorManager@#{Socket.gethostname}",
15
+ :via => :smtp,
16
+ :via_options => {
17
+ :address => 'aspmx.l.google.com',
18
+ :port => 25,
19
+ :domain => Socket.gethostname
20
+ },
21
+ :subject => "[#{Socket.gethostname}]MirrorManager Notification @ #{(Time.now).strftime("[%Y-%m-%d]%A, %H:%M")}",
22
+ :body => msg
23
+ })
24
+ end
25
+
26
+ def send_slack_message(mirror_name:, error:, runtime:)
27
+ attachment = {
28
+ fallback: "#{mirror_name} has failed syncing @ #{runtime}. Error: #{error}",
29
+ color: "#FF0000",
30
+ "fields": [
31
+ {
32
+ "title": "#{mirror_name} Failed",
33
+ "value": "#{error}"
34
+ }
35
+ ],
36
+ "footer": "#{runtime}"
37
+ }
38
+
39
+ SLACK.post '', attachments: [attachment]
40
+ puts 'hmmm'
41
+ # https://hooks.slack.com/services/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
42
+ # TODO: IMplement
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,196 @@
1
+ # require File.join(__dir__, '../../../config/initializers/production')
2
+ require_relative 'error/cmd_error'
3
+ require_relative 'logging'
4
+ require_relative 'database'
5
+ require_relative 'sync_status'
6
+ require 'marz/rsync'
7
+ require 'English'
8
+ require 'tmpdir'
9
+ require 'ostruct'
10
+ require 'open3'
11
+
12
+ module Rise
13
+ module MirrorManager
14
+ # This will create an instance
15
+ # for every mirror rsync job
16
+ class Sync
17
+ include Rise::MirrorManager::Error
18
+ include Rise::MirrorManager::Logging
19
+ include Rise::MirrorManager::SyncStatus
20
+ include Rise::MirrorManager::Database
21
+
22
+ DEFAULT_RSYNC_OPTIONS = '-avzuH --no-motd --safe-links --numeric-ids' \
23
+ ' --delay-updates --timeout=600 --contimeout=300'.freeze
24
+ DEFAULT_RSYNC_DELETE_OPTIONS = '--delete --delete-delay'.freeze
25
+
26
+ def self.run(mirror)
27
+ Sync.new(mirror).run
28
+ end
29
+
30
+ def run
31
+ err = catch :failure do
32
+ if mirror.custom_script
33
+ execute_cmd(mirror.custom_script) { update_sync_status(mirror_name: mirror.name, status: SUCCESS) }
34
+ # execute_custom_script(mirror.custom_script)
35
+ else
36
+ begin
37
+ # err = catch :failure do
38
+ @rsync_opts = build_rsync_opts(mirror.rsync_opts, mirror.rsync_delete_opts)
39
+ # Can't capture the error `File.join(mirror.local_dir, "#{mirror.name}.lock")` when mirror.local_dir == nil
40
+ @lock_file = File.join(mirror.local_dir, "#{mirror.name}.lock")
41
+ establish_lock
42
+ # execute_precmd(mirror.pre_cmd) if mirror.pre_cmd
43
+ execute_cmd(mirror.pre_cmd) if mirror.pre_cmd
44
+ sync
45
+ # execute_postcmd(mirror.post_cmd) if mirror.post_cmd
46
+ execute_cmd(mirror.post_cmd) if mirror.post_cmd
47
+ update_sync_status(mirror_name: mirror.name, status: SUCCESS, total_bandwidth: total_bandwidth)
48
+ ensure
49
+ release_lock
50
+ end
51
+ end
52
+ end
53
+ update_sync_status(mirror_name: mirror.name, status: FAILED, error_msg: err) if err
54
+ end
55
+
56
+ private
57
+
58
+ def initialize(mirror)
59
+ @mirror = mirror
60
+ @logger = SyncLog.new(mirror.name)
61
+ end
62
+
63
+
64
+ attr_reader :mirror, :lock_file, :rsync_opts, :logger, :total_bandwidth
65
+
66
+ def sync
67
+ Marz::Rsync.run(mirror.remote_source, mirror.local_dir, rsync_opts) do |result|
68
+ if result.success?
69
+ @total_bandwidth = convert_bytes(result.total_bytes_sent + result.total_bytes_received)
70
+ logger.log_info result.output
71
+ else
72
+ logger.log_error result.output
73
+ throw :failure, result.output
74
+ end
75
+ end
76
+ end
77
+
78
+ def execute_cmd(cmd)
79
+ Open3.popen2e(cmd.to_s) do |_i, oe, t|
80
+ unless t.value.success?
81
+ throw :failure, oe.read
82
+ end
83
+ end
84
+ yield if block_given?
85
+ end
86
+
87
+ # def execute_precmd(cmd)
88
+ # Open3.popen2e(cmd.to_s) do |_i, oe, t|
89
+ # unless t.value.success?
90
+ # update_precmd_result(mirror.name, FAILED)
91
+ # msg = oe.read # Can only be retrieved/read once
92
+ # logger.log_error(msg) unless msg.nil? || msg.empty?
93
+ # exit
94
+ # end
95
+ # update_precmd_result(mirror.name, SUCCESS)
96
+ # end
97
+ # rescue Errno::ENOENT => e
98
+ # update_precmd_result(mirror.name, FAILED)
99
+ # logger.log_error(e.message) unless e.message.nil? || e.message.empty?
100
+ # exit
101
+ # end
102
+
103
+ # def execute_postcmd(cmd)
104
+ # Open3.popen2e(cmd.to_s) do |_i, oe, t|
105
+ # unless t.value.success?
106
+ # update_postcmd_result(mirror.name, FAILED)
107
+ # msg = oe.read # Can only be retrieved/read once
108
+ # logger.log_error(msg) unless msg.nil? || msg.empty?
109
+ # exit
110
+ # end
111
+ # update_postcmd_result(mirror.name, SUCCESS)
112
+ # end
113
+ # rescue Errno::ENOENT => e
114
+ # update_postcmd_result(mirror.name, FAILED)
115
+ # logger.log_error(e.message) unless e.message.nil? || e.message.empty?
116
+ # exit
117
+ # end
118
+
119
+ # def execute_custom_script(cmd)
120
+ # execute_cmd(cmd) do |_oe, t|
121
+ # update_sync_status(mirror.name, SUCCESS) if t.value.success?
122
+ # end
123
+ # end
124
+
125
+ # def execute_cmd(cmd)
126
+ # Open3.popen2e(cmd.to_s) do |_i, oe, t|
127
+ # yield(oe, t) if block_given?
128
+ # process_sync_failure(oe.read) unless t.value.success?
129
+ # end
130
+ # rescue Errno::ENOENT => e
131
+ # process_sync_failure(e.message)
132
+ # end
133
+
134
+ def process_sync_failure(msg = nil)
135
+ update_sync_status(mirror.name, FAILED)
136
+ logger.log_error(msg) unless msg.nil? || msg.empty?
137
+ exit
138
+ end
139
+
140
+ def establish_lock
141
+ if File.exist?(lock_file)
142
+ acquire_lock
143
+ else
144
+ create_lock
145
+ end
146
+ end
147
+
148
+ def acquire_lock
149
+ pid = File.read(lock_file).to_i
150
+ Process.kill(0, pid)
151
+ rescue Errno::ESRCH
152
+ File.delete(lock_file) # nonexistent process
153
+ create_lock
154
+ rescue Errno::EPERM
155
+ throw :failure, "No permission/privilege to send signal to process(#{pid})"
156
+ else
157
+ throw :failure, "Cannot establish lock. Another process(#{pid}) is currently running and holding the lock."
158
+ end
159
+
160
+ def create_lock
161
+ File.open(lock_file, File::RDWR | File::CREAT, 0o644) do |f|
162
+ f.flock(File::LOCK_EX)
163
+ f.write(Process.pid.to_s)
164
+ f.flush
165
+ end
166
+ end
167
+
168
+ def release_lock
169
+ File.delete(lock_file) if File.exist?(lock_file)
170
+ end
171
+
172
+ def build_rsync_opts(rsync_opts, rsync_delete_opts)
173
+ opts = rsync_opts || DEFAULT_RSYNC_OPTIONS
174
+ opts += ' ' + (rsync_delete_opts || DEFAULT_RSYNC_DELETE_OPTIONS)
175
+ opts.split(' ')
176
+ end
177
+
178
+ def convert_bytes(bytes)
179
+ bytes = Integer(bytes)
180
+ result =
181
+ if bytes <= 104_857_0
182
+ format('%.2fK', (bytes / 1024.0))
183
+ elsif bytes <= 107_373_645_5 # 0.99GB
184
+ format('%.2fM', ((bytes / 1024.0) / 1024.0))
185
+ else
186
+ format('%.2fG', (((bytes / 1024.0) / 1024.0) / 1024.0))
187
+ end
188
+ result.gsub(/(\.)0+/, '')
189
+ end
190
+
191
+ end
192
+ end
193
+ end
194
+
195
+ # mirror = OpenStruct.new(name:"Delete Me", remote_source:"/tmp/srv/mirror/delete-me-source", local_dir:"/tmp/srv/mirror/delete-me-dest", rsync_opts:'-avzuH --no-motd --safe-links --numeric-ids --delay-updates --timeout=600', rsync_del_opts:nil, exclude:nil, include:nil, filter:nil, precmd:nil, postcmd:nil, depth:1)
196
+ # Rise::MirrorManager::Sync.new(mirror).run