diff --git a/app/models/bacula_file.rb b/app/models/bacula_file.rb index 14a7cd5..9b30cbf 100644 --- a/app/models/bacula_file.rb +++ b/app/models/bacula_file.rb @@ -1,20 +1,22 @@ # ActiveRecord class for Bacula's File resource class BaculaFile < ActiveRecord::Base - self.table_name = :File + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.File" self.primary_key = :FileId alias_attribute :file_index, :FileIndex alias_attribute :job_id, :JobId alias_attribute :path_id, :PathId alias_attribute :filename_id, :FilenameId alias_attribute :delta_seq, :DeltaSeq alias_attribute :mark_id, :MarkId alias_attribute :l_stat, :LStat alias_attribute :md5, :MD5 belongs_to :path, foreign_key: :PathId belongs_to :filename, foreign_key: :FilenameId belongs_to :job, foreign_key: :JobId has_many :base_files, foreign_key: :FileId end diff --git a/app/models/base_file.rb b/app/models/base_file.rb index a7eb94a..9cad624 100644 --- a/app/models/base_file.rb +++ b/app/models/base_file.rb @@ -1,17 +1,19 @@ # The BaseFiles table contains all the File references for a particular JobId that point # to a Base file - i.e. they were previously saved and hence were not saved in the current # JobId but in BaseJobId under FileId. class BaseFile < ActiveRecord::Base - self.table_name = :BaseFiles + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.BaseFiles" self.primary_key = :BaseId alias_attribute :id, :BaseId alias_attribute :base_job_id, :BaseJobId alias_attribute :job_id, :JobId alias_attribute :file_id, :FileId alias_attribute :file_index, :FileIndex belongs_to :base_job, foreign_key: :BaseJobId, class_name: :Job belongs_to :job, foreign_key: :JobId belongs_to :bacula_file, foreign_key: :FileId end diff --git a/app/models/cd_image.rb b/app/models/cd_image.rb index 334935f..a1f3845 100644 --- a/app/models/cd_image.rb +++ b/app/models/cd_image.rb @@ -1,6 +1,8 @@ class CdImage < ActiveRecord::Base - self.table_name = :CDImages + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.CDImages" self.primary_key = :MediaId alias_attribute :last_burn, :LastBurn end diff --git a/app/models/client.rb b/app/models/client.rb index a48f957..d44df53 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -1,141 +1,143 @@ # Bacula Client class. # All hosts that are getting backed up with Bacula have a Client entry, with # attributes concerning the Client. class Client < ActiveRecord::Base - self.table_name = :Client + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Client" self.primary_key = :ClientId alias_attribute :name, :Name alias_attribute :uname, :Uname alias_attribute :auto_prune, :AutoPrune alias_attribute :file_retention, :FileRetention alias_attribute :job_retention, :JobRetention has_many :jobs, foreign_key: :ClientId has_one :host, foreign_key: :name, primary_key: :Name scope :for_user, ->(user_id) { joins(host: :users).where(users: { id: user_id }) } DAY_SECS = 60 * 60 * 24 delegate :manually_inserted?, :origin, to: :host # Fetches the client's job_templates that are already persisted to # Bacula's configuration # # @return [ActiveRecord::Relation] of `JobTemplate` def persisted_jobs host.job_templates.where(baculized: true).includes(:fileset, :schedule) end # Fetches the client's performed jobs in reverse chronological order # # @return [ActiveRecord::Relation] of `Job` def recent_jobs jobs.order(EndTime: :desc).includes(:file_set) end # Helper method. It shows the client's job retention, # (which is expressed in seconds) in days. # # @return [Integer] def job_retention_days job_retention / DAY_SECS end # Helper method. It shows the client's file retention, # (which is expressed in seconds) in days. # # @return [Integer] def file_retention_days file_retention / DAY_SECS end # Helper method for auto_prune # # @return [String] 'yes' or 'no' def auto_prune_human auto_prune == 1 ? 'yes' : 'no' end # Helper method for displayin the last job's datetime in a nice format. def last_job_date_formatted if job_time = last_job_datetime I18n.l(job_time, format: :long) end end # Helper method for fetching the last job's datetime def last_job_datetime jobs.backup_type.last.try(:end_time) end # Fetches the first and last job's end times. # # @return [Array] of datetimes in proper format def backup_enabled_datetime_range jobs.backup_type.pluck(:end_time).minmax.map { |x| x.strftime('%Y-%m-%d') } end # Shows if a client has any backup jobs to Bacule config # # @return [Boolean] def is_backed_up? jobs.backup_type.any? end # Shows the total file size of the jobs that run for a specific client # # @return [Integer] Size in Bytes def backup_jobs_size jobs.backup_type.map(&:job_bytes).sum end # Shows the total files' count for the jobs that run for a specific client # # @return [Integer] File count def files_count jobs.map(&:job_files).sum end # Fetches the client's jobs that are running at the moment # # @return [Integer] def running_jobs jobs.running.count end # Displays the bacula config that is generated from the client's # host # # @return [String] def bacula_config return unless host host.baculize_config.join("\n") end # Fetches the job ids that will construct the desired restore # # @param file_set_id[Integer] the fileset # @param restore_point[Datetime] the restore point # # @return [Array] of ids def get_job_ids(file_set_id, restore_point) job_ids = {} backup_jobs = jobs.backup_type.terminated.where(file_set_id: file_set_id) backup_jobs = backup_jobs.where('EndTime < ?', restore_point) if restore_point job_ids['F'] = backup_jobs.where(level: 'F').pluck(:JobId).last return [] if job_ids['F'].nil? job_ids['D'] = backup_jobs.where(level: 'D').where("JobId > ?", job_ids['F']).pluck(:JobId).last job_ids['I'] = backup_jobs.where(level: 'I'). where("JobId > ?", job_ids['D'] || job_ids['F'] ).pluck(:JobId) job_ids.values.flatten.compact end # Fetches the bacula filesets that are associated with the client def file_sets FileSet.joins(:jobs).where(Job: { JobId: job_ids }).uniq end end diff --git a/app/models/configuration_setting.rb b/app/models/configuration_setting.rb index 9f30bd7..3586275 100644 --- a/app/models/configuration_setting.rb +++ b/app/models/configuration_setting.rb @@ -1,109 +1,111 @@ # ConfigurationSetting class describes a model that keeps the current Bacula # configuration. # # It has some hard coded settings as defaults. # Archiving's admins can enter new settings concerning: # # * jobs # * clients # # and override the default ones. # # ConfigurationSetting is supposed to have only one record in persisted in the database # which will hold the altered configuration as a patch to the defaults. # Admins can reset this change at any time. class ConfigurationSetting < ActiveRecord::Base + establish_connection ARCHIVING_CONF + serialize :job, JSON serialize :client, JSON serialize :pool, JSON JOB = { storage: :File, pool: :Default, messages: :Standard, priority: 10, :'Write Bootstrap' => '"/var/lib/bacula/%c.bsr"' } CLIENT = { catalog: 'MyCatalog', file_retention: 60, file_retention_period_type: 'days', job_retention: 180, job_retention_period_type: 'days', autoprune: 'yes' } POOL = { full: :Default, differential: :Default, incremental: :Default } RETENTION_PERIODS = %w{seconds minutes hours days weeks months quarters years} AUTOPRUNE_OPTIONS = ['yes', 'no'] # Fetches the current configuration for jobs. # # The current configuration is the last submitted record, patched to the default # settings. # If there is no record, the default settings are returned # # @return [Hash] with settings def self.current_job_settings (last || new).job.symbolize_keys.reverse_merge(JOB.dup) end # Fetches the current configuration for clients. # # The current configuration is the last submitted record, patched to the default # settings. # If there is no record, the default settings are returned # # @return [Hash] with settings def self.current_client_settings (last || new).client.symbolize_keys.reverse_merge(CLIENT.dup) end # Fetches the current configuration for pools. # # The current configuration is the last submitted record, patched to the default # settings. # If there is no record, the default settings are returned # # @return [Hash] with settings def self.current_pool_settings (last || new).pool.symbolize_keys.reverse_merge(POOL.dup) end # Fetches the record's configuration for jobs. # # The configuration is the record's configuration patched to the default # settings. # # @return [Hash] with settings def current_job_settings job.symbolize_keys.reverse_merge(JOB.dup) end # Fetches the record's configuration for clients. # # The configuration is the record's configuration patched to the default # settings. # # @return [Hash] with settings def current_client_settings client.symbolize_keys.reverse_merge(CLIENT.dup) end # Fetches the record's configuration for pools. # # The configuration is the record's configuration patched to the default # settings. # # @return [Hash] with settings def current_pool_settings pool.symbolize_keys.reverse_merge(POOL.dup) end end diff --git a/app/models/counter.rb b/app/models/counter.rb index 01a08aa..b2ff203 100644 --- a/app/models/counter.rb +++ b/app/models/counter.rb @@ -1,13 +1,15 @@ # Bacula Counter table # # The Counter table contains one entry for each permanent counter defined by the user. class Counter < ActiveRecord::Base - self.table_name = :Counters + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Counters" self.primary_key = :Counter alias_attribute :counter, :Counter alias_attribute :min_value, :MinValue alias_attribute :max_value, :MaxValue alias_attribute :current_value, :CurrentValue alias_attribute :wrap_coounter, :WrapCounter end diff --git a/app/models/device.rb b/app/models/device.rb index 64c342e..d16ac1b 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -1,22 +1,24 @@ class Device < ActiveRecord::Base - self.table_name = :Device + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Device" self.primary_key = :DeviceId alias_attribute :device_id, :DeviceId alias_attribute :name, :Name alias_attribute :media_type_id, :MediaTypeId alias_attribute :storage_id, :StorageId alias_attribute :dev_mounts, :DevMounts alias_attribute :dev_read_bytes, :DevReadBytes alias_attribute :dev_write_bytes, :DevWriteBytes alias_attribute :dev_read_bytes_since_cleaning, :DevReadBytesSinceCleaning alias_attribute :dev_write_bytes_since_cleaning, :DevWriteBytesSinceCleaning alias_attribute :dev_read_time, :DevReadTime alias_attribute :dev_write_time, :DevWriteTime alias_attribute :dev_read_time_since_cleaning, :DevReadTimeSinceCleaning alias_attribute :dev_write_time_since_cleaning, :DevWriteTimeSinceCleaning alias_attribute :cleaning_date, :CleaningDate alias_attribute :cleaning_period, :CleaningPeriod has_many :media, foreign_key: :DeviceId end diff --git a/app/models/file_set.rb b/app/models/file_set.rb index a5b4adf..2e07c23 100644 --- a/app/models/file_set.rb +++ b/app/models/file_set.rb @@ -1,19 +1,21 @@ # Bacula FileSet table. # # The FileSet table contains one entry for each FileSet that is used. # The MD5 signature is kept to ensure that if the user changes anything inside the FileSet, # it will be detected and the new FileSet will be used. # This is particularly important when doing an incremental update. # If the user deletes a file or adds a file, we need to ensure that a Full backup is done # prior to the next incremental. class FileSet < ActiveRecord::Base - self.table_name = :FileSet + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.FileSet" self.primary_key = :FileSetId alias_attribute :file_set_id, :FileSetId alias_attribute :file_set, :FileSet alias_attribute :md5, :MD5 alias_attribute :create_time, :CreateTime has_many :jobs, foreign_key: :FileSetId end diff --git a/app/models/filename.rb b/app/models/filename.rb index 63f2590..3340af3 100644 --- a/app/models/filename.rb +++ b/app/models/filename.rb @@ -1,14 +1,16 @@ # Bacula Filename # # The Filename table contains the name of each file backed up with the path removed. # If different directories or machines contain the same filename, # only one copy will be saved in this table. class Filename < ActiveRecord::Base - self.table_name = :Filename + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Filename" self.primary_key = :FilenameId alias_attribute :filename_id, :FilenameId alias_attribute :name, :Name has_many :bacula_files, foreign_key: :FilenameId end diff --git a/app/models/fileset.rb b/app/models/fileset.rb index 7056f3a..cd6c9bc 100644 --- a/app/models/fileset.rb +++ b/app/models/fileset.rb @@ -1,105 +1,107 @@ # 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 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 # 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 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/host.rb b/app/models/host.rb index af5a670..77f663a 100644 --- a/app/models/host.rb +++ b/app/models/host.rb @@ -1,311 +1,314 @@ # The bacula database must be independent from all of our application logic. # For this reason we have Host which is the application equivalent of a Bacula Client. # # A host is being created from our application. When it receives all the configuration # which is required it gets dispatched to bacula through some configuration files. After # that, a client with the exact same config is generated by bacula. class Host < ActiveRecord::Base + establish_connection ARCHIVING_CONF + include Configuration::Host STATUSES = { pending: 0, configured: 1, dispatched: 2, deployed: 3, updated: 4, redispatched: 5, for_removal: 6, inactive: 7, blocked: 8 } enum origin: { institutional: 0, vima: 1, okeanos: 2 } serialize :email_recipients, JSON has_many :ownerships has_many :users, through: :ownerships, inverse_of: :hosts has_many :invitations belongs_to :client, class_name: :Client, foreign_key: :name, primary_key: :name belongs_to :verifier, class_name: :User, foreign_key: :verifier_id, primary_key: :id has_many :filesets, dependent: :destroy has_many :job_templates, dependent: :destroy has_many :schedules, dependent: :destroy validates :file_retention, :job_retention, :port, :password, presence: true validates :port, numericality: true validates :fqdn, presence: true, uniqueness: true validate :fqdn_format validate :valid_recipients scope :not_baculized, -> { - joins("left join Client on Client.Name = hosts.name").where(Client: { Name: nil }) + joins("left join #{Client.table_name} on #{Client.table_name}.Name = hosts.name"). + where(Client.table_name => { Name: nil }) } scope :in_bacula, -> { where( status: STATUSES.select { |k,_| [:deployed, :updated, :redispatched, :for_removal].include? k }.values ) } scope :unverified, -> { where(verified: false) } before_validation :set_retention, :unset_baculized, :sanitize_name, :sanitize_email_recipients state_machine :status, initial: :pending do STATUSES.each do |status_name, value| state status_name, value: value end after_transition [:dispatched, :redispatched, :configured, :updated] => :deployed do |host| host.job_templates.enabled. update_all(baculized: true, baculized_at: Time.now, updated_at: Time.now) end event :add_configuration do transition [:pending, :dispatched, :inactive] => :configured end event :dispatch do transition :configured => :dispatched end event :redispatch do transition :updated => :redispatched end event :set_deployed do transition [:dispatched, :redispatched, :configured, :updated] => :deployed end event :change_deployed_config do transition [:deployed, :redispatched, :for_removal] => :updated end event :mark_for_removal do transition [:dispatched, :deployed, :updated, :redispatched] => :for_removal end event :set_inactive do transition [:deployed, :dispatched, :updated, :redispatched] => :inactive end event :disable do transition all => :pending end event :block do transition all - [:blocked] => :blocked end event :unblock do transition :blocked => :pending end end # Determines if a host has enabled jobs in order to be dispatched to Bacula # # @return [Boolean] def bacula_ready? job_templates.enabled.any? end # Shows the host's auto_prune setting def auto_prune_human client_settings[:autoprune] end # Uploads the host's config to bacula # Reloads bacula server # # It updates the host's status accordingly def dispatch_to_bacula return false if not needs_dispatch? bacula_handler.deploy_config end # Removes a Host from bacula configuration. # Reloads bacula server # # If all go well it changes the host's status and returns true # # @param force[Boolean] forces removal def remove_from_bacula(force=false) return false if not (force || needs_revoke?) bacula_handler.undeploy_config end # Restores a host's backup to a preselected location # # @param fileset_id[Integer] the desired fileset # @param location[String] the desired restore location # @param restore_point[Datetime] the desired restore_point datetime def restore(file_set_id, location, restore_point=nil) return false if not restorable? job_ids = client.get_job_ids(file_set_id, restore_point) file_set_name = FileSet.find(file_set_id).file_set bacula_handler.restore(job_ids, file_set_name, restore_point, location) end # Runs the given backup job ASAP def backup_now(job_name) bacula_handler.backup_now(job_name) end # Disables all jobs and sends the configuration to Bacula def disable_jobs_and_update job_templates.update_all(enabled: false) bacula_handler.deploy_config end # Disables all jobs if needed and then locks the host def disable_jobs_and_lock return false if can_set_inactive? && !disable_jobs_and_update block end # Determinex weather a host: # # * has all it takes to be deployed but # * the config is not yet sent to bacula # # @return [Boolean] def needs_dispatch? verified? && (can_dispatch? || can_redispatch?) end # Determines weather a host is marked for removal # # @return [Boolean] def needs_revoke? for_removal? end # Handles the host's job changes by updating the host's status def recalculate add_configuration || change_deployed_config end # Fetches an info message concerning the host's deploy status def display_message if !verified? { message: 'Your host needs to be verified by an admin', severity: :alert } elsif pending? { message: 'client not configured yet', severity: :alert } elsif configured? || dispatched? { message: 'client not deployed to Bacula', severity: :alert } elsif updated? || redispatched? { message: 'client configuration changed, deploy needed', severity: :alert } elsif for_removal? { message: 'pending client configuration withdraw', severity: :error } elsif inactive? { message: 'client disabled', severity: :alert } elsif blocked? { message: 'client disabled by admin.', severity: :error } end end # Determines if a host can issue a restore job. # # @return [Boolean] true if the host's client can issue a restore job def restorable? client.present? && client.is_backed_up? end # @return [User] the first of the host's users def first_user users.order('ownerships.created_at asc').first end # Marks the host as verified and sets the relevant metadata # # @param admin_verifier[Integer] the verifier's id def verify(admin_verifier) self.verified = true self.verifier_id = admin_verifier self.verified_at = Time.now recipients = users.pluck(:email) if save UserMailer.notify_for_verification(recipients, name).deliver if recipients.any? return true end false end # Determines if a host can be disabled or not. # Equivalent to is_deployed # # @return [Boolean] def can_be_disabled? dispatched? || deployed? || updated? || redispatched? end # Determines if a host is inserted manually from the user or # provided as an option from a list by the system via a third party # like ViMa or Okeanos # # @return [Boolean] def manually_inserted? institutional? end private # automatic setters def sanitize_name self.name = fqdn end # Sets the file and job retention according to the global settings def set_retention self.file_retention = client_settings[:file_retention] self.file_retention_period_type = client_settings[:file_retention_period_type] self.job_retention = client_settings[:job_retention] self.job_retention_period_type = client_settings[:job_retention_period_type] end def unset_baculized self.baculized = false if new_record? true end def sanitize_email_recipients self.email_recipients.reject!(&:blank?) end # validation def fqdn_format regex = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(? { where(job_status: 'R') } scope :terminated, -> { where(job_status: 'T') } scope :backup_type, -> { where(type: 'B') } scope :restore_type, -> { where(type: 'R') } HUMAN_STATUS = { 'A' => 'Canceled by user', 'B' => 'Blocked', 'C' => 'Created, not yet running', 'D' => 'Verify found differences', 'E' => 'Terminated with errors', 'F' => 'Waiting for Client', 'M' => 'Waiting for media mount', 'R' => 'Running', 'S' => 'Waiting for Storage daemon', 'T' => 'Completed successfully', 'a' => 'SD despooling attributes', 'c' => 'Waiting for client resource', 'd' => 'Waiting on maximum jobs', 'e' => 'Non-fatal error', 'f' => 'Fatal error', 'i' => 'Doing batch insert file records', 'j' => 'Waiting for job resource', 'm' => 'Waiting for new media', 'p' => 'Waiting on higher priority jobs', 's' => 'Waiting for storage resource', 't' => 'Waiting on start time' } paginates_per 20 def level_human { 'F' => 'Full', 'D' => 'Differential', 'I' => 'Incremental' }[level] end def status_human HUMAN_STATUS[job_status] end def fileset file_set.try(:file_set) || '-' end def start_time_formatted if start_time I18n.l(start_time, format: :long) end end def end_time_formatted if end_time I18n.l(end_time, format: :long) end end end diff --git a/app/models/job_histo.rb b/app/models/job_histo.rb index af72743..209dd0d 100644 --- a/app/models/job_histo.rb +++ b/app/models/job_histo.rb @@ -1,39 +1,41 @@ # Bacula JobHisto table. # # The bf JobHisto table is the same as the Job table, # but it keeps long term statistics (i.e. it is not pruned with the Job). class JobHisto < ActiveRecord::Base - self.table_name = :JobHisto + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.JobHisto" alias_attribute :job_id, :JobId alias_attribute :job, :Job alias_attribute :name, :Name alias_attribute :type, :Type alias_attribute :level, :Level alias_attribute :client_id, :ClientId alias_attribute :job_status, :JobStatus alias_attribute :sched_time, :SchedTime alias_attribute :start_time, :StartTime alias_attribute :end_time, :EndTime alias_attribute :real_end_time, :RealEndTime alias_attribute :job_t_date, :JobTDate alias_attribute :vol_session_id, :VolSessionId alias_attribute :vol_session_time, :VolSessionTime alias_attribute :job_files, :JobFiles alias_attribute :job_bytes, :JobBytes alias_attribute :read_bytes, :ReadBytes alias_attribute :job_errors, :JobErrors alias_attribute :job_missing_files, :JobMissingFiles alias_attribute :pool_id, :PoolId alias_attribute :file_set_id, :FileSetId alias_attribute :prior_job_id, :PriorJobId alias_attribute :purged_files, :PurgedFiles alias_attribute :has_base, :HasBase alias_attribute :has_cache, :HasCache alias_attribute :reviewed, :Reviewed alias_attribute :comment, :Comment belongs_to :client, foreign_key: :ClientId belongs_to :pool, foreign_key: :PoolId belongs_to :file_set, foreign_key: :FileSetId end diff --git a/app/models/job_media.rb b/app/models/job_media.rb index 7480f77..fb77a78 100644 --- a/app/models/job_media.rb +++ b/app/models/job_media.rb @@ -1,35 +1,37 @@ # Bacula JobMedia table. # # The JobMedia table contains one entry at the following: # # * start of the job, # * start of each new tape file, # * start of each new tape, # * end of the job. # # Since by default, a new tape file is written every 2GB, in general, you will have more # than 2 JobMedia records per Job. # The number can be varied by changing the "Maximum File Size" specified in the Device resource. # This record allows Bacula to efficiently position close to (within 2GB) any given file in a backup. # For restoring a full Job, these records are not very important, but if you want to retrieve a # single file that was written near the end of a 100GB backup, the JobMedia records can speed it # up by orders of magnitude by permitting forward spacing files and blocks rather than reading # the whole 100GB backup. class JobMedia < ActiveRecord::Base - self.table_name = :JobMedia + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.JobMedia" self.primary_key = :JobMediaId alias_attribute :job_media_id, :JobMediaId alias_attribute :job_id, :JobId alias_attribute :media_id, :MediaId alias_attribute :first_index, :FirstIndex alias_attribute :last_index, :LastIndex alias_attribute :start_file, :StartFile alias_attribute :end_file, :EndFile alias_attribute :start_block, :StartBlock alias_attribute :end_block, :EndBlock alias_attribute :vol_index, :VolIndex belongs_to :Job, foreign_key: :JobId belongs_to :Media, foreign_key: :MediaId end diff --git a/app/models/job_template.rb b/app/models/job_template.rb index 7b41e99..9d95bc8 100644 --- a/app/models/job_template.rb +++ b/app/models/job_template.rb @@ -1,112 +1,114 @@ # 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}" } + 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 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 # 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? ? "message_#{host.name}" : :Standard @job_settings ||= ConfigurationSetting.current_job_settings.merge(messages: messages) end end diff --git a/app/models/location.rb b/app/models/location.rb index 15abd8b..c258092 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -1,15 +1,17 @@ # Bacula Location table. # # The Location table defines where a Volume is physically. class Location < ActiveRecord::Base - self.table_name = :Location + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Location" self.primary_key = :LocationId alias_attribute :location_id, :LocationId alias_attribute :location, :Location alias_attribute :cost, :Cost alias_attribute :enabled, :Enabled has_many :media, foreign_key: :LocationId has_many :location_logs, foreign_key: :LocationId end diff --git a/app/models/location_log.rb b/app/models/location_log.rb index 57119c8..3157be9 100644 --- a/app/models/location_log.rb +++ b/app/models/location_log.rb @@ -1,16 +1,18 @@ # Bacula LocationLog table class LocationLog < ActiveRecord::Base - self.table_name = :LocationLog + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.LocationLog" self.primary_key = :LocLogId alias_attribute :loc_log_id, :LocLogId alias_attribute :date, :Date alias_attribute :comment, :Comment alias_attribute :media_id, :MediaId alias_attribute :location_id, :LocationId alias_attribute :new_vol_status, :NewVolStatus alias_attribute :new_enabled, :NewEnabled belongs_to :media, foreign_key: :MediaId belongs_to :location, foreign_key: :LocationId end diff --git a/app/models/log.rb b/app/models/log.rb index 672d966..917d674 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -1,22 +1,24 @@ # Bacula Log table. # # The Log table contains a log of all Job output. class Log < ActiveRecord::Base - self.table_name = :Log + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Log" self.primary_key = :LogId alias_attribute :log_id, :LogId alias_attribute :job_id, :JobId alias_attribute :time, :Time alias_attribute :log_text, :LogText belongs_to :job, foreign_key: :JobId paginates_per 20 def time_formatted if time I18n.l(time, format: :long) end end end diff --git a/app/models/media.rb b/app/models/media.rb index 7938be1..bbf19bc 100644 --- a/app/models/media.rb +++ b/app/models/media.rb @@ -1,61 +1,63 @@ # Bacula Media table (Volume) # # Media table contains one entry for each volume, that is each tape, cassette (8mm, DLT, DAT, ...), # or file on which information is or was backed up. # There is one Volume record created for each of the NumVols specified in the # Pool resource record. class Media < ActiveRecord::Base - self.table_name = :Media + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Media" self.primary_key = :MediaId alias_attribute :media_id, :MediaId alias_attribute :volume_name, :VolumeName alias_attribute :slot, :Slot alias_attribute :pool_id, :PoolId alias_attribute :media_type, :MediaType alias_attribute :media_type_id, :MediaTypeId alias_attribute :label_type, :LabelType alias_attribute :first_written, :FirstWritten alias_attribute :last_written, :LastWritten alias_attribute :label_date, :LabelDate alias_attribute :vol_jobs, :VolJobs alias_attribute :vol_files, :VolFiles alias_attribute :vol_blocks, :VolBlocks alias_attribute :vol_mounts, :VolMounts alias_attribute :vol_bytes, :VolBytes alias_attribute :vol_parts, :VolParts alias_attribute :vol_errors, :VolErrors alias_attribute :vol_writes, :VolWrites alias_attribute :vol_capacity_bytes, :VolCapacityBytes alias_attribute :vol_status, :VolStatus alias_attribute :enabled, :Enabled alias_attribute :recycle, :Recycle alias_attribute :action_on_purge, :ActionOnPurge alias_attribute :vol_retention, :VolRetention alias_attribute :vol_use_duration, :VolUseDuration alias_attribute :max_vol_jobs, :MaxVolJobs alias_attribute :max_vol_files, :MaxVolFiles alias_attribute :max_vol_bytes, :MaxVolBytes alias_attribute :in_changer, :InChanger alias_attribute :storage_id, :StorageId alias_attribute :device_id, :DeviceId alias_attribute :media_addressing, :MediaAddressing alias_attribute :vol_read_time, :VolReadTime alias_attribute :vol_write_time, :VolWriteTime alias_attribute :end_file, :EndFile alias_attribute :end_block, :EndBlock alias_attribute :location_id, :LocationId alias_attribute :recycle_count, :RecycleCount alias_attribute :initial_write, :InitialWrite alias_attribute :scratch_pool_id, :ScratchPoolId alias_attribute :recycle_pool_id, :RecyclePoolId alias_attribute :comment, :Comment belongs_to :pool, foreign_key: :PoolId belongs_to :storage, foreign_key: :StorageId belongs_to :device, foreign_key: :DeviceId belongs_to :location, foreign_key: :LocationId has_many :job_media, foreign_key: :MediaId has_many :location_logs, foreign_key: :MediaId end diff --git a/app/models/media_type.rb b/app/models/media_type.rb index b5e4664..ac18ab5 100644 --- a/app/models/media_type.rb +++ b/app/models/media_type.rb @@ -1,8 +1,10 @@ class MediaType < ActiveRecord::Base - self.table_name = :MediaType + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.MediaType" self.primary_key = :MediaTypeId alias_attribute :media_type_id, :MediaTypeId alias_attribute :media_type, :MediaType alias_attribute :read_only, :ReadOnly end diff --git a/app/models/ownership.rb b/app/models/ownership.rb index 3962147..f46f8de 100644 --- a/app/models/ownership.rb +++ b/app/models/ownership.rb @@ -1,4 +1,6 @@ class Ownership < ActiveRecord::Base + establish_connection ARCHIVING_CONF + belongs_to :user belongs_to :host end diff --git a/app/models/path.rb b/app/models/path.rb index dd6eacd..a2e978a 100644 --- a/app/models/path.rb +++ b/app/models/path.rb @@ -1,16 +1,18 @@ # Bacula Path table. # # The Path table contains shown above the path or directory names of all directories on # the system or systems. # As with the filename, only one copy of each directory name is kept regardless of how # many machines or drives have the same directory. # These path names should be stored in Unix path name format. class Path < ActiveRecord::Base - self.table_name = :Path + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Path" self.primary_key = :PathId alias_attribute :path_id, :PathId alias_attribute :path, :Path has_many :bacula_files, foreign_key: :PathId end diff --git a/app/models/path_hierarchy.rb b/app/models/path_hierarchy.rb index 2e68c02..8a8dd2c 100644 --- a/app/models/path_hierarchy.rb +++ b/app/models/path_hierarchy.rb @@ -1,7 +1,9 @@ class PathHierarchy < ActiveRecord::Base - self.table_name = :PathHierarchy + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.PathHierarchy" self.primary_key = :PathId alias_attribute :path_id, :PathId alias_attribute :p_path_id, :PPathId end diff --git a/app/models/path_visibility.rb b/app/models/path_visibility.rb index 418dce6..9a9b4d2 100644 --- a/app/models/path_visibility.rb +++ b/app/models/path_visibility.rb @@ -1,9 +1,11 @@ class PathVisibility < ActiveRecord::Base - self.table_name = :PathVisibility + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.PathVisibility" self.primary_key = [:JobId, :PathId] alias_attribute :path_id, :PathId alias_attribute :job_id, :JobId alias_attribute :size, :Size alias_attribute :files, :Files end diff --git a/app/models/pool.rb b/app/models/pool.rb index 8767743..bb62ba6 100644 --- a/app/models/pool.rb +++ b/app/models/pool.rb @@ -1,116 +1,118 @@ # Bacula Pool # # The Pool table contains one entry for each media pool controlled by Bacula in # this database. One media record exists for each of the NumVols contained in the Pool. # The PoolType is a Bacula defined keyword. # The MediaType is defined by the administrator, and corresponds to the MediaType # specified in the Director's Storage definition record. # The CurrentVol is the sequence number of the Media record for the current volume. class Pool < ActiveRecord::Base - self.table_name = :Pool + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Pool" self.primary_key = :PoolId alias_attribute :pool_id, :PoolId alias_attribute :name, :Name alias_attribute :num_vols, :NumVols alias_attribute :max_vols, :MaxVols alias_attribute :use_once, :UseOnce alias_attribute :use_catalog, :UseCatalog alias_attribute :accept_any_volume, :AcceptAnyVolume alias_attribute :vol_retention, :VolRetention alias_attribute :vol_use_duration, :VolUseDuration alias_attribute :max_vol_jobs, :MaxVolJobs alias_attribute :max_vol_files, :MaxVolFiles alias_attribute :max_vol_bytes, :MaxVolBytes alias_attribute :auto_prune, :AutoPrune alias_attribute :recycle, :Recycle alias_attribute :action_on_purge, :ActionOnPurge alias_attribute :pool_type, :PoolType alias_attribute :label_type, :LabelType alias_attribute :label_format, :LabelFormat alias_attribute :enabled, :Enabled alias_attribute :scratch_pool_id, :ScratchPoolId alias_attribute :recycle_pool_id, :RecyclePoolId alias_attribute :next_pool_id, :NextPoolId alias_attribute :migration_high_bytes, :MigrationHighBytes alias_attribute :migration_low_bytes, :MigrationLowBytes alias_attribute :migration_time, :MigrationTime has_many :jobs, foreign_key: :PoolId has_many :media, foreign_key: :PoolId validates_confirmation_of :name BOOLEAN_OPTIONS = [['no', 0], ['yes', 1]] POOL_OPTIONS = [:name, :vol_retention, :use_once, :auto_prune, :recycle, :max_vols, :max_vol_jobs, :max_vol_files, :max_vol_bytes, :label_format] # @return [Array] of all the available pools by name def self.available_options pluck(:Name) end # Persists the unpersisted record to bacula via its bacula handler # # @return [Boolean] according to the persist status def submit_to_bacula return false if !valid? || !ready_for_bacula? sanitize_names bacula_handler.deploy_config end # Constructs an array where each element is a line for the Job's bacula config # # @return [Array] def to_bacula_config_array ['Pool {'] + options_array.map { |x| " #{x}" } + ['}'] end # Human readable volume retention # # @return [String] the volume's retention in days def vol_retention_human "#{vol_retention_days} days" end # volume retention in days def vol_retention_days vol_retention / 1.day.to_i end private # proxy object for bacula pool handling def bacula_handler BaculaPoolHandler.new(self) end # pool names and label formats should only contain alphanumeric values def sanitize_names self.name = name.gsub(/[^a-zA-Z0-9]/, '_') self.label_format = label_format.gsub(/[^a-zA-Z0-9]/, '_') end def options_array boolean_options = Hash[BOOLEAN_OPTIONS].invert [ "Name = \"#{name}\"", "Volume Retention = #{vol_retention_human}", "Use Volume Once = #{boolean_options[use_once.to_i]}", "AutoPrune = #{boolean_options[auto_prune.to_i]}", "Recycle = #{boolean_options[recycle.to_i]}", "Maximum Volumes = #{max_vols}", "Maximum Volume Jobs = #{max_vol_jobs}", "Maximum Volume Files = #{max_vol_files}", "Maximum Volume Bytes = #{max_vol_bytes}G", "Label Format = \"#{label_format}\"", "Pool Type = Backup" ] end def ready_for_bacula? POOL_OPTIONS.all? { |attr| self.send(attr).present? } end end diff --git a/app/models/restore_object.rb b/app/models/restore_object.rb index bd4d096..fb5f83a 100644 --- a/app/models/restore_object.rb +++ b/app/models/restore_object.rb @@ -1,16 +1,18 @@ class RestoreObject < ActiveRecord::Base - self.table_name = :RestoreObject + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.RestoreObject" self.primary_key = :RestoreObjectId alias_attribute :restore_object_id, :RestoreObjectId alias_attribute :object_name, :ObjectName alias_attribute :restore_object, :RestoreObject alias_attribute :plugin_name, :PluginName alias_attribute :object_length, :ObjectLength alias_attribute :object_full_length, :ObjectFullLength alias_attribute :object_index, :ObjectIndex alias_attribute :object_type, :ObjectType alias_attribute :file_index, :FileIndex alias_attribute :job_id, :JobId alias_attribute :object_compression, :ObjectCompression end diff --git a/app/models/schedule.rb b/app/models/schedule.rb index f382c49..d81bd2b 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -1,41 +1,43 @@ # 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 # 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 end diff --git a/app/models/schedule_run.rb b/app/models/schedule_run.rb index 32899f2..455835a 100644 --- a/app/models/schedule_run.rb +++ b/app/models/schedule_run.rb @@ -1,150 +1,152 @@ # ScheduleRun is a helper class that modelizes the run directives of a schedule # resource. # # It can have 3 levels: # # * full # * differential # * incremental # # Each ScheduleRun instance holds info about the execution time of the schedule: # # * month specification is optional and is described by: # - month: full name (eg april) | month name three first letters (eg jul) # - month_range: month-month # - monthly: 'monthly' # * day specification is required and is described by: # - week_number (optional): weeks number in full text (eg: third, fourth) # - week_range (optional): week_number-week_number # - day: first three letters of day (eg: mon, fri) # - day_range: day-day # * time specification is required and is described by: # - 24h time (eg: 03:00) # # Schedule Run examples: # # Level=Full monthly first mon at 12:21 # Level=Full first sun at 12:34 # Level=Differential second-fifth sat at 15:23 # Level=Incremental mon-sat at 08:00 class ScheduleRun < ActiveRecord::Base + establish_connection ARCHIVING_CONF + enum level: { full: 0, differential: 1, incremental: 2 } MONTH_KW = %w{jan feb mar apr may jun jul aug sep oct nov dec january february march april may june july august september october november december} DAYS = (1..31).to_a WDAY_KW = %w{mon tue wed thu fri sat sun} WEEK_KW = %w{first second third fourth fifth} belongs_to :schedule validates :day, :time, :level, presence: true validate :correct_chars validate :month_valid, if: "month.present?" validate :time_valid validate :day_valid def self.options_for_select levels.keys.zip levels.keys end # Builds a sane default schedule_run # # @return [ScheduleRun] def default_run self.level = :full self.day = "first sun" self.time = '04:00' end # Composes the schedule line for the bacula configuration # # @return [String] def schedule_line [ level_to_config, pool_to_config, month, day, "at #{time}"].join(" ") end private def correct_chars [:month, :day, :time].each do |x| if self.send(x) && self.send(x).to_s.gsub(/[0-9a-zA-Z\-:]/, '').present? self.errors.add(x, 'Invalid characters') end end end def month_valid if !month_format? && !valid_month_range? self.errors.add(:month, 'Invalid month') end end def month_format? month_regex = "^(#{MONTH_KW.join('|')}|monthly)$" month.match(month_regex).present? end def valid_month_range? months = month.split('-') return false if months.length != 2 MONTH_KW.index(months.last) % 12 - MONTH_KW.index(months.first) % 12 > 0 end def time_valid if !time.match(/^([01][0-9]|2[0-3]):[0-5][0-9]$/) self.errors.add(:time, 'Invalid time') end end def day_valid components = day.split(' ') if components.length < 1 || components.length > 2 self.errors.add(:day, 'Invalid day') return false end if !valid_day?(components.last) && !valid_day_range?(components.last) self.errors.add(:day, 'Invalid day') return false end if components.length == 2 && !valid_week?(components.first) && !valid_week_range?(components.first) self.errors.add(:day, 'Invalid day') return false end true end def valid_day?(a_day) WDAY_KW.include? a_day end def valid_day_range?(a_range) days = a_range.split('-') return false if days.length != 2 WDAY_KW.index(days.last) - WDAY_KW.index(days.first) > 0 end def valid_week?(a_week) WEEK_KW.include? a_week end def valid_week_range?(a_range) weeks = a_range.split('-') return false if weeks.length != 2 WEEK_KW.index(weeks.last) - WEEK_KW.index(weeks.first) > 0 end def level_to_config "Level=#{level.capitalize}" end def pool_to_config "Pool=#{ConfigurationSetting.current_pool_settings[level.to_sym]}" end end diff --git a/app/models/status.rb b/app/models/status.rb index c6667ed..79cc957 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -1,14 +1,16 @@ # Bacula Status table. # # Status table is a lookup table linking: # # * jobs' status codes and # * status codes' messages class Status < ActiveRecord::Base - self.table_name = :Status + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Status" self.primary_key = :JobStatus alias_attribute :job_status, :JobStatus alias_attribute :job_status_long, :JobStatusLong alias_attribute :severity, :Severity end diff --git a/app/models/storage.rb b/app/models/storage.rb index 016e9f6..30df4d3 100644 --- a/app/models/storage.rb +++ b/app/models/storage.rb @@ -1,17 +1,19 @@ # Bacula Storage table # # The Storage table contains one entry for each Storage used. class Storage < ActiveRecord::Base - self.table_name = :Storage + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Storage" self.primary_key = :StorageId alias_attribute :storage_id, :StorageId alias_attribute :name, :Name alias_attribute :auto_changer, :AutoChanger has_many :media, foreign_key: :StorageId def self.available_options pluck(:Name) end end diff --git a/app/models/unsaved_file.rb b/app/models/unsaved_file.rb index 72501c4..0abc81a 100644 --- a/app/models/unsaved_file.rb +++ b/app/models/unsaved_file.rb @@ -1,10 +1,12 @@ # Bacula UnsavedFile table class UnsavedFile < ActiveRecord::Base - self.table_name = :UnsavedFiles + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.UnsavedFiles" self.primary_key = :UnsavedId alias_attribute :unsaved_id, :UnsavedId alias_attribute :job_id, :JobId alias_attribute :path_id, :PathId alias_attribute :filename_id, :FilenameId end diff --git a/app/models/user.rb b/app/models/user.rb index 8684a93..8bf783d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,122 +1,123 @@ class User < ActiveRecord::Base + establish_connection ARCHIVING_CONF attr_accessor :password, :retype_password serialize :temp_hosts, JSON has_many :ownerships has_many :hosts, through: :ownerships, inverse_of: :users has_many :invitations enum user_type: { institutional: 0, vima: 1, okeanos: 2, admin: 3 } validates :user_type, presence: true validates :username, presence: true, uniqueness: { scope: :user_type } validates :email, presence: true, uniqueness: { scope: :user_type } before_create :confirm_passwords, if: :admin? # Returns an admin user with the given password # # @param username[String] username from user input # @param a_password[String] password from user input # # @return [User] the admin user or nil def self.fetch_admin_with_password(username, a_password) hashed_pass = Digest::SHA256.hexdigest(a_password + Rails.application.secrets.salt) admin = User.admin.find_by_username_and_password_hash(username, hashed_pass) admin end # Composes the user's display name from the user's username and email # # @return [String] def display_name "#{username} <#{email}>" end # Determines if the user must select hosts from a list or enter their # FQDN manually # # @return [Boolean] def needs_host_list? vima? || okeanos? end # Determines if the user is editable or not. # Editable users are only admin users, all others come from 3rd party authorization # # @return [Boolean] def editable? admin? end # Marks a user as not enabled def ban self.enabled = false save end # Marks a user as enabled def unban self.enabled = true save end # Stores a hashed password as a password_hash # # @param a_password[String] the user submitted password # # @return [Boolean] the save exit status def add_password(a_password) self.password_hash = Digest::SHA256.hexdigest(a_password + Rails.application.secrets.salt) self.save end # Fetches the user's unverified hosts # # @return [Array] of Strings containing the hosts' names def unverified_hosts hosts.unverified.pluck(:name) end # Fetches the user's hosts that are being backed up by bacula # # @return [Array] of Strings configuration the host's names def baculized_hosts hosts.in_bacula.pluck(:name) end # Fetches the user's hosts that are NOT being backed up by bacula # # @return [Array] of Strings configuration the host's names def non_baculized_hosts hosts.not_baculized.pluck(:name) end # Determines if a vima user needs to update his hosts' list # # @return [Boolean] def refetch_hosts? return false unless vima? return true if hosts_updated_at.nil? hosts_updated_at < Archiving.settings[:skip_host_fetch_time_period].ago end private def confirm_passwords if password.blank? self.errors.add(:password, 'Must give a password') return false end if password != retype_password self.errors.add(:password, 'Passwords mismatch') self.errors.add(:retype_password, 'Passwords mismatch') return false end true end end diff --git a/app/models/version.rb b/app/models/version.rb index e823f49..9c4f3b3 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -1,10 +1,12 @@ # Bacula Version table # # The Version table defines the Bacula database version number. # Bacula checks this number before reading the database to ensure that it is # compatible with the Bacula binary file. class Version < ActiveRecord::Base - self.table_name = :Version + establish_connection BACULA_CONF + + self.table_name = "#{connection_config[:database]}.Version" alias_attribute :version_id, :VersionId end diff --git a/config/application.rb b/config/application.rb index ee4b8d0..06e1153 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,45 +1,52 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' # Production doesn't use bundler # you've limited to :test, :development, or :production. if ENV['RAILS_ENV'] != 'production' Bundler.require(*Rails.groups) else # Dependencies to load before starting rails in production require 'kaminari' require 'jquery-rails' require 'state_machine' require 'beaneater' require 'oauth2' require 'warden' require 'net/scp' end module Archiving def self.settings opts = nil @settings ||= {} return @settings if opts.nil? @settings.merge! opts @settings end class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Store/Read localtime from the database config.time_zone = 'Athens' config.active_record.default_timezone = :local # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de config.autoload_paths << Rails.root.join('lib') - # config.x = {} end end + +db_conf = YAML::load(File.open(File.join("#{Rails.root}/config/database.yml"))) +bacula_db_conf = YAML::load(File.open(File.join("#{Rails.root}/config/database_bacula.yml"))) + +ARCHIVING_CONF = db_conf[Rails.env] +BACULA_CONF = bacula_db_conf[Rails.env] + +Archiving::Application.config.active_record.table_name_prefix = "#{ARCHIVING_CONF['database']}."