diff --git a/app/models/fileset.rb b/app/models/fileset.rb index 4b31562..0b50436 100644 --- a/app/models/fileset.rb +++ b/app/models/fileset.rb @@ -1,131 +1,131 @@ # Fileset model is the application representation of Bacula's Fileset. # It has references to a host and job templates. class Fileset < ActiveRecord::Base establish_connection ARCHIVING_CONF serialize :exclude_directions, Array serialize :include_directions, JSON attr_accessor :include_files belongs_to :host has_many :job_templates validates :name, presence: true, uniqueness: { scope: :host } validate :has_included_files, on: :create validates_with NameValidator before_save :sanitize_exclude_directions, :sanitize_include_directions DEFAULT_EXCLUDED = %w{/var/lib/bacula /proc /tmp /.journal /.fsck /bacula} DEFAULT_INCLUDE_OPTIONS = { signature: :SHA1, compression: :GZIP } DEFAULT_INCLUDE_FILE_LIST = ['/'] # Constructs an array where each element is a line for the Fileset's bacula config # # @return [Array] def to_bacula_config_array ['FileSet {'] + [" Name = \"#{name_for_config}\""] + include_directions_to_config_array + exclude_directions_to_config_array + ['}'] end # Provides a human readable projection of the fileset # # @return [String] def human_readable result = "Directories:\n" result << "\t* " << include_directions['file'].join("\n\t* ") if exclude_directions.present? result << "\n\nExcluded:\n" result << "\t* " << exclude_directions.join("\n\t*") end result end # Generates a name that will be used for the configuration file. # It is the name that will be sent to Bacula through the configuration # files. # # @return [String] def name_for_config [host.name, name].join(' ') end # Returns the hosts that have enabled jobs that use this fileset # # @return [ActiveRecord::Relation] the participating hosts def participating_hosts Host.joins(:job_templates).where(job_templates: { enabled: true, fileset_id: id }).uniq end # Creates a default fileset resource for a simple config - def default_resource + def default_resource(name, time_hex) @include_files = DEFAULT_INCLUDE_FILE_LIST - self.name = "default_fileset" + self.name = "files_#{name}_#{time_hex}" self.exclude_directions = DEFAULT_EXCLUDED save! self end private def has_included_files if include_files.blank? || include_files.all?(&:blank?) errors.add(:include_files, :cant_be_blank) end end def sanitize_include_directions files = include_files.compact.uniq.keep_if(&:present?) return false if files.blank? self.include_directions = { options: DEFAULT_INCLUDE_OPTIONS, file: files } end def sanitize_exclude_directions self.exclude_directions = exclude_directions.keep_if(&:present?).uniq rescue nil end def exclude_directions_to_config_array return [] if exclude_directions.empty? [' Exclude {'] + exclude_directions.map { |x| " File = \"#{x}\"" } + [' }'] end def include_directions_to_config_array return [] if include_directions.blank? [" Include {"] + included_options + included_files + [' }'] end def included_options formatted = [" Options {"] options = include_directions.deep_symbolize_keys[:options]. reverse_merge(DEFAULT_INCLUDE_OPTIONS) options.each do |k,v| if not [:wildfile].include? k formatted << " #{k} = #{v}" else formatted << v.map { |f| " #{k} = \"#{f}\"" } end end formatted << " }" formatted end def included_files include_directions['file'].map { |f| " File = #{f}" } end def included_wildfile include_directions['wildfile'].map { |f| " wildfile = \"#{f}\"" }.join("\n") end end diff --git a/app/models/job_template.rb b/app/models/job_template.rb index f03f399..38f09b6 100644 --- a/app/models/job_template.rb +++ b/app/models/job_template.rb @@ -1,145 +1,145 @@ # JobTemplate class is a helper class that enables us to configure Bacula job # configurations, without messing with Bacula's native models (Job). # It has a unique: # # * host # * fileset # * schedule class JobTemplate < ActiveRecord::Base establish_connection ARCHIVING_CONF enum job_type: { backup: 0, restore: 1, verify: 2, admin: 3 } belongs_to :host belongs_to :fileset belongs_to :schedule validates :name, :fileset_id, presence: true validates :schedule_id, presence: true, unless: :restore? validates :name, uniqueness: { scope: :host } validates_with NameValidator before_save :set_job_type after_save :notify_host scope :enabled, -> { where(enabled: true) } # Constructs an array where each element is a line for the Job's bacula config # # @return [Array] def to_bacula_config_array ['Job {'] + options_array.map { |x| " #{x}" } + runscript.map { |x| " #{x}" } + job_settings.map { |k,v| " #{k.capitalize} = #{v}" } + ['}'] end # Fetches the Job's priority def priority job_settings[:priority] end # Helper method for the job template's enabled status def enabled_human enabled? ? 'yes' : 'no' end # Helper method for the job template's schedule name # # @return [String] The schedule's name or nothing def schedule_human schedule.present? ? schedule.name : '-' end # Generates a name that will be used for the configuration file. # It is the name that will be sent to Bacula through the configuration # files. # # @return [String] def name_for_config "#{host.name} #{name}" end # Sends a hot backup request to Bacula via BaculaHandler def backup_now return false if not (enabled? && baculized? && backup?) host.backup_now(name) end # Handles the returned attribues for api # # @return [Hash] of the desired attributes for api use def api_json { id: id, name: name, fileset: fileset.name_for_config } end # Creates a default job resource for a simple config - def default_resource - self.name = "default_backup_job" + def default_resource(name, time_hex) + self.name = "job_#{name}_#{time_hex}" save! self end private def name_format unless name =~ /^[a-zA-Z0-1\.-_ ]+$/ self.errors.add(:name, :format) end end def notify_host host.recalculate end # Sets the default job_type as backup def set_job_type self.job_type = :backup if job_type.nil? end def options_array result = [ "Name = \"#{name_for_config}\"", "Enabled = #{enabled_human}", "FileSet = \"#{fileset.name_for_config}\"", "Client = \"#{host.name}\"", "Type = \"#{job_type.capitalize}\"", "Schedule = \"#{schedule.name_for_config}\"" ] if client_before_run_file.present? result += ["Client Run Before Job = \"#{client_before_run_file}\""] end if client_after_run_file.present? result += ["Client Run After Job = \"#{client_after_run_file}\""] end result end def runscript [ 'RunScript {', " command = \"bash #{Archiving.settings[:quota_checker]} \\\"%c\\\" #{host.quota}\"", ' RunsOnClient = no', ' RunsWhen = Before', '}' ] end # Fetches and memoizes the general configuration settings for Jobs # # @see ConfigurationSetting.current_job_settings # @return [Hash] containing the settings def job_settings messages = host.email_recipients.any? ? host.message_name : :Standard @job_settings ||= ConfigurationSetting.current_job_settings.merge(messages: messages) end end diff --git a/app/models/schedule.rb b/app/models/schedule.rb index 8348edf..0f7a446 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -1,66 +1,66 @@ # Schedule model is the application representation of Bacula's Schedule. # It has references to a host and multiple schedule run in order to provide # the desired Bacula configuration class Schedule < ActiveRecord::Base establish_connection ARCHIVING_CONF has_many :schedule_runs belongs_to :host has_many :job_templates validates :name, presence: true validates :name, uniqueness: { scope: :host } validates_with NameValidator accepts_nested_attributes_for :schedule_runs, allow_destroy: true # Constructs an array where each element is a line for the Schedule's bacula config # # @return [Array] def to_bacula_config_array ['Schedule {'] + [" Name = \"#{name_for_config}\""] + schedule_runs.map {|r| " Run = #{r.schedule_line}" } + ['}'] end # Provide a human readable projection of the schedule # # @return [String] def human_readable schedule_runs.map(&:human_readable).join("\n") end # Generates a name that will be used for the configuration file. # It is the name that will be sent to Bacula through the configuration # files. # # @return [String] def name_for_config [host.name, name].join(' ') end # Returns the hosts that have enabled jobs that use this schedule # # @return [ActiveRecord::Colletion] the participating hosts def participating_hosts Host.joins(:job_templates).where(job_templates: { enabled: true, schedule_id: id }).uniq end # Creates a default schedule resource for a simple config - def default_resource(day, hour, minute) + def default_resource(time_hex, day, hour, minute) time = [hour, minute].map { |x| x.to_s.rjust(2, '0') }.join(':') full_day = "first #{day}" diff_days = "second-fifth #{day}" inc_days = (SimpleConfiguration::DAYS.values - [day]).join(',') - self.name = "daily_at_#{time.gsub(':','_')}" + self.name = "daily_at_#{time.gsub(':','_')}_#{time_hex}" save! self.schedule_runs.create(level: :full, time: time, day: full_day) self.schedule_runs.create(level: :differential, time: time, day: diff_days) self.schedule_runs.create(level: :incremental , time: time, day: inc_days) self end end diff --git a/app/models/simple_configuration.rb b/app/models/simple_configuration.rb index 75bca93..c656c41 100644 --- a/app/models/simple_configuration.rb +++ b/app/models/simple_configuration.rb @@ -1,51 +1,54 @@ class SimpleConfiguration < ActiveRecord::Base establish_connection ARCHIVING_CONF DAYS = { monday: :mon, tuesday: :tue, wednesday: :wed, thursday: :thu, friday: :fri, saturday: :sat, sunday: :sun } enum day: { monday: 0, tuesday: 1, wednesday: 2, thursday: 3, friday: 4, saturday: 5, sunday: 6 } belongs_to :host validates :host, :day, :hour, :minute, presence: true validates :hour, numericality: { greater_than_or_equal: 0, less_then: 24 } validates :minute, numericality: { greater_than_or_equal: 0, less_then: 60 } validates_with NameValidator # Initializes the configuration's 3 parameters randomnly. # Default configurations must be randomized in order to distribute the backup server's # load. def randomize self.day = SimpleConfiguration.days.keys.sample self.hour = rand(24) self.minute = rand(60) end # The day abbreviation # # @return [Symbol] def day_short DAYS[day.to_sym] end # Creates a default config, by adding resources for: # # * schedule # * fileset # * job # # Each resource handles its own defaults. def create_config - schedule = host.schedules.new.default_resource(day_short, hour, minute) - fileset = host.filesets.new.default_resource - job = host.job_templates.new(fileset_id: fileset.id, schedule_id: schedule.id).default_resource + time_hex = Digest::MD5.hexdigest(Time.now.to_f.to_s).first(4) + + schedule = host.schedules.new.default_resource(time_hex, day_short, hour, minute) + fileset = host.filesets.new.default_resource(name, time_hex) + host.job_templates.new(fileset_id: fileset.id, schedule_id: schedule.id). + default_resource(name, time_hex) end end