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}(? { 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 @@
Name <%= @client.name %>
Uname <%= @client.uname %>
Active Jobs <%= @client.running_jobs %>
Last Backup <%= @client.last_job_date_formatted %>
File Retention <%= @client.file_retention_days %> days
Job Retention <%= @client.job_retention_days %> days
Total Space Used <%= number_to_human_size @client.backup_jobs_size %>
Files count <%= number_by_magnitude(@client.files_count) %>
Auto Prune <%= @client.auto_prune_human %>
<% 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 %>
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 @@ +
+
+ <% if @client.is_backed_up? %> + <% restore_point = @client.last_job_date_formatted %> +
+
+

Restore files for "<%= @client.name %>"

+
+ +
+ + <%= bootstrap_form_tag(url: run_restore_client_path(@client), layout: :horizontal, + label_col: 'col-xs-4', control_col: 'col-xs-7' ) do |f| %> +
+ <%= f.label(:restore_point, class: 'control-label col-xs-4') %> +

<%= restore_point %>

+
+ + <%= f.select(:fileset, + options_from_collection_for_select(@client.file_sets, :id, :file_set)) %> + + <%= f.text_field :restore_location, placeholder: '/tmp' %> + +
+
+ <%= 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})" } + %> +
+
+ <% end %> +
+
+ <% else %> +
+

Can not issue a restore for this host. It has no successful backups

+
+ <% end %> + + <%= link_to 'Cancel', client_path(@client), class: 'btn btn-danger', role: 'button' %> +
+
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 @@ -

Restore files for "<%= @host.name %>"

- -
-
- <% if @host.restorable? %> - <% restore_point = @host.client.last_job_date_formatted %> -
- <%= 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) %> -
- <%= 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 %> -
- <% else %> -
-

Can not issue a restore for this host. It is either not deployed or has no successful backups

-
- <% end %> - - <%= link_to 'Cancel', client_path(@host.client), class: 'btn btn-danger', role: 'button' %> -
-
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