diff --git a/app/models/client.rb b/app/models/client.rb index d44df53..eb737b5 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -1,143 +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 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) + jobs.order(EndTime: :desc).includes(:file_set, :logs) 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/job.rb b/app/models/job.rb index 031d87b..128aa11 100644 --- a/app/models/job.rb +++ b/app/models/job.rb @@ -1,129 +1,145 @@ # Bacula Job table. # # The Job table contains one record for each Job run by Bacula. # Thus normally, there will be one per day per machine added to the database. # Note, the JobId is used to index Job records in the database, and it often is shown to the user # in the Console program. # However, care must be taken with its use as it is not unique from database to database. # For example, the user may have a database for Client data saved on machine Rufus and another # database for Client data saved on machine Roxie. # In this case, the two database will each have JobIds that match those in another database. # For a unique reference to a Job, see Job below. # # The Name field of the Job record corresponds to the Name resource record given in the # Director's configuration file. # Thus it is a generic name, and it will be normal to find many Jobs (or even all Jobs) # with the same Name. # # The Job field contains a combination of the Name and the schedule time of the Job by the Director. # Thus for a given Director, even with multiple Catalog databases, the Job will contain a unique # name that represents the Job. # # For a given Storage daemon, the VolSessionId and VolSessionTime form a unique identification # of the Job. # # This will be the case even if multiple Directors are using the same Storage daemon. # # The Job Type (or simply Type) can have one of the following values: class Job < ActiveRecord::Base establish_connection BACULA_CONF self.table_name = "#{connection_config[:database]}.Job" self.primary_key = :JobId 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 :pool, foreign_key: :PoolId belongs_to :file_set, foreign_key: :FileSetId belongs_to :client, foreign_key: :ClientId has_many :bacula_files, foreign_key: :JobId has_many :base_files, foreign_key: :BaseJobId has_many :job_media, foreign_key: :JobId has_many :logs, foreign_key: :JobId scope :running, -> { 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 + # Extracts the job's compression info by looking at the job's + # logs + # + # @return [String] the compression + def compression + logs.map { |log| log.compression }.uniq.compact.first + end + + # Extracts the job's encryption info by looking at the job's + # logs + # + # @return [String] the encryption + def encryption + logs.map { |log| log.encryption }.uniq.compact.first + 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/log.rb b/app/models/log.rb index 917d674..6a1f62a 100644 --- a/app/models/log.rb +++ b/app/models/log.rb @@ -1,24 +1,38 @@ # Bacula Log table. # # The Log table contains a log of all Job output. class Log < ActiveRecord::Base 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 + + # Extracts the log's compression info if there is any data available + # + # @return [String] the compression or nil + def compression + $1.strip if log_text =~ /.*Software Compression:(.*)\n.*/ + end + + # Extracts the log's encryption info if there is any data available + # + # @return [String] the encryption or nil + def encryption + $1.strip if log_text =~ /.*Encryption:(.*)\n.*/ + end end diff --git a/app/views/admin/clients/_recent_job.html.erb b/app/views/admin/clients/_recent_job.html.erb index 109626b..4eed9be 100644 --- a/app/views/admin/clients/_recent_job.html.erb +++ b/app/views/admin/clients/_recent_job.html.erb @@ -1,11 +1,13 @@ <%= recent_job.name %> <%= recent_job.job_id %> <%= recent_job.level_human %> <%= recent_job.fileset %> <%= recent_job.start_time_formatted %> <%= recent_job.end_time_formatted %> <%= number_to_human_size(recent_job.job_bytes) %> <%= number_by_magnitude(recent_job.job_files) %> - <%= recent_job.status_human %> + <%= recent_job.status_human %> + <%= recent_job.encryption %> + <%= recent_job.compression %> diff --git a/app/views/admin/clients/_recent_jobs.html.erb b/app/views/admin/clients/_recent_jobs.html.erb index 96514c6..951966e 100644 --- a/app/views/admin/clients/_recent_jobs.html.erb +++ b/app/views/admin/clients/_recent_jobs.html.erb @@ -1,22 +1,24 @@
+ + <%= render partial: 'recent_job', collection: @jobs %>
Name JobId Level Fileset Started At Finished At Bytes Files StatusEncryptionCompression
diff --git a/app/views/clients/_recent_job.html.erb b/app/views/clients/_recent_job.html.erb index 109626b..4eed9be 100644 --- a/app/views/clients/_recent_job.html.erb +++ b/app/views/clients/_recent_job.html.erb @@ -1,11 +1,13 @@ <%= recent_job.name %> <%= recent_job.job_id %> <%= recent_job.level_human %> <%= recent_job.fileset %> <%= recent_job.start_time_formatted %> <%= recent_job.end_time_formatted %> <%= number_to_human_size(recent_job.job_bytes) %> <%= number_by_magnitude(recent_job.job_files) %> - <%= recent_job.status_human %> + <%= recent_job.status_human %> + <%= recent_job.encryption %> + <%= recent_job.compression %> diff --git a/app/views/clients/_recent_jobs.html.erb b/app/views/clients/_recent_jobs.html.erb index 6a39c49..4a8f125 100644 --- a/app/views/clients/_recent_jobs.html.erb +++ b/app/views/clients/_recent_jobs.html.erb @@ -1,22 +1,24 @@
+ + <%= render partial: 'clients/recent_job', collection: @jobs %>
Name JobId Level Fileset Started At Finished At Bytes Files StatusEncryptionCompression
diff --git a/app/views/kaminari/_gap.html.erb b/app/views/kaminari/_gap.html.erb index 4167981..2a860c6 100644 --- a/app/views/kaminari/_gap.html.erb +++ b/app/views/kaminari/_gap.html.erb @@ -1,10 +1,10 @@ <%# Non-link tag that stands for skipped pages... - available local variables current_page: a page object for the currently displayed page total_pages: total number of pages per_page: number of items to fetch per page remote: data-remote -%>
  • - <%= t('views.pagination.truncate').html_safe %> + . . .