rise-mirror_manager 0.1.0

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