Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F5402028
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Wed, Sep 9, 7:11 PM
Size
38 KB
Mime Type
text/x-diff
Expires
Fri, Sep 11, 7:11 PM (1 d, 8 h)
Engine
blob
Format
Raw Data
Handle
447678
Attached To
rARCHIVING archiving
View Options
diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb
index 354d912..f269754 100644
--- a/app/controllers/clients_controller.rb
+++ b/app/controllers/clients_controller.rb
@@ -1,57 +1,79 @@
class ClientsController < ApplicationController
before_action :require_logged_in
- before_action :set_client, only: [:show, :jobs, :logs, :stats, :users]
+ before_action :fetch_client, only: [:show, :jobs, :logs, :stats, :users, :restore, :run_restore]
before_action :fetch_logs, only: [:logs]
# GET /clients
# POST /clients
def index
@client_ids = Client.for_user(current_user.id).pluck(:ClientId)
@clients = Client.where(ClientId: @client_ids).includes(:jobs)
@hosts = current_user.hosts.not_baculized
fetch_jobs_info
get_charts
end
# GET /clients/1
def show
@schedules = @client.host.job_templates.map(&:schedule)
@filesets = @client.host.job_templates.map(&:fileset)
end
# GET /clients/1/jobs
def jobs
@jobs = @client.recent_jobs.page(params[:page])
end
# GET /clients/1/logs
def logs; end
# GET /clients/1/stats
# POST /clients/1/stats
def stats
get_charts
end
# GET /clients/1/users
def users
@users = @client.host.users
end
+ # GET /clients/1/restore
+ def restore
+ return if @client.is_backed_up?
+
+ flash[:error] = 'Can not issue a restore for this client'
+ redirect_to client_path(@client)
+ end
+
+ # POST /clients/1/run_restore
+ def run_restore
+ location = params[:restore_location].blank? ? '/tmp/bacula-restore' : params[:restore_location]
+ fileset = params[:fileset]
+ if location.nil? || fileset.nil? || !@client.host.restore(fileset, location)
+ flash[:error] = 'Something went wrong, try again later'
+ else
+ flash[:success] =
+ "Restore job issued successfully, files will be soon available in #{location}"
+ end
+
+ redirect_to client_path(@client)
+ end
+
private
- def set_client
+ def fetch_client
@client = Client.for_user(current_user.id).find(params[:id])
@client_ids = [@client.id]
end
def fetch_jobs_info
@stats = JobStats.new(@client_ids)
end
def get_charts
days_ago = params.fetch(:days_back, 7).to_i rescue 7
@job_status = ChartGenerator.job_statuses(@client_ids, days_ago)
@job_stats = ChartGenerator.job_stats(@client_ids, days_ago - 1)
end
end
diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb
index 0b6e1b6..c4677df 100644
--- a/app/controllers/hosts_controller.rb
+++ b/app/controllers/hosts_controller.rb
@@ -1,133 +1,113 @@
class HostsController < ApplicationController
before_action :require_logged_in
before_action :fetch_host, only: [:show, :edit, :update, :destroy, :submit_config,
- :revoke, :restore, :run_restore, :disable]
+ :revoke, :disable]
before_action :fetch_hosts_of_user, only: [:new, :edit, :create]
# GET /hosts/new
def new
@host = Host.new
@host.port = 9102
end
# POST /hosts
def create
@host = Host.new(fetch_params)
@host.verified = current_user.needs_host_list?
if user_can_add_this_host? && @host.save
flash[:success] = 'Host created successfully'
current_user.hosts << @host
redirect_to host_path @host
else
flash[:error] = 'Host was not created'
render :new
end
end
# GET /hosts/1
def show
@schedules = @host.job_templates.map(&:schedule)
@filesets = @host.job_templates.map(&:fileset)
end
# GET /hosts/1/edit
def edit; end
# PATCH /hosts/1
def update
updates = fetch_params.slice(:port, :password)
if updates.present? && @host.update_attributes(updates)
@host.recalculate
flash[:success] = 'Host updated successfully. You must update your file deamon accordingly.'
redirect_to host_path @host
else
render :edit
end
end
# DELETE /hosts/1
def destroy
if @host.destroy
flash[:success] = 'Host destroyed successfully'
else
flash[:error] = 'Host not destroyed'
end
redirect_to root_path
end
# POST /hosts/1/disable
def disable
if @host.disable_jobs_and_update
flash[:success] = 'Client disabled'
else
flash[:error] = 'Something went wrong, try again later'
end
redirect_to host_path(@host)
end
# POST /hosts/1/submit_config
def submit_config
if @host.dispatch_to_bacula
flash[:success] = 'Host configuration sent to Bacula successfully'
else
flash[:error] = 'Something went wrong, try again later'
end
redirect_to host_path(@host)
end
# DELETE /hosts/1/revoke
def revoke
if @host.remove_from_bacula
flash[:success] = 'Host configuration removed from Bacula successfully'
else
flash[:error] = 'Something went wrong, try again later'
end
redirect_to root_path
end
- # GET /hosts/1/restore
- def restore
- if !@host.restorable?
- flash[:error] = "Can not issue a restore for this client"
- redirect_to @host.client.present? ? client_path(@host.client) : root_path
- end
- end
-
- # POST /hosts/1/run_estore
- def run_restore
- location = params[:restore_location]
- if location.present? && @host.restore(location)
- flash[:success] = "Restore job issued successfully, files will be soon available in #{location}"
- else
- flash[:error] = 'Something went wrong, try again later'
- end
-
- redirect_to client_path(@host.client)
- end
-
private
def fetch_hosts_of_user
return if not current_user.needs_host_list?
@hosts_of_user = session[:vms] - current_user.hosts.pluck(:fqdn)
end
def fetch_host
@host = current_user.hosts.includes(job_templates: [:fileset, :schedule]).find(params[:id])
end
def fetch_params
params.require(:host).permit(:fqdn, :port, :password)
end
def user_can_add_this_host?
!current_user.needs_host_list? || @hosts_of_user.include?(@host.fqdn)
end
end
diff --git a/app/models/client.rb b/app/models/client.rb
index e8a9a36..d5ad86f 100644
--- a/app/models/client.rb
+++ b/app/models/client.rb
@@ -1,102 +1,127 @@
# 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
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
# 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 = jobs.backup_type.last.try(:end_time)
I18n.l(job_time, format: :long)
end
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 fileset_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/host.rb b/app/models/host.rb
index f913c96..62dae9a 100644
--- a/app/models/host.rb
+++ b/app/models/host.rb
@@ -1,291 +1,295 @@
# 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
STATUSES = {
pending: 0,
configured: 1,
dispatched: 2,
deployed: 3,
updated: 4,
redispatched: 5,
for_removal: 6,
inactive: 7
}
has_many :ownerships
has_many :users, through: :ownerships, inverse_of: :hosts
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
scope :not_baculized, -> {
joins("left join Client on Client.Name = hosts.name").where(Client: { Name: nil })
}
scope :unverified, -> { where(verified: false) }
before_validation :set_retention, :unset_baculized, :sanitize_name
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
end
# Constructs the final Bacula configuration for the host by appending configs for
#
# * Client
# * Jobs
# * Schedules
# * Filesets
#
# by calling their `to_bacula_config_array` methods.
#
# @return [Array] containing each element's configuration line by line
def baculize_config
templates = job_templates.includes(:fileset, :schedule)
result = [self] + templates.map {|x| [x, x.fileset, x.schedule] }.flatten.compact.uniq
result.map(&:to_bacula_config_array)
end
# Constructs the final Bacula configuration for the host by appending configs for
#
# * Client
# * Jobs
# * Schedules
# * Filesets
#
# by calling their `to_bacula_config_array` methods.
#
# It hides the password.
#
# @return [Array] containing each element's configuration line by line
def baculize_config_no_pass
baculize_config.join("\n").gsub(/Password = ".*"$/, 'Password = "*************"')
end
# Constructs an array where each element is a line for the Client's bacula config
#
# @return [Array]
def to_bacula_config_array
[
"Client {",
" Name = #{name}",
" Address = #{fqdn}",
" FDPort = #{port}",
" Catalog = #{client_settings[:catalog]}",
" Password = \"#{password}\"",
" File Retention = #{file_retention} #{file_retention_period_type}",
" Job Retention = #{job_retention} #{job_retention_period_type}",
" AutoPrune = #{auto_prune_human}",
"}"
]
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
def remove_from_bacula
return false unless 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
- def 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?
- bacula_handler.restore(location)
+ 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, 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
# 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 }
end
end
# Determines if a host can issue a restore job.
#
# @returns [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
save
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
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
# validation
def fqdn_format
regex = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)/
unless fqdn =~ regex
self.errors.add(:fqdn)
end
end
# Proxy object for handling bacula directives
def bacula_handler
BaculaHandler.new(self)
end
# Fetches and memoizes the general configuration settings for Clients
#
# @see ConfigurationSetting.current_client_settings
# @return [Hash] containing the settings
def client_settings
@client_settings ||= ConfigurationSetting.current_client_settings
end
end
diff --git a/app/models/job.rb b/app/models/job.rb
index 10cbaa4..8df014e 100644
--- a/app/models/job.rb
+++ b/app/models/job.rb
@@ -1,126 +1,127 @@
# 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
self.table_name = :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
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/views/clients/_client_details.html.erb b/app/views/clients/_client_details.html.erb
index da00b57..809241e 100644
--- a/app/views/clients/_client_details.html.erb
+++ b/app/views/clients/_client_details.html.erb
@@ -1,50 +1,50 @@
<div class="col-xs-4">
<div class="table-responsive">
<table class="table table-striped table-bordered table-condensed ">
<tr>
<td>Name</td>
<td><%= @client.name %></td>
</tr>
<tr>
<td>Uname</td>
<td><%= @client.uname %></td>
</tr>
<tr>
<td>Active Jobs</td>
<td><%= @client.running_jobs %></td>
</tr>
<tr>
<td>Last Backup</td>
<td>
<%= @client.last_job_date_formatted %>
</td>
</tr>
<tr>
<td>File Retention</td>
<td><%= @client.file_retention_days %> days</td>
</tr>
<tr>
<td>Job Retention</td>
<td><%= @client.job_retention_days %> days</td>
</tr>
<tr>
<td>Total Space Used</td>
<td><%= number_to_human_size @client.backup_jobs_size %></td>
</tr>
<tr>
<td>Files count</td>
<td><%= number_by_magnitude(@client.files_count) %></td>
</tr>
<tr>
<td>Auto Prune</td>
<td><%= @client.auto_prune_human %></td>
</tr>
</table>
</div>
<div>
<% if @client.is_backed_up? %>
- <%= link_to 'Restore Files', restore_host_path(@client.host),
+ <%= link_to 'Restore Files', restore_client_path(@client),
class: "btn btn-warning", role: "button" %>
<% end %>
</div>
</div>
diff --git a/app/views/clients/restore.html.erb b/app/views/clients/restore.html.erb
new file mode 100644
index 0000000..55f346d
--- /dev/null
+++ b/app/views/clients/restore.html.erb
@@ -0,0 +1,42 @@
+<div class="row">
+ <div class="col-xs-4">
+ <% if @client.is_backed_up? %>
+ <% restore_point = @client.last_job_date_formatted %>
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3>Restore files for "<%= @client.name %>"</h3>
+ </div>
+
+ <br />
+
+ <%= bootstrap_form_tag(url: run_restore_client_path(@client), layout: :horizontal,
+ label_col: 'col-xs-4', control_col: 'col-xs-7' ) do |f| %>
+ <div class="form-group">
+ <%= f.label(:restore_point, class: 'control-label col-xs-4') %>
+ <p class="form-control-static col-xs-8"><%= restore_point %></p>
+ </div>
+
+ <%= f.select(:fileset,
+ options_from_collection_for_select(@client.file_sets, :id, :file_set)) %>
+
+ <%= f.text_field :restore_location, placeholder: '/tmp' %>
+
+ <div class="row">
+ <div class="col-xs-3 col-xs-offset-9">
+ <%= f.submit 'Restore Files', class: 'btn btn-warning text-right',
+ data: { confirm: "This will restore all your files at the most recent backup (#{restore_point})" }
+ %>
+ </div>
+ </div>
+ <% end %>
+ </br>
+ </div>
+ <% else %>
+ <div class="alert alert-warning">
+ <p>Can not issue a restore for this host. It has no successful backups</p>
+ </div>
+ <% end %>
+
+ <%= link_to 'Cancel', client_path(@client), class: 'btn btn-danger', role: 'button' %>
+ </div>
+</div>
diff --git a/app/views/hosts/restore.html.erb b/app/views/hosts/restore.html.erb
deleted file mode 100644
index ae62b45..0000000
--- a/app/views/hosts/restore.html.erb
+++ /dev/null
@@ -1,26 +0,0 @@
-<h1>Restore files for "<%= @host.name %>"</h1>
-
-<div class="row">
- <div class="col-xs-6">
- <% if @host.restorable? %>
- <% restore_point = @host.client.last_job_date_formatted %>
- <div class="container graybox">
- <%= bootstrap_form_tag(url: run_restore_host_path(@host), layout: :horizontal,
- label_col: 'col-xs-4', control_col: 'col-xs-8' ) do |f| %>
- <%= f.text_field(:restore_point, value: restore_point, disabled: true) %>
- <%= f.text_field(:restore_location) %>
- </br>
- <%= f.submit 'Restore Files', class: 'btn btn-warning right',
- data: { confirm: "This will restore all your files at the most recent backup (#{restore_point})" }
- %>
- <% end %>
- </div>
- <% else %>
- <div class="alert alert-warning">
- <p>Can not issue a restore for this host. It is either not deployed or has no successful backups</p>
- </div>
- <% end %>
-
- <%= link_to 'Cancel', client_path(@host.client), class: 'btn btn-danger', role: 'button' %>
- </div>
-</div>
diff --git a/config/routes.rb b/config/routes.rb
index 4ef5d91..671ef9d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,78 +1,78 @@
Rails.application.routes.draw do
root 'application#index'
post 'login' => 'application#login'
match 'vima', to: 'application#vima', :via => [:get, :post]
get 'logout' => 'application#logout'
resources :clients, only: [:index, :show] do
member do
get :jobs
get :logs
get :stats
post :stats
get :users
+ get :restore
+ post :run_restore
end
collection do
post :index
end
end
resources :hosts, only: [:new, :create, :show, :edit, :update, :destroy] do
member do
post :submit_config
- get :restore
- post :run_restore
post :disable
delete :revoke
end
resources :jobs, only: [:new, :create, :show, :edit, :update, :destroy] do
member do
patch :toggle_enable
post :backup_now
end
end
resources :filesets, only: [:show, :new, :create, :destroy]
resources :schedules, only: [:show, :new, :edit, :create, :update, :destroy]
end
namespace :admin do
match '/', to: 'base#index', via: [:get, :post]
resources :settings, only: [:index, :new, :create, :edit, :update] do
member do
delete :reset
end
end
resources :clients, only: [:index, :show] do
member do
get :jobs
get :logs
get :stats
post :stats
get :configuration
post :disable
end
end
resources :hosts, only: [:show] do
collection do
get :unverified
end
member do
post :verify
end
end
resources :users, only: [:index] do
member do
patch :ban
patch :unban
end
end
end
end
diff --git a/lib/bacula_handler.rb b/lib/bacula_handler.rb
index 29e8bd9..cb642e6 100644
--- a/lib/bacula_handler.rb
+++ b/lib/bacula_handler.rb
@@ -1,188 +1,190 @@
class BaculaHandler
require 'net/scp'
attr_accessor :host, :templates, :client, :jobs, :schedules, :filesets
# Initializes a BaculaHandler instance.
#
# Sets `host` and `templates` attributes.
# Sets the temporal files that contain the client's configuration
#
# @param host[Host] A the host instance the the bacula handler will act upon
def initialize(host)
@host = host
@templates = host.job_templates.includes(:fileset, :schedule)
@client = get_client_file
@jobs = get_jobs_file
@schedules = get_schedules_file
@filesets = get_filesets_file
end
# Deploys the host's config to the bacula director by
#
# * uploading the configuration
# * reloadind the bacula director
#
# Updates the host's status accordingly
#
# @return [Boolean] false if something went wrong
def deploy_config
return false unless send_config
if reload_bacula
if host.job_templates.enabled.any?
host.set_deployed
else
host.set_inactive
end
else
host.dispatch || host.redispatch
end
end
# Removes the host's configuration from the bacula director by
#
# * removing the host's configuration files
# * reloading the bacula director
#
# Updates the host's status accordingly
#
# @return [Boolean] false if something went wrong
def undeploy_config
return false unless remove_config
host.disable if reload_bacula
end
# Schedules an immediate backup to the bacula director for the given host and job
#
# @param job_name[String] the job's name
def backup_now(job_name)
job = host.job_templates.enabled.find_by(name: job_name)
return false unless job
command = "echo \"run job=\\\"#{job.name_for_config}\\\" yes\" | #{bconsole}"
log(command)
exec_with_timeout(command, 2)
end
# Schedules an immediate restore to the bacula director for the given host.
#
+ # @param job_ids[Array] contains the jobs that compose the restore for this fileset
+ # @param file_set_name[String] the fileset that is going to be restored
# @param location[String] the desired restore location
- def restore(location="/tmp/bacula-restore")
- command = "echo \"restore client=\\\"#{host.name}\\\" where=\\\"#{location}\\\" select current all done yes\" | #{bconsole}"
+ def restore(job_ids, file_set_name, location="/tmp/bacula-restore")
+ command = "echo \"restore client=\\\"#{host.name}\\\" jobid=#{job_ids.join(',')} where=\\\"#{location}\\\" fileset=\\\"#{file_set_name}\\\" select all done yes\" | #{bconsole}"
log(command)
exec_with_timeout(command, 2)
end
private
def get_client_file
file = a_tmpfile
file.write host.to_bacula_config_array.join("\n")
file.close
file
end
def get_jobs_file
file = a_tmpfile
file.write templates.map(&:to_bacula_config_array).join("\n")
file.close
file
end
def get_schedules_file
file = a_tmpfile
file.write templates.map(&:schedule).uniq.map(&:to_bacula_config_array).join("\n")
file.close
file
end
def get_filesets_file
file = a_tmpfile
file.write templates.map(&:fileset).uniq.map(&:to_bacula_config_array).join("\n")
file.close
file
end
def send_config
begin
Net::SCP.upload!(
ssh_settings[:host],
ssh_settings[:username],
client.path,
ssh_settings[:path] + 'clients/' + host.name + '.conf',
ssh: { keys: [ssh_settings[:key_file]] }
)
Net::SCP.upload!(
ssh_settings[:host],
ssh_settings[:username],
jobs.path,
ssh_settings[:path] + 'jobs/' + host.name + '.conf',
ssh: { keys: [ssh_settings[:key_file]] }
)
Net::SCP.upload!(
ssh_settings[:host],
ssh_settings[:username],
schedules.path,
ssh_settings[:path] + 'schedules/' + host.name + '.conf',
ssh: { keys: [ssh_settings[:key_file]] }
)
Net::SCP.upload!(
ssh_settings[:host],
ssh_settings[:username],
filesets.path,
ssh_settings[:path] + 'filesets/' + host.name + '.conf',
ssh: { keys: [ssh_settings[:key_file]] }
)
rescue
return false
end
true
end
def remove_config
begin
Net::SSH.start(ssh_settings[:host], ssh_settings[:username],
keys: ssh_settings[:key_file]) do |ssh|
ssh.exec!("rm #{ssh_settings[:path]}*/#{host.name}.conf")
end
rescue
return false
end
true
end
def reload_bacula
command = "echo \"reload quit\" | #{bconsole}"
exec_with_timeout(command, 2)
end
def exec_with_timeout(command, sec)
begin
Timeout::timeout(sec) do
`#{command}`
end
rescue
return false
end
true
end
def bconsole
"bconsole -c #{Rails.root}/config/bconsole.conf"
end
def ssh_settings
@ssh_settings ||= YAML::load(File.open("#{Rails.root}/config/ssh.yml"))[Rails.env].
symbolize_keys
end
def a_tmpfile
file = Tempfile.new(host.name)
file.chmod(0666)
file
end
def log(msg)
Rails.logger.warn("[BaculaHandler]: #{msg}")
end
end
diff --git a/spec/routing/client_routing_spec.rb b/spec/routing/client_routing_spec.rb
index 9a8a6ff..6738744 100644
--- a/spec/routing/client_routing_spec.rb
+++ b/spec/routing/client_routing_spec.rb
@@ -1,36 +1,46 @@
require 'spec_helper'
describe ClientsController do
it 'routes /clients' do
expect(get('/clients')).to route_to(controller: 'clients', action: 'index')
end
it 'routes /clients' do
expect(post('/clients')).to route_to(controller: 'clients', action: 'index')
end
it 'routes GET /clients/1' do
expect(get('/clients/1')).to route_to(controller: 'clients', action: 'show', id: '1')
end
it 'routes GET /clients/1/stats' do
expect(get('/clients/1/stats')).
to route_to(controller: 'clients', action: 'stats', id: '1')
end
it 'routes POST /clients/1/stats' do
expect(post('/clients/1/stats')).
to route_to(controller: 'clients', action: 'stats', id: '1')
end
it 'routes GET /clients/1/logs' do
expect(get('/clients/1/logs')).
to route_to(controller: 'clients', action: 'logs', id: '1')
end
it 'routes GET /clients/1/jobs' do
expect(get('/clients/1/jobs')).
to route_to(controller: 'clients', action: 'jobs', id: '1')
end
+
+ it 'routes POST /clients/1/restore' do
+ expect(post('/clients/1/run_restore')).
+ to route_to(controller: 'clients', action: 'run_restore', id: '1')
+ end
+
+ it 'routes GET /clients/1/restore' do
+ expect(get('/clients/1/restore')).
+ to route_to(controller: 'clients', action: 'restore', id: '1')
+ end
end
diff --git a/spec/routing/host_routing_spec.rb b/spec/routing/host_routing_spec.rb
index 8507964..c2eecb9 100644
--- a/spec/routing/host_routing_spec.rb
+++ b/spec/routing/host_routing_spec.rb
@@ -1,52 +1,42 @@
require 'spec_helper'
describe HostsController do
it 'routes GET /hosts/new' do
expect(get('/hosts/new')).to route_to(controller: 'hosts', action: 'new')
end
it 'routes POST /hosts' do
expect(post('/hosts')).to route_to(controller: 'hosts', action: 'create')
end
it 'routes GET /hosts/1' do
expect(get('/hosts/1')).to route_to(controller: 'hosts', action: 'show', id: '1')
end
it 'routes GET /hosts/1/edit' do
expect(get('/hosts/1/edit')).to route_to(controller: 'hosts', action: 'edit', id: '1')
end
it 'routes PUT /hosts/1' do
expect(put('/hosts/1')).to route_to(controller: 'hosts', action: 'update', id: '1')
end
it 'routes DELETE /hosts/1' do
expect(delete('/hosts/1')).to route_to(controller: 'hosts', action: 'destroy', id: '1')
end
it 'routes POST /hosts/1/submit_config' do
expect(post('/hosts/1/submit_config')).
to route_to(controller: 'hosts', action: 'submit_config', id: '1')
end
it 'routes POST /hosts/1/disable' do
expect(post('/hosts/1/disable')).
to route_to(controller: 'hosts', action: 'disable', id: '1')
end
it 'routes DELETE /hosts/1/revoke' do
expect(delete('/hosts/1/revoke')).
to route_to(controller: 'hosts', action: 'revoke', id: '1')
end
-
- it 'routes POST /hosts/1/restore' do
- expect(post('/hosts/1/run_restore')).
- to route_to(controller: 'hosts', action: 'run_restore', id: '1')
- end
-
- it 'routes GET /hosts/1/restore' do
- expect(get('/hosts/1/restore')).
- to route_to(controller: 'hosts', action: 'restore', id: '1')
- end
end
Event Timeline
Log In to Comment