Add backup setting feature.

This commit is contained in:
BoHung Chiu 2022-12-02 13:12:54 +08:00
parent 9592608857
commit 512970d590
14 changed files with 1041 additions and 0 deletions

View File

@ -439,6 +439,10 @@ class Admin::SitePanelController < OrbitAdminController
end
end
end
when 'apply_change_backup_setting'
Thread.new do
system("bundle exec rake exec_commands:change_backup_setting[#{params[:id]}]")
end
else
Thread.new do
cmds = params[:commands].split('////')
@ -516,6 +520,27 @@ class Admin::SitePanelController < OrbitAdminController
@site_server = SiteServer.find(params[:id])
end
def update_backup_setting
@site_server = SiteServer.find(params[:id])
site_server_params = params.require(:site_server).permit! rescue {}
need_rewrite_backup_setting = false
site_server_params["site_server_file_backups_attributes"].each do |k, v|
if v["_destroy"] == 'true'
need_rewrite_backup_setting = true
end
end if site_server_params["site_server_file_backups_attributes"]
site_server_params["site_server_db_backups_attributes"].each do |k, v|
if v["_destroy"] == 'true'
need_rewrite_backup_setting = true
end
end if site_server_params["site_server_db_backups_attributes"]
@site_server.update_attributes(site_server_params)
unless need_rewrite_backup_setting
need_rewrite_backup_setting = ((@site_server.site_server_file_backups.where(:need_rewrite=>true).count + @site_server.site_server_db_backups.where(:need_rewrite=>true).count) != 0)
end
if need_rewrite_backup_setting
@site_server.update(:need_rewrite_backup_setting=>true)
end
redirect_back_custom
end
private
def site_cert_params

View File

@ -17,7 +17,12 @@ class SiteServer
field :need_update_site_ids, type: Array, default: []
field :super_user_changed, type: Boolean, type: false
field :removed_super_users, type: Array, default: []
field :need_rewrite_backup_setting, type: Boolean, default: false
has_many :site_server_file_backups, :autosave => true, :dependent => :destroy
has_many :site_server_db_backups, :autosave => true, :dependent => :destroy
has_many :site_super_users, :autosave => true, :dependent => :destroy
accepts_nested_attributes_for :site_server_file_backups, :allow_destroy => true
accepts_nested_attributes_for :site_server_db_backups, :allow_destroy => true
accepts_nested_attributes_for :site_super_users, :allow_destroy => true
def check_super_user_changed
self.update(:super_user_changed => (self.site_super_users.where(:is_changed=>true).count != 0 || self.removed_super_users.count != 0))

View File

