diff --git a/app/models/fileset.rb b/app/models/fileset.rb index e2cfcd1..446d7c1 100644 --- a/app/models/fileset.rb +++ b/app/models/fileset.rb @@ -1,89 +1,90 @@ class Fileset < ActiveRecord::Base establish_connection Baas::settings[:local_db] serialize :exclude_directions 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 = ['/'] def to_bacula_config_array ['FileSet {'] + [" Name = \"#{name_for_config}\""] + include_directions_to_config_array + exclude_directions_to_config_array + ['}'] end def name_for_config [host.name, name].join(' ') 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 47a04ea..bd9f736 100644 --- a/app/models/job_template.rb +++ b/app/models/job_template.rb @@ -1,86 +1,93 @@ class JobTemplate < ActiveRecord::Base establish_connection Baas::settings[:local_db] 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) } # configurable DEFAULT_OPTIONS = { storage: :File, pool: :Default, messages: :Standard, priority: 10, :'Write Bootstrap' => '"/var/lib/bacula/%c.bsr"' } def to_bacula_config_array ['Job {'] + options_array.map { |x| " #{x}" } + DEFAULT_OPTIONS.map { |k,v| " #{k.capitalize} = #{v}" } + ['}'] end def priority DEFAULT_OPTIONS[:priority] end def enabled_human enabled? ? 'yes' : 'no' end def schedule_human schedule.present? ? schedule.name : '-' end 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 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}\"", "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 end diff --git a/app/models/schedule.rb b/app/models/schedule.rb index db586d0..a026cee 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -1,45 +1,45 @@ class Schedule < ActiveRecord::Base DEFAULT_RUNS = [ 'Level=Full 1st sun at ', 'Level=Differential 2nd-5th sun at ', 'Level=Incremental mon-sat at ' ] attr_accessor :runtime serialize :runs, JSON belongs_to :host validates :name, :runs, presence: true - validates :name, uniqueness: { scope: :host } + validates_with NameValidator before_validation :set_runs, if: Proc.new { |s| s.runtime.present? } def to_bacula_config_array ['Schedule {'] + [" Name = \"#{name_for_config}\""] + runs.map {|r| " Run = #{r}" } + ['}'] end def name_for_config [host.name, name].join(' ') end private def set_runs if valid_runtime? self.runs = DEFAULT_RUNS.map { |r| r + runtime } else self.errors.add(:runtime, :not_valid_24h_time) false end end def valid_runtime? runtime && runtime[/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/] end end diff --git a/lib/name_validator.rb b/lib/name_validator.rb new file mode 100644 index 0000000..d8a7527 --- /dev/null +++ b/lib/name_validator.rb @@ -0,0 +1,7 @@ +class NameValidator < ActiveModel::Validator + def validate(record) + unless record.name =~ /^[a-zA-Z0-1\.-_ ]+$/ + record.errors[:name] << "Can only contain numbers, letters, space, '.', '-' and '_'" + end + end +end diff --git a/spec/models/job_template_spec.rb b/spec/models/job_template_spec.rb index 3de6ecf..3b4087d 100644 --- a/spec/models/job_template_spec.rb +++ b/spec/models/job_template_spec.rb @@ -1,154 +1,154 @@ require 'spec_helper' describe JobTemplate do context 'validates' do it 'name must be present' do - expect(JobTemplate.new).to have(1).errors_on(:name) + expect(JobTemplate.new).to have(2).errors_on(:name) end it 'name must be unique on host\'s scope' do job_1 = FactoryGirl.create(:job_template, name: 'a name') job_2 = FactoryGirl.build(:job_template, name: 'a name') job_3 = FactoryGirl.build(:job_template, name: 'a name', host: job_1.host) expect(job_2).to be_valid expect(job_3).to_not be_valid end it 'fileset_id must be present' do expect(JobTemplate.new).to have(1).errors_on(:fileset_id) end it 'schedule_id must be present' do expect(JobTemplate.new).to have(1).errors_on(:schedule_id) end it 'schedule_id must NOT be present for :restore jobs' do expect(JobTemplate.new(job_type: :restore)).to have(0).errors_on(:schedule_id) end end # automatic assignments context 'when no job_type is given' do let(:job_template) { FactoryGirl.create(:job_template) } it 'sets the job_type to :backup' do expect(job_template).to be_backup end end context 'when enabling a job' do [:pending, :dispatched].each do |status| context "of a #{status} host" do let(:host) { FactoryGirl.create(:host) } let!(:job) { FactoryGirl.create(:job_template, host: host) } before { host.update_column(:status, Host::STATUSES[status]) } it 'becomes configured' do expect { job.enabled = true job.save }.to change { host.reload.human_status_name }.from(status.to_s).to('configured') end end end context 'of a configured host' do let(:host) { FactoryGirl.create(:host) } let!(:job) { FactoryGirl.create(:job_template, host: host) } before { host.update_column(:status, Host::STATUSES[:configured]) } it 'stays configured' do expect { job.enabled = true job.save }.to_not change { host.reload.human_status_name } end end context 'of a updated host' do let(:host) { FactoryGirl.create(:host) } let!(:job) { FactoryGirl.create(:job_template, host: host) } before { host.update_column(:status, Host::STATUSES[:updated]) } it 'stays updated' do expect { job.enabled = true job.save }.to_not change { host.reload.human_status_name } end end [:deployed, :redispatched].each do |status| context "of a #{status} host" do let(:host) { FactoryGirl.create(:host) } let!(:job) { FactoryGirl.create(:job_template, host: host) } before { host.update_column(:status, Host::STATUSES[status]) } it 'becomes updated' do expect { job.enabled = true job.save }.to change { host.reload.human_status_name }.from(status.to_s).to('updated') end end end end describe '#to_bacula_config_array' do let(:job_template) do FactoryGirl.create(:job_template, client_before_run_file: 'test', client_after_run_file: 'test2') end subject { job_template.to_bacula_config_array } it 'has a Job structure' do expect(subject.first).to eq('Job {') expect(subject.last).to eq('}') end JobTemplate::DEFAULT_OPTIONS.each do |k, v| it "assigns #{k.capitalize} param" do expect(subject).to include(" #{k.capitalize} = #{v}") end end it 'assigns Name param prefixed with the host\'s name' do expect(subject).to include(" Name = \"#{job_template.name_for_config}\"") end it 'assigns FileSet param' do expect(subject).to include(" FileSet = \"#{job_template.fileset.name_for_config}\"") end it 'assigns Client param' do expect(subject).to include(" Client = \"#{job_template.host.name}\"") end it 'assigns Type param' do expect(subject).to include(" Type = \"#{job_template.job_type.capitalize}\"") end it 'assigns Schedule param' do expect(subject).to include(" Schedule = \"#{job_template.schedule.name_for_config}\"") end it 'assigns Client Run After Job param' do expect(subject).to include(" Client Run After Job = \"#{job_template.client_after_run_file}\"") end it 'assigns Client Run After Job param' do expect(subject).to include(" Client Run Before Job = \"#{job_template.client_before_run_file}\"") end end end diff --git a/spec/models/schedule_spec.rb b/spec/models/schedule_spec.rb index f8069fe..3725d4a 100644 --- a/spec/models/schedule_spec.rb +++ b/spec/models/schedule_spec.rb @@ -1,58 +1,58 @@ require 'spec_helper' describe Schedule do context 'validates' do it 'presence of name' do - expect(Schedule.new).to have(1).errors_on(:name) + expect(Schedule.new).to have(2).errors_on(:name) end it 'presence of runs' do expect(Schedule.new).to have(1).errors_on(:runs) end context 'schedule name is unique in the host\'s scope' do let!(:schedule_1) { FactoryGirl.create(:schedule, name: 'Schedule_1') } let(:schedule_2) { FactoryGirl.build(:schedule, name: 'Schedule_1') } let(:schedule_3) { FactoryGirl.build(:schedule, name: 'Schedule_1', host: schedule_1.host) } it 'two schedules of diferent hosts can have the same name' do expect(schedule_2).to be_valid end it 'two schedules of the same host can NOT have the same name' do expect(schedule_3).to_not be_valid end end end describe '#to_bacula_config_array' do let(:runs) do [ 'Full 1st sun at 23:05', 'Differential 2nd-5th sun at 23:50', 'Incremental mon-sat at 23:50' ] end let(:schedule) do FactoryGirl.create(:schedule, name: 'Test Schedule', runs: runs) end subject { schedule.to_bacula_config_array } it 'is a schedule type resource' do expect(subject.first).to eq('Schedule {') expect(subject.last).to eq('}') end it 'contains the name' do expect(subject).to include(" Name = \"#{[schedule.host.name, schedule.name].join(' ')}\"") end it 'contains the runs' do runs.each do |r| expect(subject).to include(" Run = #{r}") end end end end