@ -0,0 +1,143 @@
class SiteServerDbBackup
include Mongoid::Document
include Mongoid::Timestamps
PeriodsTypes=['day','week','month']
belongs_to :site_server
field :disable, type: Boolean, default: false
field :backup_time, type: String, default: '03:00'
field :path , type: String ,default: '/home/backup/db'
field :period, type: Integer, default: 0 # 0 => daily , 1 => weekly , 2 => monthly
field :retain_count, type: Integer, default: 7
field :need_rewrite, type: Boolean, default: true
before_save do
if self.changed?
self.need_rewrite = true
end
end
def to_crontab_script(return_string=true)
# minute , hour, day(of month, 1~31), month(1~12), day(of week,0~6)
crontab_time = ['*', '*', '*', '*', '*']
hour, minute = self.backup_time.split(':').map{|s| s.to_i}
crontab_time[0] = minute.to_s rescue '0'
crontab_time[1] = hour.to_s rescue '0'
if self.period == 1 #weekly
crontab_time[4] = '0' #every Sunday
elsif self.period == 2 #monthly
crontab_time[2] = '1' #day 1 of every month
end
if self.retain_count
retain_text = "1 week ago"
if self.period != 0 || self.retain_count != 7
retain_text = "#{self.retain_count} #{PeriodsTypes[self.period]} ago"
end
scripts = ["#{crontab_time.join(' ')} mongodump -o #{self.path}/`date \"+\\%Y\\%m\\%d\"`_db"]
if hour < 23
next_hour = hour + 1
crontab_time[1] = next_hour.to_s
else
crontab_time[1] = ((hour + 1) % 24).to_s
if self.period == 1 #weekly
crontab_time[4] = '1' #every Sunday + 1 = Monday
elsif self.period == 2 #monthly
crontab_time[2] = '2' #day 1 of every month + 1 = 2
end
retain_text += " 1 day ago"
end
scripts << "#{crontab_time.join(' ')} rm -fr #{self.path}/`date --date=\"#{retain_text}\" \"+\\%Y\\%m\\%d\"`_db"
end
if return_string
scripts.join("\n")
else
scripts
end
end
def self.read_from_crontab_line(crontab_line, crontab_lines)
if crontab_line.include?('mongodump')
tmp = crontab_line.split(/\s+[^\s]*mongodump\s+/) # ex: ["0 3 * * *", "-o /home/backup/db/`date \"+\\%Y\\%m\\%d\"`_db"]
if tmp.count == 2
crontab_time = tmp[0].split(/\s+/)
path = File.dirname(tmp[1].split('-o')[1].strip) rescue nil
if path
if crontab_time[4] != '*' #weekly
period = 1
period_unit = 1.week
elsif crontab_time[2] != '*' #monthly
period = 2
period_unit = 1.month
else #daily
period = 0
period_unit = 1.day
end
time = "#{crontab_time[1].rjust(2, "0")}:#{crontab_time[0].rjust(2, "0")}"
regex_path = /\s+#{::Regexp.escape(path)}\//
match_line = crontab_lines.select{|l| l.match(/\s+(|\/usr\/bin\/)rm/) && l.match(regex_path)}.first
if match_line.nil?
retain_count = nil
else
retain_text = match_line.scan(/`\s*date\s+\-\-date=[\'\"]([^\'\"]+)[\'\"]/).flatten.first
tmp_retain_count, tmp_period = retain_text.sub(/\s+ago/, '').split(/\s+/)
tmp_retain_count = tmp_retain_count.to_i
retain_count = (tmp_retain_count.send(tmp_period) / period_unit rescue nil)
end
return [time, path, period, retain_count]
else
return nil
end
else
return nil
end
else
return nil
end
end
def self.clean_crontab_setting(crontab_lines_str)
crontab_lines = crontab_lines_str.split("\n")
related_path = []
crontab_lines.each do |crontab_line|
if crontab_line.include?('mongodump')
tmp = crontab_line.split(/\s+[^\s]*mongodump\s+/) # ex: ["0 3 * * *", "-o /home/backup/db/`date \"+\\%Y\\%m\\%d\"`_db"]
output_dir = File.dirname(tmp[1].split('-o')[1].strip) rescue nil
if output_dir
related_path << output_dir
end
end
end
related_path_reg = Regexp.union(related_path.map{|s| /\s+#{::Regexp.escape(s)}\//})
crontab_lines_str = crontab_lines.map do |crontab_line|
if crontab_line.match(related_path_reg)
nil
else
crontab_line
end
end.compact.join("\n")
end
def self.write_crontab_setting(site_server, crontab_lines_str)
new_crontab_lines = self.where(:site_server_id=> site_server.id, :disable=>false).map{|db_backup| db_backup.to_crontab_script(false)}
crontab_lines = crontab_lines_str.split("\n")
exists_indices = new_crontab_lines.map do |sub_crontab_lines|
match_reg = ::Regexp.new(sub_crontab_lines[0].gsub(/\s+/, '\s+'))
crontab_lines.index{|l| l.match(match_reg)}
end
removed_related_path = []
crontab_lines.each_with_index do |crontab_line, i|
if crontab_line.include?('mongodump') && exists_indices.exclude?(i)
tmp = crontab_line.split(/\s+[^\s]*mongodump\s+/) # ex: ["0 3 * * *", "-o /home/backup/db/`date \"+\\%Y\\%m\\%d\"`_db"]
output_dir = File.dirname(tmp[1].split('-o')[1].strip) rescue nil
if output_dir
removed_related_path << output_dir
end
end
end
removed_related_path.each_with_index do |path|
regex_path = /\s+#{::Regexp.escape(path)}\//
crontab_lines = crontab_lines.select{|l| l.match(regex_path).nil?}
end
exists_indices.each_with_index do |idx, i|
if idx.nil?
crontab_lines << new_crontab_lines[i]
end
end
crontab_lines_str = crontab_lines.compact.join("\n")
end
end

View File

@ -0,0 +1,176 @@
class SiteServerFileBackup
include Mongoid::Document
include Mongoid::Timestamps
PeriodsTypes=['daily','weekly','monthly']
DefaultConf='/etc/rsnapshot.conf'
belongs_to :site_server
field :disable, type: Boolean, default: false
field :backup_time, type: String, default: '04:15'
field :path , type: String ,default: '/home/backup/orbit'
field :period, type: Integer, default: 0 # 0 => daily , 1 => weekly , 2 => monthly
field :retain_count, type: Integer, default: 7
field :rsnapshot_conf_path, type: String, default: DefaultConf
field :backup_dir, type: String, default: '/home/{{user_name}}'
field :backup_prefix, type: String, default: 'localhost/'
field :need_rewrite, type: Boolean, default: true
before_create do
other_rsnapshot_conf_path = self.class.pluck(:rsnapshot_conf_path)
if other_rsnapshot_conf_path.include?(self.rsnapshot_conf_path)
max_postfix = other_rsnapshot_conf_path.select{|s| s.start_with?('/etc/')}.map{|s| tmp = File.basename(s).split('.conf')[0].match(/\d+/); tmp ? tmp[0].to_i : 0}.max
self.rsnapshot_conf_path = "#{self.rsnapshot_conf_path.split('.conf')[0]}#{max_postfix + 1}.conf"
end
end
before_save do
if self.changed?
self.need_rewrite = true
end
end
def to_crontab_script
# minute , hour, day(of month, 1~31), month(1~12), day(of week,0~6)
crontab_time = ['*', '*', '*', '*', '*']
time = self.backup_time.split(':').map{|s| s.sub(/0(\d)/){$1}} rescue []
crontab_time[0] = time[1].to_i.to_s rescue '0'
crontab_time[1] = time[0].to_i.to_s rescue '0'
if self.period == 1 #weekly
crontab_time[4] = '0' #every Sunday
elsif self.period == 2 #monthly
crontab_time[2] = '1' #day 1 of every month
end
extra_arg = ''
if self.rsnapshot_conf_path != DefaultConf
extra_arg = " -c #{rsnapshot_conf_path} "
end
"#{crontab_time.join(' ')} /usr/bin/rsnapshot #{extra_arg}#{period_text}"
end
def period_text
PeriodsTypes[self.period]
end
def retain_text
"retain\t#{self.period_text}\t#{self.retain_count}"
end
def override_text(contents, org_regex, new_text)
exist_flag = false
contents = contents.gsub(org_regex).with_index do |s, i|
if i == 0
exist_flag = true
"#{new_text}#{(s[-1] == "\n" ? "\n" : '')}"
else
"##{s}"
end
end
unless exist_flag
contents += "\n#{new_text}"
end
contents
end
def gsub_rsnapshot_conf(contents)
contents = self.override_text(contents, /^retain\s+[^\n]*(\n|$)/m, self.retain_text)
contents = self.override_text(contents, /^snapshot_root\s+[^\n]*(\n|$)/m, self.snapshot_root)
contents = self.override_text(contents, /^backup\s+[^\n]*(\n|$)/m, self.backup_path)
end
def backup_path
home_dir = self.backup_dir
if home_dir.include?('{{user_name}}')
user_name = self.site_server.account
home_dir = home_dir.sub('{{user_name}}', user_name)
self.backup_dir = home_dir
self.save
end
backup_text = "backup\t#{home_dir}/\t#{self.backup_prefix}"
extra_exclude_path = []
if self.class.class_variable_defined?(:@@db_backup_paths)
extra_exclude_path = @@db_backup_paths.select{|p| p.start_with?(home_dir)}
end
if self.path.start_with?(home_dir)
extra_exclude_path << self.path if extra_exclude_path.exclude?(self.path)
end
if extra_exclude_path.count != 0
tmp = []
extra_exclude_path.each do |s|
if tmp.count == 0
tmp << s
else
if tmp.exclude?(s)
s2 = File.dirname(s)
idx = tmp.index{|ss| File.dirname(ss) == s2}
if idx
tmp[idx] = s2
else
tmp << s
end
end
end
end
backup_text += "\t#{tmp.map{|p| "exclude=#{p}"}.join(',')}"
end
backup_text
end
def snapshot_root
"snapshot_root\t#{self.path}"
end
def self.read_from_crontab_line(crontab_line)
if crontab_line.include?('rsnapshot')
tmp = crontab_line.split(/\s+[^\s]*rsnapshot\s+/) # ex: ["15 4 * * *", "daily"], ["15 4 * * *", "-c /etc/rsnapshot.conf daily"]
if tmp.count == 2
crontab_time = tmp[0].split(/\s+/)
period_text = tmp[1]
rsnapshot_conf_path = DefaultConf
if period_text.start_with?('-c')
tmp2 = period_text.split(/\s+/)
if tmp2.count == 3
rsnapshot_conf_path = tmp2[1]
period_text = tmp2[2]
else
period_text = tmp2.last
end
end
if PeriodsTypes.include?(period_text)
period = PeriodsTypes.index(period_text)
else
period = 0
end
time = "#{crontab_time[1].rjust(2, "0")}:#{crontab_time[0].rjust(2, "0")}"
return [time, rsnapshot_conf_path, period]
else
return nil
end
else
return nil
end
end
def self.clean_crontab_setting(crontab_lines_str)
crontab_lines = crontab_lines_str.split("\n")
crontab_lines_str = crontab_lines.map do |crontab_line|
if crontab_line.include?('rsnapshot')
nil
else
crontab_line
end
end.compact.join("\n")
end
def self.init_class_variables(site_server)
@@db_backup_paths = SiteServerDbBackup.where(:site_server_id=> site_server.id, :disable=>false).pluck(:path)
end
def self.write_crontab_setting(site_server, crontab_lines_str)
new_crontab_lines = self.where(:site_server_id=> site_server.id, :disable=>false).flat_map{|file_backup| file_backup.to_crontab_script.split("\n")}
crontab_lines = crontab_lines_str.split("\n")
exists_indices = new_crontab_lines.map do |new_crontab_line|
match_reg = ::Regexp.new(new_crontab_line.gsub(/\s+/, '\s+'))
crontab_lines.index{|l| l.match(match_reg)}
end
crontab_lines = crontab_lines.map.with_index do |crontab_line, i|
if crontab_line.include?('rsnapshot') && exists_indices.exclude?(i)
nil
else
crontab_line
end
end
exists_indices.each_with_index do |idx, i|
if idx.nil?
crontab_lines << new_crontab_lines[i]
end
end
crontab_lines_str = crontab_lines.compact.join("\n")
end
end

View File

@ -0,0 +1,77 @@
<!-- <%= type.classify %> -->
<%
relation_field = "#{f.object_name}_#{type.pluralize}"
records = f.object.send(relation_field)
%>
<div class="tab-pane fade <%= @active_class %>" id="<%=type%>">
<!-- Add -->
<div class="add-target">
<% records.each do |record| %>
<%= f.fields_for relation_field, record do |f| %>
<%= render :partial => "backup_form_block", :locals=>{:f=>f} %>
<% end %>
<% end %>
</div>
<p class="add-btn">
<%= hidden_field_tag "#{relation_field}_count", records.count %>
<a class="btn btn-primary add_backup_setting <%=type%>" href="#"><%= t("client_management.add_backup_setting") %></a>
</p>
</div>
<style type="text/css">
.block_remove_btn{
color: red;
margin-right: 0.3em;
background: transparent;
border: 1px solid red;
margin-bottom: 1em;
padding: 0.4em 0.8em;
cursor: pointer;
}
.block_remove_btn:hover{
font-size: 1.3em;
top: 0.3em;
position: relative;
}
.backup_form_block{
border: 0.3em solid #666;
margin-bottom: 0.5em;
padding-bottom: 1em;
}
.main-forms fieldset .form-actions{
background: #fff;
}
</style>
<script type="text/javascript">
$(document).ready(function(){
$(document).on('click', ".add_backup_setting.<%=type%>", function(){
var _this = $(this);
var new_id = _this.prev().attr('value');
var old_id = new RegExp("new_<%=relation_field%>", "g");
var on = $('.language-nav li.active').index();
var le = _this.parent('.add-btn').prev('.add-target').children('.start-line').length;
_this.prev().attr('value', parseInt(new_id) + 1);
_this.parent().siblings('.add-target').append(("<%= escape_javascript(add_attribute 'backup_form_block', f, relation_field) %>").replace(old_id, new_id));
_this.parent('.add-btn').prev('.add-target').children('.start-line').eq(le).children('.input-append').find('.tab-content').each(function() {
_this.children('.tab-pane').eq(on).addClass('in active').siblings().removeClass('in active');
});
formTip();
});
$(document).on('click','.backup_timers',function(){
var _this = $(this);
_this.not('.hasDatepicker').ui_timepicker({timeFormat: 'HH:mm'});
_this.trigger('focus');
})
$(document).on('click','.block_remove_btn',function(){
var backup_form_block = $(this).parents('.backup_form_block').eq(0);
if(backup_form_block.hasClass("new_record")){
backup_form_block.remove()
}else{
if(window.confirm('<%=t("client_management.are_you_sure_to_delete")%>')){
backup_form_block.addClass('hide');
backup_form_block.find('.should_destroy').val('true');
}
}
})
})
</script>

View File

@ -0,0 +1,45 @@
<% object_name_underscore = f.object_name.gsub(/[\[\]]/,'_') %>
<div class="backup_form_block <%= f.object.new_record? ? 'new_record' : '' %>">
<span class="block_remove_btn">X</span>
<div style="clear: both;"></div>
<div class="control-group">
<%= f.label :disable ,t("client_management.disable"), :class => "control-label muted" %>
<div class="controls">
<%= f.check_box :disable, :title=> t("client_management.disable") %>
</div>
</div>
<div class="control-group">
<% default_backup_time = f.object.fields["backup_time"].options[:default] %>
<%= f.label :backup_time ,t("client_management.backup_time"), :class => "control-label muted" %>
<div class="controls">
<%= f.text_field :backup_time, :placeholder => default_backup_time, :class=>"backup_timers" %>
<div class="hint">Eg: <%=default_backup_time%></div>
</div>
</div>
<div class="control-group">
<% default_path = f.object.fields["path"].options[:default] %>
<%= f.label :path ,"Path", :class => "control-label muted" %>
<div class="controls">
<%= f.text_field :path, :placeholder => "Path(ex: #{default_path})" %>
<div class="hint">Eg: <%=default_path%></div>
</div>
</div>
<div class="control-group">
<%= f.label :period , t("client_management.period"), :class => "control-label muted" %>
<div class="controls">
<%= f.select :period, f.object.class::PeriodsTypes.map.with_index{|c, i| [t("client_management.period_type.#{c}"),i]} %>
</div>
</div>
<div class="control-group">
<% default_retain_count = f.object.fields["retain_count"].options[:default] %>
<%= f.label :retain_count , t("client_management.retain_count"), :class => "control-label muted" %>
<div class="controls">
<%= f.number_field :retain_count, :placeholder => "ex: #{default_retain_count}" %>
<div class="hint">Eg: <%=default_retain_count%></div>
</div>
</div>
<% unless f.object.new_record? %>
<%= f.hidden_field :id %>
<%= f.hidden_field :_destroy, :value => nil, :class => 'should_destroy' %>
<% end %>
</div>

View File

@ -68,6 +68,7 @@
<a class="btn btn-info" href = "<%= admin_site_panel_edit_server_info_path+"?id=#{site_server.id.to_s}" %>"><%=t(:edit)%></a>
<a class="btn btn-danger" href = "#" onclick="if(window.confirm('Do you really want to delete <%=site_server.server_name%>?')) { window.location.href = '<%= admin_site_panel_edit_server_info_path+"?id=#{site_server.id.to_s}&type=delete" %>';}"><%=t(:remove)%></a>
<a class="btn btn-primary" href = "<%= admin_site_panel_sites_list_path+"?server_name=#{site_server.server_name}" %>"><%= t('client_management.see_sites') %></a>
<a class="btn btn-success" href = "<%= backup_setting_admin_site_panel_path(site_server) %>"><%= t('client_management.backup_setting') %></a>
<a class="btn btn-dark btn-inverse detect_sites" href="javascript:void(0)" data-key="<%=site_server.server_name%>">Detect sites</a>
<a title="<%=t('client_management.super_manager_management')%>" class="btn btn-primary super_manager_management" href="<%=super_manager_management_admin_site_panel_path(:id=>site_server.id) %>"><%=t('client_management.super_manager_management')%></a>
</td>

View File

@ -0,0 +1,145 @@
<%= form_for @site_server, :url => {:action=>"update_backup_setting"}, :html => {:class => 'form-horizontal main-forms'} do |f| %>
<% content_for :page_specific_css do %>
<%= stylesheet_link_tag "lib/main-forms" %>
<% end %>
<% content_for :page_specific_javascript do %>
<% end %>
<style type="text/css">
#info_texts{
white-space: pre;
}
</style>
<%#= f.error_messages %>
<fieldset>
<h3><%= "#{f.object.server_name} (IP: #{f.object.ip}) " %></h3>
<div>
<% if f.object.need_rewrite_backup_setting %>
<a href="javascript:void(0)" class="btn btn-success apply_change_backup_setting" data-id="<%=f.object.id%>"><%= t('client_management.apply_change') %></a>
<hr>
<% end %>
</div>
<!-- Input Area -->
<div class="input-area">
<!-- Module Tabs -->
<div class="nav-name"><strong><%= t(:type) %></strong></div>
<ul class="nav nav-pills module-nav">
<li class="active">
<a href="#file_backup" data-toggle="tab"><%= t("client_management.file_backup") %></a>
</li>
<li class="">
<a href="#db_backup" data-toggle="tab"><%= t("client_management.db_backup") %></a>
</li>
</ul>
<!-- Module -->
<div class="tab-content module-area">
<%= render :partial => "backup_form", :locals=>{:f=>f,:type=>"file_backup",:@active_class=>"in active"} %>
<%= render :partial => "backup_form", :locals=>{:f=>f,:type=>"db_backup",:@active_class=>""} %>
</div>
<div class="form-actions">
<%= f.submit t('submit'), class: 'btn btn-primary' %>
</div>
</div>
</fieldset>
<style type="text/css">
.remove_btn{
float: left;
color: red;
line-height: 2rem;
cursor: pointer;
font-weight: bold;
}
.remove_btn:hover{
font-size: 1.3em;
}
</style>
<script type="text/javascript">
var close_info = false;
var timeout_id;
var status_relation = {"starting":"<span style=\"color: skyblue;\">starting</span>","execing":"<span style=\"color: skyblue;\">execing</span>","detecting":"<span style=\"color: skyblue;\">detecting</span>","error":"<span style=\"color: red;\">error</span>","finish": "<span style=\"color: darkseagreen;\">finish</span>","closed":"<span style=\"color: red;\">closed</span>"};
var need_close_info = false;
function see_infos(key){
key = key || "";
if(!close_info){
var request = $.post("<%=admin_site_panel_edit_server_info_path%>",{"type":'see_infos',"key":key});
request.done(function(data){
var infos = request.responseJSON.infos;
var status = request.responseJSON.status;
if($("#info_texts").length == 0)
return infos.join("\n")
else{
if(status == "")
var status_text = "not yet create";
else
var status_text = status_relation[status];
if(!status_text){
status_text = "<span style=\"color: skyblue;\">"+status+"</span>";
}
$("#info_texts").html(status_text+"<div style='clear:both;'></div>"+infos.join("\n"));
msg_end.scrollIntoView();
if(need_close_info && status != 'execing'){
close_info = true;
window.setTimeout(function(){
if(window.confirm("<%=t('client_management.finished_reload_page')%>")){
window.location.reload();
}
},1000);
}else{
if(status == 'execing'){
need_close_info = true;
}
timeout_id = window.setTimeout(see_infos(key),1000);
}
}
})
}else{
window.clearTimeout(timeout_id);
}
};
function show_infos_dialog(key){
key = key || "";
close_info = true;
window.clearTimeout(timeout_id);
close_info = false;
if($("#dialog-confirm").length == 0){
$("#main-wrap").before("<div id='dialog-confirm' title='site infos'>"+
"<div style='clear:both;'></div><div id='info_texts'>"+see_infos(key)+"</div><div id='msg_end' style='height:0px; overflow:hidden'></div>"+
"</div>");
}else{
see_infos(key);
};
$( "#dialog-confirm" ).dialog({
resizable: true,
minHeight: 100,
maxHeight: 400,
width: '80%',
modal: true,
open: function(){
$(this).parent().css("top",$(document).height() - $(window).height() + "px");
},
close: function(){
close_info = true;
need_close_info = false;
},
buttons: {
"<%= t('client_management.confirm') %>": function(){
$( this ).dialog( "close" );
},
"stop update": function(){
close_info = true;
}
}
});
}
$(document).ready(function(){
$('.apply_change_backup_setting').click(function(){
var item = $(this);
var item_id = item.attr("data-id");
$.post("<%=admin_site_panel_edit_site_path%>",{'id': item_id,'type':'apply_change_backup_setting'}).done(function(){
show_infos_dialog("change_backup_setting_" + item_id);
});
});
})
</script>
<% end %>

View File

@ -15,6 +15,13 @@ en:
confirm_new_password: "Confirm new password"
super_manager_management: "Super Manager Management"
create_super_manager: Create Super Manager For Site
backup_setting: "Backup Setting"
add_backup_setting: "Add Backup Setting"
file_backup: "File Backup"
db_backup: "DB Backup"
backup_time: "Backup Time"
period: "Period"
retain_count: "Retain Count"
enable_api: Enable API
api_key: API Key
setting: Setting

View File

@ -15,6 +15,13 @@ zh_tw:
confirm_new_password: "確認新密碼"
super_manager_management: "管理網站超級管理者"
create_super_manager: 創建網站超級管理者
backup_setting: "備份設定"
add_backup_setting: "新增備份設定"
file_backup: "檔案備份"
db_backup: "資料庫備份"
backup_time: "備份時間"
period: "週期"
retain_count: "保留"
enable_api: 開啟API
api_key: API Key
setting: 設定

View File

@ -68,6 +68,9 @@ Rails.application.routes.draw do
patch 'create_cert'
end
member do
get 'backup_setting'
post 'update_backup_setting'
patch 'update_backup_setting'
get "super_manager_management"
patch "update_super_manager_management"
get "super_manager_management_for_site"

View File

@ -0,0 +1,96 @@
require 'net/ssh'
require 'pathname'
require 'json'
require 'base64'
namespace :exec_commands do
desc "Change Server Backup Setting Script"
task :change_backup_setting,[:site_server_id] => :environment do |task,args|
if args.site_server_id.present?
site_server = SiteServer.find(args.site_server_id)
else
raise StandardError.new('Please Specify Server ID!')
end
thread_key = "change_backup_setting_#{site_server.id}"
@thread = Multithread.where(:key=>thread_key).first
begin
if @thread.nil?
@thread = Multithread.create(:key=>thread_key,:status=>{"infos"=>[],"status"=>"execing"})
else
@thread.update(:status=>{"infos"=>[],"status"=>"execing"})
end
ip = site_server.ip
user = site_server.account
password = site_server.password
@password = password
begin
Net::SSH.start(ip , user , password: password) do |ssh|
end
rescue Net::SSH::HostKeyMismatch
system("ssh-keygen -f \"$HOME/.ssh/known_hosts\" -R #{ip}")
rescue Errno::ENOTTY
system("ssh-add \"$HOME/.ssh/id_rsa\"")
end
Net::SSH.start(ip , user , password: password) do |ssh|
@no_stdout = true
crontab_lines_str = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' crontab -l", false, true)
if site_server.site_server_file_backups.count != 0
SiteServerFileBackup.init_class_variables(site_server)
rsnapshot_conf_default = SiteServerFileBackup::DefaultConf
rsnapshot_conf_exist = check_file_exist_for_ssh(ssh, rsnapshot_conf_default)
update_thread_infos_for_exec("Checking file backups config...")
if rsnapshot_conf_exist
rsnapshot_conf_contents = read_file_for_ssh(ssh, rsnapshot_conf_default)
else
rsnapshot_sample_conf = File.expand_path("../../../rsnapshot_sample.conf", __FILE__)
rsnapshot_conf_contents = File.read(rsnapshot_sample_conf)
write_file_for_ssh(ssh, rsnapshot_conf_default, rsnapshot_conf_contents)
end
site_server.site_server_file_backups.each do |file_backup|
rsnapshot_conf_path = file_backup.rsnapshot_conf_path
if check_file_exist_for_ssh(ssh, rsnapshot_conf_path)
tmp = read_file_for_ssh(ssh, rsnapshot_conf_path)
else
tmp = rsnapshot_conf_contents.clone
end
tmp = file_backup.gsub_rsnapshot_conf(tmp)
write_file_for_ssh(ssh, rsnapshot_conf_path, tmp)
mkdir_for_ssh(ssh, file_backup.path)
end
update_thread_infos_for_exec("Finish writing file backups config!")
end
site_server.site_server_db_backups do |db_backup|
mkdir_for_ssh(ssh, db_backup.path)
end
crontab_lines_str = SiteServerFileBackup.write_crontab_setting(site_server, crontab_lines_str)
crontab_lines_str = SiteServerDbBackup.write_crontab_setting(site_server, crontab_lines_str)
write_crontab_for_ssh(ssh, crontab_lines_str)
update_thread_infos_for_exec("Finish setting backups!")
end
site_server.site_server_file_backups.update_all(:need_rewrite=>false)
site_server.site_server_db_backups.update_all(:need_rewrite=>false)
site_server.update(:need_rewrite_backup_setting=>false)
@thread.update(:status=>@thread.status.merge({"status"=>"finish"}))
rescue => e
@thread.update(:status=>{"infos"=>@thread.status["infos"].push(e.message),"status"=>"error"})
@thread.update(:status=>{"infos"=>@thread.status["infos"].push(e.backtrace.join("\n")),"status"=>"error"})
end
end
def mkdir_for_ssh(ssh, dir)
exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' sh -c \"mkdir -p #{dir}\"", false)
end
def write_crontab_for_ssh(ssh, crontab_lines_str)
exec_ssh_command_by_sudo_and_see_output(ssh,"x='#{crontab_lines_str.gsub("\n", '\n').gsub("'","'\"'\"'")}'; sudo -p 'sudo password:' sh -c \"echo '$x'| crontab -\"", false)
end
def read_file_for_ssh(ssh, file_name)
exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' sh -c \"cat #{file_name}\"", false, true)
end
def write_file_for_ssh(ssh, file_name, contents)
exec_ssh_command_by_sudo_and_see_output(ssh,"x='#{contents.gsub("\n", '\n').gsub("'","'\"'\"'")}';sudo -p 'sudo password:' sh -c \"echo '$x' > #{file_name}\"", false)
end
def copy_file_for_ssh(ssh, src, dst)
exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' bash -l -c 'cp #{src} #{dst}'", 1)
end
def check_file_exist_for_ssh(ssh, file_name)
exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' bash -l -c 'if [ -e #{file_name} ]; then echo 1; else echo 0; fi'", false, true).include?('1')
end
end

View File

@ -164,6 +164,79 @@ namespace :create_site do
end
end
end
update_thread_infos("Detecting Backup Setting on #{site_server.server_name}...")
crontab_lines = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' crontab -l;crontab -l",false,false)
file_backups = crontab_lines.select{|s| s.match(/^\s*[^\s#]/) && s.include?('rsnapshot')}.map{|s| SiteServerFileBackup.read_from_crontab_line(s)}.compact
rsnapshot_confs_path = ([SiteServerFileBackup::DefaultConf] + file_backups.map{|time, rsnapshot_conf_path, period| rsnapshot_conf_path}).uniq
retain_settings = {}
backup_dir_info = {}
snapshot_root_info = {}
rsnapshot_confs_path.each do |rsnapshot_conf_path|
backup_retain_lines = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' grep -E '^\\s*retain\\s+' #{rsnapshot_conf_path}",false,false).map{|s| s.strip}.select{|s| s.present?}
backup_retain_lines = backup_retain_lines
next if backup_retain_lines.blank?
part_retain_settings = backup_retain_lines.map{|s| (s.strip.split(/\s+/)[1..2] rescue nil)}.compact.to_h
retain_settings[rsnapshot_conf_path] = part_retain_settings
backup_dir_subinfo = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' grep -E '^\s*backup' #{rsnapshot_conf_path}",false,false).map{|s| s.strip}.select{|s| s.present?} # ex: ["backup\t/home/rulingcom\tlocalhost/"]
backup_dir_info[rsnapshot_conf_path] = backup_dir_subinfo.map{|s| s.strip.split(/\s+/)[1..2]}
snapshot_root_subinfo = exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' grep -E '^\s*snapshot_root' #{rsnapshot_conf_path}",false,false).map{|s| s.strip}.select{|s| s.present?}.last # ex: "snapshot_root\t/home/backup/orbit"
snapshot_root_info[rsnapshot_conf_path] = snapshot_root_subinfo.strip.split(/\s+/)[1] rescue nil #only single snapshot_root in a conf
end
file_backups_group = file_backups.group_by{|time, rsnapshot_conf_path, period| rsnapshot_conf_path}
file_backup_ids = []
retain_settings.each do |rsnapshot_conf_path, part_retain_settings|
path = snapshot_root_info[rsnapshot_conf_path]
if path.nil?
path = '/home/backup/orbit'
end
backup_dir_subinfo = backup_dir_info[rsnapshot_conf_path]
tmp = file_backups_group[rsnapshot_conf_path]
unused_period = part_retain_settings.map{|period_text, retain_count| SiteServerFileBackup::PeriodsTypes.index(period_text).to_i} - tmp.map{|time, rsnapshot_conf_path, period| period}
unused_retain_settings = unused_period.map{|period| [period, part_retain_settings[SiteServerFileBackup::PeriodsTypes[period]]]}
backup_dir_subinfo.each do |backup_dir, backup_prefix|
unused_retain_settings.each do |period, retain_count|
file_backup_data = {:site_server=>@site_server, :disable=>true, :period=>period, :path=>path, :retain_count=>retain_count, :rsnapshot_conf_path=>rsnapshot_conf_path, :backup_dir=>backup_dir, :backup_prefix=>backup_prefix}
file_backup = SiteServerFileBackup.where(file_backup_data.except(:disable)).first
if file_backup.nil?
file_backup = SiteServerFileBackup.create(file_backup_data)
else
file_backup.update_attributes(file_backup_data)
end
file_backup_ids << file_backup.id
end
tmp.each do |time, rsnapshot_conf_path, period|
retain_count = part_retain_settings[SiteServerFileBackup::PeriodsTypes[period]]
if retain_count
file_backup_data = {:site_server=>@site_server, :disable=>false, :backup_time=>time, :path=>path, :period=>period, :retain_count=>retain_count, :rsnapshot_conf_path=>rsnapshot_conf_path, :backup_dir=>backup_dir, :backup_prefix=>backup_prefix}
file_backup = SiteServerFileBackup.where(file_backup_data.except(:disable)).first
if file_backup.nil?
file_backup = SiteServerFileBackup.create(file_backup_data)
else
file_backup.update_attributes(file_backup_data)
end
file_backup_ids << file_backup.id
end
end
end
end
SiteServerFileBackup.where(:id.in=>file_backup_ids).update_all(:need_rewrite=>false)
db_backup_ids = []
db_backup_infos = crontab_lines.select{|s| s.match(/^\s*[^\s#]/) && s.include?('mongodump')}.map{|s| SiteServerDbBackup.read_from_crontab_line(s, crontab_lines)}.compact
db_backup_infos.each do |backup_time, path, period, retain_count|
next if backup_time.nil?
db_backup_data = {:site_server=>@site_server, :disable=>false, :period=>period, :path=>path, :backup_time=>backup_time, :retain_count=>retain_count}
db_backup = SiteServerDbBackup.where(db_backup_data.except(:disable)).first
if db_backup.nil?
db_backup = SiteServerDbBackup.create(db_backup_data)
else
db_backup.update_attributes(db_backup_data)
end
db_backup_ids << db_backup.id
end
SiteServerDbBackup.where(:id.in=>db_backup_ids).update_all(:need_rewrite=>false)
update_thread_infos("Finish detecting Backup Setting on #{site_server.server_name}!")
end
end
end

238
rsnapshot_sample.conf Normal file
View File

@ -0,0 +1,238 @@
#################################################
# rsnapshot.conf - rsnapshot configuration file #
#################################################
# #
# PLEASE BE AWARE OF THE FOLLOWING RULE: #
# #
# This file requires tabs between elements #
# #
#################################################
#######################
# CONFIG FILE VERSION #
#######################
config_version 1.2
###########################
# SNAPSHOT ROOT DIRECTORY #
###########################
# All snapshots will be stored under this root directory.
#
#snapshot_root /var/cache/rsnapshot/
snapshot_root /home/backup/orbit
# If no_create_root is enabled, rsnapshot will not automatically create the
# snapshot_root directory. This is particularly useful if you are backing
# up to removable media, such as a FireWire or USB drive.
#
#no_create_root 1
#################################
# EXTERNAL PROGRAM DEPENDENCIES #
#################################
# LINUX USERS: Be sure to uncomment "cmd_cp". This gives you extra features.
# EVERYONE ELSE: Leave "cmd_cp" commented out for compatibility.
#
# See the README file or the man page for more details.
#
cmd_cp /bin/cp
# uncomment this to use the rm program instead of the built-in perl routine.
#
cmd_rm /bin/rm
# rsync must be enabled for anything to work. This is the only command that
# must be enabled.
#
cmd_rsync /usr/bin/rsync
# Uncomment this to enable remote ssh backups over rsync.
#
#cmd_ssh /usr/bin/ssh
# Comment this out to disable syslog support.
#
cmd_logger /usr/bin/logger
# Uncomment this to specify the path to "du" for disk usage checks.
# If you have an older version of "du", you may also want to check the
# "du_args" parameter below.
#
#cmd_du /usr/bin/du
# Uncomment this to specify the path to rsnapshot-diff.
#
#cmd_rsnapshot_diff /usr/bin/rsnapshot-diff
# Specify the path to a script (and any optional arguments) to run right
# before rsnapshot syncs files
#
#cmd_preexec /path/to/preexec/script
# Specify the path to a script (and any optional arguments) to run right
# after rsnapshot syncs files
#
#cmd_postexec /path/to/postexec/script
# Paths to lvcreate, lvremove, mount and umount commands, for use with
# Linux LVMs.
#
#linux_lvm_cmd_lvcreate /sbin/lvcreate
#linux_lvm_cmd_lvremove /sbin/lvremove
#linux_lvm_cmd_mount /bin/mount
#linux_lvm_cmd_umount /bin/umount
#########################################
# BACKUP LEVELS / INTERVALS #
# Must be unique and in ascending order #
# e.g. alpha, beta, gamma, etc. #
#########################################
#retain alpha 6
#retain beta 7
#retain gamma 4
#retain delta 3
retain daily 7
############################################
# GLOBAL OPTIONS #
# All are optional, with sensible defaults #
############################################
# Verbose level, 1 through 5.
# 1 Quiet Print fatal errors only
# 2 Default Print errors and warnings only
# 3 Verbose Show equivalent shell commands being executed
# 4 Extra Verbose Show extra verbose information
# 5 Debug mode Everything
#
verbose 2
# Same as "verbose" above, but controls the amount of data sent to the
# logfile, if one is being used. The default is 3.
# If you want the rsync output, you have to set it to 4
#
loglevel 3
# If you enable this, data will be written to the file you specify. The
# amount of data written is controlled by the "loglevel" parameter.
#
#logfile /var/log/rsnapshot.log
# If enabled, rsnapshot will write a lockfile to prevent two instances
# from running simultaneously (and messing up the snapshot_root).
# If you enable this, make sure the lockfile directory is not world
# writable. Otherwise anyone can prevent the program from running.
#
lockfile /var/run/rsnapshot.pid
# By default, rsnapshot check lockfile, check if PID is running
# and if not, consider lockfile as stale, then start
# Enabling this stop rsnapshot if PID in lockfile is not running
#
#stop_on_stale_lockfile 0
# Default rsync args. All rsync commands have at least these options set.
#
#rsync_short_args -a
#rsync_long_args --delete --numeric-ids --relative --delete-excluded
# ssh has no args passed by default, but you can specify some here.
#
#ssh_args -p 22
# Default arguments for the "du" program (for disk space reporting).
# The GNU version of "du" is preferred. See the man page for more details.
# If your version of "du" doesnt support the -h flag, try -k flag instead.n#n#du_args -cshnn# If this is enabled, rsync wont span filesystem partitions within a
# backup point. This essentially passes the -x option to rsync.
# The default is 0 (off).
#
#one_fs 0
# The include and exclude parameters, if enabled, simply get passed directly
# to rsync. If you have multiple include/exclude patterns, put each one on a
# separate line. Please look up the --include and --exclude options in the
# rsync man page for more details on how to specify file name patterns.
#
#include ???
#include ???
#exclude ???
#exclude ???
# The include_file and exclude_file parameters, if enabled, simply get
# passed directly to rsync. Please look up the --include-from and
# --exclude-from options in the rsync man page for more details.
#
#include_file /path/to/include/file
#exclude_file /path/to/exclude/file
# If your version of rsync supports --link-dest, consider enabling this.
# This is the best way to support special files (FIFOs, etc) cross-platform.
# The default is 0 (off).
#
#link_dest 0
# When sync_first is enabled, it changes the default behaviour of rsnapshot.
# Normally, when rsnapshot is called with its lowest interval
# (i.e.: "rsnapshot alpha"), it will sync files AND rotate the lowest
# intervals. With sync_first enabled, "rsnapshot sync" handles the file sync,
# and all interval calls simply rotate files. See the man page for more
# details. The default is 0 (off).
#
#sync_first 0
# If enabled, rsnapshot will move the oldest directory for each interval
# to [interval_name].delete, then it will remove the lockfile and delete
# that directory just before it exits. The default is 0 (off).
#
#use_lazy_deletes 0
# Number of rsync re-tries. If you experience any network problems or
# network card issues that tend to cause ssh to fail with errors like
# "Corrupted MAC on input", for example, set this to a non-zero value
# to have the rsync operation re-tried.
#
#rsync_numtries 0
# LVM parameters. Used to backup with creating lvm snapshot before backup
# and removing it after. This should ensure consistency of data in some special
# cases
#
# LVM snapshot(s) size (lvcreate --size option).
#
#linux_lvm_snapshotsize 100M
# Name to be used when creating the LVM logical volume snapshot(s).
#
#linux_lvm_snapshotname rsnapshot
# Path to the LVM Volume Groups.
#
#linux_lvm_vgpath /dev
# Mount point to use to temporarily mount the snapshot(s).
#
#linux_lvm_mountpath /path/to/mount/lvm/snapshot/during/backup
###############################
### BACKUP POINTS / SCRIPTS ###
###############################
# LOCALHOST
backup /home/rulingcom/ localhost/
#backup /home/ localhost/
#backup /etc/ localhost/
#backup /usr/local/ localhost/
#backup /var/log/rsnapshot localhost/
#backup /etc/passwd localhost/
#backup /home/foo/My Documents/ localhost/
#backup /foo/bar/ localhost/ one_fs=1,rsync_short_args=-urltvpog
#backup_script /usr/local/bin/backup_pgsql.sh localhost/postgres/
# You must set linux_lvm_* parameters below before using lvm snapshots
#backup lvm://vg0/xen-home/ lvm-vg0/xen-home/
# EXAMPLE.COM
#backup_exec /bin/date "+ backup of example.com started at %c"
#backup root@example.com:/home/ example.com/ +rsync_long_args=--bwlimit=16,exclude=core
#backup root@example.com:/etc/ example.com/ exclude=mtab,exclude=core
#backup_exec ssh root@example.com "mysqldump -A > /var/db/dump/mysql.sql"
#backup root@example.com:/var/db/dump/ example.com/
#backup_exec /bin/date "+ backup of example.com ended at %c"
# CVS.SOURCEFORGE.NET
#backup_script /usr/local/bin/backup_rsnapshot_cvsroot.sh rsnapshot.cvs.sourceforge.net/
# RSYNC.SAMBA.ORG
#backup rsync://rsync.samba.org/rsyncftp/ rsync.samba.org/rsyncftp/